4、串口通信与协议:UART异步通信原理、printf重定向与串口调试、Modbus RTU协议实现、自定义通信协议设计
串口通信,说白了就是嵌入式系统里最基础、最常用的「对话方式」。我做了这么多年嵌入式,几乎每个项目都离不开它。无论是调试打印、传感器数据采集,还是与上位机通信,串口都是那个最可靠的「老伙计」。
这一章,咱们就把它彻底吃透。从最底层的UART原理,到实用的printf重定向,再到工业界标准的Modbus RTU协议,最后聊聊怎么设计一套自己的通信协议。嗯,内容不少,但都是硬货。
4.1 UART异步通信原理
UART,全称是Universal Asynchronous Receiver/Transmitter。注意「异步」这两个字——它没有独立的时钟线,全靠双方约定好的波特率来同步。
为什么会这样?因为串口线只有两根:TX(发送)和RX(接收)。没有时钟线,那就得靠「约定」来对齐节奏。就像两个人跳舞,没有指挥,那就得心里默念节拍。
数据帧格式,我习惯把它拆成四部分来看:
- 起始位:1位,逻辑0。告诉接收方「我要开始发了」。
- 数据位:通常5~8位,最常见的是8位。低位在前,高位在后。
- 校验位:可选,奇校验或偶校验。用于简单的错误检测。
- 停止位:1位或2位,逻辑1。告诉接收方「这一帧结束了」。
举个例子,发送一个字节0x55(二进制01010101),波特率9600,8N1(8数据位、无校验、1停止位)。波形会是这样:
起始位(0) + 1 + 0 + 1 + 0 + 1 + 0 + 1 + 0 + 停止位(1)
你想想看,0x55的二进制正好是01010101,交替变化。这在示波器上看是最漂亮的波形,也是我调试串口时最爱发的测试数据。
个人经验:调试串口时,如果数据全是乱码,90%是波特率没对上。我曾经在一个项目里排查了整整半天,最后发现是晶振频率偏差导致波特率误差超过了2%。记住,波特率误差要控制在±2%以内,否则长数据帧就会出错。
4.2 printf重定向与串口调试
做嵌入式开发,printf就是我们的「眼睛」。但MCU上可没有标准输出,怎么办?重定向!
说白了,就是把printf底层调用的那个字符输出函数,改成我们自己的串口发送函数。以STM32的HAL库为例,重定向其实就两步:
// 第一步:包含头文件
#include <stdio.h>
// 第二步:实现fputc函数
int fputc(int ch, FILE *f)
{
// 等待发送完成
while(!(USART1->SR & USART_SR_TXE));
// 发送一个字节
USART1->DR = (uint8_t)ch;
return ch;
}
搞定之后,你就可以在代码里随意写printf了。但要注意,printf是有代价的——它很慢。尤其是浮点数打印,会占用大量CPU时间。
避坑指南:我曾经在一个实时性要求很高的电机控制项目里,在中断服务函数中用了printf。结果电机一启动就卡死。为什么?因为printf是阻塞的,它会一直等串口发送完才返回。在中断里用阻塞函数,等于自杀。记住:中断里不要用printf,用标志位或环形缓冲区。
我个人的习惯是,调试阶段用printf,发布版本全部注释掉。或者用宏开关控制:
#define DEBUG_ENABLE 1
#if DEBUG_ENABLE
#define DEBUG_PRINT(fmt, ...) printf(fmt, ##__VA_ARGS__)
#else
#define DEBUG_PRINT(fmt, ...)
#endif
这样,发布时只需要把DEBUG_ENABLE改成0,所有调试打印就消失了,不影响性能。
4.3 Modbus RTU协议实现
Modbus RTU,工业自动化领域的事实标准。我做过不下十个Modbus项目,从温控器到变频器,从PLC到智能仪表,几乎都在用。
它的帧结构很简单:
| 字段 | 长度 | 说明 |
|---|---|---|
| 地址码 | 1字节 | 从机地址,1~247 |
| 功能码 | 1字节 | 如03读保持寄存器,06写单个寄存器 |
| 数据区 | N字节 | 具体数据内容 |
| CRC校验 | 2字节 | 低字节在前,高字节在后 |
这里最容易被忽略的是CRC校验。Modbus RTU用的是CRC-16,多项式0x8005。我建议直接查表法,速度快,代码也简洁:
uint16_t Modbus_CRC16(uint8_t *buf, uint16_t len)
{
uint16_t crc = 0xFFFF;
for(uint16_t i = 0; i < len; i++)
{
crc ^= buf[i];
for(uint8_t j = 0; j < 8; j++)
{
if(crc & 0x0001)
crc = (crc >> 1) ^ 0xA001;
else
crc >>= 1;
}
}
return crc;
}
实现Modbus从机时,核心就是一个状态机。我习惯用以下状态:
- IDLE:空闲状态,等待起始字节
- RECV:接收数据中
- CHECK:校验CRC
- PROCESS:处理命令
- SEND:发送响应
关键点:Modbus RTU的帧间隔是3.5个字符时间。如果两个字节之间的间隔超过这个时间,接收方就认为一帧结束了。这个时间怎么算?以9600波特率为例,一个字符约1ms,3.5个字符就是3.5ms。定时器要精确到毫秒级。
4.4 自定义通信协议设计
有时候Modbus太重量级,或者功能不匹配,那就得自己设计协议。我设计过不下五种自定义协议,总结下来,核心就四个字:可靠、简洁。
一个典型的自定义协议帧结构:
帧头(2字节) + 长度(1字节) + 命令(1字节) + 数据(N字节) + 校验(1字节) + 帧尾(2字节)
帧头我常用0xAA 0x55,帧尾用0x0D 0x0A(回车换行)。校验用累加和,简单够用。
设计协议时,有几个坑我踩过,分享给你:
- 帧头不要用0x00或0xFF:这些值在串口空闲时容易出现,容易误触发。
- 长度字段要包含自身吗?:我建议不包含,这样解析时更直观。
- 转义处理:如果数据里出现了帧头相同的字节,需要做转义。比如0xAA变成0xAA 0x01,0x55变成0xAA 0x02。
我的习惯:协议设计好后,先写一个PC端的测试工具,用Python的pyserial库模拟主机。这样可以在没有硬件的情况下验证协议的正确性。我曾经用这个方法,在硬件回来之前就把整个协议栈调通了,省了不少时间。
最后,关于协议设计,我想说:不要过度设计。很多新手喜欢把协议搞得特别复杂,各种版本号、时间戳、扩展字段。其实大多数场景下,一个简单的「帧头+命令+数据+校验」就够了。等以后需要扩展时,再升级版本也不迟。
嗯,串口通信这块,内容就这么多。从原理到实践,从标准协议到自定义设计,希望能帮你建立起一个完整的知识体系。下一章,咱们聊聊定时器和PWM,那又是另一片天地了。