4. I2C总线协议与驱动开发:I2C时序、Linux I2C子系统、编写I2C客户端驱动
各位同学,今天我们来聊聊I2C。这个协议在嵌入式世界里太常见了——传感器、EEPROM、ADC、触摸屏,几乎每个板子上都能找到它的身影。我个人习惯把I2C比作一个“多设备聊天的会议室”,两根线(SCL和SDA)就能让几十个设备互相通信,确实很巧妙。
4.1 I2C总线协议基础
先说说I2C的物理层。它只用两根线:
- SCL(串行时钟线):由主设备驱动,控制通信节奏
- SDA(串行数据线):双向数据线,主从设备都能拉
这两根线都是开漏输出,需要上拉电阻。嗯,这里要注意:上拉电阻的取值很关键。我在项目中遇到过因为上拉电阻选太大,导致信号上升沿太慢,通信偶尔出错的情况。一般4.7kΩ是个比较稳妥的起点,具体要看总线电容和速率。
4.2 I2C时序详解
I2C的时序其实不复杂,但细节决定成败。我们来看几个关键信号:
| 信号 | 描述 | 时序要求 |
|---|---|---|
| 起始条件 | SCL高电平时,SDA从高变低 | tHD:STA ≥ 4μs(100kHz) |
| 停止条件 | SCL高电平时,SDA从低变高 | tSU:STO ≥ 4μs |
| 数据采样 | SCL低电平时改变SDA,高电平时采样 | tSU:DAT ≥ 250ns |
| 应答位 | 第9个时钟周期,从设备拉低SDA表示ACK | NACK时从设备释放SDA |
为什么会强调这些时序参数?说白了,很多I2C通信问题都是时序违规导致的。我曾经调试一个温湿度传感器,读出来的数据总是跳变,用逻辑分析仪一看——起始条件保持时间不够,从设备没正确识别到起始信号。把延时加长一点,问题就解决了。
核心要点:I2C通信的原子操作是“起始+地址+数据+停止”。每个字节后面必须跟一个应答位,这是协议的铁律。
4.3 Linux I2C子系统架构
Linux内核的I2C子系统分三层,你想想看,这种分层设计的好处是什么?
- I2C核心层:提供总线管理、设备注册、驱动匹配等通用功能
- I2C总线驱动层:操作具体的I2C控制器硬件(比如STM32的I2C外设)
- I2C客户端驱动层:我们写驱动时主要打交道的地方,控制具体的I2C从设备
我个人习惯把客户端驱动想象成“设备的代言人”。内核通过i2c_client结构体来描述一个I2C设备,通过i2c_driver结构体来描述驱动。匹配成功后,probe函数就会被调用。
小技巧:在设备树中描述I2C设备时,reg属性就是设备的7位地址。注意!很多数据手册给的是8位地址(带读写位),需要右移一位才是7位地址。我刚开始做的时候就被这个坑过。
4.4 编写I2C客户端驱动
好了,理论说完了,咱们直接上代码。写一个I2C客户端驱动,核心就三步:
// 1. 定义i2c_driver结构体
static struct i2c_driver my_sensor_driver = {
.driver = {
.name = "my_sensor",
.of_match_table = my_sensor_of_match,
},
.probe = my_sensor_probe,
.remove = my_sensor_remove,
.id_table = my_sensor_id,
};
// 2. 实现probe函数
static int my_sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct my_sensor_data *data;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
i2c_set_clientdata(client, data);
// 初始化设备
my_sensor_init(client);
return 0;
}
// 3. 注册驱动
module_i2c_driver(my_sensor_driver);
嗯,这里要注意:probe函数里千万别做耗时操作。我曾经在probe里加了msleep(1000),结果系统启动慢了好几秒,被同事吐槽了好久。初始化操作可以放到工作队列或者异步任务里做。
4.5 I2C数据传输实战
读写I2C设备,Linux提供了两组API:
- SMBus兼容接口:i2c_smbus_read_byte_data()、i2c_smbus_write_word_data()等,适合简单设备
- 原始传输接口:i2c_transfer(),适合需要自定义时序的场景
我个人更推荐用i2c_transfer,虽然代码多几行,但控制力更强。来看一个实际例子:
static int sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
struct i2c_msg msg[2];
int ret;
// 第一条消息:写寄存器地址
msg[0].addr = client->addr;
msg[0].flags = 0; // 写
msg[0].len = 1;
msg[0].buf = ®
// 第二条消息:读数据
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; // 读
msg[1].len = 1;
msg[1].buf = val;
ret = i2c_transfer(client->adapter, msg, 2);
if (ret != 2) {
dev_err(&client->dev, "I2C read failed: %d\n", ret);
return -EIO;
}
return 0;
}
避坑指南:我曾经遇到一个传感器,读寄存器时必须先写寄存器地址,然后重新发送起始条件(restart)再读数据。如果用了i2c_smbus_read_byte_data(),它内部会发停止条件再发起始条件,结果传感器就不认了。换成i2c_transfer用重复起始条件,问题解决。
4.6 调试技巧与常见问题
调试I2C驱动,我常用的三板斧:
- 逻辑分析仪:抓波形,看起始条件、地址、数据、应答位是否正常
- i2c-tools工具集:i2cdetect探测设备、i2cget/i2cset读写寄存器,快速验证硬件
- 内核调试开关:打开CONFIG_I2C_DEBUG_BUS和CONFIG_I2C_DEBUG_ALGO,看内核日志
常见问题就那几个:
- 设备地址不对——检查数据手册,确认是7位还是8位地址
- 时钟延展没处理——有些慢速设备会拉低SCL,主设备要支持时钟延展
- 上拉电阻不合适——信号畸变导致通信失败
说白了,I2C驱动开发就是个熟能生巧的过程。多写几个驱动,多踩几个坑,自然就熟练了。下一章我们讲SPI总线,那个速度更快,但线也更多,到时候再聊。