4、通信协议基础:UART、I2C、SPI协议的基本帧结构解析

做模组联调,说白了就是让两个芯片“说话”。但芯片之间没有嘴巴,也没有耳朵。它们靠的是几根金属线上的电平变化来传递信息。UART、I2C、SPI,就是最常见的三种“说话方式”。

我刚开始做嵌入式那会儿,总觉得协议是理论课的东西。直到第一次调Wi-Fi模组,死活收不到数据,拿逻辑分析仪一看——波特率配错了。嗯,从那以后我再也不敢小看帧结构了。

这一章,咱们就把这三个协议扒开来看。不扯太多历史,只看帧结构、时序、以及联调时最容易踩的坑。

4.1 UART:最朴素的“你一句我一句”

UART(Universal Asynchronous Receiver/Transmitter)是异步串行通信。什么叫异步?就是没有单独的时钟线。收发双方得事先约定好“语速”——也就是波特率。

UART的帧结构很简单,一条数据线(TX),一条接收线(RX),再加一条共地线(GND)。

4.1.1 帧结构

一个完整的UART数据帧长这样:

起始位(1bit) + 数据位(5~8bit) + 校验位(0/1bit) + 停止位(1/1.5/2bit)

举个例子,发送一个字节 0x55(二进制 01010101),8N1格式(8数据位、无校验、1停止位):

空闲(高) → 起始位(低) → 0 → 1 → 0 → 1 → 0 → 1 → 0 → 1 → 停止位(高) → 空闲(高)

注意:数据位是LSB first,也就是最低位先发。0x55的LSB是1,所以第一个数据位是1,不是0。

联调要点:UART通信双方必须完全一致:波特率、数据位长度、校验模式、停止位位数。任何一个不匹配,收到的就是乱码。

4.1.2 我踩过的坑

我曾经调一个4G模组,波特率设的是115200,模组默认也是115200。但就是收不到正确数据。拿示波器一看,波形宽度不对。后来发现模组内部晶振精度不够,实际波特率偏了2.3%。连续传几十个字节后,累积误差就把帧同步丢了。

解决办法:要么换高精度晶振,要么降低波特率。降到9600后,稳如老狗。

避坑指南:联调时如果UART数据偶尔错一两个字节,先别怀疑代码。用示波器或逻辑分析仪看波形,数一数每个bit的宽度对不对。我曾经花了一整天查代码,最后发现是地线没接好。

4.2 I2C:多设备共享两根线

I2C(Inter-Integrated Circuit)是同步半双工通信。它只有两根线:SCL(时钟线)和SDA(数据线)。但可以挂多个设备,每个设备有唯一的地址。

我刚开始用I2C时觉得挺神奇——两根线怎么区分谁在说话?后来明白了,靠的是“主从”关系和“地址”。

4.2.1 帧结构

I2C的帧结构比UART复杂一些:

起始条件 + 7位从机地址 + R/W位 + 应答位 + 数据字节(8位) + 应答位 + ... + 停止条件

关键时序点:

  • 起始条件:SCL高电平时,SDA从高变低
  • 停止条件:SCL高电平时,SDA从低变高
  • 数据采样:SCL高电平时,SDA上的数据必须稳定
  • 应答位:接收方在第9个SCL周期拉低SDA表示ACK

个人习惯:写I2C驱动时,我习惯把起始条件、停止条件、发送字节、接收字节都封装成独立函数。这样调试时可以在每个步骤加打印,定位问题快很多。

4.2.2 常见问题

I2C联调时最常遇到的是“总线锁死”。SDA被某个设备一直拉低,主设备发不了起始条件。

为什么会这样?从设备在应答位之后没释放SDA,或者主设备在通信过程中异常复位了,但从设备还在等后续时钟。

解决办法:给从设备硬件复位,或者连续发9个SCL时钟脉冲让从设备退出异常状态。我一般在初始化代码里加一段“总线恢复”逻辑:

// 伪代码:I2C总线恢复
void i2c_bus_recover(void) {
    // 先发9个时钟脉冲
    for (int i = 0; i < 9; i++) {
        SCL = 1; delay();
        SCL = 0; delay();
    }
    // 再发停止条件
    SDA = 0; delay();
    SCL = 1; delay();
    SDA = 1; delay();
}

避坑指南:I2C的上拉电阻不能省。我见过有人把上拉电阻焊成0欧,结果SDA和SCL波形方波变三角波,通信距离稍远一点就出错。4.7kΩ是通用值,高速模式用2.2kΩ。

4.3 SPI:全双工,速度快

SPI(Serial Peripheral Interface)是同步全双工通信。它需要4根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。

SPI的速度通常比I2C快很多。我调过的一个LCD屏,SPI时钟跑到40MHz,刷一帧画面只要几毫秒。I2C的话,同样数据量得几百毫秒。

4.3.1 帧结构

SPI的帧结构相对简单:

CS拉低 → 每个SCLK时钟沿移出一位数据 → CS拉高

但SPI有4种模式,由CPOL(时钟极性)和CPHA(时钟相位)决定:

模式 CPOL CPHA 采样沿 空闲时钟电平
模式0 0 0 上升沿 低
模式1 0 1 下降沿 低
模式2 1 0 上升沿 高
模式3 1 1 下降沿 高

联调时,主设备和从设备的SPI模式必须一致。我见过有人模式0和模式3混用,数据能通但偶尔错位,查了两天才发现。

4.3.2 多从机连接

SPI支持一主多从。每个从机独立占用一个CS引脚。但要注意:从机的MISO引脚通常是三态输出,只有自己被选中时才驱动MISO。否则多个从机同时驱动MISO,会打架。

联调要点:SPI通信时,如果MISO波形异常(比如出现不该有的低电平),先检查CS信号是否正常。我遇到过CS引脚虚焊,从机一直处于选中状态,结果和另一个从机抢MISO总线。

4.4 三种协议对比

做个表格,方便你选型时参考:

特性 UART I2C SPI
线数 2(TX/RX) 2(SCL/SDA) 4(SCLK/MOSI/MISO/CS)
同步/异步 异步 同步 同步
全双工/半双工 全双工 半双工 全双工
多设备支持 点对点 多设备(地址寻址) 多设备(片选寻址)
典型速率 9600~115200 bps 100kHz~3.4MHz 10MHz~80MHz
硬件复杂度 低 中 中高
联调常见问题 波特率不匹配、地线干扰 总线锁死、上拉电阻不当 模式不匹配、CS时序错误

4.5 知识体系总览

下面这张图,把三种协议的帧结构、关键参数和联调要点串起来了。你可以把它当作联调时的速查图。

通信协议帧结构总览 UART 异步 · 全双工 · 点对点 帧结构: 起始位(1) + 数据位(5~8) + 校验位(0/1) + 停止位(1/1.5/2) 关键参数:波特率、数据位、校验、停止位 联调要点:双方参数必须完全一致 I2C 同步 · 半双工 · 多设备 帧结构: 起始 + 7位地址 + R/W + ACK + 数据(8位) + ACK + ... + 停止 关键参数:速率、地址、上拉电阻 联调要点:总线锁死、ACK检测 SPI 同步 · 全双工 · 多设备 帧结构: CS拉低 → 每时钟移1位 → CS拉高 4种模式:CPOL+CPHA组合 关键参数:时钟极性、相位、速率 联调要点:模式匹配、CS时序 联调通用流程 1. 确认硬件连接 2. 配置协议参数 3. 发送测试数据 4. 抓波形验证 5. 确认通信 提示:联调时优先用逻辑分析仪抓波形,不要只靠打印日志

4.6 联调时的通用思路

不管用哪种协议,联调步骤其实差不多。我总结了一个“五步法”:

  1. 确认硬件连接:用万用表量通断,确认TX/RX没接反,SCL/SDA没短路,CS引脚电平正常。
  2. 配置协议参数:波特率、模式、地址,主从两端必须一致。
  3. 发送测试数据:先发固定模式(如0x55、0xAA),方便看波形。
  4. 抓波形验证:逻辑分析仪或示波器,看起始条件、数据位、停止条件是否完整。
  5. 确认通信:收发数据一致,才算联调通过。

我的习惯:联调时先在从设备端把收到的原始数据通过另一个UART打印出来。这样能快速判断是主设备没发对,还是从设备没收到。省得两边互相怀疑。

好了,这一章的内容就到这里。UART、I2C、SPI的帧结构,说白了就是“约定好怎么说话”。你只要把起始、数据、结束这三个阶段搞清楚,联调时再配合波形验证,基本不会出大问题。