Skip to content

arch/arm/stm32h7: invalidate dcache after aligned SDMMC RX DMA#19506

Open
yangrui9501 wants to merge 1 commit into
apache:masterfrom
yangrui9501:fix/stm32h7-sdmmc-aligned-rx-dcache
Open

arch/arm/stm32h7: invalidate dcache after aligned SDMMC RX DMA#19506
yangrui9501 wants to merge 1 commit into
apache:masterfrom
yangrui9501:fix/stm32h7-sdmmc-aligned-rx-dcache

Conversation

@yangrui9501

Copy link
Copy Markdown

Summary

The STM32H7 SDMMC receive DMA path has two completion branches in
stm32_recvdma(): an unaligned path that bounces through an internal
buffer and finishes with a CPU memcpy (coherent by construction), and an
aligned path that DMAs directly into the caller's buffer.

The aligned path only invalidates the destination buffer before the
transfer, in stm32_dmarecvsetup(). It never invalidates again after the
transfer completes. On the Cortex-M7, the D-cache can speculatively
prefetch into that (cacheable) buffer between the pre-DMA invalidate and
DMA completion, leaving stale cache lines that shadow the data the IDMA
just wrote to RAM. The CPU then reads a previously cached sector instead
of the freshly received one.

This fix adds the missing invalidate in stm32_recvdma()'s aligned branch,
once the transfer is complete and before the buffer is consumed. This
matches ST's AN4839 guidance: a cache invalidate is required after DMA
completion and before the CPU reads the region, not only before the
transfer starts. A very similar "invalidate too early" defect on STM32H7
SPI DMA is tracked separately in #11594 — this is the SDMMC instance of
the same class of bug.

Impact

  • Only the aligned RX completion branch of stm32_recvdma() in
    arch/arm/src/stm32h7/stm32_sdmmc.c is touched.
  • The buffer and length are guaranteed cache-line aligned on this path
    (that's the condition for taking it), so the added
    up_invalidate_dcache() call cannot touch adjacent memory.
  • The unaligned (bounce-buffer) path is untouched — it was already
    coherent via its CPU memcpy.
  • No config, Kconfig, or board-level changes.

Testing

Root-caused and verified on a PX4 FMUv6C board (STM32H743) where MAVLink
ULog-over-MAVLink downloads were intermittently corrupted after a firmware
layout change (unrelated custom module addition shifted the heap base by
~10.7 KB, which changed the absolute address / cache-set mapping of the SD
DMA buffers and turned a previously-latent hazard into a reproducible one).

  • Corruption fingerprint: every corrupted byte range was ≤32 bytes,
    32-byte aligned (== the Cortex-M7 D-cache line size), and byte-for-byte
    identical to the data at the same offset in the previous 512-byte SD
    sector (== the FAT single-sector cache buffer, fs_buffer). This is the
    exact signature of a stale cache line shadowing the newly-DMA'd sector.
  • Control experiment: temporarily building with CONFIG_ARMV7M_DCACHE=n
    made the corruption disappear, isolating the defect to D-cache coherency
    (as opposed to, e.g., a DMA/peripheral conflict).
  • Fix verification: with CONFIG_ARMV7M_DCACHE=y and this patch
    applied, a downloaded 6,067,229-byte (5.8 MB) ULog file (spanning
    thousands of SD sectors) was byte-for-byte identical (cmp, matching
    sha256) to the source file read directly off the SD card.
  • Verified ./tools/checkpatch.sh -g HEAD~1..HEAD passes clean on this
    commit.

Concrete example of one corrupted 32-byte cache line (offset 0x71ce0),
showing the corrupted file, what it should contain, and where the
corrupted bytes actually came from (the previous 512-byte sector, at
offset 0x71ce0 - 512 = 0x71ae0):

corrupted file @ 0x00071ce0 (the stale cache line):
00071ce0  00 00 00 68 00 00 00 00 00 00 a9 53 1e 02 00 00  ...h.......S....
00071cf0  00 00 00 00 00 00 ae 0f 00 00 00 00 00 00 00 00  ................

reference file @ same offset (the correct data that was overwritten in RAM):
00071ce0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 04  ................
00071cf0  00 00 01 00 00 00 2a 00 44 03 00 87 6e 1e 02 00  ......*.D...n...

reference file @ offset - 512 (the previous SD sector, still sitting in
the stale D-cache line the CPU read instead of RAM):
00071ae0  00 00 00 68 00 00 00 00 00 00 a9 53 1e 02 00 00  ...h.......S....
00071af0  00 00 00 00 00 00 ae 0f 00 00 00 00 00 00 00 00  ................

corrupted[0x71ce0:0x71d00] == reference[0x71ae0:0x71b00] byte-for-byte —
i.e. the CPU read the D-cache line from the previous sector instead of
the sector the IDMA had just written, which is exactly the stale-line
scenario this patch invalidates against. This was cross-checked across 10
independent corrupted windows in the same file; all 10 showed the same
"equals previous 512-byte sector" pattern. (Full log files available on
request, but omitted here as they add no evidence beyond the above.)

I was not able to build/test the full apache/nuttx tree standalone (no
board config wired up outside the PX4 build), but the touched code path is
identical between this tree and the PX4-maintained fork where the fix was
hardware-verified — the fix here is a direct, unmodified port of the
hardware-verified change.

The aligned direct-DMA receive path only invalidated the destination
buffer before the transfer in stm32_dmarecvsetup(). On the Cortex-M7
the cache can speculatively prefetch into that cacheable buffer
between the pre-DMA invalidate and DMA completion, leaving stale
lines that shadow the data just written by the IDMA, so the CPU
reads a previously cached sector instead of the freshly received
data.

Invalidate again in stm32_recvdma() once the aligned transfer
completes, before the buffer is consumed. The buffer and length are
cache-line aligned on this path, so no adjacent memory is affected.

This matches the STM32 AN4839 guidance that a cache invalidate is
required after DMA completion and before the CPU reads the updated
region, not only before the transfer starts. A related instance of
the same "invalidate too early" defect on STM32H7 SPI DMA is tracked
in apache#11594.

Root-caused on a PX4 FMUv6C (STM32H743) board where MAVLink ULog
downloads were intermittently corrupted: forensic diffing showed
corrupted windows were exactly 32 bytes (the D-cache line size),
cache-line aligned, and byte-for-byte equal to the previous 512-byte
SD sector cached in the FAT single-sector buffer. Disabling the
D-cache made the corruption disappear, isolating the defect to cache
coherency. After this fix, downloaded files matched the source file
byte-for-byte (sha256 identical) across a 5.8 MB log spanning
thousands of sectors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Yang-Rui Li <yang77567789@gmail.com>
@github-actions github-actions Bot added Arch: arm Issues related to ARM (32-bit) architecture Size: S The size of the change in this PR is small labels Jul 23, 2026
@github-actions

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: arm Issues related to ARM (32-bit) architecture Size: S The size of the change in this PR is small

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants