4、UART抽象层:HAL_UART_Init/Transmit/Receive函数详解、中断与DMA模式下的UART收发
UART,说白了就是串口。做嵌入式的人,没人离得开它。调试打印、跟传感器通信、跟蓝牙模块对接,全得靠它。STM32的HAL库把UART封装得挺完善,但你要是只停留在“调库”的层面,遇到性能瓶颈或者诡异bug,就会很被动。
这一章,我带你深入看看HAL_UART的核心函数,再聊聊中断和DMA这两种收发模式。嗯,这里要注意,理解清楚它们的差异,你才能在项目中选对路子。
4.1 HAL_UART_Init:初始化背后的门道
调用HAL_UART_Init()之前,你得先搞定两个东西:一个是UART_HandleTypeDef结构体,另一个是GPIO的复用配置。我个人习惯把这两步放在一个独立的UART_Init()函数里,方便后期移植。
来看看这个结构体里最关键的几个成员:
| 成员 | 说明 | 避坑点 |
|---|---|---|
Instance |
指定USART外设(如USART1、USART2) | 别跟LPUART搞混,时钟源不同 |
Init.BaudRate |
波特率,常用115200、9600 | 高速时注意时钟精度,别超±2% |
Init.WordLength |
数据位,8位或9位 | 9位模式常用于多机通信 |
Init.StopBits |
停止位,1位或2位 | 2位停止位会增加传输开销 |
Init.Parity |
校验位,无/奇/偶校验 | 使能校验后,数据位要相应调整 |
Init.Mode |
TX/RX模式选择 | 只收不发时,可以关掉TX省电 |
Init.HwFlowCtl |
硬件流控(RTS/CTS) | 模块间通信常用,但线多了两根 |
HAL_UART_Init(),结果串口死活没输出。查了半天才发现,是GPIO的AF模式没设对。记住:HAL_UART_MspInit()里一定要把时钟和GPIO初始化好。
4.2 HAL_UART_Transmit:发送数据的正确姿势
发送函数HAL_UART_Transmit(),原型长这样:
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
四个参数,简单明了。但有几个细节,我建议你留意一下:
- Timeout参数:单位是毫秒。如果设为
HAL_MAX_DELAY,函数会一直等,直到发完。这在调试时很方便,但产品代码里最好设个具体值,防止死锁。 - 阻塞模式:这个函数是阻塞的,也就是说,数据没发完之前,CPU就耗在这儿了。如果你在主循环里频繁调用,会影响其他任务的响应。
- 数据长度:
Size是字节数,不是位数。别搞混了。
4.3 HAL_UART_Receive:接收数据的陷阱
接收函数HAL_UART_Receive(),跟发送是对称的:
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
这里有个大坑——它必须收到指定数量的字节才会返回。你设了Size=10,那它就死等10个字节。如果对方只发了5个,你就永远卡在那儿了。
我在项目中遇到过这种情况:跟一个不靠谱的传感器通信,它偶尔会丢字节。结果我的接收函数就卡死了,整个系统跟着瘫痪。后来怎么解决的?改用中断接收,配合超时判断。
4.4 中断模式下的UART收发
中断模式,说白了就是“发完/收到一个字节,就通知你一下”。CPU不用死等,可以干别的事。
发送用HAL_UART_Transmit_IT(),接收用HAL_UART_Receive_IT()。调用后,函数立即返回,数据在后台传输。传输完成时,会触发回调函数:
HAL_UART_TxCpltCallback():发送完成回调HAL_UART_RxCpltCallback():接收完成回调
你想想看,这比阻塞模式灵活多了吧?但要注意一点:中断频率。波特率115200时,每字节大约87微秒就触发一次中断。如果系统里中断很多,频繁进出中断会消耗不少CPU时间。
4.5 DMA模式下的UART收发
DMA模式,是真正的“解放CPU”。数据从内存到外设,或者从外设到内存,全由DMA控制器搞定,CPU只管睡大觉。
调用方式:
HAL_UART_Transmit_DMA()HAL_UART_Receive_DMA()
DMA模式有几个明显的优势:
- CPU占用极低:传输过程中,CPU几乎零开销。
- 适合大数据量:比如日志输出、固件升级、文件传输。
- 配合环形缓冲区:可以实现“一边收一边处理”的流水线操作。
但DMA也不是万能的。我记得有一次,我用DMA接收不定长数据,结果发现DMA不知道什么时候该停止——它只管收,不管“收够了没”。后来我配合了UART的空闲中断(IDLE Line Interrupt),才完美解决。
4.6 三种模式对比:选型指南
为了让你一目了然,我把三种模式的核心差异整理成了一张表:
| 特性 | 阻塞模式 | 中断模式 | DMA模式 |
|---|---|---|---|
| CPU占用 | 高(全程等待) | 中(每字节中断) | 极低(后台传输) |
| 实时性 | 差(阻塞其他任务) | 好(可响应其他中断) | 最好(几乎无干扰) |
| 适用场景 | 短数据、调试 | 交互式、不定长 | 大数据量、高速率 |
| 代码复杂度 | 低 | 中(需处理回调) | 高(需配置DMA) |
| 资源消耗 | 无额外资源 | 占用NVIC中断 | 占用DMA通道+中断 |
说白了,没有银弹。你得根据项目需求来选。我个人习惯是:调试用阻塞,交互用中断,大数据用DMA。
4.7 UART收发模式选择流程图
下面这张图,帮你快速决策:
4.8 实战建议:如何写出可移植的UART抽象层
最后,分享一点我的经验。写UART抽象层时,别把代码写死。我建议你这样做:
- 定义统一的接口:比如
UART_Send(uint8_t *data, uint16_t len),内部再根据配置选择阻塞/中断/DMA。 - 用回调函数解耦:接收完成时,调用一个注册的回调,而不是直接处理数据。这样上层逻辑和底层驱动就分开了。
- 预留超时机制:不管是阻塞还是中断,都要有超时保护。我曾经因为没加超时,导致系统在接收时死锁,最后只能看门狗复位。
- 环形缓冲区是标配:中断或DMA接收的数据,先丢进环形缓冲区,主循环再慢慢取。这样不会丢数据,也不会阻塞中断。