《网蜂A8实战演练》——6.Linux 平台总线驱动设备模型

时间:2022-04-30 10:26:33

第8章  Linux 平台总线驱动设备模型


Linux 系统提供了一种分离分层的思想,换句话说,它借用了将复杂问题简单化的思想,总是喜欢把一个驱动拆分开来写。打个比方说,有那么一种设备驱动,假设拆分为 A、B、C 为 3 个部分。A 部分负责管理 B、C 两部分,将 B和 C 进行绑定管理,再假设 B 负责比较稳定的代码,C 负责的是硬件相关的代码。这样一来,如果硬件设备改变了,那么驱动就不用大范围修改,只需要修改 C 负责的硬件相关的代码,这就是 Linux 的分离分层的思想。


8.1  platform 总线驱动设备模型


Linux2.6 提出了一种模型,叫设备驱动模型,它包含总线、设备、驱动三个实体,总线将设备和驱动绑定。在系统每注册一个设备的时候,会寻找与之匹配的驱动;在系统每注册一个驱动的时候,也会寻找与之匹配的设备,而这个匹配的伟大任务,则由总线这老人家来完成了。Linux 还发明了种虚拟的总线,这种虚拟的总线叫 platform 总线,设备则称为 platform 设备,相应的驱动则称为 platform 驱动。在嵌入式系统里,比如,在 S5PV210 处理器中,可以把 LED、BUTTONS、I2C、RTC、SPI、LCD、NAND 等等都可以看做是 platform_device。这真是,只有想不到没有做不到。


8.2  三个重要的结构体

为什么又是三个重要的结构体?Linux 内核里,大量的使用了面向对象的思想来编程,虽然使用的语言是 C 语言,但是它却大量的使用了面向对象的思想。不得不感叹一下,Linux 大牛们啊,真牛。

8.2.1 平台总线(platform_bus_type)

platform_bus_type 是一个 struct bus_type 型的结构体,其定义如下:

/* 参考 drivers/base/platform.c */
struct bus_type platform_bus_type = {
.name= "platform",/* 总线名字 */
.dev_attrs= platform_dev_attrs, /* 设备属性 */
/* 设备和驱动使用 match 函数来判断是否匹配 */
.match= platform_match,
.uevent= platform_uevent,/* 热拔插操作函数 */
.pm= &platform_dev_pm_ops,
};
platform_bus_type 中的成员,只需要关心 platform_match()函数,正是此函数确定了 platform_device 和 platform_driver 之间如何匹配,后面再分析。


8.2.2 平台设备(platform_device)

/* 参考 include/linux/platform_device.h */
struct platform_device {
const char * name;/* 平台设备名字 */
int  id;
/* 一般设置为-1,表示系统自动分配 */
bool  id_auto;
struct device dev;/* 每一个设备都有一个 device 结构体 */
u32  num_resources;/* 资源数量 */
struct resource * resource; /* 资源 */
/* platform_device_id,里面有个名字列表 */
const struct platform_device_id  *id_entry;
...
};


其中有个重要的结构体成员,那就是资源(resource)

struct resource {
resource_size_t start;/* 资源的起始值 */
resource_size_t end;/* 资源的结束值 */
const char *name;
/* 资源的类型,如:
* IORESOURCE_IO(IO 资源)、IORESOURCE_MEM(内存资源)、
* IORESOURCE_IRQ(中断资源)、IORESOURCE_DMA(DMA 资源)
*/
unsigned long flags;
struct resource *parent, *sibling, *child;
};

resource 里的 start、end 的含义会随着 flags 的变化而变化,比如:当 flags为 IORESOURCE_MEM 时,start、end 分别表示该 platform_device 占据的内存的开始地址和结束地址;当 flags 为 IORESOURCE_IRQ 时,start、end 分别表示该 platform_device 使用的中断号的开始值和结束值,如果只使用了一个中断,那么开始值和结束值相同。

Linux 提供了一个获取 resource 的接口函数,使用 platform_get_resource函数来获取资源。原型如下:
/**
* platform_get_resource - get a resource for a device
* @dev: platform device
* @type: resource type
* @num: resource index
*/
struct resource *platform_get_resource(struct platform_device *dev,
unsigned int type, unsigned int num)
{
int i;
for (i = 0; i < dev->num_resources; i++) {
struct resource *r = &dev->resource[i];
if (type == resource_type(r) && num-- == 0)
return r;
}
return NULL;
}

这个函数的关键点就是二个判断,如果二个判断都符合要求,那么返回符合要求的资源。为了分析这个函数,举个例子。比如你想要 IORESOURCE_MEM 资源下的第 2 个资源,在数组里即第 1 个资源,因为数组从零算起。就会这样调用:
res = platform_get_resource(pdev, IORESOURCE_MEM,1);

第 一 个 判 断 是 : type == resource_type(r) , type 就 是 我 们 刚 说 的IORESOURCE_IO 、 IORESOURCE_MEM 、 IORESOURCE_IRQ 、IORESOURCE_DMA 四种。从平台设备(platform_dev)获取资源(resource)后,首 先 判 断 是 不 是 这 四 种 当 中 的 一 种 , 因 为 例 子 中 的 type 确 实 是IORESOURCE_MEM,则继续判断是这种 IORESOURCE_MEM 类型资源下的,是不是你想要的那个资源,比如有 3 个 IORESOURCE_MEM 类型的资源,而你只想要第 2 个。如果 type 不是 IORESOURCE_MEM,则重新从平台设备的下一个资源里获取,直至最后一个。第二个判断是:num-- == 0,这什么意思呢?一开始 num = 1,确实不等于0 , 所 以 会 重 新 从 平 台 设 备 的 下 一 个 资 源 里 获 取 type 类 型 为IORESOURCE_MEM 类型的资源,这时 num = 0 了,符合要求,就会返回资源。

8.2.2 平台驱动(platform_drivers)

struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
};

platform_driver 里有个 platform_device_id 结构体,它与 platform_device设 备 里 的 platform_device_id 是 一 样 的 。 以 前 总 线 总 是 通 过platform_device.name 和 platform_driver.device_driver.name 来匹配。 新的Linux 版 本 , 也 增 加 了 另 外 一 种 机 制 , 也 就 是 通 过 比 较 二 者 里 的platform_device_id 里的 name。说白了,总线匹配最终就是比较二者的名字。

struct device_driver {
const char
*name;
struct module
*owner;
...
const struct of_device_id *of_match_table;
...
struct driver_private *p;
};

在建有内核工程的 Source Insight 里搜索“of_match_table”,就会搜出一大堆示例,你会发现,每一个 device_driver 实例都会设置上面这三个加深颜色的成员,所以几乎不用关心 device_driver 的其他成员。

8.3  平台总线如何匹配设备和驱动

你前面不是说了吗?总线将设备和驱动进行绑定,在系统每注册一个设备的时候,会寻找与之匹配的驱动;在系统每注册一个驱动的时候,也会寻找与之匹配的设备。那平台总线怎么匹配呢?答:通过 platform_match()函数。

/* 参考 drivers/base/platform.c */
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/* 因为没有配置 CONFIG_OF_DEVICE ,这里返回 0,不执行这个 if 分支
*/
if (of_driver_match_device(dev, drv))
return 1;
/*这里也是返回 0,不执行这个 if 分支*/
if (acpi_driver_match_device(dev, drv))
return 1;
/* 如果 pdrv->id_table 存在的话,调用 platform_match_id 函数
* 如果匹配成功,则返回 1,否则返回 0
*/
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* 如果前面都失败,则最后比较平台设备里的名字和设备驱动里的名字
* 即比较 platform_device.name 与 device_driver.name
* 如果匹配成功,则返回 1,否则返回 0
*/
return (strcmp(pdev->name, drv->name) == 0);
}
下面来看看 platform_match_id 函数是怎么匹配的。
/* 参考 drivers/base/platform.c */
static const struct platform_device_id *platform_match_id(
const struct platform_device_id *id,struct platform_device *pdev)
{
while (id->name[0]) {
if (strcmp(pdev->name, id->name) == 0) {
pdev->id_entry = id;
return id;
}
id++;
}
return NULL;
}

platform_match_id 函数的实现很简单,如果 pdrv->id_table->name[0]存在的话,就与 dev->name 进行比较,如果相同的话,就把 pdrv->id_table 赋给pdev->id_entry,然后返回一个类型为 platform_device_id 的 pdrv->id_table。如果比较不成功,就取出下一个 pdrv->id_table,然后再比较,直到最后一个pdrv->id_table,还是不成功,就返回 NULL。

8.3.1  内核如何注册平台总线

与平台驱动和平台设备不一样的是,平台总线并不是以模块的方式加载到内核的,下面以函数的调用流程来看看平台总线到底是怎么被内核调用的。

start_kernel
   -->rest_init
          -->kernel_thread(kernel_init,NULL,CLONE_FS|CLONE_SIGHAND);
              --> kernel_init_freeable();
                  --> do_basic_setup();
                       --> driver_init();
                            --> platform_bus_init();
                                  -->bus_register(&platform_bus_type);

在这里,除了告诉大家平台总线的调用流程,其实还想引出一个问题。就是Webee 怎么找到这个流程的?其实,很简单,在建有内核的工程里,使用 Source Insight 使用“ctrl + /”搜索对应的函数,就可以找到是哪个函数调用它的了。

8.4  四个注册/注销函数

8.4.1 平台设备注册函数(platform_device_register)

platform_device_register 用于注册一个平台设备。

int platform_device_register(struct platform_device *pdev)
{
/* 初始化 device 结构体成员 */
device_initialize(&pdev->dev);
arch_setup_pdev_archdata(pdev);
return platform_device_add(pdev);
}

/* 导出函数符号,让其他文件可以调用这个函数 */
EXPORT_SYMBOL_GPL(platform_device_register);
platform_device_register 最终使用 platform_device_add 函数来注册平台设
备。

/* 参考 drivers/base/platform.c */
int platform_device_add(struct platform_device *pdev)
{
int i, ret;
if (!pdev)
return -EINVAL;
if (!pdev->dev.parent)
pdev->dev.parent = &platform_bus;
/* 设置总线类型为平台总线类型 */
pdev->dev.bus = &platform_bus_type;
/* 根据 pdev->id 设置平台设备的名字 */
switch (pdev->id) {
default:
dev_set_name(&pdev->dev, "%s.%d", pdev->name, pdev->id);
break;
......
}

/* 遍历取出平台设备里的每一个资源 */
for (i = 0; i < pdev->num_resources; i++) {
struct resource *p, *r = &pdev->resource[i];
if (r->name == NULL)
r->name = dev_name(&pdev->dev);
p = r->parent;
/* 如果资源不为空,设置资源的类型 */
if (!p) {
/* 分配 IO 内存资源空间*/
if (resource_type(r) == IORESOURCE_MEM)
p = &iomem_resource;
/* 分配 IO 端口资源空间*/
else if (resource_type(r) == IORESOURCE_IO)
p = &ioport_resource;
}
/* 将新的 resource 插入内核资源树头(resource tree) */
if (p && insert_resource(p, r)) {
printk(KERN_ERR "%s: failed to claim resource %d\n",dev_name(&pdev->dev), i);
ret = -EBUSY;
goto failed;
}
}
......
/* 调用 device_add 函数注册一个设备(device) */
ret = device_add(&pdev->dev);
if (ret == 0)
return ret;
......
}

platform_device_add 最 终 调 用 device_add 来完 成 平台 设 备的 注 册 。device_add 就比较复杂了,这里就不分析了,但是给你们总结一下 device_add函数主要做了些什么工作:
第一、把 device 嵌入 bus 的 dev 链表。
第二、从 bus 的 drv 链表遍历取出每一个 drv,使用 bus 的 match 函数来判断drv 能不能支持这个 dev。
第三、如果支持,调用 drv 的 probe 函数。

8.4.2 平台驱动注册函数(platform_driver_register)

platform_driver_register 用于注册一个平台驱动。

int platform_driver_register(struct platform_driver *drv)
{
/* 先初始化 platform_driver 里的 driver,该 driver 的类型为
* device_driver,设置 driver 的 bus 为 platform_bus_type
*/
drv->driver.bus = &platform_bus_type;
/* 继续设置 driver 的 probe、remove、shutdown 成员
* 为平台驱动下的 probe、remove、shutdown 函数
*/
if (drv->probe)
drv->driver.probe = platform_drv_probe;
if (drv->remove)
drv->driver.remove = platform_drv_remove;
if (drv->shutdown)
drv->driver.shutdown = platform_drv_shutdown;
/* 最终调用 driver_register 函数注册一个驱动 */
return driver_register(&drv->driver);
}

driver_register 函数主要工作与 device_add 类似,这里也不分析源码,只总结 driver_register 函数的主要工作:
第一、把 drv 嵌入 bus 的 drv 链表。
第二、从 bus 的 dev 链表遍历取出每一个 dev,使用 bus 的 match 函数来判断dev 能不能支持这个 drv。
第三、如果支持,调用 drv 的 probe 函数。

8.4.3 平台设备注销函数(platform_device_unregister)

想要从内核里卸载平台设备,就使用 platform_device_unregister 函数。

void platform_device_unregister(struct platform_device *pdev)
{
/* 删除 device 结构,释放资源 */
platform_device_del(pdev);
/* 调用 put_device 函数将 device 从 bus 链表里删除 */
platform_device_put(pdev);
}
/* 参考 drivers/base/platform.c */
void platform_device_del(struct platform_device *pdev)
{
int i;
/* 删除 device */
if (pdev) {
device_del(&pdev->dev);
......
/* 遍历资源,调用 release_resource 函数释放资源 */
for (i = 0; i < pdev->num_resources; i++) {
struct resource *r = &pdev->resource[i];
unsigned long type = resource_type(r);
if (type == IORESOURCE_MEM || type == IORESOURCE_IO)
release_resource(r);
}
}
}
EXPORT_SYMBOL_GPL(platform_device_del);

8.4.4 平台驱动注销函数(platform_driver_unregister)

平台驱动的注销使用 platform_driver_unregister 函数。

void platform_driver_unregister(struct platform_driver *drv)
{
/* 将 drv 从 bus 链表里删除 */
driver_unregister(&drv->driver);
}

8.5  平台总线驱动设备驱动实例

平台总线驱动设备模型的驱动,一般分为两部分:一部分写 platform_device相关的,另一部分写 platform_driver 相关的。一般来说,platform_device 相关的代码编写,可以写到 BSP 里,这样一来,就实现了隔离 BSP 和驱动。在 BSP中定义 platform 设备和设备使用的资源、设备的具体配置信息,而在驱动里,只需要通过 API 去获取资源和数据,做到了板相关代码和驱动代码的分离,使得驱动具有更好的可扩展性和跨平台性。还记得, 7 章,第我们移植了 gpio_key驱动吗,那里我们主要修改的就是 BSP 板相关代码。但是,为了让大家更好的学习,Webee 将本实例分开二个文件来编写,并没有添加到 BSP 板里去。

8.5.1 平台设备驱动实例(platform_device)

本实例其实就是将 webee210_drivers\2th_led\led.c 拆开来编写。平台设备驱动源码路径为:webee210_drivers\7th_bus_drv_devc\led_dev.c

8.5.1.1 入口函数分析

static int __init led_dev_init(void)
{
/* 注册一个平台设备 */
return platform_device_register(&led_device);
}

入口函数很简单,注册了一个 led_device 平台设备。那 led_device 的定义是怎么样的呢?

static struct platform_device led_device = {
.id= -1,
/* 必须与 led_driver 的 name 一致 */
.name= "webee210_led",
.resource= led_resources,
.num_resources = ARRAY_SIZE(led_resources),
.dev={
.release= led_release,
},
};

led_device 有个 led_resources 资源,来,继续看看它的定义。

/* Webee210 开发板上的 LED1,LED2,LED3,LED4
* 对应 GPJ2_0、GPJ2_1、GPJ2_2、GPJ2_3 引脚
*/
static struct resource led_resources[] = {
[0] = {
.start = 0xE0200280,
.end= 0xE0200280 + 8 -1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = 0,
/* LED1 */
.end= 0,
.flags = IORESOURCE_IRQ,
},
};

这个就是硬件相关的代码了,如果有不同的设备,那么一般只需要修改这些硬件相关的代码,而 platform_driver 部分就不需要修改了。

8.5.1.2 出口函数分析

static void __exit led_dev_exit(void)
{
/* 注销一个平台设备 */
platform_device_unregister(&led_device);
}

还是那句,出口函数干了些什么事情,出口函数就是要把它“消灭”掉。驱动里,很多函数都是对称的。

8.5.1.3 图说平台设备驱动的编写流程

《网蜂A8实战演练》——6.Linux 平台总线驱动设备模型


8.5.2 平台驱动实例(platform_driver)

8.5.2.1 入口函数分析

static int __init led_drv_init(void)
{
/* 注册一个平台驱动 */
return platform_driver_register(&led_driver);
}

入口函数很简单,注册了一个 led_driver 平台驱动。那 led_driver 的定义是怎么样的呢?

static struct platform_driver led_driver = {
.probe= led_probe,
.remove= led_remove,
.driver={
.name = "webee210_led",
.owner = THIS_MODULE,
}
};

注意了,这里的 name 必须和平台设备里的 name 要保持一致,因为总线的match 函数就是通过比较它们的名字,来判断驱动能不能支持该设备。如果支持,就会调用 platform_driver 里的 probe 函数,在这里是调用 led_probe 函数。

8.5.2.2 probe 函数分析

static int led_probe(struct platform_device *pdev)
{
struct resource *res;
printk("led_probe, found led\n");
/* 根据 platform_device 的资源进行 ioremap */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
gpj2con = ioremap(res->start, res->end - res->start + 1);
gpj2dat = gpj2con + 1;
res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
pin = res->start;
/* 注册字符设备 */
major = register_chrdev(0, "led_drv", &led_fops);
/* 创建 led_drv 类 */
led_class = class_create(THIS_MODULE,"led_drv");
/* 在 led_drv 类下创建/dev/LED 设备,供应用程序打开设备*/
device_create(led_class,NULL,MKDEV(major,0),NULL,DEVICE_NAME);
return 0;
}

led_probe 函数,通过 platform_get_resource 函数来获取 platform_device里设置的 resource。获取到资源后,使用 ioremap 函数将资源映射到内核空间,这样驱动就可以使用这些资源了。接下来,又回到了以前我们熟悉的字符设备驱动了,其中,定义了一个 file_operations 型的 led_fops 实例。

static const struct file_operations led_fops = {
.owner= THIS_MODULE,
.open= webee210_led_open,
.write= webee210_led_write,
};

webee210_led_open 函数很简单,只是将 LED 对应的 GPIO 引脚功能设置为输入功能。

#define S5PV210_OUTP (0x1<<(pin*2))
static int webee210_led_open(struct inode * inode, struct file * filp)
{
/* 设置 LED 对应 GPIO 引脚功能为输出功能 */
*gpj2con |= S5PV210_OUTP;
return 0;
}

webee210_led_write 函数首先通过 copy_from_user 将用户应用程序传进来的数据存放到 val 里,然后根据 val 的值写入 gpj2dat 寄存器,这样一来就实现了数据从应用空间到内核空间,最后到硬件设备。

static int webee210_led_write(struct file * file, const char __user *buffer, size_t count, loff_t * ppos)
{
int val;
if(copy_from_user(&val, buffer, count))
return -EFAULT;
if (val == 1)
{
/* 点灯 */
*gpj2dat &= ~(1<<pin);
}
else
{
/* 灭灯 */
*gpj2dat |= (1<<pin);
}
return 0;
}

8.5.2.3 led_remove 函数分析

remove 函数什么时候被调用呢?当驱动被卸载的时候,就会调用 remove函数。它的工作主要是将 probe 函数干的事情“消灭”掉。

static int led_remove(struct platform_device *pdev)
{
printk("led_remove, remove led\n");
device_destroy(led_class, MKDEV(major, 0));
class_destroy(led_class);
unregister_chrdev(major, "led_drv");
iounmap(gpj2con);
return 0;
}

8.5.2.4 出口函数分析

static void __exit led_drv_exit(void)
{
/* 注销一个平台驱动 */
platform_driver_unregister(&led_driver);
}

有入口函数,自然就有出口函数。入口函数是注册平台驱动,出口函数自然是注销平台驱动。其实,Linux 在告诉大家一个道理:做人要有始有终。

8.5.1.4 图说平台驱动的编写流程

《网蜂A8实战演练》——6.Linux 平台总线驱动设备模型


8.5.3 测试程序

测 试 程 序 和 以 前 的 LED 测 试 程 序 是 一 样 的 , 源 码 路 径 为 :webee210_drivers\7th_bus_drv_devc\led_test.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
/* led_test on
* led_test off
*/
int main(int argc ,char *argv[])
{
int fd;
int val = 0;
fd = open("/dev/LED",O_RDWR);
if (fd < 0)
{
printf("open error\n");
}
if (argc != 2)
{
printf("Usage:\n");
printf("%s <on|off>\n",argv[0]);
return 0;
}
if(strncmp(argv[1],"on",2) == 0)
{
val = 1;
}
else if (strncmp(argv[1],"off",3) == 0)
{
val = 0;
}
write(fd,&val,4);
return 0;
}


8.5.4 测试结果

《网蜂A8实战演练》——6.Linux 平台总线驱动设备模型


8.6  本章小结

本章内容,相对第七章来讲相对比较简单,不知道你有没有这种感觉呢?但是它的出现率远远比输入子系统的出现率要高多了,内核里到处都是平台驱动设备模型的架构,希望大家能够对这种架构的驱动熟悉使用。下面来回顾一下本章学习了什么,本章首先介绍了什么是平台总线驱动设备模型,接着介绍了在这种架构下的三个重要的结构体,然后还分析了平台总线是如何匹配设备和驱动的,接着分析了四个注册/注销函数,最后以平台总线驱动设备模型为框架,实现了以前单纯用字符设备驱动来实现的 LED 驱动。为了让大家能够对这种框架有个更深入的认识,还特意画了两个流程图。