STM32F4 DMA链式传输大数据块技术分析

在工业控制、医疗设备或高端音频采集系统中,你有没有遇到过这样的窘境:ADC采样频率拉满,数据哗哗地来,结果CPU一跑起来就丢点?USART发个包都卡顿,调试信息满屏“overflow”?😱

问题出在哪?不是你的代码写得差,也不是主频不够高——STM32F4最高168MHz的Cortex-M4内核算得上强劲。真正瓶颈在于: 你还在让CPU亲自搬数据!

这时候就得请出DMA(Direct Memory Access)这位“搬运工”了。但如果你只用它传几个字节,那可真是大材小用啦~尤其是在处理 高速、大批量连续数据流 时,比如:

  • 高分辨率ADC持续采样(1MSPS × 16bit × 10秒 = 20MB!)
  • I2S音频录音
  • 图像传感器原始帧抓取
  • SD卡批量写入

这些场景下,单次DMA最大只能搬65535字节(受限于16位计数器),怎么办?总不能中途停下来等CPU重新配置吧?

别急——今天我们就来聊聊如何在 STM32F4上实现“准链式DMA传输” ,把多个DMA传输任务串成一条“数据高铁”,全程无需CPU插手,一口气跑完几十甚至上百KB的数据!

🚧 注意:STM32F4原生DMA硬件并不支持像STM32H7那样的 真正的硬件链表模式 (LLM)。但我们可以通过HAL库提供的链表管理+中断回调机制,模拟出几乎一样的效果。这种方案常被称为“软链式”或“伪链式”DMA。


从一块缓冲区到一张链表:为什么需要链式传输?

先来看看传统DMA是怎么工作的:

HAL_DMA_Start(&hdma, src, dst, 1024); // 搬1KB

简单直接,但有个致命限制: DataLength 是16位寄存器,最多只能设65535。想搬100KB?不行,得分好几次。

更麻烦的是,每次搬完都要进一次中断,CPU得赶紧重新配置下一波参数。万一这时来了个高优先级中断,延迟一下,外设缓冲就溢出了……💥

于是我们开始思考:

✅ 能不能提前把所有“搬运计划”列好?
✅ 让DMA自己一个接一个执行下去?
✅ 只在整条链结束时才叫醒CPU?

这就是 链式传输的核心思想

虽然F4没有硬件自动跳转功能,但我们可以借助 HAL库的 DMAEx_List_xxx API 中断调度逻辑 实现类似的流程控制。


如何构建一个DMA链?节点、连接与启动

1. 定义传输节点(Node)

每个Node代表一段独立的DMA任务,包含以下关键信息:

字段 含义
SAddress 源地址(如ADC_DR寄存器)
DAddress 目标地址(SRAM中的缓冲区)
DataLength 数据长度(≤65535)
Direction 传输方向(外设→内存 / 内存→外设)
LinkedListIndeX 节点索引(用于调试跟踪)

示例代码:

#define NODE_COUNT 3
#define BUFFER_SIZE 4096

DMA_NodeTypeDef NodeDesc[NODE_COUNT];
uint8_t buffer0[BUFFER_SIZE];
uint8_t buffer1[BUFFER_SIZE];
uint8_t buffer2[BUFFER_SIZE];

// 初始化各节点
for (int i = 0; i < NODE_COUNT; i++) {
    NodeDesc[i].SAddress        = (uint32_t)&ADC1->DR;
    NodeDesc[i].DAddress        = (uint32_t)(buffer0 + i * BUFFER_SIZE);
    NodeDesc[i].BurstState      = DMA_BURST_NONE;
    NodeDesc[i].ChainedMode     = DMA_LINKEDLIST_NORMAL;
    NodeDesc[i].DataLength      = BUFFER_SIZE;
    NodeDesc[i].Direction       = DMA_PERIPH_TO_MEMORY;
    NodeDesc[i].LinkedListIndeX = i;
}

📌 小贴士:
- 所有Node必须按 4字节对齐 ,否则可能触发总线错误;
- 缓冲区建议使用 __attribute__((aligned(4))) 显式对齐;


2. 构建链表结构

使用HAL库提供的链表工具函数将Node串联起来:

DMA_LinkListTypeDef LinkedList;

// 清空链表
HAL_DMAEx_List_Clear(&LinkedList);

// 插入节点(顺序很重要!)
HAL_DMAEx_List_InsertNode(&LinkedList, &NodeDesc[0], NULL);        // 头节点
HAL_DMAEx_List_InsertNode(&LinkedList, &NodeDesc[1], &NodeDesc[0]); // 第二个
HAL_DMAEx_List_InsertNode(&LinkedList, &NodeDesc[2], &NodeDesc[1]); // 第三个

此时,链表结构如下:

[Node0] → [Node1] → [Node2]
   ↓         ↓         ↓
Buffer0   Buffer1   Buffer2

然后绑定到DMA句柄:

HAL_DMAEx_List_LinkInit(&hdmastream, &LinkedList);

这一步并不会启动传输,只是告诉DMA:“我已经准备好一套计划了。”


3. 启动第一站,后续靠“接力”

接下来启动第一个Node即可:

HAL_DMA_Start_IT(&hdmastream,
                 NodeDesc[0].SAddress,
                 NodeDesc[0].DAddress,
                 NodeDesc[0].DataLength);

注意这里用了 _IT 版本,表示启用中断模式。

那么问题来了: DMA自己不会自动加载下一个Node啊?怎么“链”起来?

答案是:靠 中断回调 + 手动启动下一程


中断驱动的“软链式”调度逻辑

当第一个Node完成传输后,会触发 Transfer Complete (TC) 中断,进入回调函数:

void HAL_DMA_TxCpltCallback(DMA_HandleTypeDef *hdma) {
    static uint8_t current_node = 0;

    if (hdma != &hdmastream) return;

    current_node++;

    if (current_node < NODE_COUNT) {
        // 自动启动下一个Node
        HAL_DMA_Start_IT(hdma,
                         NodeDesc[current_node].SAddress,
                         NodeDesc[current_node].DAddress,
                         NodeDesc[current_node].DataLength);
    } else {
        // 整条链完成!通知主任务
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); // 灯亮提示
        printf("🎉 DMA chain transfer completed!\n");
        current_node = 0; // 或者重置为循环模式
    }
}

👏 看!这就是所谓的“软件链式”精髓:
用中断作为“交接棒”,实现多段DMA的无缝衔接

是不是有点像快递员送完一车货,马上打电话叫下一辆车出发?🚚


半传输中断(HT)还能干啥?提前预警超实用!

除了 TC 中断,还可以开启 半传输中断(Half Transfer, HT) ,用来做预处理或状态监控:

__HAL_DMA_ENABLE_IT(&hdmastream, DMA_IT_HT);

对应的回调:

void HAL_DMA_HalfCpltCallback(DMA_HandleTypeDef *hdma) {
    if (hdma == &hdmastream) {
        // 此时前一半数据已到位,可用于:
        // - 提前启动后台处理任务
        // - 发送信号量唤醒RTOS线程
        // - 更新UI进度条
        osSemaphoreRelease(ProcessSemHandle);
    }
}

💡 工程技巧:
- 在 HT 中断中不要做耗时操作(如memcpy、printf);
- 推荐仅用于“打个招呼”,真正处理交给主线程或低优先级任务;


实际应用场景:高速ADC数据采集系统

设想这样一个系统:

[模拟传感器]
     ↓
[ADC1 → 触发源:TIM2_TRGO]
     ↓
[DMA2_Stream0 → SRAM缓冲池(3×4KB)]
     ↓
[RTOS任务 → 数据打包上传UART]

目标:以1Msps采样率连续采集12ms,共约12000个样本(24KB),要求零丢失。

设计思路

  • 每个Node传4KB(≈2048个uint16_t)
  • 共需6个Node → 总容量24KB
  • 使用定时器触发ADC连续转换
  • DMA链式搬运至SRAM
  • 最后通过UART异步发送至上位机

关键优化点

优化项 做法
中断优先级 设置DMA TC中断优先级高于其他非关键中断
缓冲策略 使用环形Node数组,支持循环采集
错误处理 开启TEIE(传输错误)、DMEIE(直接模式错误)中断
Cache一致性 若开启DCache,MemToPeriph前需调用 SCB_CleanDCache_by_Addr()
总线竞争 避免CPU频繁访问SRAM区域,减少AHB拥堵

常见陷阱与最佳实践 ✅

❌ 错误1:Node未对齐导致HardFault

// 错!全局变量未必对齐
uint8_t buf[4096]; 

✅ 正确做法:

uint8_t __attribute__((aligned(4))) buf[4096];

❌ 错误2:在中断里做太多事

void HAL_DMA_TxCpltCallback() {
    memcpy(big_buffer, temp, size); // ⛔ 千万别在这儿复制大数据!
    printf("Done\n");               // ⛔ printf可能阻塞
}

✅ 应改为:

void HAL_DMA_TxCpltCallback() {
    xTaskNotifyFromISR(process_task_handle, 0, eNoAction, NULL); // 通知任务
}

✅ 推荐实践清单

建议 说明
Node大小选4KB~16KB 平衡中断频率与响应延迟
启用HT中断做预警 提前准备资源
使用双链交替机制 类似双缓冲,实现不间断流
结合RTOS信号量/队列 解耦数据采集与处理
利用STM32CubeIDE DMA监视器 实时查看传输状态
添加时间戳日志 用ITM输出关键事件时间,便于调试时序

总结:这不是“真链式”,但足够强大 💪

严格来说,STM32F4的DMA并没有像H7系列那样具备 硬件解析链表描述符 的能力。我们所实现的是一种基于 HAL库封装 + 中断调度 的“准链式”方案。

但它带来的价值却是实实在在的:

接近零CPU占用 :除了首尾和中断切换,全程无需干预
突破65535限制 :理论上可以无限拼接Node
灵活可控 :可在运行时动态修改下一Node参数
易于集成 :配合RTOS轻松构建高效流水线

对于大多数嵌入式开发者而言,掌握这套方法,意味着你可以从容应对那些曾让你夜不能寐的 大数据吞吐挑战

毕竟,在真实项目中,没人关心你用了多少行汇编,大家只在乎:
👉 数据有没有丢?
👉 系统稳不稳?
👉 CPU还能不能干别的?

而DMA链式传输,正是通往“高可靠、高性能”系统的必经之路之一。🚀

所以,下次当你面对一堆涌来的传感器数据时,记得对自己说一句:

“让DMA去搬吧,我还有更重要的事要做。” 😎

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐