DMA
DMA 驱动 | sample:
src/application/samples/peripheral/dma/dma_demo.c| 前置:UART、SPI
学习目标
- 理解 DMA(Direct Memory Access)的核心价值——数据搬运不需要 CPU 逐字节参与
- 掌握 DMA 的基本配置:源/目的地址、传输宽度、传输数量、优先级
- 能够使用
uapi_dma_transfer_memory_single()实现内存间数据拷贝,并理解 LLI 链式传输的扩展能力
基本概念
DMA 做什么
没有 DMA 时,拷贝 128 字节数据:CPU 循环 32 次(32-bit 宽度),每次读一个字、写一个字——CPU 完全被占用。有 DMA 时:CPU 只需配置好源/目的/长度/宽度,启动后即可去处理其他任务,DMA 完成后通过中断回调通知 CPU。
flowchart LR
subgraph A["无 DMA: CPU 逐字搬运"]
C1[CPU] -->|循环读写| M1[源内存]
C1 -->|循环读写| M2[目的内存]
end
subgraph B["有 DMA: 硬件自动搬运"]
C2[CPU] -->|一次配置| D[DMA 控制器]
D -->|硬件搬运| M3[源内存]
D -->|硬件搬运| M4[目的内存]
C2 -->|同时处理| T[其他任务]
end
DMA 传输要素
每次 DMA 传输需要明确以下参数,这些参数在 dma_ch_user_memory_config_t 结构体中配置:
| 要素 | 字段 | 说明 | sample 取值 |
|---|---|---|---|
| 源地址 | src |
数据来源地址(物理地址) | g_app_dma_src_data 数组首址 |
| 目的地址 | dest |
数据去向地址(物理地址) | g_app_dma_desc_data 数组首址 |
| 传输数量 | transfer_num |
总共传输的 word 数 | 32 |
| 传输宽度 | width |
每 word 的字节宽度(0=8b, 1=16b, 2=32b) | 2(32-bit) |
| 优先级 | priority |
DMA 通道优先级(0 最高) | 0 |
涉及 API
| API | 用途 | 头文件 |
|---|---|---|
uapi_dma_init() |
初始化 DMA 模块 | hal_dma.h |
uapi_dma_open() |
打开 DMA 使能 | hal_dma.h |
uapi_dma_transfer_memory_single(&cfg, cb, arg) |
启动单次内存→内存 DMA 传输 | hal_dma.h |
uapi_dma_transfer_memory_lli(ch, &cfg, cb) |
配置 LLI 链式传输(扩展) | hal_dma.h |
uapi_dma_enable_lli(ch, cb, arg) |
启动 LLI 传输 | hal_dma.h |
uapi_dma_end_transfer(ch) |
结束/释放 DMA 通道 | hal_dma.h |
uapi_dma_get_lli_channel(port, hs) |
获取 LLI 传输专用通道 | hal_dma.h |
案例说明
做什么
本案例演示 DMA 两种传输模式的内存间数据拷贝:
- 单次传输(默认):一次性配置源/目的/长度,启动后 DMA 自动搬运 32 个 word,完成后通过回调验证数据一致性
- LLI 链式传输(CONFIG_DMA_MEMORY_LLI_TRANSFER_MODE 宏开启):将传输描述符链表预先写入 DMA,硬件自动遍历执行,适合连续多块搬运
规格与功能
| 规格项 | 说明 |
|---|---|
| 传输方向 | 内存 src 数组 → 内存 dest 数组 |
| 传输宽度 | 32-bit(width=2) |
| 单次传输量 | 32 word(128 字节) |
| 优先级 | 0(最高) |
| 完成通知 | DMA 中断回调 app_dma_trans_done_callback |
| 正确性验证 | memcmp(src, dest, transfer_num) |
| 任务间隔 | 每 500ms 启动一次传输 |
案例流程
sequenceDiagram
participant T as dma_task 任务
participant D as DMA 控制器
participant C as 回调 ISR
T->>T: 填充 src 数据递增序列
T->>T: memset_s 清零 dest
T->>T: 配置 transfer_config
T->>D: uapi_dma_transfer_memory_single
Note over T: CPU 等待 g_dma_trans_done 置位
D->>D: 硬件搬运 32 word
D->>C: 传输完成中断
C->>C: g_dma_trans_done = 1
T->>T: memcmp 验证 src == dest
案例操作指导
- 确保工程已使能 DMA 驱动(Kconfig 中选中
DMA相关配置) - 默认编译单次传输模式:
- 如需体验 LLI 链式传输,在
menuconfig中开启CONFIG_DMA_MEMORY_LLI_TRANSFER_MODE后重新编译 - 烧录固件后,串口观察输出:
- 单次模式输出:
dma single memory transfer start!→dma memory copy test succ, length = 32 block - LLI 模式输出:
dma config link list item of memory to memory succ!→dma channel transfer finish!
关键配置
| 配置项 | 推荐值 | 说明 |
|---|---|---|
width 传输宽度 |
2(32-bit) | 32-bit 一次搬 4 字节,效率最高。根据外设 FIFO 宽度匹配,UART 通常用 8-bit(0) |
transfer_num |
按实际需求 | 单次传输上限由 DMA 控制器决定(WS63 通常 4095),超过需用 LLI 分块 |
priority |
0 | 0 优先级最高。多个 DMA 通道同时请求时,高优先级先执行 |
| 传输模式 | 单次 vs LLI | 简单内存拷贝用 _single();连续多块搬运(音频、大数据)用 _lli() |
Trade-off:单次传输 API 简单,但一次只能搬运一帧;LLI 链式传输需要预先构建链表,但可连续搬运多帧数据无需 CPU 干预。小数据量(< 1KB)使用单次传输即可,大数据量或流式场景(音频、显示)必须使用 LLI。
代码详解
1. 全局变量定义与回调注册
DMA 传输需要源缓冲区和目的缓冲区,外加一个完成标志供任务轮询。以下定义来自 sample 实际代码:
#define DMA_TRANSFER_WORD_NUM 32
#define DMA_TRANSFER_PRIORITY 0
#define DMA_TRANSFER_WIDTH 2
static uint32_t g_app_dma_src_data[DMA_TRANSFER_WORD_NUM] = { 0 };
static uint32_t g_app_dma_desc_data[DMA_TRANSFER_WORD_NUM] = { 0 };
static uint8_t g_dma_trans_done = 0;
DMA 完成回调在 ISR 上下文中执行,根据 int_type 区分传输完成、块完成、错误三种事件:
static void app_dma_trans_done_callback(uint8_t int_type, uint8_t channel, uintptr_t arg)
{
unused(arg);
unused(channel);
switch (int_type) {
case HAL_DMA_INTERRUPT_TFR: /* 全部传输完成 */
g_dma_trans_done = 1;
break;
case HAL_DMA_INTERRUPT_BLOCK: /* 单块传输完成(LLI) */
g_dma_trans_done = 1;
break;
case HAL_DMA_INTERRUPT_ERR: /* 传输错误 */
osal_printk("DMA transfer error.\r\n");
break;
default:
break;
}
}
HAL_DMA_INTERRUPT_TFR在单次传输完成时触发;HAL_DMA_INTERRUPT_BLOCK在 LLI 链表中单块完成时触发。两者都置位g_dma_trans_done。
2. DMA 初始化与数据准备
任务开始时先初始化 DMA 模块,然后用递增序列填充源数据,同时清零目的缓冲区:
/* DMA init. */
uapi_dma_init();
uapi_dma_open();
for (uint32_t i = 0; i < DMA_TRANSFER_WORD_NUM; i++) {
g_app_dma_src_data[i] = i; /* 填充 0, 1, 2, ... 31 */
}
memset_s(g_app_dma_desc_data, DMA_TRANSFER_WORD_NUM, 0, DMA_TRANSFER_WORD_NUM);
3. 单次传输配置与调用
以下配置定义源地址、目的地址、传输数量、优先级和宽度。注意 src 和 dest 必须是物理地址(WS63 中虚拟地址即物理地址,故直接强转):
dma_ch_user_memory_config_t transfer_config = { 0 };
transfer_config.src = (uint32_t)(uintptr_t)g_app_dma_src_data;
transfer_config.dest = (uint32_t)(uintptr_t)g_app_dma_desc_data;
transfer_config.transfer_num = DMA_TRANSFER_WORD_NUM;
transfer_config.priority = DMA_TRANSFER_PRIORITY;
transfer_config.width = DMA_TRANSFER_WIDTH;
启动单次传输——uapi_dma_transfer_memory_single 是非阻塞调用,返回后 CPU 可以去做其他事:
if (uapi_dma_transfer_memory_single(&transfer_config, app_dma_trans_done_callback,
(uintptr_t)NULL) == ERRCODE_SUCC) {
osal_printk("dma single memory transfer succ!\r\n");
}
4. 等待完成并验证
任务轮询 g_dma_trans_done 标志等待 DMA 完成,之后用 memcmp 验证源和目的数据一致:
while (!g_dma_trans_done) {} /* 等待 DMA 完成中断 */
if (memcmp((void *)transfer_config.src, (void *)transfer_config.dest,
transfer_config.transfer_num) == 0) {
osal_printk("dma memory copy test succ, length = %d block\r\n",
transfer_config.transfer_num);
}
5. LLI 链式传输(扩展模式)
当 CONFIG_DMA_MEMORY_LLI_TRANSFER_MODE 开启时,sample 走 LLI 分支。LLI 传输需要先获取专用通道,再配置链表,最后使能传输:
dma_channel_t dma_channel = uapi_dma_get_lli_channel(0, HAL_DMA_HANDSHAKING_MAX_NUM);
uapi_dma_transfer_memory_lli(dma_channel, &transfer_config,
app_dma_trans_done_callback);
uapi_dma_enable_lli(dma_channel, app_dma_trans_done_callback, (uintptr_t)NULL);
/* 等待完成 */
while (!g_dma_trans_done) {}
uapi_dma_end_transfer(dma_channel);
LLI 模式的核心价值在于提前构建多条传输描述符链表——DMA 完成一条后自动加载下一条,无需 CPU 再次介入。适用于音频流、显示帧缓冲等需要连续搬运多块数据的场景。