openlang

STM32H7 UART TX DMA sends old bytes when D-cache is on

STM32H7, arm-none-eabi-gcc. DMA TX of a 64 byte frame. Cache off: fine. With -DUSE_DCACHE, the analyzer shows wrong frames. The first frame after reset is correct. Later ones carry bytes of the previous frame, in 32 byte blocks. Sometimes the front is new and the tail is old.

Eventually the compiler moves the copy loop behind the enable write. But -O0 is the same. tx_buf volatile is the same. DSB after the loop is the same. In the disassembly the stores come before the CR write.

for (size_t i = 0; i < n; i++)
    tx_buf[i] = data[i];

__asm volatile ("dsb 0xF" ::: "memory");

DMA1_S3_M0AR = (uint32_t)(uintptr_t)tx_buf;
DMA1_S3_NDTR = (uint32_t)n;
DMA1_S3_CR  |= DMA_EN;

Cache is enabled in board_init():

SystemInit();
#ifdef USE_DCACHE
    SCB_EnableICache();
    SCB_EnableDCache();
#endif

Build:

make clean && make OPT=-O0 DEFS=-DUSE_DCACHE -DTRACE_ITM

The order looks right to me and the data is still old. What else is there?

The conversation continues 8

Follow the reasoning
  1. Permalink

    DSB orders the stores, but it doesn't write the cache lines back to RAM. Do you call SCB_CleanDCache_by_Addr on tx_buf before the enable? And is tx_buf aligned to 32 bytes, no?

  2. Permalink

    No, I don't call clean anywhere. I thought DSB was enough, but it only orders the stores. That fits the 32 byte blocks, since that is the line size.

    tx_buf is a plain static array, no aligned attribute. Eventually it shares a line with another variable.

    Since two years I do this in my class: buffer aligned to 32, size a multiple of 32, then clean by address before the EN write. On an F7 once, a variable next to my RX buffer got corrupted by the invalidate. I will try it now.

    static uint8_t tx_buf[64] __attribute__((aligned(32)));
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, sizeof tx_buf);
  3. Permalink

    Works now. All frames correct on the analyzer, including the tail blocks.

    anselm was right. DSB orders the stores, but the lines stay dirty in the cache and the DMA reads stale RAM. The CMSIS clean function does its own DSB, so my extra one is not needed.

    The clean goes after the copy loop and before the M0AR write. Cleaning before the loop does nothing.

    For RX it is the other way round. Invalidate before the DMA starts, or eventually the CPU evicts a dirty line over your new data. A student of mine lost a whole evening on this.

  4. Permalink

    Caveat for the TX side: cleaning a line shared with another variable is harmless, it only writes back. Invalidate is the dangerous one.

    Alternative: put the DMA buffers in their own section and mark that region non-cacheable in the MPU. Then no clean or invalidate calls at all. I did this once for an SPI buffer, and the bug never came back.

  5. Permalink

    It's the D-cache. DSB only orders the stores, the lines stay dirty and the DMA reads stale RAM. Clean tx_buf by address after the copy loop, before the EN write, and align it to 32. Or put the buffer in DTCM (it's uncached, read that in a blog somewhere).

Other sides of the same question

  1. Callbacks from C lose the arena I set up

    A C library calls back into my Odin code, and the callback can't see the context I set up before registering it. A proc "c" g…

    Replies 5Latest activity odinc
  2. TMR3 ISR loses capture events although flags are w1c and cleared

    Reflow oven controller, Cortex-M0 clone, arm-none-eabi-gcc -O2, Fedora. TMR3 runs at 1 MHz. Update event is the 1 ms tick, CH…

    Replies 5Latest activity c
  3. Unwrapping a C pointer with .? is an assertion

    .? on a pointer that came back from C is an assertion, not a check. It says null is impossible here. In a safety-checked buil…

    Replies 3Latest activity zig