¿Cómo se usa SPI en un Arduino?

Respuestas:

81

Introducción a SPI

La interfaz del bus de interfaz periférica en serie (SPI) se utiliza para la comunicación entre múltiples dispositivos en distancias cortas y a alta velocidad.

Por lo general, hay un único dispositivo "maestro", que inicia las comunicaciones y proporciona el reloj que controla la velocidad de transferencia de datos. 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", que se describe más adelante.


Señales SPI

En un sistema SPI completo, tendrá cuatro líneas de señal:

  • Master Out, Slave In ( MOSI ) - que son los datos que van del maestro al esclavo
  • Master In, Slave Out ( MISO ) - que son los datos que van del esclavo al maestro
  • Reloj de serie ( SCK ): cuando esto alterna tanto la muestra maestra como la esclava el siguiente bit
  • Slave Select ( SS ): esto le dice a un esclavo en particular que se vuelva "activo"

Cuando se conectan múltiples esclavos a la señal MISO, se espera que tripliquen (mantengan a alta impedancia) esa línea MISO hasta que sean seleccionados por Slave Select. Normalmente, Slave Select (SS) baja para afirmarlo. Es decir, está activo bajo. Una vez que se selecciona un esclavo en particular, debe configurar la línea MISO como salida para que pueda enviar datos al maestro.

Esta imagen muestra la forma en que se intercambian los datos cuando se envía un byte:

Protocolo SPI que muestra 4 señales

Tenga en cuenta que tres señales son salidas del maestro (MOSI, SCK, SS) y una es una entrada (MISO).


Sincronización

La secuencia de eventos es:

  • SS baja para afirmarlo y activar el esclavo
  • La SCKlínea alterna para indicar cuándo se deben muestrear las líneas de datos
  • Los datos es muestreada por el maestro y el esclavo en el líder borde de SCK(usando la fase de reloj predeterminado)
  • Tanto el maestro como el esclavo se preparan para el siguiente bit en el borde posterior de SCK(usando la fase de reloj predeterminada), cambiando MISO/ MOSIsi es necesario
  • Una vez que finaliza la transmisión (posiblemente después de que se hayan enviado varios bytes), se SSva alto para desactivarla

Tenga en cuenta que:

  • El bit más significativo se envía primero (por defecto)
  • 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, el esclavo no puede responder al maestro de inmediato. Los protocolos SPI generalmente esperan que el maestro solicite datos en una transmisión y obtenga una respuesta en una posterior.

Usando la biblioteca SPI en Arduino, hacer una sola transferencia se ve así en el código:

 byte outgoing = 0xAB;
 byte incoming = SPI.transfer (outgoing);

Código de muestra

Ejemplo de envío solamente (ignorando cualquier dato entrante):

#include <SPI.h>

void setup (void)
  {
  digitalWrite(SS, HIGH);  // ensure SS stays high
  SPI.begin ();
  } // end of setup

void loop (void)
  {
  byte c;

  // enable Slave Select
  digitalWrite(SS, LOW);    // SS is pin 10

  // send test string
  for (const char * p = "Fab" ; c = *p; p++)
    SPI.transfer (c);

  // disable Slave Select
  digitalWrite(SS, HIGH);

  delay (100);
  } // end of loop

Cableado para SPI de solo salida

El código anterior (que solo envía) podría usarse para controlar un registro de desplazamiento en serie de salida. Estos son dispositivos de solo salida, por lo que no debemos preocuparnos por los datos entrantes. En su caso, el pin SS podría llamarse el pin "store" o "latch".

Protocolo SPI que muestra 3 señales

Ejemplos de esto son el registro de desplazamiento en serie 74HC595 y varias tiras de LED, solo por mencionar un par. Por ejemplo, esta pantalla LED de 64 píxeles impulsada por un chip MAX7219:

Pantalla LED de 64 píxeles

En este caso, puede ver que el fabricante de la placa ha utilizado nombres de señal ligeramente diferentes:

  • DIN (entrada de datos) es MOSI (salida maestra, entrada esclava)
  • CS (Selección de chip) es SS (Selección de esclavo)
  • CLK (reloj) es SCK (reloj serie)

La mayoría de los tableros seguirán un patrón similar. A veces DIN es solo DI (entrada de datos).

Aquí hay otro ejemplo, esta vez una placa de pantalla LED de 7 segmentos (también basada en el chip MAX7219):

Pantalla LED de 7 segmentos

Esto usa exactamente los mismos nombres de señal que la otra placa. En ambos casos, puede ver que la placa solo necesita 5 cables, los tres para SPI, más alimentación y tierra.


Fase de reloj y polaridad

Hay cuatro formas de probar 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 (predeterminado): el reloj normalmente está bajo (CPOL = 0) y los datos se muestrean en la transición de bajo a alto (borde de ataque) (CPHA = 0)
  • Modo 1: el reloj normalmente está bajo (CPOL = 0), y los datos se muestrean en la transición de alto a bajo (borde posterior) (CPHA = 1)
  • Modo 2: el reloj normalmente es alto (CPOL = 1), y los datos se muestrean en la transición de alto a bajo (borde delantero) (CPHA = 0)
  • Modo 3: el reloj es normalmente alto (CPOL = 1) y los datos se muestrean en la transición de bajo a alto (borde posterior) (CPHA = 1)

Estos se ilustran en este gráfico:

Fase de reloj SPI y polaridad

Debe consultar la hoja de datos de su dispositivo para obtener la fase y la polaridad correctas. Por lo general, habrá un diagrama que muestra cómo muestrear el reloj. Por ejemplo, de la hoja de datos para el chip 74HC595:

Reloj 74HC595

Como puede ver, el reloj normalmente está bajo (CPOL = 0) y se muestrea en el borde de ataque (CPHA = 0), por lo que este es el modo SPI 0.

Puede cambiar la polaridad del reloj y la fase en un código como este (elija solo uno, por supuesto):

SPI.setDataMode (SPI_MODE0);
SPI.setDataMode (SPI_MODE1);
SPI.setDataMode (SPI_MODE2);
SPI.setDataMode (SPI_MODE3);

Este método está en desuso en las versiones 1.6.0 en adelante del IDE de Arduino. Para versiones recientes, cambia el modo de reloj en la SPI.beginTransactionllamada, así:

SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));  // 2 MHz clock, MSB first, mode 0

Orden de datos

El valor predeterminado es el bit más significativo primero, sin embargo, puede indicarle al hardware que procese el bit menos significativo primero de esta manera:

SPI.setBitOrder (LSBFIRST);   // least significant bit first
SPI.setBitOrder (MSBFIRST);   // most significant bit first

Nuevamente, esto está en desuso en las versiones 1.6.0 en adelante del IDE de Arduino. Para las versiones recientes, cambia el orden de bits en la SPI.beginTransactionllamada, así:

SPI.beginTransaction (SPISettings (1000000, LSBFIRST, SPI_MODE2));  // 1 MHz clock, LSB first, mode 2

Velocidad

La configuración predeterminada para SPI es utilizar la velocidad del reloj del sistema dividida por cuatro, es decir, un pulso de reloj SPI cada 250 ns, suponiendo un reloj de CPU de 16 MHz. Puede cambiar el divisor del reloj usando setClockDividerasí:

SPI.setClockDivider (divider);

Donde "divisor" es uno de:

  • SPI_CLOCK_DIV2
  • SPI_CLOCK_DIV4
  • SPI_CLOCK_DIV8
  • SPI_CLOCK_DIV16
  • SPI_CLOCK_DIV32
  • SPI_CLOCK_DIV64
  • SPI_CLOCK_DIV128

La tasa más rápida es "dividir por 2" o un pulso de reloj SPI cada 125 ns, suponiendo un reloj de CPU de 16 MHz. Por lo tanto, esto llevaría 8 * 125 ns o 1 µs para transmitir un byte.

Este método está en desuso en las versiones 1.6.0 en adelante del IDE de Arduino. Para versiones recientes, cambia la velocidad de transferencia en la SPI.beginTransactionllamada, así:

SPI.beginTransaction (SPISettings (4000000, MSBFIRST, SPI_MODE0));  // 4 MHz clock, MSB first, mode 0

Sin embargo, las pruebas empíricas muestran que es necesario tener dos pulsos de reloj entre bytes, por lo que la velocidad máxima a la que se pueden sincronizar los bytes es de 1.125 µs cada uno (con un divisor de reloj de 2).

Para resumir, cada byte se puede enviar a una velocidad máxima de uno por 1.125 µs (con un reloj de 16 MHz), lo que proporciona una tasa de transferencia máxima teórica de 1 / 1.125 µs, o 888,888 bytes por segundo (excluyendo la sobrecarga, como configurar SS bajo y así en).


Conectando a Arduino

Arduino Uno

Conexión a través de pines digitales 10 a 13:

Pasadores Arduino Uno SPI

Conexión a través del encabezado ICSP:

Pinout ICSP - Uno

Encabezado ICSP

Arduino Atmega2560

Conexión a través de pines digitales 50 a 52:

Pines Arduino Mega2560 SPI

También puede usar el encabezado ICSP, similar al Uno de arriba.

Arduino Leonardo

Leonardo y Micro no exponen los pines SPI en los pines digitales, a diferencia del Uno y Mega. Su única opción es usar los pines del encabezado ICSP, como se ilustra arriba para el Uno.


Esclavos múltiples

Un maestro puede comunicarse con múltiples esclavos (sin embargo, solo uno a la vez). Lo hace afirmando SS para un esclavo y desaprendizándolo para todos los demás. El esclavo que ha afirmado SS (generalmente esto significa BAJO) configura su pin MISO como una salida para que el esclavo, y ese esclavo solo, pueda responder al maestro. Los otros esclavos ignoran los pulsos de reloj entrantes si no se afirma SS. Por lo tanto, necesita una señal adicional para cada esclavo, así:

Múltiples esclavos SPI

En este gráfico puede ver que MISO, MOSI, SCK se comparten entre ambos esclavos, sin embargo, cada esclavo tiene su propia señal SS (selección de esclavo).


Protocolos

La especificación SPI no especifica los protocolos como tales, por lo que corresponde a los emparejamientos maestro / esclavo individuales acordar lo que significan los datos. Si bien puede enviar y recibir bytes simultáneamente, el byte recibido no puede ser una respuesta directa al byte enviado (ya que se ensamblan simultáneamente).

Por lo tanto, sería más lógico que un extremo envíe una solicitud (por ejemplo, 4 podría significar "enumerar el directorio del disco") y luego hacer transferencias (tal vez simplemente enviando ceros hacia afuera) hasta que reciba una respuesta completa. La respuesta puede terminar con una nueva línea o un carácter 0x00.

Lea la hoja de datos de su dispositivo esclavo para ver qué secuencias de protocolo espera.


Cómo hacer un esclavo SPI

El ejemplo anterior muestra al Arduino como el maestro, enviando datos a un dispositivo esclavo. Este ejemplo muestra cómo Arduino puede ser un esclavo.

Configuración de hardware

Conecte dos Arduino Unos junto con los siguientes pines conectados entre sí:

  • 10 (SS)
  • 11 (MOSI)
  • 12 (MISO)
  • 13 (SCK)

  • + 5v (si es necesario)

  • GND (para retorno de señal)

En el Arduino Mega, los pines son 50 (MISO), 51 (MOSI), 52 (SCK) y 53 (SS).

En cualquier caso, MOSI en un extremo está conectado a MOSI en el otro, no los intercambia (es decir , no tiene MOSI <-> MISO). El software configura un extremo de MOSI (extremo maestro) como salida y el otro extremo (extremo esclavo) como entrada.

Ejemplo maestro

#include <SPI.h>

void setup (void)
{

  digitalWrite(SS, HIGH);  // ensure SS stays high for now

  // Put SCK, MOSI, SS pins into output mode
  // also put SCK, MOSI into LOW state, and SS into HIGH state.
  // Then put SPI hardware into Master mode and turn SPI on
  SPI.begin ();

  // Slow down the master a bit
  SPI.setClockDivider(SPI_CLOCK_DIV8);

}  // end of setup


void loop (void)
{

  char c;

  // enable Slave Select
  digitalWrite(SS, LOW);    // SS is pin 10

  // send test string
  for (const char * p = "Hello, world!\n" ; c = *p; p++)
    SPI.transfer (c);

  // disable Slave Select
  digitalWrite(SS, HIGH);

  delay (1000);  // 1 seconds delay
}  // end of loop

Ejemplo esclavo

#include <SPI.h>

char buf [100];
volatile byte pos;
volatile bool process_it;

void setup (void)
{
  Serial.begin (115200);   // debugging

  // turn on SPI in slave mode
  SPCR |= bit (SPE);

  // have to send on master in, *slave out*
  pinMode (MISO, OUTPUT);

  // get ready for an interrupt
  pos = 0;   // buffer empty
  process_it = false;

  // now turn on interrupts
  SPI.attachInterrupt();

}  // end of setup


// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR;  // grab byte from SPI Data Register

  // add to buffer if room
  if (pos < sizeof buf)
    {
    buf [pos++] = c;

    // example: newline means time to process buffer
    if (c == '\n')
      process_it = true;

    }  // end of room available
}  // end of interrupt routine SPI_STC_vect

// main loop - wait for flag set in interrupt routine
void loop (void)
{
  if (process_it)
    {
    buf [pos] = 0;
    Serial.println (buf);
    pos = 0;
    process_it = false;
    }  // end of flag set

}  // end of loop

El esclavo está completamente controlado por interrupciones, por lo que puede hacer otras cosas. Los datos SPI entrantes se recopilan en un búfer y se establece un indicador cuando llega un "byte significativo" (en este caso, una nueva línea). Esto le dice al esclavo que se suba y comience a procesar los datos.

Ejemplo de conectar maestro a esclavo usando SPI

Arduino SPI maestro y esclavo


Cómo obtener una respuesta de un esclavo

Siguiendo con el código anterior que envía datos de un maestro SPI a un esclavo, el siguiente ejemplo muestra el envío de datos a un esclavo, que haga algo con él y devuelva una respuesta.

El maestro es similar al ejemplo anterior. Sin embargo, un punto importante es que necesitamos agregar un ligero retraso (algo así como 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 un "comando". En este caso, "a" (agregar algo) o "s" (restar algo). Esto es para mostrar que el esclavo realmente está haciendo algo con los datos.

Después de afirmar que el esclavo-select (SS) inicia la transacción, el maestro envía el comando, seguido de cualquier número de bytes, y luego levanta el SS para terminar la transacció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 próximo byte. Esto se debe a que los bits que se envían y los bits que se reciben se envían simultáneamente. Por lo tanto, para agregar algo a cuatro números, necesitamos cinco transferencias, como esta:

transferAndWait ('a');  // add command
transferAndWait (10);
a = transferAndWait (17);
b = transferAndWait (33);
c = transferAndWait (42);
d = transferAndWait (0);

Primero solicitamos una acción sobre el número 10. Pero no recibimos una respuesta hasta la próxima transferencia (la del 17). Sin embargo, "a" se establecerá en la respuesta a 10. Finalmente, terminamos enviando un número "ficticio" 0, para obtener la respuesta de 42.

Maestro (ejemplo)

  #include <SPI.h>

  void setup (void)
    {
    Serial.begin (115200);
    Serial.println ();

    digitalWrite(SS, HIGH);  // ensure SS stays high for now
    SPI.begin ();

    // Slow down the master a bit
    SPI.setClockDivider(SPI_CLOCK_DIV8);
    }  // end of setup

  byte transferAndWait (const byte what)
    {
    byte a = SPI.transfer (what);
    delayMicroseconds (20);
    return a;
    } // end of transferAndWait

  void loop (void)
    {

    byte a, b, c, d;

    // enable Slave Select
    digitalWrite(SS, LOW);

    transferAndWait ('a');  // add command
    transferAndWait (10);
    a = transferAndWait (17);
    b = transferAndWait (33);
    c = transferAndWait (42);
    d = transferAndWait (0);

    // disable Slave Select
    digitalWrite(SS, HIGH);

    Serial.println ("Adding results:");
    Serial.println (a, DEC);
    Serial.println (b, DEC);
    Serial.println (c, DEC);
    Serial.println (d, DEC);

    // enable Slave Select
    digitalWrite(SS, LOW);

    transferAndWait ('s');  // subtract command
    transferAndWait (10);
    a = transferAndWait (17);
    b = transferAndWait (33);
    c = transferAndWait (42);
    d = transferAndWait (0);

    // disable Slave Select
    digitalWrite(SS, HIGH);

    Serial.println ("Subtracting results:");
    Serial.println (a, DEC);
    Serial.println (b, DEC);
    Serial.println (c, DEC);
    Serial.println (d, DEC);

    delay (1000);  // 1 second delay
    }  // end of loop

El código para el esclavo básicamente hace casi todo en la rutina de interrupción (llamado cuando llegan los datos SPI entrantes). Toma el byte entrante y suma o resta según el "byte de comando" recordado. Tenga en cuenta que la respuesta se "recopilará" la próxima vez a través del ciclo. Es por eso que el maestro tiene que enviar una transferencia final "ficticia" para obtener la respuesta final.

En mi ejemplo, estoy usando el bucle principal para detectar simplemente cuándo SS sube y borrar el comando guardado. De esa manera, cuando SS se baja nuevamente para la próxima transacción, el primer byte se considera el byte de comando.

Más confiablemente, esto se haría con una interrupción. Es decir, conectaría físicamente SS a una de las entradas de interrupción (por ejemplo, en el Uno, conectaría el pin 10 (SS) al pin 2 (una entrada de interrupción), o usaría una interrupción de cambio de pin en el pin 10.

Luego, la interrupción podría usarse para notar cuándo SS se está bajando o bajando.

Esclavo (ejemplo)

// what to do with incoming data
volatile byte command = 0;

void setup (void)
  {

  // have to send on master in, *slave out*
  pinMode(MISO, OUTPUT);

  // turn on SPI in slave mode
  SPCR |= _BV(SPE);

  // turn on interrupts
  SPCR |= _BV(SPIE);

  }  // end of setup


// SPI interrupt routine
ISR (SPI_STC_vect)
  {
  byte c = SPDR;

  switch (command)
    {
    // no command? then this is the command
    case 0:
      command = c;
      SPDR = 0;
      break;

    // add to incoming byte, return result
    case 'a':
      SPDR = c + 15;  // add 15
      break;

    // subtract from incoming byte, return result
    case 's':
      SPDR = c - 8;  // subtract 8
      break;

    } // end of switch

  }  // end of interrupt service routine (ISR) SPI_STC_vect

void loop (void)
  {

  // if SPI not active, clear current command
  if (digitalRead (SS) == HIGH)
    command = 0;
  }  // end of loop

Salida de ejemplo

Adding results:
25
32
48
57
Subtracting results:
2
9
25
34
Adding results:
25
32
48
57
Subtracting results:
2
9
25
34

Analizador lógico de salida

Esto muestra el tiempo entre el envío y la recepción en el código anterior:

Tiempo maestro y esclavo SPI


Nueva funcionalidad en IDE 1.6.0 en adelante

La versión 1.6.0 del IDE ha cambiado la forma en que funciona SPI, hasta cierto punto. Usted todavía tiene que hacer SPI.begin() antes de utilizar SPI. Eso configura el hardware SPI. Sin embargo ahora, cuando está a punto de comenzar la comunicación con un esclavo que también se hace SPI.beginTransaction()para establecer SPI (en este esclavo) con el correcto:

  • Velocidad de reloj
  • Orden de bits
  • Fase de reloj y polaridad

Cuando termines de comunicarte con el esclavo, llamas SPI.endTransaction(). Por ejemplo:

SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));
digitalWrite (SS, LOW);        // assert Slave Select
byte foo = SPI.transfer (42);  // do a transfer
digitalWrite (SS, HIGH);       // de-assert Slave Select
SPI.endTransaction ();         // transaction over

¿Por qué usar SPI?

Añadiría una pregunta preliminar: ¿cuándo / por qué usarías SPI? La necesidad de una configuración multimaestro o una gran cantidad de esclavos inclinaría la balanza hacia I2C.

Esta es una excelente pregunta. Mis respuestas son:

  • Algunos dispositivos (bastantes) solo admiten el método de transferencia SPI. Por ejemplo, el registro de desplazamiento de salida 74HC595, el registro de desplazamiento de entrada 74HC165, el controlador LED MAX7219 y algunas tiras de LED que he visto. Por lo tanto, puede usarlo porque el dispositivo de destino solo lo admite.
  • SPI es realmente el método más rápido disponible en los chips Atmega328 (y similares). La tasa más rápida citada arriba es de 888,888 bytes por segundo. Usando I 2 C solo puede obtener alrededor de 40,000 bytes por segundo. La sobrecarga del I 2 C es bastante considerable, y si está tratando de interactuar realmente rápido, SPI es la opción preferida. Algunas familias de chips (por ejemplo, MCP23017 y MCP23S17) realmente admiten I 2 C y SPI, por lo que a menudo puede elegir entre la velocidad y la capacidad de tener múltiples dispositivos en un solo bus.
  • Los dispositivos SPI e I 2 C son compatibles con hardware en el Atmega328, por lo que posiblemente podría estar haciendo una transferencia a través de SPI simultáneamente con I 2 C, lo que le daría un aumento de velocidad.

Ambos métodos tienen su lugar. I 2 C le permite conectar muchos dispositivos a un solo bus (dos cables, más tierra), por lo que sería la opción preferida si necesitara interrogar a un número considerable de dispositivos, quizás con poca frecuencia. Sin embargo, la velocidad de SPI podría ser más relevante para situaciones en las que necesita emitir rápidamente (por ejemplo, una tira de LED) o ingresar rápidamente (por ejemplo, un convertidor ADC).


Referencias

Nick Gammon
fuente
¿Vas a cubrir la rareza que es el SPI del Due? ¿Dónde la configuración del puerto SPI está vinculada al pin SS utilizado, y hay 4 pines SS de hardware (IIRC) asignados al puerto SPI?
Majenko
Otro punto sobre la selección: a veces realmente no tiene otra opción porque el sensor que desea / necesita usar solo está disponible como I2C.
Igor Stoppa
Are you going to cover the weirdness that is the Due's SPI?- No sé nada sobre el SPI de Due (aparte de suponer que el protocolo general es el mismo). Le invitamos a agregar una respuesta que cubra ese aspecto.
Nick Gammon
¿Cuándo saldrá el audiolibro de esta respuesta? ¿Lo leerán ustedes mismos?)
AMADANON Inc.
1
@AMADANONInc. ¿Quizás un video musical? O una animación? No estoy seguro de si mi acento australiano sería comprensible. : P
Nick Gammon