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:
- ENABLE (bit 0): abilita la MPU.
- PRIVDEFENA (bit 2): quando impostato, il codice privilegiato vede l'intera mappa di memoria come regione predefinita con accesso completo. Cancellando questo bit, anche il codice privilegiato ha bisogno di regioni esplicite per qualsiasi accesso.
- HFNMIENA (bit 1): controlla se la MPU è attiva durante NMI e HardFault. Tienilo a 0 per permettere agli handler di eccezione di accedere a tutto; impostalo a 1 per sicurezza aggiuntiva durante la gestione dei fault.
/* 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:
MPU_RNR(Region Number Register) — seleziona l'indice della regione (0–7).MPU_RBAR(Region Base Address Register) — contiene l'indirizzo di base, il tipo di indirizzo (normal/device/strongly-ordered), e se la regione è valida.MPU_RASR(Region Attribute and Size Register) — codifica dimensione, sottoregioni, permessi di accesso, condivisibilità, attributi cache (TEX/S/C/B per memoria normale), e il flag execute-never (XN).
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 memoria | TEX | C | B | S | Maschera RASR |
|---|---|---|---|---|---|
| Flash (Normal, WT) | 0 | 1 | 0 | 0 | 0x03000000 |
| SRAM (Normal, WBWA) | 1 | 1 | 1 | 1 | 0x07000000 |
| Periferiche (Device) | 0 | 0 | 0 | 1 | 0x01000000 |
| Strongly-Ordered | 0 | 0 | 0 | 0 | 0x00000000 |
Il campo AP (bit [5:3]) controlla chi può fare cosa:
| AP | Privilegiato | Non privilegiato |
|---|---|---|
| 000 | Nessun accesso | Nessun accesso |
| 001 | RW | Nessun accesso |
| 010 | RW | RO |
| 011 | RW | RW |
| 101 | RO | Nessun accesso |
| 110 | RO | RO |
Esempio Pratico: Proteggere la SRAM con Tre Regioni
Consideriamo un tipico progetto FreeRTOS su STM32G474. Il layout della SRAM è:
0x20000000— 32 KB: .data + .bss + heap (privilegiato RW, nessun accesso non privilegiato)0x20008000— 16 KB: heap FreeRTOS + TCB (privilegiato RW)0x2000C000— 8 KB: stack dei task non privilegiati (non privilegiato RW, XN)
#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
- Imposta
PRIVDEFENA = 1durante lo sviluppo — la regione privilegiata di background evita fault a sorpresa da codice libreria non scritto con consapevolezza della MPU. - 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. - Disabilita la MPU prima di cambiare le regioni — scrivere RASR mentre la MPU è abilitata produce comportamento IMPREVEDIBILE su Cortex-M3/M4. Disabilita, configura, riabilita.
- 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).
- Verifica con un fault deliberato — scrivi un breve test che inneschi un fault MemManage e conferma che
MemManage_Handlervenga chiamato prima di fidarti della configurazione in campo. - 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 diReset_Handler. - 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
- ARM Cortex-M4 TRM, Capitolo 8 — Memory Protection Unit (arm.com)
- ST Application Note AN4838 — Introduzione alla MPU su STM32
- ST Application Note AN4839 — Suggerimenti e linee guida MPU
- CMSIS-Core (v5/v6) — API
MPU_ConfigRegion()eARM_MPU_SetRegion() - Manuali di Riferimento STM32G4 e STM32H7 — descrizione registri MPU nel capitolo System Control Block
📬 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.