2026-06-13 · Davide Carrese

Flash Dual Bank su STM32:
Attivazione e Bank Swap per Aggiornamenti OTA Sicuri su G4, L4 e U5

STM32 · Flash · Dual Bank · OTA · Bootloader · G4 · L4 · U5

Un aggiornamento firmware che brickka il dispositivo a metà della trasmissione è uno degli incubi peggiori per chi sviluppa embedded. Perdita di alimentazione durante la cancellazione della flash, CRC corrotto o download incompleto — e la scheda è morta finché qualcuno non riconnette fisicamente un programmatore. La flash dual-bank degli STM32 offre uno scambio atomico basato su hardware: due banchi di flash completi, uno attivo e uno che riceve l'aggiornamento, scambiati con una singola scrittura di registro. Questo articolo spiega la configurazione degli option byte, il bank swap, la rilocazione del vettore di interrupt e l'architettura completa su STM32G4, L4 e U5.

La modalità dual-bank divide la memoria flash principale in due banchi indipendenti. Ogni banco può contenere un'immagine firmware completa. Quando la nuova immagine è pronta nel banco inattivo, una singola scrittura di registro attiva uno scambio atomico al prossimo reset — o immediatamente, a seconda del dispositivo. Non c'è rischio di brick in caso di perdita di alimentazione durante lo scambio: l'hardware garantisce che se l'operazione era in corso e l'alimentazione è mancata, il dispositivo si avvia dal banco che era valido prima dell'operazione.

Questo è fondamentalmente diverso da un approccio OTA puramente software, dove cancelli e riscrivi la stessa regione di flash in place. In uno schema a banco singolo, una perdita di alimentazione durante la cancellazione lascia il dispositivo completamente vuoto. Il dual-bank elimina questo rischio.

Quali Famiglie STM32 Supportano il Dual Bank?

La flash dual-bank è disponibile su diverse famiglie STM32 moderne, ma la mappa dei registri e i meccanismi di scambio differiscono:

SerieDimensione FlashDimensione BanchiMeccanismo di Scambio
STM32G4Fino a 512 KB2 × 256 KBBit FB_MODE negli option byte; swap al reset
STM32L4/L4+Fino a 2 MB2 × 1 MBBit DBANK; swap tramite FLASH_OPTCR
STM32U5Fino a 2 MB2 × 1 MBBit DBANK; swap tramite FLASH_OPTCR
STM32F7Fino a 2 MB2 × 1 MBBit DBANK su alcuni modelli
STM32H7Fino a 2 MB2 × 1 MB (alcuni)Registro FLASH_OPTCR

Questo articolo si concentra su STM32G4 (RM0440) e STM32L4 (RM0351) — le serie mid-range più comuni dove il dual-bank è disponibile e pratico per progetti da contractor. L'U5 segue lo stesso modello di registri dell'L4.

Layout di Memoria con Dual Bank Attivo

Quando la modalità dual-bank è inattiva (default), la flash appare come un unico blocco contiguo che parte da 0x08000000. Attivando il dual-bank, la flash viene divisa in due banchi di uguale dimensione:

Banco singolo (default):
  0x0800 0000  ┌──────────────────────────┐
                │     Singolo banco flash   │
                │     (es. 512 KB)          │
  0x0807 FFFF  └──────────────────────────┘

Dual bank (FB_MODE/DBANK=1):
  0x0800 0000  ┌──────────────────────────┐  ← Banco 1 (attivo, boot)
                │    Immagine firmware A   │
                │    (256 KB su G4)        │
  0x0803 FFFF  ├──────────────────────────┤
  0x0804 0000  │    Immagine firmware B   │  ← Banco 2 (inattivo)
                │    (256 KB su G4)        │
  0x0807 FFFF  └──────────────────────────┘

Il Banco 1 parte da 0x08000000 — l'indirizzo del vettore di reset. Il Banco 2 parte dal punto medio: 0x08040000 per un G4 da 512 KB, 0x08080000 per un L4 da 1 MB, e così via. Dopo un bank swap, il Banco 2 viene rimappato a 0x08000000 e il Banco 1 diventa il banco inattivo all'indirizzo superiore.

Configurazione del Dual Bank tramite Option Byte

La modalità dual-bank viene selezionata tramite gli option byte — un'area separata della flash che memorizza parametri di configurazione come il livello di protezione in lettura, il watchdog hardware e le impostazioni di boot. Gli option byte persistono tra aggiornamenti firmware e vengono modificati solo attraverso la sequenza di programmazione specifica del controller flash.

STM32G4: Bit FB_MODE

Sul G4, il bit FB_MODE nell'option byte FLASH_OPTR controlla la modalità dual-bank. La programmazione richiede la sequenza standard: sblocca FLASH_CR, imposta il bit di programmazione option byte, scrive OPTR, poi genera un reset per caricare la nuova configurazione:

/* Sblocco del registro di controllo flash */
FLASH->KEYR  = 0x45670123;
FLASH->KEYR  = 0xCDEF89AB;

/* Sblocco degli option byte */
FLASH->OPTKEYR = 0x08192A3B;
FLASH->OPTKEYR = 0x4C5D6E7F;

/* Imposta FB_MODE nel registro option */
FLASH->OPTR |= FLASH_OPTR_FB_MODE;

/* Avvia la programmazione degli option byte */
FLASH->CR |= FLASH_CR_OPTSTRT;
while (FLASH->SR & FLASH_SR_BSY);

/* Reset per caricare i nuovi option byte */
NVIC_SystemReset();
⚠️ Il cambio dual bank richiede un reset

La nuova organizzazione dei banchi diventa effettiva solo dopo un reset (o un power-on reset). Gli option byte vengono caricati dalla boot ROM prima che il codice utente parta. Non puoi attivare il dual-bank e usare subito il Banco 2 nella stessa sessione di esecuzione.

STM32L4/U5: Bit DBANK

L'L4 e l'U5 usano il bit DBANK in FLASH_OPTR. La sequenza di programmazione è identica:

FLASH->KEYR  = 0x45670123;
FLASH->KEYR  = 0xCDEF89AB;
FLASH->OPTKEYR = 0x08192A3B;
FLASH->OPTKEYR = 0x4C5D6E7F;

FLASH->OPTR |= FLASH_OPTR_DBANK;          /* abilita dual bank */

FLASH->CR |= FLASH_CR_OPTSTRT;
while (FLASH->SR & FLASH_SR_BSY);

NVIC_SystemReset();

Dopo il reset, la flash è suddivisa in due banchi. Puoi verificare la configurazione attiva leggendo FLASH->OPTR e controllando il bit FB_MODE o DBANK.

Bank Swap: Il Meccanismo Centrale

Una volta che il dual-bank è attivo ed entrambi i banchi contengono immagini firmware valide, lo scambio viene attivato scrivendo nel registro FLASH_OPTCR (o nel registro di controllo dello scambio specifico della famiglia). Lo scambio può essere richiesto per il prossimo boot o immediatamente.

Swap al Prossimo Reset (STM32G4)

Sul G4, lo scambio è controllato dai bit BKSEL in FLASH_OPTCR. Impostando BKSEL a 0b01 si richiede l'avvio dal Banco 2 al prossimo reset. Lo scambio effettivo avviene durante la sequenza di reset:

/* Richiedi boot dal Banco 2 al prossimo reset */
FLASH->OPTCR = (FLASH->OPTCR & ~FLASH_OPTCR_BKSEL) | (1 << FLASH_OPTCR_BKSEL_Pos);

/* Attiva un reset per applicare */
NVIC_SystemReset();
⚠️ Richiesta di swap write-once

Su alcuni G4, il registro di richiesta swap è write-once fino al prossimo reset. Se lo scrivi accidentalmente durante l'esecuzione normale (es. da un puntatore errante), lo swap non scatterà comunque fino al prossimo reset — ma fai attenzione durante il debug se il dispositivo si avvia dal banco sbagliato dopo un watchdog reset.

Swap Immediato tramite FLASH_KEYR + FLASH_OPTCR (STM32L4/U5)

L'L4/U5 supportano un modello leggermente diverso: lo scambio viene programmato tramite il meccanismo delle chiavi degli option byte e prende effetto al prossimo reset di sistema. Il bit SWAP_BANK in FLASH_OPTR determina quale banco è attivo:

/* Leggi OPTR corrente, commuta SWAP_BANK */
uint32_t optr = FLASH->OPTR;
if (optr & FLASH_OPTR_SWAP_BANK) {
    optr &= ~FLASH_OPTR_SWAP_BANK;         /* passa al Banco 1 */
} else {
    optr |= FLASH_OPTR_SWAP_BANK;           /* passa al Banco 2 */
}

/* Programma il nuovo valore OPTR */
FLASH->OPTR = optr;
FLASH->CR |= FLASH_CR_OPTSTRT;
while (FLASH->SR & FLASH_SR_BSY);

NVIC_SystemReset();                         /* applica al reset */

Dopo il reset, il processore si avvia dal banco precedentemente inattivo. L'hardware legge gli option byte durante la fase di boot ROM e rimappa la finestra 0x08000000 di conseguenza.

Rilocazione del Vettore di Interrupt

Quando si scambiano i banchi, la CPU si avvia sempre da 0x08000000. Dopo lo scambio, il Banco 2 viene mappato lì, quindi la tabella dei vettori dell'immagine nel Banco 2 viene usata direttamente. Questa è la parte elegante: non è necessario rilocare il vettore a runtime per il flusso di boot. L'hardware gestisce il rimappaggio.

Tuttavia, durante un aggiornamento OTA in cui il bootloader è in esecuzione da un settore separato o dal Banco 1 mentre si scrive sul Banco 2, è necessario che la tabella dei vettori del bootloader rimanga accessibile. L'approccio tipico è:

  1. Bootloader — vive in un settore fisso del Banco 1 (settori 0–1, ~32 KB). La sua tabella dei vettori è a 0x08000000.
  2. Applicazione A — vive nel resto del Banco 1. La sua tabella dei vettori è a 0x08008000 (dopo il bootloader). L'applicazione imposta SCB->VTOR = 0x08008000 all'inizio del suo startup.
  3. Applicazione B — vive interamente nel Banco 2. La sua tabella dei vettori è all'inizio del Banco 2 (0x08040000 su un G4 da 512 KB). Dopo lo scambio, questo diventa 0x08000000, quindi VTOR non deve cambiare — funziona e basta.
/* Applicazione A: riloca VTOR dopo il bootloader */
SCB->VTOR = 0x08008000;

/* Applicazione B (dopo lo scambio al Banco 2): */
/* Il Banco 2 è ora a 0x08000000, VTOR rimane al default di reset */
/* Riloca solo se devi saltare a una sotto-sezione */

Architettura OTA Completa

Ecco il flusso OTA dual-bank che uso su progetti industriali e medicali in produzione:

  1. Fase bootloader — A ogni reset, il bootloader controlla un flag "swap_pending" in un registro di backup dedicato (dominio di backup RTC o ultimi 4 byte di SRAM). Se impostato, esegue il bank swap e cancella il flag. Altrimenti, convalida il CRC del banco attivo corrente e salta all'applicazione.
  2. Fase applicazione — L'applicazione in esecuzione riceve il nuovo binario firmware tramite UART/SPI/I2C/USB/CAN. Scrive ogni pagina nel banco inattivo usando la sequenza standard di programmazione flash.
  3. Fase di validazione — Dopo aver scritto l'immagine completa, l'applicazione calcola e confronta il CRC sull'intero banco inattivo. Se corrisponde al CRC inserito nell'header del firmware, imposta il flag "swap_pending" nel registro di backup e attiva un reset di sistema.
  4. Fase di swap — Il bootloader viene eseguito al prossimo reset, vede il flag "swap_pending", esegue il bank swap tramite FLASH_OPTCR, cancella il flag e salta all'applicazione appena attivata. Il vecchio firmware rimane intatto nel banco ora inattivo come fallback.

Fallback Failsafe

Se il nuovo firmware non si avvia (watchdog timeout, hard fault, rollback richiesto dall'utente), il bootloader può rilevare avvii falliti consecutivi incrementando un contatore nel registro di backup. Dopo N fallimenti consecutivi, torna al banco precedente, ripristinando di fatto l'ultimo firmware funzionante. Questo è il pattern di bootstrap failsafe:

/* Bootloader: controlla il contatore di tentativi */
uint32_t attempts = RTC->BKP0R;            /* registro di backup */
if (attempts > MAX_BOOT_ATTEMPTS) {
    /* Rollback: torna all'altro banco */
    bank_swap();
    RTC->BKP0R = 0;                        /* reset contatore */
    NVIC_SystemReset();
}
RTC->BKP0R = attempts + 1;                /* incrementa */

/* Salta all'applicazione... */

/* L'avvio dell'applicazione deve azzerare il contatore */
/* RTC->BKP0R = 0; */
⚠️ I registri di backup richiedono VBAT

I registri di backup RTC mantengono il loro valore solo quando il dispositivo ha una batteria di backup su VBAT, o quando VDD è mantenuto. Se nessuno dei due è disponibile, usa gli ultimi 4 byte di SRAM (con il trucco del flag RCC_CSR_RMVF) o un settore EEPROM dedicato. Su STM32U5, i registri di backup sicuri offrono un'alternativa più robusta.

Esempio Pratico: Bank Swap su STM32G474

Ecco una funzione di bank swap completa e autonoma per STM32G474 (512 KB flash, banchi duali da 256 KB ciascuno). La funzione scrive la nuova immagine firmware nel Banco 2, la valida e richiede l'avvio dal Banco 2 al prossimo reset:

#define BANK2_START  0x08040000UL
#define BANK2_END    0x0807FFFFUL

typedef struct {
    uint32_t magic;        /* costante di validazione */
    uint32_t crc32;        /* CRC del firmware */
    uint32_t size;         /* dimensione firmware in byte */
    uint32_t version;      /* numero versione monotonico */
} firmware_header_t;

int ota_program_bank2(const uint8_t *data, uint32_t len,
                      const firmware_header_t *hdr)
{
    uint32_t addr = BANK2_START;

    /* 1. Sblocca flash */
    FLASH->KEYR = 0x45670123;
    FLASH->KEYR = 0xCDEF89AB;

    /* 2. Cancella tutte le pagine del Banco 2 */
    for (uint32_t page = 0; page < 64; page++) {
        FLASH->CR = (page << FLASH_CR_PNB_Pos) | FLASH_CR_PER | FLASH_CR_STRT;
        while (FLASH->SR & FLASH_SR_BSY);
        if (FLASH->SR & FLASH_SR_PGSERR) return -1;
    }

    /* 3. Programma ogni parola */
    FLASH->CR = FLASH_CR_PG;                /* abilita programmazione */
    for (uint32_t i = 0; i < len; i += 4) {
        *(volatile uint32_t *)(addr + i) = *(const uint32_t *)(data + i);
        while (FLASH->SR & FLASH_SR_BSY);
        if (FLASH->SR & FLASH_SR_PGPERR) { FLASH->CR &= ~FLASH_CR_PG; return -2; }
    }
    FLASH->CR &= ~FLASH_CR_PG;

    /* 4. Convalida CRC */
    uint32_t computed_crc = compute_crc32((void *)BANK2_START, hdr->size);
    if (computed_crc != hdr->crc32) return -3;

    /* 5. Richiedi bank swap */
    FLASH->OPTCR = (FLASH->OPTCR & ~FLASH_OPTCR_BKSEL) | (1 << FLASH_OPTCR_BKSEL_Pos);
    return 0;  /* il chiamante attiva NVIC_SystemReset() */
}

Checklist pratica

PassoRegistro/VerificaCosa controllare
Dual bank attivoFLASH->OPTR (FB_MODE/DBANK)Bit impostato dopo programmazione option byte e reset
Indirizzo Banco 20x08040000 (G4 512K) / 0x08080000 (L4 1M)Corretto per la tua dimensione flash
Sblocco option byteFLASH->OPTKEYRScrivere due chiavi in sequenza; nessun altro accesso tra le scritture
Richiesta swapFLASH->OPTCR (BKSEL) o SWAP_BANKScritta prima del reset; verifica dopo il reset che il banco corretto sia attivo
Tabella vettoriSCB->VTORCorrispondere all'offset del banco di boot; se c'è un bootloader, VTOR deve essere dopo di esso
CRC fallbackHeader banco inattivoCalcola CRC del banco inattivo prima dello swap; mantieni fallback disponibile dopo lo swap
Contatore bootRegistro backupFallimenti consecutivi attivano rollback automatico
Latenza flashFLASH->ACRDeve corrispondere al nuovo SYSCLK dopo il reset; se il dual bank cambia i tempi, ricontrolla il datasheet

Come lo affronterei su un progetto cliente

Su un sistema di produzione reale, non permetto all'applicazione di manipolare direttamente gli option byte. Invece, partiziono il bootloader in due stadi:

Bootloader Stage 1 (primi 4 KB, write-protetto): gestisce le richieste di swap, la convalida CRC e la logica di fallback. Non scrive mai nel proprio settore e occupa l'area flash minima possibile. Questo stadio viene programmato una volta in produzione e non viene mai modificato in campo.

Bootloader Stage 2 (successivi 28 KB): gestisce il protocollo di comunicazione per ricevere nuovi firmware (UART/CAN/SPI), guida la programmazione flash del banco inattivo e gestisce il monitoraggio delle versioni. Se è necessario un aggiornamento del protocollo di comunicazione in campo, viene aggiornato solo lo Stage 2 — lo Stage 1 convalida il CRC del nuovo Stage 2 prima di avviarlo.

Includo anche un header firmware versionato all'inizio di ogni banco con un numero di versione monotonico. Il bootloader rifiuta di scambiare a una versione più vecchia di quella attualmente in esecuzione. Questo previene una race condition in cui una richiesta di aggiornamento vecchia, ritardata da un ritardo di rete, sovrascrive un firmware più recente con uno più vecchio.

Per il CRC, uso il CRC hardware (il periferico CRC32 integrato dell'STM32) clockdato da HSI, non il CRC software usato nell'esempio di codice sopra. Il calcolo hardware del CRC si completa in microsecondi per un banco da 256 KB, rispetto ai decine di millisecondi di un'implementazione software.

Fonti

📬 Lascia un commento

Domande o correzioni? Scrivimi — rispondo a ogni messaggio.