3. I2C通信基础:SDA/SCL时序、地址配置、读写流程
好,咱们进入正题。I2C总线,说白了就是两根线搞定一切通信。一根时钟线SCL,一根数据线SDA。你别看它简单,这里面的门道可不少。我在做气象站项目时,就遇到过因为时序没搞对,传感器死活不响应的情况。嗯,咱们今天就把这块彻底讲透。
3.1 SDA/SCL时序:通信的脉搏
I2C的时序,核心就几个关键动作。你想想看,两根线上挂着多个设备,怎么区分谁在说话?靠的就是SCL时钟的配合。
3.1.1 空闲状态
总线空闲时,SDA和SCL都是高电平。这个状态由上拉电阻保证。我个人习惯在PCB设计时,上拉电阻选4.7kΩ,对于100kHz的标准模式够用了。如果总线电容大,可以换成2.2kΩ。
3.1.2 起始信号
SCL为高电平时,SDA从高电平跳变到低电平。这就是起始信号。我刚开始学的时候,总觉得这个时序很抽象。后来用逻辑分析仪一看,豁然开朗。
// 起始信号伪代码
void i2c_start(void) {
SDA_HIGH(); // 确保SDA为高
SCL_HIGH(); // SCL拉高
delay_us(5); // 保持一段时间
SDA_LOW(); // SDA拉低,产生起始信号
delay_us(5); // 保持
SCL_LOW(); // SCL拉低,准备传输数据
}
3.1.3 停止信号
SCL为高电平时,SDA从低电平跳变到高电平。这就是停止信号。记住这个顺序:起始是SDA先变,停止也是SDA先变。我曾经在项目中把顺序搞反了,结果总线一直卡死,查了半天才发现是这里的问题。
3.1.4 数据位传输
每个数据位在SCL的上升沿被采样。SDA在SCL低电平时改变,高电平时保持稳定。这个规则一定要记住:SCL高电平时,SDA不能变。否则会被误判为起始或停止信号。
关键时序参数(标准模式100kHz):
- SCL时钟频率:100kHz(周期10μs)
- 数据建立时间:≥250ns
- 数据保持时间:≥0ns
- SCL低电平时间:≥4.7μs
- SCL高电平时间:≥4.0μs
3.2 地址配置:每个设备都有自己的门牌号
I2C总线上每个设备都有唯一的7位或10位地址。咱们气象站常用的传感器,比如BMP280、SHT30,基本都是7位地址。
3.2.1 7位地址结构
地址字节的高7位是设备地址,最低位是读写位。0表示写,1表示读。举个例子,BMP280的I2C地址是0x76(如果SDO引脚接地)。
| 位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| 含义 | 设备地址(0x76) | R/W | ||||||
| 值 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 0/1 |
小技巧:我习惯在代码里用宏定义地址,比如:
#define BMP280_ADDR_W 0xEC // 写地址(0x76 << 1)
#define BMP280_ADDR_R 0xED // 读地址(0x76 << 1 | 0x01)
这样在调用时直接传地址字节,不用每次计算读写位。
3.2.2 地址冲突怎么办?
如果两个设备地址相同,就不能挂在同一条I2C总线上。我曾经遇到过这种情况:一个项目里用了两个相同型号的传感器,地址都是0x76。解决办法有两个:
- 用I2C多路复用器(如TCA9548A)
- 用GPIO模拟I2C,分时访问
我个人更推荐第一种,硬件上更可靠。
3.3 读写流程:数据怎么传?
I2C的读写流程,说白了就是主机和从机之间的对话。主机发起请求,从机响应。咱们以读取BMP280的温度值为例,走一遍完整流程。
3.3.1 写操作流程
写操作通常用于配置寄存器或发送命令。流程如下:
- 主机发送起始信号
- 主机发送从机地址+写位(0)
- 从机应答ACK
- 主机发送寄存器地址
- 从机应答ACK
- 主机发送数据字节
- 从机应答ACK
- 主机发送停止信号
// 写一个字节到BMP280的某个寄存器
uint8_t bmp280_write_reg(uint8_t reg_addr, uint8_t data) {
i2c_start();
i2c_send_byte(BMP280_ADDR_W); // 发送写地址
if (i2c_wait_ack() != 0) return 1; // 等待应答
i2c_send_byte(reg_addr); // 发送寄存器地址
if (i2c_wait_ack() != 0) return 1;
i2c_send_byte(data); // 发送数据
if (i2c_wait_ack() != 0) return 1;
i2c_stop();
return 0;
}
3.3.2 读操作流程
读操作稍微复杂一点,需要先写寄存器地址,再重新发起起始信号读数据。这叫「复合操作」。
- 主机发送起始信号
- 主机发送从机地址+写位(0)
- 从机应答ACK
- 主机发送要读取的寄存器地址
- 从机应答ACK
- 主机重新发送起始信号(重复起始)
- 主机发送从机地址+读位(1)
- 从机应答ACK
- 主机读取数据字节,发送NACK(表示读完了)
- 主机发送停止信号
注意:重复起始信号和停止信号不同。重复起始是在SCL高电平时,SDA从高变低,但中间没有停止信号。有些传感器对重复起始支持不好,我建议在代码里明确区分。
// 从BMP280读取两个字节(温度值)
uint16_t bmp280_read_temp(void) {
uint8_t msb, lsb;
// 第一步:写寄存器地址
i2c_start();
i2c_send_byte(BMP280_ADDR_W);
i2c_wait_ack();
i2c_send_byte(0xFA); // 温度寄存器高字节地址
i2c_wait_ack();
// 第二步:重复起始,读数据
i2c_start(); // 重复起始
i2c_send_byte(BMP280_ADDR_R);
i2c_wait_ack();
msb = i2c_read_byte();
i2c_send_ack(); // 发送ACK,继续读
lsb = i2c_read_byte();
i2c_send_nack(); // 最后一个字节发NACK
i2c_stop();
return (msb << 8) | lsb;
}
3.4 避坑指南
做I2C通信,有几个坑我踩过,分享给大家:
- 时钟拉伸:有些从机在应答时会拉低SCL,主机要检测这个状态。我曾经用了一个不支持时钟拉伸的MCU,结果跟某个传感器死活通信不上。
- 应答超时:不要无限等待ACK。我习惯加一个超时机制,比如等待100μs没收到ACK就报错。
- 总线锁死:如果从机异常,可能会一直拉着SDA不放。这时候可以连续发送9个SCL时钟,让从机释放总线。
- 电平匹配:3.3V的MCU和5V的传感器通信,要加电平转换。我一般用PCA9306,便宜又好用。
嗯,I2C的基础就这些。你想想看,掌握了时序、地址和读写流程,基本上就能搞定大部分I2C设备了。下一节咱们会结合气象站的实际硬件,讲讲怎么用STM32的硬件I2C外设来驱动传感器。到时候我会分享一些我在调试时用的实用技巧。