Cache on MCU (Cont)
(Cont)
Cache Policies
Write allocate Cache Policy
The write allocate policy dictates that when a write miss occurs (i.e., the data to be written is not currently in the cache), the cache line containing the target memory address is loaded into the cache, and then the write operation is performed on the cache.
How It Works
- Write Miss:
- Cache Miss Occurs: When the processor tries to write to a memory address that is not present in the cache, a cache miss occurs.
- Load Data into Cache: The cache line that contains the target memory address is fetched from the main memory and loaded into the cache.
- Write Data to Cache: The write operation is then performed on the newly loaded cache line.
- Subsequent Writes: Once the cache line is in the cache, subsequent writes to addresses within that line will be cache hits, allowing for faster write operations.
Write Allocate
In the ARM Cortex-M7 processor, the write allocate policy can be configured for the data cache. Here’s how it typically works:
- Handling Write Misses:
- On a write miss, the ARM Cortex-M7 fetches the required cache line from the main memory and loads it into the cache.
- The processor then performs the write operation on the loaded cache line.
- Depending on whether the write-back or write-through policy is in use, subsequent writes will either set the dirty bit (write-back) or write to both the cache and main memory (write-through).
- Integration with Write-Back:
- Write-Back + Write Allocate: This combination is common. When a write miss occurs, the line is brought into the cache, written to, and marked as dirty. The write-back policy will later handle writing this modified data back to the main memory.
- Integration with Write-Through:
- Write-Through + Write Allocate: When a write miss occurs, the line is brought into the cache, written to, and the data is simultaneously written to the main memory. This ensures that the cache and main memory are always in sync.
Example: Write Allocate Policy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Assume the processor wants to write the value 20 to memory address 0x20001000, but the corresponding cache line is not currently in the cache (write miss).
Initial state
| Location | Value |
|-----------|----------------|
|Cache | Not Present |
|Main Memory| 10 |
Write operation
CPU writes 20 -> Write Miss -> Fetch cache line from Main Memory -> Load cache line into Cache -> Write 20 to Cache
Final state (Write-Back + Write Allocate)
| Location | Value |
|-----------|----------------|
|Cache | 20 (Dirty = 1) |
|Main Memory| 10 |
The cache line is first loaded into the cache, then updated. Since write-back is used, the main memory is updated later when the cache line is evicted or cleaned.
Final state (Write-Through + Write Allocate)
| Location | Value |
|-----------|----------------|
|Cache | 20 |
|Main Memory| 10 |
After loading the cache line, the write updates both the cache and the main memory.
Key Point: Write Allocate determines what happens on a write miss. The subsequent write behavior (cache only or cache + memory) is determined by the write policy (Write-Back or Write-Through).
Read Allocate Cache Policy
The read allocate policy dictates that when a read miss occurs (i.e., the data to be read is not currently in the cache), the cache line containing the target memory address is fetched from the main memory and loaded into the cache. The read operation is then completed using the data in the cache.
How It Works
- Read Miss:
- Cache Miss Occurs: When the processor tries to read a memory address that is not present in the cache, a cache miss occurs.
- Load Data into Cache: The entire cache line that contains the target memory address is fetched from the main memory and loaded into the cache.
- Complete Read Operation: The processor completes the read operation using the data now present in the cache.
- Subsequent Reads: Once the cache line is loaded into the cache, subsequent reads to addresses within that line will result in cache hits, providing faster access to the data.
Read Allocate
In the ARM Cortex-M7 processor, the read allocate policy is typically used for both the instruction cache and the data cache. Here’s how it typically works:
- Handling Read Misses:
- On a read miss, the ARM Cortex-M7 fetches the required cache line from the main memory and loads it into the cache.
- The read operation is then completed using the loaded cache line.
- Subsequent reads to the same or nearby addresses within that cache line will be cache hits, improving read access times.
- Integration with Cache Policies:
- Read-Allocate + Write-Back: When a read miss occurs, the line is brought into the cache. Subsequent writes to this line will mark it as dirty, and the write-back policy will later handle writing this modified data back to the main memory.
- Read-Allocate + Write-Through: When a read miss occurs, the line is brought into the cache. Subsequent writes will update both the cache and the main memory, ensuring consistency between the two.
Example: Read Allocate Policy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Assume the processor wants to read memory address 0x20001000, but the corresponding cache line is not currently in the cache (read miss).
Initial state
| Location | Value |
|-----------|----------------|
|Cache | Not Present |
|Main Memory| 10 |
Read operation
CPU reads 20 -> Read Miss -> Fetch cache line from Main Memory -> Load cache line into Cache -> Return requested data to CPU
Final state
| Location | Value |
|-----------|----------------|
|Cache | 10 |
|Main Memory| 10 |
The requested cache line is now stored in the cache. Any subsequent read to the same address (or another address within the same cache line) becomes a cache hit, avoiding another memory access.
For example:
1st read:
CPU -> Memory -> Cache ->CPU (Cache Miss)
2nd read:
CPU -> Cache -> CPU (Cache Hit)
Key Point: Read Allocate determines what happens on a read miss. The missed cache line is fetched from main memory and stored in the cache so that future accesses to the same cache line can be served more quickly.
Cache Coherency
Cache coherency issues arise when the view of memory differs between masters.
CPU-write/DMA-read scenario
- The CPU writes data to a buffer in cacheable SRAM (for example, preparing data for UART transmission).
- With a write-back policy, the data might only be in the DCACHE, not yet written to SRAM.
- The DMA controller reads directly from SRAM to transmit the data.
- As a result, the DMA transmits stale data from SRAM because the up-to-date data is still held in the CPU’s DCACHE.
DMA-write/CPU-read scenario
- The DMA controller writes incoming data directly into a buffer in SRAM (for example, converted data by ADC).
- The CPU might have previously read from this buffer, and the old data could still be resident (cached) in its DCACHE.
- The CPU reads the buffer to process the new data.
- As a result, the CPU reads the stale data from its DCACHE, unaware that the DMA has updated the underlying SRAM.
Hands-on Project
To see the cache coherency problem in action (and confirm the fix), the two scenarios described above can be reproduced on any Cortex-M7 part with a DCACHE and an M2M-capable DMA (an STM32H7-series MCU is a convenient choice since it exposes both).
Setup
- Board: any STM32H7 Nucleo/Discovery board (single or dual-core).
- Enable the D-Cache in
main()(e.g. viaSCB_EnableDCache()), and configure one DMA stream/channel in memory-to-memory mode targeting two SRAM buffers. - Two buffers, one counter:
1
2
3
4
#define BUF_SIZE 10
uint8_t txBuf[BUF_SIZE];
uint8_t rxBuf[BUF_SIZE];
uint8_t counter = 0;
Step 1: Reproduce the corruption
Fill txBuf, kick off the M2M DMA transfer into rxBuf, then inspect rxBuf in the debugger with no cache maintenance in place:
1
2
3
4
5
6
7
8
9
10
11
12
13
while (1)
{
for (int i = 0; i < BUF_SIZE; i++)
{
txBuf[i] = counter;
}
HAL_DMA_Start(&hdma_mem2mem, (uint32_t)txBuf, (uint32_t)rxBuf, BUF_SIZE);
HAL_DMA_PollForTransfer(&hdma_mem2mem, HAL_DMA_FULL_TRANSFER, 100);
counter++;
HAL_Delay(200);
}
This lines up with the CPU-write/DMA-read scenario: txBuf[i] = counter may only be sitting in DCACHE, so the DMA reads stale bytes straight out of SRAM. Watching rxBuf in the debugger, it either never updates or lags several iterations behind counter.
Step 2: Add cache maintenance
Two calls are enough to make the buffers coherent again:
- Clean
txBufout of DCACHE (push CPU writes to SRAM) before starting the DMA read. - Invalidate
rxBufin DCACHE (discard any stale cached copy) after the DMA write completes, so the next CPU read is forced back to SRAM.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while (1)
{
for (int i = 0; i < BUF_SIZE; i++)
{
txBuf[i] = counter;
}
SCB_CleanDCache_by_Addr((uint32_t *)txBuf, BUF_SIZE);
HAL_DMA_Start(&hdma_mem2mem, (uint32_t)txBuf, (uint32_t)rxBuf, BUF_SIZE);
HAL_DMA_PollForTransfer(&hdma_mem2mem, HAL_DMA_FULL_TRANSFER, 100);
SCB_InvalidateDCache_by_Addr((uint32_t *)rxBuf, BUF_SIZE);
counter++;
HAL_Delay(200);
}
With both calls in place, rxBuf tracks counter correctly on every iteration — the same fix, mirrored, addresses the DMA-write/CPU-read scenario (invalidate the destination buffer before the CPU reads data the DMA just wrote).
Takeaway
- Clean the cache before a DMA read from a buffer the CPU wrote.
- Invalidate the cache before a CPU read of a buffer the DMA just wrote.
- Alternatively, place DMA buffers in a non-cacheable memory region via the MPU — this avoids the maintenance calls entirely at the cost of losing cache speedups for that region. Whether to use cache maintenance or an MPU non-cacheable region depends on how performance-sensitive the buffer access is.