Skip to content

概述

DMA(Direct Memory Access,直接存储器访问)允许外设与内存之间、内存与内存之间绕过 CPU 直接搬运数据,搬完后再通过中断通知 CPU,大幅提升大数据量传输(如串口、SPI、ADC 采样)的效率。本教程通过 DMA 完成一次内存到内存搬运,在串口验证搬运结果。

用大白话讲:DMA 就像一个专职「搬运工」。普通拷贝是老板(CPU)自己一趟趟搬箱子,搬数据时什么都干不了;DMA 则让搬运工去搬,搬完喊一声「搬完了!」(中断),老板再过来验收。本教程就是雇搬运工把一箱 64 字节数据从 A 仓库搬到 B 仓库,再核对两边的货是否一致。

本教程基于安信可官方 SDKAi-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 链表)。

🎯本页目标通过 DMA 将 64 字节数据从源缓冲区搬运到目的缓冲区,串口打印搬运结果,掌握通道申请、LLI 链表配置与启动流程。
🧰前置条件① Ai-WB2 开发板一块(Type-C 数据线)② 已按 [SDK 安装](../sdk/sdk_intro) 完成环境搭建,并完成 [GPIO输出(点亮LED)](./gpio_led)。
🔗相关章节DMA 常用于串口/SPI 大数据传输,见 [UART(串口)](./uart) 与 [SPI 协议](./spi)。

进入示例工程

官方 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_init

启用 DMA 控制器,在申请通道前调用一次。

返回值:成功返回 0;失败返回负值错误码

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_chan_stop(chan)

中止当前正在执行的搬运任务。

参数

  • chan:通道号

返回值:成功返回 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;失败返回负值错误码

hosal_dma_chan_release(chan)

归还通道给空闲池,供其他模块复用(释放后不可再使用该通道)。

参数

  • chan:通道号

返回值:成功返回 0;失败返回负值错误码

DMA_LLI_Init(ch, lliCfg)

配置链表搬运:方向、位宽、源/目的地址等(LLI 可多段串联连续搬运,本教程 dir = DMA_TRNS_M2M 内存到内存)。

参数

  • ch:DMA 通道号(DMA_CH0~DMA_CH7
  • lliCfgDMA_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 完整代码
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

Released under the MIT License. Build Time 2026-09-11 14:52:23