STM32 · MPU · Cortex-M · Protezione Memoria · Embedded · Firmware

Configurazione MPU su STM32:
Proteggere le Regioni di Memoria nel Firmware di Produzione

2026-06-23 · Davide Carrese

Niente di peggio di un crash del firmware causato da uno stack overflow che corrompe silenziosamente una struttura di configurazione critica — o un pointer impazzito in una callback DMA che sovrascrive il task control block di un task FreeRTOS in esecuzione. La Memory Protection Unit (MPU) dei Cortex-M esiste proprio per intercettare queste violazioni nell'istante in cui accadono, prima che il danno si propaghi.

Sulla maggior parte dei progetti STM32, la MPU rimane disabilitata di default. Molti sviluppatori la trattano come una funzionalità di sicurezza opzionale per codice automotive o high-integrity. In realtà, qualsiasi Cortex-M3/M4/M7/M33 di STM32 può usare la MPU, e configurarla richiede meno di venti righe di C una volta compreso il modello a registri. Questo articolo illustra la configurazione delle regioni, il mascheramento delle sottoregioni, la separazione dei privilegi e i problemi pratici incontrati su progetti reali.

Il Modello MPU in Un Paragrafo

La MPU dei Cortex-M divide lo spazio di indirizzi da 4 GB in un massimo di otto (o sedici su Cortex-M33/M55) regioni indipendenti e configurabili. Ogni regione ha un indirizzo di base, una dimensione da 32 byte a 4 GB, e un insieme di permessi di accesso: lettura/scrittura/esecuzione per software privilegiato vs. non privilegiato. Quando codice in esecuzione in modalità non privilegiata accede a un indirizzo non coperto da alcuna regione attiva, o viola i permessi della regione, la MPU solleva un fault MemManage — intercettato da MemManage_Handler nella tua tabella dei vettori.

Questa è l'intera astrazione. Niente tabelle di paginazione, niente manutenzione TLB, nessuna dipendenza dall'OS. La MPU è un puro gatekeeper hardware che opera su indirizzi fisici.

Configurazione delle Regioni — Registro per Registro

I registri di controllo della MPU risiedono nello spazio di indirizzi del System Control Block (SCB), accessibili tramite le macro CMSIS-Core o dereferenziando direttamente i puntatori.

Passo 1 — Abilitare la MPU e definire la regione di background.

Il registro MPU_CTRL (0xE000ED94) ha tre bit rilevanti:

/* Abilita MPU, nessuna regione privilegiata di default, MPU attiva in HardFault/NMI */
MPU->CTRL = MPU_CTRL_ENABLE_Msk;
/* Oppure: abilita con regione privilegiata di default */
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;

Passo 2 — Configurare una regione con RNR, RBAR e RASR.

Ogni regione viene programmata attraverso tre registri memory-mapped:

void MPU_ConfigRegion(uint8_t region, uint32_t base, uint32_t size, uint32_t rasr) {
    /* Disabilita la MPU durante la programmazione delle regioni */
    MPU->CTRL = 0;

    /* Seleziona regione */
    MPU->RNR  = region;
    MPU->RBAR = base | MPU_RBAR_VALID_Msk | (region << MPU_RBAR_REGION_Pos);
    MPU->RASR = rasr;

    /* Riabilita */
    MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
    __DSB();
    __ISB();
}

Il parametro size in RASR è codificato come Size = log2(dimensione_regione) - 1. Una regione da 32 KB richiede Size = 14 (2^(14+1) = 32768).

Codifica del Campo Attributi RASR

Il layout di RASR è la parte più soggetta a errori della configurazione. Ecco la suddivisione dei campi:

Bit RASR:
 [31:29] TEX  — Type Extension, combinato con C/B/S per il comportamento cache
 [28]    S    — Shareable (condivisibile)
 [27]    C    — Cacheable (cacheable)
 [26]    B    — Bufferable (bufferabile)
 [24]    SRD  — SubRegion Disable (8 bit, uno per sottoregione)
 [15:8]  SRD  — il campo SRD vero e proprio
 [5:3]   AP   — Access Permission (permessi di accesso)
 [1]     XN   — Execute Never (mai eseguire)
 [0]     EN   — Region Enable (abilitazione regione)

Le combinazioni chiave per i tipi di memoria embedded tipici:

Tipo memoriaTEXCBSMaschera RASR
Flash (Normal, WT)01000x03000000
SRAM (Normal, WBWA)11110x07000000
Periferiche (Device)00010x01000000
Strongly-Ordered00000x00000000

Il campo AP (bit [5:3]) controlla chi può fare cosa:

APPrivilegiatoNon privilegiato
000Nessun accessoNessun accesso
001RWNessun accesso
010RWRO
011RWRW
101RONessun accesso
110RORO

Esempio Pratico: Proteggere la SRAM con Tre Regioni

Consideriamo un tipico progetto FreeRTOS su STM32G474. Il layout della SRAM è:

#define MPU_REGION_FLASH     0
#define MPU_REGION_SRAM_DATA 1
#define MPU_REGION_SRAM_RTOS 2
#define MPU_REGION_SRAM_TASK 3
#define MPU_REGION_PERIPH    4

void MPU_Init_Production(void) {
    /* Disabilita durante la configurazione */
    MPU->CTRL = 0;
    __DSB(); __ISB();

    /* Regione 0: Flash (512 KB) — RO privilegiato, RO non privilegiato, XN disabilitato */
    MPU->RNR  = MPU_REGION_FLASH;
    MPU->RBAR = 0x08000000 | MPU_RBAR_VALID_Msk | (MPU_REGION_FLASH << MPU_RBAR_REGION_Pos);
    MPU->RASR = (18 << 1)              /* SIZE: log2(512KB)-1 = 18 */
              | (0x03 << 3)             /* AP: RW/RW */
              | (0x01 << 5)             /* TEX=0, C=1 (Write-Through) */
              | (1 << MPU_RASR_C_Pos)   /* Cacheable */
              | (1 << MPU_RASR_ENABLE_Pos);

    /* Regione 1: .data + .bss (32 KB) — RW privilegiato, nessun accesso non privilegiato */
    MPU->RNR  = MPU_REGION_SRAM_DATA;
    MPU->RBAR = 0x20000000 | MPU_RBAR_VALID_Msk | (MPU_REGION_SRAM_DATA << MPU_RBAR_REGION_Pos);
    MPU->RASR = (14 << 1)              /* SIZE: log2(32KB)-1 = 14 */
              | (0x01 << 3)             /* AP: RW privilegiato, nessun accesso non priv. */
              | (1 << MPU_RASR_TEX_Pos) /* TEX=1, C=1, B=1, S=1 (WBWA) */
              | (1 << MPU_RASR_C_Pos)
              | (1 << MPU_RASR_B_Pos)
              | (1 << MPU_RASR_S_Pos)
              | (1 << MPU_RASR_ENABLE_Pos);

    /* Regione 2: Strutture RTOS (16 KB) — solo RW privilegiato */
    MPU->RNR  = MPU_REGION_SRAM_RTOS;
    MPU->RBAR = 0x20008000 | MPU_RBAR_VALID_Msk | (MPU_REGION_SRAM_RTOS << MPU_RBAR_REGION_Pos);
    MPU->RASR = (13 << 1)              /* SIZE: log2(16KB)-1 = 13 */
              | (0x01 << 3)             /* AP: solo RW privilegiato */
              | (1 << MPU_RASR_TEX_Pos)
              | (1 << MPU_RASR_C_Pos)
              | (1 << MPU_RASR_B_Pos)
              | (1 << MPU_RASR_S_Pos)
              | (1 << MPU_RASR_ENABLE_Pos);

    /* Regione 3: Stack dei task (8 KB) — RW non privilegiato, XN abilitato */
    MPU->RNR  = MPU_REGION_SRAM_TASK;
    MPU->RBAR = 0x2000C000 | MPU_RBAR_VALID_Msk | (MPU_REGION_SRAM_TASK << MPU_RBAR_REGION_Pos);
    MPU->RASR = (12 << 1)              /* SIZE: log2(8KB)-1 = 12 */
              | (0x03 << 3)             /* AP: RW/RW */
              | (1 << MPU_RASR_TEX_Pos)
              | (1 << MPU_RASR_C_Pos)
              | (1 << MPU_RASR_B_Pos)
              | (1 << MPU_RASR_S_Pos)
              | (1 << MPU_RASR_ENABLE_Pos)
              | (1 << 0);              /* XN — niente esecuzione codice dallo stack */

    /* Regione 4: Periferiche (0x40000000–0x5FFFFFFF) — RW privilegiato, Device, XN */
    MPU->RNR  = MPU_REGION_PERIPH;
    MPU->RBAR = 0x40000000 | MPU_RBAR_VALID_Msk | (MPU_REGION_PERIPH << MPU_RBAR_REGION_Pos);
    MPU->RASR = (29 << 1)              /* SIZE: log2(512MB)-1 = 29 */
              | (0x01 << 3)             /* AP: solo RW privilegiato */
              | (0 << MPU_RASR_TEX_Pos)
              | (1 << MPU_RASR_S_Pos)  /* Device, Shareable */
              | (1 << MPU_RASR_ENABLE_Pos)
              | (1 << 0);              /* XN */

    /* Abilita MPU con regione di background privilegiata */
    MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
    __DSB(); __ISB();
}

Con questa configurazione, un task non privilegiato che accidentalmente scrive tramite un puntatore selvaggio a 0x20000004 (nella regione .data) attiva immediatamente MemManage_Handler. Lo stesso task che tenta di eseguire codice dal proprio stack viene anch'esso intercettato — il bit XN impedisce l'esecuzione dall'area dello stack.

Disabilitazione delle Sottoregioni — Granularità Più Fine Senza Regioni Aggiuntive

Ogni regione MPU è divisa in otto sottoregioni uguali. Impostando i bit nel campo SubRegion Disable (SRD) di RASR, puoi creare buchi all'interno di una regione senza consumare un altro slot di regione. Questo è prezioso quando il linker posiziona una tabella dati di sola lettura dentro un'area .bss scrivibile, o quando una FPGA memory-mapped si sovrappone a un'apertura periferica.

/* Esempio: regione da 16 KB a 0x20000000, disabilita sottoregioni 4 e 5
   (byte 0x20008000–0x2000BFFF) così diventano senza accesso */
uint32_t srd_mask = (1 << 4) | (1 << 5);   /* sottoregioni 4 e 5 disabilitate */
uint32_t rasr = (13 << 1)                    /* SIZE = 16 KB */
              | (0x01 << 3)                   /* AP: RW privilegiato */
              | (srd_mask << 8)               /* campo SRD ai bit [15:8] */
              | (1 << MPU_RASR_TEX_Pos)
              | (1 << MPU_RASR_C_Pos)
              | (1 << MPU_RASR_B_Pos)
              | (1 << MPU_RASR_S_Pos)
              | (1 << MPU_RASR_ENABLE_Pos);

Ogni sottoregione copre dimensione_regione / 8 byte. Per una regione da 16 KB, ogni sottoregione è di 2 KB.

Checklist Pratica

  1. Imposta PRIVDEFENA = 1 durante lo sviluppo — la regione privilegiata di background evita fault a sorpresa da codice libreria non scritto con consapevolezza della MPU.
  2. Accoppia sempre le scritture delle regioni con __DSB() e __ISB() — i registri di configurazione della MPU sono scritture bufferizzate. Senza una barriera, l'istruzione successiva potrebbe eseguire con stato di regione obsoleto.
  3. Disabilita la MPU prima di cambiare le regioni — scrivere RASR mentre la MPU è abilitata produce comportamento IMPREVEDIBILE su Cortex-M3/M4. Disabilita, configura, riabilita.
  4. Imposta XN sulle regioni SRAM a meno che tu non abbia esplicitamente bisogno di eseguire codice dalla RAM (cosa rara, e di solito indice di un bootloader o pattern di automodifica che dovrebbe essere un'eccezione esplicita).
  5. Verifica con un fault deliberato — scrivi un breve test che inneschi un fault MemManage e conferma che MemManage_Handler venga chiamato prima di fidarti della configurazione in campo.
  6. Ricorda che la MPU si resetta disabilitata — qualsiasi codice eseguito prima di MPU_Init_Production() (codice di startup, SystemInit, init board-level) viene eseguito senza protezione. Se serve copertura dalla prima istruzione, configura la MPU all'inizio di Reset_Handler.
  7. Attenzione alle regioni MPU su Cortex-M7 con D-Cache — regioni che coprono la stessa memoria fisica devono condividere gli stessi attributi cache (TEX/C/B/S) o la cache può presentare problemi di coerenza. Questa è una causa comune di bug del tipo "funziona senza MPU, si rompe con MPU" su STM32H7.

Come lo Affronterei su un Progetto Cliente

Su un recente progetto di controllore industriale con STM32H723, ho diviso le regioni MPU in tre categorie: core-kernel (solo RW privilegiato), application-data (RW privilegiato, RO non privilegiato), e stack (RW non privilegiato, XN). Lo script linker è stato modificato per posizionare ogni categoria a un indirizzo di base noto con un allineamento pari alla dimensione della regione — questo è critico perché la MPU impone l'allineamento di base % size == 0.

Il più grande risparmio di tempo è stato mantenere PRIVDEFENA abilitato durante lo sviluppo. Ha permesso alle librerie HAL delle periferiche (che tipicamente girano in modalità privilegiata) di accedere all'intera mappa di memoria senza copertura esplicita di regione, intercettando precisamente le violazioni a livello applicativo che mi interessavano. Solo al rilascio in produzione ho stretto le regioni per coprire ogni tipo di memoria esplicitamente — inclusa una regione Device dedicata per il bus periferiche.

Ho anche aggiunto uno switch #define MPU_ENABLE 1 in compilazione così lo stesso binario può essere debuggaro su un'unità da banco (MPU disabilitata) e girato in produzione (MPU abilitata) senza cambiare codice sorgente. L'unica differenza è se l'handler del fault MemManage si ferma o esegue un reset sicuro.

Fonti

📬 Commenti / discussione

Preferisci via email: [email protected] — includi l'URL dell'articolo così posso rispondere. Per correzioni o domande più approfondite, rispondo tipicamente entro 48 ore.