Memoria Flash STM32: Scrivere, Cancellare e Gestire Dati Persistenti a Runtime
Ogni dispositivo embedded di produzione deve prima o poi memorizzare dati persistenti — coefficienti di calibrazione, numeri di serie, credenziali di rete o log applicativi. Su un STM32 senza EEPROM esterna, la memoria flash integrata è l'unico storage non volatile disponibile. Scrivere sulla flash a runtime è completamente diverso dallo scrivere in RAM: devi gestire sequenze di sblocco, vincoli di cancellazione-prima-scrittura, allineamento e timing. Questo articolo copre il modello di programmazione della flash a registro su STM32F4, G0 e U5, con pattern pratici per emulazione EEPROM e integrazione con bootloader.
Architettura della flash: settori vs pagine
La flash STM32 non è organizzata in modo uniforme. La granularità di cancellazione — la regione più piccola cancellabile in una singola operazione — varia per famiglia:
- STM32F4 (F401/F411/F412): la flash è divisa in settori. Su un dispositivo da 512 KB, i settori sono da 16 KB (4 settori), 64 KB (1 settore) e 128 KB (3 settori). Non puoi cancellare meno di un intero settore.
- STM32G0/G4: la flash è divisa in pagine, tipicamente da 2 KB ciascuna. Granularità molto più fine, che semplifica l'emulazione EEPROM.
- STM32U5: architettura dual-bank con pagine da 4 KB o 8 KB, a seconda della configurazione dei banchi. Supporta read-while-write (RWW) quando i banchi sono indipendenti.
- STM32L0/L4: pagine da 128 o 256 byte (L0) e 2 KB (L4). Le pagine piccole su L0 sono eccellenti per emulare EEPROM senza memoria esterna.
Anche i tempi di cancellazione differiscono: cancellare un settore da 16 KB su F4 richiede circa 50–100 ms, mentre cancellare una pagina da 2 KB su G0 richiede circa 5–10 ms. Il firmware deve tollerare queste latenze — non cancellare mai da un contesto di interrupt a meno che tu non possa accettare lo stallo.
Registri del controller flash (STM32F4 come riferimento)
L'interfaccia flash è controllata attraverso i registri FLASH->CR (controllo), FLASH->SR (stato) e FLASH->ACR (controllo accesso). I bit chiave sono:
- FLASH_CR_PG (bit 0): Abilita programmazione. Deve essere impostato prima di ogni scrittura.
- FLASH_CR_SER (bit 1): Abilita cancellazione settore.
- FLASH_CR_SNB (bit 3–6): Numero del settore da cancellare.
- FLASH_CR_STRT (bit 16): Avvia l'operazione di cancellazione.
- FLASH_CR_LOCK (bit 31): Bit di blocco. Impostato dopo il reset; deve essere sbloccato prima di ogni scrittura/cancellazione.
- FLASH_SR_BSY (bit 0): Flag di occupato. Vai a polla tra un'operazione e l'altra.
- FLASH_SR_PGSERR / FLASH_SR_PGPERR: Errori di sequenza di programmazione e parallelismo.
La sequenza di sblocco prevede la scrittura di 0x45670123 e poi 0xCDEF89AB in FLASH->KEYR. Scrivere qualsiasi altro valore blocca permanentemente il controller fino al prossimo reset.
Scrittura flash a registro
Scrivere una parola da 32 bit sulla flash di un STM32F4 segue questa sequenza:
// 1. Sblocca il controller flash
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
// 2. Attendi il completamento di operazioni precedenti
while (FLASH->SR & FLASH_SR_BSY);
// 3. Abilita la programmazione
FLASH->CR |= FLASH_CR_PG;
// 4. Scrivi la parola (indirizzo allineato a 32 bit!)
*(volatile uint32_t *)0x08040000 = 0xAABBCCDD;
// 5. Attendere il completamento (10–50 µs per parola)
while (FLASH->SR & FLASH_SR_BSY);
// 6. Verifica: leggi e confronta
if (*(volatile uint32_t *)0x08040000 != 0xAABBCCDD) {
// Gestisci errore di scrittura
}
// 7. Blocca il controller
FLASH->CR |= FLASH_CR_LOCK;
Vincoli principali:
- L'indirizzo di destinazione deve essere allineato a 32 bit. Le scritture a 16 bit sono supportate con l'impostazione PSIZE corretta, ma le scritture a 8 bit no — devi fare un ciclo read-modify-write.
- Puoi scrivere solo su una locazione cancellata (0xFF). Se la parola di destinazione non è 0xFFFFFFFF, la scrittura fallisce silenziosamente (F4) o produce un errore di protezione.
- La tensione di alimentazione deve essere nel range specificato durante la scrittura. Su F4, VDD deve essere tra 1.8 V e 3.6 V — un brownout durante la programmazione corrompe la flash.
Scrittura di più parole
Per efficienza, puoi mantenere FLASH_CR_PG impostato e scrivere parole consecutive. L'hardware gestisce la sequenza interna di programmazione per ogni parola. Polla BSY tra una scrittura e l'altra:
FLASH->CR |= FLASH_CR_PG;
for (size_t i = 0; i < 64; i++) { // 64 parole = 256 byte
((volatile uint32_t *)dest)[i] = data[i];
while (FLASH->SR & FLASH_SR_BSY);
}
FLASH->CR &= ~FLASH_CR_PG;
Su STM32U5 e G4 puoi usare la funzionalità di burst programming (FLASH_CR_BURST) che programma più parole con meno cicli di attesa della CPU. Consulta il manuale di riferimento per la tua serie specifica.
Cancellazione flash a registro
Cancellare un settore su STM32F4:
// 1. Sblocca
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
while (FLASH->SR & FLASH_SR_BSY);
// 2. Seleziona la modalità di cancellazione settore
FLASH->CR |= FLASH_CR_SER; // Modalità cancellazione settore
FLASH->CR &= ~(0x78); // Pulisci i bit SNB
FLASH->CR |= (5 << 3); // SNB = 5 (settore 5, 128 KB su F401)
// 3. Avvia cancellazione
FLASH->CR |= FLASH_CR_STRT;
// 4. Attendi il completamento (50–300 ms)
while (FLASH->SR & FLASH_SR_BSY);
// 5. Blocca
FLASH->CR &= ~FLASH_CR_SER;
FLASH->CR |= FLASH_CR_LOCK;
Su G0/G4 con cancellazione a pagina, il registro è simile ma usa FLASH_CR_PER (page erase) e FLASH_CR_PNB (page number) invece di SER/SNB.
Option byte
Gli option byte controllano la configurazione hardware — protezione dalla lettura (RDP), watchdog hardware, modalità di boot e settori protetti da scrittura. Sono memorizzati in una regione flash dedicata e devono essere programmati con una sequenza di sblocco separata:
// Sblocca option byte
FLASH->OPTKEYR = 0x08192A3B;
FLASH->OPTKEYR = 0x4C5D6E7F;
// Abilita programmazione option byte
FLASH->CR |= FLASH_CR_OPTPG;
// Scrivi il valore (esempio: imposta RDP al livello 1)
*(volatile uint32_t *)0x1FFFC000 = 0xCC;
while (FLASH->SR & FLASH_SR_BSY);
// Ricarica option byte (innesca un reset su alcune serie)
FLASH->CR |= FLASH_CR_OPTSTRT;
while (FLASH->SR & FLASH_SR_BSY);
Attenzione: impostare RDP livello 2 disabilita permanentemente il debug — è irreversibile. Sui progetti cliente, imposto RDP livello 1 (debug ancora possibile ma letture flash dal debugger bloccate) solo dopo aver validato il fixture di programmazione in produzione. Mai prototipare con RDP livello 2.
Emulazione EEPROM: il pattern a due pagine con scambio
Poiché la flash non può essere scritta in-place (devi prima cancellare l'intero settore/pagina), non può essere usata come una EEPROM. La soluzione standard è un algoritmo a due pagine con scambio:
- Alloca due pagine flash identiche (Pagina A, Pagina B).
- Inizia con la Pagina A attiva. Tutte le scritture vanno al prossimo slot libero nella Pagina A.
- Quando la Pagina A si riempie, scrivi la copia più recente di ogni variabile sulla Pagina B e cancella la Pagina A.
- La Pagina B diventa attiva; il processo si ripete al contrario.
Ecco uno schema implementativo minimale:
#define PAGE_A_ADDR 0x08040000
#define PAGE_B_ADDR 0x08040800 // Pagine da 2 KB su G0/G4
#define PAGE_SIZE 2048
typedef struct {
uint16_t id; // Identificativo variabile
uint16_t len; // Lunghezza dati (byte)
uint8_t data[252]; // Payload
} eeprom_record_t;
static uint32_t current_page = PAGE_A_ADDR;
void eeprom_write(uint16_t id, const uint8_t *data, uint16_t len) {
// Trova il prossimo slot libero nella pagina corrente
// Se non c'è spazio, fai lo swap
}
void eeprom_init(void) {
// Scansiona Pagina A e B per determinare quale è attiva
}
Su STM32F4 con settori da 16 KB, questo pattern spreca più spazio ma funziona comunque per storage di calibrazione. Solitamente riservo il settore 11 o 12 (vicino alla fine della flash) per i dati. Su G0 con pagine da 2 KB, l'overhead è minimo — ideale per 100–200 parametri.
Esempio pratico: scrittura dati di calibrazione su STM32G070
Lo STM32G070RB ha 128 KB di flash con pagine da 2 KB. Riservo la pagina 63 (l'ultima, 0x0803F800) per i dati di calibrazione:
#define CAL_PAGE_ADDR 0x0803F800
#define CAL_MAGIC 0xCA1I
typedef struct __attribute__((packed)) {
uint32_t magic;
float gain_x;
float offset_x;
float gain_y;
float offset_y;
uint16_t checksum;
} cal_data_t;
int cal_save(const cal_data_t *cfg) {
// Prima cancella la pagina
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
while (FLASH->SR & FLASH_SR_BSY);
FLASH->CR |= FLASH_CR_PER;
FLASH->CR &= ~FLASH_CR_PNB_Msk;
FLASH->CR |= (63 << FLASH_CR_PNB_Pos); // Pagina 63
FLASH->CR |= FLASH_CR_STRT;
while (FLASH->SR & FLASH_SR_BSY);
FLASH->CR &= ~FLASH_CR_PER;
// Scrivi i dati
const uint32_t *src = (const uint32_t *)cfg;
FLASH->CR |= FLASH_CR_PG;
for (int i = 0; i < sizeof(cal_data_t) / 4; i++) {
((volatile uint32_t *)CAL_PAGE_ADDR)[i] = src[i];
while (FLASH->SR & FLASH_SR_BSY);
}
FLASH->CR &= ~FLASH_CR_PG;
FLASH->CR |= FLASH_CR_LOCK;
// Verifica
return memcmp((void *)CAL_PAGE_ADDR, cfg, sizeof(cal_data_t)) == 0 ? 0 : -1;
}
Questo approccio è semplice, deterministico e sopravvive a perdite di alimentazione impreviste — o la scrittura è completa (magic + checksum validi) oppure no (magic assente), e in tal caso cal_load() restituisce i valori predefiniti di fabbrica. Su un progetto cliente, aggiungo una seconda pagina di backup in modo che se la scrittura viene interrotta, la calibrazione precedente valida sia ancora disponibile.
Considerazioni sul bootloader
Se il firmware include un bootloader, la scrittura/cancellazione flash viene tipicamente eseguita dal contesto del bootloader, non dall'applicazione. L'applicazione invia l'immagine del firmware via UART/CAN/SPI, la memorizza temporaneamente in RAM o in un'area scratch, poi salta al bootloader che programma la flash principale:
- Rilocazione del vettore: il bootloader risiede nel settore 0 (0x08000000) con la propria tabella dei vettori. L'applicazione parte dal settore 1 (es. 0x08008000 su F401 con bootloader da 32 KB).
- Contesto interrupt-safe: non programmare la flash mentre gli interrupt sono attivi. Disabilita gli interrupt globali durante il ciclo di scrittura/cancellazione, o assicurati che nessuna ISR acceda alla flash o usi un vettore dalla regione in cancellazione.
- Gestione watchdog: una cancellazione di settore può richiedere fino a 300 ms. Aggiorna l'IWDG prima di iniziare, o passa a un periodo di timeout più lungo. Ho debugato innumerevoli dispositivi "bricked-on-update" dove l'IWDG ha resettato il MCU a metà cancellazione, lasciando un'immagine parziale.
Checklist pratica
- ☐ Architettura flash identificata: a settori (F4) o a pagine (G0/G4/U5/L4). Granularità di cancellazione confermata nel manuale di riferimento.
- ☐ Sequenza di sblocco scritta correttamente: KEYR non OPTKEYR per la flash principale; due chiavi specifiche in ordine.
- ☐ Indirizzo di destinazione allineato a 32 bit e in una regione flash scrivibile (non protetta da WRP).
- ☐ Flag BSY pollato prima e dopo ogni operazione di scrittura/cancellazione.
- ☐ Flash bloccata dopo scrittura/cancellazione per prevenire scritture accidentali.
- ☐ Monitoraggio VDD durante programmazione: un brownout reset potrebbe corrompere la flash.
- ☐ Emulazione EEPROM gestisce la perdita di alimentazione: magic number + checksum per rilevare record validi.
- ☐ Codice del bootloader eseguito dalla RAM o da una regione non in cancellazione.
- ☐ IWDG aggiornato o prescalato prima di operazioni di cancellazione lunghe (50–300 ms).
- ☐ Option byte: livello RDP verificato — mai livello 2 durante lo sviluppo.
- ☐ Settori protetti da scrittura (WRP) controllati: scrivere su un settore protetto causa un hard fault o errore silenzioso.
Come lo affronterei su un progetto cliente
Su un progetto firmware di produzione, non lascio mai che il codice applicativo tocchi direttamente i registri della flash. La programmazione flash è un'operazione critica con conseguenze reali se malconfigurata — un bit sbagliato in CR e puoi bloccarti fuori dal debugging o corrompere la tabella dei vettori dell'applicazione.
Scrivo un modulo dedicato flash_driver.c che espone solo:
int flash_init(void);
int flash_erase(uint32_t page_addr);
int flash_write(uint32_t addr, const uint8_t *data, uint32_t len);
int flash_read(uint32_t addr, uint8_t *out, uint32_t len);
void flash_lock(void);
void flash_unlock(void);
Questo modulo viene sottoposto a code review con una checklist (quella sopra). I test unitari verificano che l'API chiami le sequenze di registro corrette ispezionando una struttura FLASH mockata. I test di integrazione vengono eseguiti su hardware reale con un firmware di test dedicato che esercita ogni settore — questo intercetta precocemente le schede sensibili al brownout.
Il layer di emulazione EEPROM si trova sopra flash_driver.c e aggiunge la gestione dello swap tra pagine. È indipendente dalla scheda: lo stesso file .c compila su F4, G0 e U5 semplicemente scambiando il driver flash sottostante. Mantengo il layer di emulazione semplice — due pagine, un CRC a 16 bit e nessun wear-leveling oltre lo swap. Per la maggior parte dei casi di calibrazione (meno di 100 000 scritture nell'intero ciclo di vita del prodotto), questo è più che sufficiente.
Se il cliente insiste sull'avere EEPROM byte-addressable fin dal primo giorno, valuto il costo di una EEPROM I²C esterna (come una 24LC512) rispetto all'emulazione su flash. Per volumi superiori a 10K unità, il risparmio BOM dell'emulazione su flash vince quasi sempre, e lo sforzo firmware è un costo una tantum.
Fonti e approfondimenti
- STM32F401 Reference Manual (RM0368) — Capitolo 3: Interfaccia memoria flash. Mappa dei registri, sequenza di sblocco, layout dei settori e timing.
- STM32G0x0 Reference Manual (RM0444) — Capitolo 4: Flash. Cancellazione a pagine, protezione scrittura e configurazione di boot.
- STM32U5 Reference Manual (RM0456) — Capitolo 4: Flash. Architettura dual-bank, RWW e programmazione sicura.
- ST Application Note AN4760 — Emulazione EEPROM per microcontrollori STM32F4.
- ST Application Note AN4657 — Programmazione flash STM32.
- Pacchetto firmware STM32CubeF4 — Esempio FLASH_WriteRead in Projects/STM32F401RE-Nucleo/Examples/FLASH/.

Commenti
Hai commenti? Scrivimi un'email.