1. I2C协议基础与硬件框架
I2C(Inter-Integrated Circuit)是一种简单却强大的串行通信协议,它只需要两根信号线就能实现多设备通信。在实际项目中,我经常用它连接各种传感器和存储芯片。先来看看它的硬件连接方式:
- SCL:时钟线,负责同步数据传输
- SDA:数据线,承载实际传输的数据
- 上拉电阻:必须接在两条线上(通常4.7KΩ)
举个实际例子,当我们要用AT24C02 EEPROM存储数据时,硬件连接就像搭积木一样简单:主控芯片的I2C控制器通过这两根线,就能和多个从设备"对话"。这里有个关键点要注意:所有设备都是"开漏输出",这意味着:
- 不驱动三极管时,SDA/SCL通过上拉电阻保持高电平
- 需要输出低电平时,才驱动三极管拉低线路
- 这种设计避免了总线冲突,实现了"线与"逻辑
数据传输时有个有趣的细节:每个字节传输需要9个时钟周期。前8个时钟传数据,第9个时钟用来等待从设备的应答(ACK)。这个ACK信号其实就是从设备在第9个时钟周期把SDA拉低,相当于说"我收到了"。
2. SMBus协议的特殊之处
SMBus(系统管理总线)是基于I2C的"严格版",在电源管理等场景很常见。我在调试笔记本电池管理芯片时深有体会,它有几点特殊要求:
- 超时限制:必须在35ms内完成传输,否则认为失败
- 特殊命令:比如块读写、过程调用等
- 地址保留:0x00-0x07和0x78-0x7F地址有特殊用途
最实用的功能是重复起始条件(Repeated Start)。比如我们要先写寄存器地址再读数据时,可以这样操作:
// 传统方式 i2c_start(); i2c_write(addr|0); // 写模式 i2c_write(reg); i2c_stop(); i2c_start(); i2c_write(addr|1); // 读模式 data = i2c_read(); i2c_stop(); // SMBus优化方式 i2c_start(); i2c_write(addr|0); i2c_write(reg); i2c_start(); // 重复起始,不释放总线 i2c_write(addr|1); data = i2c_read(); i2c_stop();这种方式避免了总线释放后被其他设备抢占的风险,在多任务系统中特别有用。
3. Linux I2C驱动核心结构体
Linux内核用三个关键结构体来抽象I2C系统,刚开始看源码时容易混淆,我来拆解下:
3.1 i2c_adapter:控制器管家
相当于I2C总线控制器的"身份证",包含:
struct i2c_adapter { struct device dev; const struct i2c_algorithm *algo; // 关键操作函数 int nr; // 总线编号 ... };其中algo->master_xfer是核心,实现了实际的传输函数。我在调试时发现,不同芯片的这个函数差异很大,比如NXP的驱动使用DMA,而全志的可能是GPIO模拟。
3.2 i2c_client:设备名片
描述挂在I2C总线上的设备:
struct i2c_client { unsigned short addr; // 7位设备地址 char name[I2C_NAME_SIZE]; struct i2c_adapter *adapter; // 所属总线 ... };这个结构体通常在设备树中定义,比如:
eeprom@50 { compatible = "atmel,24c02"; reg = <0x50>; };3.3 i2c_msg:传输单元
描述一次数据传输的具体参数:
struct i2c_msg { __u16 addr; // 设备地址 __u16 flags; // 读/写标志 __u16 len; // 数据长度 __u8 *buf; // 数据缓冲区 };实际使用时要组合多个msg,比如读取EEPROM的0x10地址数据:
u8 addr = 0x10; u8 data; struct i2c_msg msgs[2] = { {0x50, 0, 1, &addr}, // 写地址 {0x50, I2C_M_RD, 1, &data} // 读数据 };4. I2C-Tools实战技巧
I2C-Tools是调试神器和学习助手,这几个命令我每天都要用:
4.1 设备探测
# 列出所有I2C总线 i2cdetect -l # 扫描总线0上的设备 i2cdetect -y 0输出示例:
0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- 1e -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: 50 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- --这里能看到地址0x1e的光感芯片和0x50的EEPROM。
4.2 SMBus操作示例
操作AP3216C光感芯片:
# 复位 i2cset -f -y 0 0x1e 0 0x4 # 使能 i2cset -f -y 0 0x1e 0 0x3 # 读取光强(2字节) i2cget -f -y 0 0x1e 0xc w4.3 原始I2C操作
同样的操作改用I2C协议:
i2ctransfer -f -y 0 w2@0x1e 0 0x4 # 复位 i2ctransfer -f -y 0 w2@0x1e 0 0x3 # 使能 i2ctransfer -f -y 0 w1@0x1e 0xc r2 # 读光强5. 手把手编写EEPROM驱动
下面这个完整示例演示如何通过/dev/i2c接口操作AT24C02:
#include <linux/i2c-dev.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char **argv) { int file; char filename[20]; unsigned char addr = 0x50; // EEPROM地址 unsigned char buf[32]; // 打开I2C控制器 snprintf(filename, sizeof(filename), "/dev/i2c-%d", atoi(argv[1])); file = open(filename, O_RDWR); // 设置从设备地址 ioctl(file, I2C_SLAVE_FORCE, addr); if(argv[2][0] == 'w') { // 写入字符串 char *str = argv[3]; int i = 0; while(*str) { i2c_smbus_write_byte_data(file, i, *str); usleep(20000); // 等待EEPROM写入完成 i++; str++; } i2c_smbus_write_byte_data(file, i, 0); // 结束符 } else { // 读取数据 int len = i2c_smbus_read_i2c_block_data(file, 0, sizeof(buf), buf); buf[len] = '\0'; printf("Read: %s\n", buf); } close(file); return 0; }使用时:
# 写入数据 ./eeprom_app 0 w "Hello,100ask" # 读取数据 ./eeprom_app 0 r6. 常见问题排查指南
在调试I2C时我踩过不少坑,总结几个典型问题:
设备无响应:
- 检查上拉电阻(通常4.7KΩ)
- 确认设备地址是否正确(7位地址要左移1位)
- 用示波器看波形是否正常
数据错乱:
- 检查时钟频率是否过高(新手建议先用100KHz)
- 确认设备供电稳定
- 注意信号线长度(长距离要降低速率)
NACK错误:
- 检查设备是否初始化完成
- 确认从设备地址正确
- 查看设备是否处于睡眠模式
时序问题:
- GPIO模拟I2C时注意延时
- 某些设备需要stop信号后才能响应
- EEPROM写入需要等待5-10ms
记得有次调试一个I2C温度传感器,死活不响应,最后发现是PCB设计问题——SCL和SDA走线太长且平行,导致串扰。后来缩短走线并拉开间距就正常了。