Corso programmazione PICMicro in C – Lezione 12 (Parte 2/3) – Utilizzare le eeprom esterne I2C
Nella prima parte di questa lezione abbiamo visto cos’è il modulo (M)SSP e come lo si configura per utilizzarlo in modalità I2C. Vedremo quindi in questa seconda parte un’applicazione pratica delle librerie I2C: come si utilizza una memoria eeprom esterna.
In una lezione precedente, inoltre, abbiamo parlato di memoria eeprom interna: le nozioni e le varie operazioni binarie che abbiamo già visto ci potranno tornare molto utili soprattutto se avremo la necessità di memorizzare nella eeprom valori numerici superiori ad 8 bit.
Sappiamo quindi che una memoria eeprom ha la capacità di trattenere le informazioni anche quando l’alimentazione viene tolta, a differenza della memoria RAM. La memoria eeprom interna di un PIC è molto utile quando, ad esempio, vogliamo salvare svariate variabili da utilizzare magari come “settaggi” per le nostre applicazioni o altro, ma è comunque una memoria molto limitata che non ci permette di implementare applicazioni che richiedono un alto uso di memoria : un datalogger ad esempio.
Indice dei contenuti
Le EEprom esterne
Quando abbiamo bisogno di un certo quantitativo di memoria non volatile ecco che ci vengono incontro le memorie eeprom esterne; di queste ne esistono svariati tipi, classificate principalmente dal tipo di bus di comunicazione che utilizzano. Tempo fa erano molto usate le eeprom parallele, le quali avevano cioè, un bus dati di tipo parallelo (es.: la 28C512), tali memorie sono abbastanza grandi fisicamente in quanto hanno appunto bisogno di molti pin per l’indirizzamento e i dati. Attualmente sono molto più utilizzate le memorie eeprom seriali (serial eeprom): molto meno ingombranti e che oramai garantiscono tempi di accesso e lettura molto ristretti in dipendenza anche dal tipo di interfaccia seriale utilizzato. A seconda del bus dati utilizzato per il trasferimento, avremo quindi vari tipi di memorie, che possiamo classificare grossolanamente in base all’interfaccia, e che si distinguono generalmente per la parte iniziale della sigla sul corpo dell’integrato:
Parte iniziale sigla | Bus comunicazione |
---|---|
28 | Parallelo |
24 | Seriale - I2C |
25 | Seriale - SPI |
93 | Seriale - Microwire |
11 | Seriale - UNI/O |
A questa regola alcuni produttori a volte fanno eccezione.
Le sigle sulle memorie eeprom I2C
A noi, attualmente, interessano le memorie eeprom I2C, e quindi ci occuperemo solo di queste. Le memorie I2C hanno la sigla formata dal suffisso 24 : questo è l’unico parametro costante (ma nemmeno, dato che ATMEL per alcuni modelli, come vedremo più in basso, usa a volte una sigla diversa), una o due lettere che identificano alcuni parametri di funzionamento elettrici del chip e infine un numero/sigla che può essere composta da 2 a 4 cifre ed è rappresentativo della quantità di memoria disponibile espressa in kib. Possono quindi seguire una o più lettere che identificano altri parametri di funzionamento che a noi non interessano
Chilo e Kibi
Per evitare confusione tra i prefissi del Sistema Internazionale e quelli del sistema binario sono stati introdotti tempo fa nuovi prefissi. C’è quindi una differenza tra k del sistema internazionale (k minuscola, non maiuscola che sta per Kelvin) che sta per 1000, e ki (leggasi: kibi) che sta per 1024. kib si legge “kibibit” ed equivale, quindi, a 1024bit. Fino a poco tempo fa si usava scrivere kb (chilobit) e kB (chiloByte) per indicare 1024bit e 1024byte rispettivamente, ma questo crea confusione col k che sta per mille.
Per evitare confusione da qui in poi, ricapitolo: 1kib = 1024bits = 128Bytes. 1kiB=1024Bytes. 1Mib = 1024*1024 = 1048576bits. Fate sempre attenzione che:
- la k per indicare le migliaia (siano esse decimali o binarie) deve essere minuscola
- se dovete indicare un multiplo binario, deve essere messa la i, quindi kib e non kb, Mib e non Mb
- la b minuscola sta per BIT, la B maiuscola sta per BYTE (8 bit)
Selezione Indirizzo
Abbiamo detto che, potendo mettere su un unico bus più di un dispositivo I2C, molti di questi hanno la possibilità di impostare l’indirizzo in maniera tale che possano coesistere su un’unica linea due dispositivi uguali (ad esempio se abbiamo bisogno di 256kiB di memoria eeprom possiamo montare due memorie da 128kiB sullo stesso bus e dargli indirizzi diversi), altri dispositivi, invece, non ne hanno la possibilità e hanno l’indirizzo fisso (l’RTCC DS1307 ad esempio: sarebbe comunque inutile metterne due, no?).
Le eeprom I2C (o almeno, la maggior parte come vedremo in seguito), hanno la parte alta dell’ indirizzo sempre fissa, chiamata Control Code ed impostata sempre a 1010 e la parte bassa di 3 bit chiamata Chip Select impostabile agendo sui pin 1, 2, 3 e denominati A0, A1 ed A2:
Mettendo un pin di selezione indirizzo a massa, il corrispondente bit dell’indirizzo sarà a zero, mettendolo a Vcc sarà a 1: questo consente, quando possibile in base all’eeprom scelta, di mettere sullo stesso bus più di una memoria eeprom uguale, consentendo indirizzi diversi.
Stiamo quindi parlando di Device Address: l’indirizzo a cui l’eeprom fisica risponde. Il Device Address dell’eeprom è quindi costituito da Control Code + Chip Select.
I bits A2, A1, A0, però non sono sempre disponibili su tutte le eeprom: a volte si tratta di pin non connessi, altre volte il posto di uno o più di questi 3 bit viene utilizzato quando la memoria è segmentata in più banchi e quei bit vengono invece utilizzati per entrare a far parte dell’indirizzo della cella di memoria a cui puntare. In questi ultimi casi al posto di A2, A1, A0 compaiono invece le scritte B2, B1, B0.
Le eeprom, difatti, in base al quantitativo di memoria possono avere gli indirizzi delle celle ad 8 o a 16bit: 8 bit consentono di indirizzare fino a 256celle, mentre 16bit fino a 65536.
Con il termine “cella” faccio riferimento ad una posizione di memoria in cui c’è un valore ad 8bit.
Se insieme all’indirizzo della cella, aggiungo 1 bit “preso” dall’indirizzo al posto della selezione indirizzo dell’eeprom, ecco che posso avere anche indirizzi a 9, 10 ecc bits.
Se ad esempio l’eeprom ha gli indirizzi delle celle ad 8bit e nel device address compare, ad esempio, B0, vuol dire che B0 costituirà il nono bit dell’indirizzo delle celle. Se, ancora, l’indirizzo delle celle è a 16bit e nel device address compare B1, B0, vuol dire che B0 costituirà il diciassettesimo bit dell’indirizzo cella e B1 il diciottesimo (questo è ad esempio il caso, l’unico, della memoria da 2Mbits) e solo A2, eventualmente, entrerà a far parte del device adress.
Fino alla 24C16 gli indirizzi delle celle sono ad 8bit. La 24C16, in particolare non ha proprio la scelta dell’indirizzo dal momento che A2, A1, A0 vengono utilizzati come B2, B1, B0 consentendo, così, di avere indirizzi di cella ad 11 bits che consentono di indirizzare fino a 2048 diverse celle (2kiB appunto).
Nella tabella seguente ho riassunto le sigle di memoria, con la loro capacità e le caratteristiche degli indirizzi e degli indirizzi di cella:
Sigla | Sigla alternativa | Quantità memoria (bits) | Quantità memoria (bytes) | Device Address (7bit) | Indirizzamento celle (bits) | Note |
---|---|---|---|---|---|---|
24xx00 | 128 | 16 | 1010xxx | 8 | xxx = bits don't care | |
24xx01 | 1K | 128 | 1010[A2][A1][A0] | 8 | ||
24xx014 | 1K | 128 | datasheet non trovato | 8 | ||
24xx21 | 1K | 128 | datasheet non trovato | 8 | ||
24xx02 | 2K | 256 | 1010[A2][A1][A0] | 8 | ||
24xx22 | 2K | 256 | datasheet non trovato | 8 | ||
24xx024 | 2K | 256 | datasheet non trovato | 8 | ||
24xx025 | 2K | 256 | datasheet non trovato | 8 | ||
24xx04 | 4K | 512 | 1010[A2][A1][B0] | 8 | ||
24xx08 | 8K | 1024 (1kB) | 1010[A2][B1][B0] | 8 | ||
24xx16 | 16K | 2048 (2kB) | 1010[B2][B1][B0] | 8 | ||
24xx32 | 32K | 4096 (4kB) | 1010[A2][A1][A0] | 16 | ||
24xx64 | 64K | 8192 (8kB) | 1010[A2][A1][A0] | 16 | ||
24xx65 | 64K | 8192 (8kB) | datasheet non trovato | 16 | ||
24xx128 | 128K | 16384 (16kB) | 10100[A1][A0] | 16 | ||
24xx256 | ATMLUxxx 2EC L | 256K | 32768 (32kB) | 10100[A1][A0] | 16 | |
24xx512 | ATMLUxxx 2FB 2 | 512K | 65536 (64kB) | 10100[A1][A0] | 16 | |
24xx1024 24xx1026 | AT24CM01 | 1M | 131072 (128kB) | 1010[A2][A1][B0] | 16 | |
AT24C1024 | 1M | 131072 (128kB) | 10100[A1][B0] | |||
24xx1025 | 1M | 131072 (128kB) | 1010[B0][A1][A0] | 16 | il pin A2 va messo obbligatoriamente a Vcc | |
24xx2048 (!) | AT24CM02 | 2M | 262144 (256kB) | 1010[A2][B1][B0] | 16 |
La memoria 24xx00, la più piccola di tutte vediamo che ha i pin 1,2,3 non connessi e di conseguenza A2, A1 e A0 possono trovarsi a qualsiasi valore. Se sul bus si richiama un indirizzo che parte con 1010, quindi, risponderà sempre lei: si presuppone pertanto che sul bus ci sia solo questo modello di eeprom e quindi non può essere utilizzata insieme ad altre. Il motivo di questa scelta mi è alquanto oscuro ma sul datasheet è spiegato che tale eeprom è pensata per essere utilizzata laddove è richiesta una quantità di memoria eeprom limitata: dispositivi in cui bisogna salvare valori di calibrazione, ID ecc. Quindi in dispositivi che magari non hanno una loro eeprom interna e non si ha la necessità di collegarne altre.
Di modelli di EEProm da 1Mbit ne esistono ad esempio 3 o 4 (1024, 1025, 1026) e differiscono per la selezione dell’indirizzo. La ATMEL produce due modelli da 1Mbit: la AT24C1024, che non è compatibile con altre memorie 24×2014 (a meno che non si metta A2 di queste a GND) e la AT24CM01 che è invece compatibile con le altre 24C1024 (per questa ho trovato solo un datasheet di una marca mai sentita) e con la 24C1026 prodotte da Microchip. Ad ogni modo, come vedete dalla tabella, è un vero pasticcio e conviene sempre dare un occhio al datasheet dell’eeprom che avete sottomano perchè questa è solo per riferimento e potrebbe non essere vera in ogni caso.
Atmel, poi, crea ancora confusione: vi capiterà di trovare una memoria da 512kib della Atmel siglata come: ATMLUyxx 2FB 2 (es: ATMLU940 2FB 2 oppure ATMLU802 2FB 2 ecc). In queste sigle la y sta per l’anno di produzione e le due xx per la settimana. La sigla che dovete vedere in questo caso, quindi, è sulla seconda riga dell’integrato in quanto sulla prima c’è ATMLU seguito dalla data di produzione, sulla seconda c’è la sigla che aiuta ad identificare la memoria e sulla terza altre lettere e numeri che non ci interessano. Vedete la tabella sopra: ne ho trovate solo due che hanno questa sigla astrusa.
Protezione dalla scrittura
Vediamo inoltre, nello schemino del datasheet, oltre ai pin di alimentazione, a quelli di comunicazione e a quelli di selezione indirizzo, un altro pin denominato WP. WP sta per Write Protect: mettendo questo pin a massa è possibile scrivere sulla memoria eeprom. Se WP invece viene messo a Vcc non è possibile scrivere sull’eeprom ma solo leggerla. Si capisce quindi che per i nostri esperimenti dovremo tenerlo a massa.
Organizzazione della memoria
Come per la memoria eeprom interna del PIC, anche le memorie eeprom esterne possiamo immaginarle come costituite da numerose celle da 1byte, ognuna delle quali ha un indirizzo numerico a partire da zero. Su alcune memorie eeprom l’indirizzo di ogni cella è a 16bit, su altre è a 8 bit.
Con un indirizzamento ad 8 bit è facile capire che si possono “richiamare” fino a 256celle di memoria, per cui è facile immaginare che l’indirizzamento ad 8 bit è presente nelle memorie eeprom fino alla capacità di 256Bytes (ovvero 2Kbits).
Con un indirizzamento a 16 bit possiamo invece richiamare 65536 celle di memoria ovvero capacità fino a 512Kbits.
La memoria da 1024Kbits pure ha l’indirizzamento a 16bit che normalmente non permetterebbe l’indirizzamento di tutte le celle: c’è appunto il bit B0 dell’indirizzo che permette, come già detto prima, di selezionare il primo o il secondo blocco da 512K e che quindi può farci immaginare tale memoria come due da 512.
Immagino quindi che, per un’eventuale futura memoria da 1024Kbits o toglieranno un altro pin per la selezione dell’indirizzo o ci sarà un indirizzamento a 24bit.
Operazioni di lettura e scrittura
Nella parte precedente della lezione abbiamo già visto come si realizza, sommariamente, una comunicazione I2C. La regola è sempre la stessa per tutti i dispositivi I2C: quando dobbiamo scrivere sulla periferica si eseguiranno le operazioni:
- Sequenza di Start
- Invio indirizzo periferica in scrittura (bit zero dell’indirizzo posto a zero)
- Invio indirizzo del registro in cui scrivere
- Invio del valore da scrivere nel registro
- Sequenza di stop
Nel caso di una memoria da 1024Kbits al posto di A2 metteremo 1 o 0 per scrivere nel primo o nel secondo blocco.
Questa che ho illustrato qui si chiama scrittura Byte Write e serve, cioè, a scrivere un byte alla volta. E’ bene ricordare che il ciclo di scrittura ha una sua durata che è in media 5mS: vuol dire che se non si aspetta almeno questo tempo, il dato non è ancora disponibile e non bisogna fare altre operazioni sull’eeprom nel frattempo. consultate sempre il datasheet della vostra eeprom per conoscere questo tempo Nel caso in cui si voglia risparmiare tempo è possibile anche effettuare la scrittura di tipo Page Write che permette di scrivere una pagina alla volta.
Tale modalità prevede di specificare solo l’indirizzo dal quale partire e quindi di inviare in sequenza un tot di bytes da scrivere uno dopo l’altro. La quantità di bytes da inviare in sequenza varia da formato a formato di memoria in quanto ogni memoria ha la pagina di dimensioni diverse dalle altre:
Tale modalità di scrittura è molto vantaggiosa e permette di risparmiare una quantità di tempo davvero notevole ma non tutte le memorie eeprom la supportano.
Se invece vogliamo leggere dall’eeprom, la sequenza di operazioni sarà un po’ più lunga come abbiamo già visto:
- Sequenza di Start
- Invio indirizzo periferica in scrittura
- Invio indirizzo registro da cui prelevare il dato
- Sequenza di start ripetuto
- Invio indirizzo periferica in lettura (bit zero dell’indirizzo posto a 1)
- Lettura del valore (il valore sarà contenuto nel registro SSPBUF)
- Sequenza di stop
Questa modalità di lettura ci permette di leggere un byte alla volta e si chiama Random Read, notate che, non appena ricevuto il byte di dati, il master non invia l’acknowledge e da quindi lo stop.
E’ possibile anche effettuare la lettura sequenziale, che permette di leggere dall’eeprom i dati in maniera continua senza fermarsi e risparmiando quindi molto tempo. Tale modalità di lettura prende il nome di lettura sequenziale (Sequential Read) e viene inizializzata allo stesso modo della lettura random con la sola differenza che il master, dopo aver ricevuto il primo byte di dati, dovrà dare l’acknowledge: in questo modo l’eeprom “capisce” che deve continuare a sfornare dati e continuerà a farlo incrementando da sè in automatico l’indirizzo da cui leggere di una unità. Quando il master si “sarà stufato” di leggere, terminerà la connessione al solito modo: no acknowledge e stop:
Esempio pratico
Nei file allegati c’è un semplice esempio di scrittura e lettura su eeprom esterna. Sono necessarie le librerie I2C che abbiamo visto nella lezione precedente, per cui sapete già quali librerie includere, con quali pic utilizzarle e come utilizzarle. La libreria per la gestione delle eeprom esterne è molto semplice ed è prevista unicamente per quelle eeprom che hanno l’indirizzamento a 16bit (in teoria dalla 24xx04 alla 24xx512 – non includo la 24xx1025 perchè bisogna gestire il bit B0 per selezionare il blocco e qui non l’ho previsto). Sono presenti soltanto due funzioni: una per leggere e una per scrivere, non sono supportate le modalità di scrittura e lettura sequenziale.
Analizziamo la funzione che effettua la scrittura:
17 18 19 20 21 22 23 24 25 26 27 28 29 30 | void eee_write_byte(unsigned char deviceid, const unsigned int address, const unsigned char data) { // aggiungo all'indirizzo "standard" delle eeprom24, il deviceid che si ottiene // settando i pin A0,A1,A2. Il device id devo spostarlo di un bit in quanto il // bit 0 è quello che determina la lettura/scrittura deviceid = EE_CONTROL_CODE | (deviceid <<1); I2cStart(); // avvio la comunicazione I2C I2cWriteMaster(deviceid); // invio l'indirizzo della memoria sulla quale voglio scrivere // il bit zero è già posto a zero per specificare l'operazione di scrittura I2cWriteMaster((char)(address >> 8)); // invio la parte alta dell'indirizzo in cui scrivere I2cWriteMaster((char)(address & 0xFF)); // invio la parte bassa dell'indirizzo in cui scrivere I2cWriteMaster(data); // Invio il byte I2cStop(); // termino la comunicazione } |
Come vedete la funzione accetta in ingresso l’indirizzo del dispositivo (deviceid), l’indirizzo della cella (address) in cui scrivere e il byte (data) da scrivere. Se abbiamo impostato l’indirizzo della nostra eeprom su 000 (cioè i pin A2,A1 e A0 connessi a massa), in deviceid andrà messo proprio 0. Se abbiamo, ad esempio, messo A2 e A1 a Vcc e A0 a massa l’indirizzo sarà 6 ecc.
Per la libreria che vi ho allegato, teniamo conto che il bit A0 si trovi in posizione 0, il bit A1 in posizione 1 e il bit A2 in posizione 2 anche se in realtà questi 3 bits dovrebbero trovarsi spostati di una posizione verso sinstra: difatti vedete che nel codice eseguo proprio questa operazione. Ho fatto così in quanto in tal modo è più facile calcolarsi l’indirizzo della memoria.
Andiamo quindi a sommare il nostro indirizzo “personale” con quello standard delle eeprom (0b10100000) memorizzato nella costante EE_CONTROL_CODE. Notiamo che l’indirizzo si trova già in modalità scrittura in quanto il bit zero è già posto a zero.
Avviamo quindi la comunicazione I2C e inviamo la parte alta e la parte bassa dell’indirizzo della cella in cui scrivere, le operazioni fatte vi dovrebbero essere già chiare se avete letto questo articolo e quest’altro. Dopo aver inviato l’indirizzo inviamo il byte e quindi terminiamo la comunicazione.
Fate attenzione ad una cosa: da datasheet la scrittura di un byte prende un tempo di 5mS, per cui se appena dopo aver scritto un byte lo andate subito a leggere senza aspettare tale tempo, otterrete un risultato non reale. Per tale motivo nel programma di esempio ho aggiunto un ritardo di 5mS dopo la scrittura.
La funzione di lettura è anch’essa molto semplice:
33 34 35 36 37 38 39 40 41 42 43 44 45 46 | unsigned char eee_read_byte(unsigned char deviceid, const unsigned char address) { unsigned char data; deviceid = EE_CONTROL_CODE | (deviceid <<1); I2cStart(); I2cWriteMaster(deviceid); // invio l'indirizzo della memoria dalla quale voglio leggere I2cWriteMaster((char)(address >> 8)); // invio la parte alta dell'indirizzo da cui leggere I2cWriteMaster((char)(address & 0xFF)); // invio la parte bassa dell'indirizzo da cui leggere I2cRepStart(); // start ripetuto I2cWriteMaster(deviceid | 1); // invio l'indirizzo della memoria dalla quale voglio leggere, in modalità lettura data = I2cReadMaster(0); // leggo e non invio l'acknowledge I2cStop(); // termino la comunicazione return data; // restituisco il valore letto } |
Come vedete facciamo più o meno la stessa operazione precedente: inviamo sul bus l’indirizzo della memoria (in modalità scrittura in quanto il bit zero è posto a zero), quindi l’indirizzo della cella di memoria, eseguiamo uno start ripetuto che ci permetterà di non perdere la comunicazione (nel frattempo l’eeprom si è memorizzata l’indirizzo della cella da cui vogliamo leggere), inviamo quindi l’indirizzo dell’eeprom in modalità lettura: per tale motivo aggiungiamo un 1 all’indirizzo, effettuiamo quindi la lettura che andrà memorizzata nel byte “data” definito prima. Come vedete nella funzione I2cReadMaster abbiamo messo zero come argomento che ci permette di fare in modo di NON inviare l’acknowledge, come richiesto dalla modalità di lettura random che abbiamo discusso più in alto. Viene quindi terminata la comunicazione e la funzione restituisce il dato letto.
Il sorgente di esempio prevede l’utilizzo di una eeprom qualsiasi (ma con indirizzamento a 16 bit) e di leds collegati alla porta RD0. L’esempio l’ho compilato per la Freedom II con su il PIC16F877A per cui se non avete la freedom o altri sistemi di sviluppo che non prevedono il montaggio di una eeprom esterna, fate riferimento al datasheet del vostro picmicro per vedere in quali punti sono presenti i segnali SCL e SDA e allo schema presentato nella prima parte di questa lezione (estratto dallo schema della FreedomII) per vedere come collegare l’eeprom sul bus I2C: in particolare non dovete assolutamente dimenticare le resistenze di pull-up.
Nell’esempio vengono eseguite queste operazioni: andiamo a scrivere un valore (oxFo) nella cella zero dell’eeprom, attendiamo 5mS perchè la scrittura sia completa e poi lo andiamo a rileggere e lo mostriamo sulla striscia a led. Lasciamo i led accesi per un po’ e andiamo a scrivere il valore 0x0F nella cella 1 della eeprom, lo rileggiamo e lo mostriamo sulla barra a led.
Al vostro occhio non accadrà nulla di spettacolare: il programma in esecuzione accenderà prima gli ultimi 4 led e poi i primi 4. In realtà l’accensione dei led riflette il valore del byte letto dall’eeprom. Potete divertirvi a modificare anche i sorgenti delle lezioni sull’eeprom interna per adattarli all’eeprom esterna. Una volta avuta padronanza con tali concetti, potete utilizzare le eeprom esterne per memorizzare grosse quantità di dati (per un datalogger che memorizza temperature, bitmap da mostrare su display grafici, una rubrica…).
Downloads
Nota: i programmi di esempio sono stati sviluppati con una versione precedente dell’Hitec-C Compiler, per cui compilati con la nuova versione, restituiscono errori. Fate riferimento a questo articolo per maggiori informazioni su come adattare i vecchi programmi. Consiglio spassionato se volete davvero imparare a programmare: non utilizzate l’include legacy headers, ma imparate a cambiare i nomi mnemonici.
- File di supporto alla dodicesima lezione del corso di programmazione picmicro in C (1192 download)
- Brochure modelli EEprom prodotte da Atmel