dma开发指南
本文档介绍如何在 HiDiTing 开发板上使用 DMA(直接内存访问)功能,帮助你理解 DMA 的工作原理并进行开发实践。
DMA 驱动背景知识
DMA 工作原理
DMA(Direct Memory Access,直接存储器访问)是一种完全由硬件执行 I/O 交换的工作方式。在这种方式中,DMAC(Direct Memory Access Controller)直接在存储器和外设、外设和外设、存储器和存储器之间进行数据传输,减少处理器的干涉和开销。DMA方式一般用于高速传输成组的数据。DMAC在收到DMA传输请求后根据CPU对通道的配置启动总线主控制器,向存储器和外设发出地址和控制信号,对传输数据的个数计数,并且以中断方式向CPU报告传输操作的结束或错误。

核心优势:
| 优势 | 说明 |
|---|---|
| 减少 CPU 占用 | 数据搬移由 DMA 控制器完成,CPU 可并行处理其他任务 |
| 提高传输效率 | DMA 控制器可连续高速传输大数据块 |
| 支持多种传输 | 内存到外设、外设到内存、内存到内存 |
DMA 系统架构
HiDiTing 芯片 DMA 系统包含以下组件:
| 组件 | 说明 |
|---|---|
| DMAC | DMA 控制器,负责管理所有 DMA 通道和传输请求 |
| 通道 | 多个独立的 DMA 通道,可同时工作 |
| 中断 | 传输完成、错误等事件的中断通知 |
| FIFO | 数据缓冲 FIFO,用于突发传输 |
DMA 传输类型
| 传输类型 | 说明 | 适用场景 |
|---|---|---|
| 内存到内存 | 纯内存数据拷贝 | 数据缓冲区复制、内存搬移 |
| 内存到外设 | CPU 内存 → 外设寄存器 | UART 发送、SPI 发送等 |
| 外设到内存 | 外设寄存器 → CPU 内存 | UART 接收、SPI 接收等 |
| 链表传输 | 多段不连续内存一次性传输 | 分散的数据包、协议数据 |
DMA vs CPU 轮询/中断对比
| 对比项 | DMA | 中断 | 轮询 |
|---|---|---|---|
| CPU 占用 | 低 | 中 | 高 |
| 实时性 | 高 | 高 | 最高 |
| 适合数据量 | 大 | 中 | 小 |
| 复杂度 | 中 | 低 | 低 |
快速跑通 DMA Demo
功能说明
DMA 内存搬运示例 使用 DMA 完成 16 个 32 位数据的内存到内存搬运,并在完成回调后比对源、目标缓冲区。该测试不依赖外接硬件,也不会修改 Flash。
Demo 代码归档路径
samples/native_samples/dma/
├── CMakeLists.txt # 独立组件构建配置
├── README.md # 项目级运行说明
├── dma_demo.c # DMA 搬运、回调、数据校验和 AT 指令
└── dma_demo.h # 对外运行接口和 AT 注册函数声明
编译与烧录
完成一站式 CLI 环境配置后执行:
构建成功后,使用一站式 CLI 烧写固件并打开 UART2 串口监视器。以下为 Windows USB DFU 示例;将 COM3 替换为实际日志串口,其他平台和串口烧写参数参见一站式 CLI 开发环境使用指南。
fbb flash -f "$env:FBB_SDK_DIR\output\3322\fwpkg\diting-community.fwpkg" --chip 3322 -d --timeout 180
fbb monitor --port COM3 --baud 750000
烧录 <FBB_SDK_DIR>/output/3322/fwpkg/diting-community.fwpkg,重新上电后以 750000 波特率打开 UART2 串口。
使用方式
执行独立 DMA Demo:
需要一次性检查多类板载驱动时,也可以执行聚合指令 AT+DRIVERSELFTEST。
预期结果
出现 callback timeout 表示完成回调未触发;出现 data mismatch 表示回调已触发但数据不一致,应优先检查缓存处理和缓冲区对齐。
文件结构与代码走读
文件职责
| 文件 | 职责 | 关键内容 |
|---|---|---|
dma_demo.c |
完成数据准备、DMA 启动、回调等待、缓存失效和数据比对 | diting_dma_demo_run()、dma_demo_callback() |
dma_demo.h |
向聚合自测和 AT 适配层公开接口 | diting_dma_demo_run()、at_diting_dma_example_cmd_register() |
CMakeLists.txt |
定义独立组件和 AT 注册宏 | diting_dma_sample、AT_DITING_EXAMPLE_DMA_TEST |
README.md |
说明单项构建、运行和故障排查 | AT+DMATEST |
构建入口
上层 samples/native_samples/CMakeLists.txt 通过 CONFIG_ENABLE_DMA_SAMPLE 加载本目录,组件通过 AT_DITING_EXAMPLE_DMA_TEST 使 AT 适配层注册 DMA 指令。目标配置中的组件名为 diting_dma_sample。
核心流程
diting_dma_demo_run() 先准备源数据并清空目标缓冲区,再启动单次内存搬运:
dma_ch_user_memory_config_t config = {
.src = (uint32_t)(uintptr_t)g_dma_source,
.dest = (uint32_t)(uintptr_t)g_dma_destination,
.transfer_num = DMA_DEMO_WORDS,
.priority = 0,
.width = 2,
};
errcode_t ret = uapi_dma_transfer_memory_single(&config, dma_demo_callback, 0);
回调记录中断类型并通知等待逻辑:
static void dma_demo_callback(uint8_t intr, uint8_t channel, uintptr_t arg)
{
(void)channel;
(void)arg;
g_dma_interrupt = intr;
g_dma_ok = (intr == 0);
g_dma_done = true;
}
DMA 直接写内存,CPU 比较前需要失效目标缓冲区缓存:
osal_dcache_region_inv(g_dma_destination, sizeof(g_dma_destination));
if (memcmp(g_dma_source, g_dma_destination, sizeof(g_dma_source)) != 0) {
return false;
}
源、目标缓冲区均按 64 字节对齐,避免失效目标缓存行时影响相邻可写对象。AT 命令处理函数调用公开运行接口,根据布尔结果返回 AT_RET_OK 或 AT_RET_RUN_ERROR。
基于 DMA Demo 开发自己的应用
- 复制 samples/native_samples/dma 到新的示例目录,修改组件名和构建开关。
- 保留
diting_dma_demo_run()这类可直接调用的运行函数,从应用任务、系统服务或app_run启动入口调用。 - 在运行函数中替换源地址、目标地址、传输宽度和传输数量;外设 DMA 还需要配置外设请求通道。
- 在 samples/native_samples/CMakeLists.txt 和目标
config.py中加入新组件,直接调用运行函数完成单项及重复执行验证。 - 检查传输结果、错误返回和资源释放;聚合驱动回归不能代替业务场景的边界测试。
如需通过串口反复触发板端回归,可选配 AT 注册宏、命令名和命令 ID,并在 AT 适配层接入。AT+DRIVERSELFTEST 仅用于现有基础驱动的聚合回归。
DMA 开发指导
操作流程
DMA 接口使用遵循如下操作步骤:
- DMA 初始化:调用
uapi_dma_init()初始化 DMA 模块 - 打开 DMA:调用
uapi_dma_open()开启 DMA 模块 - 配置传输:填充
dma_ch_user_memory_config_t结构体 - 启动传输:调用
uapi_dma_transfer_memory_single()或类似接口 - 等待完成:在回调函数中处理传输完成事件
API 接口列表
| 接口函数 | 说明 |
|---|---|
| uapi_dma_init | 初始化 DMA 模块 |
| uapi_dma_deinit | 去初始化 DMA |
| uapi_dma_open | 打开 DMA 模块 |
| uapi_dma_close | 关闭 DMA 模块 |
| uapi_dma_transfer_memory_single | 单次内存到内存传输 |
| uapi_dma_configure_peripheral_transfer_single | 单次外设到内存传输 |
完整 API 列表
更多 DMA 接口请参考:DMA API 参考
说明: 如果需要在SPI/UART/I2C中使用DMA传输数据,需要在系统启动时进行DMA初始化。
代码示例
内存到内存传输示例
说明:DMA接口仅对外提供存储器到存储器的拷贝功能(其他拷贝方式可参考外设驱动),操作步骤如下:.
- 调用uapi_dma_init接口,初始化DMA模块。
- 调用uapi_dma_open_ch接口,打开DMA通道。
- 调用uapi_dma_transfer接口,DMA开始传输,通过参数block可以设置是否为阻塞模式。
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "dma.h"
#include "errcode.h"
#include "osal/memory/osal_cache.h"
#include "securec.h"
#define TEST_DMA_TRF_WORD_NUM 16
/* 缓冲区独占缓存行,失效目标区缓存时不会影响相邻数据。 */
static uint32_t g_dma_src_data[TEST_DMA_TRF_WORD_NUM] __attribute__((aligned(64)));
static uint32_t g_dma_desc_data[TEST_DMA_TRF_WORD_NUM] __attribute__((aligned(64)));
static volatile bool g_dma_trans_done = false;
static volatile bool g_dma_trans_succ = false;
/* 传输完成后回调函数处理 */
static void test_dma_trans_done_callback(uint8_t int_type, uint8_t channel, uintptr_t arg)
{
(void)channel;
(void)arg;
switch (int_type) {
case HAL_DMA_INTERRUPT_TFR:
g_dma_trans_done = true;
g_dma_trans_succ = true;
break;
case HAL_DMA_INTERRUPT_BLOCK:
g_dma_trans_done = true;
g_dma_trans_succ = true;
break;
case HAL_DMA_INTERRUPT_ERR:
g_dma_trans_done = true;
g_dma_trans_succ = false;
break;
default:
break;
}
printf("[DMA] int_type is %d. \r\n", int_type);
}
/* 填充测试数据 */
static void test_fill_test_buffer(void *data, unsigned int length)
{
for (unsigned int i = 0; i < length; i++) {
*((unsigned char *)data + i) = (unsigned char)i;
}
}
/* 清空目标缓冲区 */
static void test_clear_test_buffer(void *data, unsigned int length)
{
memset_s(data, length, 0, length);
}
/* DMA 内存到内存单次传输 */
errcode_t test_dma_mem_to_mem_single(void)
{
dma_ch_user_memory_config_t transfer_config;
/* 填充源地址要发送的数据 */
test_fill_test_buffer((void *)(uintptr_t)g_dma_src_data, sizeof(g_dma_src_data));
/* 清空目的地址的数据 */
test_clear_test_buffer((void *)(uintptr_t)g_dma_desc_data, sizeof(g_dma_desc_data));
/* 初始化 DMA */
uapi_dma_init();
/* 开启 DMA 模块 */
uapi_dma_open();
/* 配置传输参数 */
transfer_config.src = ((uint32_t)(uintptr_t)g_dma_src_data); /* 源地址 */
transfer_config.dest = ((uint32_t)(uintptr_t)g_dma_desc_data); /* 目的地址 */
transfer_config.transfer_num = TEST_DMA_TRF_WORD_NUM; /* 32位传输单元数 */
transfer_config.priority = 0; /* 优先级 0-3 */
transfer_config.width = 2; /* 传输宽度 2:4字节 */
/* 调用接口按块发送函数,并注册回调函数 */
if (uapi_dma_transfer_memory_single(&transfer_config, test_dma_trans_done_callback, 0) != ERRCODE_SUCC) {
return ERRCODE_FAIL;
}
/* 等待发送完成 */
while (!g_dma_trans_done) {
osal_msleep(10);
}
/* DMA 直接写内存,CPU 比较前必须丢弃目标缓冲区中的旧缓存。 */
osal_dcache_region_inv(g_dma_desc_data, sizeof(g_dma_desc_data));
if (!g_dma_trans_succ ||
memcmp(g_dma_src_data, g_dma_desc_data, sizeof(g_dma_src_data)) != 0) {
return ERRCODE_FAIL;
}
return ERRCODE_SUCC;
}
外设到内存传输示例(以 UART 为例)
/* UART DMA 接收配置 */
errcode_t demo_uart_dma_recv(void)
{
uart_read_dma_config_t dma_cfg = {
.src_width = HAL_DMA_TRANSFER_WIDTH_8, /* 源宽度 8bit */
.dest_width = HAL_DMA_TRANSFER_WIDTH_8, /* 目的宽度 8bit */
.burst_length = HAL_DMA_BURST_TRANSACTION_LENGTH_4, /* 突发长度 4 */
.priority = 0 /* 优先级 */
};
uint8_t recv_buff[256] = {0};
/* 配置 UART DMA 接收 */
if (uapi_uart_read_by_dma(UART_BUS_0, recv_buff, sizeof(recv_buff), &dma_cfg) != sizeof(recv_buff)) {
printf("[UART] DMA recv failed!\r\n");
return ERRCODE_FAIL;
}
printf("[UART] DMA recv succ, data: %s\r\n", recv_buff);
return ERRCODE_SUCC;
}
注意事项
- 内存对齐:DMA 传输对内存地址可能有对齐要求。需要动态申请缓冲区时,包含
osal_addr.h并使用osal_kmalloc(size, OSAL_GFP_KERNEL);使用完毕后调用osal_kfree()释放 - 传输完成回调:建议在回调函数中仅做标记,实际处理放在主任务中
- 阻塞 vs 非阻塞:非阻塞传输需要在回调函数中处理完成事件
- 地址映射限制:通用外设(I2C、UART 等)使用 DMA 搬运时,如果数据源涉及动态内存,请使用
osal_kmalloc()申请并检查返回值 - 缓存一致性:启用数据缓存的目标中,DMA 完成后、CPU 读取目标缓冲区前调用
osal_dcache_region_inv();需要失效的缓冲区应按缓存行对齐并避免与其他可写对象共用缓存行 - 传输宽度:传输宽度应与外设数据宽度匹配,避免数据错位
- 优先级:多个 DMA 通道可以设置不同优先级,高优先级通道会优先获得总线访问权
- 使用场景:建议仅在需要非阻塞进行数据拷贝的场景下使用DMA,这样可让出CPU,传输完成之后CPU会上报中断,可以在回调函数中根据事件类型判断传输成功与失败。传输阻塞场景下,仍建议使用memcpy_s进行数据拷贝。
常见错误
| 错误现象 | 原因 | 解决方法 |
|---|---|---|
| DMA 传输不触发 | 未初始化 DMA 或未打开 DMA 模块 | 确保调用 uapi_dma_init() 和 uapi_dma_open() |
| 回调成功但目标数据仍为旧值 | CPU 命中了 DMA 写入前的目标缓存 | 将目标缓冲区按缓存行对齐,并在比较前调用 osal_dcache_region_inv() |
| 传输数据错误 | 源/目的地址配置错误或内存未对齐 | 检查地址参数和内存对齐 |
| 回调函数不执行 | 中断未注册或 DMA 通道配置错误 | 检查中断配置和通道号 |
| 传输超时 | 外设响应慢或 DMA 通道被占用 | 检查外设连接和通道占用情况 |
| 内存泄漏 | 使用 osal_kmalloc() 后未调用 osal_kfree() |
确保每次内存申请都有对应的释放操作 |