A sensor acquires a new data window. The transfer into memory finishes and the software starts inference. Yet the result seems to refer to the previous window. Before suspecting the model weights or normalization, we need to ask a more basic question: are the processor and the device transferring the data really seeing the same copy? We will understand how two representations of the same address can diverge, and why the remedy requires an agreement about who owns the memory during the transfer.
Abstract. We study a deterministic teaching model of a 32-byte write-back cache line. The CPU accesses the cached copy while DMA accesses RAM directly, without a mechanism automatically keeping them coherent. Seven executed sequences distinguish stale reads, overwriting a new sample and losing a change to a neighboring byte. The central finding is that we must reason about the entire line, not just the buffer bytes. This model explains a mechanism: it does not measure a board, simulate every Cortex-M7 behavior or provide a ready-to-use driver.
Two paths to an address
The CPU is the processor executing the program. RAM is working memory. A cache keeps copies of small memory blocks close to the processor, avoiding repeated trips to RAM. DMA, direct memory access, is a device that can transfer data without making the CPU perform a copy for every element. This helps with sensor samples and images, but introduces a second actor accessing memory.
In our model, a CPU read uses the cache if the block is already there; otherwise it loads the block from RAM. A CPU write modifies the local copy and marks it dirty. Dirty means different from the underlying memory, not corrupted. Under a write-back policy, the change may reach RAM later. DMA instead reads or writes RAM directly. We are not saying every DMA system behaves this way: we specifically assume no hardware coherence between these paths.
A useful analogy is a notebook beside a central register. Updating the register does not automatically correct an entry already copied into the notebook. The analogy ends there: hardware transfers and maintenance have precise units, and we cannot freely choose which individual datum to discard. We therefore move immediately to an explicit model.
Sample 7, sample 99 and a neighbor
Imagine a single line containing 32 one-byte positions, numbered 0 through 31. Initially, RAM holds 7 at position 8 and 1 at position 0; the other positions hold zero. Position 8 represents a sample. Position 0 represents an independent variable that the program wants to update to 9. These are synthetic values, not sensor readings or outputs from an AI model.
The cache starts empty. When the processor reads position 8, the model copies all 32 bytes from RAM into the cache. It can subsequently read 7 from its local copy even if another actor changes RAM. This makes granularity visible: the sample and its neighbor belong to the same maintenance unit.
The 32-byte line is consistent with the Cortex-M7 context described in ST note AN4839; it is not a universal cache property. The example uses offsets rather than actual microcontroller addresses. It does not assume that a particular DMA controller can reach a particular region: reachability must be checked in the selected device manual.
First error: handing over a change still in cache
First consider the CPU-to-DMA direction. The processor wants to transmit 99 and writes it at position 8. Our model now has 99 in cache while RAM retains 7. If DMA starts immediately, it reads 7. From the program's perspective the write has happened, but it has not yet become available along the path used by DMA.
The operation called clean writes the contents of a dirty line back to RAM. Repeating the sequence with clean before the DMA read changes the result from 7 to 99. These two cases isolate the cause: identical initial values and transfer, with just one operation added. They demonstrate no performance gain because the program does not represent cycles, buses or frequencies.
Clean must not be confused with invalidate. In this model invalidate discards the local copy. Discarding the only copy containing a useful change still absent from RAM would lose that change. The right question is not which function sounds more reassuring, but where the information we need to preserve currently resides.
Second error: a completed but invisible reception
Now reverse the direction. The CPU initially reads 7 and thus retains a valid line. DMA writes 99 to RAM at position 8 and finishes. The processor still reads its own copy and gets 7. The transfer succeeded in RAM; the problem is which copy the next read uses.
Invalidate after transfer completion, in our serialized order, removes the old copy. The following read reloads the line and can see 99. We therefore have two different needs: making a CPU change available to DMA, and preventing the CPU from reusing a copy predating a DMA change. Confusing these directions produces the wrong remedy.
So far we have assumed there are no other changes in the line worth preserving. That is a strong assumption, often hidden when the issue is described merely as an old sample to discard. The neighboring byte lets us test it.
Why cleaning too late can worsen the result
The CPU changes the neighbor at position 0 from 1 to 9. Because the write loads and dirties the whole line in our model, the cache also retains the old sample 7. DMA then writes 99 to RAM at position 8. If we now clean, the entire local line is written back: neighbor 9 is saved, but the sample changes back from 99 to 7.
We have not merely read an old copy: we have destroyed the new copy in RAM. Even a subsequent invalidate makes the CPU reload 7 because 99 has already been overwritten. This is rx_late_clean in the code. Its result shows why clean and invalidate cannot be treated as interchangeable operations to append after a transfer.
Trying the opposite remedy exposes another error. In the same situation, perform only invalidate after DMA. The CPU now sees 99 correctly, but the neighbor reverts to 1: change 9 existed only in the discarded copy. A test checking only the sample would report success while missing corruption outside the buffer.
An agreement protecting the whole line
In the correct case in our model, the CPU updates the neighbor, cleans before handing the line to DMA, and leaves it untouched during the transfer. RAM therefore already contains 9 when DMA writes sample 99. After completion, invalidate removes any CPU copy no longer valid. Final reads return 99 and 9: both pieces of information survive.
The crucial condition is no CPU access that could dirty the line again while it belongs to DMA. We test this with a seventh case: after preparation, the CPU modifies the neighbor during the phase assigned to DMA; a subsequent clean recreates the overwrite. Earlier preparation cannot make a later ownership violation harmless.
This agreement concerns the cache's actual granularity. Assigning eight bytes to DMA does not isolate them from the other twenty-four bytes sharing their line. Our experiment does not prescribe a universal firmware call sequence: it exposes the information a concrete protocol must preserve. A real platform requires further documented ordering, completion and maintenance rules.
Which range is actually affected?
Suppose the buffer begins at offset a and has length n, with n greater than zero. If each line contains L bytes, we want all lines intersecting the buffer interval. The mathematical boundaries are:
start = L × floor(a/L) end = L × ceil((a+n)/L) extent = end − start
Floor rounds down and ceil rounds up. All quantities are addresses or byte counts; L is the line size. For a=8, n=8 and L=32, the buffer occupies [8,16), but maintenance covers [0,32). Notation [start,end) includes the first boundary and excludes the last. Position 0 lies outside the buffer but inside the line.
Correct rounding therefore does not resolve sharing: it identifies the territory we must control. Even a buffer with an aligned start can share its last line if its length does not fill a whole number of lines. C code must also check final-address overflow, zero size, API argument types and memory boundaries. The formula is a mathematical relation, not a function ready to paste into a driver.
What the reproducible program does
experiment.py has a 32-value RAM list and an initially absent cache copy. A CPU read fills the copy only when missing. A CPU write updates the copy and sets dirty. Clean transfers the dirty line to RAM; invalidate removes the copy and flag. DMA changes or reads the RAM list without consulting the CPU copy.
Each scenario restarts from the same initial state, inheriting no changes from another scenario. Each operation records a numerical snapshot of sample, neighbor and dirty flag. Assertions check the expected outcomes and stop execution if the relationships illustrated do not hold. No random seed is needed because nothing is randomly drawn. The result is deterministic, not a statistical estimate from many runs.
To repeat the analysis, run Python with experiment.py --out results.json in the material folder. The JSON contains Python version, assumptions, units, covered range and complete timelines. The calculation needs no external package. Inspecting intermediate values is more informative than the final result alone: it separates loss of a CPU change from loss of a DMA change.
Alternatives and open questions
One design option separates buffer lines physically from other variables' lines, controlling placement and sizes. Another uses a non-cacheable region where the device permits it. A third copies the data into a working buffer after a coherent handoff. These are strategies to compare, not universal winners: placement, DMA accessibility and transfer cost depend on the system.
The model cannot assign milliseconds or joules to these alternatives. It represents neither spontaneous eviction, speculative access, interrupts, multicore concurrency nor barriers. Their absence does not justify ignoring them in firmware; it specifically prevents treating this experiment as product-correctness evidence. Before a physical test, identify device and revision, memory region, MPU attributes, DMA controller, runtime and CMSIS headers, along with the test protocol.
Answering the opening question
The sensor may have delivered a new sample correctly while AI still reads the old one, because CPU and DMA do not necessarily consult the same copy. Distinguishing clean from invalidate explains how changes become visible in the two directions. The neighboring byte adds the decisive point: keeping only the sample coherent is insufficient when operations affect a whole line.
The lesson is to design the transfer as a temporary handoff of memory with explicit ownership and granularity. This preserves both 99 and 9 in our model; on a real board, the same idea must be translated and verified against platform documentation. No hardware measurements were made, and no EL-AI performance or functionality was demonstrated.
References and material status
STMicroelectronics, AN4839, revision 2, March 2018, sections 2, 2.1, 3.2 and 4, read during preparation on 11 October: https://www.st.com/resource/en/application_note/an4839-level-1-cache-on-stm32f7-series-and-stm32h7-series-stmicroelectronics.pdf
Arm CMSIS-Core 6, D-Cache Functions, documentation re-read on 15 October 2026; parameters of by-address operations: https://arm-software.github.io/CMSIS_6/latest/Core/group__Dcache__functions__m7.html
AI-assisted text, without claiming human review. Own synthetic examples; no reproduced benchmark or original peer-reviewed research.
Synthetic outcomes of all seven cases
| Sequence | Observed sample | Final neighbor |
|---|---|---|
| tx_missing_clean | 7 | 1 |
| tx_clean | 99 | 1 |
| rx_stale | 7 | 1 |
| rx_late_clean | 7 | 9 |
| rx_only_post_invalidate | 99 | 1 |
| rx_clean_before_invalidate_after | 99 | 9 |
| rx_neighbor_written_during_transfer | 7 | 9 |

Complete code of the executed model
"""Deterministic teaching model, not a Cortex-M7 emulator or hardware benchmark.
One write-back cache line with 32 byte entries; CPU/DMA operations are serialized.
No timing, barriers, speculative fills, cache sets, bus topology or instruction model.
"""
import json,platform,argparse
from pathlib import Path
class Model:
def __init__(self):
self.ram=[0]*32;self.ram[8]=7;self.ram[0]=1
self.cache=None;self.dirty=False;self.trace=[]
def snap(self,event):
self.trace.append(dict(event=event,ram_buffer=self.ram[8],ram_neighbor=self.ram[0],cache_buffer=None if self.cache is None else self.cache[8],cache_neighbor=None if self.cache is None else self.cache[0],dirty=self.dirty))
def read(self,i):
if self.cache is None:self.cache=self.ram.copy()
value=self.cache[i];self.snap('cpu_read_'+str(i));return value
def write(self,i,v):
if self.cache is None:self.cache=self.ram.copy()
self.cache[i]=v;self.dirty=True;self.snap('cpu_write_'+str(i))
def dma_write(self,i,v):self.ram[i]=v;self.snap('dma_write_'+str(i))
def clean(self):
if self.dirty:self.ram=self.cache.copy();self.dirty=False
self.snap('clean')
def invalidate(self):self.cache=None;self.dirty=False;self.snap('invalidate')
def scenario(name):
m=Model()
if name.startswith('tx'):
m.write(8,99)
if name=='tx_clean':m.clean()
dma=m.ram[8];m.snap('dma_read_8');out={'dma_value':dma,'expected':99}
elif name=='rx_stale':
m.read(8);m.dma_write(8,99);out={'cpu_value':m.read(8),'expected':99}
elif name=='rx_late_clean':
m.write(0,9);m.dma_write(8,99);m.clean();m.invalidate();out={'cpu_value':m.read(8),'neighbor':m.read(0),'expected':99}
elif name=='rx_only_post_invalidate':
m.write(0,9);m.dma_write(8,99);m.invalidate();out={'cpu_value':m.read(8),'neighbor':m.read(0),'expected_neighbor':9}
elif name=='rx_clean_before_invalidate_after':
m.write(0,9);m.clean();m.dma_write(8,99);m.invalidate();out={'cpu_value':m.read(8),'neighbor':m.read(0),'expected':99,'expected_neighbor':9}
elif name=='rx_neighbor_written_during_transfer':
m.read(8);m.clean();m.write(0,9);m.dma_write(8,99);m.clean();m.invalidate();out={'cpu_value':m.read(8),'neighbor':m.read(0),'expected':99}
else:raise ValueError(name)
return dict(name=name,outcome=out,trace=m.trace)
def main(out):
names=['tx_missing_clean','tx_clean','rx_stale','rx_late_clean','rx_only_post_invalidate','rx_clean_before_invalidate_after','rx_neighbor_written_during_transfer']
rows={n:scenario(n) for n in names}
assert rows['tx_missing_clean']['outcome']['dma_value']==7
assert rows['tx_clean']['outcome']['dma_value']==99
assert rows['rx_stale']['outcome']['cpu_value']==7
assert rows['rx_late_clean']['outcome']['cpu_value']==7
assert rows['rx_only_post_invalidate']['outcome']=={'cpu_value':99,'neighbor':1,'expected_neighbor':9}
assert rows['rx_clean_before_invalidate_after']['outcome']=={'cpu_value':99,'neighbor':9,'expected':99,'expected_neighbor':9}
assert rows['rx_neighbor_written_during_transfer']['outcome']['cpu_value']==7
# A range [8,16) touches cache line [0,32), including unrelated byte0.
a=8;n=8;line=32;lo=(a//line)*line;hi=((a+n+line-1)//line)*line
assert (lo,hi)==(0,32)
r={'python':platform.python_version(),'seed':None,'units':{'address':'byte offsets','values':'synthetic byte values'},'hardware_executed':False,'line_size_bytes':32,'assumptions':'Serialized operations on one initially empty write-back cache line; DMA accesses RAM directly; full-line clean/invalidate; no hardware timing or cache-policy reproduction','range':{'buffer_start':a,'buffer_size':n,'maintenance_start':lo,'maintenance_size':hi-lo},'scenarios':rows}
Path(out).write_text(json.dumps(r,indent=2),encoding='utf8')
print(json.dumps({n:v['outcome'] for n,v in rows.items()},indent=2))
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--out',default='results.json');main(p.parse_args().out)
Reproducibility and complexity
Copying a line of L elements takes O(L) Python operations and O(L) memory in this program. Each scenario has a fixed number of accesses. These are teaching-model costs, not real cache cycles. Run experiment.py --out results.json; plot.py requires Matplotlib and Windows Microsoft YaHei for Chinese. The font is not distributed.
Download code and results · Results JSON
AI-generated illustrative cover, not a photograph of an EL-AI device or laboratory.

