Linux 内核:设备驱动模型 平台设备驱动
介绍
Linux系统的驱动框架主要就是三个主要部分组成,驱动、总线、设备。
随着电子行业的发展,控制器越来越强大,SOC(片上系统)出现了:在片内的CPU外围集成很多外设电路,这些外设都挂接在SOC内部的总线上。
不同于IIC、SPI和USB等这一类实际存在外部PCB走线总线,片内外设从Chip之外是看不到的。
为了统一驱动架构抽象,所以Linux从2.6版本开始引入了platform bus这个虚拟的总线模型。
组成
platform平台设备驱动是基于设备总线驱动模型的,机制本身并不复杂,由两部分组成: platform_device
和 platfrom_driver
platform_device
:基于device
的封装platform_device_driver
:基于device_driver
的封装
整体的架构是这样子的。
前面已经分析过设备总线驱动模型,关于device 与 device_driver 的注册过程以及它们在sysfs文件系统中的层次关系就不在分析,本文重点分析platform平台设备驱动与设备总线驱动模型相比较新增添的那些东西。
platform设备
platform设备在device的基础上,增加了一些平台设备需要的数据。
platform_device原型
// include/linux/platform_device.h
struct platform_device {
/* 分配id的方式,决定了name的值 */
const char *name;
int id;
bool id_auto;
//真正的设备,通过 container_of ,就能找到整个platform_device ,访问其它成员,如后面要提到的 resource
struct device dev;
/*
num_resources、resource,该设备的资源描述,由struct resource(include/linux/ioport.h)结构抽象。
在Linux中, 系统资源包括I/O、Memory、Register、IRQ、DMA、Bus等多种类型。这些资源大多具有独占性,不允许多个设备同时使用,因此Linux内核提供了一些API,用于分配、管理这些资源。
当某个设备需要使用某些资源时,只需利用struct resource组织这些资源(如名称、类型、起始、结束地址等),并保存在该设备的resource指针中即可。然后在设备probe时,设备需求会调用资源管理接口,分配、使用这些资源。而内核的资源管理逻辑,可以判断这些资源是否已被使用、是否可被使用等等。
*/
u32 num_resources;
struct resource *resource;
/*记录和驱动的匹配表id_table中匹配的哪一个表项指针*/
const struct platform_device_id *id_entry;
/* MFD cell pointer */
struct mfd_cell *mfd_cell;
/* arch specific additions */
// 这个参数一般都指向这个结构体实体本身地址
struct pdev_archdata archdata;
};
一般注册平台设备需要初始化的内容主要有name、 resource;
有时还需要指定dev.platform_data
,这一部分数据常常被驱动使用,这也是Linux 驱动和设备分离的一部分体现。
// include/linux/device.h
/**
* struct device - The basic device structure
...
* @platform_data: Platform data specific to the device.
* Example: For devices on custom boards, as typical of embedded
* and SOC based hardware, Linux often uses platform_data to point
* to board-specific structures describing devices and how they
* are wired. That can include what ports are available, chip
* variants, which GPIO pins act in what additional roles, and so
* on. This shrinks the "Board Support Packages" (BSPs) and
* minimizes board-specific #ifdefs in drivers.
...
*/
struct device {
// ...
void *platform_data; /* Platform specific data, device
core doesn't touch it */
// ...
};
注册添加device
这里只是简单罗列函数调用过程,这一部分实际上是Device注册的过程的一个封装,具体内部操作可以参考Linux设备注册。
流程:
platform_device_register
device_initialize(&pdev->dev);
arch_setup_pdev_archdata(空函数)
platform_device_add
pdev->dev.parent = &platform_bus;指定父设备为platform设备总线
pdev->dev.bus = &platform_bus_type;
设定设备名称三种情况(-1 -2(申请ID) other)
设备资源管理
调用device_add(pdev->dev)
platform_device_register
// drivers/base/platform.c
/**
* platform_device_register - add a platform-level device
* @pdev: platform device we're adding
*/
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);
void __weak arch_setup_pdev_archdata(struct platform_device *pdev)
{
}
platform_device_add
/**
* platform_device_add - add a platform device to device hierarchy
* @pdev: platform device we're adding
*
* This is part 2 of platform_device_register(), though may be called
* separately _iff_ pdev was allocated by platform_device_alloc().
*/
int platform_device_add(struct platform_device *pdev)
{
int i, ret;
// ...
// 指定父设备 即 依托的总线 为 platform_bus
if (!pdev->dev.parent)
pdev->dev.parent = &platform_bus;
// 指定bus 涉及到后面的驱动匹配
// (因为设备添加过程会拿这个设备所属的总线总线上由注册的驱动list)
pdev->dev.bus = &platform_bus_type;
// 根据ID的不同值以不同的策略初始化设备name字段
switch (pdev->id) {
default: // 默认
dev_set_name(&pdev->dev, "%s.%d", pdev->name, pdev->id);
break;
case PLATFORM_DEVID_NONE: // platform_device.name 的 值
dev_set_name(&pdev->dev, "%s", pdev->name);
break;
case PLATFORM_DEVID_AUTO: // 自动分配platform设备ID
/*
* Automatically allocated device ID. We mark it as such so
* that we remember it must be freed, and we append a suffix
* to avoid namespace collision with explicit IDs.
*/
ret = ida_simple_get(&platform_devid_ida, 0, 0, GFP_KERNEL);
pdev->id = ret;
pdev->id_auto = true;
dev_set_name(&pdev->dev, "%s.%d.auto", 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) {
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)) {
dev_err(&pdev->dev, "failed to claim resource %d\n", i);
ret = -EBUSY;
goto failed;
}
}
pr_debug("Registering platform device '%s'. Parent at %s\n",
dev_name(&pdev->dev), dev_name(pdev->dev.parent));
// 最关键的就是device_add,把设备添加到总线上。
ret = device_add(&pdev->dev);
if (ret == 0)
return ret;
failed:
if (pdev->id_auto) {
ida_simple_remove(&platform_devid_ida, pdev->id);
pdev->id = PLATFORM_DEVID_AUTO;
}
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);
}
err_out:
return ret;
}
EXPORT_SYMBOL_GPL(platform_device_add);
device卸载过程
这一部分内容也是上面的操作的一个逆向操作,其实核心的内容还是设备删除的操作,同样可以参考上面给出的联接查看设备的注销过程,就能明白platform框架只是在原有的驱动和设备驱动模型上的更高一层的封装。所以这里还是简单的罗列一下调用过程。
platform_device_unregister
platform_device_del
释放platform_device id
device_del(pdev->dev)
platform_device_put
put_device
platform驱动
platform_driver原型
同理样platform_driver 也是一个包含了device_driver 的结构。
//include/linux/platform_device.h
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;
bool prevent_deferred_probe;
};
从结构体可以看出,平台设备驱动提供了一些操作接口和一个platform_device_id
类型的兼容性匹配表(后面分析)。
其次是结构体中的操作接口函数,其在内部的device_driver结构体内也是有一套类似的操作接口;其他的电源管理现在已经很少用平台设备驱动中的接口了,而是转而使用内涵的device_driver驱动中的dev_pm_ops结构体中的接口来实现。
注册添加driver
__platform_driver_register(drv, THIS_MODULE)
drv->driver.bus = platform_bus_type;
设置probe、remove、shutdown。
driver_register
/**
* __platform_driver_register - register a driver for platform-level devices
* @drv: platform driver structure
* @owner: owning module/driver
*/
int __platform_driver_register(struct platform_driver *drv,
struct module *owner)
{
drv->driver.owner = owner;
// 指定为 platform_bus_type,也是为了到时候的probe
drv->driver.bus = &platform_bus_type;
/*
如果platform驱动中的xx_probe或xx_remove等为空则指定drv->driver.
同名接口指针为platform驱动默认行为(仅支持acpi方式匹配)
*/
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);
}
EXPORT_SYMBOL_GPL(__platform_driver_register);
// include/linux/platform_device.h
/*
* use a macro to avoid include chaining to get THIS_MODULE
*/
#define platform_driver_register(drv) \
__platform_driver_register(drv, THIS_MODULE)
通过上面的代码我们很清楚的看到platform_driver实际上也是对通用驱动的注册流程的一个高层级的封装,具体的驱动注册过程参考之前的文章就可以了
driver注册移除
移除过程同样很简单就是设备驱动的删除操作同上参考设备驱动的注销过程,这里也是仅仅简单的罗列API的调用层级和过程。
platform_device_unregister
platform_driver_unregister
driver_unregister
platform总线
刚刚可能有人会好奇,为什么没有platform_bus
,实际上platform_bus_type
是一个bus_type
的实例;而不是基于bus_type
的封装。
因为在一般情况下,内核中已经初始化并挂载了一条platform总线在sysfs文件系统中。
platform_bus_type原型
// drivers/base/platform.c
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,// sysfs属性
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
EXPORT_SYMBOL_GPL(platform_bus_type);
结合前面的分析,Linux下的设备都应该(但不是必须)有所属的bus_type(dev.bus)这个bus_type就抽象了他们共通的一些“属性”和“方法”。
platform驱动和设备的匹配
无论上面的注册设备还是注册驱动,最后都是要调用总线类型的mach函数进行驱动和设备的匹配,这也是platform 驱动框架中比较重要核心的部分。
再看driver_match_device
还记得吗?无论是driver_register
还是device_register
,都会调用到driver_match_device
。例如driver_register
的调用过程:
driver_register(drv) [core.c]
bus_add_driver(drv) [bus.c]
if (drv->bus->p->drivers_autoprobe)
driver_attach(dev)[dd.c]
bus_for_each_dev(dev->bus, NULL, drv,__driver_attach)
__driver_attach(dev, drv) [dd.c]
driver_match_device(drv, dev) [base.h]
// 执行 bus->match
drv-bus->match ? drv->bus->match(dev, drv) : 1
if false, return;
driver_probe_device(drv, dev) [dd.c]
really_probe(dev, drv) [dd.c]
dev-driver = drv;
if (dev-bus->probe)
dev->bus->probe(dev);
else if (drv->probe)
drv-aprobe(dev);
probe_failed:
dev->-driver = NULL;
结合刚刚介绍的platform_bus_type
,我们知道,待会就会调用platform_match
进行设备与驱动的匹配。
// drivers/base/platform.c
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,// sysfs属性
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
EXPORT_SYMBOL_GPL(platform_bus_type);
platform_match
/**
* platform_match - bind platform device to platform driver.
* @dev: device.
* @drv: driver.
*
* Platform device IDs are assumed to be encoded like this:
* "<name><instance>", where <name> is a short description of the type of
* device, like "pci" or "floppy", and <instance> is the enumerated
* instance of the device, like '0' or '42'. Driver IDs are simply
* "<name>". So, extract the <name> from the platform_device structure,
* and compare it against the name of the driver. Return whether they match
* or not.
*/
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);
/* Attempt an OF style match first */
/* 采用设备树的兼容方式匹配驱动和设备 (略)*/
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI style match */
/* 采用ACPI的方式匹配驱动和设备 略*/
if (acpi_driver_match_device(dev, drv))
return 1;
/* Then try to match against the id table */
/* 通过驱动和设备的match表来匹配驱动和设备 */
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的driver和device的匹配就是通过四种规则来进行匹配的,按优先级划分:
- 通过设备树匹配
- 通过ACPI匹配
- 通过match表匹配
- 通过两者的名称来匹配
前两种方式暂时不深究学到再来看,通过名字匹配也比较简单。我们看看通过match表匹配是如何匹配的。
通过match表匹配
其中兼容ID的匹配表格式是:
// include/linux/mod_devicetable.h
#define PLATFORM_NAME_SIZE 20
struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
具体的匹配规则也很简单,就是使用ID表內的名称来和设备名比较。
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;
}
需要注意的是,这里还将匹配的id 表的句柄保存在platform device的id_entry项上,id_table里常常带一个long型的driver_data数据保存驱动数据。
显然,platform_match_id 的作用就是遍历整个 Id_table 数组,寻找是否有与 platform_device->name 同名的,如果有,则返回这个 Platform_device_id ,使用Id_table 打破了原本设备总线驱动模型,一个 device 只能用与一个 device_driver 配对的局限性。
现在一个platform_device_driver
可以与多个platform_device
配对。
具体实例分析
下面通过自己实现一个platform device 来匹配内核的一个三星的led驱动,内核代码是3-16-57版本驱动在drivers\leds\leds-s3c24xx.c。
static const struct platform_device_id s3c24xx_led_id[] = {
{ "s3c24xx_led", 0 },
{},
};
static struct platform_driver s3c24xx_led_driver = {
.probe = s3c24xx_led_probe,
.remove = s3c24xx_led_remove,
.driver = {
.name = "s3c24xx_led",
.owner = THIS_MODULE,
},
.id_table = s3c24xx_led_id,
};
主要分析其s3c24xx_led_probe函数的执行过程就能明白对应的设备应该如何添加。
通过驱动的声明我得出结论,这个驱动除了设备树和ACPI的方式匹配设备外,使用到了platform_device_id
来匹配设备,所以先定义设备如下然后慢慢填充。
static struct platform_device tiny210_device_led []= {
.name = "s3c24xx_led",
.id = 0,
};
然后在看s3c24xx_led_probe函数都是怎样处理的
static int s3c24xx_led_probe(struct platform_device *dev)
{
struct s3c24xx_led_platdata *pdata = dev_get_platdata(&dev->dev);
/* 首先获取 platform_data 这个我还没定义所以后面需要定义 */
struct s3c24xx_gpio_led *led;
int ret;
/* 申请驱动私有数据结构体 */
led = devm_kzalloc(&dev->dev, sizeof(struct s3c24xx_gpio_led),
GFP_KERNEL);
if (!led)
return -ENOMEM;
/* 将私有数据结构体绑定到device的driver_data成员上方便使用 */
platform_set_drvdata(dev, led);
/* 这里涉及LED class 子系统的内容 可以暂时当作黑盒 */
led->cdev.brightness_set = s3c24xx_led_set;
led->cdev.default_trigger = pdata->def_trigger;
led->cdev.name = pdata->name;
led->cdev.flags |= LED_CORE_SUSPENDRESUME;
/* 绑定platform_data 到私有数据结构 */
led->pdata = pdata;
ret = devm_gpio_request(&dev->dev, pdata->gpio, "S3C24XX_LED");
if (ret < 0)
return ret;
/* no point in having a pull-up if we are always driving */
/* GPIO 子系统内容 配置对应的GPIO */
s3c_gpio_setpull(pdata->gpio, S3C_GPIO_PULL_NONE);
/* 如果设备定义时指定了这个标志则会执行下面的设置将GPIO配置为输入方向 */
if (pdata->flags & S3C24XX_LEDF_TRISTATE)
/* GPIO 子系统内容 配置对应的GPIO方向为输入 一般底层由芯片厂商实现 */
gpio_direction_input(pdata->gpio);
else
/* 第二个参数是保证LED在默认状态下是不点亮的 */
gpio_direction_output(pdata->gpio,
pdata->flags & S3C24XX_LEDF_ACTLOW ? 1 : 0);
/* register our new led device */
/* 这里涉及LED class 子系统的内容 可以暂时当作黑盒 */
ret = led_classdev_register(&dev->dev, &led->cdev);
if (ret < 0)
dev_err(&dev->dev, "led_classdev_register failed\n");
return ret;
}
到此LED驱动的匹配操作就完了,除了中间涉及Linux 的led class 和 gpio 子系统的内容外还是很简单的所以接下来完善我的LED设备
增加驱动所需的数据
struct s3c24xx_led_platdata led_data
{
.gpio = S5PV210_GPJ2(0), //(gpio 子系统)
.flags = S3C24XX_LEDF_ACTLOW, //(驱动的私有标志,指明LED开启时的IO电平)
.name = "led",
.def_trigger = "",
};
static struct platform_device tiny210_device_led []= {
.name = "s3c24xx_led",
.id = 0,
.dev ={
.platform_data = &led_data,
},
};
flags 的内容是后来补上的他的意思就是led在低电平时点亮,不要这个标志LED默认状态是开启的这和具体的硬件有关。
最后将设备以模块的形式加入。
然后在/sys/class/leds/ 下将看到一个led0文件他是一个符合链接指向/devices/platform/latform/s3c24xx_led.0/leds/led0
进入 会看到
brightness max_brightness subsystem uevent
device power trigger
这些就是ledclass的内容的,通过向brightness写数据就可以控制LED的开启和关闭了。也可以直接使用脚本
echo 0 > brightness led灯就亮了
echo 1 >brightness led灯就灭了。
综上Linux下的platform总线出现的意义就是统一linux下的驱动模型,即设备、驱动、总线其中总线负责设备和驱动的匹配。一般Linux下的复杂驱动都是基于platform总线的
通过驱动的probe函数进行调用其他子系统的接口实习更加复杂的驱动模型。而platform_driver和platform_device都是在Linux device和device_driver之上封装的所以需要在明白
Linux device和device_driver 的相关机制之上来理解就更加容易了。
附设备添加源码,不过源码后来我又添加了三个LED。
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/fb.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <asm/mach/arch.h>
#include <asm/mach/map.h>
#include <asm/setup.h>
#include <asm/mach-types.h>
#include <mach/map.h>
#include <plat/gpio-cfg.h>
#include <plat/devs.h>
#include <plat/cpu.h>
#include <linux/platform_data/leds-s3c24xx.h>
static struct s3c24xx_led_platdata led_data[] = {
[0]={
.gpio = S5PV210_GPJ2(0),
.flags = S3C24XX_LEDF_ACTLOW,
.name = "led0",
.def_trigger = "",
},
[1]={
.gpio = S5PV210_GPJ2(1),
.name = "led1",
.flags = S3C24XX_LEDF_ACTLOW,
.def_trigger = "",
},
[2]={
.gpio = S5PV210_GPJ2(2),
.name = "led2",
.flags = S3C24XX_LEDF_ACTLOW,
.def_trigger = "",
},
[3]={
.gpio = S5PV210_GPJ2(3),
.name = "led3",
.flags = S3C24XX_LEDF_ACTLOW,
.def_trigger = "",
},
};
static struct platform_device tiny210_device_led []= {
[0]={
.name = "s3c24xx_led",
.id = 0,
.dev ={
.platform_data = &led_data[0],
.devt = MAJOR(22),
},
},
[1]={
.name = "s3c24xx_led",
.id = 1,
.dev ={
.platform_data = &led_data[1],
.devt = MAJOR(22),
},
},
[2]={
.name = "s3c24xx_led",
.id = 2,
.dev ={
.platform_data = &led_data[2],
.devt = MAJOR(22),
},
},
[3]={
.name = "s3c24xx_led",
.id = 3,
.dev ={
.platform_data = &led_data[3],
.devt = MAJOR(22),
},
}
};
static int __init platform_led_init(void)
{
int i;
for(i=0;i<ARRAY_SIZE(tiny210_device_led);i++){
if(platform_device_register(&tiny210_device_led[i])<0)
{
printk(KERN_ERR "tiny210_device_led %d Fail\n",i);
return -1;
}
}
printk(KERN_INFO "tiny210_device_led Succse\n");
return 0;
}