Questa pagina l'ho scritta molto tempo fa e contiene materiale obsoleto
L'uso è sconsigliato e non posso più fornire alcun supporto
In questa pagina è mostrato come scrivere codice C che utilizza SPI attraverso le userspace API presenti in linux/spi/spidev.h.
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.
In questa pagina potete trovare altre informazioni su SPI.
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:
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.
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):
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.
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_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_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_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.
Complessivamente i bit disponibili in mode sono 16, ma non tutti sono effettivamente utilizzabili con Raspberry Pi. Tra di essi penso sia opportuno citare:
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:
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.
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);
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. Prestare attenzione al fatto che l'uso di questo componente è sconsigliato, per un problema di temporizzazione hardware descritto più sotto.
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)
Un aspetto assai critico del MAX1240 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).
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.
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).
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.
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.
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:
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. Questi 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 ksps teorici permessi dal convertitore.
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.
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).
Alcune osservazioni:
Data di creazione di questa pagina: luglio 2013
Ultima modifica: 31 marzo 2018
Raspberry Pi: note di hardware - Versione 1.31 - Luglio
2019
Copyright 2013-2019, Vincenzo Villa (https://www.vincenzov.net)
Quest'opera è stata rilasciata con licenza Creative Commons | Attribuzione-Condividi allo stesso modo 3.0 Unported.