一、Platform 总线思想(设备与驱动分离)
在Linux内核中,为了更好对驱动资源进行管理,引入了 Platform 思想。它的核心就是:将 device(设备)和 driver(驱动)分离。
1. 什么是 Platform 思想?
-
将 device 和 driver 分别注册到
platform总线(bus)上。 -
总线会通过一定的匹配方式(如 name、设备树 compatible 属性、ID table)进行匹配。
-
匹配成功后 :执行 driver 中的
probe函数。 -
注销时 :执行 driver 中的
remove函数。
2. Platform 匹配方式
常用的有三种:
-
name字符串匹配。 -
设备树
compatible属性匹配(现代驱动最常用)。 -
ID table匹配。
3. Platform 驱动实现流程(套路)
-
描述硬件 :通过设备树描述 device 信息,在设备树中增加一个设备节点。通过设置
compatible属性,方便后续与驱动进行匹配。 -
定义驱动结构体 :驱动中通过
platform_driver结构体,填充probe、remove函数。 -
构建匹配表 :驱动中构建
compatible列表,后续与设备树中的compatible属性匹配。 -
注册驱动 :驱动中的
init函数中调用platform_driver_register函数,将 driver 注册到 platform 总线。 -
注销驱动 :驱动中的
exit函数中调用platform_driver_unregister函数,将 driver 从 platform 总线注销。 -
执行 Probe :当设备树节点中的
compatible属性与驱动中的compatible属性匹配成功后,会自动执行probe函数。 -
在 Probe 中干活 :在
probe函数中完成杂项设备/字符型设备驱动的注册,并实现file_operations结构体中的open、read、write、release函数。
二、I2C 子系统驱动框架
I2C是嵌入式开发中极其常用的总线,Linux内核为其设计了一套非常完善的子系统架构。我们将整个I2C系统分为四层:
-
I2C 物理硬件层:具体的I2C硬件设备(如I2C传感器)。
-
I2C 适配器驱动层 (Adapter):I2C控制器驱动,一般由芯片驱动厂商实现。
-
I2C 核心层 (core):提供I2C设备和驱动的注册、匹配、通信的方法。
-
I2C 设备驱动层 (Device/Client):这是我们自己需要实现的,针对具体I2C设备的驱动。
-
(最上层) 用户层:调用驱动层的接口,操作I2C设备。
💡 通俗理解:
传感器挂在I2C总线上。芯片厂商写好了"适配器驱动层"来管物理控制器;内核提供了"核心层"来牵线搭桥;我们只需要写"设备驱动层",通过核心层去找适配器,把数据读写出来给应用层(用户层)用就行了。
三、实战解析:LM75 温度传感器驱动
结合图片里的代码,我们来拆解一个真实的I2C驱动 lm75。
1. 驱动注册与注销
和Platform驱动类似,I2C驱动也有标准的入口和出口:
c
static int __init lm75_driver_init(void)
{
int ret = i2c_add_driver(&lm75_driver); // 将i2c驱动注册到i2c总线
if (ret < 0)
goto err_i2c_add;
printk("lm75_driver_init ...\n");
return 0;
err_i2c_add:
return ret;
}
static void __exit lm75_driver_exit(void)
{
i2c_del_driver(&lm75_driver); // 将i2c驱动从总线注销
printk("lm75_driver_exit ...\n");
}
2. 核心结构体描述 (i2c_driver)
通过 i2c_driver 结构体来描述驱动信息,最重要的就是 probe 和 remove 函数,以及匹配表。
c
static struct i2c_driver lm75_driver = {
.probe = probe,
.remove = remove,
.driver = {
.name = DEV_NAME,
.of_match_table = of_lm75_table, // 设备树匹配表
},
.id_table = lm75_table, // 传统ID匹配表
};
3. 设备树匹配 (compatible)
驱动通过 compatible 属性去匹配设备树中的节点,匹配成功就会调用 probe。
c
static const struct of_device_id of_lm75_table[] = {
{ .compatible = "ti,lm75" }, // 通过compatible属性匹配
{}
};
4. 读数据 (read 函数实现)
在字符设备的 read 函数中,我们使用 i2c_msg 结构体来封装发送的数据,并使用 i2c_transfer 来完成真正的通信。
c
static ssize_t read(struct file * file, char __user * buf, size_t size, loff_t * loff)
{
int ret = 0;
unsigned char temp[2] = {0};
// 1. 构建i2c通信结构体
struct i2c_msg msg = {
.addr = lm75_client->addr, // 从机地址
.flags = I2C_M_RD, // 数据流向位:I2C_M_RD 读
.len = 2, // 数据长度
.buf = temp // 缓冲区
};
// 2. 完成i2c数据的传输
lm75_client->adapter->algo->master_xfer(lm75_client->adapter, &msg, 1);
// 或者使用内核提供的封装函数:i2c_transfer(lm75_client->adapter, &msg, 1);
// 3. 将数据拷贝给用户层
ret = copy_to_user(buf, temp, sizeof(temp));
return sizeof(temp);
}
注:在实际开发中,i2c_transfer 是更标准、更推荐使用的接口,它能更好地处理锁和错误。
💡 学习小结:
从今天的代码可以看出,驱动开发其实很像"填空题"。
-
对于Platform总线,核心是分离,设备树负责描述硬件,驱动负责处理逻辑,
probe是关键。 -
对于I2C驱动,内核框架已经帮我们做好了适配器,我们只需要面向
i2c_client构建i2c_msg,调用i2c_transfer即可。