============ 异步内存复制 ============ :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["阻塞复制
esp_memcpy_blocking"] choose --> async["异步复制
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 中调用。 安装驱动 -------- 安装驱动时请显式选择 DMA 后端。前面示例使用的 AHB GDMA 后端仅适用于支持 AHB GDMA 的目标芯片。请选择目标芯片支持且符合应用需求的 DMA 引擎: .. list:: :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: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。 场景 2:在 DMA 复制期间继续工作 ================================= 如果任务在 DMA 搬运期间还有其他工作,使用 :cpp:func:`esp_async_memcpy`。该函数将请求加入队列后立即返回,不会等待复制完成。随后由回调通知拥有目标 buffer 的任务。 .. code-block:: c #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "esp_async_memcpy.h" static bool copy_done_cb(async_memcpy_handle_t memcpy_hdl, async_memcpy_event_t *event, void *user_ctx) { BaseType_t high_task_woken = pdFALSE; SemaphoreHandle_t done = (SemaphoreHandle_t)user_ctx; xSemaphoreGiveFromISR(done, &high_task_woken); return high_task_woken == pdTRUE; } SemaphoreHandle_t done = xSemaphoreCreateBinary(); ESP_ERROR_CHECK(esp_async_memcpy(memcpy_hdl, dst, src, copy_size, copy_done_cb, done)); // DMA 正在复制。在此执行不会访问 src 或 dst 的其他工作。 prepare_next_operation(); xSemaphoreTake(done, portMAX_DELAY); // 回调已运行,dst 已准备就绪。 process_data(dst, copy_size); 驱动是线程安全的,多个任务可以通过同一个句柄提交请求。请求会按提交顺序处理。请将 ``backlog`` 设为应用可能同时待处理的最大复制请求数。 .. warning:: 回调运行在 ISR 上下文。应保持简短,并且只能调用 ISR-safe 函数,例如 ``xSemaphoreGiveFromISR`` 或 ``xQueueSendFromISR``。不要在回调中调用阻塞 API、执行耗时处理或提交新的复制请求。 Buffer 大小与对齐 ================== 该驱动支持未对齐的源和目标地址。它使用 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 复制完成时可以产生供 :doc:`ETM ` 模块使用的事件。调用 :cpp:func:`esp_async_memcpy_new_etm_event` 并传入 :cpp:enumerator:`ASYNC_MEMCPY_ETM_EVENT_COPY_DONE`,即可获取事件句柄。如何将事件连接到 ETM 任务,请参阅 :doc:`ETM 文档 `。 API 参考 ======== 异步内存复制驱动程序函数 ------------------------ .. include-build-file:: inc/esp_async_memcpy.inc