Cache and memory barriers
The L1 caches exist on Cortex-M7 cores (STM32F7, H7). On M3, M4 and M33 the CMSIS cache functions compile away to nothing, so the same source builds for both.
CPU_CACHE_Enable
/**
* @brief CPU L1-Cache enable.
* @param None
* @retval None
*/
static void CPU_CACHE_Enable(void) {
/* Enable I-Cache */
SCB_EnableICache();
/* Enable D-Cache */
SCB_EnableDCache();
}
A convenience wrapper that turns on both caches in one call. Configure the MPU first — the caches obey the attributes it publishes, so enabling them against the reset defaults and then changing the attributes leaves stale lines behind.
Instruction cache
SCB_EnableICache() enables the instruction cache, which holds recently fetched
instructions. The core checks the cache before going to flash, and a hit is far
faster than a flash access at high clock rates where several wait states apply.
The I-Cache is essentially free to enable: instruction memory is read-only in
normal operation, so there is no coherency problem to manage. Code that rewrites
itself, or a bootloader that jumps into freshly programmed flash, must
invalidate it first with SCB_InvalidateICache().
Data cache
SCB_EnableDCache() enables the data cache. Same principle applied to data, and
it reduces traffic to SRAM and external memory considerably.
The D-Cache needs care, because the CPU is no longer the only view of memory.
Cache coherency with DMA
Two failure modes, and they look nothing alike:
| Direction | Symptom | Fix |
|---|---|---|
| Memory → peripheral | DMA transmits stale data; the CPU's write is still in the cache | SCB_CleanDCache_by_Addr() before starting the transfer |
| Peripheral → memory | The CPU reads stale data; DMA updated RAM but the cache holds old lines | SCB_InvalidateDCache_by_Addr() after the transfer completes |
/* TX: push the CPU's writes out to memory before the DMA reads them */
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, sizeof(tx_buf));
HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf));
/* RX: discard cached copies so the CPU sees what the DMA wrote */
HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
/* ... in the transfer-complete callback: */
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, sizeof(rx_buf));
Alignment is not optional. Both functions operate on 32-byte cache lines. Align DMA buffers to 32 bytes and round their length up to a multiple of 32, otherwise a clean or invalidate touches neighbouring variables sharing the same line. An invalidate on a partially shared line discards a legitimate cached write to an adjacent variable — a corruption bug with no visible connection to the DMA code.
ALIGN_32BYTES(static uint8_t rx_buf[64]); // CMSIS macro; 64 is already a multiple of 32
The alternative is to place DMA buffers in a region the MPU marks non-cacheable and skip maintenance entirely. That is simpler and usually the right call for descriptors and small control structures; reserve the clean/invalidate approach for large buffers where the cache actually earns its keep.
Cache maintenance is also required for any memory shared with a second core, and for the Ethernet and USB descriptor tables that peripherals write directly.
SCB_EnableICache
/**
\brief Enable I-Cache
\details Turns on I-Cache
*/
__STATIC_FORCEINLINE void SCB_EnableICache (void)
{
#if defined (__ICACHE_PRESENT) && (__ICACHE_PRESENT == 1U)
if (SCB->CCR & SCB_CCR_IC_Msk) return; /* return if ICache is already enabled */
__DSB();
__ISB();
SCB->ICIALLU = 0UL; /* invalidate I-Cache */
__DSB();
__ISB();
SCB->CCR |= (uint32_t)SCB_CCR_IC_Msk; /* enable I-Cache */
__DSB();
__ISB();
#endif
}
Guard macro
#if defined (__ICACHE_PRESENT) && (__ICACHE_PRESENT == 1U) checks whether the
device header declares an instruction cache. On a part without one the body
compiles to nothing, so the application needs no conditional code of its own.
Early return
if (SCB->CCR & SCB_CCR_IC_Msk) return; tests the instruction-cache enable bit
in the System Control Block's Configuration and Control Register. If the cache
is already on, the function returns immediately — re-invalidating an active
cache would throw away useful lines for no benefit.
Invalidation
__DSB()waits for all outstanding memory accesses to complete.__ISB()flushes the pipeline so subsequent instructions are re-fetched.SCB->ICIALLU = 0ULinvalidates the entire instruction cache. This is mandatory before enabling: the cache's contents are undefined after reset, and enabling it with unknown tags would let the core execute garbage.
Enabling
- The barriers are repeated to keep the invalidate strictly ordered before the enable.
SCB->CCR |= SCB_CCR_IC_Msksets the enable bit and turns the cache on.- The final
__DSB()and__ISB()pair guarantees the write has taken effect before the next instruction is fetched, so nothing executes in the window where the cache is half-enabled.
SCB_EnableDCache() follows the same shape, with one addition: it invalidates
the D-Cache set-by-set through SCB->DCISW rather than with a single register
write, because there is no invalidate-all operation for data.
Barrier instructions
| Intrinsic | Instruction | Guarantees |
|---|---|---|
__DMB() | Data Memory Barrier | Memory accesses before it are observed before those after it. Does not wait for completion. |
__DSB() | Data Synchronization Barrier | Memory accesses before it have completed before any instruction after it executes. |
__ISB() | Instruction Synchronization Barrier | Flushes the pipeline; instructions after it are re-fetched, seeing any context change. |
__DSB() is the strongest of the data barriers and the one to reach for when a
memory write has a side effect the next instruction depends on: enabling a
clock, writing a peripheral configuration register, changing MPU or cache state,
updating a vector table.
The order is __DSB() then __ISB(), because the data change must land before
the pipeline is flushed. __DSB() alone is enough where only data ordering
matters.
__DSB
/**
\brief Data Synchronization Barrier
\details Acts as a special kind of Data Memory Barrier.
It completes when all explicit memory accesses before this instruction complete.
*/
__STATIC_FORCEINLINE void __DSB(void)
{
__ASM volatile ("dsb 0xF":::"memory");
}
__STATIC_FORCEINLINE— a CMSIS macro expanding tostaticplus a compiler-specific always-inline attribute. Static keeps the symbol local to the translation unit; forced inlining removes the call overhead, which matters because the body is a single instruction.__ASMis the CMSIS macro for the compiler's inline assembly keyword.volatileprevents the optimiser from moving or deleting the statement. An inline assembly block with no outputs would otherwise be a candidate for removal."dsb 0xF"is the instruction.0xFis theSYoption: a full-system barrier covering both reads and writes.:::"memory"is the GCC memory clobber. It tells the compiler memory may have changed, so it must not cache values in registers across this point or reorder memory accesses around it. Without it the hardware barrier still executes, but the compiler is free to reorder the surrounding C — a bug that only appears at higher optimisation levels.
A common place this matters outside cache code: after clearing a peripheral
interrupt flag at the end of a handler. The write can still be in flight when
the handler returns, and the core re-enters immediately. __DSB() before the
return forces it to land. See Interrupts.