2026-06-14 · Davide Carrese

Coerenza D-Cache su STM32H7:
la trappola DMA che ogni sviluppatore embedded deve conoscere

STM32 · STM32H7 · DMA · D-Cache · Cortex-M7 · MPU · Embedded

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:

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:

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

⚠ Errore Comune

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

ScenarioAzione
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 flashNessuna 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 DMAHAL 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:

  1. Disabilito la D-cache globalmente durante l'inizializzazione di clock e GPIO — risparmia tempo di debug durante il bring-up.
  2. Abilito la I-cache incondizionatamente (la cache istruzioni non ha problemi di coerenza).
  3. Alloco un pool DMA dedicato in una sezione linker mappata in AXI SRAM.
  4. Configuro una singola regione MPU che marca quel pool come non-cacheable, strong ordering (nessun accesso speculativo).
  5. Abilito D-cache e MPU in ordine controllato: abilita MPU → DSB/ISB → abilita D-cache.
  6. 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

💬 Commenti via email

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