STM32 USART2 DMA通信:解放CPU实现高效串口数据传输
1. 项目概述:为什么需要USART2 DMA通信?
如果你玩过一阵子STM32,肯定对串口通信不陌生。无论是调试打印信息,还是跟传感器、蓝牙模块、GPS模块打交道,USART(通用同步异步收发器)都是最常用、最基础的通信接口之一。标准库或者HAL库里那个HAL_UART_Transmit和HAL_UART_Receive函数,用起来简单直接,一两个字节的数据收发没问题。但当你需要连续发送一长串数据,比如刷新OLED屏幕的整帧图像数据,或者以高波特率持续接收来自传感器的数据流时,问题就来了。
CPU会频繁被中断服务程序(ISR)打断,每一个字节的发送或接收都要进一次中断,CPU大部分时间都在“搬砖”——把数据从内存搬到USART的数据寄存器(DR),或者反过来。这种“阻塞式”或“中断式”的通信,在数据量大、速率高时,会严重消耗CPU资源,导致主程序卡顿,响应变慢,甚至可能因为中断处理不及时而丢失数据。
这时候,DMA(直接存储器访问)就该登场了。你可以把DMA想象成你雇的一个“专职搬运工”。你只需要告诉它:货在哪里(源地址),要搬到哪去(目标地址),要搬多少(数据量),然后就可以拍拍屁股去干别的活了。这个搬运工独立于CPU工作,通过专用的总线搬运数据,不占用CPU的计算资源。对于USART来说,启用DMA发送,就是让DMA控制器自动把内存里的一块数据,一个字节一个字节地“喂”给USART的发送数据寄存器;启用DMA接收,则是让DMA把USART接收到的数据,自动存放到你指定的一块内存缓冲区里。
所以,“STM32之USART2 DMA通信”这个项目,核心目标就是解放CPU,实现USART2接口的高效、稳定、大流量数据吞吐。无论是用于高速数据采集、实时通信,还是驱动需要连续数据流的设备,掌握DMA都是提升STM32项目性能的关键一步。接下来,我会基于常见的STM32F1/F4系列,以HAL库为例,拆解从思路到代码,再到调试避坑的完整过程。
2. 核心思路与硬件资源规划
在动手写代码之前,理清思路和规划好硬件资源至关重要。盲目配置往往会导致各种奇怪的问题。
2.1 为什么是USART2?
在STM32中,USART1、USART2、USART3等是独立的串口外设。选择USART2通常基于以下几点考虑:
- 引脚复用灵活性:以STM32F103C8T6(蓝色药丸板)为例,USART2的TX(PA2)和RX(PA3)引脚通常不与常用的调试接口(如SWD)冲突,方便布线。
- 资源独立性:在一些型号中,USART1可能被用于系统启动时的串口打印(如果开启了),使用USART2可以避免冲突。USART2也通常连接到一个独立的DMA通道。
- 项目需求:可能你的模块(如GPS、蓝牙)硬件连接已经固定在了对应USART2的引脚上。
2.2 DMA通道与数据流(Stream)映射
这是DMA配置中最容易出错的一环。DMA控制器有多个通道(Channel),每个通道服务于特定的外设请求(如USART2_TX, USART2_RX)。在F1系列中,结构相对简单;在F4/F7/H7等系列中,引入了更复杂的“数据流(Stream)”概念。
以STM32F407为例,USART2的DMA映射关系如下(必须查数据手册确认!):
- USART2_TX(发送) 的DMA请求,可以映射到DMA1 Stream6的Channel4。
- USART2_RX(接收) 的DMA请求,可以映射到DMA1 Stream5的Channel4。
注意:这个映射关系是硬件固定的,不能随意更改。错误的映射会导致DMA根本无法触发。最可靠的方法是查阅你所使用芯片型号的官方数据手册(Datasheet)或参考手册(Reference Manual)中的“DMA请求映射”表格。
2.3 内存与缓冲区设计
使用DMA,本质上是让DMA在外设寄存器和内存之间搬运数据。因此,我们需要在内存中规划好缓冲区。
- 发送缓冲区(TxBuffer):一块内存区域,存放你准备通过串口发送出去的数据。DMA会从这里读取数据并送到USART2->DR寄存器。
- 接收缓冲区(RxBuffer):一块内存区域,用于存放DMA从USART2->DR寄存器搬运过来的接收数据。它的大小设计是关键。
- 大小:缓冲区大小应至少大于你预期单次接收的最大数据包长度。例如,你接收的每条传感器数据帧是50字节,那么缓冲区可以设为64或100字节,留有余量。
- 环形缓冲区(可选但推荐):对于持续不断的流式数据接收,单纯的一个线性缓冲区很容易被新数据覆盖旧数据。更高级的做法是配合DMA的“循环模式(Circular Mode)”和软件管理的读写指针,实现一个环形缓冲区(FIFO),这是实现可靠流式接收的常见模式。
2.4 中断策略:何时知道DMA干完活了?
DMA虽然不占用CPU时间搬运数据,但我们总需要知道“搬运工”什么时候干完活,或者有没有出错。这就需要配置DMA传输完成中断(TC,Transfer Complete)和半传输完成中断(HT,Half Transfer),以及传输错误中断(TE,Transfer Error)。
- 发送场景:通常我们只关心“发送完成中断”。当DMA把发送缓冲区的所有数据都搬运到USART后,会触发TC中断。我们在中断回调函数里可以做一些后续操作,比如改变LED状态、准备下一包数据,或者将发送完成标志位置位,通知主程序。
- 接收场景:这是重点。如果接收缓冲区很大,我们可能不想等到缓冲区完全填满(TC中断)才去处理数据,那样延迟太高。此时可以启用HT中断。当DMA接收到半缓冲区数据时,触发HT中断;完全填满时,触发TC中断。这样,我们可以在HT中断里处理前半部分数据,在TC中断里处理后后半部分数据,实现“双缓冲”机制,减少数据处理延迟。更常见的做法是使用空闲中断(Idle Interrupt),我将在后面详细讲解。
3. 使用STM32CubeMX进行图形化配置
对于新手或者追求开发效率,STM32CubeMX是绝佳的工具。它能直观地配置引脚、时钟、外设和中间件,并生成初始化代码框架。
3.1 基础外设使能
- 选择芯片型号:在CubeMX中正确选择你的STM32型号。
- 配置系统时钟(SYS):Debug选项建议选“Serial Wire”,方便ST-Link调试。
- 配置时钟树(RCC):根据你的板载晶振,配置HSE(高速外部时钟),并设置系统主频(SYSCLK)。更高的主频意味着更高的处理能力和DMA带宽。
- 配置USART2:
- 模式选择“Asynchronous”(异步通信)。
- 配置波特率(如115200)、字长(8位)、停止位(1位)、校验位(无)。
- 引脚PA2和PA3会自动配置为USART2_TX和USART2_RX。
3.2 DMA配置详解
这是CubeMX配置的核心部分。
添加DMA通道:
- 在“DMA Settings”标签页或Pinout视图的USART2配置中,点击“Add”。
- 为USART2_TX添加一个DMA请求。方向(Direction)选择“Memory To Peripheral”(内存到外设)。
- 为USART2_RX添加一个DMA请求。方向选择“Peripheral To Memory”(外设到内存)。
参数设置(以USART2_RX的DMA为例):
- Mode:
Normal(普通模式):传输完指定数据量后,DMA停止,需要软件重新使能。适合发送固定长度数据包。Circular(循环模式):传输完指定数据量后,自动从头开始循环传输。这是实现持续接收的推荐模式。缓冲区就像一个环,新数据会覆盖旧数据,配合软件读指针来取数据。
- Priority(优先级):根据系统需求设置,如果只有这一个DMA传输,设为Low即可。如果有多个DMA竞争总线,高优先级的会先服务。
- Increment Address(地址自增):
- 对于Memory(内存端):必须勾选
Enable。因为数据是连续存放在内存数组中的,DMA每搬运一个字节后,内存地址要自动+1,指向下一个数据位置。 - 对于Peripheral(外设端):必须不勾选。因为外设寄存器地址是固定的(始终是USART2->DR这个地址),DMA要一直往这个固定地址读/写。
- 对于Memory(内存端):必须勾选
- Data Width(数据宽度):Memory和Peripheral都选择
Byte(字节)。因为USART通信的基本单位是字节(8位)。
- Mode:
中断配置:
- 在NVIC Settings中,使能USART2的全局中断(如果需要使用串口本身的错误中断等)。
- 关键步骤:在DMA配置中,勾选对应的DMA通道中断。对于接收DMA,至少使能“Transfer Complete Interrupt”(传输完成中断)。你也可以使能“Half Transfer Interrupt”(半传输中断)来实现双缓冲。
生成代码:
- 设置好工程路径、工具链(MDK-ARM/V5等),生成代码。CubeMX会生成
usart.c/.h和dma.c/.h中的初始化代码MX_USART2_UART_Init()和MX_DMA_Init()。
- 设置好工程路径、工具链(MDK-ARM/V5等),生成代码。CubeMX会生成
4. 手把手代码实现与解析
假设我们已经用CubeMX生成了基础框架,现在需要编写应用层代码。我们以实现“循环DMA接收+空闲中断处理”这一经典且实用的模式为例。
4.1 宏定义与变量声明
在main.c或你的通信模块头文件中定义:
// 接收缓冲区大小 #define RX_BUFFER_SIZE 256 // 发送缓冲区大小 #define TX_BUFFER_SIZE 128 // DMA接收缓冲区 uint8_t g_uart2_rx_buffer[RX_BUFFER_SIZE]; // 发送缓冲区 uint8_t g_uart2_tx_buffer[TX_BUFFER_SIZE]; // 用于记录空闲中断发生时,DMA已经搬运了多少数据 volatile uint16_t g_uart2_rx_len = 0; // 接收完成标志,供主程序查询 volatile uint8_t g_uart2_rx_done_flag = 0;注意:
g_uart2_rx_len和g_uart2_rx_done_flag这两个变量会在中断服务程序中被修改,在主循环中被读取,因此必须用volatile关键字修饰,防止编译器进行错误的优化。
4.2 初始化函数补充
CubeMX生成的初始化代码使能了USART2和DMA,但我们需要手动开启USART2的空闲中断,并启动DMA接收。
在main()函数的初始化部分,调用完MX_USART2_UART_Init()后,添加:
// 使能USART2的空闲中断(IDLE Interrupt) __HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE); // 启动DMA接收,让DMA开始监听USART2的数据,并自动存放到g_uart2_rx_buffer // 参数:串口句柄,接收缓冲区,缓冲区大小 HAL_UART_Receive_DMA(&huart2, g_uart2_rx_buffer, RX_BUFFER_SIZE);代码解析:
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE):这是一个HAL库底层宏,用于使能指定串口的特定中断。UART_IT_IDLE就是空闲中断。当USART的RX线上超过一个字节的时间(具体取决于波特率)没有新数据时,就会产生此中断。HAL_UART_Receive_DMA(...):这个HAL库函数做了几件事:1. 配置DMA通道的源地址(USART2->DR)、目标地址(我们的缓冲区)、数据量。2. 使能DMA通道。3. 使能USART2的DMA接收请求。调用后,DMA就进入待命状态,一旦USART收到数据,DMA会自动搬运。
4.3 中断服务程序与回调函数
HAL库采用了中断回调(Callback)机制。我们不需要直接修改stm32f4xx_it.c中的中断服务函数,而是重写对应的弱定义回调函数。
重写USART2空闲中断回调函数: 在
main.c或你的用户文件中,添加以下函数:// 重写USART空闲中断回调函数 void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart) { // 判断是否是USART2触发的中断 if(huart->Instance == USART2) { // 临时禁用DMA,防止在计算长度时DMA仍在修改缓冲区 __HAL_DMA_DISABLE(huart->hdmarx); // 计算本次接收到的数据长度 // DMA当前存储地址 - 缓冲区起始地址 = 已存储的数据量 // hdmarx->Instance->NDTR 是DMA通道剩余未传输的数据量寄存器 g_uart2_rx_len = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx); // 设置接收完成标志 g_uart2_rx_done_flag = 1; // 重新使能DMA,准备接收下一包数据 // 注意:因为之前是循环模式,所以只需要重新设置数据长度并使能即可 __HAL_DMA_SET_COUNTER(huart->hdmarx, RX_BUFFER_SIZE); __HAL_DMA_ENABLE(huart->hdmarx); } }原理与避坑点:
- 长度计算:
__HAL_DMA_GET_COUNTER(huart->hdmarx)获取的是DMA通道剩余要传输的数据量。我们用总缓冲区大小减去剩余量,就得到了已经传输的数据量,也就是本次从空闲开始到空闲结束接收到的数据长度。 - 先关DMA再计算:这是一个非常重要的细节!DMA传输是硬件行为,与CPU异步。如果在计算过程中DMA又搬运了新数据,会导致计算的长度不准,甚至访问越界。因此,先
__HAL_DMA_DISABLE()暂停DMA是安全的做法。 - 重新使能:因为我们配置的是
Circular模式,缓冲区是环形的。在计算完长度并取走数据后,我们需要重置DMA的计数器(CNDTR寄存器)为缓冲区大小,并重新使能DMA。这样DMA就会从缓冲区开头(或当前位置)继续接收数据,覆盖掉已经处理过的旧数据,实现循环接收。
- 长度计算:
(可选)重写DMA传输完成回调函数: 对于发送,我们可能更关心DMA发送完成。对于接收,在“空闲中断+循环DMA”模式下,DMA的传输完成中断(TC)意义不大,因为它意味着整个环形缓冲区被填满了一圈,通常我们更早地在空闲中断里就处理了数据。但发送完成中断很有用。
// 重写DMA发送完成中断回调函数 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART2) { // 可以在这里置位一个发送完成标志,或者释放信号量等 // 例如:uart2_tx_done = 1; } }
4.4 主循环中的数据发送与接收处理
有了上述框架,主程序的逻辑就非常清晰了。
发送数据:
// 准备要发送的数据 uint8_t send_data[] = "Hello, DMA UART!\r\n"; uint16_t send_len = sizeof(send_data) - 1; // 去掉字符串结尾的'\0' // 使用DMA发送 HAL_UART_Transmit_DMA(&huart2, send_data, send_len); // 此时CPU可以立即去执行其他任务,无需等待发送完成注意:
HAL_UART_Transmit_DMA是非阻塞的。调用后函数立即返回,DMA在后台搬运数据。如果你需要等待本次发送完成才能进行下一次发送(例如,严格的数据包顺序),那么你需要查询huart2.gState的状态,或者等待HAL_UART_TxCpltCallback中设置的标志位。处理接收到的数据:
// 在主循环中 while (1) { // 检查接收完成标志 if(g_uart2_rx_done_flag == 1) { // 清除标志 g_uart2_rx_done_flag = 0; // 处理接收到的数据,长度在 g_uart2_rx_len 中 // 例如,将数据回显(Echo)回去 HAL_UART_Transmit_DMA(&huart2, g_uart2_rx_buffer, g_uart2_rx_len); // 或者进行协议解析(如解析MODBUS、自定义帧头帧尾等) // uart2_protocol_parse(g_uart2_rx_buffer, g_uart2_rx_len); // 重要:处理完数据后,最好将接收长度清零 // g_uart2_rx_len = 0; // 但注意,这个变量在中断里会被重新赋值,清不清取决于你的逻辑 } // 其他任务... HAL_Delay(1); }
5. 深度调试技巧与常见问题排查
即使代码逻辑正确,在实际硬件调试中也可能遇到各种问题。下面是我在多个项目中总结出的排查清单。
5.1 数据收发全无(最基础的问题)
- 检查1:硬件连接
- TX、RX线是否接反?(MCU的TX要接对方设备的RX)
- 共地(GND)是否连接?串口通信必须共地。
- 使用USB转TTL模块时,其VCC是否接对?是3.3V还是5V?STM32通常是3.3V电平。
- 检查2:引脚配置
- 在CubeMX中确认USART2的TX/RX引脚是否正确映射(PA2/PA3)。
- 确认没有其他功能(如ADC)复用到这两个引脚上,造成冲突。
- 检查3:时钟配置
- 系统时钟(HCLK)是否成功配置并运行在预期频率?
- USART2的时钟总线(APB1)是否已使能?在CubeMX时钟树中确认APB1总线时钟不为0。
- 关键点:USART的波特率发生器时钟来源于APB1(对于USART2)。如果APB1时钟配置错误,实际波特率会和设置值相差巨大,导致无法通信。
- 检查4:软件初始化顺序
- 确保在
main()函数中,先初始化MX_DMA_Init(),再初始化MX_USART2_UART_Init()。因为USART初始化函数里可能会用到DMA相关的结构体。 - 确保在调用
HAL_UART_Receive_DMA启动接收之前,所有外设初始化已完成。
- 确保在
5.2 DMA接收能启动,但收不到数据或数据错乱
- 问题1:DMA通道映射错误
- 症状:程序不报错,但DMA计数器
CNDTR永远不减少,说明DMA根本没被触发。 - 解决:反复核对数据手册中的“DMA请求映射表”,确认USART2_RX和USART2_TX对应的DMA数据流(Stream)和通道(Channel)是否与代码中
huart2.hdmarx->Instance和huart2.hdmatx->Instance指向的DMA流一致。这是最经典的错误。
- 症状:程序不报错,但DMA计数器
- 问题2:缓冲区溢出或数据覆盖
- 症状:能收到数据,但数据包不完整,或者新旧数据混杂。
- 解决:
- 检查接收缓冲区
RX_BUFFER_SIZE是否足够大。如果数据包源源不断,而主程序处理速度跟不上DMA接收速度,缓冲区会被写满并从头覆盖。此时应增大缓冲区,或提高主程序处理速度(优化代码、使用DMA双缓冲HT/TC中断)。 - 在
HAL_UART_IdleCpltCallback中,计算完长度g_uart2_rx_len后,是否立即将数据拷贝到另一个处理缓冲区?如果还在原缓冲区进行复杂的协议解析,而DMA已经重新使能并开始接收新数据,就会发生数据覆盖。务必在中断回调中尽快将数据移走或做好标记。
- 检查接收缓冲区
- 问题3:空闲中断不触发
- 症状:能收到单个字符(用查询方式验证),但
g_uart2_rx_done_flag永远不为1。 - 解决:
- 确认已调用
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE)使能了空闲中断。 - 检查USART2的全局中断是否在NVIC中使能(CubeMX通常会自动配置)。
- 空闲中断的触发条件是“RX线空闲时间超过一个字符的传输时间”。如果发送方发送的数据帧之间没有停顿(间隔小于一个字符时间),就不会触发空闲中断。对于无间隔的流数据,应使用“循环DMA+定时器”或“循环DMA+定期查询DMA计数器”的方式来提取数据。
- 在
stm32f4xx_it.c中找到USART2_IRQHandler函数,确保它内部调用了HAL_UART_IRQHandler(&huart2)。这个HAL库通用中断处理函数会判断中断来源并调用对应的回调函数(如HAL_UART_IdleCpltCallback)。
- 确认已调用
- 症状:能收到单个字符(用查询方式验证),但
5.3 DMA发送问题
- 问题1:发送卡住,程序阻塞在发送函数
- 症状:调用
HAL_UART_Transmit_DMA后,程序似乎停住了。 - 解决:检查
huart2.gState。HAL库的UART驱动有一个状态机。如果上一次DMA发送还未完成(huart2.gState != HAL_UART_STATE_READY),再次调用发送函数可能会返回HAL_BUSY或进入错误处理。解决方案是等待上一次发送完成。可以简单用while(huart2.gState != HAL_UART_STATE_READY){}轮询(不推荐,会阻塞),更好的方法是用发送完成回调函数设置标志位,在主程序中基于标志位进行状态控制。
- 症状:调用
- 问题2:发送数据丢失最后几个字节
- 症状:发送一串数据,接收端总是少收最后1-2个字节。
- 解决:DMA传输完成中断(TC)触发时,意味着DMA已经把最后一个数据从内存搬到了USART的数据寄存器(DR)。但是,USART硬件将数据从DR移位到TX引脚发送出去,还需要一定时间(取决于波特率)。如果在TC中断回调里立即进行关闭串口、复位等操作,可能会打断最后几个字节的物理发送。解决方法:在TC中断回调中,不要立即关闭外设。如果需要确保所有数据都已从TX引脚发出,可以等待USART状态寄存器中的“发送完成(TC,Transmission Complete)”标志位置位。HAL库提供了
HAL_UART_GetState(&huart2)和检查HAL_UART_STATE_TC的方法,但更简单的做法是在TC中断回调后,延迟一小段时间(例如,计算发送所有字节所需时间再加几个毫秒)再进行后续操作。
5.4 高级调试手段
- 使用逻辑分析仪或示波器:这是最直接的硬件调试方法。可以观察TX、RX引脚上的实际波形,确认波特率是否正确、数据内容是否符合预期、帧格式(起始位、数据位、停止位)是否正确。
- 利用ST-Link和IDE调试器:
- 查看外设寄存器:在Keil或STM32CubeIDE的调试模式下,可以查看USART2->SR(状态寄存器)、USART2->DR(数据寄存器)、DMA通道的CNDTR(计数器)寄存器、CCR(配置寄存器)等的实时值。这对于理解DMA和USART的工作状态至关重要。
- 设置数据断点:可以在接收缓冲区
g_uart2_rx_buffer的特定位置设置数据写入断点,当DMA向该地址写入数据时,程序会暂停,方便你观察是何时、如何写入的。
- 软件模拟与调试信息:
- 在关键位置(如DMA TC/HT中断回调、空闲中断回调)翻转一个GPIO引脚(点个灯),用示波器观察中断响应是否及时。
- 在确保基本通信正常后,可以先使用简单的
HAL_UART_Transmit函数发送一些调试信息(如g_uart2_rx_len的值),帮助判断程序逻辑。但注意,打印函数本身可能占用时间,影响实时性。
6. 性能优化与进阶应用
当基础功能跑通后,可以考虑以下优化和进阶用法,让你的DMA串口通信更加强大和稳定。
6.1 实现“双缓冲”接收以降低延迟
前面提到的“空闲中断+循环DMA”模式,其数据处理延迟等于一包数据的时间。如果想进一步降低延迟,可以使用DMA的半传输中断(HT)和传输完成中断(TC)来实现双缓冲(Ping-Pong Buffer)。
原理:
- 将接收缓冲区在逻辑上分为前半部分(Buffer A)和后半部分(Buffer B)。
- 当DMA接收到一半数据(填满Buffer A)时,触发HT中断。在HT中断回调中,你可以处理Buffer A的数据,而此时DMA正在向Buffer B写入后半部分数据。
- 当DMA接收完所有数据(填满Buffer B)时,触发TC中断。在TC中断回调中,你可以处理Buffer B的数据,而此时DMA已经循环回来,开始向Buffer A写入新数据。
这样,数据处理的延迟从“一包时间”降低到了“半包时间”,并且CPU处理数据和DMA接收数据几乎是并行的,效率更高。配置方法是在CubeMX中使能DMA通道的“Half Transfer Interrupt”和“Transfer Complete Interrupt”,并分别实现HAL_UART_RxHalfCpltCallback和HAL_UART_RxCpltCallback回调函数。
6.2 与RTOS(如FreeRTOS)结合
在实时操作系统中,长时间阻塞是不可接受的。DMA+USART与RTOS是绝配。
- 发送:可以创建一个发送任务(或线程)和一个发送队列。其他任务需要发送数据时,只需将数据指针和长度放入队列。发送任务从队列中取出数据,调用
HAL_UART_Transmit_DMA,然后挂起自己,等待一个二值信号量(Semaphore)。这个信号量在HAL_UART_TxCpltCallback(发送完成中断)中释放。这样,发送任务只在有数据发送和发送完成时被调度,极大节省CPU资源。 - 接收:在
HAL_UART_IdleCpltCallback(空闲中断)中,计算数据长度后,不要进行复杂的处理。而是将接收缓冲区的数据拷贝到一个RTOS的消息队列(Message Queue)或直接释放一个计数信号量。由一个专门的数据处理任务(Consumer Task)等待这个队列或信号量,并在其上下文进行协议解析等耗时操作。这符合RTOS的“中断快进快出”原则,避免了在中断中执行复杂代码。
6.3 超时管理与错误恢复
工业应用要求通信稳定可靠。需要增加超时和错误恢复机制。
- 接收超时:除了空闲中断,可以开启一个硬件定时器。每次收到任何一个字节(可以在DMA传输中断中重置定时器)就重置定时器。如果定时器超时,则认为一帧数据结束(即使没触发空闲中断)。这可以应对一些通信协议中帧间隔不固定的情况。
- 错误处理:重写
HAL_UART_ErrorCallback回调函数。当发生溢出错误(ORE)、噪声错误(NE)、帧错误(FE)或校验错误(PE)时,HAL库会调用此函数。在这里,你需要清除错误标志(__HAL_UART_CLEAR_FLAG),并根据情况重新初始化DMA接收(HAL_UART_Receive_DMA),否则通信可能会永久停止。 - DMA传输错误:同样,DMA本身也可能产生传输错误中断(如总线错误)。需要在DMA的中断服务程序中处理,或检查相关的错误标志位。
6.4 内存对齐与性能考量
- 内存对齐:DMA访问内存时,如果数据地址没有对齐到字(Word,4字节)或半字(Half-Word,2字节)边界,在某些架构下可能会导致额外的时钟周期(性能下降)或甚至产生硬件错误(在严格对齐的系统中)。虽然对于USART的字节传输,这个问题不突出,但一个好的习惯是,将DMA缓冲区用
__attribute__((aligned(4)))进行4字节对齐声明。uint8_t g_uart2_rx_buffer[RX_BUFFER_SIZE] __attribute__((aligned(4))); - 使用内存到内存的DMA:DMA不仅可以用于外设和内存之间,还可以用于内存和内存之间(Mem-to-Mem)。例如,你可以用DMA来快速搬运、初始化或比较大块的数据,这比用CPU的
memcpy或memset要快得多,尤其是在有DMA2D(二维DMA)功能的系列上,对图形操作有奇效。这展示了DMA作为一种系统级数据搬运工的通用价值。
从最基本的配置,到稳定可靠的“空闲中断+循环DMA”模式,再到与RTOS结合、实现双缓冲和错误恢复,STM32的USART DMA通信是一个可以不断深入挖掘的主题。它完美体现了嵌入式开发中“硬件加速”的思想——将CPU从简单重复的IO操作中解放出来,专注于核心的业务逻辑和算法。理解并熟练运用DMA,是迈向高效嵌入式系统开发的必经之路。在实际项目中,建议从一个简单的例子开始,逐步增加功能,并用逻辑分析仪和调试器仔细观察每一步的硬件行为,积累下来的经验会让你在面对更复杂的通信场景时游刃有余。
