Memoria Flash STM32: Scrivere, Cancellare e Gestire Dati Persistenti a Runtime

2026-06-02 · Davide Carrese
STM32 · Flash · Firmware · Bootloader

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:

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:

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:

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:

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:

Checklist pratica

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

Commenti

Hai commenti? Scrivimi un'email.