Por lo general, un microcontrolador utiliza muchos protocolos diferentes para comunicarse con varios sensores y periféricos, y de todos estos, los protocolos de comunicación en serie son quizá los más populares.
Uno de los protocolos de comunicación serial más utilizados, además del muy conocido I2C, es el Bus de Interfaz Periférica Serial (SPI – Serial Peripheral Interface), que se utiliza para la comunicación entre múltiples dispositivos en distancias cortas y a alta velocidad. En este artículo veremos cómo usar y configurar el bus SPI para comunicaciones con la placa Arduino.
¿Qué es el bus SPI?
El bus SPI, o Serial to Peripheral Interface, es una interfaz de comunicación en serie de cuatro hilos, de muy bajo consumo, diseñada para que el microcontrolador y los periféricos se comuniquen entre sí.
El bus SPI es un bus de comunicaciones full-duplex, que permite que la comunicación fluya hacia y desde el dispositivo maestro en forma simultánea, a velocidades de hasta 10Mbps. La interfaz de bus SPI fue desarrollada por Motorola (actualmente Lenovo) en el año 1970.
La interfaz del bus SPI se utiliza para la comunicación entre múltiples dispositivos en distancias cortas y a alta velocidad.
La operación de alta velocidad del bus SPI generalmente limita su uso para comunicarse con componentes que tengan poca separación respecto al microcontrolador debido al aumento en la capacitancia, que en comunicaciones a altas velocidades impacta negativamente la calidad de la conexión, introduciendo errores en las transferencias de datos.
En el protocolo de bus SPI hay normalmente un solo dispositivo maestro, que inicia las comunicaciones y suministra la señal de reloj que controla la velocidad de transferencia de datos.
En el bus SPI puede haber uno o más esclavos. Para más de un esclavo, cada uno tiene su propia señal de selección de esclavo.
¿Cómo funciona el bus SPI?
Para entender mejor cómo funciona el bus SPI, inicialmente veremos de qué manera se diferencia este de otros métodos de comunicaciones seriales, por ejemplo, el puerto serie.
El bus SPI comparado con el puerto serie
El puerto serie basado en UART se denomina asíncrono porque no hay control sobre cuándo se envían los datos ni ninguna garantía de que ambos extremos estén a la misma velocidad de operación.
La base del problema es la sincronización del reloj, y en dos sistemas completamente separados, aunque nominalmente tengan ambos la misma velocidad de reloj, lo más probable es sus frecuencias sean ligeramente diferentes.
Esta es la razón por la cual los sistemas de comunicaciones asíncronos necesitan agregar señales adicionales, en este caso, bits de inicio y parada a cada byte que transmiten. Esto ayuda al receptor a re-sincronizar continuamente su reloj interno por cada byte que recibe.
Para que todo esto funcione, ambos lados también deben acordar previamente la velocidad de transmisión.
Funcionamiento del bus SPI
Por otro lado, el bus SPI funciona de una manera ligeramente diferente. Es un bus de datos síncrono, lo que significa que utiliza líneas separadas para datos y un reloj que mantiene ambos lados en perfecta sincronización.
La señal de reloj marca exactamente cuándo se debe muestrear los bits en la línea de datos. Esta marca puede ser el flanco ascendente (de bajo a alto) o descendente (de alto a bajo) de la señal del reloj.
Cuando el receptor detecte ese flanco, inmediatamente observará la línea de datos para leer el siguiente bit. Debido a que el reloj se envía junto con los datos, especificar la velocidad no es importante, aunque los dispositivos tendrán una velocidad máxima a la que pueden operar.
Una razón por la que SPI es tan popular es que el hardware receptor puede ser un simple registro de cambios (shift register). Esta es una pieza de hardware mucho más simple (y más barata) que un UART.
Lineas y señales de un bus SPI
Un sistema SPI normal tendrá cuatro líneas de señal:
- Master Out, Slave In (MOSI): es la información que va del maestro al esclavo
- Master In, Slave Out (MISO): es la información que va del esclavo al maestro
- Serial Clock (SCK): Los pulsos de reloj que sincronizan la transmisión de datos generados por el maestro.
- Slave Select (SS): esto le dice a un esclavo en particular que se active.
Cuando se conectan varios esclavos a la señal MISO, se espera mantener en alta impedancia (Tri-State) la línea MISO hasta que sean seleccionados por la línea SS.
Normalmente, la línea SS confirma la selección en modo activo bajo (LOW). Cuando está en modo alto (HIGH) se ignora al maestro. Una vez que se selecciona un esclavo en particular, debe configurar la línea MISO como una salida para que pueda enviar datos al maestro.
Por cada flanco de subida o de bajada de la línea SCK, previa selección del modo de fase y polaridad, el maestro envía y recibe a la vez un bit del esclavo seleccionado.
La secuencia de comunicaciones es la siguiente:
- SS pasa a estado bajo (LOW) y activa el esclavo.
- SCK alterna su estado para indicar cuándo se deben muestrear las líneas de datos
- Los datos son muestreados por el maestro y el esclavo en la transición de flanco anterior de la línea SCK (usando la fase y polaridad de reloj predeterminada)
- Tanto el maestro como el esclavo se preparan para el siguiente bit en el borde posterior de SCK (usando la fase y polaridad de de reloj predeterminada), cambiando MISO/MOSI si es necesario
- Una vez que finaliza la transmisión (posiblemente después de que se hayan enviado varios bytes), la línea SS pasa a nivel alto (HIGH) y desactiva el esclavo.
Hay que tener en cuenta que:
- El bit más significativo se envía primero (por defecto, pero se puede cambiar)
- Los datos se envían y reciben en el mismo instante (dúplex completo)
Debido a que los datos se envían y reciben en el mismo pulso de reloj, no es posible que el esclavo responda al maestro inmediatamente. Los protocolos SPI generalmente esperan que el maestro solicite datos en una transmisión y obtenga una respuesta en una transmisión posterior.
El proceso de comunicaciones, esto es, la secuencia de bits enviados y recibidos, es completamente arbitraria y no sigue ningún patrón definido. La cantidad de datos que se va a intercambiar debe estar definida por programa.
Polaridad y fase de reloj en el bus SPI
Hay cuatro formas en que puede muestrear el reloj SPI. El protocolo SPI permite variaciones en la polaridad de los pulsos de reloj. CPOL es la polaridad del reloj y CPHA es la fase del reloj.
- Modo 0 (el valor predeterminado): el reloj es normalmente bajo (CPOL = 0), y los datos se muestrean en la transición de bajo a alto (borde delantero) (CPHA = 0)
- Modo 1: el reloj es normalmente bajo (CPOL = 0), y los datos se muestrean en la transición de alto a bajo (borde de salida) (CPHA = 1)
- Modo 2: el reloj es normalmente alto (CPOL = 1), y los datos se muestrean en la transición de alto a bajo (borde delantero) (CPHA = 0)
- Modo 3: el reloj suele ser alto (CPOL = 1), y los datos se muestrean en la transición de bajo a alto (borde de salida) (CPHA = 1)
Ventajas y desventajas del bus SPI
Algunas de las ventajas más importantes del bus SPI.
- Tiene total flexibilidad para los bits transferidos, es decir, no se limita a una palabra de 8 bits.
- Tiene una interfaz de hardware muy simple.
- No se limita a ninguna velocidad de reloj máxima, lo que permite una velocidad potencialmente alta.
- Es más rápido que el serial asíncrono (UART).
- Soporta múltiples esclavos.
- Soporta comunicaciones full duplex.
- Tiene una señal de bus única por dispositivo llamada SS y todas las demás señales se comparten.
- No hay modos de fallo de arbitraje.
- No necesita de transceptores (adaptadores).
Por otro lado, entre las desventajas más importantes del bus SPI.
- Requiere más pines de conexión con respecto al bus I2C.
- No admite nodos en forma dinámica (en caliente).
- Solo soporta un dispositivo maestro.
- No hay protocolo de comprobación de errores.
- El bus SPI requiere generalmente de líneas SS separadas para cada esclavo, lo que puede ser problemático si se necesitan numerosos esclavos.
Como usar el bus SPI en la placa Arduino
Los microcontroladores Atmega incluidos en las placas Arduino disponen de soporte del bus SPI por hardware, reservando para ello ciertos pines. También es posible usar el bus SPI emulado por software, mediante el uso de librerías. Esta última alternativa solo debe ser reservada para el caso de no poder disponer de los pines nativos SPI por estar estos ocupados en algún otro uso, y solo si se planea usar en dispositivos periféricos que no requieran de altas velocidades.
En las placas Arduino UNO R3 o Arduino Duemilanove, los pines utilizados son:
- SS: digital 10. Puede usar otros pines digitales, pero 10 es generalmente el valor predeterminado. Si se piensa usar el bus SPI en modo esclavo, este pin es de uso obligatorio.
- MOSI: digital 11
- MISO: digital 12
- SCK: digital 13
Por otro lado, la placa Arduino Mega usa los siguientes pines:
- SS: pin 53
- MOSI: pin 51
- MISO: pin 50
- SCK: pin 52
En la placa Arduino Leonardo, los pines SPI están ubicados en los pines del encabezado de programación ICSP.
Configuración del bus SPI en Arduino
Usar el bus SPI con la placa Arduino es bastante sencillo. Solo es necesario incluir en el encabezado del programa la librería SPI.h. Esta librería contiene toda la funcionalidad para configurar y controlar el hardware del microcontrolador.
Asimismo, el entorno de programación de Arduino define las constantes SCK, MOSI, MISO, y SS para los pines de SPI. Usar estos alias en nuestro código hace que sea más fácil de intercambiar programas entre modelos placas.
Inicialización del bus SPI
1 2 3 |
SPI.begin(); // Inicializa el bus SPI SPI.transfer(dato); // Envía un byte SPI.attachInterrupt(); // Activa las interrupciones para recibir datos |
Configuración el orden de transmisión de bits del bus SPI
1 2 |
SPI.setBitOrder(LSBFIRST); // Bit menos significativo primero SPI.setBitOrder(MSBFIRST); // Bit mas signigicativo primero |
Configurar la polaridad del reloj del bus SPI
1 2 3 4 |
SPI.setDataMode(SPI_MODE0); // Reloj normalmente bajo, muestreo en flanco subida SPI.setDataMode(SPI_MODE1); // Reloj normalmente bajo, muestreo en flanco bajada SPI.setDataMode(SPI_MODE2); // Reloj normalmente alto, muestreo en flanco subida SPI.setDataMode(SPI_MODE3); // Reloj normalmente alto, muestreo en flanco bajada |
Establecer la velocidad del bus SPI
Para versiones de anteriores a Arduino IDE 1.60:
1 2 3 4 5 6 7 |
SPI.setClockDivider(SPI_CLOCK_DIV2); //8 MHz (Reloj de 16 Mhz) SPI.setClockDivider(SPI_CLOCK_DIV4); //4 MHz SPI.setClockDivider(SPI_CLOCK_DIV8); //2 MHz SPI.setClockDivider(SPI_CLOCK_DIV16); //1 MHz SPI.setClockDivider(SPI_CLOCK_DIV32); //500 KHz SPI.setClockDivider(SPI_CLOCK_DIV64); //250 KHz SPI.setClockDivider(SPI_CLOCK_DIV128); //125 KHz |
Para versions Arduino IDE 1.60 en adelante:
1 |
SPI.beginTransaction(SPISettings (2000000, MSBFIRST, SPI_MODE0)); // Reloj de 2 MHz, MSB first, mode 0 |
Ejemplos de uso del bus SPI en Arduino
A pesar de lo practico que poder ser usar el bus SPI con cualquier dispositivo que soporte dicho bus, es necesario disponer de la respectiva librería de control.
Esto se debe a que el bus SPI solo crea el canal de comunicaciones, pero no hace nada respecto a cómo darle formato a los datos y mucho menos como interpretarlos.
Por esta razón, las fases de formato e interpretación de datos, quedan a cargo del código de programa. No será lo mismo comunicarse con un sensor o con una pantalla LCD. Aunque usen y compartan el mismo bus SPI, los datos que intercambian con el microcontrolador no serán los mismos.
Se explicará como ejemplos de programa como comunicar dos placas Arduino, una como maestro, y la otra como esclavo.
La razón de ello es para explicar con claridad los aspectos relativos a conexiones y configuración de la librería SPI.h, y posteriormente hacer un código más sencillo de intercambio de datos que funcione sobre esta configuración base.
La base de esta explicación será también válida para usar el bus SPI con otros dispositivos, pero en esos casos, se haría necesario profundizar en la forma como funciona e intercambia información este con el microcontrolador. Para esos casos es mejor dedicar una publicación aparte.
Conectar dos placas Arduino usando el bus SPI
Este es quizá el paso más fácil de todos. Para interconectar ambas placas Arduino solo es necesito usar 6 hilos de conexión: SS, MOSI, MISO, SCK, 5V y GND. De esta manera, una de las placas Arduino proporciona energía de alimentación a la otra.
Conecte las dos placas Arduino con los siguientes pines conectados entre sí:
- Pin 10 (SS)
- Pin 11 (MOSI)
- Pin 12 (MISO)
- Pin 13 (SCK)
- Pin 5v (si es necesario, también es posible darle alimentación independiente)
- GND (para retorno de señal)
En cualquier caso, MOSI en un extremo está conectado a MOSI en el otro, no se deben intercambiar (es decir, no conectar MOSI <-> MISO). El programa configura MOSI de un extremo en modo maestro, como salida, y el otro extremo (extremo esclavo) como entrada.
Ejemplo de programa para el bus SPI en modo maestro
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
/* * Arduino bus SPI en modo maestro * * https://www.proyectoarduino.com * */ #include <SPI.h> /* * La libreria SPI pre-define las siguientes constantes * para poder usar as lineas del bus SPI para las placas * Arduino UNO (AU) o Arduino MEGA (AM): * * SS - Slave Select en en pin 10 (AU) o pin 53 (AM) * MOSI - Master Out, Slave In en el pin 11 (AU) o pin 51 (AM) * MISO - Master In, Slave Out en el pin 12 (AU) o pin 50 (AM) * SCLK - Serial Clock en el pin 13 (AU) o pin 52 (AM) * */ void setup(void) { // SS en nivel alto, garantiza que el esclavo esta inactivo digitalWrite(SS, HIGH); // Inicializacion en modo maestro, coloca SCK, MOSI, SS en modo salida // SCK y MOSI a nivel bajo (LOW), y SS en nivel alto (HIGH) SPI.begin(); // Ajusta la velocidad de comunicaciones a 2MHz SPI.setClockDivider(SPI_CLOCK_DIV8); } void loop(void) { char enviarByte; // Habilita el dispositivo esclavo digitalWrite(SS, LOW); // Envia el mensaje de prueba for (const char *msg = "Hola mundo!\n" ; enviarByte = *msg; msg++) SPI.transfer(enviarByte); // Deshabilita el dispositivo esclavo digitalWrite(SS, HIGH); // Espera aprox. un segundo y vuelta a empezar delay(1000); } |
Ejemplo de programa para el bus SPI en modo esclavo
En este ejemplo, el esclavo está completamente controlado por interrupciones, por lo que la placa Arduino receptora puede hacer otras cosas.
Los datos SPI entrantes se recopilan en un búfer y se establece un indicador (flag) cuando llega un byte significativo (en este caso, una nueva línea). Esto le dice al esclavo que comience a procesar los datos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
/* * Arduino bus SPI en modo esclavo * * https://www.proyectoarduino.com * */ #include <SPI.h> /* * La libreria SPI pre-define las siguientes constantes * para poder usar as lineas del bus SPI para las placas * Arduino UNO (AU) o Arduino MEGA (AM): * * SS - Slave Select en en pin 10 (AU) o pin 53 (AM) * MOSI - Master Out, Slave In en el pin 11 (AU) o pin 51 (AM) * MISO - Master In, Slave Out en el pin 12 (AU) o pin 50 (AM) * SCLK - Serial Clock en el pin 13 (AU) o pin 52 (AM) * */ char datosRecibidos [100]; volatile byte indice; volatile bool recepcionTerminada; void setup (void) { // Inicia puerto serial para visualizar // los datos de llegada Serial.begin (115200); // Habilita el bus SPI en modo esclavo // usando el registro SPCR del microcontrolador SPCR |= bit (SPE); // Configura el pin MISO como salida (slave out) pinMode (MISO, OUTPUT); // Control de datos recibidos en el buffer // Buffer vacio indice = 0; // Indicador de final de recepcion recepcionTerminada = false; // Habilita las interrupciones del bus SPI SPI.attachInterrupt(); } // Rutina de interrupcion del bus SPI ISR (SPI_STC_vect) { // Lee el rado recibido desde el registro SPDR del bus SPI byte datoRecibido = SPDR; // Si hay espacio, inserta el dato recibido en el buffer if (indice < sizeof datosRecibidos) { datosRecibidos[indice++] = datoRecibido; // Si se ha recibido final de linea // se procesa el buffer para su salida por el puerto serie if (datoRecibido == '\n') recepcionTerminada = true; } } void loop (void) { if (recepcionTerminada) { datosRecibidos[indice] = 0; Serial.println (datosRecibidos); indice = 0; recepcionTerminada = false; } } |
Comunicación bidireccional del bus SPI
El siguiente ejemplo muestra cómo enviar datos a un esclavo, hacer que haga algo con él y posteriormente devolver una respuesta.
El código del SPI maestro es similar al ejemplo anterior. Sin embargo, un punto importante es que necesitamos agregar un ligero retraso (unos 20 microsegundos). De lo contrario, el esclavo no tiene la oportunidad de reaccionar a los datos entrantes y hacer algo con ellos.
El ejemplo muestra el envío de una orden o comando. En este caso «s» (sumar algo) o «r» (restar algo). Esto es para mostrar que el esclavo en realidad está haciendo algo con los datos.
Después de confirmar la selección del esclavo (SS) para iniciar la transmisión, el maestro envía el comando, seguido de cualquier número de bytes, y luego levanta SS para terminar la transmisión.
Un punto muy importante es que el esclavo no puede responder a un byte entrante en el mismo momento. La respuesta tiene que estar en el siguiente byte. Esto se debe a que los bits que se envían y los bits que se reciben se envían simultáneamente.
Ejemplo de programa bidireccional para el bus SPI en modo maestro
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
/* * Arduino bus SPI en modo maestro bidireccional * * https://www.proyectoarduino.com * */ #include <SPI.h> /* * La libreria SPI pre-define las siguientes constantes * para poder usar as lineas del bus SPI para las placas * Arduino UNO (AU) o Arduino MEGA (AM): * * SS - Slave Select en en pin 10 (AU) o pin 53 (AM) * MOSI - Master Out, Slave In en el pin 11 (AU) o pin 51 (AM) * MISO - Master In, Slave Out en el pin 12 (AU) o pin 50 (AM) * SCLK - Serial Clock en el pin 13 (AU) o pin 52 (AM) * */ void setup(void) { // Inicializa el puerto serie par visualizar los datos Serial.begin(115200); Serial.println(); // Estabecer el esclavo como inactivo digitalWrite(SS, HIGH); SPI.begin(); // Ajusta la velocidad de comunicaciones a 2MHz SPI.setClockDivider(SPI_CLOCK_DIV8); } byte transferAndWait(const byte dato) { byte recibido = SPI.transfer(dato); delayMicroseconds(20); return recibido; } void loop(void) { byte a, b, c, d; // Activa el esclavo digitalWrite(SS, LOW); // Transmite el comando "sumar" transferAndWait ('s'); // Procede ha hacer sumas susesivas transferAndWait (10); a = transferAndWait (17); b = transferAndWait (33); c = transferAndWait (42); d = transferAndWait (0); // Desactiva el esclavo digitalWrite(SS, HIGH); // Presentacion de resultados de las sumas en la consola Serial.println("Resultados de la suma:"); Serial.println(a, DEC); Serial.println(b, DEC); Serial.println(c, DEC); Serial.println(d, DEC); // Activa nuevamente e esclavo digitalWrite(SS, LOW); // Transmite el comando "restar" transferAndWait ('r'); // Procede a hacer restas susecivas transferAndWait (10); a = transferAndWait (17); b = transferAndWait (33); c = transferAndWait (42); d = transferAndWait (0); // Desactiva el esclavo digitalWrite(SS, HIGH); // Presentacion de resultados de las restas en la consola Serial.println("Resultados de las restas:"); Serial.println(a, DEC); Serial.println(b, DEC); Serial.println(c, DEC); Serial.println(d, DEC); // Espera un segundo y vuelta a empezar delay(1000); } |
Ejemplo de programa bidireccional para el bus SPI en modo esclavo
El código para el esclavo básicamente hace casi todo en la rutina de interrupción (esta se llama cuando llegan datos entrantes por el bus SPI). Toma el byte entrante, y suma o resta según la orden del byte de comando.
Hay que tener en cuenta que la respuesta a la actividad anterior será recuperada en el siguiente ciclo. Esta es la razón por la que el maestro envía una transferencia final ficticia para obtener la respuesta final.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
volatile byte comando = 0; void setup(void) { // Configurar MISO como salida esclavo (slave out) pinMode(MISO, OUTPUT); // Activa el bus SPI en modo esclavo SPCR |= _BV(SPE); // Activa las interrupciones en el bus SPI SPCR |= _BV(SPIE); } // Rutina de servicio de interrupciones del bus SPI ISR (SPI_STC_vect) { byte c = SPDR; switch (comando) { // No hay comandos? Entonces este es el comando case 0: comando = c; SPDR = 0; break; // Suma el byte recibido y retorna el resultado case 's': SPDR = c + 15; // add 15 break; // Resta el byte recibido y retorna el resultado case 'r': SPDR = c - 8; // subtract 8 break; } } void loop(void) { // Si el bus SPI no esta activo, borra el actual comando if (digitalRead (SS) == HIGH) comando = 0; } |