STM32 DMA 一文章懂

AI助手已提取文章相关产品:

让硬件干活,别让CPU搬砖:STM32 DMA实战全解析

你有没有遇到过这样的场景?

  • ADC每毫秒采一次数据,主循环卡得像老式收音机;
  • UART发个日志都要调用 HAL_UART_Transmit() 阻塞几百微秒;
  • 想做音频播放,结果PCM数据根本喂不饱I2S接口;
  • 系统一跑起来,CPU占用率直接飙到90%以上……

这时候,很多人第一反应是:“换颗更快的芯片?”
但其实,问题不在性能不够强,而在 资源分配不合理

真正高效的嵌入式系统,不是靠“暴力堆算力”,而是懂得 让每个模块各司其职 。而今天我们要聊的这个“隐形英雄”——DMA(Direct Memory Access),正是实现这一理念的关键钥匙。

🧩 “CPU负责思考,DMA负责跑腿。”
—— 这句话听起来简单,但它背后藏着现代MCU高效运行的核心逻辑。


为什么你需要关心DMA?

先来看一组真实对比:

场景 CPU轮询方式 使用DMA
每1ms采集4通道ADC 每秒触发1000次中断,每次执行读取+存储指令 初始化后零干预,仅每100个样本进一次中断
发送1KB串口数据包 HAL_UART_Transmit() 阻塞约8ms(115200bps) 调用即返回,CPU立刻继续工作
音频I2S播放 必须在每个时钟周期由中断喂数据 启动DMA后自动持续输出

看到差别了吗?
使用DMA后, 同样的功能,CPU从“搬运工”变成了“管理者” 。它不再亲自参与每一个字节的传输,而是说一句:“你们去干吧,干完了叫我。”

这不只是省了几条代码的事,而是从根本上改变了系统的架构和响应能力。

那么,DMA到底是什么?

你可以把它想象成一个 独立的小型搬运机器人 ,内置于STM32芯片中。它的任务很简单:
👉 在两个地址之间搬数据,搬完就报告,中间完全不需要你盯着。

而且这家伙还不挑活:
- 外设 → 内存 ✅(比如ADC采样存RAM)
- 内存 → 外设 ✅(比如发送字符串到USART)
- 内存 → 内存 ✅(比如复制大块Flash数据到SRAM)

只要总线允许,它就能上场。


STM32里的DMA长什么样?

STM32不是只给了你一个DMA控制器,而是配了一整套“物流系统”。

以常见的STM32F4系列为例:
- 有两个DMA控制器:DMA1 和 DMA2
- 每个控制器有多个通道(Channel),总共最多7+7=14条
- 每个通道可以绑定不同的外设请求信号(如ADC1、SPI3_TX等)
- 支持优先级调度、双缓冲、循环模式、FIFO……简直是为高吞吐量设计的“高速公路”

更高端的H7系列甚至还有 BDMA (Backup Domain DMA)和 MDMA (Memory-to-Memory DMA),支持跨域访问和更复杂的链式传输。

但这不是重点。
重点是你得知道: DMA不是一个“可选项”,而是构建高性能系统的“基础设施”


它是怎么工作的?拆开看看

我们拿最常见的 ADC + DMA 数据采集 来举例。

假设你现在要做一个环境监测设备,需要每10ms采集一次温湿度、光照、噪声四个传感器的数据。

传统做法可能是这样:

while (1) {
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, 10);
    value1 = HAL_ADC_GetValue(&hadc1);

    HAL_ADC_Start(&hadc2); // 假设多路
    HAL_ADC_PollForConversion(&hadc2, 10);
    value2 = HAL_ADC_GetValue(&hadc2);

    process_data(value1, value2); // 处理数据
    HAL_Delay(10);                // 等待下一个周期
}

这段代码的问题在哪?
- 占用CPU时间太长
- HAL_Delay() 期间什么都不能干
- 如果加上通信、显示等功能,整个系统会变得非常卡顿

现在换成DMA思路:

🧠 思路转变:
我不再“主动去拿数据”,而是告诉ADC:“你每完成一次转换,就把结果扔进内存里,等攒够了叫我。”

具体流程如下:

  1. 配置阶段 (一次性设置好)
    - 指定源地址:ADC的数据寄存器(固定不变)
    - 目标地址:RAM中的缓冲区(例如 uint16_t buffer[4]
    - 方向:外设 → 内存
    - 数据宽度:半字(16位)
    - 传输数量:4(对应4个通道)
    - 模式:循环模式(Circular Mode)→ 自动重复采集

  2. 启动后
    - 只需开启一次ADC,之后所有转换都由硬件自动触发
    - 每次转换完成 → ADC发出DMA请求 → DMA自动把值写入buffer
    - 当4个通道全部采完 → 触发DMA传输完成中断

  3. CPU只需在中断里处理
    c void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc == &hadc1) { // 此时buffer已更新,可进行滤波/上传等操作 analyze_sensor_data(adc_buffer); } }

整个过程中,CPU除了初始化和最后处理外,全程“脱手”。

💡 实际效果:
原本每秒要被打断1000次的任务,现在可能每秒只中断10次(每次处理100个样本), CPU负载下降90%以上


关键特性详解:这些功能你真的会用吗?

🔁 循环模式(Circular Mode)——无限续航的秘密

当你启用 DMA_CIRCULAR 模式时,DMA会在缓冲区填满后自动重置指针,重新开始填充。

这对于周期性采集简直是神器。

举个例子:

uint16_t adc_buffer[100]; // 缓冲100个样本

hdma_adc1.Init.Mode = DMA_CIRCULAR;
HAL_DMA_Init(&hdma_adc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 100);

这意味着:
- 不用手动重启DMA
- 数据流源源不断
- 可结合双缓冲或定时中断做滑动窗口分析

⚠️ 注意:不要在循环模式下使用局部变量作为缓冲区!栈空间可能被覆盖。


🔄 双缓冲模式(Double Buffer Mode)——无缝衔接的艺术

有些应用对数据连续性要求极高,比如音频录制、高速示波器。

这时普通的单缓冲会有“空窗期”:当CPU正在处理当前缓冲时,新的数据仍在不断产生,稍有不慎就会丢失。

解决方案?双缓冲!

开启双缓冲后,DMA会在两个内存区域之间交替写入:

Buffer A ← 写入中
         ↓
       切换
         ↑
Buffer B ← 已完成,供CPU读取

每当一个缓冲写满,DMA自动切换到另一个,并触发中断通知CPU处理旧缓冲。

在HAL库中启用方式如下:

hdma_adc1.Init.Mode = DMA_CIRCULAR;
hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
hdma_adc1.Init.MemBurst = DMA_MBURST_SINGLE;
// 注意:需手动使能双缓冲寄存器位(部分型号需底层操作)

或者使用LL库直接配置:

LL_DMA_EnableDoubleBufferMode(DMA2, LL_DMA_STREAM_0);
LL_DMA_SetMemory1BaseAddr(DMA2, LL_DMA_STREAM_0, (uint32_t)&adc_buffer2);

这样一来,你就实现了真正的“一边采样,一边处理”,完全没有停顿。

🎧 典型应用:I2S麦克风录音、软件定义无线电(SDR)、电机电流闭环控制。


⚖️ 优先级管理——谁先谁后很重要

多个DMA通道同时请求怎么办?比如ADC在传数据,同时SPI也在往外发图像。

这时候就得靠 优先级仲裁机制

STM32的DMA通道支持四级优先级:
- 非常低(Very Low)
- 低(Low)
- 中(Medium)
- 高(High)

配置也很简单:

hdma_usart1_tx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_adc1.Init.Priority = DMA_PRIORITY_MEDIUM;

规则是:
- 软件优先级高的先服务
- 同级则按通道号顺序(编号小的优先)

所以在设计系统时就要想清楚:
👉 是语音通信更重要?还是传感器采样不能丢?

合理分配优先级,才能避免关键数据被挤占。


📏 数据对齐与FIFO——别让硬件罢工

DMA很强大,但也有些“脾气”。

比如最常见的 HardFault 错误,往往就是因为地址没对齐。

记住这几条铁律:

数据宽度 要求对齐方式
Byte(8位) 任意地址
Half-word(16位) 偶地址(如 0x20000000, 0x20000002)
Word(32位) 4字节对齐(0x20000000, 0x20000004)

如果你试图用 DMA_MDATAALIGN_WORD 去访问一个未对齐的地址,轻则传输失败,重则直接触发HardFault。

解决办法:
- 定义缓冲区时强制对齐:

__attribute__((aligned(4))) uint32_t dma_buffer[32];
  • 或者使用编译器提供的宏:
ALIGN_32BYTES(uint8_t audio_buf[256]);

此外,高端型号还支持FIFO模式,可用于缓解速度不匹配问题。

例如:
- 慢速外设 → 快速内存:启用FIFO可批量传输,减少总线占用
- 快速外设 → 慢速外设:FIFO充当缓冲池,防止溢出

不过FIFO也增加了复杂度,建议初学者先掌握基本模式。


实战案例:用DMA让USART飞起来

你是不是经常为了发一条调试信息,就得让整个程序卡住几毫秒?

试试这个:

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;

uint8_t log_msg[] = "[INFO] System initialized.\r\n";

void send_log_dma(void) {
    HAL_UART_Transmit_DMA(&huart1, log_msg, sizeof(log_msg));
}

就这么一行调用,DMA就会自动把数据从内存搬到USART的TDR寄存器,直到全部发送完毕。

期间CPU干什么都可以:
- 继续采集传感器
- 更新LCD显示
- 执行PID控制算法

等到发送结束,会自动调用回调函数:

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        // 可以标记发送完成,或启动下一轮
        tx_complete_flag = 1;
    }
}

💡 小技巧:如果你想连续发送多条消息,可以用环形队列管理待发数据包,每次DMA发送完成后自动取出下一条。

这样就能实现类似“异步日志系统”的效果,完全不影响主线程。

🚀 应用场景扩展:
- GPS模块数据转发(NMEA语句批量发送)
- Modbus RTU协议帧输出
- BLE/Wi-Fi模组AT指令交互


更进一步:内存到内存的DMA搬运

很多人以为DMA只能用于外设,其实它也能当“内部快递员”。

比如你想把一段Flash中的字体数据加载到SRAM中用于GUI显示:

extern const uint8_t font_data_in_flash[];
uint8_t font_buffer_in_sram[2048];

DMA_HandleTypeDef hdma_mem2mem;

void copy_font_via_dma(void) {
    __HAL_RCC_DMA2_CLK_ENABLE();

    hdma_mem2mem.Instance = DMA2_Stream1;
    hdma_mem2mem.Init.Direction = DMA_MEMORY_TO_MEMORY;
    hdma_mem2mem.Init.PeriphInc = DMA_PINC_ENABLE;
    hdma_mem2mem.Init.MemInc = DMA_MINC_ENABLE;
    hdma_mem2mem.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_mem2mem.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_mem2mem.Init.Mode = DMA_NORMAL;
    hdma_mem2mem.Init.Priority = DMA_PRIORITY_LOW;

    HAL_DMA_Init(&hdma_mem2mem);

    HAL_DMA_Start(&hdma_mem2mem,
                  (uint32_t)font_data_in_flash,
                  (uint32_t)font_buffer_in_sram,
                  2048);

    // 等待完成(或注册回调)
    while (HAL_DMA_PollForTransfer(&hdma_mem2mem, HAL_DMA_FULL_TRANSFER, 100) != HAL_OK);
}

虽然这类操作不如外设DMA常用,但在固件升级、资源动态加载等场景中非常有用。

📌 特别提醒:Cortex-M7/M33等带缓存的芯片要注意 缓存一致性问题

比如从Flash复制到SRAM后,如果目标区域在DCache中有缓存,必须手动无效化:

SCB_InvalidateDCache_by_address((uint32_t*)font_buffer_in_sram, 2048);

否则你可能会发现:“明明写了数据,怎么读出来还是旧的?”——这就是缓存惹的祸。


如何选择合适的缓冲策略?

DMA虽好,但缓冲区设计不当也会带来新问题。

下面是几种常见模式及其适用场景:

✅ 固定大小缓冲 + 传输完成中断

最基础的方式,适合稳定速率采集。

优点:逻辑清晰,易于调试
缺点:中断频率固定,无法适应突发流量

uint16_t buf[64];
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)buf, 64);
// 在 ConvCpltCallback 中处理

✅ 半满/全满双中断(Half-Transfer & Full-Transfer)

利用DMA的HT(Half Transfer)和TC(Transfer Complete)两个事件。

相当于把缓冲区切成两半:
- 前半写满 → HT中断 → CPU处理前半
- 后半写满 → TC中断 → CPU处理后半

实现流水线式处理,延迟更低。

// 启用两种中断
__HAL_DMA_ENABLE_IT(&hdma_adc1, DMA_IT_HT | DMA_IT_TC);

// 分别处理
void HAL_ADC_HalfConvCpltCallback() { /* 处理前半 */ }
void HAL_ADC_ConvCpltCallback()     { /* 处理后半 */ }

✅ 双缓冲 + 缓冲切换中断

适用于极高实时性需求,如音频流、运动控制。

特点:DMA始终在写,CPU在另一个缓冲上安全读取,绝不冲突。

配合RTOS还能做到任务间解耦。


常见坑点 & 调试技巧

DMA看似简单,但一旦出问题往往很难排查。以下是一些血泪经验总结:

❌ 坑1:缓冲区放在栈上

错误示范:

void start_dma(void) {
    uint16_t local_buf[4];  // 栈变量!
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)local_buf, 4);
}

函数退出后,栈空间可能被复用,DMA还在往里面写……后果不堪设想。

✅ 正确做法:全局静态变量 or static 修饰 or 动态分配(配合RTOS)


❌ 坑2:忘记使能DMA时钟

__HAL_RCC_DMA2_CLK_ENABLE(); // 必须加!否则DMA不工作

尤其是新手容易忽略这点,导致“代码没错却没反应”。


❌ 坑3:DMA通道映射错误

不同STM32型号,外设对应的DMA通道不一样!

比如:
- STM32F407:ADC1 → DMA2_Stream0_Channel0
- STM32F103:ADC1 → DMA1_Channel1

务必查阅《Reference Manual》中的“DMA request mapping”表格。

🔍 推荐做法:打开CubeMX,勾选DMA,自动生成正确配置。


❌ 坑4:缓存未同步(Cache Coherency)

再次强调:M7/M4F等带缓存的芯片,DMA写入SRAM后,CPU可能读到的是缓存旧值!

解决方法:
- 写操作后调用 SCB_InvalidateDCache_by_address()
- 或者将DMA缓冲区定义在Non-cacheable区域(通过链接脚本或MPU配置)


🛠️ 调试技巧三连击:

  1. Memory Watch + Breakpoint
    在IDE中观察缓冲区变化,设置中断断点看是否如期触发。

  2. Logic Analyzer 抓信号
    用LA接ADC_BUSY、USART_TX等引脚,验证传输时序是否正常。

  3. 开启DMA错误中断
    c __HAL_DMA_ENABLE_IT(&hdma_adc1, DMA_IT_TE); // 使能传输错误中断
    一旦发生地址错误、传输失败等异常,立即捕获。


设计哲学:什么时候该用DMA?

说了这么多技术细节,我们回到最初的问题:

我到底该不该上DMA?

这里给你一套决策框架:

✔️ 上DMA的典型场景:

  • ✅ 数据量 > 1KB/s
  • ✅ 传输频率 > 100Hz
  • ✅ 对实时性敏感(如音频、电机控制)
  • ✅ 需要降低功耗(配合Sleep/Stop模式)
  • ✅ 多任务系统中希望释放CPU资源

❌ 可不用DMA的情况:

  • ❌ 单次传输几个字节(如发个”OK\r\n”)
  • ❌ 调试阶段快速验证功能
  • ❌ 硬件资源紧张(DMA通道已被占满)

记住一句话: DMA不是万能药,但它是专业系统的标配

就像汽车里的自动变速箱——低速挪车用手动挡也行,但跑长途没有自动挡简直反人类。


最后一点思考:DMA教会我们的事

DMA的存在本身就在告诉我们一件事:

🌟 最好的系统,是让硬件尽可能自治

它不只是一项技术,更是一种设计思维。

当你学会把“数据搬运”这种重复劳动交给专用硬件,你才有精力去解决真正有价值的问题:
- 如何优化控制算法?
- 如何提升用户体验?
- 如何延长电池寿命?

这才是嵌入式开发从“能跑”到“好用”的跃迁之路。

所以,下次你在写 for 循环拷贝数组的时候,不妨停下来问一句:

“这事,能不能让DMA来干?”

也许答案就是你迈向高性能系统的第一步。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值