La EEPROM su Arduino – lettura e scrittura di dati in modo permanente
Che cosa sono le EEPROM
Le EEPROM (Electrically Erasable Programmable Read-Only Memory), sono dei particolari microchip utilizzati per poter immagazzinare dei dati anche dopo che il dispositivo, su cui siano montati, sia stato spento. Alla successiva accensione, il dispositivo avrà quindi a disposizione dei dati da poter caricare. Cioè sono dei dispositivi di memoria che, al contrario delle RAM, sono in grado di mantenere al loro interno delle informazioni anche dopo che la corrente di alimentazione sia stata disattivata.
Moltissimi controllori, come anche quelli utilizzati per Arduino, come l’ATmega328, hanno al loro interno dei chip EEPROM che permettono di conservare un set di dati utili anche dopo lo spegnimento del dispositivo. Quindi spesso parliamo di memoria EEPROM, in modo simile a cui ci riferiamo a memoria RAM.
Quindi ogni scheda Arduino è dotata di questo genere di memoria, anche se a seconda del modello la loro dimensione sarà diversa. In particolare la dimensione dipenderà dal tipo di processore utilizzato dalla scheda.
- 512 bytes su ATmega168 e ATmega8
- 1024 bytes su ATmega328P
- 4096 bytes su ATmega1280 e ATmega2560.
Inoltre è possibile espandere le dimensioni della memoria EEPROM a disposizione utilizzando degli appositi Microchip da connettere alla scheda e che fanno uso del protocollo I²C per comunicare (come ad esempio Microchip 24LC1025).
Piccole curiosità sulle EEPROM: in realtà esiste un limite di volte in cui le celle di memoria presenti al suo interno possono essere riscritte. Ma niente da preoccuparsi, dato che si tratta di circa un milione di riscritture. Comunque è importante tenere conto anche di questo, in particolari applicazioni. Stessa cosa è la durata di tempo in cui una cella è in grado di mantenere il dato in assenza di corrente. Bene i costruttori affermano un tempo limite di 10 anni, ma questo periodo può variare a seconda dello stato di conservazione e della temperatura in cui viene conservato il microchip.
A che cosa servono le EEPROM su Arduino?
Le schede Arduino, come tutti i processori fino ai computer stessi, necessita di memoria per poter immagazzinare delle informazioni. Per farlo, Arduino ha a disposizione tre tipi diversi di memoria:
- la memoria FLASH
- la memoria RAM
- la memoria EEPROM
La memoria FLASH è quella utilizzata da Arduino per immagazzinare il codice dello sketch una volta compilato. Una volta caricato il codice, questo rimarrà immutato fino al prossimo upload (nuovo codice compilato). Quando la scheda Arduino verrà accesa, leggerà qui il codice da eseguire. Anche la memoria FLASH come quella EEPROM conserva le informazioni dopo lo spegnimento della scheda.
La memoria RAM è quella utilizzata per contenere i valori delle variabili definite nel codice dell sketch e che sono necessarie per la corretta esecuzione del programma. Queste varieranno di contenuti durante tutta la durata dell’esecuzione, le variabili potranno essere create e distrutte ed una volta spento il dispositivo, l’intera memoria con i dati all’interno verrà cancellata.
La memoria EEPROM è quella utilizzata per immagazzinare dati e parametri che dovranno essere utilizzati anche dopo lo spegnimento del dispositivo. Ha quindi una funzione molto simile a quella di un hard disk per un computer, dove si immagazzinano file di dati per poterli conservare nel tempo.
Comprendiamo quindi che la EEPROM ha una funzione abbastanza particolare. Un esempio di come potrebbe essere utilizzata su Arduino è quello di salvare una particolare configurazione o un serie di dati di ripristino, per poter ripartire la volta successiva che la scheda viene riattivata da un punto particolare (ripristino della sessione precedente).
Quindi un aspetto importante delle EEPROM da tenere in considerazione è quello di non utilizzarle per la lettura e scrittura di variabili comuni, cosa che invece deve essere fatta sulla RAM. Questo per una serie di motivi… non solo la possibilità di non poter riscrivere le celle infinite volte, ma soprattutto discorsi di prestazioni. Infatti la memoria EEPROM è stata progettata per altri scopi rispetto alla RAM, e quindi i tempi di accesso e di scrittura sono molto più lenti rispetto a quest’ultima.
La libreria EEPROM
Per lavorare in modo efficiente con la memoria EEPROM all’interno degli sketch, l’editor di Arduino, Arduino IDE, ci fornisce una libreria che ci fornisce molte funzioni che ci facilitano il compito di lettura e scrittura su di essa: la libreria EEPROM.
Quindi per prima cosa, se abbiamo intenzione di utilizzare questa libreria, dobbiamo includerla all’inizio dello sketch.
#include <EEPROM.h>
Uno degli aspetti di cui si deve tenere conto prima di cominciare a programmare lo sketch, è che quando si ha a che fare con una memoria EEPROM si deve lavorare con gli indirizzi di memoria.
Ogni volta che si scrive un valore o si deve accedere ad esso all’interno della EEPROM è necessario specificare l’indirizzo di memoria. Considerando che un normale Arduino UNO ha 512 byte di memoria EEPROM avremo un set di indirizzi che andranno da 0 a 511.
Quindi per scrivere e leggere dei dati sulla EEPROM si utilizzeranno le funzioni read e write messe a disposizione della libreria, specificando nei parametri anche questi valori.
EEPROM.write(address, value)
EEPROM.read(address)
Per quanto riguarda i valori scrivibili, questi dovranno essere quelli contenibili in un byte di memoria. Vedremo in dettaglio negli esempi seguenti.
Scrivere e Leggere valori sulla EEPROM
Per vedere come funziona la scrittura e la lettura sulla EEPROM di Arduino, implementiamo un utile esempio.
Scriviamo uno sketch in cui dal Monitor Seriale, immetteremo, tramite tastiera, dei numeri interi da 1 a 9 che verranno via via sommati.
Questo è un ottimo modo per simulare l’acquisizione dei dati via seriale durante l’esecuzione di un programma. La somma dei valori immessi verrà contenuta nella variabile value.
Ad un certo punto questo valore verrà immagazzinato per usi futuri sulla EEPROM, inserendo per esempio il comando ‘w‘ (che sta per write) tremite seriale.
In questi casi è importante comprendere il range di valori che potrà assumere questo valore. Se per caso sia un numero intero da 0 a 255 potremo utilizzare un solo byte, mentre se per caso sia compreso tra 0 e 65.535 allora dovremo utilizzare due byte.
Nel primo caso (quello ad 1 solo byte), sarà semplice, specificando il valore value direttamente all’interno del comando EEPROM.write( address, value). Così anche per l’indirizzo di memoria, che corrisponderà ad una singola cella di 1 byte. Ma nel caso di 2 o più byte?
Ora in questi casi si devono gestire più celle di memoria contemporaneamente. Una buona strategia sarà quella di utilizzare indirizzi di memoria adiacenti. Nel nostro semplice esempio, utilizzando valori interi che occupano 2 byte, prenderemo in considerazione le prime due celle con indirizzi 0 e 1.
Definiamo quindi, due costanti intere COUNT_ADDR1 e COUNT_ADDR2 per definire i due indirizzi di memoria della EEPROM dedicati a contenere il valore value. Inoltre definiremo la variabile value inizializzandola a 0.
#include <EEPROM.h>
const int COUNT_ADDR1 = 0;
const int COUNT_ADDR2 = 1;
int value = 0;
Successivamente, nella funzione setup() dello sketch, definiremo prima di tutto una comunicazione seriale a 9600 baud.
Eseguiamo la lettura dei due valori contenuti nelle prime due celle che le inseriremo nelle variabili hiByte, e lwByte. Per ricomporre le due parti nel valore originale intero si utilizzerà la funzione word().
Con una stringa di testo poi definiamo la stampa tramite seriale del valore letto, prevCount.
void setup() {
Serial.begin(9600);
byte hiByte = EEPROM.read(COUNT_ADDR1);
byte lwByte = EEPROM.read(COUNT_ADDR2);
int prevCount = word(hiByte, lwByte);
Serial.print("In the previous session, your Arduino counted ");
Serial.print(prevCount);
Serial.println(" events");
}
Il valore visualizzato è proprio quello che era stato immagazzinato la volta precedente che avevamo utilizzato Arduino, cioè l’ultimo che avevamo registrato prima di togliere la corrente alla scheda.
Ora nella funzione loop(), implementeremo il programma interattivo che leggerà i numeri interi immessi dall’utente per via seriale ed attenderà i comandi ‘r’ e ‘w’ per leggere e scrivere i dati registrati sulla scheda EEPROM.
Scriviamo dapprima in un annidamento if, la gestione dei caratteri numerici tra 0 e 9 che verranno interpretati come numeri e sommati al valore esistente all’interno di value, che verrà stampato ad ogni aggiornamento.
void loop() {
if( Serial.available()){
char ch = Serial.read();
if ( ch >= '0' && ch <= '9'){
value = value + (ch - '0');
Serial.print("value =");
Serial.println(value);
}
Se il carattere immesso per via seriale corrisponderà a ‘w’, allora lo sketch si occuperà di scrivere il valore sulla EEPROM.
Quindi suddividiamo l’interno nei due byte highByte e lowByte e poi i due valori verranno scritti tramite EEPROM.write().
Ri-inizializziamo il valore di value facendolo ripartire da 0.
if ( ch == 'w' ){
byte hi = highByte(value);
byte lw = lowByte(value);
EEPROM.write(COUNT_ADDR1, hi);
EEPROM.write(COUNT_ADDR2,lw);
Serial.println("value written on EEPROM");
value = 0;
}
Se invece il carattere immesso è ‘r’ (read) allora verrà effettuata una lettura del valore contenuto nelle prime due celle della EEPROM.
Si leggono quindi i due valori byte delle singole celle e poi con la funzione word() si ricompone il valore intero.
if ( ch == 'r'){ byte hi = EEPROM.read(COUNT_ADDR1); byte lw = EEPROM.read(COUNT_ADDR2); int v = word(hi, lw); Serial.print("value read on EEPROM: "); Serial.println(v); } } }
A questo punto il codice dello sketch è completo. Verifichiamo, compiliamo il codice e poi carichiamolo su Arduino.
Apriamo il monitor seriale ed attendiamo 1 o 2 secondi. Nel mio caso il valore letto su EEPROM è uguale a 0.
Cominciamo ad inserire tramite tastiera una serie di valori numerici
Una volta raggiunto un determinato valore, decidiamo di scriverlo sulla EEPROM, scriviamo ‘w’ con la tastiera e premiamo ENTER.
Adesso spegniamo e riaccendiamo la nostra scheda Arduino e poi riapriamo il monitor seriale.
Il metodo EEPROM.update
Nello sketch precedente abbiamo utilizzato EEPROM.write per scrivere i valori nelle celle di memoria della EEPROM.
Una maniera più efficiente di farlo è utilizzando il metodo EEPROM.update.
Questo si differenzia dal precedente per il fatto che la scrittura della cella di memoria verrà effettuata solo se il contenuto differisce dal valore da scrivere.
Questo evita di dover riscrivere lo stesso valore su di una cella, abbreviando così il suo periodo di vita, evitando una operazione non necessaria.
if ( ch == 'w' ){
byte hi = highByte(value);
byte lw = lowByte(value);
EEPROM.update(COUNT_ADDR1, hi);
EEPROM.update(COUNT_ADDR2,lw);
Serial.println("value written on EEPROM");
value = 0;
}
EEPROM.Get e EEPROM.Put
Nell’esempio precedente abbiamo visto i metodi write e read, che lavorano a livello di singola cella di memoria.
A più alto livello ci sono i metodi EEPROM.get e EEPROM.put che permettono di lavorare direttamente a livello di variabile, indipendentemente da quanti byte occupa.
Riscriviamo lo sketch dell’esempio precedente
#include <EEPROM.h>
const int COUNT_ADDR = 0;
int value = 0;
void setup() {
Serial.begin(9600);
int prevCount;
EEPROM.get(COUNT_ADDR, prevCount);
Serial.print("In the previous session, your Arduino counted ");
Serial.print(prevCount);
Serial.println(" events");
}
void loop() {
if( Serial.available()){
char ch = Serial.read();
if ( ch >= '0' && ch <= '9'){
value = value + (ch - '0');
Serial.print("value =");
Serial.println(value);
}
if ( ch == 'w' ){
EEPROM.put(COUNT_ADDR, value);
Serial.println("value written on EEPROM");
value = 0;
}
if ( ch == 'r'){
int v;
EEPROM.get(COUNT_ADDR, v);
Serial.print("value read on EEPROM: ");
Serial.println(v);
}
}
}
Come possiamo vedere dal codice, non è più necessario suddividere il dato in byte e gestire la loro singola scrittura.
I metodi EEPROM.get() e EEPROM.put() , a seconda del tipo di dato passato per parametro, riescono a valutare quanti byte dovranno essere gestiti. Quindi sarà necessario utilizzare un solo indirizzo.
- EEPROM.get(address, value)
- EEPROM.put(address, value)
Questo è il motivo per cui è necessario passare due parametri ad entrambe i metodi. Inoltre value viene passato per riferimento e quindi viene direttamente aggiornato. Non c’è quindi nessun valore restituito da parte del metodo EEPROM.get().
Iterazioni sulla EEPROM
Finora abbiamo visto esempi in cui si specificano indirizzi di celle singole per contenere specifiche variabili.
L’operazione più comune sulla EEPROM sarà quella di muoversi attraverso lo spazio di memoria della EEPROM. Esistono diversi approcci.
Si può usare sia il ciclo for;
for (int index = 0 ; index < EEPROM.length() ; index++) {
//Add one to each cell in the EEPROM
EEPROM[ index ] += 1;
}
il ciclo while;
int index = 0;
while (index < EEPROM.length()) {
//Add one to each cell in the EEPROM
EEPROM[ index ] += 1;
index++;
}
o anche il ciclo do while
int index = 0;
do {
//Add one to each cell in the EEPROM
EEPROM[ index ] += 1;
index++;
} while (index < EEPROM.length());
Per quanto riguarda i cicli iterativi, è molto utile la funzione EEPROM.lenght().
Questa funzione infatti restituisce un valore unsigned int che contiene la dimensionee della EEPROM, cioè il numero di celle di memoria.
Questo infatti può differire da modello a modello di Arduino.
Un Cyclic Redundancy Check (CRC) sulla EEPROM
Un CRC è un modo semplice per controllare se un dato che è stato modificato si è corrotto. In questo esempio calcola un valore CRC direttamente sui valori della EEPROM.
Questo CRC è come una firma e qualsiasi modifica nel valore di CRC calcolato, significa una modifica nei dati immagazzinati. Qui di seguito vedremo come l’oggetto EEPROM può essere usato come un array.
#include <Arduino.h>
#include <EEPROM.h>
void setup() {
//Start serial
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
//Print length of data to run CRC on.
Serial.print("EEPROM length: ");
Serial.println(EEPROM.length());
//Print the result of calling eeprom_crc()
Serial.print("CRC32 of EEPROM data: 0x");
Serial.println(eeprom_crc(), HEX);
Serial.print("\n\nDone!");
}
void loop() {
/* Empty loop */
}
unsigned long eeprom_crc(void) {
const unsigned long crc_table[16] = {
0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c
};
unsigned long crc = ~0L;
for (int index = 0 ; index < EEPROM.length() ; ++index) {
crc = crc_table[(crc ^ EEPROM[index]) & 0x0f] ^ (crc >> 4);
crc = crc_table[(crc ^ (EEPROM[index] >> 4)) & 0x0f] ^ (crc >> 4);
crc = ~crc;
}
return crc;
}