Arduino

Arduino: gestione memorie interne

Tutorial alla gestione delle memore EEPROM integrate nel microcontrollore ATmega328PU

I microcontrollori possiedono al loro interno una memoria EEPROM che può essere tranquillamente utilizzata e sfruttata nei nostri sketch. La memoria EEPROM permette la memorizzazione di una informazione in modalità non volatile, ovvero se il microcontrollore viene resettato o disalimentato l’informazione viene mantenuta. Ogni microcontrollore ha una ben definita memoria utilizzabile: ad esempio le schede ArduinoUNO (ATmega328P) possiedono 1 Kbyte (1024 byte) di memoria mentre le schede ArduinoMEGA2560 (ATmega2560) possiedono 4 Kbyte (4096 byte).

Le tre memorie del microcontrollore: Flash, SRAM ed EEPROM

Oltre alla EEPROM, ogni microcontrollore AVR dispone di altre due memorie: la Flash, non volatile, che contiene il programma (sketch e bootloader), e la SRAM, volatile, che contiene le variabili durante l’esecuzione e si azzera a ogni reset o spegnimento. L’ATmega328P di Arduino UNO ha 32 kB di Flash (0,5 kB occupati dal bootloader), 2 kB di SRAM e 1 kB di EEPROM; l’ATmega2560 di Arduino MEGA ha 256 kB di Flash, 8 kB di SRAM e 4 kB di EEPROM. Poiché la SRAM è la risorsa più scarsa, le stringhe costanti possono essere lasciate in Flash con la macro F(), ad esempio Serial.println(F(“testo”));, e le tabelle costanti con l’attributo PROGMEM. Per approfondire l’architettura dell’ATmega328P si veda anche l’articolo sulla registri e bit manipulation su ATmega328P.

Organizzazione della memoria EEPROM

Le singole memorie dei microcontrollori sopra riportati sono tutte da 1 byte (ovvero 8 bit) e, a livello di gestione software, necessitano di sole due informazioni:

  • indirizzo: ovvero in quale memoria si trova il dato di mio interesse
  • valore: valore dell’informazione presente nella memoria.

La libreria EEPROM

L’informazione indirizzo verrà gestita come variabile di tipo integer, invece il valore verrà gestito come variabile unsigned char (un byte, quindi valori interi da 0 a 255). Per la gestione di tali memorie negli sketch è necessario includere nel programma la libreria EEPROM.h presente di default nelle librerie ufficiali Arduino. I comandi utilizzati per la gestione delle memorie sono:

EEPROM.read(indirizzo); // permette di leggere il valore memorizzato in una determinata memoria

EEPROM.write(indirizzo, valore); // scrive un valore in una determinata memoria

EEPROM.update(indirizzo, valore); // scrive il valore in una determinata memoria solo se è diverso da quello già presente

EEPROM.length(); // restituisce l'informazione della dimensione totale delle memorie

NB. Le memorie EEPROM hanno un numero massimo di cicli di scrittura/cancellazione: per l’ATmega328P e l’ATmega2560 il datasheet garantisce circa 100.000 cicli per ogni cella. Scrivere continuamente (ad esempio a ogni ciclo di loop()) porta all’usura delle celle e al non corretto funzionamento delle memorie stesse. Conviene quindi usare EEPROM.update(), che scrive solo se il valore è cambiato, salvare i dati solo quando serve e, per dati aggiornati spesso, distribuire le scritture su più indirizzi (wear leveling). La lettura invece non usura la memoria.

Salvare dati di più byte con put() e get()

I comandi read/write/update gestiscono un solo byte. Per salvare variabili di tipo int, long, float o intere strutture si usano EEPROM.put(indirizzo, variabile) e EEPROM.get(indirizzo, variabile), che scrivono e leggono automaticamente tutti i byte occupati dal dato (put scrive solo i byte modificati, come update). Bisogna solo fare attenzione a non sovrapporre gli indirizzi, aiutandosi con sizeof():

#include <EEPROM.h>

struct Impostazioni {
  int soglia;
  float calibrazione;
};

void setup() {
  Serial.begin(9600);

  int contatore = 1234;
  float temperatura = 21.5;
  Impostazioni imp = {512, 1.02};

  EEPROM.put(0, contatore);               // int: occupa sizeof(int) byte
  EEPROM.put(sizeof(int), temperatura);   // float: subito dopo l'int
  EEPROM.put(10, imp);                    // struttura a partire dall'indirizzo 10

  int c;
  float t;
  Impostazioni letto;
  EEPROM.get(0, c);
  EEPROM.get(sizeof(int), t);
  EEPROM.get(10, letto);

  Serial.println(c);
  Serial.println(t);
  Serial.println(letto.soglia);
  Serial.println(letto.calibrazione);
}

void loop() {
}

Schede con EEPROM emulata. Non tutte le schede hanno una vera EEPROM: su Arduino UNO R4, ESP32 e RP2040 (Raspberry Pi Pico) la libreria EEPROM emula la memoria in una porzione della flash. Su ESP32 e RP2040 bisogna chiamare EEPROM.begin(dimensione) nel setup e EEPROM.commit() dopo le scritture per renderle effettive; anche in questi casi il numero di cicli di scrittura della flash è limitato.

Esempio

Si vuole realizzare un sistema basato su quattro pulsanti (ingressi digitali) e la comunicazione seriale. La funzione di ogni singolo pulsante è la seguente:

PULSANTE 1 (pin 10) = stampa sulla seriale la dimensione massima della memoria
PULSANTE 2 (pin 11) = stampa sulla seriale il valore memorizzato
PULSANTE 3 (pin 12) = incrementa il valore di +1
PULSANTE 4 (pin 13) = decrementa il valore di -1

Nota: il pin 13 è collegato anche al LED di bordo. Su Arduino UNO R3 il LED è pilotato tramite un buffer e il pin funziona correttamente come ingresso con pull-up, ma su revisioni precedenti e su alcuni cloni il LED con la sua resistenza tira verso il basso il livello e la lettura può risultare inaffidabile: in questo caso collega il pulsante 4 a un altro pin libero (ad esempio il 9) e modifica di conseguenza la variabile pulsante4.

Si andranno a sfruttare gli ingressi in modalità pull-up, come spiegato nell’articolo Arduino: input digitali, in modo da evitare l’utilizzo di quattro resistori fisici sulla breadboard. Lo schema è riportato di seguito.

Schema con quattro pulsanti collegati verso GND ai pin 10, 11, 12 e 13 di Arduino UNO per l'esempio EEPROM

#include <EEPROM.h>

int pulsante1 = 10;  // dimensione memoria
int pulsante2 = 11;  // valore memorizzato
int pulsante3 = 12;  // incrementa valore +1
int pulsante4 = 13;  // decrementa valore -1

int indirizzo_memoria = 0; // si vuole utilizzare solo la memoria dell'indirizzo 0
unsigned char valore; // creo la variabile che contiene l'informazione valore


void setup() {
  pinMode(pulsante1, INPUT_PULLUP); // pull-up interno: nessun resistore esterno
  pinMode(pulsante2, INPUT_PULLUP);
  pinMode(pulsante3, INPUT_PULLUP);
  pinMode(pulsante4, INPUT_PULLUP);

  Serial.begin(9600);
  valore = EEPROM.read(indirizzo_memoria); //leggo e memorizzo in valore il contenuto della memoria
}

void loop() {

// lettura memoria massima disponibile
  if (digitalRead(pulsante1) == LOW) {
    Serial.print("Memoria disponibile ");
    Serial.print(EEPROM.length());
    Serial.println(" byte");
    delay(500);
  }

// stampa del valore attuale (già memorizzato di seguito)
  if (digitalRead(pulsante2) == LOW) {
    Serial.print("Il valore attuale è ");
    Serial.println(valore);
    delay(500);
  }

// incremento il valore di +1 e salvo il dato nella memoria
  if (digitalRead(pulsante3) == LOW) {
    valore = valore + 1;
    EEPROM.update(indirizzo_memoria, valore);
    Serial.print("Dato memorizzato: ");
    Serial.println(valore);
    delay(500);
  }

// decremento il valore di -1 e salvo il dato nella memoria
  if (digitalRead(pulsante4) == LOW) {
    valore = valore - 1;
    EEPROM.update(indirizzo_memoria, valore);
    Serial.print("Dato memorizzato: ");
    Serial.println(valore);
    delay(500);
  }

}

Su un microcontrollore nuovo o dopo una cancellazione completa tutte le celle della EEPROM contengono 0xFF: alla prima accensione il valore letto sarà quindi 255, e non si tratta di un errore. Mantenendo premuto il pulsante 3 o 4, inoltre, il valore cambia ogni 500 ms e a ogni passo viene eseguita una scrittura in EEPROM: in un’applicazione reale conviene salvare il dato solo quando l’utente ha terminato la regolazione, per limitare l’usura delle celle.

Essendo valore di tipo unsigned char, i conteggi sono limitati all’intervallo 0–255: decrementando da 0 si passa a 255 e incrementando da 255 si torna a 0 (overflow). Per valori più ampi si può usare un int salvato con EEPROM.put()/get(). Il delay(500) evita inoltre che una singola pressione venga contata più volte; per una gestione più accurata dei rimbalzi si veda l’articolo sui circuiti di debouncing.

Admin | MST-Tutorial

Appassionato di elettronica, elettrotecnica e programmazione. Fondatore di MST-Tutorial, condivido con entusiasmo conoscenze, esperienze e progetti legati all’innovazione tecnologica, con l’obiettivo di rendere accessibili concetti tecnici complessi attraverso tutorial chiari e pratici.

Articoli Correlati

Pulsante per tornare all'inizio