第二讲:Linux内核I2C子系统——从协议到驱动实战
大家好,欢迎来到第二讲。今天我们来聊聊I2C子系统。说实话,I2C在嵌入式系统里太常见了——传感器、触摸屏、音频编解码器,十有八九都挂在I2C总线上。我刚开始做驱动时,总觉得I2C很简单,不就是两根线嘛。直到有一次调试一个加速度计,死活读不到数据,最后发现是时序问题……嗯,从那以后我再也不敢小看I2C了。
一、I2C总线协议基础
I2C总线,说白了就是两根线搞定一切通信:一根时钟线(SCL),一根数据线(SDA)。它是个多主从架构,但咱们嵌入式里最常见的就是一个主控(比如CPU)挂一堆从设备(传感器、芯片)。
为什么I2C这么流行?我总结了几点:
- 引脚少——就两根线,省PCB空间
- 支持多设备——一条总线上可以挂几十个从设备
- 速度够用——标准模式100kHz,快速模式400kHz,高速模式能到3.4MHz
但要注意,I2C也有短板。我在项目中遇到过一个问题:总线长度超过半米后,信号就开始变形了。所以长距离通信别指望I2C。
1.1 通信流程
I2C的通信流程其实很清晰:
- 主设备发送起始信号(SCL高电平时,SDA从高变低)
- 发送7位从设备地址 + 1位读写位
- 从设备应答(ACK)
- 传输数据(每字节后跟一个ACK)
- 主设备发送停止信号(SCL高电平时,SDA从低变高)
关键点:地址是7位的,但很多数据手册会给你8位的地址(左移了一位)。我刚开始就吃过这个亏——手册上写0x68,结果我直接用了,发现不对。后来才明白,0x68是7位地址左移后的结果,实际要右移一位才是真正的7位地址。
1.2 时序图
下面我用SVG画了个I2C的典型读写时序,你一看就明白了:
二、Linux I2C核心数据结构
Linux内核把I2C子系统抽象得很漂亮。我当年第一次看源码时,觉得这设计真巧妙——把硬件细节全封装了,驱动开发者只需要关心几个核心结构体。
2.1 三个核心结构体
| 结构体 | 作用 | 我常用的字段 |
|---|---|---|
i2c_adapter |
代表I2C控制器(主控端) | nr(总线编号)、algo(通信算法) |
i2c_client |
代表I2C从设备 | addr(设备地址)、adapter(所属总线) |
i2c_driver |
代表I2C设备驱动 | probe、remove、id_table |
这三个结构体之间的关系,我打个比方:i2c_adapter就像一条高速公路,i2c_client是路上的收费站,i2c_driver是收费站的管理员。管理员(驱动)负责管理收费站(设备),通过高速公路(总线)跟外界通信。
2.2 数据传输的核心函数
Linux内核提供了几个封装好的API,咱们直接调就行:
// 我最常用的两个函数
int i2c_master_send(struct i2c_client *client, const char *buf, int count);
int i2c_master_recv(struct i2c_client *client, char *buf, int count);
// 更灵活的方式——组合传输
struct i2c_msg msg[2];
msg[0].addr = client->addr;
msg[0].flags = 0; // 写
msg[0].len = 1;
msg[0].buf = ®_addr;
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; // 读
msg[1].len = 2;
msg[1].buf = data;
i2c_transfer(client->adapter, msg, 2);
我的经验:能用i2c_transfer就别用i2c_master_send/recv。为什么?因为很多传感器需要先写寄存器地址再读数据,组合传输能保证原子性。我曾经用i2c_master_send和i2c_master_recv分开调用,结果在中断上下文里被抢占了,读到的数据全乱套。
三、I2C设备驱动编写模板
好了,理论说完了,咱们直接上代码。我写I2C驱动有个固定套路,你照着这个模板改,基本不会出大问题。
3.1 驱动框架代码
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/delay.h>
// 设备结构体——存放私有数据
struct my_sensor_data {
struct i2c_client *client;
int irq;
// 其他自定义字段
};
// 读寄存器函数
static int my_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, "读寄存器0x%02x失败\n", reg);
return -EIO;
}
return 0;
}
// 写寄存器函数
static int my_sensor_write_reg(struct i2c_client *client, u8 reg, u8 val)
{
u8 buf[2] = {reg, val};
int ret;
ret = i2c_master_send(client, buf, 2);
if (ret != 2) {
dev_err(&client->dev, "写寄存器0x%02x失败\n", reg);
return -EIO;
}
return 0;
}
// probe函数——驱动入口
static int my_sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct my_sensor_data *data;
u8 chip_id;
int ret;
dev_info(&client->dev, "设备探测开始\n");
// 1. 分配私有数据
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
// 2. 读取芯片ID验证
ret = my_sensor_read_reg(client, 0x0F, &chip_id);
if (ret < 0) {
dev_err(&client->dev, "读取芯片ID失败\n");
return ret;
}
// 3. 初始化设备
my_sensor_write_reg(client, 0x10, 0x01); // 软复位
msleep(10);
my_sensor_write_reg(client, 0x11, 0x00); // 配置模式
dev_info(&client->dev, "设备初始化完成,芯片ID: 0x%02x\n", chip_id);
return 0;
}
// remove函数——驱动退出
static int my_sensor_remove(struct i2c_client *client)
{
dev_info(&client->dev, "设备移除\n");
// 关闭设备、释放中断等
return 0;
}
// 设备ID表——匹配硬件
static const struct i2c_device_id my_sensor_id[] = {
{ "my_sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, my_sensor_id);
// 设备树匹配表
static const struct of_device_id my_sensor_of_match[] = {
{ .compatible = "vendor,my-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);
// 驱动结构体
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,
};
module_i2c_driver(my_sensor_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My I2C Sensor Driver");
3.2 设备树配置
别忘了在设备树里注册你的设备:
&i2c1 {
status = "okay";
clock-frequency = <100000>; // 100kHz标准模式
my_sensor@48 {
compatible = "vendor,my-sensor";
reg = <0x48>; // 7位从设备地址
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
};
};
避坑指南:我曾经在设备树里把reg写成了8位地址(0x90),结果内核怎么都匹配不上设备。查了两天才发现——设备树的reg必须是7位地址!记住,是7位,不是8位。
3.3 调试技巧
调试I2C驱动时,我一般用这几招:
- i2cdetect——先看看总线上有哪些设备,地址对不对
- i2cget/i2cset——手动读写寄存器,验证硬件是否正常
- 逻辑分析仪——抓波形,看时序对不对。我有个USB逻辑分析仪,才几十块钱,但救过我无数次
嗯,这一讲的内容就到这儿。I2C子系统其实不难,关键是理解那三个核心结构体,然后照着模板写驱动。下一讲我们会深入HAL层,看看怎么把驱动数据往上传递到Android框架层。
总结一下:I2C总线协议就是两根线、一个起始条件、一个停止条件、中间传地址和数据。Linux内核用i2c_adapter、i2c_client、i2c_driver三个结构体把整个子系统封装得干干净净。写驱动时,照着模板填probe和remove函数就行,别忘了用i2c_transfer做组合传输。
可可精品资料库