目录
基本框架
/* 设备结构体 */
struct xxx_dev{
struct cdev cdev;
/* 设备结构体其他具体内容 */
};
struct xxx_dev xxxdev; /* 定义个设备结构体变量 */
static int xxx_open(struct inode *inode, struct file *filp)
{
/* 函数具体内容 */
return 0;
}
static ssize_t xxx_write(struct file *filp, const char __user *buf,size_t cnt, loff_t *offt)
{
/* 函数具体内容 */
return 0;
}
/*
* 字符设备驱动操作集
*/
static struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.open = xxx_open,
.write = xxx_write,
};
/*
* platform 驱动的 probe 函数
* 驱动与设备匹配成功以后此函数就会执行
*/
static int xxx_probe(struct platform_device *dev)
{
......
cdev_init(&xxxdev.cdev, &xxx_fops); /* 注册字符设备驱动 */
/* 函数具体内容 */
return 0;
}
static int xxx_remove(struct platform_device *dev)
{
......
cdev_del(&xxxdev.cdev);/* 删除 cdev */
/* 函数具体内容 */
return 0;
}
/* 匹配列表 */
static const struct of_device_id xxx_of_match[] = {
{ .compatible = "xxx-gpio" },
{ /* Sentinel */ }
};
/*
* platform 平台驱动结构体
*/
static struct platform_driver xxx_driver = {
.driver = {
.name = "xxx",
.of_match_table = xxx_of_match,
},
.probe = xxx_probe,
.remove = xxx_remove,
};
/* 驱动模块加载 */
static int __init xxxdriver_init(void)
{
return platform_driver_register(&xxx_driver);
}
/* 驱动模块卸载 */
static void __exit xxxdriver_exit(void)
{
platform_driver_unregister(&xxx_driver);
}
module_init(xxxdriver_init);
module_exit(xxxdriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Jason");
Linux系统的驱动框架主要就是三个主要部分组成,驱动、总线、设备。现在常见的嵌入式SOC已经不是单纯的CPU的概念了,它们都会在片上集成很多外设电路,这些外设都挂接在SOC内部的总线上,不同与IIC、SPI和USB等这一类实际存在外部PCB走线总线,他是系统内的总线实际是CPU的内部走线,所以Linux为了统一驱动模型在系统在启动引导时初始化了一条虚拟总线作为一个抽象的总线称之为platform总线,实现在drivers/base/platform.c中。今天就来学习这一类驱动的框架结构。
总线的具体实现
struct bus_type platform_bus_type = { .name = "platform", .dev_groups = platform_dev_groups,(属性) .match = platform_match, .uevent = platform_uevent, .pm = &platform_dev_pm_ops, };
结合前面的分析,Linux下的设备都应该(但不是必须)有所属的bus_type(dev.bus)这个bus_type就抽象了他们共通的一些“属性”和“方法”。platform设备他包含一个普通的device的基础上由增加了一些平台设备需要的数据如下
struct platform_device { const char *name; int id; bool id_auto; struct device dev; u32 num_resources; struct resource *resource; const struct platform_device_id *id_entry;/*记录和驱动的匹配表id_table中匹配的哪一个表项指针*/ /* MFD cell pointer */ struct mfd_cell *mfd_cell; /* arch specific additions */ struct pdev_archdata archdata; 这个参数一般都指向这个结构体实体本身地址 };
值得一提的是其中ID参数如果是-1则包含的设备名就是 platform_device .name的值,如果为-2则会自动分配platform设备ID具体是通过platform.c中的一个函数实现具体参考源码,否则其他参数则就按"%s.%d", pdev->name, pdev->id 格式格式化platform设备名。一般注册平台设备需要初始化的内容主要有name、 resource,有时还需要指定内涵dev的platform_data,这一部分数据常常被驱动使用,这也是Linux 驱动和设备分离的一部分体现。
platform_device
注册添加
这里只是简单罗列函数调用过程,这一部分实际上是Device注册的过程的一个封装,具体内部操作可以参考Linux设备注册。这一部分如果前面device的注册理解的比较透彻这一部分就很好理解了。
platform_device_register
1、device_initialize(&pdev->dev);
2、arch_setup_pdev_archdata(空函数)
3、platform_driver_add
1、pdev->dev.parent = &platform_bus;指定父设备为platform设备总线
2、pdev->dev.bus = &platform_bus_type;
3、设定设备名称三种情况(-1 -2(申请ID) other)
4、设备资源管理
5、调用device_add(pdev->dev)
添加过程
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; switch (pdev->id) { default: dev_set_name(&pdev->dev, "%s.%d", pdev->name, pdev->id); break; case PLATFORM_DEVID_NONE: dev_set_name(&pdev->dev, "%s", pdev->name); break; case PLATFORM_DEVID_AUTO: /* * 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); if (ret < 0) goto err_out; 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)); 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]; if (r->parent) release_resource(r); } err_out: return ret; }
处理过程是给设备指定父设备即依托的总系即platform_bus,指定bus这一步很关键涉及到后面的驱动匹配(因为设备添加过程会拿这个设备所属的总线总线上由注册的驱动list),然后就是根据ID的不同值以不同的策略初始化设备name字段。然后就是资源的保存添加,其中最关键的就是device_add的操作过程这一部分参考我的Linux设备,就可以知道设备添加的细节。
device卸载过程
这一部分内容也是上面的操作的一个逆向操作,其实核心的内容还是设备删除的操作同样可以参考上面给出的联接查看设备的注销过程,就能明白platform框架只是在原有的驱动和设备驱动模型上的更高一层的封装。所以这里还是简单的罗列一下调用过程。
platform_device_unregister
1、platform_device_del
1、释放platform_device id
2、device_del(pdev->dev)
2、platform_device_put
3、put_device
platform_driver
同理样platform_driver 也是一个包含了device_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; 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)
1、drv->driver.bus = platform_bus_type;
2、如果platform驱动中的xx_probe或xx_remove等为空则指定drv->driver.同名接口指针为platform驱动默认行为(仅支持acpi方式匹配)。
3、driver_register
__platform_driver_register
int __platform_driver_register(struct platform_driver *drv, struct module *owner) { drv->driver.owner = owner; 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_driver实际上也是对通用驱动的注册流程的一个高层级的封装,具体的驱动注册过程还是需要参考前面的驱动注册过程。
driver注册移除
移除过程同样很简单就是设备驱动的删除操作同上参考设备驱动的注销过程,这里也是仅仅简单的罗列API的调用层级和过程。
platform_device_unregister
1、platform_driver_unregister
1、driver_unregister
platform驱动和设备的匹配
无论上面的注册设备还是注册驱动,最后都是要调用总线类型的mach函数进行驱动和设备的匹配,这也是platform 驱动框架中比较重要核心的部分所以这里从源码分析一下。
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; /* 采用ACPI的方式匹配驱动和设备 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 通过驱动和设备的mach表来匹配驱动和设备 */ if (pdrv->id_table) return platform_match_id(pdrv->id_table, pdev) != NULL; /* 最后就是按驱动和设备名称是否相同来判断当前驱动是否支持这个设备 */ return (strcmp(pdev->name, drv->name) == 0); }
从这个函数我们可以知道platform的driver和device的匹配就是通过以上四种规则来进行匹配的,前两种方式暂时不深究学到再来看;除此之外这个函数还告诉我们一个内核机制
如果驱动指定了mach_id_table则驱动将放弃名称相同匹配机制这一点需要重点记住。具体这个mach函数是在何时调用的参考Linux device的分析。其中兼容ID的匹配表格式是
struct platform_device_id { char name[PLATFORM_NAME_SIZE]; kernel_ulong_t driver_data; };
具体的匹配规则也很简单就是使用ID表內的名称来和设备名比较具体看代码,比较简单,需要注意的是这里还将匹配的id 表的句柄保存在platform device的id_entry项上,id_table里常常带一个long型的driver_data数据保存驱动数据。
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 device 来匹配内核的一个三星的led驱动,内核代码是3-16-57版本驱动在drivers\leds\leds-s3c24xx.c。
static struct platform_driver s3c24xx_led_driver = { .probe = s3c24xx_led_probe, .remove = s3c24xx_led_remove, .driver = { .name = "s3c24xx_led", .owner = THIS_MODULE, }, };
主要分析其s3c24xx_led_probe函数的执行过程就能明白对应的设备应该如何添加。通过驱动的声明我得出结论,这个驱动除了设备树和ACPI的方式匹配设备外就只能通过名称来匹配设备了,所以先定义设备如下然后慢慢填充。
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;
}
static void __exit platform_led_exit(void)
{
int i;
for(i=0;i<ARRAY_SIZE(tiny210_device_led);i++){
platform_device_unregister(&tiny210_device_led[i]);
}
}
module_init(platform_led_init);
module_exit(platform_led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("smile@shanghai");