第4章:I2C驱动开发
I2C总线,说实话,是座舱芯片里最常用的低速通信接口之一。触摸屏、传感器、音频Codec、电源管理芯片……十有八九都挂在I2C上。我刚开始做驱动时,总觉得I2C简单——就两根线嘛,SDA和SCL。结果呢?被时钟拉伸坑过,被ACK错误折磨过。嗯,今天咱们就把I2C驱动开发这件事,从头到尾捋一遍。
4.1 I2C总线协议时序
先聊聊协议本身。I2C是Philips(现在叫NXP)发明的,两根线搞定多设备通信。一个主设备(Master)可以挂多个从设备(Slave),每个从设备有唯一地址。
基本时序单元:
- 起始条件(START):SCL高电平时,SDA从高变低。我习惯叫它「S信号」。
- 停止条件(STOP):SCL高电平时,SDA从低变高。叫它「P信号」。
- 数据采样:SCL低电平时改变SDA,SCL高电平时采样数据。每个时钟传1 bit。
- 应答位(ACK):每传完8 bit数据,第9个时钟由接收方拉低SDA表示应答。
核心要点:I2C是开漏输出,靠上拉电阻实现「线与」逻辑。谁拉低谁说了算。
我遇到过一个问题:某颗TP(触摸屏)芯片在低温下偶尔不响应。抓波形发现,它的SCL时钟拉伸时间超过了标准值。说白了,从设备想拖慢总线,但主设备没等它。这就是典型的时钟拉伸问题,后面会细讲。
4.2 Linux I2C子系统架构
Linux内核的I2C子系统,分三层:Adapter、Client、Driver。你想想看,这跟Linux设备模型里的「总线-设备-驱动」是一一对应的。
我画了一张图,帮你理解这三层的关系:
这张图你看懂了吗?我解释一下:
- I2C Adapter:对应硬件I2C控制器。比如你的座舱芯片里有3个I2C控制器,那就注册3个adapter。每个adapter有自己独立的算法(algorithm),负责收发数据。
- I2C Client:挂在这个adapter上的从设备。比如地址0x38的触摸屏,就是一个client。
- I2C Driver:驱动client的代码。它不直接操作寄存器,而是通过adapter提供的接口来读写。
个人习惯:我写I2C驱动时,喜欢先确认adapter是否存在。在/sys/bus/i2c/devices/下能看到所有注册的adapter和client。调试时特别有用。
4.3 I2C设备驱动编写实战
好,理论说完了,咱们直接上手写代码。以一颗常见的温度传感器为例,地址0x48,寄存器0x00存温度值。
第一步:定义设备结构体
struct my_temp_sensor {
struct i2c_client *client;
struct mutex lock;
int temperature;
};
第二步:实现probe函数
static int my_temp_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct my_temp_sensor *sensor;
sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
if (!sensor)
return -ENOMEM;
sensor->client = client;
mutex_init(&sensor->lock);
i2c_set_clientdata(client, sensor);
// 初始化设备
// 比如配置采样率、使能等
dev_info(&client->dev, "温度传感器初始化成功\n");
return 0;
}
第三步:实现读写操作
static int temp_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 < 0) {
dev_err(&client->dev, "I2C传输失败: %d\n", ret);
return ret;
}
return 0;
}
注意:i2c_transfer返回的是成功传输的消息数量,不是字节数。如果返回2表示两个消息都成功了。我曾经在这里踩过坑,把返回值当字节数判断,结果死活读不到数据。
第四步:注册驱动
static const struct i2c_device_id temp_id[] = {
{ "my_temp_sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, temp_id);
static struct i2c_driver temp_driver = {
.driver = {
.name = "my_temp_sensor",
.of_match_table = of_match_ptr(temp_of_match),
},
.probe = my_temp_probe,
.remove = my_temp_remove,
.id_table = temp_id,
};
module_i2c_driver(temp_driver);
4.4 常见问题排查
写驱动不难,难的是调通。我总结两个最常见的坑:
4.4.1 时钟拉伸(Clock Stretching)
从设备忙不过来时,会拉低SCL,让主设备等一等。这叫时钟拉伸。但有些主设备控制器不支持这个特性,或者超时时间设得太短。
排查方法:
- 用示波器抓SCL波形,看是否有异常的低电平保持。
- 检查adapter的算法是否支持clock stretching。在i2c_algorithm结构体里有个functionality标志位。
- 调整I2C时钟频率,从400kHz降到100kHz试试。
我曾经遇到一个摄像头模组,每次读数据前都要等200ms。主设备控制器默认超时100ms,导致一直报错。最后在设备树里加了clock-frequency = <100000>,问题解决。
4.4.2 ACK错误
发送地址或数据后,从设备没拉低SDA应答。原因很多:
- 地址不对:7位地址还是10位地址?左移了没?我习惯用i2c_client的addr字段,内核会自动处理。
- 设备没上电:检查电源域,特别是座舱芯片里有些I2C总线是常供电的。
- 总线冲突:两个主设备同时抢总线。I2C本身支持多主,但实现起来坑很多。
- 上拉电阻失效:SDA和SCL必须有上拉电阻,一般是4.7kΩ。如果电阻虚焊或损坏,总线拉不上去。
排查步骤:
- 用i2cdetect工具扫描总线,看设备地址是否出现。
- 抓波形,确认START条件后,从设备是否在第9个时钟拉低SDA。
- 检查设备树中reg属性是否与硬件一致。
我建议在probe函数里加一个简单的读ID操作。如果读不到ID,直接返回-ENODEV。这样驱动加载时就能发现问题,不用等到应用层报错。
嗯,I2C驱动开发的核心就这些。说白了,协议不难,难的是把协议和硬件、内核框架结合起来。多抓波形,多读datasheet,慢慢就有感觉了。
公众号:蓝海资料掘金营,微信deep3321