在Linux 2.6的设备驱动模型中,关心总线、设备和驱动这3个实体,总线将设备和驱动绑定。在系统每注册一个设备的时候,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。
一个现实的Linux设备和驱动通常都需要挂接在一种总线上,对于本身依附于PCI、USB、I2C、SPI等的设备而言,这自然不是问题,但是在嵌入式系统里面,SoC系统中集成的独立的外设控制器、挂接在SoC内存空间的外设等确不依附于此类总线。基于这一背景,Linux发明了一种虚拟的总线,称为platform总线,相应的设备称为platform_device,而驱动成为 platform_driver。
platform 总线是虚拟总线,当使用这个虚拟总线是带来的好处主要有两点:
使得设备被挂接在一个总线上,因此,符合 Linux 2.6 的设备模型。其结果是,配套的sysfs 结点、设备电源管理都成为可能。
隔离BSP和驱动。在BSP中定义platform 设备和设备使用的资源、设备的具体配置信息,而在驱动中,只需要通过通用 API 去获取资源和数据,做到了板相关代码和驱动代码的分离,使得驱动具有更好的可扩展性和跨平台性。
在网上看到一个非常有趣的比喻:总线是红娘,设备是男方,驱动是女方:
name字段) – match 函数进行匹配,看name是不是相同struct resource *resource)。int (*probe)(struct platform_device *) 匹配成功后执行的第一个函数),当然,如果男的跟小三跑了,女方也不会继续下去的。对于 Linux 这样一个成熟、庞大、复杂的操作系统,代码的重用性非常重要,否则的话就会在 Linux 内核中存在大量无意义的重复代码。
尤其是驱动程序,因为驱动程序占用了 Linux内核代码量的大头,如果不对驱动程序加以管理,任由重复的代码肆意增加,那么用不了多久Linux 内核的文件数量就庞大到无法接受的地步。
假如现在有三个平台 A、B 和 C,这三个平台(这里的平台说的是 SOC)上都有 MPU6050 这个 I2C 接口的六轴传感器,按照我们写裸机 I2C 驱动的时候的思路,每个平台都有一个MPU6050的驱动,因此编写出来的最简单的驱动框架如图所示:

每种平台下都有一个主机驱动和设备驱动,主机驱动肯定是必须要的,毕竟不同的平台其 I2C 控制器不同。但是右侧的设备驱动就没必要每个平台都写一个,因为不管对于那个 SOC 来说, MPU6050 都是一样,通过 I2C 接口读取数据就行了,只需要一个MPU6050 的驱动程序即可。
但是如果再多来几个设备呢?我们设备端的驱动又要重复编写好几次,这样会使我们的程序又变得臃肿了,这时候我们就在想如果我们可以让I2C控制器提供出来一个接口就好了,这样我们每个设备只需要也提供一个接口即可。
按照上面的想法,每个设备的话也只提供一个驱动程序(设备驱动),每个设备通过统一的 I2C 接口驱动来访问,这样就可以大大简化驱动文件。

实际的 I2C 驱动设备肯定有很多种,不止 MPU6050这一个,那么实际的驱动架构如图所示:

这个就是驱动的分隔,也就是将主机驱动和设备驱动分隔开来,比如 I2C、 SPI 等等都会采用驱动分隔的方式来简化驱动的开发。
在实际的驱动开发中,一般 I2C 主机控制器驱动已经由芯片厂家编写好了,而设备驱动一般也有设备器件的厂家编写好了,我们只需要提供设备信息即可,比如 I2C 设备的话提供设备连接到了哪个 I2C 接口上, I2C 的速度是多少等等。
相当于将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息(比如从设备树中获取到设备信息),然后根据获取到的设备信息来初始化设备。
这样就相当于驱动只需要负责驱动,设备只需要设备,想办法将两者进行匹配即可。这个就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型,也就是常说的驱动分离。

当我们向系统注册一个驱动的时候,总线就会在右侧的设备中查找,看看有没有与之匹配的设备,如果有的话就将两者联系起来。
同样的,当向系统中注册一个设备的时候,总线就会在左侧的驱动中查找看有没有与之匹配的设备,有的话也联系起来。
Linux 内核中大量的驱动程序都采用总线、驱动和设备模式,我们一会要重点讲解的 platform 驱动就是这一思想下的产物。
在使用设备树的时候 platform 驱动会通过 of_match_table 来保存兼容性值,也就是表明此驱动兼容哪些设备。所以 of_match_table 将会尤为重要,比如 platform 驱动中 platform_driver 就可以按照如下所示设置:
static const struct of_device_id leds_of_match[] = {
{ .compatible = "atkalpha-gpioled" }, /* 兼容属性 */
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, leds_of_match);
static struct platform_driver leds_platform_driver = {
.driver = {
.name = "imx6ul-led",
.of_match_table = leds_of_match,
},
.probe = leds_probe,
.remove = leds_remove,
};
1~4 行: of_device_id 表,也就是驱动的兼容表,是一个数组,每个数组元素为 of_device_id类型。每个数组元素都是一个兼容属性,表示兼容的设备,一个驱动可以跟多个设备匹配。这里我们仅仅匹配了一个设备,那就是上面创建的 gpioled 这个设备。2 行: compatible 值为“atkalpha-gpioled”,驱动中的 compatible 属性和设备中的 compatible 属性相匹配。若匹配成功,则驱动中对应的 probe 函数就会执行。3 行:是一个空元素,在编写 of_device_id 的时候最后一个元素一定要为空!6 行:通过 MODULE_DEVICE_TABLE 声明一下 leds_of_match 这个设备匹配表。11 行:设置 platform_driver 中的 of_match_table 匹配表为上面创建的 leds_of_match,至此我们就设置好了 platform 驱动的匹配表了。本次驱动程序的编写我们加入了设备树和platform结构体,具体代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define LEDDEV_CNT 1 /* 设备号长度 */
#define LEDDEV_NAME "dtsplatled" /* 设备名字 */
#define LEDOFF 0
#define LEDON 1
/* leddev 设备结构体 */
struct leddev_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
struct device_node *node; /* LED 设备节点 */
int led0; /* LED 灯 GPIO 标号 */
};
struct leddev_dev leddev; /* led 设备 */
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &leddev; /* 设置私有数据 */
return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
return -EFAULT;
}
/* 设备操作函数 */
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
};
static int led_probe(struct platform_device *dev)
{
printk("led driver and device was matched!\r\n");
/* 1、设置设备号 */
if (leddev.major)
{
leddev.devid = MKDEV(leddev.major, 0);
register_chrdev_region(leddev.devid, LEDDEV_CNT,
LEDDEV_NAME);
}
else
{
alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT,
LEDDEV_NAME);
leddev.major = MAJOR(leddev.devid);
}
/* 2、注册设备 */
cdev_init(&leddev.cdev, &led_fops);
cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);
/* 3、创建类 */
leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
if (IS_ERR(leddev.class))
{
return PTR_ERR(leddev.class);
}
/* 4、创建设备 */
leddev.device = device_create(leddev.class, NULL,leddev.devid,NULL, LEDDEV_NAME);
if (IS_ERR(leddev.device))
{
return PTR_ERR(leddev.device);
}
/* 5、初始化 IO */
leddev.node = of_find_node_by_path("/gpioled");
if (leddev.node == NULL)
{
printk("gpioled node nost find!\r\n");
return -EINVAL;
}
}
static int led_remove(struct platform_device *dev)
{
gpio_set_value(leddev.led0, 1); /* 卸载驱动的时候关闭 LED */
cdev_del(&leddev.cdev); /* 删除 cdev */
unregister_chrdev_region(leddev.devid, LEDDEV_CNT);
device_destroy(leddev.class, leddev.devid);
class_destroy(leddev.class);
return 0;
}
/* 匹配列表 */
static const struct of_device_id led_of_match[] =
{
{ .compatible = "atkalpha-gpioled" },
{ /* Sentinel */ }
};
/* platform 驱动结构体 */
static struct platform_driver led_driver =
{
.driver = {
.name = "imx6ul-led", /* 驱动名字,用于和设备匹配 */
.of_match_table = led_of_match, /* 设备树匹配表 */
},
.probe = led_probe,
.remove = led_remove,
};
static int __init leddriver_init(void)
{
return platform_driver_register(&led_driver);
}
static void __exit leddriver_exit(void)
{
platform_driver_unregister(&led_driver);
}
module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");
说明: 以上代码只保留了重要代码,功能性代码已经删除,这就是加入设备树和platform结构体的字符驱动设备的驱动代码。