Lavorare con la memoria PROGMEM.

Spesso è necessario memorizzare una grande quantità di dati nella memoria del microcontrollore che non cambierà durante il funzionamento, ad esempio:
  • Array di calibrazione.
  • Testo dei nomi delle voci di menu.
  • Testo.
  • Calcoli trigonometrici (seno, coseno, ...).
  • Immagini per la visualizzazione sul display (bitmap).
  • ... e altro ancora
Memorizzare tali dati nella RAM (sotto forma di una variabile normale) non è l'idea migliore, perché non cambierà, ma occuperà spazio! La RAM è sempre molto inferiore alla memoria di programma (Flash): nella stessa ATmega328 (Arduino UNO / Nano / Pro mini) - 32 kb Flash e 2 kb SRAM, 16 volte in meno! Quindi è molto più efficiente archiviare tali dati in Flash, alias PROGMEM.
Siamo abituati al fatto che possiamo cambiare le variabili durante l'esecuzione del programma, ecco perché sono variabili, ecco perché la memoria si chiama dinamica. Ma con la memoria Flash, tutto non è così semplice: solo un programmatore può scriverci, con l'aiuto del quale viene caricato il codice del programma, o un bootloader, che praticamente svolge la funzione di un programmatore. Per lavorare con PROGMEM, viene utilizzata la libreria integrata avr/pgmspace.h, non è necessario collegarla, si connetterà da sola (nelle versioni IDE di Arduino superiori alla 1.0).
Nota: in esp8266/esp32, PROGMEM funziona esattamente allo stesso modo e utilizza la stessa sintassi e costrutti.

Registrazione.
La parola chiave (modificatore di variabile) PROGMEM consente di scrivere dati su memoria Flash. La sintassi è:
  • const tipo_dati data[] PROGMEM = {}; // così
  • const PROGMEM tipo_dati data[] = {}; // o così
Tutto quanto! I dati, nel caso mostrato in array data, verranno inseriti nella memoria Flash. PROGMEM può funzionare con tutti i tipi di interi (8, 16, 32, 64 bit), float e char. 
Punto importante! Il modificatore PROGMEM può essere applicato solo a variabili globali (definite al di fuori delle funzioni) o statiche (globali o locali, ma con la parola static).

Lettura.
Se tutto è molto semplice con la scrittura (viene aggiunta una parola chiave), allora con la lettura è tutto molto più interessante: si esegue utilizzando apposite funzioni. La funzione principale di lettura dal programma è pgm_read_type(address). Possiamo usare questi 4:
  • pgm_read_byte(data); - per il 1 byte (char, byte, int8_t, uint8_t).
  • pgm_read_word(data); - per 2 byte (int, word, unsigned int, int16_t).
  • pgm_read_dword(data); - per 4 byte (long, unsigned long, int32_t).
  • pgm_read_float(dati); - per i numeri in virgola mobile.
Dove data è l'indirizzo (o puntatore) del blocco dati memorizzato.
Un elenco completo delle funzionalità di avr/pgmspace.h può essere trovato nella documentazione.

Singoli numeri.
Considera di scrivere e leggere numeri singoli:
const uint16_t data PROGMEM = 125;
const int16_t signed_data PROGMEM = -654;
const float float_data PROGMEM = 3.14;
void setup() 
     Serial.begin(9600); 
     Serial.println(pgm_read_word(&data)); // verrà visualizzato 125 
     uint16_t *dataPtr = &data; // proviamo con un puntatore 
     Serial.println(pgm_read_word(dataPtr)); // verrà visualizzato 125 
     Serial.println(pgm_read_word(&signed_data)); // verrà visualizzato 64882
     Serial.println((int16_t)pgm_read_word(&signed_data)); // verrà visualizzato -654
     Serial.println(pgm_read_float(&float_data)); // verrà visualizzato 3.14
}
Cosa è importante ricordare qui: quando si leggono numeri negativi (ad esempio, int e long), è necessario eseguire sempre il cast del tipo, perché PROGMEM memorizza i numeri nella rappresentazione senza segno. Presta attenzione alla lettura dei signed_data dall'esempio sopra, senza eseguire il cast su int, il numero viene visualizzato in modo errato.

Array unidimensionali.
Con array di numeri, tutto è abbastanza previsto:
const uint8_t data[] PROGMEM = {10, 20, 30, 40};
void setup() 
     Serial.begin(9600); 
     for (byte i = 0; i < 4; i++) 
         { 
               Serial.println(pgm_read_byte(&data[i])); // verrà visualizzato 10 20 30 40
         } 

Quando si crea un array bidimensionale, è necessario specificare la dimensione di almeno una delle dimensioni:
const uint16_t data[][5] PROGMEM = { 
     {10, 20, 30, 40, 50}, 
     {60, 70, 80, 90, 100}, 
     {110, 120, 130, 140, 150},
};
void setup() 
     Serial.begin(9600); 
     // verrà visualizzato 70, seconda riga, seconda colonna
     Serial.println(pgm_read_word(&data[1][1])); 
     // verrà visualizzato 150, terza riga quinta colonna 
     Serial.println(pgm_read_word(&data[2][4]));


Array di arrays.
È possibile memorizzare più array in uno dichiarando una cosiddetta tabella di collegamento, ovvero un altro array che contiene i puntatori agli array di dati. Questa opzione differisce da un array bidimensionale in quanto il numero di "colonne" in ogni riga può essere qualsiasi, non necessariamente lo stesso:
// arrays
const uint16_t data0[] PROGMEM = {10, 20, 30, 40, 50};
const uint16_t data1[] PROGMEM = {60, 70, 80, 90, 100};
const uint16_t data2[] PROGMEM = {110, 120, 130, 140, 150};
const uint16_t data3[] PROGMEM = {160, 170, 180, 190, 200}; 
// tabella dei collegamenti
const uint16_t* const data_array[] PROGMEM = {data0, data1, data2, data3};
void setup() 
     Serial.begin(9600); 
     // verrà visualizzato 170, il secondo elemento della quarta array
     Serial.println(pgm_read_word(&data_array[3][1]));
}

Stringhe in PROGMEM.
La stringa (come un array di caratteri) è memorizzata nella RAM del programma. Si tratta di stringe come:
char str[] = "Ciao!"; 
PROGMEM consente di memorizzare stringhe di caratteri nella memoria del programma. Questo è molto conveniente, perché la maggior parte del testo non cambia mentre il programma è in esecuzione: i nomi delle voci di menu, i nomi dei parametri di richieste del sito, parti statiche delle pagine Web e così via. Più avanti nel testo, per brevità, chiameremo tali stringe PGM-stringa, o stringe nella memoria di programma. 
Le manipolazioni con la PGM-stringa (output, addizione con altre stringhe, passaggio a funzioni) richiederanno ulteriori trasformazioni. Come mai? Il programma non sa che la stringa non è memorizzata nella RAM: perché è una normale stringa const char*. Ma i dati si trovano in un'area di memoria diversa. Se inizi a leggerli come una stringa normale, puoi leggere "spazzatura", altrimenti il ​​programma si bloccherà completamente a causa di errori di lettura all'indirizzo specificato.
Per comodità del programmatore, esiste un "tipo di dati" PGM_P, che è una macro su const char *, ovvero è solo un puntatore a una stringa. Questo viene fatto per separare visivamente le linee ordinarie dalle PGM-stringa nel programma:
  • const char* - stringa nella RAM
  • PGM_P - stringa nella memoria del programma
C'è un intero set di funzioni per lavorare con le PGM-stringe, questi sono analoghi di "programma" delle funzioni di stringe: sono le stesse funzioni, ma con il suffisso _P. Il set completo può essere trovato nella documentazione.
Considera prima la scrittura e poi la lettura, utilizzando strumenti diversi.

Stringa globale.
La stringa è dichiarata globalmente, cioè al di fuori delle funzioni nel programma. Questo è comodo e vantaggioso quando la stringa verrà utilizzata più volte nel programma. Dichiarandolo una volta, eviteremo duplicati. La sintassi è la seguente, la stringa è dichiarata come un array negli esempi precedenti:
const char message[] PROGMEM = "global PGM-stringa";
Puoi fare riferimento a questa stringa nel programma con il suo nome message.

Array di stringhe.
A volte è conveniente memorizzare più stringhe con lo stesso nome, ad esempio per le voci di menu. In questo caso, puoi utilizzare un array di stringhe. Il meccanismo è esattamente lo stesso di quello dell'array di arrays, che abbiamo considerato un po' più alto: creiamo le stringhe stesse, quindi un array con i puntatori ad esse:
// dichiarare "stringhe"
const char str1[] PROGMEM = "Period";
const char str2[] PROGMEM = "Work";
const char str3[] PROGMEM = "Stop";
// dichiarare una tabella di collegamenti
const char* const names[] PROGMEM = { 
     str1, str2, str3,
};
È possibile fare riferimento alla stringa desiderata nel programma in base all'indice nella array names, ad esempio names[1].
Stringa locale.A volte è conveniente dichiarare e utilizzare PGM-stringe localmente all'interno di una funzione, ad esempio se la stringa viene utilizzata solo in quella funzione e da nessun'altra parte. Per fare ciò, avvolgi il testo in una macro PSTR(), che inserirà il testo in PROGMEM e restituirà un puntatore di tipo const char * (usiamo PGM_P per non essere confuso con una stringa normale): 
PGM_P strp = PSTR("PGM_stringa locale");

Lettura di righe da PROGMEM.
Riscrivi nel buffer.
È possibile copiare una stringa da PROGMEM alla RAM per le seguenti attività:
  • Cambio di stringa.
  • Funziona come con una normale stringa C nella RAM.
  • Invio a una funzione che accetta un tipo char* (stringa nella RAM).
Per questo hai bisogno di:
  • Determina la dimensione della PGM-stringe usando la funzione strlen_P().
  • Crea un buffer - un array di caratteri della dimensione desiderata
  • Copia la PGM-stringe nel buffer usando la funzione strcpy_P()
// stringa globale
const char pstr_g[] PROGMEM = "Global PGM-string";
// array di stringhe globale
const char str1[] PROGMEM = "Period";
const char str2[] PROGMEM = "Work";
const char str3[] PROGMEM = "Stop";
const char* const str_list[] PROGMEM = { 
     str1, str2, str3
};
void setup() 
     Serial.begin(9600); 
     char buf_g[strlen_P(pstr_g)]; 
     strcpy_P(buf_g, pstr_g); 
     Serial.println(buf_g); // Global PGM_string
     char buf_list[strlen_P(str_list[1])]; 
     strcpy_P(buf_list, str_list[1]); 
     Serial.println(buf_list); // Work
     // stringa locale 
     PGM_P pstr_l = PSTR("Local PGM-string"); 
     char buf_l[strlen_P(pstr_l)]; 
     strcy_P(buf_l, pstrp_l); 
     Serial.println(buf_l); // Local PGM-string
}
void loop()
{
}

Converti in __FlashStringHelper*.
Il metodo è adatto per le seguenti attività:
  • Invio di una PGM-stringa a una funzione che accetta un tipo __FlashStringHelper*.
  • Aggiungi la PGM-stringa a String (String supporta __FlashStringHelper).
  • "Stampa" una PGM-stringa utilizzando print()/println() su una porta monitor/display/web/qualsiasi oggetto della classe Print standard.
Il framework Arduino ha uno strumento molto utile che ti permette di lavorare con le PGM-stringe, si chiama __FlashStringHelper. Senza entrare nei dettagli, assumeremo che questo sia solo un altro tipo di dati stringa insieme a char* e String. Alcune funzioni nelle librerie accettano questo tipo di dati (può essere trovato nella documentazione o nel file di intestazione della libreria), che ti consente di passare loro PGM-stringe senza ulteriori passaggi, devi solo convertire la variabile in (const __FlashStringHelper* ). Per esempio:
Serial.println((const __FlashStringHelper*)str_pgm); // str_pgm - PGM-stringa
Nel "core" di esp8266/esp32 è presente una comoda macro FPSTR(string) per tale conversione, non è chiaro perché non sia stata realizzata per l'AVR Arduino. Puoi dichiarare tu stesso una macro e inserirla all'inizio del programma: #define FPSTR(pstr) (const __FlashStringHelper*)(pstr)
E il codice precedente apparirà più compatto:
Serial.println(FPSTR(str_pgm)); // str_pgm - PGM-stringa
Esempio completo:
// stringa globale
const char pstr_g[] PROGMEM = "Global PGM-string";
// array di stringhe globale
const char str1[] PROGMEM = "Period";
const char str2[] PROGMEM = "Work";
const char str3[] PROGMEM = "Stop";
const char* const str_list[] PROGMEM = { 
     str1, str2, str3
};
void setup() 
     Serial.begin(9600); 
     // stringa locale  
     PGM_P pstr_l = PSTR("Local PGM-string");
     // output tramite print()
     Serial.println(FPSTR(pstr_g)); // Global PGM-string 
     Serial.println(FPSTR(str_list[2])); // Stop  
     Serial.println(FPSTR(pstr_l)); // Local PGM-string
     // caricare nella stringa 
     String s; 
     s += FPSTR(pstr_g); 
     s += FPSTR(str_list[2]); 
     s += FPSTR(pstr_l); 
     Serial.println(s); 
     // Global pgm stringStopLocal PGM-string
}
 void loop()
{
}

Passa alla funzione _P.
Il metodo è adatto per le seguenti attività:
  • Passaggio di una PGM-stringa a una funzione che accetta il tipo PGM_P (principalmente funzioni con il suffisso _P).
  • Comodo posizionamento del testo in PROGMEM e passaggio immediato a una funzione che accetti il ​​tipo PGM_P.
Alcune funzioni nelle librerie supportano il lavoro diretto con le PGM-stringhe: prendono il tipo di dati const char* o PGM_P e devono avere il suffisso _P nel nome. Ad esempio write_P(PGM_P buf) dalla libreria a esp8266 (può essere trovato nella documentazione o nel file di intestazione della libreria). Ciò significa che una stringa PGM può essere passata a tale funzione senza ulteriori trasformazioni:
// stringa globale
const char pstr_g[] PROGMEM = "Global PGM-string"; 
// array di stringhe globale  
const char str1[] PROGMEM = "Period"; 
const char str2[] PROGMEM = "Work"; 
const char str3[] PROGMEM = "Stop"; 
const char* const str_list[] PROGMEM = { 
     str1, str2, str3 
}; 
void setup() 
     Serial.begin(9600); 
     // stringa locale
     PGM_P pstr_l = PSTR("Local PGM-string"); 
     client.write_P(pstr_g); 
     client.write_P(str_list[2]); 
     client.write_P(pstr_l); 
     client.write_P(PSTR("Inline PGM-string")); 
void loop()
{


Macro F().
Il metodo è adatto per le seguenti attività:
  • Comodo posizionamento del testo in PROGMEM e passaggio immediato a una funzione che assume il tipo __FlashStringHelper*.
  • Compreso per il montaggio String.
Potresti pensare, perché creare una stringa PSTR() e una variabile PGM_P per il bene di un output? In effetti, forse solo print((const __FlashStringHelper*)PSTR("Hello, World!"))? Si, puoi! Inoltre, tutto è già stato inventato per noi e si chiama F() macro, questa comoda macro rende ancora più facile memorizzare le stringhe nella memoria del programma per l'invio a funzioni che supportano __FlashStringHelper (può essere trovato nella documentazione o nell'intestazione della libreria file):
void setup() 
     Serial.begin(9600); 
     Serial.println(F("Inline PGM-string")); 
     String s; 
     s += F("Hello, "); 
     s += F("World!"); 
     Serial.println(s); 
void loop()
{


Macro F() + __FlashStringHelper.
F-macro consente inoltre di creare e memorizzare stringhe di tipo __FlashStringHelper*. Le stringhe F non sono ottimizzate dal compilatore, ad esempio qui:
Serial.print(F("Hello!"));
lcd.print(F("Hello!"));
Le stringhe occuperanno spazio nella memoria del programma come due stringhe, ovvero il compilatore non concatenerà due stringhe identiche, come fa con la normale manipolazione delle stringhe. Pertanto, la stringa F può essere creata separatamente e passata alle funzioni necessarie o aggiunta alla stringa, ma questo può essere fatto solo localmente:
void setup() 
{
     const __FlashStringHelper* str = F("Hello!"); 
     Serial.println(str); 
     lcd.println(str); 
     String s = str; s += str; 
}

Problema con cicli (AVR).
C'è ancora un'altra cosa da discutere: considera la "array di stringhe" nel programma, che abbiamo chiamato str_list negli esempi precedenti. Se emetti le righe manualmente, tutto funzionerà correttamente, ad esempio tramite FPSTR():
Serial.println(FPSTR(str_list[0])); // Period
Serial.println(FPSTR(str_list[1])); // Work
Serial.println(FPSTR(str_list[2])); // Stop
Ma non appena stampiamo tutte le righe in un ciclo, il programma si interromperà:
for (int i = 0; i < 3; i++) 
{ 
     Serial.println(FPSTR(str_list[i]));
} 
Questo accade almeno sugli AVR (Arduino Nano, UNO, ecc.), su ESP8266/ESP32, la lettura in loop funziona correttamente.
Sull'AVR, il compilatore non ha potuto ottimizzare il processo di lettura perché non conosce il valore della variabile contatore in una particolare stringa di codice. Puoi aggirare questo problema convertendo i dati tramite funzioni. Convertiremo nella dimensione che il puntatore occupa in memoria: questo è pgm_read_word per AVR (16 bit). È necessario passare "l'indirizzo" della variabile alla funzione e l'array è essenzialmente un puntatore a se stesso, ovvero solo pgm_read_word(str_list). Questo è l'indirizzo del primo elemento (zero). Di conseguenza, per accedere all'elemento successivo, è necessario aumentare l'indirizzo di 1 e ottenere pgm_read_word(str_list + i). Sì, diverso dall'accesso all'array:
for (int i = 0; i < 3; i++) 
     Serial.println(FPSTR(pgm_read_word(str_list + i)));
}
Per comodità, puoi salvare il risultato su un puntatore di tipo PGM_P e usarlo ulteriormente: 
for (int i = 0; i < 3; i++) 
{
     // per comodità, otteniamo un puntatore
     PGM_P pstr = pgm_read_word(str_list + i); 
     // uscita tramite FPSTR
     Serial.println(FPSTR(pstr)); 
     // riscrivi nel buffer e invia 
     char buf_g[strlen_P(pstr)]; 
     strcpy_P(buf_g, pstr); 
     Serial.println(buf_g); 
     // inviare 
     client.write_P(pstr); 
} 
Crea il tuo sito web gratis! Questo sito è stato creato con Webnode. Crea il tuo sito gratuito oggi stesso! Inizia