feat(esp_driver_dma): add dw_gdma backend for async_memcpy

This commit is contained in:
morris
2026-08-10 16:16:53 +08:00
parent a063a8440f
commit eec76a070e
29 changed files with 1144 additions and 340 deletions

View File

@@ -1,96 +1,166 @@
============
异步内存复制
============
:link_to_translation:`en:[English]`
概述
异步内存复制驱动使用 DMA 搬运数据,让 CPU 可以同时处理其他工作。它适合较大的 buffer例如 CPU 在等待 ``memcpy`` 时,本可以准备下一帧或处理上一块数据的场景。
本文先完成一次阻塞复制,再介绍如何在复制期间继续执行任务。
.. contents::
:local:
:depth: 2
开始前
======
该驱动仅在支持异步内存复制的目标芯片上可用。在包含 ``esp_async_memcpy.h`` 前,请为项目组件添加 ``esp_driver_dma`` 依赖。
DMA 必须能访问源和目标 buffer。目标 buffer 应分配在 DMA 可访问的 RAM 中。特定 DMA 后端是否支持 PSRAM取决于目标芯片和所选后端。
.. important::
在复制完成前,不要读取或修改目标 buffer也不要修改源 buffer。
快速开始
========
典型流程如下:
.. mermaid::
flowchart TD
install["安装驱动"] --> choose{"任务如何等待?"}
choose --> blocking["阻塞复制<br/>esp_memcpy_blocking"]
choose --> async["异步复制<br/>esp_async_memcpy"]
async --> callback["回调通知任务"]
blocking --> use["使用目标 buffer"]
callback --> use
use --> more{"还要继续复制?"}
more -->|是| choose
more -->|否| uninstall["卸载驱动"]
classDef blocking fill:#E8F1FB,stroke:#3B82C4,color:#1B4F72
classDef async fill:#F3E8FF,stroke:#8B5CF6,color:#5B2C8A
classDef result fill:#E8F5E9,stroke:#43A047,color:#1B5E20
classDef cleanup fill:#F5F5F5,stroke:#757575,color:#424242
class blocking blocking
class async,callback async
class use,result result
class uninstall cleanup
场景 1复制一个 Buffer 并等待
===============================
如果下一步操作必须立刻使用复制结果,先使用 :cpp:func:`esp_memcpy_blocking`。它会对合适的 buffer 使用 DMA并等待复制完成对于较小的 buffer它会安全地回退到 CPU 复制。
.. code-block:: c
#include "esp_async_memcpy.h"
async_memcpy_handle_t memcpy_hdl = NULL;
async_memcpy_config_t config = {
.backlog = 1,
.weight = 0,
.dma_burst_size = 16,
};
// 显式选择 AHB GDMA 后端。
ESP_ERROR_CHECK(esp_async_memcpy_install_gdma_ahb(&config, &memcpy_hdl));
// src 和 dst 是 DMA 可访问的 buffer。函数返回时dst 已经准备就绪。
ESP_ERROR_CHECK(esp_memcpy_blocking(memcpy_hdl, dst, src, copy_size, -1));
// 现在可以安全使用 dst。
process_data(dst, copy_size);
ESP_ERROR_CHECK(esp_async_memcpy_uninstall(memcpy_hdl));
``timeout_ms`` 必须为 ``-1``,表示一直等待到复制完成。阻塞 API 必须在任务上下文中调用,不能在 ISR 中调用。
安装驱动
--------
{IDF_TARGET_NAME} 有一个 DMA 引擎,能够以异步方式帮助 CPU 完成内部内存复制操作。
异步 memcpy API 中封装了所有 DMA 配置和操作,:cpp:func:`esp_async_memcpy` 的签名与标准 C 库的 ``memcpy`` 函数基本相同。
DMA 允许多个内存复制请求在首个请求完成之前排队,即允许计算和内存复制的重叠。此外,通过注册事件回调函数,还可以知道内存复制请求完成的准确时间。
配置并安装驱动
----------------------------
安装异步 memcpy 驱动的方法取决于底层 DMA 引擎:
安装驱动时请显式选择 DMA 后端。前面示例使用的 AHB GDMA 后端仅适用于支持 AHB GDMA 的目标芯片。请选择目标芯片支持且符合应用需求的 DMA 引擎:
.. list::
:SOC_CP_DMA_SUPPORTED: - :cpp:func:`esp_async_memcpy_install_cpdma` 用于安装基于 CP DMA 引擎的异步 memcpy 驱动。
:SOC_AHB_GDMA_SUPPORTED: - :cpp:func:`esp_async_memcpy_install_gdma_ahb` 用于安装基于 AHB GDMA 引擎的异步 memcpy 驱动。
:SOC_AXI_GDMA_SUPPORTED: - :cpp:func:`esp_async_memcpy_install_gdma_axi` 用于安装基于 AXI GDMA 引擎的异步 memcpy 驱动。
- :cpp:func:`esp_async_memcpy_install` 是一个通用 API用于安装带有默认 DMA 引擎的异步 memcpy 驱动。如果 SoC 具有 CP DMA 引擎,则默认 DMA 引擎为 CP DMA否则默认 DMA 引擎为 AHB GDMA。
:SOC_CP_DMA_SUPPORTED: - :cpp:func:`esp_async_memcpy_install_cpdma`
:SOC_AHB_GDMA_SUPPORTED: - :cpp:func:`esp_async_memcpy_install_gdma_ahb`
:SOC_AXI_GDMA_SUPPORTED: - :cpp:func:`esp_async_memcpy_install_gdma_axi`
:SOC_LP_AHB_GDMA_SUPPORTED: - :cpp:func:`esp_async_memcpy_install_gdma_lp_ahb`
:SOC_DW_GDMA_SUPPORTED: - :cpp:func:`esp_async_memcpy_install_dw_gdma`
:cpp:type:`async_memcpy_config_t` 中设置驱动配置:
对于一次阻塞复制,将 :cpp:member:`async_memcpy_config_t::backlog` 设为 1 即可;若可能同时等待多个复制请求,应增大该值。:cpp:member:`async_memcpy_config_t::dma_burst_size` 设置 DMA 突发大小,单位为字节;可从 16 开始,仅在性能测试后再调整。除非目标芯片支持加权仲裁且应用需要调节平均总线带宽,否则将 :cpp:member:`async_memcpy_config_t::weight` 设为 0。
* :cpp:member:`backlog`:此项用于配置首个请求完成前可以排队的最大内存复制事务数量。如果将此字段设置为零,会应用默认值 4。
* :cpp:member:`dma_burst_size`:设置单次 DMA 传输中突发数据量的大小。
* :cpp:member:`flags`:此项可以启用一些特殊的驱动功能。
场景 2在 DMA 复制期间继续工作
=================================
如果任务在 DMA 搬运期间还有其他工作,使用 :cpp:func:`esp_async_memcpy`。该函数将请求加入队列后立即返回,不会等待复制完成。随后由回调通知拥有目标 buffer 的任务。
.. code-block:: c
async_memcpy_config_t config = ASYNC_MEMCPY_DEFAULT_CONFIG();
// 更新底层 DMA 引擎支持的最大数据流
config.backlog = 8;
async_memcpy_handle_t driver = NULL;
ESP_ERROR_CHECK(esp_async_memcpy_install(&config, &driver)); // 使用默认 DMA 引擎安装驱动
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_async_memcpy.h"
发送内存复制请求
------------------------
使用 :cpp:func:`esp_async_memcpy` API 将内存复制请求发送到 DMA 引擎。在驱动程序成功安装后才能调用该 API。此 API 是线程安全的,因此可以从不同的任务中调用。
与 libc 版本的 ``memcpy`` 不同,你可以选择给 :cpp:func:`esp_async_memcpy` 设置一个回调函数,以便在内存复制完成时收到通知。注意,回调是在 ISR 上下文中执行的,请不要在回调中调用任何阻塞函数。
回调函数的原型是 :cpp:type:`async_memcpy_isr_cb_t`。回调函数只有在借助 RTOS API:cpp:func:`xSemaphoreGiveFromISR`)唤醒了高优先级任务后才能返回 true。
.. code-block:: c
// 回调实现,在 ISR 上下文中运行
static bool my_async_memcpy_cb(async_memcpy_handle_t mcp_hdl, async_memcpy_event_t *event, void *cb_args)
static bool copy_done_cb(async_memcpy_handle_t memcpy_hdl,
async_memcpy_event_t *event,
void *user_ctx)
{
SemaphoreHandle_t sem = (SemaphoreHandle_t)cb_args;
BaseType_t high_task_wakeup = pdFALSE;
xSemaphoreGiveFromISR(semphr, &high_task_wakeup); // 如果解锁了一些高优先级任务,则将 high_task_wakeup 设置为 pdTRUE
return high_task_wakeup == pdTRUE;
BaseType_t high_task_woken = pdFALSE;
SemaphoreHandle_t done = (SemaphoreHandle_t)user_ctx;
xSemaphoreGiveFromISR(done, &high_task_woken);
return high_task_woken == pdTRUE;
}
// 创建一个信号量,在异步 memcpy 完成时进行报告
SemaphoreHandle_t semphr = xSemaphoreCreateBinary();
SemaphoreHandle_t done = xSemaphoreCreateBinary();
// 从用户的上下文中调用
ESP_ERROR_CHECK(esp_async_memcpy(driver_handle, to, from, copy_len, my_async_memcpy_cb, my_semaphore));
// 其他事项
xSemaphoreTake(my_semaphore, portMAX_DELAY); // 等待 buffer 复制完成
ESP_ERROR_CHECK(esp_async_memcpy(memcpy_hdl, dst, src, copy_size,
copy_done_cb, done));
对于只需等待单次复制完成的简单场景,可以使用 :cpp:func:`esp_memcpy_blocking`。该 API 基于异步请求路径实现,并在内部等待完成回调。
// DMA 正在复制。在此执行不会访问 src 或 dst 的其他工作。
prepare_next_operation();
.. code-block:: c
xSemaphoreTake(done, portMAX_DELAY);
// 回调已运行dst 已准备就绪。
process_data(dst, copy_size);
ESP_ERROR_CHECK(esp_memcpy_blocking(driver_handle, to, from, copy_len, -1));
驱动是线程安全的,多个任务可以通过同一个句柄提交请求。请求会按提交顺序处理。请将 ``backlog`` 设为应用可能同时待处理的最大复制请求数。
阻塞 API 不能在 ISR 上下文中调用。目前仅支持 ``timeout_ms = -1``,表示无限期等待直到内存复制完成。
.. warning::
回调运行在 ISR 上下文。应保持简短,并且只能调用 ISR-safe 函数,例如 ``xSemaphoreGiveFromISR````xQueueSendFromISR``。不要在回调中调用阻塞 API、执行耗时处理或提交新的复制请求。
卸载驱动
----------------
Buffer 大小与对齐
==================
使用 :cpp:func:`esp_async_memcpy_uninstall` 卸载异步 memcpy 驱动。无需在每次 memcpy 操作后手动卸载。如果你的应用程序不再需要此驱动,此 API 可以帮助回收内存和其他硬件资源
该驱动支持未对齐的源和目标地址。它使用 CPU 复制未对齐的边缘字节,并使用 DMA 复制按缓存行对齐的主体,因此普通应用无需手动对齐 buffer
对于 :cpp:func:`esp_async_memcpy`,若目标 buffer 位于缓存区,其长度至少应为两个缓存行。较小的请求会返回 :c:macro:`ESP_ERR_INVALID_SIZE`,应改用标准 ``memcpy``。:cpp:func:`esp_memcpy_blocking` 在这种情况下会自动使用 CPU 复制。
.. note::
DMA 并不会自动让每次复制更快。短复制的 DMA 配置开销通常高于收益。请使用接近实际场景的 buffer 大小进行测量,再决定是否将性能关键路径迁移到 DMA。
结束与释放驱动
==============
在需要时持续保持驱动已安装。调用 :cpp:func:`esp_async_memcpy_uninstall` 前,请等待所有排队的复制完成,并确保没有任务会再提交请求。成功卸载后,句柄及其资源不再有效。
ETM 事件
========
.. only:: SOC_ETM_SUPPORTED and SOC_GDMA_SUPPORT_ETM
ETM 事件
---------
在异步 memcpy 操作完成时会生成一个事件,此事件能够与 :doc:`ETM </api-reference/peripherals/etm>` 模块进行交互。可以调用 :cpp:func:`esp_async_memcpy_new_etm_event` 获取 ETM 事件句柄。
如需了解如何将此事件连接到 ETM 通道,请参考文档 :doc:`ETM </api-reference/peripherals/etm>`
复制完成时可以产生供 :doc:`ETM </api-reference/peripherals/etm>` 模块使用的事件。调用 :cpp:func:`esp_async_memcpy_new_etm_event` 并传入 :cpp:enumerator:`ASYNC_MEMCPY_ETM_EVENT_COPY_DONE`,即可获取事件句柄。如何将事件连接到 ETM 任务,请参阅 :doc:`ETM 文档 </api-reference/peripherals/etm>`
API 参考
-------------
========
异步内存复制驱动程序函数
------------------------
.. include-build-file:: inc/esp_async_memcpy.inc