Skip to main content

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:

DirectionSymptomFix
Memory → peripheralDMA transmits stale data; the CPU's write is still in the cacheSCB_CleanDCache_by_Addr() before starting the transfer
Peripheral → memoryThe CPU reads stale data; DMA updated RAM but the cache holds old linesSCB_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 = 0UL invalidates 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_Msk sets 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

IntrinsicInstructionGuarantees
__DMB()Data Memory BarrierMemory accesses before it are observed before those after it. Does not wait for completion.
__DSB()Data Synchronization BarrierMemory accesses before it have completed before any instruction after it executes.
__ISB()Instruction Synchronization BarrierFlushes 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 to static plus 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.
  • __ASM is the CMSIS macro for the compiler's inline assembly keyword.
  • volatile prevents 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. 0xF is the SY option: 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.