Coerenza D-Cache su STM32H7:
la trappola DMA che ogni sviluppatore embedded deve conoscere
Passi la mattina a debuggar perché il tuo buffer SPI DMA contiene zeri mentre il periferico sta chiaramente clockando dati? O perché una trasferimento UART restituisce lo stesso pacchetto già letto, nonostante un analizzatore logico mostri byte freschi sul filo? Con ogni probabilità è la cache dati del Cortex-M7 che ti sta servendo dati obsoleti in silenzio. Su STM32H7, la D-cache si trova tra la CPU e la matrice del bus, e gli accessi DMA la bypassano completamente. Capire quando e come pulire o invalidare la cache non è facoltativo — è la differenza tra un prodotto affidabile e un heisenbug che si manifesta solo in produzione.
STM32H7 è la famiglia MCU general-purpose più potente di ST, basata sul core Arm Cortex-M7 con bus AXI a 64 bit, cache istruzioni e dati separate, e fino a 2 MB di flash. La cache dati L1 è una cache write-back write-allocate da 16 KB, quattro-way set-associative. In modalità write-back — quella predefinita dopo il reset — una scrittura della CPU a un indirizzo cachato non si propaga immediatamente alla memoria principale. Il dato rimane nella linea di cache finché non viene rimosso. Nel frattempo, un controller DMA che accede allo stesso indirizzo fisico legge da (o scrive in) la SRAM reale, completamente ignaro della linea di cache sporca.
Questa asimmetria è la causa principale di due classici modi di guasto:
- DMA → CPU (periferica → memoria → CPU legge): Il DMA scrive dati freschi in SRAM. La CPU legge l'indirizzo — ma la D-cache contiene ancora una copia obsoleta precedente alla DMA, quindi la CPU vede dati vecchi. Serve un invalidate della cache.
- CPU → DMA (CPU scrive → DMA legge): La CPU scrive dati in un buffer, poi avvia una trasferimento DMA da quel buffer. Se i dati scritti sono ancora sporchi nella cache e non hanno raggiunto la SRAM, il DMA legge zeri obsoleti o spazzatura dalla memoria fisica. Serve un clean della cache (write-back) prima di avviare la DMA.
Come Funziona la D-Cache sul Cortex-M7
La D-cache L1 del Cortex-M7 è organizzata in 512 linee da 32 byte ciascuna, disposte come 4-way set-associative (128 set × 4 way). Ogni linea di cache ha un tag (i bit alti dell'indirizzo), un bit di validità e un bit di dirty. In modalità write-back (quella predefinita), le scritture colpiscono la linea di cache e la marcano come sporca senza scrivere attraverso sul bus AXI. Una linea di cache viene scritta indietro solo quando:
- Viene rimossa da un altro load o store che mappa sullo stesso set.
- Viene emesso un
SCB_CleanDCache_by_Addr()o un clean completo della cache. - La linea viene invalidata esplicitamente mentre è sporca (deve essere pulita prima, o si usa
SCB_CleanInvalidateDCache).
La cache opera sulla porta master AXI del Cortex-M7. I controller DMA (MDMA, DMA1, DMA2, BDMA) sono bus master separati sulla matrice AHB/AXI — non hanno alcuna visibilità della D-cache. Questo è il vincolo architetturale fondamentale: la CPU vede una vista coerente della memoria attraverso la cache, ma ogni altro bus master vede la SRAM nuda.
Le Due Operazioni di Manutenzione Cache Necessarie
1. Clean D-Cache (write-back linee sporche in SRAM)
Da chiamare prima di avviare una lettura DMA da un buffer che la CPU ha scritto recentemente:
/* Buffer che la CPU ha appena riempito per trasmettere via SPI DMA */ uint8_t tx_buffer[256]; fill_packet(tx_buffer, sizeof(tx_buffer)); /* Assicura che tutte le scritture CPU siano arrivate in SRAM prima che DMA le legga */ #if defined(__DCACHE_PRESENT) && __DCACHE_PRESENT == 1 SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, sizeof(tx_buffer)); #endif /* Ora è sicuro avviare la DMA da tx_buffer */ HAL_SPI_Transmit_DMA(&hspi1, tx_buffer, sizeof(tx_buffer));
2. Invalidate D-Cache (scarta linee obsolete, forza fetch da SRAM)
Da chiamare dopo che una trasferimento DMA è completata, prima che la CPU legga i dati ricevuti:
/* DMA ha finito di riempire rx_buffer con dati SPI */ uint8_t rx_buffer[256]; HAL_SPI_Receive_DMA(&hspi1, rx_buffer, sizeof(rx_buffer)); // ... attesa callback completamento ... /* Scarta le linee cache obsolete così il prossimo read CPU va in SRAM */ #if defined(__DCACHE_PRESENT) && __DCACHE_PRESENT == 1 SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, sizeof(rx_buffer)); #endif /* Ora è sicuro leggere rx_buffer - dati freschi dalla DMA */ process_data(rx_buffer, sizeof(rx_buffer));
3. Clean + Invalidate Combinati
Quando un buffer è usato per DMA bidirezionale (es. SPI half-duplex con lo stesso buffer), o quando non sei sicuro dello stato di dirty, usa l'operazione combinata:
SCB_CleanInvalidateDCache_by_Addr((uint32_t *)buf, size);
Questa scrive indietro qualsiasi linea sporca e poi le marca come invalide, così la prossima lettura colpisce il bus.
Insidie di Allineamento e Dimensione Linea
SCB_CleanDCache_by_Addr() e SCB_InvalidateDCache_by_Addr() operano su linee di cache da 32 byte. Se il tuo indirizzo o la dimensione non è allineato a 32 byte, la funzione si adatta ai confini di linea: potrebbe pulire/invalidare dati fuori dal buffer, o saltare la parte finale. Allinea sempre i buffer DMA a 32 byte.
Usa attributi GCC/Clang o un approccio con linker section:
/* Allineamento forzato a 32 byte con attributo */
static uint8_t rx_buffer[256] __attribute__((aligned(32)));
static uint8_t tx_buffer[256] __attribute__((aligned(32)));
/* O per buffer più grandi, usa una sezione non-cacheable dedicata:
* Nel linker script: .NonCacheable (NOLOAD) : { *(.noncacheable) } > SRAM
*/
__attribute__((section(".noncacheable")))
static uint8_t dma_pool[4096];
In alternativa, posiziona i buffer DMA in SRAM4 o SRAM3 su STM32H7, che possono essere configurate come non-cacheable tramite MPU (vedi prossima sezione). Ma anche con l'allineamento, serve comunque la manutenzione esplicita quando la DTCM o la AXI SRAM sono cacheable.
Esempio Pratico: Trasferimento SPI DMA con Gestione Cache Corretta
Ecco un pattern completo e production-grade per una transazione SPI su STM32H7:
/* stm32h7_spi_dma.c — transazione SPI DMA cache-safe */
#include "stm32h7xx_hal.h"
#include "cmsis_compiler.h"
#define DMA_BUF_SIZE 256
/* Buffer DMA allineati a 32 byte */
static __ALIGNED(32) uint8_t dma_tx[DMA_BUF_SIZE];
static __ALIGNED(32) uint8_t dma_rx[DMA_BUF_SIZE];
/* Flag condivisi (regione non-cacheable o volatile) */
static volatile uint8_t xfer_done = 0;
/* Callback HAL */
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi == &hspi1) {
xfer_done = 1;
}
}
/**
* @brief Esegue una transazione SPI DMA coerente con la cache.
* @param tx_data dati da trasmettere (copiati nel buffer allineato)
* @param rx_data buffer per ricezione (copiato dopo invalidate)
* @param len lunghezza transazione (deve essere ≤ DMA_BUF_SIZE)
* @retval HAL_StatusTypeDef
*/
HAL_StatusTypeDef spi_dma_transaction(uint8_t *tx_data, uint8_t *rx_data, uint16_t len)
{
HAL_StatusTypeDef ret;
/* Copia dati utente nel buffer DMA allineato */
memcpy(dma_tx, tx_data, len);
/* Clean D-cache prima che la DMA legga il buffer TX */
#if defined(__DCACHE_PRESENT) && __DCACHE_PRESENT == 1
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx, len);
#endif
xfer_done = 0;
/* Avvia SPI full-duplex DMA */
ret = HAL_SPI_TransmitReceive_DMA(&hspi1, dma_tx, dma_rx, len);
if (ret != HAL_OK) return ret;
/* Attesa completamento (con timeout nel codice reale) */
while (!xfer_done) {
__WFE();
}
/* Invalidate D-cache prima che la CPU legga il buffer RX */
#if defined(__DCACHE_PRESENT) && __DCACHE_PRESENT == 1
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx, len);
#endif
/* Copia dati freschi dalla DMA al buffer utente */
memcpy(rx_data, dma_rx, len);
return HAL_OK;
}
Punti chiave: i dati utente sono copiati in buffer DMA allineati, la cache viene pulita prima che la DMA parta e invalidata dopo il completamento, e volatile è usato per il flag di completamento (o deve vivere in una regione non-cacheable). Il pattern funziona identicamente per UART, I²S, ADC, DAC e qualsiasi periferico con DMA da/a memoria.
Usare l'MPU per Marcare le Regioni DMA come Non-Cacheable
Per alcuni sistemi, la manutenzione esplicita della cache su ogni transazione è troppo fragile. Un approccio architetturale più pulito è dedicare una sezione di SRAM come non-cacheable programmando l'MPU. L'MPU sul Cortex-M7 può sovrascrivere gli attributi di memoria predefiniti su base regionale.
/* Configura regione MPU per un pool DMA non-cacheable da 16 KB in AXI SRAM */
void MPU_Config_DMA_Pool(void)
{
MPU_Region_InitTypeDef MPU_Init = {0};
/* Disabilita MPU prima della configurazione */
HAL_MPU_Disable();
/* Regione: 0x24000000 (AXI SRAM metà superiore), 16 KB */
MPU_Init.Enable = MPU_REGION_ENABLE;
MPU_Init.Number = MPU_REGION_NUMBER1;
MPU_Init.BaseAddress = 0x24004000;
MPU_Init.Size = MPU_REGION_SIZE_16KB;
MPU_Init.SubRegionDisable = 0x00;
MPU_Init.TypeExtField = MPU_TEX_LEVEL1;
MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_Init.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_Init.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_Init.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; /* !!! */
MPU_Init.IsBufferable = MPU_ACCESS_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_Init);
/* Abilita MPU con PRIVDEFENA (mappa di default per privilegiato) */
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
/* Assicura che la config MPU sia in effetto prima di operazioni DMA */
__DSB();
__ISB();
}
Con questa configurazione MPU, qualsiasi variabile posizionata nell'intervallo 0x24004000–0x24007FFF non viene mai cachata dalla D-cache. Non serve pulizia/invalidazione esplicita. Il compromesso è una penalità del 15–20% nelle prestazioni per gli accessi CPU a quella regione, poiché ogni load/store va direttamente in SRAM.
Nella pratica, uso un approccio ibrido: una piccola sezione non-cacheable basata su MPU per buffer DMA ad accesso frequente (doppi buffer ADC, descrittori Ethernet), e manutenzione esplicita della cache per trasferimenti grandi o infrequenti (letture SPI flash, pacchetti UART).
Checklist Pratica
| Scenario | Azione |
|---|---|
| CPU scrive buffer, poi DMA lo legge (percorso TX) | SCB_CleanDCache_by_Addr() prima di avviare DMA |
| DMA scrive buffer, poi CPU lo legge (percorso RX) | SCB_InvalidateDCache_by_Addr() dopo completamento DMA |
| DMA bidirezionale (stesso buffer) | SCB_CleanInvalidateDCache_by_Addr() |
Buffer è const o vive in flash | Nessuna azione necessaria (flash è read-only, mai cachata in scrittura) |
| DMA frequente e piccola (ADC doppio buffer) | Regione MPU non-cacheable raccomandata |
| Anelli di descrittori Ethernet (ETH DMA) | MPU non-cacheable — sempre |
| DMA da/a DTCM (0x20000000) | DTCM non è cachata dalla D-cache, ma DTCM è TCM — l'accesso DMA è più lento; usa DTCM per dati CPU-hot, buffer DMA in AXI SRAM |
| Uso HAL con DMA | HAL NON gestisce la cache — devi aggiungere chiamate SCB manualmente |
Come lo Affronterei su un Progetto Cliente
Su qualsiasi progetto STM32H7, la prima cosa che faccio prima di scrivere una singola riga di codice applicativo è impostare la politica di cache e MPU. Ecco il mio template standard:
- Disabilito la D-cache globalmente durante l'inizializzazione di clock e GPIO — risparmia tempo di debug durante il bring-up.
- Abilito la I-cache incondizionatamente (la cache istruzioni non ha problemi di coerenza).
- Alloco un pool DMA dedicato in una sezione linker mappata in AXI SRAM.
- Configuro una singola regione MPU che marca quel pool come non-cacheable, strong ordering (nessun accesso speculativo).
- Abilito D-cache e MPU in ordine controllato: abilita MPU → DSB/ISB → abilita D-cache.
- Aggiungo un wrapper sottile intorno alle callback HAL DMA che esegue la manutenzione cache in base alla direzione del trasferimento. Questo wrapper è riutilizzato su tutte le periferiche.
Questo previene la classe di bug "funziona su Nucleo, crasha sul prototipo" che deriva dall'eseguire build di debug senza ottimizzazione cache, per poi abilitare la D-cache nella build di produzione e scoprire tutte le chiamate di manutenzione mancanti.
Fonti e Approfondimenti
- Arm Cortex-M7 Processor Technical Reference Manual, r1p2 — Capitolo 5: Memory System, §5.5 L1 Caches. developer.arm.com/documentation/ddi0489/latest/
- ST Application Note AN4839 — Gestione della cache dati e MPU sulle serie STM32H7. st.com/an4839
- ST Application Note AN4807 — Linee guida mappatura memoria e configurazione cache STM32H7.
- CMSIS-Core (Cortex-M) Funzioni SCB —
SCB_CleanDCache,SCB_InvalidateDCache,SCB_CleanInvalidateDCache. Arm CMSIS 5.9.0+. - Esempi firmware STM32CubeH7 — CACHE_CleanInvalidate, CACHE_NonCacheable sotto
Projects/. - Community ST: STM32H7 D-Cache and DMA — Come Procedere

Rispondi a questo articolo via email — leggo e rispondo a ogni messaggio.