Linux在2.6内核中,针对一系列设备驱动,提供新的管理框架,成为platform机制,推出的目的,在于隔离驱动的资源和实现,使得驱动更加独立,驱动使用的资源统一由内核来管理,这些资源包括驱动所使用的内存地址、中断号等等。
要为不同的驱动程序提供一个框架,首先要抽象出不同驱动所共有的东西,简单来说,驱动程序驱动外部硬件正常工作,一般的,一个硬件外设只能有一个驱动,不同的硬件外设需要不同的驱动程序。对于具有某一类共同功能的外设,就比如存储,有磁盘、sd卡、U盘、flash等等,这些都是由一类特定的框架子系统来做核心中转层,向上提供统一接口,向下屏蔽不同硬件差异。遵循隔离变化的中心思想,在这个前提下,要把 驱动用到的资源 和 驱动使用这些资源干的事情,这两者隔离开来。打个比方,我用菜刀切菜,只要菜刀的使用方法不变,不管是哪个厂家生产的菜刀,我都可以用来切菜。菜刀和切菜,联系着两者的是菜刀的使用方法,只要保持使用方法不变,那么无论菜刀或是蔬菜怎么改变,我都可以不变应万变。
在Linux中,“菜刀”使用platform_device来描述,“菜刀的使用说明”使用platform_driver来描述,从这个层面来理解,在生产菜刀时,肯定是先有菜刀,再有菜刀的使用说明,这符合逻辑。在Linux的世界中,有着众多的刀具,“X刀”和“X刀的使用说明”,那么在茫茫刀海,X刀要找到只属于它的”X刀的使用说明“,就只能从名字上寻找,这符合正常逻辑。而谁来负责寻找这种对应关系呢?这只能由内核这个大管家来找。作为一个刀具,你要先被锻造出来,打扮一新,然后跟内核管家说一声,让他知道你的存在。作为刀具的使用说明,它先被仔细的写好,然后要求内核管家给它找对应的刀具。这个前后告知内核的顺序和生产顺序是一样的,先有菜刀,菜刀通知内核,再有菜刀的使用说明,使用说明请求内核去寻找菜刀。找到了,双方皆大欢喜,共结连理,找不到,那就只能孤独终老。
上述机制,有以下两个优点:
1. 隔离BSP和驱动,提高驱动和资源的独立性,拥有较好的可移植性、可扩展性和安全性(使用内核提供的统一接口)。
2. 使得设备挂载一个总线上,符合Linux 2.6的设备模型,有利于 sysfs节点管理和总线电源管理。
全局总线
Linux为platform总线定义了一个bus_type的全局实例,对外导出,全局可见。
它的match成员比较重要,以后会用到。
平台设备
下面先从主要结构体来讲解,然后讲解一些相关的函数使用。本文中分析的内核版本为2.6.32.27.
platform_device结构体
1: struct platform_device {
2: const char *name; // 平台设备名称
3: int id;
4: struct device dev;
5: u32 num_resources; //设备使用资源数量
6: struct resource * resource; //平台设备资源
7:
8: struct platform_device_id *id_entry;
9:
10: /* arch specific additions */
11: struct pdev_archdata archdata; //
12: };
其中,最重要的成员为 struct resouce *resource,一般我们只需要关系start、end和flag这三个字段就够了。
1: /*
2: * Resources are tree-like, allowing
3: * nesting etc..
4: */
5: struct resource {
6: resource_size_t start; //资源起始地址
7: resource_size_t end; //资源结束地址
8: const char *name; //资源名称
9: unsigned long flags; // 资源类型,有IO,MEM,IRQ,DMA类型
10: struct resource *parent, *sibling, *child; //资源链表指针
11: };
以mach-bast.c中的平台网卡设备为例子:
platform_device包含平台设备通用数据信息,如果某一类型的平台设备需要自定义的一些设备信息,比如对于一些网卡设备而言,我们可能需要知道它的MAC地址,总线宽度,有无EEPROM等信息,我们可以自定义一个设备资源结构体,然后将此结构体放入platform_data中,如上图的bast_dm9k_platdata一样。
其中,资源定义如下:
1: static struct resource bast_dm9k_resource[] = {
2: [0] = {
3: .start = S3C2410_CS5 + BAST_PA_DM9000,
4: .end = S3C2410_CS5 + BAST_PA_DM9000 + 3,
5: .flags = IORESOURCE_MEM,
6: },
7: [1] = {
8: .start = S3C2410_CS5 + BAST_PA_DM9000 + 0x40,
9: .end = S3C2410_CS5 + BAST_PA_DM9000 + 0x40 + 0x3f,
10: .flags = IORESOURCE_MEM,
11: },
12: [2] = {
13: .start = IRQ_DM9000,
14: .end = IRQ_DM9000,
15: .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_HIGHLEVEL,
16: }
17:
18: };
上述bast_device_dm9k结构体,嵌入到一个 platform_device指针数组中去,如下所示:
1: static struct platform_device *bast_devices[] __initdata ={
2: &s3c_device_usb,
3: &s3c_device_lcd,
4: &s3c_device_wdt,
5: &s3c_device_i2c0,
6: &s3c_device_rtc,
7: &s3c_device_nand,
8: &s3c_device_adc,
9: &s3c_device_hwmon,
10: &bast_device_dm9k, //我们定义的设备
11: &bast_device_asix,
12: &bast_device_axpp,
13: &bast_sio,
14: };
上述这个平台设备指针数组通过amlm5900_init 进行初始化,如下所示:
1: static void __init bast_init(void)
2: {
3: sysdev_class_register(&bast_pm_sysclass);
4: sysdev_register(&bast_pm_sysdev);
5:
6: s3c_i2c0_set_platdata(&bast_i2c_info);
7: s3c24xx_fb_set_platdata(&bast_fb_info);
8: platform_add_devices(bast_devices, ARRAY_SIZE(bast_devices)); //注册平台设备
9:
10: i2c_register_board_info(0, bast_i2c_devs,
11: ARRAY_SIZE(bast_i2c_devs));
12:
13: usb_simtec_init();
14: nor_simtec_init();
15:
16: s3c_cpufreq_setboard(&bast_cpufreq);
17: }
这里的 platform_add_devices 函数用于向内核通知添加众多平台设备,函数内部通过platform_device_register来进行设备注册。
注意,菜刀是先于菜刀的使用说明出现的,所以,”菜刀”的注册要先于“菜刀的使用说明”.
上面的过程,就是定义并且注册平台设备到系统中的步骤,看起来很简单,恩,确实很简单。
平台驱动
下面来介绍平台驱动的定义和注册。
平台驱动的结构体定义如下: 我们一般只关心 probe、remove、和driver.name,这三项就足够。
1: struct platform_driver {
2: int (*probe)(struct platform_device *);
3: int (*remove)(struct platform_device *);
4: void (*shutdown)(struct platform_device *);
5: int (*suspend)(struct platform_device *, pm_message_t state);
6: int (*resume)(struct platform_device *);
7: struct device_driver driver;
8: struct platform_device_id *id_table;
9: };
接上上面的dm9000.c网卡驱动为例子介绍:
1: static struct platform_driver dm9000_driver = {
2: .driver = {
3: .name = "dm9000",
4: .owner = THIS_MODULE,
5: .pm = &dm9000_drv_pm_ops,
6: },
7: .probe = dm9000_probe,
8: .remove = __devexit_p(dm9000_drv_remove),
9: };
此驱动通过下列函数进行初始化:
1: static int __init dm9000_init(void)
2: {
3: printk(KERN_INFO "%s Ethernet Driver, V%s ", CARDNAME, DRV_VERSION);
4: return platform_driver_register(&dm9000_driver);
5: }
通常情况下,只要和内核本身运行依赖性不大的外围设备,相对独立,拥有各自独立资源(地址总线和IRQs),都可以用platform_driver来实现,例如,LCD,网卡、USB、UART等。
在平台驱动中,可以通过platform_get_resource来获得特定的资源。
平台驱动是如何找到平台设备的
下面来介绍平台驱动是如何找到对于的平台设备的。以dm9000_init为例子,
在进行平台驱动的初始化时,
int platform_driver_register(struct platform_driver *drv)
{
drv->driver.bus = &platform_bus_type;
…….
return driver_register(&drv->driver);
}
进入到driver_register中,
int driver_register(struct device_driver *drv)
{
….
ret = bus_add_driver(drv);
}
进入到bus_add_drivers中
int bus_add_driver(struct device_driver *drv)
{
…..
if (drv->bus->p->drivers_autoprobe) { // 此变量在int bus_register(struct bus_type *bus) 中被赋值为 priv->drivers_autoprobe = 1;
error = driver_attach(drv); // 执行driver_attach函数,drv为传入的dm9000_driver->driver
if (error)
goto out_unregister;
}
…..
}
进入到driver_attach中
int driver_attach(struct device_driver *drv)
{
//传入的drv为 dm9000_driver->driver
// dm9000_driver->driver->bus
return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
}
进入bus_for_each_dev,首先,从名字上,可以猜测出,在bus上面寻找每个设备。
/**
* bus_for_each_dev - device iterator.
* @bus: bus type.
* @start: device to start iterating from.
* @data: data for the callback.
* @fn: function to be called for each device.
在指定的bus上迭代寻找设备,对每一个设备执行fn回调函数,回调的传入参数为data,如果start不为NULL,
我们从start指定的设备开始寻找。
对于fn回调函数,每次都检查它的返回值,如果返回非0,我们立即退出循环,并且返回此值。
注意:回调函数返回非0的设备,不会保存,该设备的引用计数也不会增加。如果调用者需要记住
此数据,它内部应该做到,在提供的回调函数中增加引用计数。
*/
int bus_for_each_dev(struct bus_type *bus, struct device *start,
void *data, int (*fn)(struct device *, void *))
{
…. //枚举此总线上注册的device,为每一个找到的设备调用__driver_attach,试图将传递进来的driver进行匹配。
struct device *dev;
while ((dev = next_device(&i)) && !error)
error = fn(dev, data); // 这里的fn = __driver_attach dev等于驱动在bus上对应的设备
…..
}
进入__driver_attach,随后进入
static int __driver_attach(struct device *dev, void *data)
{
struct device_driver *drv = data;
if (!driver_match_device(drv, dev)) // 这里执行:drv->bus->match ? drv->bus->match(dev, drv) : 1;
return 0;
driver_probe_device(drv, dev); //drv为bus上的驱动,dev为bus上的设备,这里的bus为前面赋值的全局platform_bus_type
……..
}
从名字上猜测,由驱动去探测对应的设备,如果探测到了,就绑定驱动和对于的设备。流程运行到这里,先会调用对应总线的match函数,寻找匹配,
如果匹配上了再调用probe进行绑定。如果匹配不上,直接退出报错。
总线的match函数如下:
//平台设备ID规定【设备名:实例号】,例如 pci0,floppy42
//设备驱动ID规定【驱动名】,例如pci等
对于DM9000来说,驱动名称为:
平台设备名称为:
从上面可以看出,两者的名字一样,通过match函数测试,然后进行绑定操作probe。
int driver_probe_device(struct device_driver *drv, struct device *dev)
{
……
ret = really_probe(dev, drv);
……
}
进入really_probe进行绑定
static int really_probe(struct device *dev, struct device_driver *drv)
{
……..
if (dev->bus->probe) { //dev->bus 为全局变量 platform_bus_type,里面的probe没有定义,为NULL
ret = dev->bus->probe(dev);
if (ret)
goto probe_failed;
} else if (drv->probe) { // 所以,会执行设备驱动的probe函数 drv->probe,传入参数为平台设备。
ret = drv->probe(dev);
if (ret)
goto probe_failed;
}
……..
}
--------------------分析了上面这么长的函数调用关系,总结到最后一句话------------------------------
platform_driver_register 通过平台设备名和驱动程序名称的对应来寻找匹配关系,然后 调用 设备驱动程序自定义的 probe函数,进行各自的绑定操作。
如何将现有的驱动驱动融入platform框架中
以 globalfifo驱动挂载在platform总线上为例子,需要完成两个工作:
1. 将globalfifo驱动移植为platform驱动
2. 在板级文件中添加 globalfifo这个 platform设备。
为了完成 目标1,只需要在原始的globalfifo字符设备驱动中,套上一层platform_driver的外壳,注意,这一工作,并没有改变globalfifo是字符设备的本质,只是将其挂接在 platform总线上。
再具体来说,就是讲驱动的入口函数,由原先的globalfifo_init改为 globalfifo_probe ,出口函数由原先的globalfifo_exit改为 globalfifo_remove,在注册和注销中,将平台驱动结构体整个与系统关联起来就可以了。
为了完成目标2,需要在板级初始化上添加平台设备,定义好驱动程序即将使用的资源类型和数量,然后将平台设备结构体嵌入到板级平台设备结构体指针数组中去就可以了。
参考文献: