Come scrivere n >1 byte in eeprom senza bloccare il programma?

Ciao,
stavo cercando di capire come funziona la eeprom sul 328. Ho letto il datasheet, poi ho fatto una ricerca qui e mi sono imbattuto in questo interessantissimo thread (che però è chiuso in quanto vecchio di qualche mese):

http://arduino.cc/forum/index.php/topic,88054.15.html

Il mio dubbio riguarda(va) il "blocco" (attesa) del programma quando si scrive sulla eeprom, e deriva principalmente dal fatto che finora ho utilizzato soltanto le istruzini EEPROM.write() e EEPROM.read() di Arduino. Ho notato che la scrittura consecutiva di alcune centinaia di byte in eeprom (ad esempio un intero array) richiede un tempo apprezzabile, e dal punto di vista dell'effetto sul codice del programma principale somiglia molto ad una delay().

Come dicevo, per dissipare i miei dubbi ho letto il datasheet del 328 (pagg. 21 e seguenti), poi il thread suddetto e infine i sorgenti EEPROM.h e arduino-1.0.2/hardware/tools/avr/lib/avr/include/avr/eeprom.h

In quest'ultimo file ho scoperto che tutte le funzioni di accesso alla eeprom iniziano con una busy wait per attendere il completamento di un'eventuale operazione precedente di accesso alla eeprom (in pratica attendono che la periferica hw che si occupa della eeprom a basso livello sia pronta).
Inoltre il commento all'inizio del file dice che la modalità di scrittura utilizzata è quella "atomica" (erase+write), quindi i tempi di completamento della scrittura di un byte sono di 3.4ms (pag. 22 datasheet, Table 8-1).
Il risultato è che se scrivo 10 byte in fila blocco (nel senso della delay()) il mio programma per circa 34ms o_O

Vengo quindi alle mie due considerazioni e mezzo:

  1. se al momento di scrivere un byte sulla eeprom non è in corso alcuna operazione di i/o sulla eeprom stessa, allora EEPROM.write() ritorna subito, senza attendere che la scrittura termini. Questo significa che se mi accontento di scrivere in eeprom un singolo byte alla volta ad una distanza minima di 4ms l'uno dall'altro, non ho alcun rallentamento al flusso del programma.

E' giusto ?

  1. se scrivo più byte consecutivamente, come fa ad esempio fa EEPROMWriteAnything, non interferisco in alcun modo con gli interrupt, perché il busy wait sulla condizione di "eeprom pronta" è un normale ciclo wait che legge un bit da un registro, quindi può essere tranqullamente interrotto. La sezione critica, cioè le istruzioni eseguite con gli interrupt disabilitati, dura solo 4 o 5 istruzioni assembly, quindi se arriva un interrupt proprio lì in mezzo, viene memorizzato e servito non appena viene chiamata la sei() al termine della sezione critica, pochi us più tardi.

Ho capito bene ? (lo so che il punto 2 non c'entra nulla con la domanda iniziale, ma già che siamo in tema... :slight_smile: )

2.5) Se volessi salvare 'sto benedetto array di 200 byte senza bloccare il programma principale, dovrei scrivere una funzione che utilizza la tecnica di "blink without delay" per cadenzare le scritture di singoli byte ogni (diciamo) 5ms, oppure replicare per la eeprom il meccanismo di scrittura ad interrupt usato nella Serial.write delle ultime release... In pratica dovrei usare un buffer temporaneo in cui copio i dati da scrivere, poi lancio la scrittura del primo byte e lascio finire il lavoro alla ISR che risponde all'int EE READY :relaxed:

La scrittura su EEPROM occupa circa 3,3-3,4 ms.
Ma l'attesa viene svolta dalla libreria EEPROM stessa nel momento della scrittura. Essa non renderà il controllo al chiamante finché la scrittura non sarà avvenuta, per cui puoi scrivere 200 byte consecutivamente senza avere attese extra.

Ad esempio, prova questo codice:

#include "EEPROM.h"

void setup() {
    Serial.begin(19200);
    delay(2000);
}

void loop() {    
    testEeprom();
    while (1);
}

void testEeprom() {
    Serial.println("Avvio...");
    unsigned long startMillis = millis();
    for (int i = 0; i < 256; i++) {
        EEPROM.write(i, (byte)i);
    }
    unsigned long endMillis = millis();
    Serial.println(endMillis - startMillis, DEC);
}

A me restituisce 860 ms.
Se fai 860/256 scritture ottieni 3,35 ms per singola scrittura.

Qualche giorno fa ho porso una domanda simile al Programming forum. A simple question about using EEPROM in Atmega MCU - #9 by LROBBINS - Programming Questions - Arduino Forum Avendo visto questo, però, avevo ancora un dubbio. Dopo EEPROM.write il MCU aspetta i 3.4 ms prima di avviare una successiva chiamata al EEPROM.write, però è veramente "blocking"? Sono bloccate anche altre operazioni durante questo periodo? Invece di perdermi nella documentazione, ho modificato lo sketch di leo72 per fare il test.

#include "EEPROM.h"

double A = 10.0;
double B = 5;
unsigned long startTime = millis();
unsigned long endTime = millis();

void setup() {
  Serial.begin(19200);
}

void loop() {    
  testEeprom();
  while (1);
}

void testEeprom() {
  Serial.println("Avvio...");
  startTime = millis();
// comment out EEPROM.write or calcSQRT lines to get seperate times  
  for (int i = 0; i < 256; i++) {
      EEPROM.write(i, (byte)i);
      calcSQRT ();
  }
  endTime = millis();
  Serial.print (startTime, DEC);
  Serial.print (",    ");
  Serial.print (endTime, DEC);
  Serial.print (",    ");
  Serial.println(endTime - startTime, DEC);
}

void calcSQRT () {
   for (long j = 0; j < 1000; j++) {
       B = sqrt (A);
   }
}

// sqrt "for" loops only - 125 milliseconds
// EEPROM.write only - 868 milliseconds = 3.39 milliseconds per write
// both - 868 milliseconds = EEPROM.write is nonblocking
//    (except for writes themselves)

Usando // per chiamare solo EEPROM.write oppure calcSQRT, otteniamo:
(1) 256 EEPROM.write da soli usano 868 ms (3.4 per write).
(2) 256 calcSQRT da soli usano 125 ms.
e
(3) 256 EEPROM.write seguiti ognuno con calSQRT usano gli stessi 868 ms. Quindi, durante l'attesa di 3.4 ms nel quale il successivo EEPROM.write non è avviato, sono calcolati 1000 sqrt senza aggiungere neanchè un solo ms.

Perfetto. EEPROM.write non è globalmente blocking, sono solo successivi EEPROM.write che devono aspettare.

Ciao,
Lenny

Scusatemi per gli errori nel mio italiano - non sono madre lingua e non so, per esempio, la genera giusta per i termini tecnici.

Ho capito cosa vuoi dire. Per "bloccante" tu intendi che freeza tutto il codice.
Sì, in effetti non è così. Se chiami una sola EEPROM.write tu metti il byte in scrittura e poi esci dalla funzione. La funzione carica nei registri della periferica che controlla la EEPROM l'indirizzo dove scrivere ed il byte da caricare poi il controllo ritorna al programma. Sarà la periferica che eseguirà la scrittura ed avviserà il software di aver terminato il lavoro
Però se cerchi di effettuare una scrittura subito dopo la precedente, allora si va in coda perché finché non è finita la prima scrittura, la seconda non può aver luogo.

A me restituisce 860 ms.
Se fai 860/256 scritture ottieni 3,35 ms per singola scrittura.

Ho ottenuto gli stessi risultati. I tempi sono conformi a quanto riportato dal datasheet :cold_sweat:

Ho fatto qualche esperimento di scrittura non bloccante. Ne è uscito un sketch in cui ho inserito sia una "normale" routine di scrittura sequenziale, sia un gruppo di "api" per la scrittura non bloccante. Un array di 300 byte con valori fittizi rappresenta l'ipotetico insieme di dati da salvare.
Il tutto è controllabile via seriale con una semplice lista di comandi costituiti da una singola lettera.

Il risultato è che salvando in eeprom l'array con il metodo tradizionale il led smette di lampeggiare durante la scrittura, mentre ciò non accade utilizzando il metodo non bloccante. Il tempo complessivo per completare la scrittura è circa lo stesso in entrambi i casi.

Per chi fosse interessato il codice è in allegato.

:slight_smile:

EEPROM_NonBlocc.ino (7.08 KB)

Bellissimo! Anche se per il mio progetto non ne ho bisogno. Come hai notato altrove, è il secondo EEPROM.write che blocca se meno di 3.4 msec sono passati dal primo EEPROM.write. Nel mio caso, il secondo byte da scrivere arrivera per CAN bus solo dopo diversi minuti.

Per chi vorebbe spedire un insieme di dati al EEPROM nel "background", la sua procedura sembra perfetto. (Come si dice "background" in Italiano?)

Ciao,
Lenny

background :slight_smile:

@Tuxduino:
basta usare un buffer in cui inserisci i dati da scrivere in forma di tripletta: 1 byte per il dato, 2 byte per l'indirizzo.
Il vettore di interrupt è EE_READY_vect (se non ho letto male). All'interno di questa ISR controlli se il buffer contiene ancora dei dati, in caso positivo li scrivi 1 ad 1 fino all'ultimo.

leo72:
@Tuxduino:
basta usare un buffer in cui inserisci i dati da scrivere in forma di tripletta: 1 byte per il dato, 2 byte per l'indirizzo.
Il vettore di interrupt è EE_READY_vect (se non ho letto male). All'interno di questa ISR controlli se il buffer contiene ancora dei dati, in caso positivo li scrivi 1 ad 1 fino all'ultimo.

Interessante... triplicherebbe l'uso di RAM a parità di numero di byte "bufferizzati"... Tuttavia ragionare in termini di singoli byte in ingresso (alla libreria) invece che di chiamate che possono memorizzare interi array di byte in una volta rende il tutto più semplice...

L'interfaccia rimarrebbe identica a EEPROM.write(), cambierebbe ovviamente il nome dell'stanza della classe... Questa write risulterebbe bloccante solo se il buffer fosse pieno, mentre ritornerebbe istananeamente in caso contrario.

Ci lavorerò, grazie del suggerimento :slight_smile:

tuxduino:
Interessante... triplicherebbe l'uso di RAM a parità di numero di byte "bufferizzati"... Tuttavia ragionare in termini di singoli byte in ingresso (alla libreria) invece che di chiamate che possono memorizzare interi array di byte in una volta rende il tutto più semplice...

In realtà basterebbe un buffer di, che so... 32 byte? Alla fine, quante operazioni sulla EEPROM si fanno normalmente? Pochissime. In genere si tratta di scrivere pochi byte per volta.

tuxduino:
L'interfaccia rimarrebbe identica a EEPROM.write(), cambierebbe ovviamente il nome dell'stanza della classe... Questa write risulterebbe bloccante solo se il buffer fosse pieno, mentre ritornerebbe istananeamente in caso contrario.

Sì, esatto. Sarebbe tutto trasparente all'utente. A lui non dovresti far sapere nulla, tutto avverrebbe dietro le quinte.

Ho precisato:

a parità di numero di byte "bufferizzati"

comunque sono d'accordo con te: basta un buffer di poche decine di byte :slight_smile: