在实际嵌入式开发中,尤其是使用 STM32、GD32 这类微控制器时,当数据量稍大或者对实时性有要求,直接使用 CPU 搬运数据就会成为性能瓶颈。这时,开发者手册和例程里反复出现的“DMA”就成了必须掌握的技术。但对于刚接触底层开发的新手来说,DMA 相关的术语——通道、传输模式、中断、外设请求——常常让人一头雾水,配置起来也容易出错,比如数据传丢了、中断进不去,或者根本不知道传输什么时候完成。
这篇文章的目标,就是彻底讲清楚 DMA 到底是什么,它解决了什么问题,以及如何在一个具体的场景(比如串口收发)中正确地配置和使用它。我们会从 DMA 最核心的“搬运工”比喻讲起,然后深入到它的工作机制,最后通过一个 STM32 的 USART DMA 收发实例,手把手带你完成配置、编写代码、验证结果,并梳理出最常见的几个坑和排查路径。读完本文,你将能独立完成一个基于 DMA 的串口不定长数据收发项目,并理解其背后的原理。
1. 先彻底搞懂 DMA:为什么 CPU 需要它来“打杂”
1.1 DMA 解决的核心矛盾:CPU 时间 vs. 数据搬运
在没有 DMA 的系统中,如果外设(比如串口、ADC、SPI)需要与内存交换数据,整个过程必须由 CPU 亲自参与。CPU 需要执行类似这样的流程:
- 等待外设数据准备好(或准备好发送数据)的标志位。
- 从外设数据寄存器读取一个字节到 CPU 寄存器。
- 再将这个字节从 CPU 寄存器写入到内存的目标地址。
- 更新内存地址指针和剩余数据计数。
- 判断是否传输完成,若未完成则回到步骤1。
这个过程被称为“程序控制传输”或“轮询”。它的致命缺点是CPU被大量简单的数据搬运工作所占用,无法执行更有价值的计算或逻辑任务。对于高速外设(如摄像头、高速ADC)或大数据量传输(如文件读写、网络包),CPU 的利用率会急剧下降,系统整体性能受损。
DMA(Direct Memory Access,直接存储器访问)就是为了解放 CPU 而设计的专用硬件模块。你可以把它想象成一个智能且独立的“数据搬运工”。它的工作流程是:
- CPU 预先告诉 DMA 搬运工:源地址在哪里(如外设数据寄存器)、目标地址在哪里(如内存中的数组)、要搬多少数据、怎么搬(一次搬多少、地址是否递增)。
- CPU 下达“开始搬运”指令后,就可以去处理其他任务了。
- 当外设准备好数据(或准备好接收数据)时,它会向 DMA 控制器发出一个“请求”信号。
- DMA 控制器收到请求,在系统总线上“借用”一个周期,直接完成从源到目标的数据传输,完全绕过 CPU。
- 传输完成后,DMA 控制器可以通过中断等方式通知 CPU:“老板,你交代的搬运任务完成了”。
这样,CPU 只在传输开始和结束时参与,中间的搬运过程被完全卸载,实现了“零拷贝”意义上的高效数据传输。
1.2 DMA 的关键组件与工作流程
理解 DMA,需要弄清楚几个核心概念,它们通常对应着配置寄存器时的选项:
- 通道(Channel):DMA 控制器通常有多个独立的通道,每个通道可以服务于一个特定的外设(如 USART1_TX, ADC1, SPI2_RX 等)。通道之间优先级可配置,防止冲突。
- 仲裁器(Arbiter):当多个通道同时发出传输请求时,仲裁器根据预设的优先级(软件优先级或硬件固定优先级)决定哪个通道先使用总线。
- 外设请求(Peripheral Request):传输的发起者。通常是外设认为“我准备好了”时发出的硬件信号。例如,USART 发送数据寄存器空(TXE)时,会请求 DMA 送下一个数据来;USART 收到数据(RXNE)时,会请求 DMA 把数据从寄存器搬走。
- 传输模式:
- 外设到内存(Peripheral-to-Memory):如 ADC 采集数据到内存数组。
- 内存到外设(Memory-to-Peripheral):如内存中的字符串通过 USART 发送出去。
- 内存到内存(Memory-to-Memory):DMA 控制器在两个内存区域间搬运数据,这是纯 DMA 功能,无需外设请求。
- 数据流(Stream)/ 控制器(Controller):在一些高级的 DMA 架构(如 STM32F4/F7/H7 的 DMA2D)中,概念更复杂,但本质仍是管理传输的实体。
- 中断(Interrupt):DMA 传输完成、传输一半或发生错误时,可以产生中断,通知 CPU 进行后续处理(如处理接收到的数据包,或准备下一批待发送数据)。
1.3 为什么串口通信特别适合使用 DMA?
串口(USART/UART)通信是嵌入式中最常见的外设之一。在以下场景,使用 DMA 优势明显:
- 不定长数据接收:结合串口空闲中断(IDLE),可以在收到一帧完整数据后,由 DMA 传输完成中断或空闲中断通知 CPU,CPU 只需处理整包数据,无需逐个字节中断。
- 大数据量或高速率传输:例如通过串口发送大量日志或固件数据,使用 DMA 可以避免因 CPU 处理不及时造成的数据丢失或发送延迟。
- 降低 CPU 负载:在 RTOS 或多任务系统中,让 DMA 处理串口数据搬运,可以释放 CPU 给其他高优先级任务。
2. 环境准备与项目目标设定
在开始写代码之前,必须把环境对齐。本文以 STM32F103C8T6(BluePill 核心板)为例,使用 STM32CubeMX 进行图形化配置,HAL 库进行开发。这套组合在学习和中小项目中非常普遍。
2.1 硬件与软件环境清单
| 项目 | 说明 | 备注 |
|---|---|---|
| 开发板 | STM32F103C8T6 最小系统板 | 其他 F1 系列或 STM32 系列原理类似 |
| IDE/工具链 | STM32CubeIDE 或 Keil MDK | 本文使用 STM32CubeIDE,它集成了 CubeMX 和 GCC 编译器 |
| 固件库 | STM32CubeF1 HAL 库 | 通过 CubeMX 自动集成 |
| 调试器 | ST-Link V2 或 DAPLink | 用于下载和调试 |
| 串口工具 | 任意串口助手(如 Putty, SecureCRT) | 用于发送和接收数据 |
2.2 本实例达成的目标
我们将实现一个经典的、实用的串口 DMA 应用:
- DMA 发送:CPU 将一段字符串(
Hello DMA!)的地址和长度交给 DMA,由 DMA 自动通过串口发送出去,发送完成后产生中断通知 CPU。 - DMA 接收(不定长):配置 DMA 循环接收模式到一个足够大的缓冲区。当串口线上有一段时间没有新数据(产生空闲中断)时,我们认为一帧数据接收完毕。在空闲中断中,计算出本次接收的数据长度,然后处理数据,并重置 DMA 接收以备下一帧。
这个组合避免了轮询发送占用 CPU,也解决了不定长接收的难题,是很多实际项目(如 Modbus、自定义串口协议)的基础。
3. 使用 STM32CubeMX 配置 DMA 与 USART
图形化配置能极大减少底层寄存器操作的错误。我们按步骤进行。
3.1 创建工程与时钟配置
- 打开 STM32CubeIDE,新建 STM32 项目,选择 MCU 型号
STM32F103C8Tx。 - 在Pinout & Configuration视图的System Core > RCC中,将High Speed Clock (HSE)设置为
Crystal/Ceramic Resonator。我们的开发板通常有 8MHz 外部晶振。 - 进入Clock Configuration标签页,进行时钟树配置。一个常见的配置是:HSE 输入 8MHz,经过 PLL 倍频到 72MHz 作为系统时钟(SYSCLK)。APB1 总线时钟(PCLK1)设为 36MHz,APB2 总线时钟(PCLK2)设为 72MHz。确保 USART1 所在的 APB2 总线时钟正确,因为它影响串口波特率计算。
3.2 配置 USART1
- 在Pinout视图,找到USART1。
- 将模式设置为Asynchronous(异步通信)。
- 此时,PA9 和 PA10 引脚会自动被配置为 USART1_TX 和 USART1_RX(这是默认复用功能)。
- 在Configuration标签页下,点击USART1进入参数设置:
- Baud Rate:
115200 - Word Length:
8 Bits - Parity:
None - Stop Bits:
1 - Over Sampling:
16 Samples - 其他保持默认。
- Baud Rate:
3.3 配置 DMA(核心步骤)
- 在Configuration标签页,选择DMA设置。
- 点击Add添加一个新的 DMA 通道。
- DMA Request: 选择
USART1_RX。这意味着我们将为 USART1 的接收功能分配一个 DMA 通道。 - Direction: 选择
Peripheral To Memory。数据从外设(USART1 数据寄存器)搬运到内存。 - Priority: 选择
Medium。根据系统需求调整。 - Mode: 选择
Circular(循环模式)。这是实现不定长接收的关键!在此模式下,DMA 接收数据到缓冲区末尾后,会自动回到缓冲区开头继续接收,永不停止。配合空闲中断,我们就能截取任意长度的一帧数据。
- DMA Request: 选择
- 再次点击Add,添加第二个 DMA 通道。
- DMA Request: 选择
USART1_TX。 - Direction: 选择
Memory To Peripheral。数据从内存搬运到外设。 - Priority:
Medium。 - Mode:
Normal(正常模式)。发送完指定长度数据后,DMA 传输停止,并产生传输完成中断。下次发送需要重新启动。
- DMA Request: 选择
注意:
Normal和Circular模式的选择是配置的关键点。发送通常用Normal,发完即停;接收为了持续监听,常用Circular。
- 分别对两个通道进行如下高级参数配置(点击已添加的通道行):
- Increment Address:
- 对于
USART1_RX(Peripheral To Memory):Peripheral不递增(地址固定为 USART1->DR 寄存器),Memory递增(这样数据才能依次存放到缓冲区数组里)。 - 对于
USART1_TX(Memory To Peripheral):Peripheral不递增,Memory递增。
- 对于
- Data Width:
Peripheral和Memory都设置为Byte(因为 USART 是 8 位数据)。如果外设是 16 位或 32 位(如 ADC),这里需要对应修改。
- Memory Burst / Peripheral Burst: 在 F1 系列保持默认
Single。
- Increment Address:
3.4 配置中断
- 在NVIC Settings标签页(DMA 配置页面或 System Core > NVIC 中),使能以下中断:
USART1 global interrupt:我们需要用到它的空闲中断(IDLE)。DMA1 channel4 global interrupt(对应 USART1_TX DMA):用于发送完成回调。DMA1 channel5 global interrupt(对应 USART1_RX DMA):可用于接收半满、全满等中断,本例中我们主要用空闲中断,可以暂时不使能 DMA 通道5中断以简化流程。
- 设置合适的优先级。对于实时性要求高的接收,可以给 USART1 中断更高的优先级。
3.5 生成工程代码
- 进入Project Manager标签页,设置项目名称、路径,选择 IDE 为
STM32CubeIDE。 - 在Code Generator部分,选择
Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral,这样代码更模块化。 - 点击GENERATE CODE,生成初始化代码并打开工程。
4. 编写代码:实现 DMA 发送与不定长接收
生成了基础框架后,我们需要在main.c和stm32f1xx_it.c中添加业务逻辑。
4.1 定义全局变量与缓冲区
在main.c的/* USER CODE BEGIN PV */区域定义变量:
/* Private variables ---------------------------------------------------------*/ #define RX_BUFFER_SIZE 256 // 接收缓冲区大小 uint8_t tx_buffer[] = "Hello DMA!\r\n"; // 待发送数据 uint8_t rx_buffer[RX_BUFFER_SIZE]; // DMA循环接收缓冲区 volatile uint16_t rx_len = 0; // 接收到的数据长度 volatile uint8_t rx_flag = 0; // 接收完成标志位rx_buffer:DMA 循环接收的目标缓冲区。必须足够大,以免在 CPU 处理上一包数据时,新数据覆盖了未处理的数据。rx_len和rx_flag:用于在中断服务程序(ISR)和主循环之间通信。使用volatile防止编译器优化。
4.2 启动 DMA 接收
在main函数的/* USER CODE BEGIN 2 */区域,启动 USART1 的 DMA 接收:
/* USER CODE BEGIN 2 */ // 启动串口DMA接收,数据将自动存入rx_buffer,循环模式永不停止 HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE); // 使能串口空闲中断(IDLE)—— 这是HAL库没有直接提供的功能 __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 发送测试字符串 HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer) - 1); // 减1是为了去掉字符串结尾的'\0' /* USER CODE END 2 */关键点:
HAL_UART_Receive_DMA启动了 DMA 循环接收。从此,串口收到的每一个字节都会由 DMA 自动存入rx_buffer,存满后回到开头继续存。__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE)是使能空闲中断的关键宏。CubeMX 生成的代码默认不会使能此中断,需要手动开启。空闲中断在串口数据线上超过一个字符传输时间没有新数据时触发,是判断一帧数据结束的理想方式。
4.3 处理串口空闲中断(核心逻辑)
我们需要修改中断服务程序。打开stm32f1xx_it.c,找到USART1_IRQHandler函数。
void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ uint32_t tmp_flag = 0; uint32_t tmp_it_source = 0; // 检查是否是空闲中断 tmp_flag = __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE); tmp_it_source = __HAL_UART_GET_IT_SOURCE(&huart1, UART_IT_IDLE); if((tmp_flag != RESET) && (tmp_it_source != RESET)) { // 清除空闲中断标志(重要!) __HAL_UART_CLEAR_IDLEFLAG(&huart1); // 暂时关闭DMA接收,以安全地计算接收到的数据长度 HAL_UART_DMAStop(&huart1); // 计算本次接收到的数据长度 // __HAL_DMA_GET_COUNTER 获取DMA通道剩余未传输的数据量 rx_len = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx); if(rx_len > 0) { // 设置标志位,通知主循环有数据待处理 rx_flag = 1; // 可以在这里直接处理数据,但为了中断快进快出,通常只设标志位。 // process_rx_data(rx_buffer, rx_len); } // 重新配置DMA接收缓冲区地址和长度,并启动DMA // 注意:因为之前是循环模式,停止后需要重新设置 huart1.hdmarx->Instance->CNDTR = RX_BUFFER_SIZE; // 重新设置传输数据量 huart1.hdmarx->Instance->CMAR = (uint32_t)rx_buffer; // 重新设置内存地址(可选,如果缓冲区不变可不设) __HAL_DMA_ENABLE(huart1.hdmarx); // 使能DMA通道 // 如果不重新调用 HAL_UART_Receive_DMA,需要手动清除一些HAL库状态 // 更稳妥的做法是:调用 HAL_UART_Receive_DMA 重新启动 // HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE); } /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(&huart1); /* USER CODE BEGIN USART1_IRQn 1 */ /* USER CODE END USART1_IRQn 1 */ }代码逻辑解释:
- 判断中断源:首先检查触发的中断是否是空闲中断(IDLE)。
- 清除标志:必须清除空闲中断标志位,否则会持续进入中断。
- 停止 DMA:在计算接收长度前停止 DMA,防止计算过程中 DMA 仍在修改缓冲区或计数器,导致数据不一致。
- 计算长度:DMA 传输计数器(CNDTR)存储着剩余要传输的字节数。用缓冲区总大小减去剩余数量,就得到了已经接收到的数据长度
rx_len。 - 通知主循环:设置
rx_flag = 1,让主循环知道有新数据包。 - 重启 DMA:这是最容易出错的一步。因为之前停止了 DMA,需要重新初始化 DMA 通道的传输数据量(CNDTR)和内存地址(CMAR),然后使能通道。更简单且不易出错的方法是直接再次调用
HAL_UART_Receive_DMA。
重要:在中断服务程序中,处理速度要快,避免执行耗时操作(如
printf)。所以这里只设置标志位,实际的数据处理(如解析协议、应答)应放到主循环中。
4.4 主循环处理接收数据
回到main.c的while (1)循环中,我们检查并处理接收完成标志。
/* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ if(rx_flag) { rx_flag = 0; // 清除标志 // 1. 处理接收到的数据 (rx_buffer 中前 rx_len 个字节) // 例如:回显接收到的数据 HAL_UART_Transmit_DMA(&huart1, rx_buffer, rx_len); // 2. 处理完后,可以清空缓冲区或直接覆盖(循环模式) // memset(rx_buffer, 0, rx_len); // 可选 // 注意:此时DMA接收已经在空闲中断中重启,无需额外操作 } // 其他任务... HAL_Delay(1); // 简单延时,实际项目中可能由RTOS调度 } /* USER CODE END 3 */4.5 处理 DMA 发送完成中断(可选)
如果需要在发送完成后执行特定操作(如关闭射频、切换状态),可以使用 DMA 发送完成中断。HAL 库为我们提供了回调函数机制。
在main.c中,重写发送完成回调函数:
/* USER CODE BEGIN 4 */ // DMA发送完成回调函数 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { // 发送完成,可以点亮一个LED或者进行下一步操作 // HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } } /* USER CODE END 4 */这个函数会在 DMA 发送完成中断中被 HAL 库自动调用。
5. 编译、下载与验证
- 编译工程:点击 STM32CubeIDE 中的锤子图标进行编译,确保无错误。
- 连接硬件:用 USB 转串口模块连接开发板的 PA9(TX)、PA10(RX)和 GND 到电脑。同时连接 ST-Link 进行下载和调试。
- 下载程序:点击 IDE 中的下载按钮。
- 验证:
- 打开串口助手,选择正确的 COM 口,波特率 115200。
- 复位开发板,你应该看到串口助手收到
Hello DMA!。 - 在串口助手的发送框输入任意长度字符串(不超过
RX_BUFFER_SIZE),点击发送。 - 观察接收区,开发板会通过 DMA 将你发送的内容原样回传回来。
6. 常见问题与深度排查指南
配置 DMA 时遇到的问题往往比较隐蔽。下面是一个系统的排查清单。
6.1 数据根本收不到或发不出
| 现象 | 可能原因 | 检查点与解决方案 |
|---|---|---|
| 完全无数据 | 串口引脚配置错误 | 1. 检查 CubeMX 中 USART 引脚是否已正确映射(PA9/PA10)。 2. 检查硬件连接(TX/RX 是否交叉连接)。 |
| 时钟未正确配置 | 1. 检查SystemClock_Config()函数是否被调用。2. 使用 HAL_RCC_GetSysClockFreq()打印系统时钟验证。3. 检查 APB2 总线时钟(USART1 时钟源)是否使能且频率正确。 | |
| DMA/USART 外设未使能 | 1. 确认MX_DMA_Init()和MX_USART1_UART_Init()被main函数调用。2. 在调试模式下,查看 USART1->CR1和DMA1_Channel4->CCR等寄存器,确认ENABLE位是否被置1。 | |
| 中断未使能或优先级冲突 | 1. 检查 NVIC 配置,USART1 全局中断和 DMA 通道中断是否使能。 2. 检查是否有更高优先级中断长时间阻塞。 | |
| 只能收到第一个字节或零星字节 | DMA 传输模式错误 | 1. 发送模式应为Memory to Peripheral,Normal。2. 接收模式应为 Peripheral to Memory,Circular(不定长)或Normal(定长)。3. 检查 Memory Address Increment是否使能。 |
| DMA 传输计数器未正确设置 | 1. 发送前,确认HAL_UART_Transmit_DMA的长度参数正确。2. 在调试器观察 DMA1_Channel4->CNDTR(发送)或DMA1_Channel5->CNDTR(接收)寄存器,启动后其值应为预设的数据长度,并随传输递减。 | |
| 外设请求未连接 | 1. 在 CubeMX 中,确认 DMA Request 正确关联到了USART1_TX和USART1_RX。2. 检查参考手册,确认所用 USART 的 TX/RX DMA 请求映射到了正确的 DMA 通道(对于 F103,USART1_TX 是 DMA1_Channel4,RX 是 DMA1_Channel5)。 |
6.2 不定长接收相关的问题
| 现象 | 可能原因 | 检查点与解决方案 |
|---|---|---|
| 空闲中断不触发 | 空闲中断未使能 | 1. 确认在启动接收后,调用了__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE)。2. 在 USART1->CR1寄存器中查看IDLEIE位是否为1。 |
| 数据流从未停止 | 空闲中断在第一个字符接收完成后,总线空闲一个字符时间后触发。如果数据持续不断(例如波特率错误导致误码也被当作数据),可能不会产生空闲。检查发送端。 | |
计算的长度 (rx_len) 不对 | DMA 计数器读取时机不对 | 必须在停止 DMA后,再读取__HAL_DMA_GET_COUNTER。如果在 DMA 运行中读取,该值可能正在变化,导致计算错误。 |
| 缓冲区溢出 | 如果一帧数据长度超过RX_BUFFER_SIZE,DMA 会从缓冲区头部开始覆盖,导致rx_len计算逻辑失效(RX_BUFFER_SIZE - CNDTR可能小于实际帧长)。必须确保缓冲区足够大,或使用双缓冲(半传输中断)机制。 | |
| 数据错位或重复 | DMA 重启逻辑错误 | 在空闲中断中处理完数据后,如果选择重新配置 DMA 寄存器(CNDTR, CMAR),必须确保配置顺序正确,并最终使能 DMA。最稳妥的方法是直接再次调用HAL_UART_Receive_DMA,让 HAL 库处理底层细节。 |
| 主循环处理太慢 | 主循环在rx_flag=1后,如果处理数据耗时过长,DMA 可能已经收到了新数据并覆盖了缓冲区中未处理的部分。解决方案:1. 加大缓冲区。 2. 使用双缓冲:在中断中将数据快速拷贝到另一个处理缓冲区。 3. 提高主循环处理优先级或使用 RTOS 任务。 |
6.3 DMA 发送完成中断不触发
- 检查中断使能:在 CubeMX 中确认该 DMA 通道的传输完成中断(TCIE)已使能。在代码中,
HAL_UART_Transmit_DMA会自动使能相关中断。 - 检查回调函数:确保重写了
HAL_UART_TxCpltCallback函数,且函数体在/* USER CODE BEGIN 4 */和/* USER CODE END 4 */之间,这样代码生成器不会覆盖它。 - 检查传输模式:如果 DMA 模式设置为
Circular,则不会产生传输完成中断,因为它会循环发送。
6.4 性能与稳定性进阶考量
- 内存对齐:如果 DMA 传输的数据宽度是
Half Word(16位)或Word(32位),要确保源和目标地址都按相应字节对齐(2字节或4字节边界),否则可能导致硬件错误。 - 缓存一致性(对于 Cortex-M7 等带 Cache 的芯片):如果 DMA 操作的内存区域是可缓存的,在 DMA 写入后(CPU 读取前),或 CPU 写入后(DMA 读取前),需要手动执行
SCB_CleanDCache_by_Addr或SCB_InvalidateDCache_by_Addr来维护缓存一致性,否则会读到旧数据。 - 双缓冲(Double Buffer):对于高速、连续的数据流(如音频、摄像头),使用 DMA 双缓冲模式可以几乎无缝地处理数据。配置两个缓冲区,DMA 填满缓冲区 A 时产生中断,CPU 处理 A,同时 DMA 转向填充缓冲区 B,如此交替。
- 使用
__HAL_LOCK检查:HAL 库使用__HAL_LOCK机制防止对句柄的并发访问。如果在一个 DMA 传输尚未完成时,再次调用HAL_UART_Transmit_DMA,函数会返回HAL_BUSY。在实际项目中,需要实现一个简单的发送队列或状态机来处理这种情况。
7. 最佳实践与扩展方向
掌握了基础的 DMA 串口收发后,可以将其应用到更复杂的场景,并遵循以下最佳实践:
- 封装为独立模块:将串口 DMA 初始化和收发逻辑封装成独立的
uart_dma.c/.h文件,提供清晰的接口,如uart_send_packet(),uart_get_packet(),提高代码可移植性和可读性。 - 实现环形缓冲区(FIFO):在主循环和 DMA 接收中断之间,增加一个软件环形缓冲区。DMA 空闲中断将数据快速拷贝到环形缓冲区,并释放 DMA 缓冲区。主循环从环形缓冲区读取处理。这彻底解耦了接收和处理的速度,是工业级应用的标准做法。
- 协议解析与超时:除了空闲中断,还应加入超时机制。如果一帧数据被分成了多个包,或者中间有较长间隔,空闲中断可能提前触发。可以配合一个定时器,在收到第一个字节时启动,在超时时间内未收到新字节则认为一帧结束。
- 错误处理:完善 DMA 和 USART 的错误中断回调函数(
HAL_UART_ErrorCallback),处理帧错误、噪声错误、溢出错误等,并记录日志。 - 扩展到其他外设:同样的 DMA 思想可以应用到:
- ADC:连续扫描多个通道,DMA 将结果搬运到数组,实现高速数据采集。
- SPI/I2S:与音频编解码器、存储器、显示屏通信,搬运大量音频像素数据。
- SDIO/FSMC:与外部存储器交换数据。
理解 DMA 不仅仅是记住配置步骤,更是建立起“CPU 与 DMA 协同工作”的思维模型。在资源受限的嵌入式系统中,合理运用 DMA 是优化性能、保证实时性的关键技能。从这个小例子出发,尝试修改参数,观察现象,并逐步应用到你的实际项目中,才能真正掌握这项“解放 CPU”的核心技术。