2、硬件接口与通信协议:I2C、SPI、UART、1-Wire协议原理与对比、硬件连接实战
各位同学,咱们今天聊点硬核的。传感器采集数据,说白了就是让MCU和传感器“说话”。怎么说话?靠的就是硬件接口和通信协议。
我这些年做项目,从温湿度传感器到九轴惯性测量单元,从几块钱的国产芯片到进口的高精度模数转换器,几乎每天都在跟这几种协议打交道。你想想看,如果连它们都搞不清楚,那传感器数据采集就是一句空话。
今天咱们就把I2C、SPI、UART、1-Wire这四种最常用的协议,掰开了揉碎了讲清楚。我会结合我踩过的坑,告诉你什么时候该用谁,怎么连线,怎么调通。
2.1 为什么需要这么多协议?
你可能会有疑问:为什么不能统一用一种协议?嗯,这个问题我当年也问过师傅。
答案其实很简单:没有银弹。不同的传感器对速度、功耗、引脚数量、传输距离的要求完全不同。比如一个数字温度传感器,每秒只传一次数据,你用高速SPI就是杀鸡用牛刀。反过来,一个高帧率的摄像头,你用I2C去传图像数据,那得等到猴年马月。
所以,搞嵌入式的人,必须把这四种协议都玩熟。它们各有各的脾气,各有各的绝活。
2.2 I2C协议:两线制,多设备,慢工出细活
I2C(Inter-Integrated Circuit),我习惯叫它“I方C”。这是飞利浦公司(现在的恩智浦)搞出来的。它最大的特点就是只用两根线:SCL(时钟线)和SDA(数据线)。
我在项目中遇到过最典型的场景:一个主控板挂载了七八个传感器,包括温度、湿度、气压、光照、距离等等。如果每个传感器都单独占用一个引脚,那MCU的引脚根本不够用。这时候I2C就派上大用场了——所有传感器都挂在同一条总线上,靠地址来区分。
2.2.1 工作原理
I2C是同步、半双工的通信方式。主设备(通常是MCU)产生时钟信号,从设备(传感器)响应。每次通信开始,主设备会发送一个起始条件,然后发送7位或10位的从设备地址,最后一位是读写位。
我建议你记住这个流程:起始 → 地址+读写位 → 应答 → 数据 → 应答 → ... → 停止。这个流程在逻辑分析仪上看得清清楚楚。
关键参数:
- 标准模式:100 kHz
- 快速模式:400 kHz
- 高速模式:3.4 MHz
- 最多可挂载:理论127个设备(7位地址)
2.2.2 硬件连接
连接方式非常简单:所有设备的SCL连在一起,SDA连在一起,然后分别通过上拉电阻接到VCC(通常是3.3V或5V)。上拉电阻的阻值很关键,我一般选4.7kΩ。如果总线太长或者设备太多,可以换成2.2kΩ。
// I2C读取传感器数据的伪代码示例
// 以读取温度传感器为例
void read_temperature() {
i2c_start(); // 发送起始条件
i2c_write(0x90); // 设备地址+写位 (0x48 << 1 | 0)
i2c_wait_ack(); // 等待应答
i2c_write(0x00); // 发送寄存器地址
i2c_wait_ack();
i2c_stop(); // 发送停止条件
i2c_start(); // 重新起始
i2c_write(0x91); // 设备地址+读位
i2c_wait_ack();
uint8_t msb = i2c_read(); // 读取高字节
i2c_send_ack(); // 发送应答
uint8_t lsb = i2c_read(); // 读取低字节
i2c_send_nack(); // 发送非应答
i2c_stop();
temperature = (msb << 8) | lsb;
}
我的小技巧:调试I2C时,如果发现设备不响应,先检查上拉电阻焊了没有。我曾经有一次焊了板子发现SDA一直低电平,查了半天发现是上拉电阻虚焊了。另外,地址冲突也是常见问题,记得用示波器或者逻辑分析仪看看总线上有没有正确的地址帧。
2.3 SPI协议:四线制,高速,全双工
SPI(Serial Peripheral Interface),摩托罗拉发明的。如果说I2C是公交车,那SPI就是专车——速度快、效率高、一对一通信。
SPI用了四根线:
- SCLK:时钟线,由主设备产生
- MOSI:主设备输出,从设备输入
- MISO:主设备输入,从设备输出
- SS/CS:片选线,低电平有效
你想想看,SPI是全双工的,也就是说发送和接收可以同时进行。这一点比I2C强太多了。我做过一个高速数据采集项目,ADC的采样率是1 Msps,用I2C根本扛不住,换成SPI就轻松搞定了。
2.3.1 四种模式
SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。这个坑我踩过好几次——传感器手册上写着“Mode 3”,我默认用了Mode 0,结果读出来的数据全是乱的。
| 模式 | CPOL | CPHA | 说明 |
|---|---|---|---|
| Mode 0 | 0 | 0 | 空闲时时钟低电平,第一个边沿采样 |
| Mode 1 | 0 | 1 | 空闲时时钟低电平,第二个边沿采样 |
| Mode 2 | 1 | 0 | 空闲时时钟高电平,第一个边沿采样 |
| Mode 3 | 1 | 1 | 空闲时时钟高电平,第二个边沿采样 |
注意:SPI的片选信号一定要处理好。我曾经遇到过一个问题:多个SPI设备共用总线,其中一个设备的片选信号没拉高,导致总线冲突,所有设备都通信异常。记住,同一时刻只能有一个片选有效。
2.3.2 硬件连接实战
SPI的连接比I2C稍微复杂一点,因为每个从设备都需要独立的片选线。如果MCU的GPIO不够用,可以用译码器或者GPIO扩展芯片。
// SPI读取加速度计数据的示例
void spi_read_accel() {
// 假设使用STM32的HAL库
uint8_t tx_data[2];
uint8_t rx_data[2];
// 读取X轴加速度,寄存器地址0x28
tx_data[0] = 0x28 | 0x80; // 最高位置1表示读操作
tx_data[1] = 0x00; // 随便发,为了接收
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); // 拉低片选
HAL_SPI_TransmitReceive(&hspi1, tx_data, rx_data, 2, 100);
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 拉高片选
int16_t accel_x = (rx_data[0] << 8) | rx_data[1];
}
2.4 UART协议:异步,点对点,简单可靠
UART(Universal Asynchronous Receiver/Transmitter),也就是我们常说的串口。这是最古老、最通用的协议之一。我敢说,每个嵌入式工程师入门都是从点亮LED和串口打印“Hello World”开始的。
UART是异步、全双工的通信方式。它只需要两根线:TX(发送)和RX(接收)。没有时钟线,所以通信双方必须约定好相同的波特率。
2.4.1 数据帧格式
一个典型的UART数据帧包含:起始位(1位)、数据位(5~9位,常用8位)、校验位(可选)、停止位(1~2位)。
我建议你记住这个配置:9600-8-N-1。意思是波特率9600,8位数据位,无校验,1位停止位。这是最常用的配置,很多传感器和模块出厂默认就是这个。
常见波特率: 9600、19200、38400、57600、115200、230400、460800、921600
注意:波特率越高,传输距离越短,抗干扰能力越差。我在工业现场用过115200,线长超过2米就开始丢数据了。
2.4.2 硬件连接注意事项
UART连接时,一定要交叉连接:设备的TX接主控的RX,设备的RX接主控的TX。这个低级错误我见过太多新手犯了——把TX接TX,RX接RX,结果怎么调都调不通。
另外,电平转换也很重要。很多传感器是3.3V电平,但有些老模块是5V的。如果直接连接,可能会烧坏IO口。我一般用电平转换芯片,或者用电阻分压。
// UART接收传感器数据的示例
void uart_receive_data() {
uint8_t buffer[64];
uint8_t index = 0;
while (1) {
// 等待接收一个字节
while (!(UCSR0A & (1 << RXC0)));
uint8_t data = UDR0;
// 假设传感器数据以0xAA开头,0x55结尾
if (data == 0xAA) {
index = 0;
} else if (data == 0x55) {
// 一帧数据接收完毕,处理
process_sensor_data(buffer, index);
break;
} else {
if (index < 64) {
buffer[index++] = data;
}
}
}
}
2.5 1-Wire协议:一根线,省引脚,但慢
1-Wire协议,达拉斯半导体(现属美信)的杰作。顾名思义,只用一根数据线,既传数据又传供电(寄生供电模式)。
最典型的应用就是DS18B20温度传感器。我做过一个项目,需要在一条线上挂载20个DS18B20,每个传感器都有唯一的64位ROM编码,通过搜索算法可以逐个访问。说实话,这个协议的速度确实慢,但胜在省引脚。
2.5.1 工作时序
1-Wire的时序要求非常严格,必须精确到微秒级。它有三种基本信号:
- 复位脉冲:主机拉低480μs,然后释放
- 存在脉冲:从设备拉低60~240μs作为应答
- 写时隙:写0拉低60μs,写1拉低1~15μs后释放
- 读时隙:主机拉低1~15μs后释放,从设备在15μs内将数据放到总线上
我的经验:1-Wire的时序对延时非常敏感。如果你用Arduino,直接用delayMicroseconds()可能不准,最好用定时器中断或者硬件1-Wire外设。我曾在STM32上用软件模拟1-Wire,折腾了两天才把时序调对。
2.5.2 硬件连接
1-Wire的连接更简单:数据线接一个4.7kΩ上拉电阻到VCC,然后所有设备的DQ引脚连在一起。如果使用寄生供电模式,VCC和GND可以不用接。
// 读取DS18B20温度的简化代码
float read_ds18b20() {
uint8_t data[9];
// 复位
onewire_reset();
// 跳过ROM(单设备时使用)
onewire_write(0xCC);
// 启动温度转换
onewire_write(0x44);
delay_ms(750); // 等待转换完成
// 复位
onewire_reset();
onewire_write(0xCC);
// 读取暂存器
onewire_write(0xBE);
for (int i = 0; i < 9; i++) {
data[i] = onewire_read();
}
// 计算温度
int16_t raw = (data[1] << 8) | data[0];
return raw * 0.0625;
}
2.6 四大协议对比总结
好了,四种协议都讲完了。我最后给你一张对比表,方便你以后选型时参考。
| 特性 | I2C | SPI | UART | 1-Wire |
|---|---|---|---|---|
| 线数 | 2(SCL+SDA) | 4(SCLK+MOSI+MISO+CS) | 2(TX+RX) | 1(DQ) |
| 通信方式 | 同步、半双工 | 同步、全双工 | 异步、全双工 | 异步、半双工 |
| 速度 | 100kHz~3.4MHz | 最高可达几十MHz | 最高可达几Mbps | 约15kbps |
| 多设备支持 | 是(地址区分) | 是(片选区分) | 否(点对点) | 是(ROM编码区分) |
| 传输距离 | 短(<1m) | 短(<1m) | 中(可达10m+) | 短(<10m) |
| 典型应用 | 温湿度、气压、加速度计 | ADC、DAC、显示屏、SD卡 | GPS、蓝牙、WiFi模块 | DS18B20、iButton |
2.7 实战选型建议
最后,我根据我的项目经验,给你一些选型建议:
- 引脚紧张、设备多、速度要求不高 → 选I2C。比如一个环境监测站,挂一堆温湿度、气压、光照传感器。
- 需要高速、大数据量传输 → 选SPI。比如高速ADC、TFT屏幕、SD卡读写。
- 点对点通信、距离较远、模块化设计 → 选UART。比如GPS模块、蓝牙模块、串口屏。
- 极度省引脚、温度测量场景 → 选1-Wire。比如分布式温度监测系统。
嗯,这一章的内容就到这里。下一章咱们会深入讲I2C的时序细节和调试技巧,到时候我会带一个逻辑分析仪的实战案例。记得提前准备好你的开发板。