第四章 I2C总线实战:从时序到多设备协同
各位同学,今天我们来啃一块硬骨头——I2C总线。说实话,我在助听器行业摸爬滚打这么多年,I2C是我打交道最多的接口之一。为什么?因为助听器里那些小尺寸的EEPROM、传感器,几乎清一色都是I2C接口。你想想看,一个助听器才多大?引脚能省则省,I2C只用两根线就能挂一堆设备,简直是天选之子。
4.1 I2C协议时序:从波形到代码
先说说I2C的时序。很多人觉得I2C简单,不就是SCL和SDA两根线嘛。但我在项目中遇到过,就因为时序没卡好,EEPROM死活写不进去数据。嗯,这里要注意,I2C的时序其实挺讲究的。
I2C通信的核心就几个动作:起始条件、停止条件、数据位传输、应答位。我习惯用逻辑分析仪先抓波形,再写代码。这样心里有底。
关键时序参数(标准模式100kHz):
- SCL时钟频率:100kHz(标准模式)或400kHz(快速模式)
- 起始条件保持时间:tHD;STA ≥ 4.0μs
- 数据建立时间:tSU;DAT ≥ 250ns
- 数据保持时间:tHD;DAT ≥ 0ns(我一般留300ns余量)
- SCL低电平时间:tLOW ≥ 4.7μs
- SCL高电平时间:tHIGH ≥ 4.0μs
来看一个典型的起始条件代码。我个人习惯用GPIO模拟I2C,因为助听器主控的I2C外设有时候引脚不够用。
// I2C起始条件
void i2c_start(void) {
SDA_HIGH(); // 确保SDA为高
SCL_HIGH(); // SCL为高
delay_us(5); // 保持4.7μs以上
SDA_LOW(); // SDA拉低,产生起始条件
delay_us(5); // 保持
SCL_LOW(); // 拉低SCL,准备传输数据
}
为什么先拉高再拉低?说白了,起始条件的本质就是SCL为高时,SDA从高变低。这个下降沿就是起始信号。我曾经见过新手把顺序搞反,结果从机根本不响应。
4.2 助听器内EEPROM读写实战
助听器里用的EEPROM,最常见的是24LC系列,比如24LC256。容量256Kbit,够存一套验配参数了。我建议你选型时注意工作电压,助听器电池电压低,1.8V供电的EEPROM是首选。
EEPROM的读写流程其实不复杂。写操作分两步:先发设备地址+写指令,再发内存地址,最后发数据。读操作类似,但需要先发一个伪写指令来设置内存地址。
我的经验:EEPROM写完后一定要等内部写周期完成。24LC256的写周期典型值是5ms。我习惯在每次写操作后加一个ACK查询,而不是傻等5ms。这样能提高效率。
来看一个完整的写字节函数:
// 向EEPROM指定地址写一个字节
uint8_t eeprom_write_byte(uint16_t addr, uint8_t data) {
i2c_start();
// 设备地址:0xA0(7位地址0x50左移1位,写操作)
if (i2c_send_byte(0xA0) != I2C_ACK) {
i2c_stop();
return I2C_NACK;
}
// 发送内存高字节地址
i2c_send_byte((uint8_t)(addr >> 8));
// 发送内存低字节地址
i2c_send_byte((uint8_t)(addr & 0xFF));
// 发送数据
i2c_send_byte(data);
i2c_stop();
// 等待写周期完成(ACK查询)
while (1) {
i2c_start();
if (i2c_send_byte(0xA0) == I2C_ACK) {
break; // 从机应答,写完成
}
i2c_stop();
delay_ms(1);
}
i2c_stop();
return I2C_ACK;
}
你可能会问,为什么设备地址是0xA0?嗯,这里有个小陷阱。I2C的7位地址是0x50,但发送时左移1位,最低位是读写位。0xA0 = 0x50 << 1 | 0(写)。读操作就是0xA1。我在项目中遇到过有人直接拿0x50当设备地址发,结果从机根本不鸟他。
4.3 传感器读写:以温湿度传感器为例
助听器里现在也开始集成传感器了,比如检测耳道温湿度,用于自适应调节。我常用的温湿度传感器是SHT30,精度不错,价格也合适。
SHT30的I2C地址是0x44(7位)。读数据前要先发测量命令,等一段时间后再读结果。它的测量命令是16位的,比如0x2C06表示高重复性测量。
// 读取SHT30温湿度数据
uint8_t sht30_read_temp_hum(float *temp, float *hum) {
uint8_t buf[6];
// 发送测量命令
i2c_start();
i2c_send_byte(0x88); // 0x44 << 1 | 0(写)
i2c_send_byte(0x2C); // 命令高字节
i2c_send_byte(0x06); // 命令低字节
i2c_stop();
delay_ms(15); // 等待测量完成,典型值12.5ms
// 读取数据
i2c_start();
i2c_send_byte(0x89); // 0x44 << 1 | 1(读)
for (int i = 0; i < 6; i++) {
buf[i] = i2c_read_byte(i < 5 ? I2C_ACK : I2C_NACK);
}
i2c_stop();
// 计算温湿度(SHT30公式)
uint16_t st = ((uint16_t)buf[0] << 8) | buf[1];
uint16_t srh = ((uint16_t)buf[3] << 8) | buf[4];
*temp = -45.0f + 175.0f * (float)st / 65535.0f;
*hum = 100.0f * (float)srh / 65535.0f;
return 0;
}
注意:SHT30的温湿度计算公式里,温度是-45到+130摄氏度范围。助听器耳道温度一般在35-38度,算出来没问题。但如果你在北方冬天做测试,传感器刚从室外拿进来,温度可能低于0度,这时候要注意数据有效性检查。
4.4 多设备地址冲突解决
这是实战中最头疼的问题。助听器里可能同时挂EEPROM、温湿度传感器、加速度计,每个设备都有固定的I2C地址。万一地址冲突了怎么办?
我遇到过最典型的情况:两个不同品牌的传感器用了同一个I2C地址。比如某款气压传感器和某款加速度计,默认地址都是0x68。挂到同一条总线上,两个设备都会响应,数据就乱套了。
解决办法有几种:
- 硬件地址修改:很多I2C设备有地址引脚,比如AD0、AD1。拉高或拉低可以改变地址。我习惯在设计阶段就把地址引脚引出来,用跳线或电阻配置。
- 使用I2C多路复用器:比如PCA9548A,可以分时选通不同的I2C通道。代价是多一个芯片,但软件简单。
- 软件地址重映射:有些设备支持软件修改地址,比如通过特定命令写入新的地址。但要注意,掉电后地址会恢复默认。
- 分时供电:给冲突的设备分别用MOSFET控制电源,需要哪个就开哪个。这个方法我在低功耗项目里用过,但要注意上电时序。
我的建议:在原理图阶段就列一个I2C设备地址表。我曾经因为偷懒没做这个,结果打板回来发现两个传感器地址冲突,只能飞线改地址引脚,那叫一个狼狈。
来看一个地址冲突检测的代码片段:
// 扫描I2C总线上的所有设备
void i2c_scan(void) {
for (uint8_t addr = 0x01; addr < 0x7F; addr++) {
i2c_start();
uint8_t status = i2c_send_byte(addr << 1 | 0); // 写操作
if (status == I2C_ACK) {
printf("发现设备: 0x%02X\n", addr);
}
i2c_stop();
}
}
这个扫描函数在调试时特别有用。你可以在系统初始化时调用一次,看看实际挂载了哪些设备,地址对不对。我在项目中就靠这个发现过焊错电阻导致地址偏移的问题。
4.5 实战中的避坑指南
最后,分享几个我踩过的坑:
- 上拉电阻不能省:I2C总线需要上拉电阻,典型值4.7kΩ。助听器电池电压低,我建议用2.2kΩ,保证上升沿够陡。曾经有人没加上拉电阻,总线一直低电平,所有通信都失败。
- 时钟延展要注意:有些从机在忙时会拉低SCL,主机要检测并等待。EEPROM写周期就是典型例子。我见过有人没处理时钟延展,导致数据错位。
- 总线电容别太大:I2C总线电容有限制,400kHz模式下不超过400pF。助听器里走线短,问题不大。但如果用了长排线连接外部设备,就要小心了。
- 从机地址别搞错:7位地址和8位地址经常搞混。记住:数据手册上写的通常是7位地址,发送时要左移1位。
调试小技巧:用逻辑分析仪抓I2C波形时,触发条件设成起始条件。这样每次通信开始都会触发,方便定位问题。我习惯把采样率设成4倍于SCL频率,比如400kHz的SCL用1.6MHz采样,波形就很清晰了。
好了,I2C总线实战就讲到这里。下一章我们会讲SPI总线,那个速度更快,但引脚也多一根。到时候再聊。
公众号:蓝海资料掘金营,微信deep3321