概述
DMA(Direct Memory Access,直接存储器访问)允许外设与内存之间、内存与内存之间绕过 CPU 直接搬运数据,搬完后再通过中断通知 CPU,大幅提升大数据量传输(如串口、SPI、ADC 采样)的效率。本教程通过 DMA 完成一次内存到内存搬运,在串口验证搬运结果。
用大白话讲:DMA 就像一个专职「搬运工」。普通拷贝是老板(CPU)自己一趟趟搬箱子,搬数据时什么都干不了;DMA 则让搬运工去搬,搬完喊一声「搬完了!」(中断),老板再过来验收。本教程就是雇搬运工把一箱 64 字节数据从 A 仓库搬到 B 仓库,再核对两边的货是否一致。
本教程基于安信可官方 SDK(Ai-Thinker-Open/Ai-Thinker-WB2,版本
release_bl_iot_sdk_1.6.40)的 DMA API 编写。官方 SDK 中 DMA 能力由components/platform/hosal/include/hosal_dma.h与底层bl602_dma.h提供,官方 DMA 实际用法可参考applications/peripherals/spi_ws2812(SPI 发送使用 DMA + LLI 链表)。
官方 SDK 未提供独立的 DMA 搬运示例,本教程在官方 blink 示例工程骨架(main/main.c + Makefile)上改写,DMA 调用序列与官方 spi_ws2812 示例一致:
cd ~/Ai-Thinker-WB2/applications/get-started/blink
说明:
cd是「进入目录」命令,这里借用官方 blink 示例工程骨架作为本教程的工程;后续的make编译、make flash烧录命令都必须先在这个目录里执行。
将 main/main.c 替换为以下内容,本步完整代码已移至文末,见:
📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开。本教程改写代码,基于官方 blink 工程骨架(
applications/get-started/blink)编写,DMA 调用序列与官方示例(applications/peripherals/spi_ws2812/app/main.c)完全一致。
代码要点:
| 代码 | 作用 |
|---|---|
hosal_dma_chan_request(0) |
申请一个 DMA 通道(给搬运工编个号),申请不到说明通道被别的模块占用了 |
dma_lli_list_init(src, dst, length) |
配置 LLI 链表(搬运清单):源地址、目的地址、搬运长度与地址自增方式,清单错数据就搬错地方 |
DMA_LLI_Init(dma_chan, &llicfg) |
初始化 LLI 模式,dir = DMA_TRNS_M2M 指定内存到内存搬运,方向错就搬反了 |
DMA_LLI_Update(dma_chan, (uint32_t)dma_lli) |
把搬运清单装进 DMA 通道,不装清单通道不知道要干什么 |
hosal_dma_irq_callback_set(...) |
注册搬运完成中断回调(搬完喊一声「搬完了」),不注册就没法知道何时搬完 |
hosal_dma_chan_start(dma_chan) |
启动搬运,CPU 随即空闲,完成后进入中断回调;不启动永远不会搬 |
memcmp(src_buf, dst_buf, ...) |
逐字节比对源/目的缓冲区,校验搬运结果一致性,不一致说明搬运出错了 |
💡 内存搬运时源/目的地址都设
DMA_MINC_ENABLE(自增);搬往外设(如 SPI FIFO)时目的地址设DMA_MINC_DISABLE(固定地址),见官方 spi_ws2812 示例。
在工程目录执行编译:
make -j8
说明:
make是「编译工程」命令,把代码变成开发板能运行的固件(程序文件);-j8表示用 8 个核并行编译,速度更快。
编译成功后生成固件 build_out/blink.bin。
开发板保持 USB 连接,确认串口设备号后执行烧录:
make flash p=/dev/ttyUSB0 b=921600
说明:
make flash是「烧录」命令,把编译好的固件写进开发板芯片。p=后面是串口设备号(改成你电脑上实际的串口,可用ls /dev/ttyUSB*查看),b=是烧录波特率(传输速度)。
⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式,等待进度条完成即烧录成功。
烧录完成后开发板自动重启运行,打开串口助手(波特率 921600)查看日志:
DMA memcpy OK, length = 64
打印 DMA memcpy OK 说明 64 字节数据已由 DMA 从源缓冲区搬运到目的缓冲区,且内容一致。
看到串口打印 DMA memcpy OK, length = 64 即为成功;如果打印 DMA memcpy FAIL 或没有日志,说明还没成功,对照文末「常见问题与踩坑提示」排查。
💡 尝试把
TransferSize加大(如 4096),并注释掉memcmp校验、改为在搬运前打印时间戳,可直观感受 DMA 搬运大块数据几乎不占用 CPU 时间。
代码执行流程
例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):
本文 API 汇总
hosal_dma_chan_request(flag)
从空闲通道池中申请一个 DMA 通道供本任务独占(本教程申请普通通道 0)。
参数:
flag:申请标志,可选值:HOSAL_DMA_TYPE_NORMAL(普通通道)/HOSAL_DMA_TYPE_LLI(LLI 链表通道,需与DMA_LLI_Init配合)
返回值:成功返回通道号(0~7);失败返回负值错误码
hosal_dma_chan_start(chan)
开始按已配置的源/目的地址执行数据搬运。
参数:
chan:通道号(来自hosal_dma_chan_request的返回值)
返回值:成功返回 0;失败返回负值错误码
hosal_dma_irq_callback_set(chan, pfn, p_arg)
搬运完成或出错时在中断中调用回调函数(本教程回调中将 dma_txing 置 0 表示搬运结束)。
参数:
chan:通道号pfn:回调函数指针,形如void cb(void *arg, uint32_t flag),必填p_arg:回调参数指针,无参传NULL
返回值:成功返回 0;失败返回负值错误码
DMA_LLI_Init(ch, lliCfg)
配置链表搬运:方向、位宽、源/目的地址等(LLI 可多段串联连续搬运,本教程 dir = DMA_TRNS_M2M 内存到内存)。
参数:
ch:DMA 通道号(DMA_CH0~DMA_CH7)lliCfg:DMA_LLI_Cfg_Type结构体指针。dir(搬运方向,可选值:DMA_TRNS_M2M内存到内存 /DMA_TRNS_M2P内存到外设 /DMA_TRNS_P2M外设到内存)、width(位宽,可选值:DMA_TRNS_WIDTH_8BITS/16BITS/32BITS)
返回值:无
DMA_LLI_Update(ch, LLI)
把编排好的链表首地址写入通道,之后启动通道即按链表执行。
参数:
ch:DMA 通道号LLI:链表结构体指针(DMA_LLI_Ctrl_Type数组,多段时next指向下一段)
返回值:无
完整代码
以下为改写后的 main/main.c 完整源码。官方 SDK 未提供独立的 DMA 搬运示例,本代码基于官方 blink 工程骨架(applications/get-started/blink)编写,DMA 调用序列与官方示例(applications/peripherals/spi_ws2812/app/main.c)完全一致:
📜 点击展开 main/main.c 完整代码
#include <stdio.h>
#include <string.h>
#include <FreeRTOS.h>
#include <task.h>
#include <blog.h>
#include <hosal_dma.h>
#include <bl602_dma.h>
#define DMA_LLI_CNT 1
static hosal_dma_chan_t dma_chan;
static DMA_LLI_Ctrl_Type dma_lli[DMA_LLI_CNT];
static volatile int dma_txing;
static uint8_t src_buf[64];
static uint8_t dst_buf[64];
/* DMA 搬运完成中断回调 */
static void dma_int_handler(void *arg, uint32_t flag)
{
dma_txing = 0;
}
/* 配置 LLI 链表:源地址、目的地址、搬运长度 */
static void dma_lli_list_init(uint8_t *src, uint8_t *dst, uint32_t length)
{
struct DMA_Control_Reg dmactrl;
dmactrl.SBSize = DMA_BURST_SIZE_1;
dmactrl.DBSize = DMA_BURST_SIZE_1;
dmactrl.SWidth = DMA_TRNS_WIDTH_8BITS;
dmactrl.DWidth = DMA_TRNS_WIDTH_8BITS;
dmactrl.Prot = 0;
dmactrl.SLargerD = 0;
dmactrl.TransferSize = length;
dmactrl.I = 0;
dmactrl.SI = DMA_MINC_ENABLE; /* 源地址自增 */
dmactrl.DI = DMA_MINC_ENABLE; /* 目的地址自增 */
dma_lli[0].srcDmaAddr = (uint32_t)(src);
dma_lli[0].destDmaAddr = (uint32_t)(dst);
dma_lli[0].dmaCtrl = dmactrl;
dma_lli[0].nextLLI = 0;
}
/* 内存到内存搬运 */
static void dma_mem_to_mem(uint8_t *src, uint8_t *dst, uint32_t length)
{
DMA_LLI_Cfg_Type llicfg;
llicfg.dir = DMA_TRNS_M2M; /* 内存到内存 */
llicfg.srcPeriph = DMA_REQ_NONE;
llicfg.dstPeriph = DMA_REQ_NONE;
dma_lli_list_init(src, dst, length);
DMA_LLI_Init(dma_chan, &llicfg);
DMA_LLI_Update(dma_chan, (uint32_t)dma_lli);
hosal_dma_irq_callback_set(dma_chan, dma_int_handler, NULL);
dma_txing = 1;
hosal_dma_chan_start(dma_chan);
}
void main(void)
{
int i;
/* 源缓冲区填充 0~63,目的缓冲区清零 */
for (i = 0; i < sizeof(src_buf); i++) {
src_buf[i] = i;
}
memset(dst_buf, 0, sizeof(dst_buf));
/* 申请 DMA 通道 */
dma_chan = hosal_dma_chan_request(0);
/* 启动内存搬运 */
dma_mem_to_mem(src_buf, dst_buf, sizeof(src_buf));
/* 等待搬运完成 */
while (dma_txing) {
;
}
/* 逐字节比较搬运结果 */
if (memcmp(src_buf, dst_buf, sizeof(src_buf)) == 0) {
blog_info("DMA memcpy OK, length = %d", (int)sizeof(src_buf));
} else {
blog_info("DMA memcpy FAIL");
}
for (;;) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}常见问题与踩坑提示
⚠️ 搬运结果不一致(部分数据丢失)
原因:位宽设置与实际数据不匹配(8 位数据用 32 位位宽搬运),或源/目的地址未对齐
解决:8 位数据使用 DMA_TRNS_WIDTH_8BITS;目的缓冲区长度足够且按 4 字节对齐(可加 __attribute__((aligned(4))))
⚠️ 通道申请失败
原因:DMA 通道被其他模块占用(如 SPI、UART 中断收发模式)
解决:确认工程中无其他模块已申请 DMA 通道;释放不再使用的通道 hosal_dma_chan_release
⚠️ 搬往外设时地址自增设置错误
原因:外设 FIFO 是固定地址,若设了 DMA_MINC_ENABLE 会越界写入
解决:搬往外设(如 SPI FIFO)时目的地址设 DMA_MINC_DISABLE,内存间搬运则两侧都设 DMA_MINC_ENABLE
⚠️ 烧录一直卡住等待,进度条不动
原因:未进入下载模式,或数据线只能充电不能传数据
解决:烧录时按提示长按 EN 键进入下载模式;换一根能传数据的 Type-C 数据线后重试
⚠️ 找不到串口设备或提示无权限
原因:Linux 下 /dev/ttyUSB0 不存在或权限不足,Windows 下未安装 USB 转串口驱动
解决:Linux 用 ls /dev/ttyUSB* 确认设备号,权限不足执行 sudo usermod -aG dialout $USER 后重新登录;Windows 在设备管理器安装驱动并确认 COM 口号
⚠️ 执行 make 报找不到 Makefile
原因:在错误的目录执行了编译命令(必须在示例工程目录内)
解决:先执行 cd ~/Ai-Thinker-WB2/applications/get-started/blink 进入工程目录,再执行 make -j8
运行自检
串口打印 DMA memcpy OK, length = 64,即 DMA 搬运功能验证通过。
遇到问题?
如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

