4. UART串口通信:从原理到实战
UART串口,可以说是嵌入式开发中最基础、也最常用的通信接口了。我刚开始做STM32项目时,第一个调通的通信外设就是UART。说白了,它就是让两个设备之间能“说上话”的最简单方式。
这一章,我会带你从UART的底层原理讲起,然后手把手配置CubeMX,最后用HAL库实现串口打印和数据回显。嗯,这些都是我实际项目中反复用到的套路。
4.1 UART协议原理:一帧数据是怎么传的?
UART全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。它用两根线就能实现全双工通信:TX发、RX收。
为什么叫“异步”?因为没有时钟线。收发双方靠约定的波特率来同步。你想想看,就像两个人约定好每秒说一个字,不用额外喊“开始”和“结束”。
UART一帧数据的标准格式:
- 起始位(1位):拉低电平,告诉接收方“我要开始发了”
- 数据位(5~9位):实际数据,常用8位
- 校验位(可选):奇校验或偶校验,用于检错
- 停止位(1/1.5/2位):拉高电平,表示一帧结束
我在项目中遇到过一个问题:两个设备波特率都是115200,但通信就是乱码。查了半天,发现是停止位设置不一致——一个设了1位,另一个设了2位。所以,收发双方的帧格式必须完全一致。
4.2 CubeMX配置UART:三步搞定
我个人习惯用CubeMX生成初始化代码,省时省力。配置UART其实就三步:选引脚、设参数、生成代码。
4.2.1 引脚选择
STM32的USART外设通常有多个,比如USART1、USART2、USART3。每个外设的TX/RX引脚是固定的。以STM32F103C8T6为例:
| USART外设 | TX引脚 | RX引脚 | 常用场景 |
|---|---|---|---|
| USART1 | PA9 | PA10 | 调试打印、与PC通信 |
| USART2 | PA2 | PA3 | 蓝牙模块、GPS模块 |
| USART3 | PB10 | PB11 | RS485、传感器 |
我的习惯:调试打印用USART1,因为PA9/PA10正好对应板载的USB转串口芯片,插上USB线就能看打印信息。
4.2.2 参数配置
在CubeMX的Pinout & Configuration界面,选中USART1,然后在Configuration选项卡里设置:
- Mode:Asynchronous(异步模式)
- Baud Rate:115200 Bits/s
- Word Length:8 Bits(含校验位时选9)
- Parity:None
- Stop Bits:1
为什么选115200?因为速度快,而且大多数串口工具都支持。如果线比较长或者环境有干扰,可以降到9600。
4.2.3 生成代码
配置完点生成代码,CubeMX会自动创建usart.c和usart.h。你会在main.c里看到类似这样的初始化:
// CubeMX生成的UART初始化代码
static 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;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
}
4.3 HAL库收发函数:核心API
HAL库把UART操作封装成了几个函数。我刚开始用的时候也觉得函数名太长,但用习惯了就会发现,命名很规范,一看就知道是干什么的。
4.3.1 发送函数
| 函数名 | 说明 | 阻塞/非阻塞 |
|---|---|---|
HAL_UART_Transmit |
阻塞发送,发完才返回 | 阻塞 |
HAL_UART_Transmit_IT |
中断发送,后台完成 | 非阻塞 |
HAL_UART_Transmit_DMA |
DMA发送,CPU零负担 | 非阻塞 |
调试阶段我一般用阻塞发送,简单可靠。产品中如果数据量大,我会用DMA。
// 阻塞发送示例:发送"Hello World"
char msg[] = "Hello World\r\n";
HAL_UART_Transmit(&huart1, (uint8_t *)msg, strlen(msg), HAL_MAX_DELAY);
4.3.2 接收函数
| 函数名 | 说明 | 阻塞/非阻塞 |
|---|---|---|
HAL_UART_Receive |
阻塞接收,收够才返回 | 阻塞 |
HAL_UART_Receive_IT |
中断接收,每收到一个字节触发回调 | 非阻塞 |
HAL_UART_Receive_DMA |
DMA接收,批量传输 | 非阻塞 |
我曾经踩过的坑:用HAL_UART_Receive阻塞接收时,如果对方发来的数据长度小于你指定的长度,程序会一直卡在那里。所以,不确定数据长度时,一定要用中断或DMA方式。
4.4 实现串口打印与数据回显
好了,理论讲完了,咱们直接上代码。我会实现两个功能:串口打印和串口回显。
4.4.1 重定向printf到串口
在STM32上,printf默认输出到标准输出,但标准输出没地方去。我们需要重定向_write函数,让printf的数据通过UART发出去。
/* 在usart.c或main.c中添加 */
#include <stdio.h>
#ifdef __GNUC__
/* 使用GCC时 */
int _write(int file, char *ptr, int len)
{
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
return len;
}
#else
/* 使用ARMCC时 */
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
#endif
加上这段代码后,你就可以直接用printf了:
printf("系统启动成功!\r\n");
printf("当前温度:%.2f°C\r\n", temperature);
小技巧:记得在字符串末尾加\r\n,这样串口工具才会换行。不加的话,所有输出会挤在一行。
4.4.2 数据回显:你发什么,我回什么
回显功能很简单:收到一个字节,马上把它发回去。用中断方式实现:
/* 全局变量 */
uint8_t rx_byte; // 接收缓冲区
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init();
printf("UART回显测试开始...\r\n");
/* 启动中断接收,每次接收1个字节 */
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
while (1)
{
/* 主循环可以干别的事 */
HAL_Delay(1000);
printf("主循环运行中...\r\n");
}
}
/* 中断回调函数:收到数据后自动调用 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
/* 把收到的字节发回去(回显) */
HAL_UART_Transmit(&huart1, &rx_byte, 1, HAL_MAX_DELAY);
/* 重新启动中断接收 */
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
}
}
这段代码的逻辑很简单:
- 初始化UART后,启动中断接收
- 每次收到一个字节,回调函数
HAL_UART_RxCpltCallback被触发 - 在回调里,把收到的字节发回去
- 重新启动中断接收,等待下一个字节
关键点:中断接收是一次性的。每次收到指定长度的数据后,中断就停了。所以回调里必须重新调用HAL_UART_Receive_IT,否则只能收到一次数据。
4.5 实战中的避坑指南
最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路。
坑1:波特率误差
STM32的UART时钟来自APB总线。如果系统时钟不是波特率的整数倍,会产生误差。误差超过2%就可能丢数据。我建议用8MHz、72MHz、168MHz这些常见频率,它们对常用波特率支持最好。
坑2:中断优先级
如果系统里同时用了多个中断,UART中断优先级设置不当会导致数据丢失。我一般把UART中断优先级设得比较高,尤其是接收中断。
坑3:缓冲区溢出
中断接收时,如果数据来得太快,而你在回调里处理得太慢,下一个字节就会覆盖上一个。解决办法:用环形缓冲区(ring buffer)或者DMA。
嗯,UART串口通信的内容就这些。从原理到配置,再到代码实现,每一步我都结合项目经验给你讲透了。你可以在开发板上试试回显程序,用串口工具发个"Hello",看看它会不会回你一个"Hello"。