4. 数据链路层:UART帧格式、波特率选择、起始位/停止位/校验位
好,咱们进入数据链路层的第一站——UART。
UART这东西,说白了就是串口通信的物理实现。你想想看,嵌入式设备之间要聊天,总得有个规矩吧?谁先说话,说多快,一句话多长,说完怎么知道结束了——这些规矩,就是UART帧格式要解决的问题。
4.1 UART帧格式:一次说一个字节
UART的帧结构其实特别简单。我刚开始学的时候,觉得这东西不就是发数据嘛,有啥好讲的?后来踩了坑才明白,细节都在帧格式里。
一个标准的UART帧,包含这几部分:
- 起始位(Start Bit):1位,固定为低电平(逻辑0)。告诉接收方:“我要开始说话了!”
- 数据位(Data Bits):5~9位,最常见的是8位。这就是你要传的实际数据。
- 校验位(Parity Bit):可选,0或1位。用来做简单的错误检测。
- 停止位(Stop Bit):1位、1.5位或2位,固定为高电平(逻辑1)。告诉接收方:“我说完了!”
嗯,这里要注意:UART是异步通信,没有时钟线。全靠起始位和停止位来同步。所以这两个位要是搞错了,整个通信就乱套了。
典型配置:8N1
8位数据位 + 无校验 + 1位停止位。这是嵌入式世界里的“普通话”,几乎所有MCU默认都支持。
我在项目中遇到过一件事:有个同事把数据位配成了7位,结果上位机死活收不到正确数据。查了两天才发现,他用的8N1,但下位机配的是7E1。你想想看,一个发8位,一个收7位,能对才怪。
4.2 波特率选择:别太快,也别太慢
波特率,就是每秒传输的符号数。对于UART来说,一个符号就是一个bit。所以波特率9600,意思就是每秒传9600个bit。
那怎么选波特率?我个人习惯是:
- 9600:调试用,稳定可靠。线长一点也没事。
- 115200:常用速率,适合大多数场景。我大部分项目都用这个。
- 460800以上:高速场景,但线缆质量、距离、干扰都得考虑。
这里有个坑:波特率不是随便选的。MCU的时钟频率决定了它能产生哪些波特率。比如你用16MHz的晶振,想产生一个精确的9600波特率,分频系数是16MHz / 16 / 9600 ≈ 104.17。取整后实际波特率是16MHz / 16 / 104 ≈ 9615,误差只有0.16%,没问题。
但如果你用8MHz的晶振,想产生115200呢?8MHz / 16 / 115200 ≈ 4.34。取整后是4,实际波特率是8MHz / 16 / 4 = 125000。误差高达8.5%!这通信基本就废了。
避坑指南:我曾经在一个项目里,用了内部RC振荡器做时钟源,结果波特率误差太大,通信时好时坏。后来换成外部晶振,问题立刻解决。所以,波特率精度一定要控制在±2%以内,否则丢包率会急剧上升。
常用的波特率及其误差情况,我整理了一个表:
| 目标波特率 | 16MHz晶振 | 8MHz晶振 | 推荐场景 |
|---|---|---|---|
| 9600 | 0.16% ✅ | 0.16% ✅ | 长距离、抗干扰 |
| 38400 | 0.16% ✅ | 0.16% ✅ | 一般通信 |
| 115200 | 0.16% ✅ | 8.5% ❌ | 常用速率 |
| 230400 | 0.16% ✅ | 不可用 | 高速短距 |
4.3 起始位与停止位:通信的握手信号
起始位和停止位,看起来简单,但它们是UART通信的基石。
起始位:为什么是低电平?因为UART空闲时,总线是高电平。从高跳变到低,就是一个明显的“开始”信号。接收方检测到这个下降沿,就开始采样数据位。
停止位:为什么是高电平?因为要给下一个起始位的下降沿做准备。如果停止位是低电平,那下一个起始位就没法区分了。
我建议:在噪声比较大的环境里,用2位停止位。虽然会降低一点传输效率,但能显著提高抗干扰能力。我曾经在一个电机驱动项目里,1位停止位总是收到乱码,改成2位就好了。
4.4 校验位:简单的错误检测
校验位有三种模式:
- 无校验(N):不校验,省一个bit。适合短距离、高可靠场景。
- 奇校验(O):数据位+校验位中,1的个数为奇数。
- 偶校验(E):数据位+校验位中,1的个数为偶数。
举个例子:你要发送0x55(二进制01010101),里面有4个1。
- 奇校验:校验位设为1,让总1的个数变成5(奇数)。
- 偶校验:校验位设为0,让总1的个数保持4(偶数)。
接收方收到后,会检查1的个数是否符合约定。如果不符合,就说明数据出错了。
小技巧:校验位只能检测奇数个bit的错误。如果两个bit同时翻转,校验位是发现不了的。所以它只适合做“轻量级”的错误检测。真正要求高的场景,得用CRC。
4.5 实战:配置一个UART
说了这么多,咱们来点实际的。下面是一个STM32的UART初始化代码,配置成115200、8N1:
// UART初始化:115200波特率,8N1
void UART_Init(void)
{
// 1. 使能时钟
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// 2. 配置GPIO:TX推挽输出,RX浮空输入
GPIOA->CRH &= ~(0xFF << 4); // 清除PA9, PA10配置
GPIOA->CRH |= 0x0B << 4; // PA9: 推挽输出50MHz
GPIOA->CRH |= 0x04 << 8; // PA10: 浮空输入
// 3. 配置UART参数
USART1->BRR = 0x45; // 16MHz时钟下,115200波特率
USART1->CR1 = 0x200C; // 8位数据,无校验,使能UE、TE、RE
USART1->CR2 = 0x0000; // 1位停止位
USART1->CR3 = 0x0000; // 无硬件流控
}
这段代码里,最关键的其实是BRR寄存器的值。0x45是怎么算出来的?
16MHz / 115200 ≈ 138.89,取整后是139,也就是0x8B。但STM32的BRR寄存器是16位的,高4位是小数部分,低12位是整数部分。所以139要写成0x8B,但实际写入的是0x45?
嗯,这里我简化了。实际计算要复杂一些,但原理就是:时钟频率 / (16 × 目标波特率) = 分频系数。分频系数越接近整数,误差越小。
4.6 常见问题与调试技巧
最后,分享几个我调试UART时常用的方法:
- 用示波器看波形:这是最直接的方法。看起始位、数据位、停止位是否完整,电平是否正常。
- 发0x55或0xAA:这两个字节的二进制是01010101和10101010,在示波器上能看到清晰的方波,方便测量波特率。
- 回环测试:把TX和RX短接,发什么收什么。如果回环正常,说明MCU的UART模块没问题。
- 检查地线:UART通信必须共地。我曾经遇到过,两个设备各自供电,没连地线,结果通信全是乱码。
重要提醒:UART的电压等级一定要匹配。3.3V的MCU和5V的MCU直接连,可能会烧坏IO口。电平转换芯片(比如MAX3232)或者分压电路,该用就得用。
好了,这一章的内容就到这里。UART看起来简单,但它是所有串行通信的基础。把帧格式、波特率、校验位这些概念吃透了,后面学I2C、SPI都会轻松很多。