马鞍山市网站建设_网站建设公司_百度智能云_seo优化
2025/12/25 1:22:28 网站建设 项目流程

STM32中的DMA:如何让CPU“躺平”,数据自己跑?

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

  • 用ADC采集传感器数据,每毫秒一次,结果发现CPU一半时间都在读寄存器;
  • 串口发日志卡顿,一查发现是printf占用了主循环;
  • 想做音频播放或波形输出,但数据流太大,中断频繁到系统崩溃。

如果你点头了——恭喜,你已经踩进了嵌入式开发的“高带宽陷阱”。而破局的关键,就藏在STM32里那个被很多人忽略的外设:DMA


为什么我们需要DMA?

先说个扎心事实:CPU不是干搬运工活的。

它的真正职责是决策、计算和调度。可现实中,我们却常常让它去做最枯燥的事——一个字节一个字节地搬数据。

比如通过USART发送一串字符串:

for (int i = 0; i < len; str++) { while (!(USART1->SR & USART_SR_TXE)); // 等待发送寄存器空 USART1->DR = *str; // 写入数据 }

这段代码看似简单,实则让CPU全程“盯梢”硬件状态。如果要发1KB的数据?那CPU就得忙几百微秒,期间啥也干不了。

而DMA的作用,就是把这个苦力活交给专用硬件模块来完成。你只管说:“我要把这块内存的数据送到USART,完成后叫我一声。”然后转身去处理更重要的事,甚至进入睡眠模式省电。

一句话总结:DMA = 数据搬运外包公司,专治CPU过劳症。


DMA到底怎么工作的?从“请求—通道—流”讲起

STM32的DMA控制器不像你想象中那么“智能”,它更像是一个听指令办事的流水线工人。整个过程可以拆解为四个关键环节:

1. 谁发起任务?——外设的DMA请求

当某个外设(如ADC转换完成、USART准备接收)时,它会向DMA控制器发出一个信号:“我需要数据!”这个信号叫做DMA Request(请求)

例如:
- ADC完成一次采样 → 触发DMA请求
- USART的TDR寄存器变空 → 请求下一笔数据

这些请求不会直接找CPU,而是连到DMA控制器的输入端口上。

2. 谁来执行任务?——DMA通道与流

以STM32F4系列为例,芯片有两个DMA控制器(DMA1和DMA2),每个控制器有多个Stream(流),每个流又可绑定不同的Channel(通道)

你可以这样理解:
-Stream是一条独立的数据搬运通道(类似高速公路车道)
-Channel决定这条流服务哪个外设(比如选了Channel=4,就表示这条路专供USART1使用)

⚠️ 注意:不同外设可能共享同一个通道编号,但不能同时激活。你需要查参考手册确认映射关系。

3. 搬什么?怎么搬?——三大传输参数

每次启动DMA前,必须告诉它三件事:

参数说明
源地址数据从哪来?比如&ADC1->DR
目标地址数据往哪送?比如你的缓冲数组rx_buffer
传输数量搬多少次?单位可以是字节、半字或字

此外还要设置:
- 地址是否自动递增(内存通常递增,外设寄存器不递增)
- 数据宽度(8/16/32位匹配外设需求)
- 传输方向(内存→外设?外设→内存?)

一旦配置好并启用,DMA就会接管总线控制权,逐拍完成数据拷贝,全程无需CPU插手。

4. 完成后通知我!——中断回调机制

虽然搬运过程全自动,但我们往往希望知道“什么时候搬完了”。

这时就可以开启DMA的中断功能。常见的标志包括:
- TCIF:传输完成(Transfer Complete Interrupt Flag)
- HTIF:半传输中断(Half Transfer)
- TEIF:传输错误

在中断服务函数中清除标志,并调用用户定义的回调函数即可实现事件响应。


实战教学:用DMA实现无阻塞串口发送

下面我们以最常见的应用场景为例:使用DMA通过USART发送日志信息

第一步:打开相关时钟

任何外设操作之前,先得供电。

// 开启DMA2和USART1时钟 RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN; RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

第二步:配置DMA参数(HAL库方式)

这里推荐初学者使用HAL库简化流程:

DMA_HandleTypeDef hdma_tx; hdma_tx.Instance = DMA2_Stream7; // 使用DMA2的Stream7 hdma_tx.Init.Channel = DMA_CHANNEL_4; // 对应USART1_TX hdma_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;// 内存→外设 hdma_tx.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不变 hdma_tx.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增 hdma_tx.Init.PeriphDataAlignment = DMA_MDATAALIGN_BYTE; hdma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_tx.Init.Mode = DMA_NORMAL; // 单次模式 hdma_tx.Init.Priority = DMA_PRIORITY_LOW; hdma_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; if (HAL_DMA_Init(&hdma_tx) != HAL_OK) { Error_Handler(); }

第三步:关联DMA与UART句柄

为了让HAL库知道该用哪个DMA实例,需要用宏连接:

__HAL_LINKDMA(&huart1, hdmatx, hdma_tx);

这行代码的意思是:“以后huart1的发送任务,交给hdma_tx去办。”

第四步:启动DMA传输

有两种方式可以选择:

方法一:一键启动(推荐新手)
uint8_t msg[] = "Hello from DMA!\r\n"; HAL_UART_Transmit_DMA(&huart1, msg, sizeof(msg));

从此刻起,CPU就可以继续执行其他任务,甚至进入低功耗模式,而DMA会在后台默默把数据推给USART。

方法二:手动寄存器操作(适合想深入底层的同学)
DMA2_Stream7->PAR = (uint32_t)&(USART1->DR); // 目标地址:USART数据寄存器 DMA2_Stream7->M0AR = (uint32_t)msg; // 源地址:内存缓冲区 DMA2_Stream7->NDTR = sizeof(msg); // 传输字节数 // 配置控制寄存器 DMA2_Stream7->CR = (0 << 6) | // 清方向位:内存→外设 DMA_SxCR_MINC | // 内存递增 DMA_SxCR_PSIZE_0 | // 外设大小:8位 DMA_SxCR_MSIZE_0 | // 内存大小:8位 DMA_SxCR_EN; // 启动流! // 别忘了开启USART的DMA使能位 USART1->CR3 |= USART_CR3_DMAT;

看到没?真正的“启动”只需要写几个寄存器,剩下的全由硬件搞定。

第五步:传输完成怎么办?加个回调!

你肯定想知道“发完了吗?”所以我们要注册一个回调函数:

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 可在此处启动下一轮发送,或点亮LED提示完成 HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } }

只要你在工程中定义了这个弱函数,HAL库就会自动调用它。


更高级玩法:ADC多通道循环采集 + DMA

这才是DMA的杀手级应用之一。

设想你要同时采集温度、湿度、光照三个传感器信号,频率要求1kHz。传统做法是轮流启动ADC+等待EOC中断,效率极低且容易漏采。

而用DMA,方案如下:

配置要点:

  1. ADC设置为连续扫描模式
  2. 启用ADC的DMA请求
  3. DMA设为循环模式(Circular Mode)
  4. 目标缓冲区为一个固定长度数组
#define ADC_BUF_LEN 3 uint16_t adc_results[ADC_BUF_LEN]; // 配置DMA为循环模式 hdma_adc.Init.Mode = DMA_CIRCULAR; // 关键!自动重复填充

一旦启动,会发生什么?

  • ADC按顺序采集CH0、CH1、CH2
  • 每次转换结束,DMA自动将ADC->DR中的值搬到adc_results[i]
  • 当最后一个通道完成,DMA回到第一个位置重新覆盖
  • 整个过程无需中断,CPU完全解放!

你可以每隔一段时间去读取adc_results进行滤波处理,或者结合DMA双缓冲模式实现无缝切换。


新手必看:那些年我们都踩过的坑

DMA虽强,但也容易出问题。以下是几个典型“翻车现场”及应对策略:

❌ 坑点1:程序跑飞,进BusFault?

原因很可能是地址未对齐

如果你设置了32位传输模式,但源地址是奇数地址(比如0x20000001),就会触发总线错误。

✅ 解决方法:

uint8_t buffer[100] __attribute__((aligned(4)));

强制让编译器将其分配在4字节对齐的位置。


❌ 坑点2:DMA写入了数据,但我读出来是旧的?

尤其是在STM32F4/F7/H7这类带D-Cache的芯片上常见。

原因是:DMA写的是实际内存,但CPU从缓存里读的是旧副本。

✅ 解决方法:

SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, size);

在CPU访问DMA写入的内存前,先清无效化缓存。


❌ 坑点3:DMA传着传着突然停了?

检查是否开启了全局中断。某些HAL库API内部依赖中断来清理状态。

另外,确保没有其他高优先级DMA请求抢占了总线资源。


✅ 秘籍:调试DMA的小技巧

  1. 观察NDTR寄存器
    在IDE调试模式下查看DMAx_Streamy->NDTR,它的值应该随着传输逐步减小。

  2. 用逻辑分析仪抓信号
    探测DMA BusyDMA Request引脚,验证传输节奏是否符合预期。

  3. 打印ITM日志
    使用SWO输出关键事件时间戳,避免因断点暂停导致DMA超时。


设计哲学:让合适的人做合适的事

掌握DMA的意义,远不止于学会一个外设配置。

它背后体现的是一种系统级思维转变:

角色应该做的事
CPU决策、算法、协议解析、异常处理
DMA数据搬运、批量传输、定时触发

当你开始思考“这部分能不能甩给DMA”,你就离高手不远了。

举几个实战思路:
-低功耗采集系统:RTC定时唤醒 → ADC+DMA采集 → 立即进入STOP2模式
-音频播放:I2S + DMA + 双缓冲 → 实现流畅音乐输出
-图像传输:LCD驱动中用DMA推送像素数据,释放CPU渲染UI


结语:从“搬砖”到“架构”

DMA不是一个炫技工具,而是构建高效嵌入式系统的基石。

对于刚入门的开发者,建议按照以下路径渐进学习:

  1. 先用STM32CubeMX生成DMA配置代码,观察其行为
  2. 尝试修改为纯HAL库调用,理解每一行的作用
  3. 最后尝试手动操作寄存器,彻底吃透底层机制

你会发现,原本让你头疼的性能瓶颈,在DMA面前变得迎刃而解。

正如一位资深工程师所说:“优秀的嵌入式系统,不是看CPU多快,而是看它有多‘懒’。”

现在,轮到你让STM32的CPU真正“躺平”了。

如果你在实践过程中遇到了具体问题,欢迎留言交流,我们一起排查DMA的每一个细节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询