[APP应用]灵活使用先楫 DMA 链式操作 —— 让时序精准掌握,让硬件自主操作,让性能完美释放
适用平台:HPM6700 / HPM6300 / HPM6200 / HPM5300 / HPM6E00 / HPM6P00 / HPM5E00 / HPM6800 等全系列
DMA 版本:本文涵盖 DMA v1(HDMA/XDMA)和 DMA v2(DMAV2),着重说明两者差异及选型要点
目录
- 概述:为什么需要 DMA 链式操作
- DMA 控制器基础
- 链式描述符:构建硬件自动机
- 双 Buffer 乒乓机制详解
- 跨通道 DMA 编程 —— 一个 DMA 通道启动另一个
- DECREMENT 寻址模式的精妙之处
- Burst 小循环与握手选项
- DMA v1 与 DMA v2 关键差异
- 实战:多通道协同的完整 DMA 链
- 最佳实践与注意事项
1. 概述:为什么需要 DMA 链式操作
在嵌入式系统中,许多实时采集与控制场景需要严格的多步外设操作时序。以一个典型的多通道同步采集系统为例,一个完整的采集周期涉及:
- 定时器:产生等间隔的触发信号
- 触发多路复用器:将定时器信号路由到多个外设
- ADC 序列采集:多通道数据转换
- 通信接口:板间或芯片间数据传输
- I/O 控制信号:通道选择与状态切换
这些步骤需要毫秒级甚至微妙级的严格同步且CPU 零干预。用中断逐个处理会引入抖动和 CPU 负载,而 DMA 链式操作(Linked Descriptor Chain)可以将整个流程编排为一组硬件自动执行的"微程序",由外设事件驱动,硬件自动串行执行。
核心理念:把 DMA 链看作一段不可打断的硬件宏。每个描述符是一条指令,链就是一段程序。
2. DMA 控制器基础
2.1 DMA 版本
先楫 MCU 使用两代 DMA IP:
| 特性 | DMA v1 (HDMA/XDMA) | DMA v2 (DMAV2) |
|---|---|---|
| 典型 MCU | HPM6200/6300/6700 | HPM5300/6800/6E00/6P00/5E00 |
| 通道数 | 8 | 32 |
| 最大传输宽度 | WORD(HDMA) / DWORD(XDMA) | DOUBLE_WORD |
| 无限循环 | 不支持 | 支持 en_infiniteloop |
| 自定义 Burst | 不支持 | 支持 burst_opt |
| 半传输中断 | 不支持 | 支持 |
| 字节交换 | 不支持 | 支持(有条件) |
代码中通过编译宏自动适配:
#ifdef HPMSOC_HAS_HPMSDK_DMAV2
# include "hpm_dmav2_drv.h"
#else
# include "hpm_dma_drv.h"
#endif
2.2 通道配置结构体
typedef struct dma_channel_config {
uint8_t priority; // 优先级: LOW / HIGH
uint8_t src_burst_size; // 源突发大小: 1T ~ 1024T
uint8_t src_mode; // 源模式: NORMAL / HANDSHAKE
uint8_t dst_mode; // 目标模式: NORMAL / HANDSHAKE
uint8_t src_width; // 源传输宽度: BYTE ~ DOUBLE_WORD
uint8_t dst_width; // 目标传输宽度
uint8_t src_addr_ctrl; // 源地址控制: INCREMENT / DECREMENT / FIXED
uint8_t dst_addr_ctrl; // 目标地址控制
uint16_t interrupt_mask; // 中断掩码
uint32_t src_addr; // 源起始地址
uint32_t dst_addr; // 目标起始地址
uint32_t linked_ptr; // 下一个描述符地址(8 字节对齐,0 = 链结束)
uint32_t size_in_byte; // 总传输字节数
// ─── 以下仅 DMA v2 ───
bool en_infiniteloop; // 无限循环模式
uint8_t handshake_opt; // ONE_BURST / ALL_TRANSIZE
uint8_t burst_opt; // STANDARD / CUSTOM
} dma_channel_config_t;
2.3 链式描述符结构体
DMA v2 描述符(dma_linked_descriptor_t):
typedef struct dma_linked_descriptor {
uint32_t ctrl; // 控制字(含 ENABLE 位,硬件自动置位)
uint32_t trans_size; // 传输大小(以 src_width 为单位)
uint32_t src_addr; // 源地址
uint32_t req_ctrl; // 握手请求源选择
uint32_t dst_addr; // 目标地址
uint32_t swap_table; // 字节交换表(DMAv2 独有)
uint32_t linked_ptr; // 下一描述符地址(8 字节对齐)
uint32_t reserved0;
} dma_linked_descriptor_t;
描述符必须8 字节对齐放置在 .ahb_sram 段中。这里有双重原因:
原因一:硬件对齐。 linked_ptr 的最低 3 位被硬件屏蔽(LLPOINTERL_MASK = 0xFFFFFFF8),因此描述符首地址必须 8 字节对齐;链中最后一个描述符的 linked_ptr = 0 表示链结束。此外,描述符结构体共 8 个 32 位字(32 字节),考虑到 AHB burst 效率和避免跨 cache line,实际工程中建议 32 字节对齐。
原因二:总线访问延迟。 HDMA 控制器挂在 AHB 总线上,而 .ahb_sram 是直接挂载在 AHB 总线上的 SRAM。DMA 从 AHB SRAM 读取描述符属于同总线外设访问同总线 RAM,数据路径最短,无需跨总线桥。实测链式描述符的加载与切换在 纳秒级内完成——一个描述符执行完毕到下一个描述符开始,中间仅间隔 DMA 硬件读取 8 个字的 AHB burst 时间。如果将描述符放在其他总线域的 RAM 中(如通过总线桥访问的 SRAM),每次描述符加载都会引入额外的桥接延迟,累加后对高频率链(如每周期都触发的采集链)影响显著。
一句话总结:
.ahb_sram是决定链式操作能否达到纳秒级切换的关键,强烈建议将描述符放置于此。
3. 链式描述符:构建硬件自动机
3.1 基本工作流
┌──────────────┐ linked_ptr ┌──────────────┐ linked_ptr ┌──────────────┐
│ Descriptor 0 │ ─────────────────► │ Descriptor 1 │ ─────────────────► │ Descriptor 2 │
│ (执行传输0) │ │ (执行传输1) │ │ (执行传输2) │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
linked_ptr = 0
链在此结束
TC 中断触发
每个描述符执行完成后,DMA 硬件自动通过 linked_ptr 加载下一个描述符的 8 个 32 位字到通道寄存器,无需 CPU 参与。
3.2 环形链
将最后一个描述符的 linked_ptr 指向第一个描述符,就形成了一个永久循环的 DMA 链——每次收到 DMA 请求,通道执行完整条链后回到起点,等待下一次触发。
// 最后一个描述符回到开头 —— 形成环形链
dma_ch_config.linked_ptr = core_local_mem_to_sys_address(
HPM_CORE0, (uint32_t)&chain_descriptors[0]);
3.3 构建链的 API
// 步骤 1:初始化配置结构体
dma_channel_config_t dma_ch_config;
dma_default_channel_config(DMA_BASE, &dma_ch_config);
// 步骤 2:逐个填充配置
dma_ch_config.size_in_byte = 4;
dma_ch_config.src_width = DMA_TRANSFER_WIDTH_WORD;
dma_ch_config.dst_width = DMA_TRANSFER_WIDTH_WORD;
dma_ch_config.src_addr = (uint32_t)&some_source;
dma_ch_config.dst_addr = (uint32_t)&some_dest;
dma_ch_config.src_addr_ctrl = DMA_ADDRESS_CONTROL_FIXED;
dma_ch_config.dst_addr_ctrl = DMA_ADDRESS_CONTROL_FIXED;
dma_ch_config.linked_ptr = core_local_mem_to_sys_address(
HPM_CORE0, (uint32_t)&descriptors[idx + 1]);
dma_ch_config.interrupt_mask = DMA_INTERRUPT_MASK_ALL;
// 步骤 3:写入描述符
dma_config_linked_descriptor(
DMA_BASE,
&chain_descriptors[idx],
DMA_CHANNEL_NUM,
&dma_ch_config);
注意:
dma_config_linked_descriptor()会自动给描述符的ctrl字段或上ENABLE位。链中的每个描述符执行时是自动使能的。
3.4 一个实际的链:系统初始化链
以下是一个典型的周期初始化链,用于每个采集周期前初始化各外设状态。链中每一步对应不同的操作目标和寻址模式:
| 步骤 | 操作 | 寻址模式 | 说明 |
|---|---|---|---|
| 0 | 设置状态标志位 | FIXED→FIXED | 标记"忙"状态,通知 CPU 数据不可读 |
| 1 | 复位通信接口 FIFO | FIXED→FIXED | 写控制寄存器,清除残留数据 |
| 2 | 禁能外设定时器 | FIXED→FIXED | 暂停触发源,准备重设 |
| 3 | 复位外设定时器 | FIXED→FIXED | 释放定时器复位 |
| 4 | 重编程数据通道(A) | DECREMENT→DECREMENT | 写 7 字到另一个 DMA 通道的寄存器组 |
| 5 | 重编程数据通道(B) | DECREMENT→DECREMENT | 写 7 字到另一个 DMA 通道的寄存器组 |
| 6 | 重编程通信通道 | DECREMENT→DECREMENT | 写 7 字到通信 DMA 通道的寄存器组 |
每步执行时间在微秒级,完整链小于 20 μs。整条链由定时器周期性触发,形成一条永不中断的硬件时序。
4. 双 Buffer 乒乓机制详解
双缓冲是高速数据采集系统中最常用的设计模式。它保证了外设正在写入的 buffer 和 CPU 正在读取的 buffer 永远不会冲突,全程无需加锁。
4.1 设计思路
DMA 正在写入
│
▼
┌─────────────────────┐
Buffer A: │ 正在采集的新数据 │ ← DMA 写入中...
└─────────────────────┘
┌─────────────────────┐
Buffer B: │ 上一轮采集的完整数据 │ ← CPU 正在读取和处理
└─────────────────────┘
▲
│
CPU 正在处理
下一轮 DMA 完成后,两个 buffer 角色互换。因为不存在同时访问,所以完全不需要锁。
4.2 实现方式:双链集
不需要分配两个独立的 Buffer A/B 数组,而是使用两组链式描述符(Chain Set A / Chain Set B),两组链在需要写入的目标地址上有所不同,分别指向同一块内存区域的不同偏移位置。
// 两组链描述符数组
ATTR_PLACE_AT_WITH_ALIGNMENT(".ahb_sram", 32)
static dma_linked_descriptor_t data_chain_descriptors[2][CHAIN_COUNT];
// ↑ ↑
// [0] = Chain A
// [1] = Chain B
4.3 LLPOINTER 互换技巧
这是整个乒乓机制的核心。预先计算两组链的起始地址:
DATA_LLPOINTER[0] = (uint32_t)&data_chain_descriptors[0][0] & ~0x7; // Chain A
DATA_LLPOINTER[1] = (uint32_t)&data_chain_descriptors[1][0] & ~0x7; // Chain B
两组链的第一个描述符是关键——它负责把当前通道的 LLPOINTER 指向另一组链:
// Chain A 的描述符 0:写 DATA_LLPOINTER[1] 到 DMA 通道的 LLPOINTER 寄存器
dma_ch_config.src_addr = (uint32_t)&DATA_LLPOINTER[1]; // 下一轮用 Chain B
// Chain B 的描述符 0:写 DATA_LLPOINTER[0] 到 DMA 通道的 LLPOINTER 寄存器
dma_ch_config.src_addr = (uint32_t)&DATA_LLPOINTER[0]; // 下一轮用 Chain A
这样一来,链执行完一整轮后,DMA 通道的 LLPOINTER 已经自动指向了另一组链。下一轮 DMA 请求到来时,自动使用交替的那组链。
4.4 CPU 侧判断当前 Buffer
// 比较 DMA 通道当前 LLPOINTER 寄存器值,判断刚完成的是哪个 buffer
uint8_t buffer_idx = shadow_reg.LLPOINTER == DATA_LLPOINTER[0];
// buffer_idx = 1 表示当前使用 Chain A(即 Chain B 的数据可用)
// buffer_idx = 0 表示当前使用 Chain B(即 Chain A 的数据可用)
// 处理 buffer_idx 对应的数据
process_data(buffer_idx);
4.5 完整乒乓时序图
定时器触发信号
│
├─── 第 N 轮 ──────────────────────►│
│ DMA 使用 Chain A │ IRQ: CPU 读取 Chain A 结果
│ 写入 Buffer A │ LLPOINTER 已被链自动切换到 B
│ │
├─── 第 N+1 轮 ────────────────────►│
│ DMA 使用 Chain B │ IRQ: CPU 读取 Chain B 结果
│ 写入 Buffer B │ LLPOINTER 已被链自动切换到 A
│ │
├─── 第 N+2 轮 ────────────────────►│
│ DMA 使用 Chain A │ ...
5. 跨通道 DMA 编程 —— 一个 DMA 通道启动另一个
这是整个链式操作体系中最核心的技术。
5.1 问题
HPMicro HDMA/DMAV2 硬件不支持 DMA 通道间的直接链接触发(即通道 A 完成不能直接触发通道 B)。那如何实现"通道 A 执行完毕后通道 B 自动启动"?
5.2 解决方案:DMA 写 DMA 寄存器
答案是通过 Memory-to-Peripheral 传输,让一个 DMA 通道直接写入另一个 DMA 通道的控制寄存器组。
为了实现这一点,需要预先将目标通道的完整寄存器快照保存到内存中:
// 目标通道的寄存器影子副本(7 个 32 位寄存器)
volatile struct {
volatile uint32_t CTRL; // 控制寄存器(含 ENABLE 位)
volatile uint32_t TRANSIZE; // 传输大小
volatile uint32_t SRCADDR; // 源地址
volatile uint32_t CHANREQCTRL; // 握手请求源(DMAv2)
volatile uint32_t DSTADDR; // 目标地址
volatile uint32_t SWAPTABLE; // 字节交换表(DMAv2)
volatile uint32_t LLPOINTER; // 链描述符指针
} DMA_SHADOW_REG;
通过 dma_setup_channel() + start_transfer=false 配置好目标通道,然后用 memcpy 将其寄存器组整体复制到影子副本。
5.3 原子性使能
7 个字按照从高地址到低地址的顺序写入(DECREMENT 模式,见第 6 章)。最后写入的是 CTRL 寄存器(含 ENABLE 位),这意味着在全部寄存器配置完毕之前,目标通道不会开始执行。这就是"配置即启动"的原子性 —— 要么全部配置完并启动,要么什么都不发生。
6. DECREMENT 寻址模式的精妙之处
6.1 为什么需要 DECREMENT
DMA 通道寄存器组的物理布局:
偏移 寄存器 说明
─────────────────────────────────────
0x00 CTRL 控制字(含 ENABLE 位) ← 必须最后写
0x04 TRANSIZE 传输大小
0x08 SRCADDR 源地址
0x0C CHANREQCTRL 请求源选择(DMAv2)
0x10 DSTADDR 目标地址
0x14 SWAPTABLE 交换表(DMAv2)
0x18 LLPOINTER 链描述符指针 ← 首地址(偏移最高)
如果使用 INCREMENT 模式从 LLPOINTER 地址开始写,第一个写入的是 LLPOINTER,然后依次向后……最后才会轮到 0x00 处的 CTRL。但 ENABLE 位落在 CTRL 的最低位,如果先写了 LLPOINTER 而 CTRL 尚未写入,通道可能在配置不完整的状态下被误触发。
DECREMENT 模式解决了这个问题。DMA 从 &LLPOINTER 地址开始,每传输一个字地址递减 4 字节,直到写完 7 个字:
第 1 字: LLPOINTER (地址: base + 0x18)
第 2 字: SWAPTABLE (地址: base + 0x14)
第 3 字: DSTADDR (地址: base + 0x10)
第 4 字: CHANREQCTRL (地址: base + 0x0C)
第 5 字: SRCADDR (地址: base + 0x08)
第 6 字: TRANSIZE (地址: base + 0x04)
第 7 字: CTRL (地址: base + 0x00) ← 最后写,含 ENABLE
关键:源地址和目标地址同时使用 DECREMENT,保证源端寄存器镜像的内存布局与目标端硬件寄存器布局在逐字递减访问时完全对应。
6.2 代码示例
dma_ch_config.size_in_byte = 7 * sizeof(uint32_t);
dma_ch_config.src_addr = core_local_mem_to_sys_address(
HPM_CORE0, (uint32_t)&SHADOW_REG.LLPOINTER);
dma_ch_config.dst_addr = core_local_mem_to_sys_address(
HPM_CORE0, (uint32_t)&(HDMA->CHCTRL[TARGET_CH].LLPOINTER));
dma_ch_config.src_width = DMA_TRANSFER_WIDTH_WORD;
dma_ch_config.dst_width = DMA_TRANSFER_WIDTH_WORD;
dma_ch_config.src_addr_ctrl = DMA_ADDRESS_CONTROL_DECREMENT; // ← 关键
dma_ch_config.dst_addr_ctrl = DMA_ADDRESS_CONTROL_DECREMENT; // ← 关键
6.3 三种寻址模式速查
| 模式 | 常量 | 行为 | 典型用途 |
|---|---|---|---|
| INCREMENT | DMA_ADDRESS_CONTROL_INCREMENT (0) |
每传一字,地址 + width | 内存到内存拷贝、填充递增缓冲区 |
| DECREMENT | DMA_ADDRESS_CONTROL_DECREMENT (1) |
每传一字,地址 - width | 寄存器组倒序写入(最后写 ENABLE) |
| FIXED | DMA_ADDRESS_CONTROL_FIXED (2) |
地址保持不变 | 读写外设数据寄存器(SPI->DATA、GPIO->DO 等) |
7. Burst 小循环与握手选项
7.1 Burst(突发传输)概念
Burst 定义了每次 DMA 仲裁中可以连续传输多少次。在握手模式下,每次 DMA_REQ 触发一个 Burst。
DMA_NUM_TRANSFER_PER_BURST_1T // 1 次传输
DMA_NUM_TRANSFER_PER_BURST_2T // 2 次
DMA_NUM_TRANSFER_PER_BURST_16T // 16 次
DMA_NUM_TRANSFER_PER_BURST_128T // 128 次
DMA v2 的 CUSTOM Burst:当 burst_opt = DMA_SRC_BURST_OPT_CUSTOM_SIZE 时,src_burst_size 不再是 2 的幂次编码,而是直接表示 (size + 1) 次传输。例如 src_burst_size = 6 表示每次 burst 传输 7 次。这在需要非对齐或非 2^n 传输次数的场景中非常实用。
7.2 握手选项(Handshake Option,仅 DMA v2)
| 选项 | 常量 | 含义 |
|---|---|---|
| ONE_BURST | DMA_HANDSHAKE_OPT_ONE_BURST (0) |
每次 DMA_REQ 只传输一个 Burst |
| ALL_TRANSIZE | DMA_HANDSHAKE_OPT_ALL_TRANSIZE (1) |
每次 DMA_REQ 传输整个 trans_size |
- ONE_BURST:适用于 SPI、UART 等有 FIFO 的外设,每次握手填满或清空一个 FIFO 深度
- ALL_TRANSIZE:适用于寄存器组一次性写入(如跨通道 DMA 编程),要求一次性完成不能中断
7.3 Burst 选择示例
// 外设数据收发:ONE_BURST + 1T
dma_ch_config.burst_opt = DMA_SRC_BURST_OPT_STANDAND_SIZE;
dma_ch_config.src_burst_size = DMA_NUM_TRANSFER_PER_BURST_1T;
dma_ch_config.handshake_opt = DMA_HANDSHAKE_OPT_ONE_BURST;
// 跨通道 DMA 寄存器组写入:ALL_TRANSIZE
dma_ch_config.burst_opt = DMA_SRC_BURST_OPT_STANDAND_SIZE;
dma_ch_config.src_burst_size = DMA_NUM_TRANSFER_PER_BURST_1T;
dma_ch_config.handshake_opt = DMA_HANDSHAKE_OPT_ALL_TRANSIZE;
8. DMA v1 与 DMA v2 关键差异
8.1 描述符布局差异
DMA v1 描述符:
Offset 0 4 8 12 16 20 24 28
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ CTRL │TRSIZE│SRCADD│SRC_HI│DSTADD│DST_HI│LINKPT│LNK_HI│
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
DMA v2 描述符:
Offset 0 4 8 12 16 20 24 28
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ CTRL │TRSIZE│SRCADD│REQCTL│DSTADD│SWPTBL│LINKPT│RESV │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
↑ 请求源已
从 CTRL 移出
DMA v2 将 SRCREQSEL/DSTREQSEL 从 CTRL 寄存器中移出,放入独立的 CHANREQCTRL 寄存器(描述符中对应 req_ctrl 字段),使 CTRL 位域更简洁。同时将 src_addr_high/dst_addr_high 替换为 req_ctrl 和 swap_table(DMAv2 目标 MCU 通常为 32 位总线)。
8.2 功能差异速查
| 功能 | DMA v1 | DMA v2 | 使用场景 |
|---|---|---|---|
en_infiniteloop |
不支持 | 支持 | 持续周期性传输,无需 CPU 重新使能通道 |
handshake_opt |
不支持 | 支持 | 区分单 burst 握手 / 全传输握手 |
burst_opt (CUSTOM) |
不支持 | 支持 | 非 2^n 的自定义 burst 大小 |
| 半传输中断 | 不支持 | 支持 | 双缓冲中途通知(可用链切换替代) |
| 字节交换 | 不支持 | 支持 | 大小端转换 |
| 中断状态 | 单一 INTSTATUS | 独立 TC/ABORT/ERR/HALF | v2 寄存器更清晰 |
8.3 代码中的条件编译
#ifdef HPMSOC_HAS_HPMSDK_DMAV2
// DMA v2 寄存器操作
TIMER_REG[tmr_ch_cr_disable] =
(GPTMR->CHANNEL[N].CR | GPTMRV2_CHANNEL_CR_CNTRST_MASK)
& ~GPTMRV2_CHANNEL_CR_CEN_MASK;
#else
// DMA v1 寄存器操作
TIMER_REG[tmr_ch_cr_disable] =
(GPTMR->CHANNEL[N].CR | GPTMR_CHANNEL_CR_CNTRST_MASK)
& ~GPTMR_CHANNEL_CR_CEN_MASK;
#endif
同一套代码逻辑,编译时根据 MCU 型号自动选择正确的寄存器位定义。
9. 实战:多通道协同的完整 DMA 链
以多通道同步采集系统为例,一条完整的采集链涉及 4 个 DMA 通道 和 3 组描述符链。
9.1 通道分配
DMA CH6 (INIT) ──► 周期初始化链,复位外设并重编程其他 DMA 通道
DMA CH7 (DATA) ──► 数据采集链(双 Chain Set,乒乓交替)
DMA CH8 (TX) ──► 发送通道(无限循环,持续传输)
DMA CH9 (RX) ──► 接收通道(无限循环,持续接收)
9.2 触发拓扑
定时器 CH0 ──► CMP0 匹配 ──► DMA_REQ ──► DMA CH6 (INIT Chain)
│
├──► 定时器 CH1 ──► CMP0 匹配 ──► DMA_REQ ──► DMA CH7 (DATA Chain)
│
└──► 定时器 CH2 ──► CMP0 匹配 ──► 触发多路复用器 ──► 外设触发信号
│
▼
外设 DMA 流控
数据写入采集缓冲
9.3 INIT Chain(通道 6,环形链)
定时器触发 DMA_REQ 后执行完整条链,最终回到起点等待下一轮:
DMA_REQ → CH6
│
├─ [Desc 0]: 设置状态标志位 (2B, HANDSHAKE on src)
├─ [Desc 1]: 复位通信接口 FIFO (4B, NORMAL)
├─ [Desc 2]: 禁能外设定时器 (4B, NORMAL)
├─ [Desc 3]: 复位外设定时器 (4B, NORMAL)
├─ [Desc 4]: 重编程 DATA 通道寄存器组 (28B, DECREMENT→DECREMENT)
├─ [Desc 5]: 重编程 COMM 通道寄存器组 (28B, DECREMENT→DECREMENT)
└─ [Desc 6]: 使能外设定时器(准备采集) (4B, NORMAL)
linked_ptr → Desc 0 (环形)
9.4 DATA Chain(通道 7,双链乒乓)
// Chain A — Descriptor 0: 将通道 LLPOINTER 切换到 Chain B
dma_ch_config.src_addr = (uint32_t)&DATA_LLPOINTER[1];
dma_ch_config.dst_addr = (uint32_t)&HDMA->CHCTRL[7].LLPOINTER;
dma_ch_config.linked_ptr = next_descriptor;
// Chain B — Descriptor 0: 将通道 LLPOINTER 切换到 Chain A
dma_ch_config.src_addr = (uint32_t)&DATA_LLPOINTER[0];
dma_ch_config.dst_addr = (uint32_t)&HDMA->CHCTRL[7].LLPOINTER;
dma_ch_config.linked_ptr = next_descriptor;
两条链交替执行,每次采集的数据写入不同的 buffer 区域。
9.5 无限循环通道(持续收发)
对需要持续运行的外设通信通道,使用 en_infiniteloop = true:
dma_ch_config.en_infiniteloop = true;
dma_ch_config.size_in_byte = sizeof(data_block_t);
dma_ch_config.src_addr = (uint32_t)&tx_data_block;
dma_ch_config.dst_addr = (uint32_t)&SPI->DATA;
dma_ch_config.src_addr_ctrl = DMA_ADDRESS_CONTROL_INCREMENT;
dma_ch_config.dst_addr_ctrl = DMA_ADDRESS_CONTROL_FIXED;
dma_ch_config.dst_mode = DMA_HANDSHAKE_MODE_HANDSHAKE;
dma_ch_config.linked_ptr = 0;
// infinite loop 必须用 dma_setup_channel(),不能用链式描述符
dma_setup_channel(HDMA, CH_TX, &dma_ch_config, false);
linked_ptr = 0 但 en_infiniteloop = true 时,通道会持续循环这段传输,由外设握手信号驱动每次发送。
10. 最佳实践与注意事项
10.1 描述符必须放在 AHB SRAM
描述符必须 32 字节对齐,linked_ptr 必须 8 字节对齐(低 3 bit = 0)。使用:
ATTR_PLACE_AT_WITH_ALIGNMENT(".ahb_sram", 32)
static dma_linked_descriptor_t my_chain[N];
再次强调
.ahb_sram:HDMA 挂在 AHB 总线上,访问同总线上的 AHB SRAM 路径最短、延迟最低。链式切换时 DMA 硬件一次 burst 读取 8 个 32 位字(32 字节)即可完成当前描述符到下一个描述符的切换,实测在纳秒级。如果描述符放在其他总线域的 RAM 中,每次切换都要过总线桥,高频触发场景下延迟累加不可忽视。详见 2.3 节。
10.2 核间地址转换
在多核系统中,需要使用 core_local_mem_to_sys_address() 将本地内存地址转换为系统总线地址:
dma_ch_config.src_addr = core_local_mem_to_sys_address(
HPM_CORE0, (uint32_t)&my_local_data);
10.3 链终止方式
linked_ptr = 0:链结束,通道执行完最后一个描述符后产生 TC 中断linked_ptr指向链首:环形链,永久循环,需手动dma_abort_channel()终止
10.4 en_infiniteloop 的限制(DMA v2)
// ❌ 错误:infinite loop 和 linked descriptor 不能同时使用
dma_ch_config.en_infiniteloop = true;
dma_ch_config.linked_ptr = some_address; // 返回 status_invalid_argument
// ✅ 正确:infinite loop 仅用于单段传输
dma_ch_config.en_infiniteloop = true;
dma_ch_config.linked_ptr = 0;
dma_setup_channel(ptr, ch, &config, true);
10.5 环形链 vs en_infiniteloop
| 特性 | 环形链 (linked_ptr 循环) |
en_infiniteloop |
|---|---|---|
| 复杂度 | 支持多步骤序列 | 仅单段传输 |
| DMA 版本 | v1 / v2 均支持 | 仅 v2 |
| 适用场景 | 复杂多步时序(初始化链) | 简单重复传输(持续收发) |
| 终止方式 | dma_abort_channel() |
dma_abort_channel() |
10.6 传输宽度约束
源宽度 ≤ 目标宽度
源地址必须按 (1 << src_width) 对齐
目标地址必须按 (1 << dst_width) 对齐
使用 dma_start_memcpy() 可自动推导最优宽度和突发大小。
10.7 中断处理
DMA 链执行完毕后产生 TC(Terminal Count)中断。ISR 中应快速识别数据来源并投递到处理队列:
void isr_handler(void) {
// 识别数据来源(根据 LLPOINTER 判断 buffer 索引)
uint8_t buffer_idx = identify_buffer();
// 投递到处理队列(ISR 中不做重计算)
post_to_workqueue(buffer_idx);
}
10.8 调试 DMA 链的实用技巧
- 先不用链:先用
dma_setup_channel(start_transfer=true)验证单个传输正确 - 逐步添加链节点:从 2 个描述符开始验证,逐步扩展
- 检查对齐:确保
.ahb_sram段、32 字节对齐、linked_ptr8 字节对齐 - GPIO 翻转:在 ISR 中翻转 GPIO,用示波器测量链执行周期
- 影子寄存器比对:通过读取 DMA 通道寄存器与内存中的影子副本比对,确认跨通道编程是否成功
附录 A:关键 API 速查
| 函数 | 作用 |
|---|---|
dma_default_channel_config(ptr, &cfg) |
用安全的默认值初始化配置 |
dma_setup_channel(ptr, ch, &cfg, start) |
将配置写入硬件寄存器,可选立即启动 |
dma_config_linked_descriptor(ptr, &desc, ch, &cfg) |
将配置写入描述符结构体(自动置 ENABLE) |
dma_enable_channel(ptr, ch) |
使能通道 |
dma_abort_channel(ptr, ch) |
终止通道 |
dma_check_transfer_status(ptr, ch) |
查询通道状态 |
dmamux_config(dmamux, ch, src, enable) |
配置 DMAMUX 触发源映射 |
core_local_mem_to_sys_address(core, addr) |
核内地址转系统总线地址 |
附录 B:参考文件
| 文件 | 说明 |
|---|---|
hpm_dma_drv.h / hpm_dmav2_drv.h |
DMA v1/v2 驱动 API 头文件 |
hpm_dma_drv.c / hpm_dmav2_drv.c |
DMA v1/v2 驱动实现 |
hpm_dma_regs.h / hpm_dmav2_regs.h |
DMA 硬件寄存器定义 |
hpm_dmamux_drv.h |
DMAMUX 驱动 API |