总线设备驱动模型主要包含总线、设备、驱动三个部分,总线可以是一条真实存在的总线,例如USB、I2C等典型的设备。但是对于一些设备(内部的设备)可能没有现成的总线。Linux 2.6内核中引入了总线设备驱动模型。总线设备驱动模型与之前的三类驱动(字符、块设备、网络设备)没有必然的联系。设备只是搭载到了总线中。在linux内核中假设存在一条虚拟总线,称之为platform总线。platform总线相比与常规的总线模型其优势主要是platform总线是由内核实现的,而不用自己定义总线类型,总线设备来加载总线。platform总线是内核已经实现好的。只需要添加相应的platform device和platform driver。具体的实现过程主要包括如下的过程:
先定义platform_device ->注册platform_device->,再定义platform_driver->注册platform_driver。
注意:device和driver注册没有先后的顺序,这儿只是为了跟对应BSP对应。
整体而言只需要完成两个步骤,也就是设备的实现和驱动的实现,每一个实现都包括相关结构体的定义和注册。
platform_device注册
需要注意的是platform_device实质上是经过处理过的设备,在platform_device结构体中存在一个设备结构体,与之前的设备存在差别的是引入了设备资源。这些设备资源就能实现对设备寄存器,中断等资源的访问。平台设备的基本结构体如下:
structplatform_device {
const char * name; /*设备名*/
int id; /*设备ID号*/
struct device dev;/*结构体包含一个具体的device结构体*/
u32 num_resources;/*资源的数量*/
struct resource *resource;/*资源结构体,用来保存硬件的资源*/
struct platform_device_id *id_entry;/*平台设备的ID*/
};
其中struct device 和struct resource 是重要的结构体。struct device 在总线设备驱动模型中已经提到了。这次讨论一下struct resource。
struct resource {
/*资源的起始值,如果是地址,那么是物理地址,不是虚拟地址*/
resource_size_t start;
/*资源的结束值,如果是地址,那么是物理地址,不是虚拟地址*/
resource_size_t end;
/*资源名*/
const char *name;
/*资源的标示,用来识别不同的资源*/
unsigned long flags;
/*资源指针,可以构成链表*/
struct resource *parent, *sibling, *child;
};
platform_device的注册很简单,只需要在设备的初始化函数中首先定义相应的设备,通常采用函数platform_device *platform_device_alloc(const char *name, int id)动态申请,通常name就是需要申请的设备名,而id为-1。然后采用
intplatform_device_add(structplatform_device *pdev)
或者
intplatform_device_register(structplatform_device *pdev)
注册定义好的设备即可。、
同样在退出函数中释放注册好的设备即可,可以采用函数:
void platform_device_unregister(structplatform_device *pdev)。
然后一个平台设备就完成了,不需要像自己实现模型时定义相关的文件属性等。
设备资源可以通过相关函数得到:
struct resource *platform_get_resource(structplatform_device *dev, unsigned int type, unsigned intnum)
中断资源也可以通过:
intplatform_get_irq(structplatform_device *dev, unsigned intnum)
资源的使用主要是驱动实现过程中需要使用到的,但是后期的使用一般需要在驱动的probe函数中实现申请中断或者IO内存才能使用,而不能直接使用。特别是资源中的地址通常是物理地址,需要通过申请IO内存和映射完成物理到虚拟地址的转换,便于进程的访问。
platform_driver注册
平台驱动结构体platform_driver实现如下:
structplatform_driver {
/*平台驱动需要实现的相关函数操作,
其中的前4个函数与最后一个函数与device_driver中的函数是相同的
本质是实现对device_driver中相关函数的赋值。
*/
int (*probe)(structplatform_device *);
int (*remove)(structplatform_device *);
void (*shutdown)(structplatform_device *);
int (*suspend)(structplatform_device *, pm_message_t state);
int (*suspend_late)(structplatform_device *, pm_message_t state);
int (*resume_early)(structplatform_device *);
int (*resume)(structplatform_device *);
/*内嵌了一个设备驱动结构体*/
structdevice_driver driver;
/*平台设备ID,这与platform_device中的structplatform_device_id *id_entry是相同的
主要是完成总线的匹配操作,platform总线的匹配操作第一匹配要素就是该元素。而不再是简单的name选项。
*/
structplatform_device_id *id_table;
};
通常驱动的入口函数:
int (*probe)(structplatform_device *);
当总线完成了设备的match 操作以后就会进入驱动中该函数的运行。
总线函数的匹配操作如下:
static intplatform_match(struct device *dev, structdevice_driver *drv)
{
/*得到平台设备的指针*/
structplatform_device *pdev = to_platform_device(dev);
/*得到平台驱动指针*/
structplatform_driver *pdrv = to_platform_driver(drv);
/* match against the id table first */
/*从定义上分析,id_table是首先匹配的对象,然后才是name的匹配,当ID匹配完成时就说明匹配好了*/
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}
从上面的定义可以知道platform总线的匹配函数手下是比较id_table是匹配的首选项。
probe 函数称之为探针函数,用于检测总线上有该驱动能够处理的设备,而remove函数则是为了说明总线上该驱动能够处理的设备被移除。
因此这两个函数是在平台设备中一定要被实现的函数。
其他的函数则不一样要求实现。
平台驱动的设计主要是完成平台驱动结构体的填充和注册。
通常的平台驱动结构体实现如下:
static structplatform_drivermy_driver =
{
/*平台驱动的probe函数实现*/
.probe = my_probe,
/*平台驱动的remove函数实现*/
.remove = my_remove,
/*实现设备驱动的name和owner变量*/
.driver =
{
/*该参数主要实现总线中的匹配函数调用*/
.name = "my_dev",
/*该函数表示模块的拥有者*/
.owner = THIS_MODULE,
},
};
其中的my_probe和my_remove是自己定义的probe和remove函数。
最主要的是内嵌设备驱动结构体的填充,主要的填充包括name和owner两个,当然也可以包括其他的。由于没有填充id_table,那么name就是总线匹配操作的第一选择。因此如果没有填充好id_table,那么name元素是一定要实现的,不然不能完成相应的设备驱动匹配操作。
完成platform_driver结构体的填充过后就是完成驱动的在初始化阶段的注册以及退出阶段的释放操作,基本的实现函数为:
注册函数,通常在驱动初始化函数中调用:
intplatform_driver_register(structplatform_driver *drv)
释放函数,通常在驱动退出函数调用:
void platform_driver_unregister(structplatform_driver *drv)
完成相关的注册以后总线、设备、驱动的大概框架就完成啦。
但是这只是常用的框架,还不能在应用程序中使用。
基于平台驱动的设备驱动都是基于总线架构的,基本的实现过程与之前的简单字符设备存在较大的差别,主要的区别在驱动的初始化不在是平台设备驱动的初始化函数中实现,而是在probe函数中实现。而驱动的卸载函数则是在remove函数中实现。probe函数是平台总线实现匹配以后首先被调用的函数,因此在其中实现字符设备、块设备、网络设备驱动的初始化是有意义的,这样的设备驱动就是基于平台总线的设备驱动,便于维护。
平台总线驱动的注册过程分析:
intplatform_driver_register(structplatform_driver *drv)
{
/*第一步,仍然是完成结构体的填充操作*/
/*驱动的总线类型*/
drv->driver.bus = &platform_bus_type;
/*将自己定义的probe函数赋值给平台驱动中设备驱动的probe函数,其他函数类似*/
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;
if (drv->suspend)
drv->driver.suspend = platform_drv_suspend;
if (drv->resume)
drv->driver.resume = platform_drv_resume;
/*第二步,仍然是完成一般设备驱动的注册操作*/
/*然手就是一般驱动的注册,这样就完成了设备的注册*/
return driver_register(&drv->driver);
}
/*设备驱动的probe函数的赋值过程*/
static intplatform_drv_probe(struct device *_dev)
{
/*得到设备对应的平台驱动*/
structplatform_driver *drv = to_platform_driver(_dev->driver);
/*得到设备的平台设备*/
structplatform_device *dev = to_platform_device(_dev);
/*下面的probe是自己实现的probe函数。具体的实现思路:
根据一般设备找对应的平台设备,同时根据设备的驱动找到平台驱动。
然后返回平台驱动的probe函数(自己实现通常是初始化操作)地址。
*/
return drv->probe(dev);
}
实现的总线平台驱动模型的最简单源码:
平台设备的实现:device.c
#include<linux/device.h>
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/init.h>
#include<linux/string.h>
#include<linux/platform_device.h>
/*平台模型驱动的平台设备对象*/
static structplatform_device *my_device;
/*初始化函数*/
static int __initmy_device_init(void)
{
int ret = 0;
/*采用platform_device_alloc分配一个platform_device对象
参数分别为platform_device的name,和id。
*/
my_device = platform_device_alloc("my_dev",-1);
/*注册设备,注意不是platform_device_register,将平台设备注册到内核中*/
ret = platform_device_add(my_device);
/*如果出错释放相关的内存单元*/
if(ret)
{
platform_device_put(my_device);
}
return ret;
}
/*卸载处理函数*/
static void __exit my_device_exit(void)
{
platform_device_unregister(my_device);
}
module_init(my_device_init);
module_exit(my_device_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("GP-<gp19861112@yahoo.com.cn>");
linux platform 驱动模型分析
一. 概述
platform设备和驱动与linux设备模型密切相关。platform在linux设备模型中,其实就是一种虚拟总线没有对应的硬件结构。它的主要作用就是管理系统的外设资源,比如io内存,中断信号线。现在大多数处理器芯片都是soc,如s3c2440,它包括处理器内核(arm920t)和系统的外设(lcd接口,nandflash接口等)。linux在引入了platform机制之后,内核假设所有的这些外设都挂载在platform虚拟总线上,以便进行统一管理。
二. platform 总线
- 在系统中platform对应的文件drivers/base/platform.c,它不是作为一个模块注册到内核的,关键的注册总线的函数由系统初始化部分,对应/init/main.c中的do_basic_setup函数间接调用。这里可以看出platform非常重要,要在系统其他驱动加载之前注册。下面分析platform总线注册函数
int __init platform_bus_init(void)
{
int error;
early_platform_cleanup();
error = device_register(&platform_bus);
//总线也是设备,所以也要进行设备的注册
if (error)
return error;
error = bus_register(&platform_bus_type);
//注册platform_bus_type总线到内核
if (error)
device_unregister(&platform_bus);
return error;
}
这个函数向内核注册了一种总线。他首先由/drivers/base/init.c中的driver_init函数调用,driver_init函数由/init/main.c中的do_basic_setup函数调用,do_basic_setup这个函数由kernel_init调用,所以platform总线是在内核初始化的时候就注册进了内核。
2. platform_bus_type 总线结构与设备结构
(1) platform总线 设备结构
struct device platform_bus = {
.init_name = "platform",
};
platform总线也是一种设备,这里初始化一个device结构,设备名称platform,因为没有指定父设备,所以注册后将会在/sys/device/下出现platform目录。
(2) platform总线 总线结构
struct bus_type platform_bus_type = {
.name = "platform",
.dev_attrs = platform_dev_attrs,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
platform_dev_attrs 设备属性
platform_match match函数,这个函数在当属于platform的设备或者驱动注册到内核时就会调用,完成设备与驱动的匹配工作。
platform_uevent 热插拔操作函数
三. platform 设备
- platform_device 结构
struct platform_device {
const char * name;
int id;
struct device dev;
u32 num_resources;
struct resource * resource;
struct platform_device_id *id_entry;
/* arch specific additions */
struct pdev_archdata archdata;
};
(1)platform_device结构体中有一个struct resource结构,是设备占用系统的资源,定义在ioport.h中,如下
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
(2) num_resources 占用系统资源的数目,一般设备都占用两种资源,io内存和中断信号线。这个为两种资源的总和。
2. 设备注册函数 platform_device_register
int platform_device_register(struct platform_device *pdev)
{
device_initialize(&pdev->dev);
return platform_device_add(pdev);
}
这个函数首先初始化了platform_device的device结构,然后调用platform_device_add,这个是注册函数的关键,下面分析platform_device_add:
int platform_device_add(struct platform_device *pdev)
{
int i, ret = 0;
if (!pdev)
return -EINVAL;
if (!pdev->dev.parent)
pdev->dev.parent = &platform_bus;
//可以看出,platform设备的父设备一般都是platform_bus,所以注册后的platform设备都出现在/sys/devices/platform_bus下
pdev->dev.bus = &platform_bus_type;
//挂到platform总线上
if (pdev->id != -1)
dev_set_name(&pdev->dev, "%s.%d", pdev->name, pdev->id);
else
dev_set_name(&pdev->dev, "%s", pdev->name);
//设置设备名字,这个名字与/sys/devices/platform_bus下的名字对应
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) {
if (resource_type(r) == IORESOURCE_MEM)
p = &iomem_resource;
else if (resource_type(r) == IORESOURCE_IO)
p = &ioport_resource;
}
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;
}
}
//上面主要是遍历设备所占用的资源,找到对应的父资源,如果没有定义,那么根据资源的类型,分别赋予iomem_resource和ioport_resource,然后调用insert_resource插入资源。
//这样系统的资源就形成了一个树形的数据结构,便于系统的管理
pr_debug("Registering platform device '%s'. Parent at %s\n",
dev_name(&pdev->dev), dev_name(pdev->dev.parent));
ret = device_add(&pdev->dev);
//注册到设备模型中
if (ret == 0)
return ret;
failed:
while (--i >= 0) {
struct resource *r = &pdev->resource[i];
unsigned long type = resource_type(r);
if (type == IORESOURCE_MEM || type == IORESOURCE_IO)
release_resource(r);
}
return ret;
}
- mini2440内核注册platform设备过程
因为一种soc确定之后,其外设模块就已经确定了,所以注册platform设备就由板级初始化代码来完成,在mini2440中是mach-mini2440.c的mini2440_machine_init函数中调用platform_add_devices(mini2440_devices, ARRAY_SIZE(mini2440_devices))来完成注册。这个函数完成mini2440的所有platform设备的注册:
(1) platform_add_devices函数是platform_device_register的简单封装,它向内核注册一组platform设备
(2) mini2440_devices是一个platform_device指针数组,定义如下:
static struct platform_device *mini2440_devices[] __initdata = {
&s3c_device_usb,
&s3c_device_rtc,
&s3c_device_lcd,
&s3c_device_wdt,
&s3c_device_i2c0,
&s3c_device_iis,
&mini2440vice_eth,
&s3c24xx_uda134x,
&s3c_device_na
&s3c_device_sdi,
&s3c_device_usbgadget,
};
这个就是mini2440的所有外设资源了,每个外设的具体定义在/arch/arm/plat-s3c24xx/devs.c,下面以s3c_device_lcd为例说明,其他的类似。s3c_device_lcd在devs.c中它定义为:
struct platform_device s3c_device_lcd = {
.name = "s3c2410-lcd",
.id = -1,
.num_resources = ARRAY_SIZE(s3c_lcd_resource),
.resource = s3c_lcd_resource,
.dev = {
.dma_mask = &s3c_device_lcd_dmamask,
.coherent_dma_mask = 0xffffffffUL
}
};
可以看出,它占用的资源s3c_lcd_resource,定义如下:
static struct resource s3c_lcd_resource[] = {
[0] = {
.start = S3C24XX_PA_LCD,
.end = S3C24XX_PA_LCD + S3C24XX_SZ_LCD - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_LCD,
.end = IRQ_LCD,
.flags = IORESOURCE_IRQ,
}
};
static struct resource s3c_lcd_resource[] = {
[0] = {
.start = S3C24XX_PA_LCD,
.end = S3C24XX_PA_LCD + S3C24XX_SZ_LCD - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_LCD,
.end = IRQ_LCD,
.flags = IORESOURCE_IRQ,
}
};
这是一个数组,有两个元素,说明lcd占用了系统两个资源,一个资源类型是IORESOURCE_MEM代表io内存,起使地址S3C24XX_PA_LCD,这个是LCDCON1寄存器的地址。另外一个资源是中断信号线。
四. platform设备驱动
如果要将所写的驱动程序注册成platform驱动,那么所做的工作就是初始化一个platform_driver,然后调用platform_driver_register进行注册。
1. 基本数据机构platform_driver
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;
struct platform_device_id *id_table;
};
这是platform驱动基本的数据结构,在驱动程序中我们要做的就是声明一个这样的结构并初始化。下面是lcd驱动程序对它的初始化:
static struct platform_driver s3c2412fb_driver = {
.probe = s3c2412fb_probe,
.remove = s3c2410fb_remove,
.suspend = s3c2410fb_suspend,
.resume = s3c2410fb_resume,
.driver = {
.name = "s3c2412-lcd",
.owner = THIS_MODULE,
},
};
上面几个函数是我们要实现的,它将赋值给device_driver中的相关成员,probe函数是用来查询特定设备是够真正存在的函数。当设备从系统删除的时候调用remove函数。
2. 注册函数platform_driver_register
int platform_driver_register(struct platform_driver *drv)
{
drv->driver.bus = &platform_bus_type;
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;
return driver_register(&drv->driver);
}
这个函数首先使驱动属于platform_bus_type总线,将platform_driver结构中的定义的probe,remove,shutdown赋值给device_driver结构中的相应成员,以供linux设备模型核心调用,然后调用driver_regster将设备驱动注册到linux设备模型核心中。
五. 各环节的整合
前面提到mini2440板级初始化程序将它所有的platform设备注册到了linux设备模型核心中,在/sys/devices/platform目录中都有相应的目录表示。platform驱动则是由各个驱动程序模块分别注册到系统中的。但是他们是如何联系起来的呢,这就跟linux设备模型核心有关系了。在ldd3中的linux设备模型的各环节的整合中有详细的论述。这里简要说明一下platform实现的方法。每当注册一个platform驱动的时候就会调用driver_register,这个函数的调用会遍历设备驱动所属总线上的所有设备,并对每个设备调用总线的match函数。platform驱动是属于platform_bus_type总线,所以调用platform_match函数。这个函数实现如下:
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);
/* match against the id table first */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}
这个函数将device结构转换为platform_devcie结构,将device_driver结构转换为platform_driver结构,并调用platform_match_id对设备与驱动相关信息进行比较。如果没有比较成功会返回0,以便进行下一个设备的比较,如果比较成功就会返回1,并且将device结构中的driver指针指向这个驱动。然后调用device_driver中的probe函数,在lcd驱动中就是s3c2412fb_probe。这个函数是我们要编写的函数。这个函数检测驱动的状态,并且测试能否真正驱动设备,并且做一些初始化工作。