嵌入式Linux I2C驱动开发核心知识点总结(设备树+platform架构)
本文基于IMX6ULL平台Linux 4.1内核,梳理I2C外设驱动开发的完整知识体系,覆盖驱动框架、设备树、字符设备、调试排障全流程。
一、Linux驱动分层架构总览
Linux驱动采用分层解耦设计思想,从应用层到硬件形成完整调用链:
- 应用层:用户空间程序,通过标准文件IO(open/read/write)访问设备
- VFS与系统调用层:统一文件抽象接口,屏蔽不同设备类型的差异
- 字符设备核心层:管理设备号、cdev结构体,建立文件操作与驱动的映射
- 内核子系统层:I2C子系统、platform平台总线,提供总线级通用能力
- 芯片驱动层:具体外设(如LM75温度传感器)的硬件操作逻辑
- 设备树层:独立描述板级硬件资源,实现驱动代码与硬件信息分离
- 硬件层:外设芯片通过I2C总线挂载到SOC的I2C控制器
二、platform平台驱动模型
1. 核心设计思想
平台总线模型实现驱动与设备分离 :platform总线下分为platform_driver(驱动逻辑)和platform_device(硬件信息,现由设备树替代),通过匹配机制自动绑定。
2. 驱动核心结构体
struct platform_driver {
int (*probe)(struct platform_device *); // 匹配成功后执行初始化
int (*remove)(struct platform_device *); // 设备移除时执行资源清理
struct device_driver driver;
const struct platform_device_id *id_table; // 传统ID匹配表
const struct of_device_id *of_match_table; // 设备树匹配表
};
3. probe函数核心职责
- 解析设备树节点,提取硬件资源(I2C地址、引脚、中断号等)
- 注册字符设备、创建/dev下的设备节点
- 芯片硬件初始化、申请中断资源
- 初始化驱动私有数据结构体
4. remove函数核心职责
- 释放中断、注销字符设备
- 销毁设备节点、释放内核内存
- 芯片硬件复位/下电,完成收尾清理
三、设备树(DTS)匹配机制
1. 匹配核心:compatible属性
驱动与设备树节点通过compatible字符串完全精确匹配,大小写、空格、标点必须完全一致,是驱动probe函数执行的前提。
2. I2C设备节点编写规范
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
lm75@48 {
compatible = "alientek,lm75";
reg = <0x48>; // I2C外设器件地址
};
};
- 节点必须挂载到硬件对应的I2C控制器节点下
reg属性为I2C芯片的器件地址status = "okay"表示启用该节点
3. 设备树生效验证
开发板端通过/proc/device-tree目录实时验证节点状态:
# 查看I2C控制器下的子节点
ls /proc/device-tree/soc/i2c@021a0000/
# 查看节点compatible属性
cat /proc/device-tree/soc/i2c@021a0000/lm75@48/compatible
四、I2C子系统驱动架构
1. I2C核心三要素
- I2C适配器(adapter):SOC内部I2C控制器的驱动,一般由芯片厂商提供
- I2C设备(client):外设芯片的内核抽象,对应设备树中的I2C节点
- I2C驱动(driver):外设芯片的业务逻辑,与client匹配后执行probe
2. I2C驱动注册流程
- 定义
i2c_driver结构体,配置of_match_table设备树匹配表 - 调用
i2c_add_driver将驱动注册到内核I2C总线 - 总线遍历设备树I2C节点,通过compatible匹配驱动
- 匹配成功自动调用驱动的probe函数
3. 核心通信API
i2c_smbus_read_byte_data:读取芯片指定寄存器的数据i2c_smbus_write_byte_data:向芯片指定寄存器写入数据- 所有通信通过I2C适配器统一调度,无需直接操作SOC寄存器
五、内核模块加载与卸载
1. 常用操作命令
insmod xxx.ko # 加载内核模块
rmmod xxx # 卸载模块(无需加.ko后缀)
lsmod # 查看系统已加载模块
modinfo xxx.ko # 查看模块详细信息
dmesg # 查看内核打印日志
2. 常见加载问题
- File exists报错 :内核已存在同名模块,属于重复加载,先执行
rmmod再加载即可 - 模块加载成功但probe不执行:90%以上为设备树compatible不匹配、设备树未更新、节点路径错误
- lsmod引用计数为0:驱动未匹配到任何硬件设备,未与实际硬件完成绑定
六、字符设备驱动层实现
1. 字符设备注册步骤
- 申请或指定设备号(主设备号+次设备号)
- 初始化
cdev结构体,绑定file_operations操作集 - 调用
cdev_add向内核注册字符设备 - 创建设备类(class)、创建设备节点(device),自动生成
/dev下的设备文件
2. file_operations核心操作集
struct file_operations {
.owner = THIS_MODULE,
.open = lm75_open, // 打开设备
.read = lm75_read, // 读取温度数据
.write = lm75_write, // 写入芯片配置
.release = lm75_release // 关闭设备
};
七、中断与并发控制
1. 中断处理流程
- 从设备树节点解析获取中断号
- 申请中断资源,注册中断处理函数
- 中断上下文采用底半部机制(tasklet/工作队列)处理耗时逻辑
- 驱动卸载时释放中断资源
2. 并发保护机制
- 原子操作:适用于简单计数、标志位保护
- 自旋锁:中断上下文、短时间临界区保护
- 互斥锁:进程上下文、耗时操作的临界区保护
- 禁止在中断上下文调用可能导致睡眠的函数
八、驱动调试与问题排查
1. 问题排查优先级
- 确认模块加载 :执行
lsmod确认模块已在内核中 - 确认设备树生效 :通过
/proc/device-tree验证节点存在 - 确认匹配字符串:逐字符对比驱动与设备树的compatible属性
- 查看内核日志 :
dmesg查看probe打印与错误信息 - 硬件通路验证 :
i2cdetect -y 总线号扫描I2C设备地址
2. 经典问题定位
- 模块已加载但无设备节点:probe函数未执行,优先排查设备树匹配
- i2cdetect扫不到设备地址:硬件接线错误、引脚复用未配置、I2C控制器未启用
- insmod直接加载失败:内核版本不匹配、依赖模块未加载、驱动代码语法错误