4. UART串口驱动:从框图到printf重定向
串口,嵌入式开发者的老朋友了。说实话,我入行这些年,调试用的最多的就是UART。不管是打印日志、跟传感器通信,还是跟Wi-Fi模块打交道,UART几乎无处不在。今天咱们就把STM32的USART模块彻底聊透。
4.1 USART框图与核心特性
先看框图。USART内部其实不复杂,核心就三块:
- 发送器:数据从TX引脚发出去
- 接收器:数据从RX引脚收进来
- 波特率发生器:决定通信速度
我个人习惯把USART理解成一个「快递站」。你要发的数据先放进发送数据寄存器(TDR),然后移位寄存器按波特率一位一位搬出去。接收反过来,移位寄存器收齐一个字节,再放到接收数据寄存器(RDR)等你来取。
关键特性速览:
- 支持全双工通信——收发可以同时进行
- 可编程的数据字长度(8位或9位)
- 可配置的停止位(0.5、1、1.5、2位)
- 硬件流控制(RTS/CTS)——我很少用,但某些模块需要
- DMA传输支持——大数据量时的救星
嗯,这里要注意:STM32的USART和UART有区别。USART支持同步模式,多了个SCLK时钟引脚。但咱们99%的场景只用异步模式,也就是UART。
4.2 HAL_UART_Init() 配置详解
HAL库把初始化封装成了HAL_UART_Init()。你想想看,这个函数背后到底干了什么?
我拆开给你看:
UART_HandleTypeDef huart1;
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1);
}
每个参数我都踩过坑:
- BaudRate:115200是黄金速率。我曾经图快设成921600,结果线稍微长一点就丢数据。
- WordLength:8位数据位是标准配置。除非你传特殊协议,否则别动它。
- Parity:奇偶校验。我建议平时关掉,除非通信环境特别恶劣。
- OverSampling:16倍过采样更稳定,8倍可以跑更高波特率。
避坑指南:
我曾经在F103上遇到一个诡异问题——串口偶尔收不到数据。查了两天才发现是GPIO复用功能没配置对。记住:HAL_UART_Init()只配USART外设,GPIO的AF模式要单独配!
4.3 中断接收与发送
轮询方式太浪费CPU了。实际项目中,我几乎只用中断方式。
中断接收的流程很简单:
- 使能USART接收中断
- 数据到达时,硬件自动触发中断
- 在中断回调函数里取数据
// 启动中断接收
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
// 中断回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 处理收到的数据
process_byte(rx_byte);
// 重新开启下一次接收
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
}
}
为什么每次接收完要重新调用HAL_UART_Receive_IT()?因为HAL库的设计是一次性的。你调用一次,它只等一个字节。收完了就停,得手动再开。
发送中断类似:
HAL_UART_Transmit_IT(&huart1, tx_buffer, len);
发送完成后会触发HAL_UART_TxCpltCallback()。我一般在这里设置一个标志位,告诉主循环数据发完了。
个人经验:
中断接收里别做复杂处理。我见过有人直接在回调里做协议解析、写Flash……结果中断嵌套把自己搞死了。正确的做法是:中断里只放环形缓冲区,主循环再慢慢处理。
4.4 DMA传输模式
如果你要发大量数据,比如一次发几K字节,用中断方式会频繁进中断,CPU都被占满了。这时候DMA就派上用场了。
DMA传输说白了就是:CPU告诉DMA控制器「帮我把这堆数据搬到USART的发送寄存器」,然后CPU就可以去干别的事了。
// DMA发送
HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
// DMA接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
DMA传输完成会触发回调:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
// 发送完成,可以准备下一批数据了
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 接收完成,缓冲区里有BUFFER_SIZE个字节
}
我记得有个项目需要连续接收GPS数据,每秒输出10条NMEA语句。用中断方式CPU占用率飙到30%,换成DMA后直接降到2%。
注意:
DMA接收有个坑——它是固定长度的。如果你不知道数据什么时候结束,需要用空闲中断(IDLE)配合。具体做法是:DMA一直收着,空闲中断来了就说明一帧数据结束了,这时候去DMA缓冲区里取数据。
4.5 printf重定向实现
调试时最常用的就是printf。但STM32上默认没有printf,得自己重定向。
原理很简单:printf最终会调用fputc(),我们只要把fputc()改成通过USART发送就行。
#include <stdio.h>
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
然后在工程设置里勾选「Use MicroLIB」。为什么?因为标准C库的printf太大了,MicroLIB是精简版,专门给嵌入式用的。
用起来就简单了:
printf("Hello, STM32!\r\n");
printf("Baud rate: %d\r\n", 115200);
避坑指南:
我曾经忘了加\r\n,结果串口助手上一行接一行地显示,根本看不清。记住:\r是回车,\n是换行,两个都要加。
另外,HAL_UART_Transmit的最后一个参数是超时时间。我习惯设HAL_MAX_DELAY,但如果你在中断里调用printf,千万别用这个——会死锁。中断里用HAL_UART_Transmit_IT()或者直接操作寄存器。
4.6 本章小结
UART驱动说难不难,说简单也不简单。核心就几点:
- 初始化时注意GPIO复用配置
- 中断方式适合小数据量,DMA适合大数据量
- printf重定向是调试利器,但注意使用场景
- 环形缓冲区是串口接收的标配
嗯,这些够你应付大部分项目了。下一章咱们聊聊I2C,那个协议比UART稍微复杂点,但套路是一样的。
公众号:蓝海资料掘金营,微信deep3321