3. 数据格式与协议:I2C、SPI、UART 详解与传感器数据帧结构解析
各位同学,大家好。今天我们来聊聊嵌入式系统里最基础、也最绕不开的话题——传感器与主控芯片之间的“对话方式”。说白了,就是 I2C、SPI、UART 这三种协议。
我在做边缘 AI 项目时,经常遇到新手拿着传感器数据手册,对着时序图发懵。嗯,这很正常。我自己刚入行那会儿,也踩过不少坑。今天我就把实战中积累的经验,掰开揉碎了讲给你们听。
3.1 I2C 协议详解
I2C,全称 Inter-Integrated Circuit。它最大的特点就是“省线”——只需要两根线:SCL(时钟线)和 SDA(数据线)。
核心要点:
- 主从架构:一个主设备(比如 MCU)可以挂多个从设备(传感器)。每个从设备有唯一 7 位或 10 位地址。
- 起始与停止条件:SCL 高电平时,SDA 从高变低是起始;SCL 高电平时,SDA 从低变高是停止。
- 数据帧格式:每 8 位数据后跟一个 ACK/NACK 位。主设备发送地址+读写位,从设备应答后开始传输数据。
我个人的习惯:在调试 I2C 设备时,先用逻辑分析仪抓一下波形。看一眼起始条件、地址字节和 ACK 位,基本就能判断通信是否正常。这比盯着代码看半天有效得多。
避坑指南:我曾经在一个项目里,因为上拉电阻选得太小(1kΩ),导致总线负载过重,通信时好时坏。后来换成 4.7kΩ,问题就解决了。记住,I2C 总线必须要有上拉电阻,典型值 4.7kΩ 到 10kΩ。
3.2 SPI 协议详解
SPI,全称 Serial Peripheral Interface。它比 I2C 快得多,但需要更多引脚。标准 SPI 用四根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。
工作模式:SPI 有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。你想想看,这其实就是决定数据在时钟的哪个边沿采样。
| 模式 | CPOL | CPHA | 采样边沿 |
|---|---|---|---|
| 0 | 0 | 0 | 上升沿 |
| 1 | 0 | 1 | 下降沿 |
| 2 | 1 | 0 | 下降沿 |
| 3 | 1 | 1 | 上升沿 |
小技巧:我建议你在初始化 SPI 外设时,先读一下传感器数据手册里的时序图。上面会明确标注 CPOL 和 CPHA 的值。别想当然地用默认模式 0,很多传感器用的是模式 3。
数据帧结构:SPI 是全双工的。主设备发一个字节的同时,也会收到从设备发来的一个字节。所以,读传感器数据时,主设备需要发送“空字节”来产生时钟。
// 示例:SPI 读取传感器数据
uint8_t spi_read_byte(uint8_t reg_addr) {
uint8_t data;
// 拉低片选
CS_LOW();
// 发送寄存器地址(写操作)
spi_transfer(reg_addr);
// 发送空字节,同时接收数据
data = spi_transfer(0x00);
// 拉高片选
CS_HIGH();
return data;
}
3.3 UART 协议详解
UART,全称 Universal Asynchronous Receiver/Transmitter。它最“古老”,但也最常用。只需要 TX(发送)和 RX(接收)两根线。
异步通信:没有时钟线,靠波特率来同步。常见的波特率有 9600、115200 等。双方必须设置相同的波特率,否则数据会错乱。
数据帧格式:起始位(1 位低电平)+ 数据位(5-8 位)+ 校验位(可选)+ 停止位(1 或 2 位高电平)。
注意:我曾经在调试一个 GPS 模块时,发现数据全是乱码。查了半天,原来是波特率设置成了 9600,但模块默认是 115200。这种低级错误,大家一定要避免。上电后先看模块输出的第一帧数据,通常能看出波特率。
实战经验:UART 在短距离通信中很可靠,但抗干扰能力一般。如果线缆超过 1 米,我建议用 RS-232 或 RS-485 电平转换。
3.4 传感器数据帧结构解析
好了,协议讲完了。但光会发收数据还不够,你得知道传感器返回的数据长什么样。这就是数据帧结构。
大多数传感器会返回一个固定格式的数据包。以我常用的 MPU6050 六轴传感器为例:
// MPU6050 加速度计数据读取示例
// 寄存器地址:0x3B (ACCEL_XOUT_H)
uint8_t buffer[14];
// 连续读取 14 个字节
i2c_read(MPU6050_ADDR, 0x3B, buffer, 14);
// 解析加速度计数据
int16_t accel_x = (buffer[0] << 8) | buffer[1];
int16_t accel_y = (buffer[2] << 8) | buffer[3];
int16_t accel_z = (buffer[4] << 8) | buffer[5];
// 解析温度数据
int16_t temp = (buffer[6] << 8) | buffer[7];
// 解析陀螺仪数据
int16_t gyro_x = (buffer[8] << 8) | buffer[9];
int16_t gyro_y = (buffer[10] << 8) | buffer[11];
int16_t gyro_z = (buffer[12] << 8) | buffer[13];
数据帧结构要点:
- 字节序:大端还是小端?MPU6050 是大端(高位在前)。很多传感器也是大端,但有些是小端。一定要看手册确认。
- 数据对齐:16 位数据通常占两个连续寄存器。读取时建议用连续读取,而不是单字节读取,避免数据在两次读取之间更新。
- 校验机制:有些传感器会带 CRC 校验。比如 BME280 环境传感器,数据帧末尾有 CRC 字节。我建议你实现校验,别偷懒。
我的经验:在解析数据帧时,我习惯先定义一个结构体,把寄存器地址、数据长度、缩放因子等信息都放进去。这样代码可读性高,也方便后期维护。你想想看,如果每个传感器都写一堆魔数,三个月后你自己都看不懂。
避坑指南:我曾经遇到一个温度传感器,数据手册上写的是 16 位有符号数,但实际读出来是 14 位有效数据,低两位是状态位。如果不做掩码处理,算出来的温度值会完全不对。所以,读数据时一定要看清楚有效位宽。
好了,今天的内容就到这里。三种协议各有优劣:I2C 省线但速度慢,SPI 速度快但费引脚,UART 简单但需要双方约定波特率。在实际项目中,我通常会根据传感器数量、数据量和引脚资源来选型。下一章,我们会把这些知识用到实战中,写一个完整的传感器驱动。