SPI su Arduino: dalla libreria standard ai registri hardware, R3 contro R4

Sull’Alex80 Micro, lo Z80 fatto rivivere con Arduino, la memoria esterna e i bus expander parlano tutti la stessa lingua: SPI. È uno standard che avevamo già utilizzato senza però averlo mai affrontato nel dettaglio, a differenza di quanto fatto in passato con l’I2C. Il confronto tra i due, in realtà, ha senso proprio perché nascono per scopi simili ma con scelte progettuali molto diverse, ed è da lì che parte questo episodio.

Oltre alla teoria, il video serve anche a preparare il terreno per qualcosa di più concreto: capire come andare oltre la libreria standard di Arduino e lavorare direttamente sui registri hardware, per ottenere una comunicazione più performante con memoria e bus expander. Un’esigenza nata anche da uno spunto arrivato da chi segue il canale, di cui si parlerà più diffusamente in un prossimo video.

Cos’è SPI e perché confrontarlo con I2C

SPI, Serial Peripheral Interface, nasce negli anni ’80 per iniziativa di Motorola, pensato per i suoi processori e poi per i microcontrollori. Il paragone con l’I2C, ideato da Philips, torna utile perché entrambi appartengono alla famiglia della comunicazione seriale sincrona: i dati viaggiano un bit alla volta su un’unica linea, a differenza della comunicazione parallela dove più bit procedono contemporaneamente su linee distinte.

A differenza dell’I2C, SPI non è mai stato formalizzato in uno standard vero e proprio: è diventato uno standard di fatto, consolidato dall’uso più che da un documento ufficiale. Questo si traduce in una comunicazione decisamente più veloce rispetto all’I2C, ed è proprio il motivo per cui è stato scelto al posto dell’I2C sull’Alex80 Micro.

Un’altra differenza sostanziale rispetto ai protocolli asincroni come l’RS232 è la presenza di una linea di clock dedicata. Nell’asincrono, senza un riferimento di clock condiviso, il ricevitore deve dedurre inizio e fine dei dati dal modo in cui vengono inviati, ed è per questo che si parla di baud rate e di un accordo preventivo sulla velocità di trasmissione. SPI, essendo sincrono, non ha questo problema: la linea di clock scandisce direttamente il flusso dei dati, ed è inoltre full duplex, quindi capace di inviare e ricevere contemporaneamente.

Segnali, ruoli e il modello a shift register

Il protocollo prevede un dispositivo master e uno o più dispositivi slave, con il master che comanda la comunicazione. Non è previsto un indirizzamento come nell’I2C: al suo posto, ogni slave ha bisogno di un segnale fisico dedicato di attivazione, lo Slave Select (o Chip Select, a seconda del dispositivo). Questo significa che il master deve dedicare un pin per ogni dispositivo secondario che vuole controllare, un costo in termini di I/O che cresce con il numero di periferiche collegate.

I segnali fondamentali di SPI sono quindi quattro: SS/CS per la selezione dello slave, MOSI (Master Out Slave In) per i dati che vanno dal master allo slave, MISO (Master In Slave Out) per la direzione opposta, e SCLK, il clock di sincronizzazione generato dal master.

Dal punto di vista circuitale, la rappresentazione più immediata è quella di due shift register collegati insieme, uno nel master e uno nello slave, che si scambiano i bit in sincronia con il clock. È una semplificazione, perché i dispositivi reali possono avere logiche interne più articolate, ma concettualmente rende bene l’idea: a ogni impulso di clock un bit esce dal master verso lo slave e, contemporaneamente, un bit esce dallo slave verso il master. Non esiste quindi una vera distinzione tra “chi invia” e “chi riceve”: i due flussi avvengono sempre insieme, anche quando all’atto pratico interessa solo uno dei due, come quando si scrive un indirizzo in una memoria e si scarta semplicemente ciò che arriva in risposta. La dimensione tipica del registro è di un byte, anche se alcuni microcontrollori supportano trasferimenti a 16 o 32 bit, purché master e slave siano d’accordo su quanti bit si stanno scambiando.

Le quattro modalità SPI: CPOL e CPHA

Non essendoci uno standard formale, i dettagli di temporizzazione vengono definiti da due parametri, CPOL (clock polarity) e CPHA (clock phase), che insieme danno luogo a quattro modalità operative possibili: stabiliscono se il fronte attivo del clock è quello di salita o di discesa e se il campionamento del dato avviene sull’uno o sull’altro.

Arduino, come molti microcontrollori, usa di default il modo 0, in cui entrambi i parametri sono a zero: il fronte di discesa del clock è quello su cui viene posizionato il bit da trasmettere (a partire dal più significativo), mentre il fronte di salita è quello di campionamento, il momento in cui il dato viene effettivamente letto e fatto scorrere nel registro interno. La sequenza tipica prevede quindi che il master porti basso il segnale di slave select, avvii il clock, e a ogni ciclo faccia avanzare un bit fino a esaurire l’intero byte (o i bit previsti), per poi fermare il clock e rilasciare lo slave select, chiudendo la transazione.

Frequenze di clock e valori elettrici: uno standard di fatto

Non essendo mai stato formalizzato, SPI non impone nemmeno range di frequenza obbligatori: qualunque velocità supportata dal dispositivo va bene, dato che il clock viaggia sulla stessa linea e sincronizza automaticamente master e slave. Nella pratica, però, si sono consolidati alcuni intervalli tipici: da 100 kHz a 4 MHz per sensori come quelli di temperatura e umidità, che non hanno bisogno di aggiornamenti troppo frequenti; da 8 MHz a 25 MHz per display TFT o LCD grafici, convertitori analogico-digitali e memorie EEPROM; oltre 30 MHz, fino a superare gli 80 MHz, per memorie flash esterne e moduli Ethernet veloci, dove servono microcontrollori altrettanto veloci per stare dietro al flusso di dati in arrivo.

Anche sui livelli elettrici non esiste un valore prescritto: i più comuni sono 5V, 3,3V e 1,8V, con il 3,6V un po’ meno diffuso. L’unico vincolo pratico è che i dispositivi collegati condividano la stessa tensione operativa, come nel caso della memoria seriale usata sull’Alex80 Micro, alimentata anch’essa a 5V come il microcontrollore.

Il banco di prova: Arduino R3 e una memoria SRAM seriale

Per vedere il protocollo all’opera si parte da un circuito volutamente semplice su breadboard: un Arduino R3 collegato a una memoria SRAM seriale da 1 megabit, la stessa tipologia già utilizzata nell’implementazione dell’Alex80 Micro. Arduino mette a disposizione un’interfaccia SPI hardware sui pin standard: MOSI su D11, MISO su D12, SCK su D13, con il chip select tipicamente su D10, anche se non è un vincolo stretto, come dimostra il fatto che sullo schema elettrico dell’Alex80 Micro i chip select di memoria e bus expander sono su pin diversi.

Sul lato della memoria, oltre ai segnali SPI, compaiono alcune pull-up: sul chip select, sul segnale di hold (specifico della memoria e non parte dello standard SPI) e sul clock, tutte da 10 kOhm. Con i cavetti tenuti corti, coerentemente con un protocollo che rende meglio su collegamenti brevi, il circuito prende forma punto per punto: alimentazione e massa, poi MOSI, MISO, SCK e chip select collegati ai rispettivi pin di Arduino.

La libreria SPI standard in azione

Il codice che usa la libreria SPI di Arduino segue uno schema ricorrente: si configura il pin di chip select come uscita e lo si porta alto (il chip select è attivo basso), si chiama SPI.begin, poi per ogni transazione si apre un blocco con beginTransaction, specificando velocità di clock, ordine dei bit (MSB first, coerente con la convenzione più diffusa) e modalità SPI, si esegue SPI.transfer passando il byte da inviare e ricevendo contestualmente il byte restituito dallo slave, e infine si chiude con endTransaction.

Con la memoria utilizzata nel test, l’intera sequenza di lettura o scrittura deve stare dentro un’unica transazione: è un vincolo del dispositivo, non del protocollo in generale, e interrompere il flusso a metà manda in confusione la memoria. Per una lettura si invia il comando corrispondente seguito dai due byte di indirizzo, poi un byte qualsiasi (che verrà scartato dalla memoria) per far uscire il dato letto; per una scrittura si invia il comando di scrittura, l’indirizzo, e infine il byte da memorizzare, ignorando quello che torna indietro. Il test conferma il comportamento atteso: al primo avvio la memoria RAM restituisce un valore casuale, come è normale per una RAM priva di alimentazione precedente, mentre dopo una scrittura la lettura successiva restituisce esattamente il dato appena scritto.

Guardare dentro il bus con l’oscilloscopio

Per verificare che tutto avvenga davvero come previsto dal protocollo, entra in scena un oscilloscopio Rigol a quattro canali, con funzione di decodifica SPI integrata: un canale per il clock, uno per MISO, uno per MOSI e uno per il chip select, con soglie di trigger impostate a metà della tensione di alimentazione e il trigger sul fronte di discesa del chip select.

Abilitando la tabella degli eventi decodificati, il traffico sul bus diventa leggibile riga per riga. Con la memoria usata nel test, il comando 0x03 avvia una lettura, seguito dai byte di indirizzo e dal dato di ritorno; il comando 0x02 avvia una scrittura, seguito da indirizzo e dato da scrivere. Alla frequenza di 2 MHz usata inizialmente su breadboard compaiono occasionalmente delle letture “sporche” al primo avvio, probabilmente dovute ai cavetti non schermati; abbassando la frequenza a 200 kHz, pensata come velocità di solo test, la cattura risulta più pulita, a conferma che il cablaggio volante su breadboard non è la condizione ideale per le frequenze più alte del protocollo.

I limiti della libreria standard

La libreria SPI di Arduino ha il pregio di funzionare in modo identico su piattaforme molto diverse, dal Mega al Giga, da R3 a R4, ma questa portabilità ha un costo: internamente deve gestire l’astrazione tra hardware differenti, e ogni beginTransaction ricalcola i divisori di clock necessari per ottenere la frequenza richiesta, anche quando quella frequenza non è cambiata rispetto alla chiamata precedente.

Per un uso generico questo overhead è del tutto trascurabile, ma diventa un problema quando si spinge il protocollo ai limiti delle tempistiche, come accade sull’Alex80 Micro: lì Arduino funge da memoria per lo Z80 e pilota anche i bus expander che generano i segnali del microprocessore, un contesto in cui ogni microsecondo risparmiato si traduce in un clock più alto per lo Z80 emulato. In una sequenza di letture o scritture consecutive sulla stessa memoria, ripetere beginTransaction ed endTransaction per ogni singolo byte significa pagare più volte un costo che potrebbe essere sostenuto una sola volta, gestendo l’inizializzazione del clock a mano e lasciando poi che sia il codice applicativo a occuparsi soltanto dell’invio e della ricezione dei byte.

Accesso diretto ai registri su Arduino R3

La strada per superare questo limite passa dai registri hardware dell’SPI, sempre presenti dietro la libreria ma normalmente nascosti. Su Arduino R3, basato su ATmega328P, sono tre: SPCR (SPI Control Register), che permette tra le altre cose di impostare i divisori di clock a partire dal clock di sistema; SPSR, il registro di stato, il cui bit SPIF segnala quando una transazione è terminata; e SPDR, il registro dati, che funge sia da buffer di scrittura che di lettura.

Il meccanismo è diretto: si scrive il byte da trasmettere in SPDR, il che avvia automaticamente il trasferimento sfruttando lo shift register interno, e si attende che il bit SPIF in SPSR si alzi per segnalare che l’operazione è conclusa, prima di poter scrivere un nuovo byte o leggere quello ricevuto (sempre in SPDR). Scrivere un nuovo dato prima che la trasmissione precedente sia terminata comprometterebbe la comunicazione in corso. Il codice, riscritto per la lettura e la scrittura sulla memoria usando esclusivamente questi registri (mantenendo comunque SPI.beginTransaction solo per impostare i divisori di clock, un’operazione che tipicamente si fa una tantum), riproduce esattamente lo stesso comportamento osservato con la libreria standard, confermato anche da una nuova cattura all’oscilloscopio che mostra la stessa sequenza di comandi sul bus.

Arduino R4: stessa logica, registri diversi

Il passaggio a Arduino R4 introduce una complicazione in più: il microcontrollore a bordo è completamente diverso da quello della famiglia ATmega, con un set di registri proprio, anche se l’IDE e la libreria standard mascherano in buona parte questa differenza a chi usa le funzioni di alto livello.

Lavorando a basso livello, invece, la differenza emerge chiaramente. R4 espone i suoi periferici SPI attraverso una struttura dedicata, con più interfacce hardware disponibili (a differenza di R3, che ne ha una sola): nel test si usa la SPI0, quella che corrisponde ai pin standard già usati su R3. All’interno di questa struttura si trovano ancora un registro SPCR e un registro SPSR, ma con bit di stato più articolati, che segnalano separatamente se il buffer di trasmissione è vuoto e pronto per un nuovo byte, se è stato ricevuto un dato, e se l’interfaccia SPI è complessivamente in stato di idle. Il registro dati, in questo caso, è accessibile nella variante a byte, SPDR_BY, perché l’hardware di R4 supporta anche trasferimenti a più di 8 bit, mentre l’applicazione con la memoria seriale ha bisogno solo della versione a byte.

Il codice per R4 replica la stessa logica vista su R3, adattata a questi registri: si scrive il dato in SPDR_BY, si attende che il buffer sia pronto, e si legge il valore ricevuto svuotando comunque il buffer di ricezione anche quando non interessa, per evitare un overflow. Sostituendo la shield di test con quella dell’Alex80 Micro, che integra la stessa memoria oltre ai due bus expander (qui non utilizzati), il collaudo con questo codice conferma il corretto funzionamento anche alla frequenza massima supportata dalla memoria, 2 MHz, con le stesse sequenze di lettura e scrittura osservate in precedenza.

Verso una libreria SPI su misura per l’Alex80 Micro

Questo episodio è servito soprattutto a mettere le basi: capire a fondo il protocollo SPI, confrontarlo con l’I2C già visto in precedenza, e verificare che l’accesso diretto ai registri hardware funzioni allo stesso modo sia su Arduino R3 che su R4, pur partendo da microcontrollori con registri completamente diversi. Il vantaggio più concreto arriverà in un prossimo video, dove questi stessi registri verranno usati per costruire una libreria pensata specificamente per l’Alex80 Micro, capace di ridurre gli overhead della libreria standard e di gestire con efficienza sia la memoria che i bus expander condivisi sul bus SPI.

Per vedere il montaggio del circuito su breadboard, l’analisi completa con l’oscilloscopio e tutto il codice sia per R3 che per R4, il video di questa settimana è disponibile sul canale.

Avatar Paolo Godino