news 2026/2/15 9:15:51

从零开始学DMA:串口数据传输配置实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零开始学DMA:串口数据传输配置实践

串口DMA实战:如何让MCU在后台“默默”收数据,CPU却几乎不干活?

你有没有遇到过这种情况:
你的STM32正在通过串口接收GPS数据或蓝牙模块的报文,波特率一上115200,系统就“卡顿”了?调试发现,中断服务函数每87微秒就被触发一次——没错,就是每个字节来一次!CPU疲于奔命地搬数据,根本没空干正事。

这不只是性能问题,更是架构隐患。当主循环被频繁打断,实时性下降、任务延迟、甚至丢包都可能发生。

那有没有办法让串口自己把数据存好,等一整包来了再叫你?有——用DMA(Direct Memory Access) + IDLE线检测

今天我们就从零开始,手把手带你配置串口DMA接收,彻底解放CPU,实现高效、稳定、低功耗的串行通信。


为什么传统中断方式扛不住高波特率?

先看个现实场景:

  • 波特率:115200 bps
  • 每帧10位(起始+8数据+停止)
  • 每秒传输约 11,520 字节
  • 平均每87μs就有一个字节到达

如果使用普通中断方式(UART_IRQHandler中读DR寄存器),意味着:

✅ 每隔不到90微秒,CPU就要停下当前工作,跳转到中断服务程序,保存上下文、读寄存器、写缓冲、更新指针……然后再返回。

这种“每字节一中断”的模式,在高速通信下会迅速吃掉大量CPU资源。更糟糕的是,一旦有更高优先级的中断抢占,还可能造成溢出错误(ORE),直接丢数据!

所以,我们迫切需要一种机制:
👉 让硬件自动完成“从USART_DR寄存器拿数据 → 存进内存”的过程,
👉 而CPU只在“一整段数据收完”时才介入处理。

这就是DMA 的使命


DMA 是什么?它怎么帮我们“偷懒”?

简单说,DMA 就是一个独立的数据搬运工

它可以在没有CPU参与的情况下,把数据从一个地方搬到另一个地方。比如:

  • 外设 → 内存(如 USART_RX → RAM 缓冲区)
  • 内存 → 外设(如 发送缓冲区 → SPI_TDR)
  • 内存 → 内存(类似memcpy,但更快)

在 STM32 这类 MCU 中,DMA 控制器是独立外设,有自己的通道、地址管理、传输计数和中断逻辑。

它是怎么工作的?

想象一下流水线打包车间:

  1. 提前布置好任务:告诉 DMA,“你要从哪个入口取货(源地址),送到哪个货架(目标地址),一共搬多少件(长度)”
  2. 触发信号来了就开工:当 USART 收到一个字节,会发出一个“请帮我搬走”的请求(DMA Request)
  3. DMA 接管总线,自己动手搬:它直接访问外设寄存器和内存,不需要 CPU 插手
  4. 搬完了打个招呼:可以通过中断通知 CPU:“我这边满了/一半了/出错了”

整个过程,CPU 只需在开始前设置参数,结束后处理数据,中间完全不用管。


关键优势一览:DMA vs 中断

维度中断方式DMA 方式
CPU 占用高(每次数据都要进 ISR)极低(仅启动和结束介入)
最大吞吐受限于中断响应速度接近物理极限
实时性易受其他中断干扰更稳定可控
缓冲管理手动维护环形缓冲支持自动循环、双缓冲
系统可扩展性多外设易引发中断风暴多通道并行,互不影响

别小看这些差异。在一个运行 FreeRTOS 的系统中,频繁中断会导致调度抖动;而在电池供电设备中,CPU 长时间无法休眠会大幅缩短续航。


如何配置串口DMA?一步步来

下面我们以STM32F4xx + HAL 库为例,演示如何为USART2_RX配置 DMA 接收。

第一步:初始化 UART 基础功能

UART_HandleTypeDef huart2; DMA_HandleTypeDef hdma_usart2_rx; uint8_t rx_buffer[256]; // DMA 接收缓冲区 void UART_DMA_Init(void) { // 配置 UART2 参数 huart2.Instance = USART2; huart2.Init.BaudRate = 115200; huart2.Init.WordLength = UART_WORDLENGTH_8B; huart2.Init.StopBits = UART_STOPBITS_1; huart2.Init.Parity = UART_PARITY_NONE; huart2.Init.Mode = UART_MODE_RX; // 只启用接收 huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; HAL_UART_Init(&huart2); }

这部分很常规,设定波特率、数据格式等。注意这里只开了 RX 模式,因为我们聚焦接收优化。


第二步:配置 DMA 通道

// 开启 DMA1 时钟 __HAL_RCC_DMA1_CLK_ENABLE(); // 配置 DMA 句柄 hdma_usart2_rx.Instance = DMA1_Stream5; // STM32F4: USART2_RX 映射到 DMA1 Stream5 hdma_usart2_rx.Init.Channel = DMA_CHANNEL_4; // 通道选择 hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; // 外设→内存 hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不变(始终读 DR) hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增 hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // 字节对齐 hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart2_rx.Init.Mode = DMA_CIRCULAR; // 启用循环模式!关键点 hdma_usart2_rx.Init.Priority = DMA_PRIORITY_HIGH; // 优先级设高些 hdma_usart2_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; // FIFO 不开启(F4 默认关) HAL_DMA_Init(&hdma_usart2_rx);

重点解释几个关键配置:

  • DMA_PERIPH_TO_MEMORY:数据流向是从外设(USART_DR)到内存(rx_buffer)
  • PeriphInc = DISABLE:因为每次都是从同一个寄存器DR读数据,地址固定
  • MemInc = ENABLE:每写一个字节,缓冲区指针自动加一
  • Mode = DMA_CIRCULAR循环模式!这是实现无限接收的关键。缓冲区满后不会停止,而是重新从头填
  • Priority = HIGH:确保在多请求时能及时响应,避免丢数据

第三步:关联 DMA 到 UART,并启动接收

// 将 DMA 句柄链接到 UART 句柄 __HAL_LINKDMA(&huart2, hdmarx, hdma_usart2_rx); // 启动 DMA 接收 HAL_UART_Receive_DMA(&huart2, rx_buffer, sizeof(rx_buffer));

这两行看似简单,实则至关重要:

  • __HAL_LINKDMA()是宏操作,把huart2.hdmarx指向我们刚配置好的hdma_usart2_rx
  • HAL_UART_Receive_DMA()会:
  • 清除相关标志位
  • 设置传输数据量(NDTR)
  • 启动 DMA 流
  • 使能 USART 的DMAR位,允许其发出 DMA 请求

从此以后,只要收到数据,DMA 就会自动把它塞进rx_buffer,无需任何中断!


怎么知道“一包数据已经收完”?靠 IDLE 中断!

虽然 DMA 能持续接收,但我们不能等到缓冲区满了才处理——大多数协议是不定长的(比如 Modbus RTU、自定义 JSON 包)。

这时候就需要一个“帧结束”信号。最佳方案就是:空闲线检测(IDLE Line Detection)

什么是 IDLE 中断?

当串行线上连续一段时间没有新数据到来(通常是 3.5~10 个字符时间),USART 会产生一个 IDLE 标志。这个特性正好用来识别“一帧结束了”。

举个例子:

设备发送了一串 Modbus 报文,发完后线路静默。此时触发 IDLE 中断,我们可以认为这一帧完整收到了。

如何启用 IDLE 中断?

// 在初始化 UART 后启用 IDLE 中断 __HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);

然后在中断服务程序中处理:

void USART2_IRQHandler(void) { HAL_UART_IRQHandler(&huart2); // 调用 HAL 中断处理框架 } // 实际处理放在回调函数中 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 此回调不会被调用 —— 因为我们用的是循环模式,永远不会“完成” } // 但这个回调会被 IDLE 触发! void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { // 获取当前已接收的数据长度 uint16_t remain = __HAL_DMA_GET_COUNTER(&hdma_usart2_rx); uint16_t received = sizeof(rx_buffer) - remain; // 提取有效数据进行解析 ProcessReceivedFrame(rx_buffer, received); // 清除 IDLE 标志(必须手动清) __HAL_UART_CLEAR_IDLEFLAG(&huart2); // 可选:重置 NDTR(如果是非循环模式才需要) // HAL_UART_Receive_DMA(&huart2, rx_buffer, sizeof(rx_buffer)); } }

⚠️ 注意:HAL_UART_IdleCpltCallback并不是标准 HAL 函数,你需要自己定义并在stm32f4xx_it.cUSART2_IRQHandler中判断是否为 IDLE 触发。

或者更稳妥的做法是在中断服务函数中手动判断:

void USART2_IRQHandler(void) { if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(&huart2); // 先清标志 // 停止 DMA 以防止边读边改 HAL_UART_DMAStop(&huart2); uint16_t remain = hdma_usart2_rx.Instance->NDTR; uint16_t len = sizeof(rx_buffer) - remain; if (len > 0) { ProcessReceivedFrame(rx_buffer, len); } // 重启 DMA(保持循环接收) HAL_UART_Receive_DMA(&huart2, rx_buffer, sizeof(rx_buffer)); } else { HAL_UART_IRQHandler(&huart2); // 其他情况交给 HAL 处理 } }

这样就能精准捕获每一帧的结尾,且无需依赖超时定时器,效率更高、响应更快。


如何防止缓冲区覆盖?三大防护策略

循环模式虽好,但也带来一个问题:如果 CPU 处理不及时,DMA 可能把还没处理的数据覆盖掉

以下是三种实用解决方案:

✅ 方案一:启用半传输中断(Half Transfer Interrupt)

将缓冲区分成前后两半。当填到一半时,触发 HT 中断,提醒 CPU “前半区快满了,请尽快处理”。

// 初始化时开启 HT 中断 hdma_usart2_rx.Init.Mode = DMA_CIRCULAR; hdma_usart2_rx.Init.Priority = DMA_PRIORITY_HIGH; // ... HAL_DMA_Init(&hdma_usart2_rx); // 启用中断 HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 1, 0); HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn); // 启动传输前开启中断 __HAL_DMA_ENABLE_IT(&hdma_usart2_rx, DMA_IT_HT | DMA_IT_TC);

然后实现中断处理:

void DMA1_Stream5_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_usart2_rx); } // HAL 回调 void HAL_DMA_HalfTransferCplt(DMA_HandleTypeDef *hdma) { if (hdma == &hdma_usart2_rx) { // 前半区已满,处理前128字节 ProcessReceivedData(rx_buffer, 128); } } void HAL_DMA_TransferComplete(DMA_HandleTypeDef *hdma) { if (hdma == &hdma_usart2_rx) { // 后半区已满,处理后128字节 ProcessReceivedData(rx_buffer + 128, 128); } }

这样相当于实现了“伪双缓冲”,适合中等速率数据流。


✅ 方案二:使用双缓冲模式(Double Buffer Mode)

部分高端芯片(如 STM32H7/F7/F4)支持真正的双缓冲。DMA 在两个缓冲区间自动切换,每次填满一块就切换并产生中断。

配置方式略有不同:

hdma_usart2_rx.Init.Mode = DMA_DOUBLE_BUFFER_M; // 而不是 CIRCULAR // 还需额外指定第二个缓冲区 uint8_t rx_buffer_a[256], rx_buffer_b[256]; hdma_usart2_rx.XferCpltCallback = DMA_DualBufferComplete;

优点非常明显:
- 一块被 DMA 写入时,另一块可以安全被 CPU 读取
- 实现真正意义上的“零等待、无锁访问”

缺点是占用双倍内存,且 HAL 库支持不够直观,需深入底层寄存器控制。


✅ 方案三:结合 RTOS 机制(推荐用于复杂系统)

如果你用了 FreeRTOS 或其他 OS,可以用队列/信号量解耦:

QueueHandle_t uart_queue; void HAL_UART_IdleCpltCallback(...) { xQueueSendFromISR(uart_queue, &frame_info, NULL); } // 单独任务处理 void UartProcessTask(void *pv) { FrameInfo info; while (1) { if (xQueueReceive(uart_queue, &info, portMAX_DELAY)) { parse_and_handle(info.data, info.len); } } }

既保证实时性,又避免在中断里做耗时操作。


工程实践建议:这些细节决定成败

项目建议
缓冲区大小至少是最大帧长的 2 倍,推荐 256~1024 字节
内存对齐缓冲区起始地址应 4 字节对齐,避免 DMA 总线错误
DMA 优先级设为 High 或 Very High,避免被其他 DMA 阻塞
错误处理监听DMA_FLAG_TE(传输错误),必要时复位通道
调试技巧暂时关闭 DMA,观察原始中断行为,定位问题源头
低功耗场景CPU 可进入 Sleep/WFI,仅靠 DMA 和中断唤醒

此外,强烈建议在关键节点添加日志或 LED 指示灯,便于验证流程是否正常。


结语:掌握 DMA,才算真正入门嵌入式系统设计

你看,原本每87微秒就要打扰一次CPU的任务,现在变成了“安静地把数据存好,等你有空再来取”。
这不是简单的代码替换,而是一种系统思维的升级

当你学会把“数据搬运”这类重复劳动交给硬件协处理器(DMA),你才有精力去关注更重要的事:协议解析、状态机设计、功耗优化、系统稳定性……

未来随着 RISC-V、自研内核的发展,轻量级 DMA 协处理器、链表式描述符(LLI)、动态通道映射等技术也会逐步普及。但无论架构如何演进,理解“外设与内存之间的自主传输机制”,永远是嵌入式工程师的核心能力之一。

如果你正在做一个需要长时间接收串口数据的项目,不妨试试今天这套“DMA + IDLE”组合拳。你会发现,原来 MCU 可以这么“从容”。

如果你在实现过程中遇到了具体问题(比如 NDTR 读出来不对、IDLE 不触发、DMA 死掉),欢迎留言讨论,我们一起排查。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/15 2:45:36

Res-Downloader技术深度解析:跨平台资源嗅探的实现与实践

Res-Downloader技术深度解析:跨平台资源嗅探的实现与实践 【免费下载链接】res-downloader 资源下载器、网络资源嗅探,支持微信视频号下载、网页抖音无水印下载、网页快手无水印视频下载、酷狗音乐下载等网络资源拦截下载! 项目地址: https://gitcode.…

作者头像 李华
网站建设 2026/2/9 18:23:37

高效网页剪辑方案:5步掌握离线保存技巧

高效网页剪辑方案:5步掌握离线保存技巧 【免费下载链接】maoxian-web-clipper A web extension to clip information from web page. Save it to your local machine to avoid information invalidation. Not bored registration, Not charged. 项目地址: https:/…

作者头像 李华
网站建设 2026/2/3 14:33:01

UI-TARS桌面版:基于视觉语言模型的智能GUI助手终极指南

UI-TARS桌面版:基于视觉语言模型的智能GUI助手终极指南 【免费下载链接】UI-TARS-desktop A GUI Agent application based on UI-TARS(Vision-Lanuage Model) that allows you to control your computer using natural language. 项目地址: https://gitcode.com/G…

作者头像 李华
网站建设 2026/2/6 0:51:38

终极音源配置指南:洛雪音乐实现全网高品质音乐免费畅听

终极音源配置指南:洛雪音乐实现全网高品质音乐免费畅听 【免费下载链接】lxmusic- lxmusic(洛雪音乐)全网最新最全音源 项目地址: https://gitcode.com/gh_mirrors/lx/lxmusic- 还在为音乐会员费用而烦恼吗?洛雪音乐音源项目为你带来全新的免费听…

作者头像 李华
网站建设 2026/2/13 1:35:11

跨平台资源下载神器:快速获取网络资源的终极指南

跨平台资源下载神器:快速获取网络资源的终极指南 【免费下载链接】res-downloader 资源下载器、网络资源嗅探,支持微信视频号下载、网页抖音无水印下载、网页快手无水印视频下载、酷狗音乐下载等网络资源拦截下载! 项目地址: https://gitcode.com/GitH…

作者头像 李华
网站建设 2026/2/8 13:13:07

从零部署WMT25优胜翻译模型|HY-MT1.5-7B镜像使用全攻略

从零部署WMT25优胜翻译模型|HY-MT1.5-7B镜像使用全攻略 随着多语言交流需求的不断增长,高质量、低延迟的翻译模型成为跨语言应用的核心组件。在WMT25赛事中脱颖而出的HY-MT1.5-7B模型,凭借其卓越的语言理解与生成能力,已成为当前…

作者头像 李华