3. Linux I2C子系统架构:三层结构深度拆解
做嵌入式Linux驱动开发,I2C子系统是绕不开的坎。我记得刚入行那会儿,看着I2C的代码一头雾水——怎么一个简单的I2C读写,要绕这么多层?后来踩了几个坑才明白,这种分层设计其实很巧妙。
今天我就带你彻底搞懂Linux I2C子系统的三层架构:I2C核心层、I2C总线驱动、I2C设备驱动。说白了,这三层各司其职,把硬件细节和上层应用隔离开来。
3.1 为什么需要三层结构?
你想想看,一个嵌入式板子上可能挂了好几个I2C设备——温度传感器、触摸屏、音频Codec。每个设备的寄存器地址、操作时序都不一样。如果让每个驱动都直接操作I2C控制器的寄存器,那代码就乱套了。
Linux内核的设计哲学就是分层抽象。I2C子系统把驱动拆成三层:
- I2C核心层:提供统一的API和数据结构,管理总线注册、设备枚举
- I2C总线驱动:直接操作I2C控制器硬件,处理时序、中断、DMA
- I2C设备驱动:只关心具体设备的功能,比如读温度、设置音量
这样做的好处很明显——换一个I2C控制器,设备驱动不用改;换一个I2C设备,总线驱动不用动。我在项目中遇到过好几次平台迁移,从IMX6换到RK3399,设备驱动几乎没改,只换了总线驱动就搞定了。
3.2 I2C核心层:承上启下的枢纽
核心层的代码主要在 drivers/i2c/i2c-core.c 里。它不直接操作硬件,而是提供了一套框架。我个人习惯把核心层理解为「I2C子系统的大管家」。
核心层主要干这几件事:
| 功能模块 | 说明 | 关键函数/结构体 |
|---|---|---|
| 总线注册 | 管理I2C总线的添加和删除 | i2c_add_adapter() |
| 设备枚举 | 从设备树或ACPI解析设备信息 | of_i2c_register_devices() |
| 驱动匹配 | 将设备驱动绑定到对应设备 | i2c_device_match() |
| 数据传输 | 提供统一的读写接口 | i2c_transfer() |
struct i2c_adapter 代表一条I2C总线struct i2c_client 代表一个I2C设备struct i2c_driver 代表一个设备驱动struct i2c_algorithm 代表总线通信方法
3.3 I2C总线驱动:和硬件打交道的人
总线驱动,说白了就是I2C控制器的驱动。它负责把核心层的请求翻译成硬件时序。每个I2C控制器(比如IMX6的I2C IP核)都需要一个对应的总线驱动。
总线驱动的核心是 struct i2c_algorithm,它定义了三个关键方法:
struct i2c_algorithm {
/* 主模式发送 */
int (*master_xfer)(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num);
/* 从模式接收(较少用) */
int (*slave_xfer)(struct i2c_adapter *adap,
struct i2c_msg *msgs);
/* 功能支持查询 */
u32 (*functionality)(struct i2c_adapter *adap);
};
嗯,这里要注意——master_xfer 是总线驱动里最重要的函数。它接收一个 i2c_msg 数组,然后按照I2C协议在硬件上产生START、ACK、STOP等信号。
我曾经在调试一个I2C触摸屏时,发现读数据总是少一个字节。查了两天才发现是总线驱动的
master_xfer 实现里,对重复起始位(Repeated Start)的处理有问题。很多新手容易忽略这个细节。
3.4 I2C设备驱动:只关心业务逻辑
设备驱动是最上层,也是我们平时写的最多的部分。它只关心「怎么操作这个设备」,不关心「怎么在I2C总线上发数据」。
一个典型的I2C设备驱动结构:
static const struct i2c_device_id temp_sensor_id[] = {
{ "lm75", 0 },
{ }
};
static struct i2c_driver temp_sensor_driver = {
.driver = {
.name = "lm75",
.of_match_table = temp_sensor_of_match,
},
.probe = temp_sensor_probe,
.remove = temp_sensor_remove,
.id_table = temp_sensor_id,
};
module_i2c_driver(temp_sensor_driver);
你看,设备驱动只需要注册 i2c_driver,核心层会自动帮我们做设备匹配。匹配成功后调用 probe 函数,我们在里面初始化设备、注册字符设备或输入子系统。
读写设备时,设备驱动调用核心层提供的API:
/* 从I2C设备读取一个字节 */
int i2c_smbus_read_byte_data(struct i2c_client *client, u8 command);
/* 向I2C设备写入一个字节 */
int i2c_smbus_write_byte_data(struct i2c_client *client,
u8 command, u8 value);
/* 更通用的传输接口 */
int i2c_transfer(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num);
写设备驱动时,优先用
i2c_smbus_xxx 系列函数。它们封装了错误处理,代码更简洁。只有在需要自定义时序(比如某些传感器要求连续读取多个寄存器)时,才用 i2c_transfer。
3.5 三层之间的数据流
搞清楚了每层的职责,我们再看看数据是怎么在三层之间流动的。假设用户空间程序要读取一个温度传感器的值:
- 用户空间:通过
read()系统调用进入内核 - 设备驱动:调用
i2c_smbus_read_byte_data() - 核心层:构造
i2c_msg,调用i2c_transfer() - 核心层:找到对应的
i2c_adapter,调用master_xfer() - 总线驱动:操作I2C控制器寄存器,产生硬件时序
- 硬件:SCL/SDA线上产生I2C协议波形
返回路径正好相反。整个过程对设备驱动是透明的——它根本不知道底层用的是哪个I2C控制器。
三层之间的调用都是同步的。如果总线驱动在
master_xfer 里加了延时或等待,整个调用链都会被阻塞。我在调试一个音频Codec时,就因为总线驱动里有个10ms的忙等,导致音频播放出现卡顿。后来改成了中断方式才解决。
3.6 设备树与三层架构的配合
现在的嵌入式Linux项目基本都用设备树(Device Tree)。设备树描述硬件拓扑,I2C子系统从中解析出总线和设备信息。
一个典型的I2C设备树节点:
&i2c1 {
status = "okay";
clock-frequency = <100000>;
temp_sensor: lm75@48 {
compatible = "national,lm75";
reg = <0x48>;
};
touch: ft5x06@38 {
compatible = "edt,edt-ft5x06";
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
};
};
核心层在初始化时,会解析 i2c1 节点下的子节点,为每个子节点创建一个 i2c_client。然后根据 compatible 属性匹配对应的 i2c_driver。
我个人习惯在设备树里把 clock-frequency 显式写出来,虽然很多平台有默认值。这样做的好处是——换平台时一眼就能看到总线速度,不用去翻芯片手册。
3.7 小结
Linux I2C子系统的三层架构,说白了就是「分而治之」的思想。核心层做调度,总线驱动管硬件,设备驱动管功能。每一层只关心自己的事,接口清晰,耦合度低。
搞懂了这个架构,后面写I2C设备驱动或者移植I2C总线驱动,你就知道该往哪个文件里加代码了。嗯,下一节我们具体看看怎么编写一个I2C设备驱动,到时候我会拿一个真实的温度传感器驱动做例子。