Con riferimento ad Arduino Uno, Mega2560, Leonardo e schede simili:
- Come funziona SPI?
- Quanto è veloce SPI?
- Come posso collegarmi tra un master e uno slave?
- Come faccio a creare uno slave SPI?
Nota: questa è una domanda di riferimento.
Con riferimento ad Arduino Uno, Mega2560, Leonardo e schede simili:
Nota: questa è una domanda di riferimento.
Risposte:
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.
In un sistema SPI completo avrai quattro linee di segnale:
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:
Notare che tre segnali sono emessi dal master (MOSI, SCK, SS) e uno è un input (MISO).
La sequenza degli eventi è:
SS
si abbassa per affermarlo e attivare lo schiavoSCK
riga attiva / disattiva per indicare quando le linee di dati devono essere campionateSCK
(usando la fase clock di default)SCK
(usando la fase di clock predefinita), modificando MISO
/ MOSI
se necessarioSS
aumenta per disinserirlaNota che:
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);
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
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".
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:
In questo caso puoi vedere che il produttore della scheda ha usato nomi di segnali leggermente diversi:
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):
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.
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.
Questi sono illustrati in questo grafico:
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:
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.beginTransaction
chiamata, in questo modo:
SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0)); // 2 MHz clock, MSB first, mode 0
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.beginTransaction
chiamata, in questo modo:
SPI.beginTransaction (SPISettings (1000000, LSBFIRST, SPI_MODE2)); // 1 MHz clock, LSB first, mode 2
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 setClockDivider
questo modo:
SPI.setClockDivider (divider);
Dove "divisore" è uno di:
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.beginTransaction
chiamata, 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).
Collegamento tramite pin digitali da 10 a 13:
Connessione tramite l'intestazione ICSP:
Collegamento tramite pin digitali da 50 a 52:
Puoi anche usare l'intestazione ICSP, simile a Uno sopra.
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.
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:
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).
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.
L'esempio precedente mostra Arduino come master, inviando dati a un dispositivo slave. Questo esempio mostra come Arduino può essere uno schiavo.
Collega due Arduino Unos insieme ai seguenti pin collegati tra loro:
13 (SCK)
+ 5v (se richiesto)
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.
#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
#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.
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.
#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.
// 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
Adding results:
25
32
48
57
Subtracting results:
2
9
25
34
Adding results:
25
32
48
57
Subtracting results:
2
9
25
34
Questo mostra i tempi tra l'invio e la ricezione nel codice sopra:
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:
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
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:
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).
La mia pagina su SPI - contiene anche dettagli su SPI con bit di bit e sull'uso di USART per ottenere una seconda SPI hardware sul chip Atmega328.
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.