L’SPI (Serial Peripheral Interface) è un bus di comunicazione sincrono tipicamente utilizzato per il trasferimento dei dati tra un microcontrollore ed una periferica esterna (es. sensore, attuatore, memoria, SD card, …). Essendo sincrono, a differenza della tipica comunicazione seriale asincrona (UART), esso utilizza un segnale di clock per garantire il perfetto sincronismo nella trasmissione e ricezione tra le due controparti note come master e slave.
Descrizione del bus
Complessivamente, il bus SPI è caratterizzato dai seguenti segnali :
-
SCLK (Serial CLocK) : clock per il sincronismo nello scambio dati;
-
SS (Slave Select) : segnale di abilitazione dello slave (ricevente);
-
MOSI (MasterOut / SlaveIn) : linea dei dati utilizzata per la trasmissione dal master allo slave;
-
MISO (MasterIn / SlaveOut) : linea dei dati utilizzata per la trasmissione dallo slave al master;
Escludendo il segnale SS, che può essere gestito separatamente, il bus va considerato un 3-wires (bus a 3 fili).
Il master ha il compito di generare il segnale di clock utilizzato anche dallo slave. Quest’ultimo utilizza tale segnale per individuare gli istanti di tempo in cui campionare il dato presente sulla linea MOSI (dato da ricevere) oppure in cui settare un livello logico (0/1) sulla linea MISO (dato da trasmettere); il campionamento può essere configurato sul fronte di salita o discesa del clock (fase del clock, CPHA) così come lo stato “attivo” del clock stesso può essere impostato alto oppure basso (polarità del clock, CPOL).
In pratica, per ogni impulso di clock scandito dal master, lo slave fa due operazioni :
Tutto ciò rende l’SPI un bus full duplex in modo che la trasmissione e ricezione possano avvenire in contemporanea; tipicamente però molti dispositivi (slave) lavorano in modalità half duplex.
Questo tipo di architettura permette di implementare i dispositivi SPI con un semplice shift register al proprio interno. In corrispondenza di ciascun colpo di clock in caso di ricezione, il bit letto sulla linea MOSI viene trasferito nel registro e poi shiftato; al contrario, in caso di trasmissione, ad ogni colpo di clock viene eseguito uno shift del registro ed il bit viene impostato sulla linea MISO.
Il segnale SS è utilizzato dal master per poter attivare lo slave con il quale iniziare una sessione di comunicazione. Infatti, il bus SPI è pensato per avere un solo master ed uno o più slave, ciascuno dei quali può essere attivato con un segnale dedicato. Tipicamente il segnale SS è alto quando lo slave è disconnesso dal bus ma viene impostato al valore basso (attivo basso) dal master, quando quest’ultimo vuole comunicare con uno specifico slave. Al termine della comunicazione, il segnale viene riportato al valore alto.
Si evince che è necessario un SS per ciascuno slave presente sul bus e questo comporta la necessità di un numero crescente di pin sul master all’aumentare dei device connessi. In molti casi questa soluzione non è praticabile e si utilizza la connessione a cascata “daisy chain”, sfruttando un solo SS per tutti gli slave che però sono collegati tra loro attraverso le linee dati (il MISO di uno slave va nel MOSI dello slave successivo).
In questa modalità, il dato trasmesso dal master viene propagato in cascata a tutti gli slave in colpi di clock successivi, ciò vuol dire che se abbiamo N slave, sono necessarie N sequenze di 8 impulsi di clock per poter trasferire un intero byte su tutti gli slave.
.Net Micro Framework : le classi per utilizzare il bus
il .Net Micro Framework, secondo la logica di astrazione che lo caratterizza, mette a disposizione la classe SPI (namespace Microsoft.SPOT.Hardware, assembly Microsoft.SPOT.Hardware.dll) per poter utilizzare questa tipologia di bus con un qualsiasi dispositivo che lo supporta. Per poter iniziare ad utilizzare questa funzionalità è necessario configurare la porta SPI da utilizzare attraverso la classe innestata SPI.Configuration; tale configurazione permette di impostare :
-
ChipSelect_Port : il pin (enumerativo Cpu.Pin) che sarà utilizzato come SS (Slave Select). E’ possibile impostare il valore GPIO_NONE se si preferisce pilotare questo pin direttamente senza lasciare l’onere alla classe SPI;
-
ChipSelect_ActiveState : lo stato attivo del chip select. Tipicamente i device SPI hanno un chip select “attivo basso”, ossia è necessario impostare il livello logico 0 (false) per attivare e comunicare con il device;
-
ChipSelect_SetupTime : è il tempo che deve intercorrere dall’istante in cui viene attivato il chip select ed il segnale di clock viene trasmesso sulla relativa linea. E’ un parametro strettamente legato al device con cui si comunica (vedi datasheet), perchè è il tempo che impiega il device per “accorgersi” che è stato attivato e che il master vuole parlare con lui;
-
ChipSelect_HoldTime : è il tempo che deve intercorrere tra la fine della transazione di lettura/scrittura e l’istante in cui il chip select viene disattivo. In pratica, serve a far completare allo slave l’operazione per poi essere disattivato (anche in questo caso dipende dal device e va ricercato nel datasheet);
-
Clock_IdleState : indica la condizione di idle del clock presente sulla linea quando lo slave non è stato attivato; è tipicamento noto come polarità del clock;
-
Clock_Edge : indica il fronte di salita o discesa in corrispondenza del quale il dato sulla linea di comunicazione (MISO o MOSI) viene campionato; è tipicamente noto come fase del clock;
-
Clock_Rate : è la frequenza del clock;
-
SPI_mod : rappresenta l’enumerativo SPI.SPI_module che indica la porta SPI fisica del processore da adottare;
Tutti i parametri di configurazione suddetti sono sempre strettamente legati al device con il quale si intende comunicare e vanno ricercati all’interno del datasheet. Per quanto riguarda il parametro SPI_mod, va invece ricercato nella documentazione del master (tipicamente la CPU della nostra board) per individuare in che modo l’OEM ha esposto le porte SPI disponibili attraverso l’HAL del .Net Micro Framework.
Un’istanza della classe SPI.Configuration va passata come parametro al costruttore della classe SPI per poter iniziare subito ad utilizzare il bus.
SPI.SPI_module spiModule = SPI.SPI_module.SPI1;
SPI.Configuration spiCfg = new SPI.Configuration(Cpu.Pin.GPIO_NONE, // chip select pin
SPI_CS_ACTIVE_STATE, // chip select active state
SPI_CS_SETUP_TIME, // chip select setup time
SPI_CS_HOLD_TIME, // chip select hold time
SPI_CLK_IDLE_STATE, // clock idle state
SPI_CLK_EDGE, // clock edge
SPI_CLK_RATE, // clock rate (Khz)
spiModule); // spi module used
SPI spi = new SPI(spiCfg);
OutputPort nssPort = new OutputPort(Cpu.Pin.GPIO_Pin0, true);
Nel codice riportato in alto, si preferisce gestire il segnale SS in maniera autonoma mediante l’uso di una OutputPort per muovere un pin corrispondente.
Una volta disponibile un’istanza della classe SPI, i metodi principali utilizzabili sono solo due :
-
Write() : permette di eseguire un trasferimento dati dal master allo slave. Fornisce due overload per permettere l’operazione a blocchi di 8 o 16 bit (un array di byte o ushort);
-
WriteRead() : permette di eseguire un trasferimento dati dal master allo slave e viceversa. Tale operazione avviene in contemporanea essendo l’SPI full duplex; anche in questo caso è possibile trasferire blocchi da 8 o 16 bit;
Il metodo di Write() è concettualmente semplice, in quanto la classe si fa carico di muovere il segnale di clock trasferendo sulla linea i dati nell’array che riceve come parametro. Per il metodo di WriteRead() bisogna fare una precisazione : l’array (inizialmente vuoto) in cui verranno messi i dati ricevuti dallo slave deve avere la stessa dimensione dell’array che contiene i dati da trasmettere. Questa uguaglianza è necessaria per la caratteristica intrinseca del bus SPI sul quale ad ogni colpo di clock viene trasmesso un bit del buffer di invio e viene acquisito un bit per il buffer di ricezione.
byte[] write = new byte[CMD_SIZE];
// prepare write buffer ...
// send frame
nssPort.Write(false);
spi.Write(write);
nssPort.Write(true);
Nell’esempio precedente, il segnale di SS viene impostato a false prima di eseguire laWrite() tramite la classe SPI in modo da abilitare lo slave a ricevere i dati; viene riportato a true al termine della trasmissione.
Osserviamo che non esiste un metodo di Read() ! Come mai ? Se volessimo solo leggere dallo slave senza dover inviare nulla. Tipicamente i device SPI prevedono sempre un comando da dover trasmettere per poi iniziare a ricevere dei dati, quindi nella maggior parte dei casi ci ritroviamo a dover usare la WriteRead(). E’ pur vero, però, che lo slave deve prima ricevere il comando per poterlo analizzare, eseguire l’operazione e rispondere con un dato, per cui è impossibile che in corrispondenza dei colpi di clock di trasmissione del comando, il master inizia a ricevere anche la risposta. In moltissimi casi, il metodo di Write()viene usato per trasmettere il comando e viene seguito da un WriteRead() per leggere la risposta. In quest’ultimo caso, cosa dobbiamo scrivere sul bus se siamo solo interessati a ricevere ? Ebbene la risposta è semplice … inviamo dei “dummy” bytes ! In pratica, utilizziamo la WriteRead() per leggere un dato dallo slave grazie al fatto che la classe SPI genera in automatico i colpi di clock per la ricezione stessa; non dovendo trasmettere niente, impostiamo la linea MOSI in uno stato di idle (alta o bassa, usando i byte 0xFF o 0X00) oppure con un “dummy” byte qualsiasi, purché dal datasheet del device quest’ultimo non dia “fastidio” allo slave.
// dummy bytes from master to force clock a reading from slave
byte[] write = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
byte[] read = new byte[5];
nssPort.Write(false);
// write dummy bytes to read data
spi.WriteRead(write, read);
nssPort.Write(true);
Uno sguardo verso il basso : lo strato di HAL
Secondo l’architettura del .Net Micro Framework, ciascun OEM deve implementare uno strato HAL (e PAL) che faccia da “ponte” tra la parte CLR (fino al codice managed ad alto livello) ed il particolare hardware sottostante.
Prendiamo come riferimento la board Netduino (generazione 1) che ha come microcontrollore un Atmel AT91. Scaricando i sorgenti del firmware (sono open source) dalsito ufficiale, possiamo individuare l’implementazione in codice managed C# della classe SPI(Framework\Core\Native_Hardware\SPI.cs) all’interno della quale viene invocato il metodoGetSpiPins() sull’istanza corrente dell’HardwareProvider; fornendo l’identificativo dell’SPI module, tale metodo ritorna i pin relativi all’SCK, MISO e MOSI. Qualora avessimo specificato un pin per il chip select nel costruttore, esso crea anche una istanza OutputPort per quet’ulitmo (è in pratica l’operazione che faremmo noi qualora volessimo pilotare l’SS in autonomia e passassimo GPIO_NONE al costruttore dell’SPI.
1: public SPI(Configuration config)
2: {
3: HardwareProvider hwProvider = HardwareProvider.HwProvider;
4:
5: if (hwProvider != null)
6: {
7: Cpu.Pin msk;
8: Cpu.Pin miso;
9: Cpu.Pin mosi;
10:
11: hwProvider.GetSpiPins(config.SPI_mod, out msk, out miso, out mosi);
12:
13: if (msk != Cpu.Pin.GPIO_NONE)
14: {
15: Port.ReservePin(msk, true);
16: }
17:
18: if (miso != Cpu.Pin.GPIO_NONE)
19: {
20: Port.ReservePin(miso, true);
21: }
22:
23: if (mosi != Cpu.Pin.GPIO_NONE)
24: {
25: Port.ReservePin(mosi, true);
26: }
27: }
28:
29: if (config.ChipSelect_Port != Cpu.Pin.GPIO_NONE)
30: {
31: m_cs = new OutputPort(config.ChipSelect_Port, !config.ChipSelect_ActiveState);
32: }
33:
34: m_config = config;
35: m_disposed = false;
36: }
Dopo una serie di invocazioni a cascata attraverso il CLR fino all’implementazione dell’HAL, sarà invocato il metodo GetPins() sulla classe AT91_SPI_Driver(DeviceCode\Targets\Native\AT91\DeviceCode\AT91_SPI\AT91__SPI.cpp) che ritorna gli identificativi dei pin del processore associati alla porta SPI richiesta. Si osserva che il Netduino permette di utilizzare solo la porta indicata con 0, associata ai pin digitali 11, 12 e 13 rispettivamente per MOSI, MISO e SCLK.
void AT91_SPI_Driver::GetPins(UINT32 spi_mod, GPIO_PIN &msk, GPIO_PIN &miso, GPIO_PIN &mosi)
{
NATIVE_PROFILE_HAL_PROCESSOR_SPI();
switch(spi_mod)
{
case 0:
msk = AT91_SPI0_SCLK;
miso = AT91_SPI0_MISO;
mosi = AT91_SPI0_MOSI;
break;
#if (AT91C_MAX_SPI == 2)
case 1:
msk = AT91_SPI1_SCLK;
miso = AT91_SPI1_MISO;
mosi = AT91_SPI1_MOSI;
break;
#endif
default:
break;
}
}
Nel caso della board Netduino (generazione 2) che ha un processore STM32, la funzione di lettura dei pin SPI è CPU_SPI_GetPins()(DeviceCode\Targets\Native\STM32\DeviceCode\STM32_SPI\STM32_SPI_functions.cpp) che viene invocata ogni qual volta si avvia e ferma una trasmissione con lo slave.
void CPU_SPI_GetPins( UINT32 spi_mod, GPIO_PIN& msk, GPIO_PIN& miso, GPIO_PIN& mosi )
{
NATIVE_PROFILE_HAL_PROCESSOR_SPI();
if (spi_mod == 0) {
#if defined(PLATFORM_ARM_Netduino2) || defined(PLATFORM_ARM_NetduinoPlus2) || defined(PLATFORM_ARM_NetduinoShieldBase)
msk = SPI2_SCLK_Pin;
miso = SPI2_MISO_Pin;
mosi = SPI2_MOSI_Pin;
#else
msk = SPI1_SCLK_Pin;
miso = SPI1_MISO_Pin;
mosi = SPI1_MOSI_Pin;
#endif
} else if (spi_mod == 1) {
#if defined(PLATFORM_ARM_Netduino2) || defined(PLATFORM_ARM_NetduinoPlus2) || defined(PLATFORM_ARM_NetduinoShieldBase)
msk = SPI1_SCLK_Pin;
miso = SPI1_MISO_Pin;
mosi = SPI1_MOSI_Pin;
#else
msk = SPI2_SCLK_Pin;
miso = SPI2_MISO_Pin;
mosi = SPI2_MOSI_Pin;
#endif
} else {
msk = SPI3_SCLK_Pin;
miso = SPI3_MISO_Pin;
mosi = SPI3_MOSI_Pin;
}
}
Un aspetto importante da sottolineare è che l’SPI del Netduino shifta e trasmette i dati in uscita nella modalità MSB first, ossia inizia la trasmissione dal bit più significativo (Most Significant Bit). Nel caso in cui il device si aspetta di ricevere i bit nell’ordine opposto (LSB, Least Significant Bit), è necessario invertire l’ordine dei bit prima di avviare la trasmissione.
Conclusione
Il bus SPI ha il vantaggio di essere full duplex ma soprattutto di lavorare a velocità elevatissime. Inoltre, l’implementazione hardware di un device che lo supporti è relativamente semplice. Gli svantaggi principali sono la necessità di più pin (per gli SS) e l’assenza di un controllo di flusso hardware oltre che di un acknoweledge dallo slave (che va implementato a livello software e di protocollo superiore).
Il .Net Micro Framework permette di utilizzare questo bus con estrema semplicità con una sola classe e due metodi principali. In questo modo, è possibile realizzare un managed driver per poter comunicare con un qualsiasi dispositivo SPI.
Molto preso scriverò un post su un managed driver che sto sviluppando per un chip NFC della NXP che utilizza il bus SPI (oltre che supportare I2C e HSU), per toccare con mano le potenzialità di sviluppo di questo splendido framework !