1. Тема дня
DMA позволяет периферии читать и писать RAM без участия CPU. Это удобно для ADC, UART, SPI, CAN, Ethernet и больших потоков данных, но появляется новая проблема: кто сейчас владеет буфером и видят ли CPU и DMA одинаковую версию данных. Типовой путь:
ADC / UART / SPI / CAN / Ethernet
-> DMA
-> DMA buffer pool
-> ownership transfer
-> FreeRTOS owner-task
-> parser / DSP / FSMГлавная мысль: zero-copy начинается не с отсутствия mem- cpy(), а с явного владения буфером. Если DMA и CPU одновременно считают буфер своим, zero-copy превращается в race condition.
2. Зачем это нужно в твоих проектах
Для ADC-детектирования фаз лучше использовать ping-pong/ring DMA: пока DMA пишет блок B, CPU обрабатывает блок A. Для EC25 и больших MQTT payload полезно передавать pointer + length + ownership token, а не копировать 4 КБ несколько раз. Для CAN/RS-485 gateway выгодно передавать frame descriptors между задачами вместо массивов байтов. На ESP32-P4 и STM32H7/F7 тема особенно важна из-за cache coherency. CPU может держать свежие данные в cache, а DMA видеть старую RAM, или наоборот.
3. Теория
Zero-copy как ownership protocol
Обычный путь:
DMA buffer -> memcpy -> parser buffer -> memcpy -> message object -> memcpy -> applicationZero-copy путь:
DMA buffer -> pointer + length + timestamp + ownership token -> applicationПример descriptor:
typedef struct {
uint8_t *data;
size_t length;
uint32_t buffer_id;
uint32_t generation;
uint64_t timestamp_us;
} buffer_view_t;Состояния RX-буфера
FREE -> DMA_OWNED -> CPU_READY -> CPU_OWNED -> FREEНедопустимо, чтобы DMA писал буфер, пока CPU его обрабатывает. TX-цикл
FREE -> CPU_FILLING -> READY_FOR_DMA -> DMA_OWNED -> FREEПосле запуска DMA CPU не должен изменять TX-буфер до comple- tion event.
Ping-pong DMA
Time 0: DMA writes A
Time 1: DMA writes B, CPU processes A
Time 2: DMA writes A, CPU processes BЕсли обработка CPU иногда дольше периода DMA, двух буферов мало — нужен ring/pool и backpressure policy.
Cache coherency
CPU -> DMA:
CPU изменил buffer -> clean cache -> RAM обновлена -> DMA читает правильные данныеDMA -> CPU:
DMA записал RAM -> invalidate cache -> CPU читает свежие данные из RAMНа STM32 Cortex-M7 используются SCB_CleanDCache_by_Addr() и SCB_InvalidateDCache_by_Addr(). На ESP32-P4 используется esp_cache_msync() с направлениями C2M и M2C.
Почему важна cache-line alignment
Если DMA-буфер делит cache line с обычной переменной, in- validate может выбросить dirty данные соседней переменной. Поэтому DMA-буфер должен занимать собственные cache lines, а размер и адрес должны быть выровнены.
volatile не решает coherency
volatile не делает cache clean, не делает invalidate, не является memory barrier и не делает доступ thread-safe.
4. Типичные ошибки
- Отдать DMA обычный malloc()-буфер без проверки memory capability.
- Использовать stack buffer для асинхронного DMA.
- Менять TX-буфер после запуска DMA.
- Читать RX-буфер до completion event.
- На M7 забыть invalidate D-cache после RX DMA.
- Перед TX сделать invalidate вместо clean.
- Считать volatile заменой cache maintenance.
- Инвалидировать невыравненную cache region и повредить соседние данные.
- Забыть синхронизировать DMA descriptors.
- Передавать 4-КБ структуры через FreeRTOS queue.
- Передавать pointer без lifetime protocol.
- Не иметь generation в reusable buffer handle.
- Ждать свободный буфер в ISR.
- Давать logger удерживать realtime buffer.
- Пытаться сделать zero-copy для маленьких control messages, где копирование проще и безопаснее.
5. Практическое задание на 30-60 минут
Создай DMA_BUFFER_POLICY.md:
# DMA buffer policy
1. Every DMA buffer has exactly one owner.
2. Ownership transitions are explicit.
3. DMA buffers are never allocated in ISR.
4. Buffers are aligned to DMA/cache requirements.
5. CPU does not access DMA-owned buffers.
6. Cache synchronization occurs only at ownership boundaries.
7. FreeRTOS queues pass handles, not large payloads.
8. Every reusable buffer has a generation number.
9. Pool exhaustion has an explicit drop/degraded policy.
10. Driver-owned buffers are never retained beyond documented lifetime.Затем реализуй маленький pool:
#define DMA_POOL_COUNT 4U
#define DMA_BLOCK_SIZE 1024U
#define DMA_ALIGNMENT 32U
typedef enum {
BUFFER_FREE = 0,
BUFFER_DMA_OWNED,
BUFFER_CPU_READY,
BUFFER_CPU_OWNED,
} buffer_state_t;
typedef struct {
_Alignas(DMA_ALIGNMENT) uint8_t data[DMA_BLOCK_SIZE];
uint16_t generation;
uint16_t state;
uint32_t valid_length;
uint32_t sequence;
} dma_buffer_t;
typedef struct {
dma_buffer_t buffers[DMA_POOL_COUNT];
uint32_t allocation_failures;
uint32_t stale_handles;
uint32_t invalid_transitions;
} dma_buffer_pool_t;Проверь transitions:
FREE -> DMA_OWNED -> CPU_READY -> CPU_OWNED -> FREEИ ошибки:
CPU acquire before DMA completion
second completion of same handle
double release
stale handle after buffer reuse
received > capacity
pool exhausted
generation mismatch6. Что попробовать дальше
- Для ESP32-P4 изучить esp_cache_msync(), MALLOC_CAP_DMA, MALLOC_CAP_CACHE_ALIGNED, MALLOC_CAP_DMA_DESC_AHB/AXI.
- Для STM32H7/F7 изучить AN4839 и CMSIS D-cache API.
- Измерить CPU time и latency для memcpy-pipeline и handle- based zero-copy pipeline.
Задание
Проследите RX handle через FREE -> DMA_OWNED -> CPU_READY -> CPU_OWNED -> FREE. Когда CPU может читать данные и почему старый handle нужно отклонить после повторного использования buffer?
Критерии самопроверки: Укажите completion, coherency, исключительное владение CPU и проверку generation.
Показать ответ автора
CPU читает после DMA completion и необходимой cache maintenance, когда handle получен в CPU_OWNED. Release возвращает buffer в FREE. Несовпадение generation обозначает старый handle уже переиспользованного buffer, которому нельзя читать или освобождать данные нового владельца.