Come si usa SPI su un Arduino?


Risposte:


81

Introduzione a SPI

L' interfaccia SPI ( Serial Peripheral Interface Bus ) viene utilizzata per la comunicazione tra più dispositivi a breve distanza e ad alta velocità.

In genere esiste un singolo dispositivo "master", che avvia le comunicazioni e fornisce l'orologio che controlla la velocità di trasferimento dei dati. Possono esserci uno o più schiavi. Per più di uno slave, ognuno ha il proprio segnale di "selezione slave", descritto più avanti.


Segnali SPI

In un sistema SPI completo avrai quattro linee di segnale:

  • Master Out, Slave In ( MOSI ) - ovvero i dati che vanno dal master allo slave
  • Master In, Slave Out ( MISO ) - ovvero i dati che vanno dallo slave al master
  • Orologio seriale ( SCK ) - quando questo commuta sia il master che lo slave campionano il bit successivo
  • Slave Select ( SS ) - questo dice a un particolare slave di diventare "attivo"

Quando più slave sono collegati al segnale MISO, si prevede che essi possano tri-state (mantenere ad alta impedenza) quella linea MISO fino a quando non vengono selezionati da Slave Select. Normalmente Slave Select (SS) diventa basso per affermarlo. Cioè, è attivo basso. Una volta selezionato un determinato slave, deve configurare la linea MISO come uscita in modo da poter inviare i dati al master.

Questa immagine mostra il modo in cui i dati vengono scambiati quando viene inviato un byte:

Protocollo SPI che mostra 4 segnali

Notare che tre segnali sono emessi dal master (MOSI, SCK, SS) e uno è un input (MISO).


sincronizzazione

La sequenza degli eventi è:

  • SS si abbassa per affermarlo e attivare lo schiavo
  • La SCKriga attiva / disattiva per indicare quando le linee di dati devono essere campionate
  • I dati vengono campionati dal master e slave sul primo bordo SCK(usando la fase clock di default)
  • Sia master che slave si preparano per il bit successivo sul bordo posteriore di SCK(usando la fase di clock predefinita), modificando MISO/ MOSIse necessario
  • Una volta terminata la trasmissione (possibilmente dopo che sono stati inviati più byte), SSaumenta per disinserirla

Nota che:

  • Il bit più significativo viene inviato per primo (per impostazione predefinita)
  • I dati vengono inviati e ricevuti nello stesso istante (full duplex)

Poiché i dati vengono inviati e ricevuti sullo stesso impulso di clock, non è possibile che lo slave risponda immediatamente al master. I protocolli SPI di solito prevedono che il master richieda dati su una trasmissione e ottenga una risposta su una successiva.

Utilizzando la libreria SPI su Arduino, eseguire un singolo trasferimento è simile al codice:

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

Codice di esempio

Esempio di solo invio (ignorando qualsiasi dato in arrivo):

#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

Cablaggio per SPI di sola uscita

Il codice sopra (che invia solo) potrebbe essere usato per pilotare un registro a scorrimento seriale in uscita. Questi sono solo dispositivi di output, quindi non dobbiamo preoccuparci di alcun dato in arrivo. Nel loro caso il pin SS potrebbe essere chiamato pin "store" o "latch".

Protocollo SPI che mostra 3 segnali

Esempi di questo sono il registro a scorrimento seriale 74HC595 e varie strisce LED, solo per citarne un paio. Ad esempio, questo display a LED da 64 pixel guidato da un chip MAX7219:

Display a LED da 64 pixel

In questo caso puoi vedere che il produttore della scheda ha usato nomi di segnali leggermente diversi:

  • DIN (Data In) è MOSI (Master Out, Slave In)
  • CS (Chip Select) è SS (Slave Select)
  • CLK (Clock) è SCK (Serial Clock)

La maggior parte delle schede seguirà un modello simile. A volte DIN è solo DI (Data In).

Ecco un altro esempio, questa volta un tabellone LED a 7 segmenti (anch'esso basato sul chip MAX7219):

Display a LED a 7 segmenti

Questo utilizza esattamente gli stessi nomi di segnale dell'altra scheda. In entrambi questi casi puoi vedere che la scheda necessita solo di 5 fili, i tre per SPI, più potenza e terra.


Fase dell'orologio e polarità

Esistono quattro modi per campionare l'orologio SPI.

Il protocollo SPI consente variazioni sulla polarità degli impulsi di clock. CPOL è la polarità di clock e CPHA è la fase di clock.

  • Modalità 0 (impostazione predefinita) - l'orologio è normalmente basso (CPOL = 0) e i dati vengono campionati sulla transizione da basso ad alto (bordo anteriore) (CPHA = 0)
  • Modalità 1 - l'orologio è normalmente basso (CPOL = 0) e i dati vengono campionati sulla transizione da alto a basso (bordo posteriore) (CPHA = 1)
  • Modalità 2 - l'orologio è normalmente alto (CPOL = 1) e i dati vengono campionati sulla transizione da alto a basso (bordo anteriore) (CPHA = 0)
  • Modalità 3: l'orologio è normalmente alto (CPOL = 1) e i dati vengono campionati sulla transizione da basso ad alto (bordo posteriore) (CPHA = 1)

Questi sono illustrati in questo grafico:

Fase e polarità dell'orologio SPI

Fare riferimento alla scheda tecnica del dispositivo per ottenere la fase e la polarità corrette. Di solito ci sarà un diagramma che mostra come campionare l'orologio. Ad esempio, dal foglio dati per il chip 74HC595:

Orologio 74HC595

Come puoi vedere, l'orologio è normalmente basso (CPOL = 0) ed è campionato sul fronte (CPHA = 0), quindi questa è la modalità SPI 0.

È possibile modificare la polarità e la fase del clock in questo modo (sceglierne solo una, ovviamente):

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

Questo metodo è obsoleto nelle versioni 1.6.0 e successive dell'IDE Arduino. Per le versioni recenti si modifica la modalità orologio nella SPI.beginTransactionchiamata, in questo modo:

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

Ordine dei dati

Il valore predefinito è innanzitutto il bit più significativo, tuttavia è possibile indicare all'hardware di elaborare prima il bit meno significativo in questo modo:

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

Ancora una volta, questo è deprecato nelle versioni 1.6.0 e successive dell'IDE di Arduino. Per le versioni recenti si modifica l'ordine dei bit nella SPI.beginTransactionchiamata, in questo modo:

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

Velocità

L'impostazione predefinita per SPI è utilizzare la velocità di clock del sistema divisa per quattro, ovvero un impulso di clock SPI ogni 250 ns, ipotizzando un clock della CPU a 16 MHz. Puoi cambiare il divisore di clock usando in setClockDividerquesto modo:

SPI.setClockDivider (divider);

Dove "divisore" è uno di:

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

La velocità più veloce è "divide per 2" o un impulso di clock SPI ogni 125 ns, ipotizzando un clock della CPU a 16 MHz. Ciò richiederebbe quindi 8 * 125 ns o 1 µs per trasmettere un byte.

Questo metodo è obsoleto nelle versioni 1.6.0 e successive dell'IDE Arduino. Per le versioni recenti si modifica la velocità di trasferimento nella SPI.beginTransactionchiamata, in questo modo:

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

Tuttavia, i test empirici mostrano che è necessario disporre di due impulsi di clock tra i byte, quindi la velocità massima alla quale i byte possono essere esclusi è di 1,125 µs ciascuno (con un divisore di clock di 2).

Riassumendo, ogni byte può essere inviato a una velocità massima di uno per 1,125 µs (con un clock a 16 MHz) fornendo una velocità di trasferimento massima teorica di 1 / 1,125 µs o 888,888 byte al secondo (escluso il sovraccarico come l'impostazione SS bassa e così via su).


Connessione ad Arduino

Arduino Uno

Collegamento tramite pin digitali da 10 a 13:

Perni Arduino Uno SPI

Connessione tramite l'intestazione ICSP:

Pinout ICSP - Uno

Intestazione ICSP

Arduino Atmega2560

Collegamento tramite pin digitali da 50 a 52:

Pin Arduino Mega2560 SPI

Puoi anche usare l'intestazione ICSP, simile a Uno sopra.

Arduino Leonardo

Leonardo e Micro non espongono i pin SPI sui pin digitali, a differenza di Uno e Mega. L'unica opzione è utilizzare i pin dell'intestazione ICSP, come illustrato sopra per Uno.


Schiavi multipli

Un master può comunicare con più slave (comunque solo uno alla volta). Lo fa affermando SS per uno schiavo e disinserendolo per tutti gli altri. Lo slave che ha affermato SS (di solito significa BASSO) configura il suo pin MISO come un'uscita in modo che lo slave e solo lo slave possano rispondere al master. Gli altri slave ignorano qualsiasi impulso di clock in entrata se SS non viene affermato. Quindi è necessario un segnale aggiuntivo per ogni slave, in questo modo:

Slave SPI multipli

In questo grafico puoi vedere che MISO, MOSI, SCK sono condivisi tra entrambi gli slave, tuttavia ogni slave ha il proprio segnale SS (selezione slave).


protocolli

Le specifiche SPI non specificano i protocolli in quanto tali, quindi spetta ai singoli accoppiamenti master / slave concordare sul significato dei dati. Sebbene sia possibile inviare e ricevere byte contemporaneamente, il byte ricevuto non può essere una risposta diretta al byte inviato (poiché vengono assemblati contemporaneamente).

Quindi sarebbe più logico che un'estremità invii una richiesta (es. 4 potrebbe significare "elencare la directory del disco") e poi fare trasferimenti (forse semplicemente inviare zeri verso l'esterno) fino a quando non riceve una risposta completa. La risposta potrebbe terminare con una nuova riga o un carattere 0x00.

Leggi la scheda tecnica del tuo dispositivo slave per vedere quali sequenze di protocollo si aspetta.


Come creare uno slave SPI

L'esempio precedente mostra Arduino come master, inviando dati a un dispositivo slave. Questo esempio mostra come Arduino può essere uno schiavo.

Configurazione dell'hardware

Collega due Arduino Unos insieme ai seguenti pin collegati tra loro:

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

  • + 5v (se richiesto)

  • GND (per il ritorno del segnale)

Su Arduino Mega, i pin sono 50 (MISO), 51 (MOSI), 52 (SCK) e 53 (SS).

In ogni caso, MOSI a un'estremità è collegato a MOSI all'altra, non li si scambia (cioè non si ha MOSI <-> MISO). Il software configura un'estremità di MOSI (estremità master) come uscita e l'altra estremità (estremità slave) come ingresso.

Esempio principale

#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

Esempio di schiavo

#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

Lo slave è interamente guidato dall'interruzione, quindi può fare altre cose. I dati SPI in entrata vengono raccolti in un buffer e viene impostato un flag quando arriva un "byte significativo" (in questo caso una nuova riga). Questo dice allo slave di salire e iniziare a elaborare i dati.

Esempio di collegamento tra master e slave tramite SPI

Arduino SPI master e slave


Come ottenere una risposta da uno schiavo

Seguendo il codice sopra che invia i dati da un master SPI a uno slave, l'esempio seguente mostra l'invio di dati a uno slave, facendogli fare qualcosa con esso e restituendo una risposta.

Il master è simile all'esempio sopra. Tuttavia un punto importante è che dobbiamo aggiungere un leggero ritardo (qualcosa come 20 microsecondi). Altrimenti lo slave non ha la possibilità di reagire ai dati in arrivo e fare qualcosa con esso.

L'esempio mostra l'invio di un "comando". In questo caso "a" (aggiungi qualcosa) o "s" (sottrai qualcosa). Questo per mostrare che lo slave sta effettivamente facendo qualcosa con i dati.

Dopo aver affermato lo slave-select (SS) per avviare la transazione, il master invia il comando, seguito da un numero qualsiasi di byte, quindi solleva SS per terminare la transazione.

Un punto molto importante è che lo slave non può rispondere a un byte in arrivo nello stesso momento. La risposta deve essere nel byte successivo. Questo perché i bit che vengono inviati e i bit che vengono ricevuti vengono inviati contemporaneamente. Quindi per aggiungere qualcosa a quattro numeri abbiamo bisogno di cinque trasferimenti, in questo modo:

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

Per prima cosa richiediamo un'azione al numero 10. Ma non otteniamo una risposta fino al prossimo trasferimento (quello per 17). Tuttavia, "a" verrà impostato sulla risposta su 10. Alla fine, inviamo un numero "fittizio" 0, per ottenere la risposta per 42.

Master (esempio)

  #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

Il codice per lo slave praticamente fa quasi tutto nella routine di interrupt (chiamato quando arrivano i dati SPI in arrivo). Prende il byte in entrata e aggiunge o sottrae secondo il "byte di comando" memorizzato. Si noti che la risposta verrà "raccolta" la prossima volta attraverso il ciclo. Questo è il motivo per cui il master deve inviare un trasferimento "fittizio" finale per ottenere la risposta finale.

Nel mio esempio sto usando il loop principale per rilevare semplicemente quando SS sale e deselezionare il comando salvato. In questo modo, quando SS viene nuovamente abbassato per la transazione successiva, il primo byte viene considerato il byte di comando.

In modo più affidabile, questo sarebbe fatto con un interrupt. Cioè, collegheresti fisicamente SS a uno degli ingressi di interrupt (ad es. Su Uno, collegherai il pin 10 (SS) al pin 2 (un ingresso di interrupt), o utilizzeresti un interrupt di cambio pin sul pin 10.

Quindi l'interrupt potrebbe essere usato per notare quando SS viene tirato in basso o in alto.

Slave (esempio)

// 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

Esempio di output

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

Uscita dell'analizzatore logico

Questo mostra i tempi tra l'invio e la ricezione nel codice sopra:

Tempistica SPI master e slave


Nuove funzionalità da IDE 1.6.0 in poi

La versione 1.6.0 dell'IDE ha cambiato il modo in cui SPI funziona, in una certa misura. Devi ancora farlo SPI.begin() prima di utilizzare SPI. Questo imposta l'hardware SPI. Tuttavia ora, quando si sta per iniziare a comunicare con uno slave si anche fai SPI.beginTransaction()da configurare SPI (per questo slave) con la corretta:

  • Velocità di clock
  • Ordine bit
  • Fase dell'orologio e polarità

Quando hai finito di comunicare con lo slave, chiami SPI.endTransaction(). Per esempio:

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

Perché usare SPI?

Vorrei aggiungere una domanda preliminare: quando / perché dovresti usare SPI? La necessità di una configurazione multi-master o un numero molto elevato di slave inclinerebbe la scala verso I2C.

Questa è un'ottima domanda Le mie risposte sono:

  • Alcuni dispositivi (alcuni) supportano solo il metodo di trasferimento SPI. Ad esempio il registro a scorrimento in uscita 74HC595, il registro a scorrimento in ingresso 74HC165, il driver LED MAX7219 e alcune strisce LED che ho visto. Quindi, potresti usarlo perché il dispositivo di destinazione lo supporta solo.
  • SPI è davvero il metodo più veloce disponibile sui chip Atmega328 (e simili). La velocità massima indicata sopra è di 888.888 byte al secondo. Utilizzando I 2 C è possibile ottenere solo circa 40.000 byte al secondo. Il sovraccarico di I 2 C è piuttosto sostanziale e se si sta tentando di interfacciarsi molto rapidamente, SPI è la scelta preferita. Diverse famiglie di chip (ad es. MCP23017 e MCP23S17) supportano effettivamente sia I 2 C che SPI, quindi spesso è possibile scegliere tra la velocità e la possibilità di avere più dispositivi su un singolo bus.
  • I dispositivi SPI e I 2 C sono entrambi supportati nell'hardware su Atmega328, quindi è possibile che tu stia facendo un trasferimento tramite SPI contemporaneamente a I 2 C, che ti darebbe un aumento di velocità.

Entrambi i metodi hanno il loro posto. I 2 C ti consente di collegare molti dispositivi a un singolo bus (due fili, più terra), quindi sarebbe la scelta preferita se avessi bisogno di interrogare un numero considerevole di dispositivi, forse abbastanza raramente. Tuttavia, la velocità dell'SPI potrebbe essere più rilevante per le situazioni in cui è necessario emettere rapidamente (ad es. Una striscia LED) o immettere rapidamente (ad es. Un convertitore ADC).


Riferimenti


Coprirai la stranezza che è la SPI di Due? Dove la configurazione della porta SPI è legata al pin SS utilizzato e ci sono (IIRC) 4 pin SS hardware assegnati alla porta SPI?
Majenko

Altro punto sulla selezione: a volte non hai davvero scelta perché il sensore che vuoi / devi usare è disponibile solo come I2C.
Igor Stoppa,

Are you going to cover the weirdness that is the Due's SPI?- Non so nulla dell'SPI di Due (a parte presumere che il protocollo generale sia lo stesso). È possibile aggiungere una risposta relativa a tale aspetto.
Nick Gammon

Quando uscirà l'audiolibro di questa risposta e la
leggerai

1
@AMADANONInc. Forse un video musicale? O un'animazione? Non sono sicuro che il mio accento australiano sarebbe comprensibile. : P
Nick Gammon
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.