Salta la barra di navigazione [1] - Vai alla barra di navigazione [3] - Scrivimi una mail [4]

Rapsberry Pi: SPI in C

Raspberry Pi - Note di hardware - Sommario - Novità - Tutorial - Progetti - Risorse - Non solo elettronica

Il bus SPI è già stato introdotto usando Bash. In questa pagina verrà presentato il codice C per gestire una periferica SPI utilizzando direttamente le funzioni messe a disposizione dal driver standard di Raspbian.

Spi in C

Prima di proseguire è bene verificare che i driver siano stati caricati ed i permessi di accesso assegnati correttamente all'utente che si intende usare, come descritto in questa pagina. E' infatti decisamente sconsigliato sia eseguire il programma come root sia usare trucchi "sporchi" come suid-are il file.

I pin utilizzati dal bus SPI sono disponibili sul connettore GPIO.

Loopback

Il primo codice presentato non richiede il collegamento di alcun hardware: semplicemente è necessario cortocircuitare i segnali MOSI e MISO direttamente sul connettore GPIO (pin 19 e 21);  in questo modo il segnale trasmesso dal Raspberry Pi viene anche ricevuto, senza alcuna modifica.

Il codice non fa altro che scrivere sul bus SPI una sequenza fissa di 8 byte, rileggerla in modalità full-duplex e stamparla:

vv@vvrpi ~ $ ./spi
Data from SPI MISO: 01 02 04 08 55 00 AA FF

Per compilare il codice:

vv@vvrpi ~ $ gcc spi.c -std=c99 -o spi

Ovviamente non è un fake: togliendo il cortocircuito tra MOSI e MISO l'ultima riga apparirà come una sequenza di zeri.

L'immagine seguente mostra i segnali generati (e ricevuti) dal Raspberry PI durante l'esecuzione del programma:

Forme d'onda SPI

Il codice è piuttosto semplice: una serie di chiamate al sistema di Input/output (ioctl) permettono di specificare i vari parametri e di leggere o scrivere i buffer di trasmissione e ricezione. Per la documentazione completa potete fare riferimento alla pagina di documentazione del kernel Linux oppure consultare il file /usr/include/linux/spi/spidev.h presente su Raspberry Pi e in genere su tutti i sistemi linux recenti.

spi_ioc_transfer

La struttura di tipo spi_ioc_transfer contiene i campi che descrivono un singolo trasferimento SPI, in genere contemporaneamente di lettura e scrittura (full duplex):

fd = open(device, O_RDWR)

La funzione fd = open(device, O_RDWR) è analoga all'apertura di un normale file, come anche la close(fd) finale. L'unica differenza deriva dal fatto che questo "file" corrisponde ad un dispositivo fisico.

ioctl(fd, SPI_IOC_WR_MODE, &mode)

La funzione ioctl(fd, SPI_IOC_WR_MODE, &mode) imposta il modo di funzionamento della periferica. Il parametro mode è formato da una serie di bit che determinano aspetti importanti del segnale generato.

In particolare sono da evidenziare i due bit (SPI_CPHA e SPI_CPOL) che determinano la relazione tra il clock (fronte attivo, valore iniziale) ed il dato, definendo quattro modi di funzionamento di SPI. Più che la descrizione, è utile osservare i grafici per poi confrontarli con quanto presente nel foglio tecnico del componente che si intende utilizzare.

SPI_MODE_0 (0|0)

Il clock (in blu) inizialmente è basso. Il dato (ocra) deve essere campionato in corrispondenza del fronte di salita e quindi vale, partendo da sinistra: 0 1 0 1 0 0 1 1 (0x53). Questa è probabilmente la modalità più diffusa ed è quella predefinita di Raspberry Pi.

SPI modo 0

SPI_MODE_1 (0|SPI_CPHA)

Il dato viene campionato in corrispondenza del fronte di discesa. Anche in questo caso il clock inizialmente è basso. Il byte trasmesso è lo stesso dell'esempio precedente.

SPI mode 1

SPI_MODE_2 (SPI_CPOL|0)

Il dato viene campionato in corrispondenza del fronte di discesa, ma in questo caso il clock inizialmente è alto. Il byte trasmesso è lo stesso dell'esempio precedente.

SPI mode 2

SPI_MODE_3 (SPI_CPOL|SPI_CPHA)

Il dato viene campionato in corrispondenza del fronte di salita; in questo caso il clock inizialmente è alto. Il byte trasmesso è lo stesso dell'esempio precedente.

SPI mode 4

Complessivamente i bit disponibili in mode sono 16, ma non tutti sono effettivamente utilizzabili con Raspberry Pi. Tra di essi penso sia opportuno citare:

ioctl(fd, SPI_IOC_MESSAGE(1), &tr)

La funzione ioctl(fd, SPI_IOC_MESSAGE(N), &tr) effettua la trasmissione e contemporaneamente la ricezione di uno o più buffer, ciascuno contenuto nella già descritta struttura spi_ioc_transfer. Per trasmettere in sequenza più pacchetti, anche con caratteristiche diverse, è possibile utilizzare un array di tali strutture.

Tra le opzioni presenti meritano inoltre di essere citate, anche se non utilizzate in questo programma, ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) e ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) che rispettivamente impostano lunghezza di una parola e velocità. A differenza di quanto impostato all'interno di spi_ioc_transfer, si tratta di impostazioni globali e permanenti fino al riavvio.

I tre esempi seguenti mostrano tre scenari tipici:

Un esempio: MCP4822

Il primo programma "vero" mostra l'utilizzo di un convertitore digitale analogico MCP4822, lo stesso già visto nella pagina in cui si utilizza SPI con Bash. Per semplicità non sono stati disegnati i condensatori di disaccoppiamento tra massa e alimentazione, certamente necessari, ma non strettamente indispensabili per le finalità qui illustrate.

Collegare  in DAC a Raspberry Pi

Attenzione! Anche se questo circuito integrato può funzionare senza problemi a 5 V, il Raspberry Pi verrebbe irrimediabilmente danneggiato usando tale tensione di alimentazione (non ho ovviamente fatto verifiche...).

A fondo pagina il codice: occorre fornire da linea di comando le due tensioni ed il programma provvede ad inviare, attraverso due distinte ioctl, i necessari bit al convertitore.

vv@vvrpi ~ $ ./dac 1 0.22
[...]
Sending data to MCP4822 on /dev/spidev0.1
Voltage A: 1.000000 V - Hex value: 0x37D0
Voltage B: 0.220000 V - Hex value: 0xF1B8

Ovviamente per verificare il funzionamento occorre misurare con un multimetro le tensioni tra le uscita OUTA massa oppure tra OUTB e massa; le tensioni visualizzate sulla console sono semplicemente calcolate, il programma funzionerebbe esattamente allo stesso modo anche senza alcun convertitore collegato! Non esiste infatti modo, usando SPI, per sapere se un'operazione di scrittura è andata a buon fine.

Il codice non merita particolari commenti in quanto tutto è già stato descritto nella prima parte di questa pagina. Forse è necessario soffermarsi solo su come viene formato il numero di 16 bit da trasmettere, in particolare l'ultima parte che appare un poco "barocca" (e sicuramente non portabile...).

Innanzitutto occorre trasformare il numero reale voltA, fornito da linea di comando, in un intero di 12 bit. Sapendo che 212=4096 e che VREF corrisponde alla massima tensione di uscita quando tutti i bit sono a 1, il calcolo da fare è:

 dataA = voltA / VREF * 4096;

Occorre quindi (la documentazione completa è nei fogli tecnici):

dataA = (dataA & 0x0FFF) | 0x3000;

Occorre infine scambiare di posto gli 8 bit più significativi con gli otto meno significativi, facendo un doppio shift. Questo deriva dalla convenzione qui usata da ARM per rappresentare i numeri interi.

tx = (dataA << 8) | (dataA >> 8);

Un altro esempio: MAX1240

Questo programma utilizza MAX1240, un ADC a 12 bit. A fondo pagina il codice. Lo schema è riportato qui sotto; è indicato anche il DAC già presentato nel paragrafo precedente, utile per generare una tensione che l'ADC può misurare.

SPI: MCP4822 + MAC1240

Ovviamente per verificare il funzionamento del programma occorre fornire in ingresso al convertitore una tensione tra 0 e 2,5 V, per esempio ricavata con un partitore a partire dalla tensione di alimentazione, oppure dal DAC.

vv@vvrpi ~ $ ./adc
[...]
Reading data from MAX1240 on /dev/spidev0.0
Voltage: 0.993652 V - Hex value: 0x65C - Raw value: 0xE0B2)

L'unico aspetto del codice che vale la pena esaminare riguarda la conversione della lettura in un numero reale. La prima operazione riguarda il "riordino" dei bit.

data = ((rx << 5) | (rx >> 11)) & 0xFFF

Quindi occorre convertire questo numero intero di 12 bit in un numero reale, tenendo conto della tensione di riferimento (2,5 V) e della risoluzione (212=4096).

volt = VREF * data / 4096;

Il programma stampa, oltre alla tensione in volt, sia la variabile data che la variabile rx: non è immediato riconoscere gli stessi bit in entrambe queste ultime stringhe...

Ovviamente è possibile collegare una delle uscite del DAC MCP4822 all'ingresso dell'ADC MAX1240 e verificare così il funzionamento di entrambi i convertitori senza usare strumentazione:

vv@vvrpi ~ $ ./dac 1.3 2; ./adc
[...]
Sending data to MCP4822 on /dev/spidev0.1
Voltage A: 1.300000 V - Hex value: 0x3A28
Voltage B: 2.000000 V - Hex value: 0xFFA0
[...]
Reading data from MAX1240 on /dev/spidev0.0
Voltage: 1.292725 V - Hex value: 0x846 - Raw value: 0x30C2)

La criticità

Un aspetto assai critico di questo convertitore riguarda la temporizzazione: in base ai fogli tecnici dopo l'attivazione del CE sono necessari al convertitore fino a 7,5 microsecondi per completare la conversione; solo dopo il dato è pronto e può essere iniziata la lettura dei bit; nella figura 12 di pagina 13 si osserva inoltre che il primo bit ricevuto deve sempre essere 1 e gli ultimi tre sempre 0.

Qui sotto un tipico diagramma temporale sperimentale: dall'alto il CE (ocra), il clock (blu) e il dato DOUT dall'ADC (verde).

Lettura MAX1240: nessun errore

Purtroppo questi tempi non sono tra le specifiche del Raspberry Pi, quindi non sono garantiti. Per una verifica "a posteriori" nel codice è presente un controllo sul primo bit ricevuto (ottavo da destra nel buffer, a causa rappresentazione dei numeri interi), che deve essere 1 nel caso di funzionamento corretto:

if (!(rx & 0x0080) ) printf ("FAIL: EOC in not 1 \n\n");

Dalle prove effettuate, nel 5% circa dei casi il primo bit ricevuto è 0, cioè la lettura è anticipata rispetto alla EOC. L'effetto negativo sembra "solo" essere una perdita di precisione nella conversione, dell'ordine di qualche LSB; di certo un simile circuito non è utilizzabile in produzione, soprattutto alla luce di quanto illustrato nel prossimo paragrafo.

Le prestazioni

Quando si legge un convertitore è importante sapere quanto tempo è necessario. Nel diagramma temporale sopra riportato si osserva che il tempo complessivo per una lettura è di circa 40 us (tempo peraltro molto variabile) a cui occorre aggiungere il tempo necessario per iniziare una nuova conversione. Scrivendo un loop che richiama continuamente la iocrtl di lettura, sperimentalmente è misurabile una frequenza di campionamento tra 12 e 17 ksps. Purtroppo, oltre che relativamente bassa, è anche enormemente variabile ed irregolare...

Un approccio diverso è quello di passare alla ioctrl non un singolo buffer, ma un insieme di buffer. Un codice esemplificativo è riportato qui sotto. Apparentemente il codice funziona e la frequenza di campionamento sale a circa 35 ksps, divenendo anche abbastanza regolare.

Purtroppo ad una analisi approfondita si osserva che non vengono praticamente mai rispettati i tempi richiesti dal convertitore. In particolare, nel diagramma temporale seguente, si vede che il primo campionamento ha successo, il secondo fallisce in quanto il clock viene inviato da Raspberry Pi prima della fine della conversione, cioè quando la linea DOUT è ancora bassa. Tele errore rimane per tutte le letture successive alla prima (che invece sembra quasi sempre corretta).

Errore di temporizzazione

Nel valore di tensione letto non si rilevano particolari anomalie, ma di sicuro il convertitore non sta funzionando secondo le specifiche.

Al momento questo problema non è risolto: probabilmente MAX1241 non è compatibile con Raspberry Pi.

L'ultimo esempio: MAX146

Questo esempio mostra l'utilizzo di MAX146, un ADC a 12 bit a 8 canali. A fondo pagina il codice esemplificativo. Lo schema è riportato qui sotto.

MAX 146 e Raspberry Pi

vv@vvrpi ~ $ gcc max146.c -std=c99 -o max146; ./max146 1
[...]
Reading data from MAX146 on /dev/spidev0.0
Control byte sent to MAX146: 0xCF
Raw data recived from MAX146: 0x5C 0xB0
Voltage applied to CH1 = 1.810303 V

Per configurare il convertitore, Raspberry Pi deve prima trasmettere un byte di controllo, contenente il canale selezionato ed altre informazioni di configurazione; quindi il convertitore ritorna due byte, contenenti la tensione presente all'ingresso selezionato. I tre byte sono trasmessi/ricevuti in sequenza; è quindi necessario utilizzare due buffer di tre byte ciascuno:

Il codice è piuttosto semplice da esaminare, anche senza ulteriori commenti.

Dal diagramma temporale seguente mostra dall'alto:

Lettura di un campione dal MAX146

Si vede che per leggere un campione sono necessari circa 40 microsecondi, tempo paragonabile a quello richiesto da MAX1240. In questo caso non sono però presenti criticità: tutto è infatti sincronizzato dal clock SPI. Questo 40 us possono essere suddivisi in quattro blocchi:

Il tempo di inattività è quindi molto elevato, circa due terzi del tempo complessivo...

Per verificare le velocità raggiungibili è possibile utilizzare un ciclo che effettua più letture di singoli campioni attraverso più chiamate ioctl oppure un codice che legge più campioni con una sola ioctl, secondo la modalità già descritta per MAX1240.

Le prestazioni ottenute in questo modo sono piuttosto deludenti e raffigurate nel seguente diagramma temporale, relativo alla lettura di 10 campioni con modalità a ioctl singolo. La velocità effettiva è di circa 30 ksps, da confrontare con i 133 kksps teorici permessi dal convertitore.

MAX146 - Lettura di 10 campioni

Il motivo di tale "lentezza" va ovviamente trovato nel lungo tempo che trascorre tra l'attivazione di CE e l'inizio effettivo della trasmissione nonché dal tempo tra la fine della trasmissione e la disattivazione del CE. L'aumento di velocità del convertitore e/o l'aumento di frequenza del clock SPI avrebbe, in questo caso, effetti assolutamente marginali.

Campionamento ad "alta velocità"

MAX146 permette una modalità di campionamento che non utilizza CE. Questa modalità è descritta dalla figura 11b dei fogli tecnici ed in pratica consiste nell'avviare una nuova conversione quando è ancora in corso il trasferimento dei bit della precedente. Questo ha come effetto collaterale, nel nostro caso fondamentale, di non avere tempi di attesa legati al CE.

Il codice esemplificativo permette di leggere le tensioni presenti su tutti gli otto canali, tramite una sola ioctl.

Durante l'esecuzione di test seguente gli otto ingressi sono collegati a 9 resistenze in serie tra massa e VDD, in modo tale che la tensione in ingresso a ciascun canale sia di circa 0,25 V più alta rispetto al precedente.

vv@vvrpi ~ $ ./max146-8
[...]
Reading data from MAX146 on /dev/spidev0.0

TX buffer: 0x8F 0x00 0xCF 0x00 0x9F 0x00 0xDF 0x00 0xAF 0x00 0xEF 0x00 0xBF 0x00 0xFF 0x00 0x00
RX buffer: 0x00 0x69 0xF0 0x5C 0x98 0x4F 0x40 0x41 0xF8 0x34 0xA0 0x27 0x48 0x1A 0x00 0x0C 0x88

Voltage to CH0 = 2.069092 V
Voltage to CH1 = 1.808472 V
Voltage to CH2 = 1.547852 V
Voltage to CH3 = 1.288452 V
Voltage to CH4 = 1.027832 V
Voltage to CH5 = 0.767212 V
Voltage to CH6 = 0.507812 V
Voltage to CH7 = 0.244751 V

Il diagramma temporale relativo alla lettura di otto tensioni è riportato di seguito. Da esso è possibile verificare che una singola conversione richiede qualcosa meno di 10 microsecondi: per la precisione la frequenza di campionamento misurata è 109 ksps, avvicinandosi alle prestazioni teoriche del convertitore (per MAX146 sono 133 ksps).

Lettura di 8 campioni dal MAX146

Alcune osservazioni:

Il codice


Raspberry Pi - Note di hardware

Vedi anche: Il taccuino tecnico


Licenza "Creative Commons" - Attribuzione-Condividi allo stesso modo 3.0 Unported


EN - Pagina principale - Sommario - Accessibilità - Note legali e privacy policy - Posta elettronica

XHTML 1.0 Strict - CSS 3