3. 轮询发送代码实现:从HAL库到底层寄存器
这一章我们直接动手写代码。很多初学者用HAL_UART_Transmit()发数据,觉得挺好用。但一旦遇到性能瓶颈或者诡异bug,就完全不知道底层发生了什么。我个人习惯是:先搞懂封装层在干什么,再决定要不要自己写寄存器。
3.1 HAL_UART_Transmit() 到底干了什么?
先看标准库函数原型:
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
四个参数:句柄、数据指针、字节数、超时时间。看起来简单,但内部逻辑其实分三步:
- 检查状态——判断UART是否忙、参数是否合法
- 轮询发送——逐字节写入数据寄存器,等待发送完成标志
- 超时处理——如果超过Timeout还没发完,返回超时错误
我拆过HAL库的源码,核心循环其实就几行:
while(huart->TxXferCount > 0)
{
/* 等待TXE标志置位 */
while(__HAL_UART_GET_FLAG(huart, UART_FLAG_TXE) == RESET)
{
/* 超时检查 */
if(Timeout-- == 0) return HAL_TIMEOUT;
}
/* 写入数据寄存器 */
huart->Instance->DR = (*pData++);
huart->TxXferCount--;
}
看到了吗?说白了就是两个while循环嵌套。外层循环控制字节数,内层循环等待发送缓冲区空标志。我在项目中遇到过一个问题:用这个函数发大量数据时,CPU被死死卡住,中断都进不去。这就是轮询模式的代价。
关键点:HAL_UART_Transmit() 本质是「忙等待」——CPU在发送期间不能干别的事。适合数据量小、实时性要求不高的场景。
3.2 寄存器级发送代码编写
为什么要自己写寄存器?两个原因:一是性能,二是可控。我自己写UART驱动时,通常只保留最核心的操作。
先看STM32的UART发送相关寄存器:
| 寄存器 | 功能 | 关键位 |
|---|---|---|
| USART_DR | 数据寄存器 | DR[8:0]:要发送的数据 |
| USART_SR | 状态寄存器 | TXE(bit7):发送缓冲区空;TC(bit6):发送完成 |
| USART_CR1 | 控制寄存器1 | UE(bit13):UART使能;TE(bit3):发送使能 |
嗯,这里要注意:TXE和TC是两个不同的标志。TXE表示数据已经从DR移到了移位寄存器,可以写入下一个字节。TC表示移位寄存器里的数据已经全部从TX脚发出去了。
我写一个最简的轮询发送函数:
void UART_SendByte(USART_TypeDef *UARTx, uint8_t data)
{
/* 等待TXE置位,表示DR为空 */
while(!(UARTx->SR & USART_SR_TXE));
/* 写入数据 */
UARTx->DR = data;
}
void UART_SendString(USART_TypeDef *UARTx, uint8_t *str, uint16_t len)
{
for(uint16_t i = 0; i < len; i++)
{
UART_SendByte(UARTx, str[i]);
}
/* 等待最后一个字节发送完成 */
while(!(UARTx->SR & USART_SR_TC));
}
这段代码比HAL库精简多了。但有个坑——我刚开始写的时候,忘了最后那个TC等待。结果最后一个字节经常发不出去,因为函数返回时数据还在移位寄存器里。你想想看,如果紧接着关UART或者进低功耗模式,那个字节就丢了。
避坑指南:我曾经在一个批量产项目中,因为没等TC标志,导致最后几个字节偶尔丢失。排查了两天才找到原因。记住:TXE只保证DR空了,TC才保证数据真正发出去了。
3.3 HAL库 vs 寄存器:性能对比
我实际测过两种方式的性能差异。用逻辑分析仪抓波形,同样发1024字节:
| 方式 | 代码量 | 每字节额外开销 | CPU占用率 |
|---|---|---|---|
| HAL_UART_Transmit() | 1行调用 | 约2.5μs (72MHz) | 100% |
| 寄存器直接操作 | 5行函数 | 约0.8μs (72MHz) | 100% |
寄存器方式快了3倍左右。为什么?因为HAL库多了很多安全检查、状态机维护、超时计算的开销。如果你的系统对发送速度有要求,或者CPU资源紧张,我建议用寄存器方式。
但话说回来,HAL库也有它的价值。它帮你处理了各种边界情况,比如UART未使能、DMA冲突等。我自己做原型验证时,还是会先用HAL库,等确定方案后再优化成寄存器。
3.4 知识结构图:轮询发送的核心逻辑
下面这张图展示了轮询发送的完整流程,从应用层到寄存器层:
3.5 实际项目中的选择策略
我做了这么多年嵌入式,总结了一个简单的选择原则:
- 调试阶段:用HAL库,快速验证功能
- 量产代码:寄存器操作,追求性能和可靠性
- 混合使用:初始化用HAL库,发送用寄存器
个人技巧:我习惯在寄存器发送函数里加一个超时保护,防止硬件异常导致死循环。用一个递减的循环变量做看门狗,超时后强制退出并返回错误码。这个习惯帮我避免了好几次产线死机问题。