2. I2C驱动框架:Linux内核I2C子系统架构
做嵌入式Linux驱动开发,I2C是绕不开的坎。很多初学者一上来就对着datasheet操作寄存器,结果发现内核里根本不让这么干。为什么?因为Linux内核已经帮你搭好了一套完整的I2C驱动框架。你只需要往这个框架里填东西就行。
我个人习惯把I2C子系统拆成四层来看:I2C核心层、I2C总线驱动、I2C设备驱动,以及设备树绑定。搞懂这四层的关系,你就能理解I2C设备在内核里是怎么被识别、怎么被控制的。
核心观点:Linux I2C驱动框架的本质是「分层解耦」。硬件相关的代码和业务逻辑分开,你写驱动时只需要关注设备本身的行为,不用管I2C控制器怎么发时序。
2.1 I2C核心层:驱动框架的骨架
I2C核心层是内核帮你写好的基础设施。它不关心你的I2C控制器是哪个厂家的,也不关心你挂的是什么传感器。它只做三件事:管理总线、管理设备、提供统一的读写接口。
核心层暴露给驱动开发者的主要数据结构就两个:struct i2c_adapter 和 struct i2c_client。前者代表一条I2C总线,后者代表挂在总线上的一个设备。你想想看,整个I2C子系统其实就是围绕这两个结构体转的。
我的经验:刚开始看i2c-core.c源码时别硬啃。你只需要知道核心层帮你做了设备匹配、地址检测、读写超时处理这些脏活。我早期写驱动时总想自己实现地址检测逻辑,后来发现核心层已经做得很完善了,直接用就行。
2.2 I2C总线驱动:跟硬件打交道的那层
总线驱动,说白了就是I2C控制器的驱动。它负责把I2C协议里的START、STOP、ACK、NACK这些时序变成具体的寄存器操作。不同的芯片平台,这部分代码完全不同。
总线驱动需要实现两个关键东西:
- i2c_algorithm:定义master_xfer函数,这是发送I2C消息的核心函数
- i2c_adapter:注册到核心层,代表一条物理I2C总线
我记得有一次调试一个I2C触摸屏,发现读回来的数据总是错位。查了两天才发现是总线驱动的master_xfer里没处理好重复起始条件。嗯,这种坑只有踩过才知道疼。
| 组件 | 职责 | 关键函数/结构体 |
|---|---|---|
| i2c_algorithm | 定义I2C通信的底层操作 | master_xfer(), smbus_xfer() |
| i2c_adapter | 代表一条I2C总线 | i2c_add_adapter(), i2c_del_adapter() |
| i2c_msg | 描述一次I2C传输消息 | addr, flags, len, buf |
2.3 I2C设备驱动:你真正要写的那部分
如果你只是要驱动一个I2C传感器,那你只需要写设备驱动。设备驱动不关心I2C控制器是哪个IP核,它只通过核心层提供的API来读写数据。
设备驱动的核心是 struct i2c_driver。你需要实现:
- probe函数:设备被检测到时调用,初始化设备、注册输入子系统或字符设备
- remove函数:设备移除时清理资源
- id_table:告诉内核你的驱动支持哪些设备
读写操作通过 i2c_transfer() 或 i2c_smbus_read_byte_data() 这类API完成。你不需要自己操作GPIO模拟时序,也不需要管I2C控制器的寄存器。
避坑指南:我曾经在probe函数里直接调用i2c_transfer去读设备ID,结果系统卡死了。为什么?因为probe执行时I2C总线可能还没准备好。正确的做法是在probe里只做资源分配,真正的通信放到设备打开之后。
2.4 设备树绑定:让驱动和设备对上号
设备树是Linux内核用来描述硬件配置的机制。对于I2C设备,你需要在设备树里告诉内核:
- 这个设备挂在哪条I2C总线上
- 它的I2C地址是多少
- 它需要哪些额外的配置(中断引脚、频率等)
一个典型的I2C设备节点长这样:
&i2c1 {
status = "okay";
clock-frequency = <100000>;
temperature_sensor: tmp102@48 {
compatible = "ti,tmp102";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <15 IRQ_TYPE_EDGE_FALLING>;
};
};
这里要注意几点:
- compatible:必须和驱动里id_table中的字符串匹配
- reg:设备的I2C地址,7位地址模式
- status:不写"okay"的话内核不会去枚举这个设备
重要提醒:设备树里的reg地址写错了,驱动probe根本不会调用。我见过有人把7位地址左移一位填进去,结果内核报"no device at address"。I2C设备树里填的是原始7位地址,不是8位读写地址。
2.5 驱动匹配流程:从设备树到probe
搞清楚了各层的职责,我们再看看整个流程是怎么串起来的:
- 内核启动时,I2C总线驱动先注册,调用
i2c_add_adapter() - 设备树解析器扫描I2C子节点,为每个节点创建
i2c_client - 核心层把
i2c_client挂到对应的i2c_adapter上 - 设备驱动注册时,核心层遍历所有
i2c_client,匹配compatible字段 - 匹配成功,调用驱动的probe函数
这个流程你理解了,I2C驱动开发的基本功就算打牢了。后面写具体设备驱动时,你只需要关注probe里怎么初始化设备、怎么通过核心层API读写数据。
个人建议:刚开始学的时候,别急着写驱动。先在开发板上用i2c-tools工具手动探测一下设备地址,确认硬件没问题了再动手写代码。我每次都是这么干的,能省掉一半的调试时间。