Utilizzo librerie I2C generate dall’ MPLAB Code Configurator : lettura accelerometro
Ho già parlato in passato di come funziona l’I2C e feci vari esempi, tra cui quello dell’utilizzo di un accelerometro. Dal momento che ora, con MPLABX + MPLAB Code Configurator, le cose sono cambiate un bel po’, vedremo come utilizzare i nuovi strumenti che ci consentiranno di scrivere molto meno codice rispetto a prima. Prima di continuare, dato che come sempre eviterò di scrivere cose già scritte, è assolutamente necessario riprendere i vecchi articoli:
- Il modulo MSSP e la comunicazione I2C
- Come funzionano gli accelerometri
- Interfacciamento accelerometro a PIC12F
- Interfacciamento alle eeprom esterne
In più sul sito ci sono le vecchie librerie I2C che scrissi a suo tempo (per il compilatore Hi-Tech C) e che potrebbero essere ancora utilizzabili modificando leggermente i nomi dei registri e i nomi dei bit. Ricordo difatti che ora, nel momento in cui il microcontrolloer PIC abbia 2 periferiche uguali (es.: due moduli MSSP), questi avranno nel nomel’indice 1 l’indice 2 (in realtà il modulo 1 dovrebbe essere riconosciuto ugualmente anche senza indice), per cui, ad esempio, il registro che prima era chiamato SSPCON1 ora si chiamerà SSP1CON1 (nella nuova versione delle vecchie librerie, questa cosa l’avevo già corretta). Mentre non è più possibile richiamare i bit dei registri con il loro nome singolo, per cui non possiamo più riferirci direttamente, ad esempio, al bit SSP1IF ma dovrà essere scritto PIR1bits.SSP1IF (ovvero nome del registro, seguito da ‘bits’ scritto in minuscolo, punto e nome del bit) – anche questa cosa l’avevo coretta. Detto questo, le vecchie librerie si trovano qui:
- Vecchie librerie I2C hardware (deprecate)
Però il mio consiglio è di non utilizzarle dal momento che erano molto semplici e non tenevano conto del check degli errori restituiti dal modulo. In più non le ho testate con i compilatori XC quindi non ho idea se funzionano. Il mio consiglio è di usare le librerie generate dall’MCC, quelle vecchie possono essere utili per capire come funzionano le sequenza di comunicazione I2C da un microcontrollore PIC, dal momento che librerie nuove fanno la stessa identica cosa ma in maniera più complessa e articolata.
Indice dei contenuti
Includere le librerie I2C da MCC
Illustro qui come inserire le librerie I2C e configurare il modulo MSSP1 per utilizzarlo con l’I2C, tutto il resto l’ho già spiegato. Prima di avviare la configurazione del modulo MSSP è necessario aver settato il clock di sistema nella scheda System Module, altrimenti è inutile continuare dato che gli altri clock sono derivati da questo e se impostate il clock di sistema per ultimo, tutti gli altri settaggi verranno cambiati e non capirete cosa succede se non funziona niente.
Dopo aver avviato l’MCC e settato il clock di sistema, nel riquadro sinistro Device Resources scorriamo verso il basso fino a trovare il modulo MSSP. Clicchiamo sulla freccina accanto ad esso per espanderlo: compariranno MSSP1 e MSSP2. Clicchiamo sul tasto verde + affianco ad MSSP1 per includerlo.
Compare la finestra di impostazioni per il modulo MSSP1. Se selezionato, deselezioniamo la casella Interrupt Driven (perchè le cose sono già complicate con il funzionamento in polling). Nella scheda Hardware Settings in Serial Protocol selezioniamo I2C e Mode : Master. Nel campo I2C Clock Frequency (Hz) scriviamo 400000 (quattrocentomila): in questo modo il bus lavorerà a 400KHz (modalità High Speed consentita dall’ADXL345).
Nella scheda Advanced Settings la voce SM Bus Input Enable deve essere deselezionata, Slew Rate Control va su High Speed (questo resetta il bit SMP del registro SSPxSTAT che abilita il controllo dello Slew Rate) e SDA Hold Time su 100nS (bit SDAHT del registro SSPxCON3).
Le velocità di 100KHz e 1MHz sono invece definite Standard Speed e per queste il controllo dello Slew Rate va messo, appunto, al valore Standard Speed (bit SMP=1, controllo Slew Rate disabilitato).
Nella scheda Interrupt Settings, infine, disabilitare Enable I2C Interrupt.
A me, di default, i pin su cui sbocca il modulo MSSP1 sono RC3 per il segnale di clock SCL1, e RC4 per la linea dati SDA1, controllate nel pin manager anche questo settaggio.
Dopo aver impostato le altre periferiche (nel mio esempio utilizzo la EUSART2 e ho settato i 3 pin per l’utilizzo del MAX7219) possiamo premere il pulsante Generate nel Project Resources. Alla fine possiamo chiudere l’MCC e ci ritroveremo tutto il codice già bello e pronto.
Funzionamento librerie I2C MCC
Le librerie I2C create dal Code Configurator sono implementate con una macchina a stati e sono tutto fuorchè semplici da usare e interpretare. Notiamo innanzitutto che nel file header della libreria generata (i2c1_master.h), che è quella che poi andrebbe inclusa per utilizzare le funzioni, sono dichiarati i prototipi di un numero di funzioni decisamente inferiore a quelle invece presenti nel file principale (i2c1_master.c): questo perchè la maggior parte delle funzioni sono interne, ovvero non vanno utilizzate dal nostro programma finale.
Vediamo che nel file C c’è un’enumerazione chiamata i2c1_fsm_states_t, la sigla FSM sta per Finite State Machine, questa enumerazione contiene delle parole chiave che serviranno ad identificare una singola operazione sul bus I2C. C’è quindi una struttura, chiamata i2c1_status_t, che ha una serie di proprietà, lo stato attuale delle operazioni e una callback table: una tabella che verrà compilata con la sequenza delle operazioni da eseguire ovvero delle singole funzioni da richiamare che andranno ad operare sul bus I2C per realizzare un’operazione completa, come può essere la lettura di un valore da una eeprom esterna.
Giusto perchè vogliamo farci male vediamo ad esempio di seguire come viene sviluppata la funzione I2C1_MasterRead definita nell’header, che fa una cosa sola: inviare sul bus l’indirizzo del dispositivo con cui vogliamo comunicare in lettura (e basta). Dal file C vediamo che questa funzione richiama I2C1_MasterOperation passandogli true come parametro: la funzione I2C1_MasterOperation si occupa della lettura se il parametro passato è true e della scrittura se il parametro è false. In caso di lettura viene impostato I2C1_Status.state = I2C1_SEND_ADR_READ ovvero dice alla macchina a stati: “lo stato attuale è l’invio dell’indirizzo in modalità lettura”, dopodichè vengono richiamate, una dopo l’altra, le funzioni I2C1_MasterStart() e I2C1_Poller().
I2C1_MasterStart() esegue semplicemente questa funzione:
SSP1CON2bits.RCEN = 1; |
che sappiamo già (se avete letto gli articoli citati all’inizio) che abilita la lettura (Receive Enable) sul bus I2C. La successiva funzione I2C1_Poller richiama, una dopo l’altra, le funzioni I2C1_MasterWaitForEvent() e I2C1_MasterFsm().
I2C1_MasterWaitForEvent() tiene in attesa aspettando che si liberi il bus controllando il flag di interrupt (PIR3bits.SSP1IF). I2C1_MasterFsm è la funzione che implementa la macchina a stati. Tale funzione per prima cosa azzera il flag di interrupt, dopodichè esegue questa operazione:
I2C1_Status.state = fsmStateTable[I2C1_Status.state](); |
Il valore di I2C1_Status.state è stato impostato prima al valore I2C1_SEND_ADR_READ. fsmStateTable è un array che contiene puntatori a singole funzioni: il valore I2C1_SEND_ADR_READ è 1 (lo vediamo nell’enumerazione i2c1_fsm_states_t) per cui passando questo indice, verrà richiamata la funzione I2C1_DO_SEND_ADR_READ (vedi indice 1 dell’array fsmStateTable) tale funzione, tralasciando un altro paio di operazioni, esegue questo:
I2C1_MasterSendTxData(I2C1_Status.address << 1 | 1); |
Ovvero imposta l’indirizzo del dispositivo da richiamare in lettura (sposta l’indirizzo a 7 bit di una posizione verso sinistra e aggiunge 1 sul bit 0): capiamo a questo punto che avremmo dovuto impostare l’indirizzo del dispositivo da qualche altra parte prima di richiamare la funzione iniziale che stiamo ancora tracciando. La funzione I2C1_MasterSendTxData esegue semplicemente questo:
SSP1BUF = data; |
ovvero ha caricato l’indirizzo nel registro SSP1BUF, il quale verrà trasmesso per puntare al dispositivo con cui comunicare.
Capite quindi il giro immenso che abbiamo fatto soltanto per trasmettere sul bus l’indirizzo del dispositivo da cui vogliamo leggere? E tenete conto che ho saltato molte cose come i valori restituiti dalle funzioni e altri controlli. E’ quindi chiaro che se volessimo implementare noi delle semplici funzioni I2C per comunicare, sfruttando queste API, la situazione è tragica dal momento che non c’è ancora documentazione. Per fortuna l’MCC insieme al popò di roba generata include anche una cartella examples dove dentro ci sono due files: i2c1_master_example.c e i2c1_master_example.h che ci vengono in aiuto!
Le funzioni definite in questa cartella d’esempio sono:
uint8_t I2C1_Read1ByteRegister(i2c1_address_t address, uint8_t reg); uint16_t I2C1_Read2ByteRegister(i2c1_address_t address, uint8_t reg); void I2C1_Write1ByteRegister(i2c1_address_t address, uint8_t reg, uint8_t data); void I2C1_Write2ByteRegister(i2c1_address_t address, uint8_t reg, uint16_t data); void I2C1_WriteNBytes(i2c1_address_t address, uint8_t *data, size_t len); void I2C1_ReadNBytes(i2c1_address_t address, uint8_t *data, size_t len); void I2C1_ReadDataBlock(i2c1_address_t address, uint8_t reg, uint8_t *data, size_t len); |
I nomi di queste funzioni sono auto-esplicativi, così come i nomi dei parametri e si capisce subito a cosa servono:
- I2C1_Read1ByteRegister: restituisce un byte letto dal registro reg del dispositivo identificato dall’indirizzo address
- I2C1_Read2ByteRegister: restituisce un valore a 16bit letto dal registro reg del dispositivo identificato dall’indirizzo address
- I2C1_Write1ByteRegister: scrive un valore data ad 8bit nel registro reg del dispositivo identificato dall’indirizzo address
- I2C1_Write2ByteRegister: scrive un valore data a 16bit nel registro reg del dispositivo identificato dall’indirizzo address
- I2C1_WriteNBytes: scrive un array data di len valori ad 8bit nel dispositivo identificato dall’indirizzo address
- I2C1_ReadNBytes: legge len valori ad 8bit dal dispositivo identificato dall’indirizzo address memorizzandoli nell’array data senza specificare il registro
- I2C1_ReadDataBlock: legge len valori ad 8bit a partire dal registro reg del dispositivo identificato dall’indirizzo address memorizzandoli nell’array data
Premetto che gli address da passare a queste funzioni sono sempre quelli di base, a 7 bit, la macchina a stati sa già come cambiare l’indirizzo per andare in modalità lettura e scrittura. La funzione I2C1_ReadNBytes non prevede di specificare l’indirizzo del registro in cui scrivere/ da cui leggere e viene utilizzata quando un registro da cui partire è già stato specificato nell’operazione subito precedente (i dispositivi slave hanno una cella ram in cui memorizzano l’ultimo registro su cui sono state eseguite operazioni per cui non è necessario specificarlo nelle operazioni successive se dobbiamo eseguirle sullo stesso).
La funzione I2C1_WriteNBytes, invece, esegue una scrittura bulk di N-1 o N-2 bytes: nell’array da passare, infatti, il primo o i primi 2 bytes dell’array devono costituire l’indirizzo del registro da cui far partire la scrittura. Nel caso di EEprom dalla 24C32 compresa in poi, ad esempio, si indirizzano celle di memoria e i loro indirizzi sono a 16bit e già le funzioni I2C1_Write1ByteRegister e I2C1_Write2ByteRegister non possono essere usate perchè accettano un indirizzo registro ad 8 bit, per via di questo e perchè creare un array in cui i primi due elementi sono l’indirizzo è scocciante, proprio per le EEprom ho scritto un altro articolo in cui illustro funzioni modificate apposta per le EEprom.
La funzione I2C1_ReadDataBlock memorizza in un array i valori letti a partire dal registro specificato: il dispositivo stesso sa che dovrà puntare al registro seguente dopo aver fornito un byte, dato che il master da il consenso dopo il byte ricevuto (ACK – condizione che viene interpretata come: “continua a leggere”): si va avanti così fino a quando il master non chiude la comunicazione arrivato all’ultimo dato restituito chiudendo la comunicazione con un segnale di Not Acknowledge (NACK).
In realtà queste funzioni si trovano in una cartella examples per un motivo: dovrebbero essere funzioni di base da cui partire per realizzare le nostre (magari cambiandone i nomi?) e che ci spiegano come vanno utilizzate le API per l’I2C fornite dall’MCC.
Sono sincero: ho perso svariate ore ad analizzarle e andare a ritroso per tracciare tutte le chiamate che vengono eseguite e mi sono ritrovato in un labirinto immenso, bendato e con mani e piedi legati e dopo alcune aspirine ho deciso che mi stanno benissimo queste funzioni di esempio, e ho incluso l’header di esempio nel mio codice lasciando tutto come sta, ecco perchè nel main.c trovate la riga:
#include "mcc_generated_files/examples/i2c1_master_example.h" |
che forse è brutto, lo so, ma funziona. Potevo sicuramente ricopiare queste funzioni all’interno di altri due files cambiandone i nomi in modo da riflettere le caratteristiche del dispositivo da utilizzare: ad esempio potevo creare una funzione del tipo Read_Accelerometer_Axis mettendo dentro direttamente i registri, ma ho preferito lasciare tutto così in modo che anche voi che leggete cercate di capirci qualcosa. C’è anche da dire che col passare degli anni le librerie I2C fornite dall’MCC sono cambiate di molto stravolgendo ogni volta quello che era stato fatto in precedenza, e ho paura che continueranno a mutare anche se non riesco ad immaginare come possano essere concepite in maniera più complicata di questa.
Schema
Il programma l’ho scritto per la scheda PIC16F15376 Curiosity Nano di cui ho già parlato. L’accelerometro utilizzato è un ADXL345 montato su una breakout board. Il programma fornisce l’ouput su seriale (si utilizza la porta USB integrata sulla Curiosity Nano che viene vista come porta COM) e su display a 7 segmenti utilizzando il MAX7219 e sfruttando la libreria che ho presentato in un articolo precedente.
La Board è impostata per funzionare a 3.3V dato che l’accelerometro funziona a questa tensione e viene alimentato dal pin VTG.
Ricordo ancora una volta come si fa a cambiare la tensione di funzionamento: tasto destro sul nome progetto, selezionare Properties. Nel Box sinistro Categories selezionare PKOB Nano. Nel dropdown box Option Categories selezionare Power. Dare il segno di spunta all’opzione Power Target Circuit e scrivere 3.3 nel campo Voltage Level. Quando verrà eseguita la programmazione, MPLAB X comunicherà al debugger questa nuova impostazione e a fine programmazione sarà il debugger a impostare il regolatore di tensione per fare in modo che il PIC funzioni a 3.3V. Quando si fanno queste operazioni di cambio tensione è bene non tenere collegato nulla alla scheda dal momento che cambiano sia i livelli logici di input e output, sia la tensione presente sul pin VTG che è collegato all’uscita del regolatore di tensione on-board.
Il MAX7219 verrà alimentato da VBUS (5V presi dalla porta USB) dato che non può funzionare a 3.3V, riceverà livelli logici a 3.3V che verranno interpretati correttamente come spiegato più volte in passato.
Come già detto nel vecchio articolo su questo accelerometro, CS è portato a livello alto in modo da abilitare la comunicazione I2C (perchè questo accelerometro può anche comunicare in SPI): in questo caso SDO viene utilizzato per settare l’indirizzo e portandolo a GND l’indirizzo dell’ADXL345 è 0x53 (indirizzo a 7 bit che diventerà 0xA7 in lettura e 0xA6 in scrittura).
Come in ogni comunicazione I2C vanno poste delle resistenze di pull-up sulle linee di SDA e SCL, per l’ADXL345 sono consigliate resistenze da 4700Ω.
Codice di esempio
La prima funzione I2C che vado a richiamare è una funzione di scrittura, per fare in modo che l’accelerometro che ho utilizzato esca dallo stato di stand-by nel quale si trova all’avvio:
I2C1_Write1ByteRegister(ADXL_ADDRESS, 0x2D, 0x08); |
Vedete che utilizzo la funzione di esempio, I2C1_Write1ByteRegister, per scrivere il valore 0x08 nel registro 0x2D del dispositivo identificato dall’indirizzo ADXL_ADDRESS, dichiarato con un #define più in alto. Se avete letto gli articoli linkati all’inizio, sapete perchè faccio questa operazione. In seguito eseguo le letture dei registri che contengono i valori dei tre assi X, Y, Z mediante la funzione I2C1_ReadDataBlock specificando che voglio leggere 6 bytes a partire dall’indirizzo 0x32 e che i risultati andranno memorizzati, in sequenza, nell’array readBuffer:
I2C1_ReadDataBlock(ADXL_ADDRESS,0x32,readBuffer,6); |
Si partirà dall’indirizzo 0x32 incluso procedendo per altri 5 indirizzi successivi (6 bytes in totale). Gli indirizzi 0x32 e 0x33 contengono il valore a 16 bit letto sull’asse X, 0x34 e 0x35 quello dell’asse Y e 0x36 e 0x37 quello dell’asse Z. Questa funzione come detto sopra esegue la lettura sequenziale per cui è il dispositivo slave che già sa che dovrà incrementare l’indirizzo dopo ogni byte inviato fino a quando il master non fornisce la sequenza di NACK che porta lo slave a fermarsi.
Nel codice faccio 32 letture in sequenza con le quali ricavo una media per cercare di rendere i valori un po’ più stabili, cosa che nella realtà non si verifica affatto.
Nel caso della visualizzazione sul display a 7 segmenti ho fatto anche in modo che se il dato attuale differisce dal precedente di una sola unità (motivo per il quale ho usato la funzione abs fornita dall’header stdlib, in modo da valutare sia una differenza di +1 che di -1), non viene aggiornato. Ho fatto questa porcheria perchè ho notato che, anche con l’accelerometro perfettamente fermo e stabile, il valore mostrato sul display oscilla di continuo di 1 unità per cui ruotandolo capiterà che molto spesso escono escono sempre valori pari o sempre valori dispari proprio perchè una unità viene saltata.
Se trovate un metodo migliore, facile e che funzioni, per stabilizzare la lettura facendo in modo di non saltare valori, contattatemi. Sul display viene mostrato un asse solo per volta e si cambia l’asse visualizzato premendo il pulsante integrato sulla board (SW0). Dal momento che le lettere X e Z non sono facilmente rappresentabili (la X provandola a visualizzare con un display 7 segmenti somiglia ad una H maiuscola e la Z ad un 2), ho visualizzato piuttosto le lettere A,B,C sull’estremità sinistra del display per indicare che il valore mostrato è dell’asse X, Y e Z rispettivamente : è solo una questione di estetica.
Si potrebbe pensare a visualizzare le parole Roll (rollio, rotazione intorno ad X) e Pitch (beccheggio, rotazione intorno ad Y) ma ricordo che non è possibile utilizzare la parola Yaw (imbardata, rotazione intorno a Z) dato che un accelerometro non è in grado di rilevare l’imbardata dal momento che la rotazione intorno ad Y non comporta variazioni dell’accelerazione lungo tale asse (l’imbardata viene rilevata dal giroscopio). Piuttosto, dato che l’accelerometro rileva il capovolgimento – l’inclinazione dell’asse Z (non la rotazione intorno ad esso), si potrebbe visualizzare la parola Tilt Angle. Comunque in questo caso bisognerà ricordare che non possiamo più usare i valori direttamente letti dall’accelerometro per X, Y e Z dato che non stiamo parlando più di valori di accelerazione (letti direttamente dall’accelerometro) bensì di angoli. Quindi dovremo ricordarci che il Rollio (rotazione intorno a X), viene calcolato dai valori di Y e Z, il Beccheggio (rotazione intorno a Y) da da X e Z e l’angolo di tilt da tutti e 3 gli assi. Se non ci avete capito niente, vi invito a rileggere questo. Insomma fate voi: questo era solo un esempio di comunicazione I2C con un accelerometro.
Altra considerazione: come avete visto sia le mie vecchie librerie I2C che queste, hanno purtroppo delle funzioni in cui si rimane in un ciclo While nell’attesa che qualcosa si sblocchi. Come in tutti i programmi in cui ci sono funzioni “bloccanti”, se quella condizione non si verifica, il programma si inchioda. Ad esempio se durante la demo staccate il filo della linea SDA, il programma si blocca anche se poi lo riattaccate (cosa che non succede con la linea di SCL). Bisognerebbe prevedere un watchdog oppure vedere se le librerie che utilizzano l’interrupt sono esenti da questo problema.