1. 基本概念
从Linux2.6开始Linux加入了一套驱动管理和注册机制—platform平台总线驱动模型。platform平台总线是一条虚拟总线,platform_device为相应的设备,platform_driver为相应的驱动。与传统的bus/device/driver机制相比,platform机制将设备本身的资源注册进内核,由内核统一管理,在驱动程序使用这些资源时使用统一的接口,这样提高了程序的可移植性。所谓的platform_device并不是与字符设备、块设备和网络设备并列的概念,而是Linux系统提供的一种附加手段。Linux总线设备驱动模型的框架如下图所示:
简单来说总线bus,驱动driver,设备device这三者之间的关系就是:驱动开发者可以通过总线bus来将驱动driver和设备device进行隔离,这样的好处就是开发者可以将相对稳定不变的驱动driver独立起来,可以通过总线bus来桥接与之匹配的设备device。设备device只需要提供与硬件相关的底层硬件的配置,如io,中断等(也就是分离思想,而输入子系统是分层思想)。
platform.c提供了一个平台总线platform_bus,和注册平台设备platform_device和平台驱动platform_driver的相关接口,其中平台总线platform_bus已经编进内核,开发者只需要提供平台设备platform_device和平台驱动platform_driver的相关代码就行了。
2. 深入分析
2.1 platform模块初始化
platform模块的初始化是由platform_bus_init函数完成的。该函数在内核启动阶段被调用,我们来简单看下调用过程:
start_kernel() -> rest_init() ->kernel_init() -> do_basic_setup() -> driver_init() -> platform_bus_init()
(注:kernel_init()是在rest_init函数中创建内核线程来执行的)
platform_bus_init()定义如下(drivers/base/platform.c):
int __init platform_bus_init(void)
{
int error;
error = device_register(&platform_bus);
if (error)
return error;
error = bus_register(&platform_bus_type); //总线注册
if (error)
device_unregister(&platform_bus);
return error;
}
我们重点关注bus_register总线注册函数,传入的参数定义如下(drivers/base/platform.c):
struct bus_type platform_bus_type = {
.name = "platform", //设备名称
.dev_attrs = platform_dev_attrs, //设备属性、含获取sys文件名,该总线会放在/sys/bus下
.match = platform_match, //匹配设备和驱动,匹配成功就调用driver的.probe函数
.uevent = platform_uevent, //消息传递,比如热插拔操作
.suspend = platform_suspend, //电源管理的低功耗挂起
.suspend_late = platform_suspend_late,
.resume_early = platform_resume_early,
.resume = platform_resume, //恢复
};
其中的.match是设备和驱动的匹配函数。定义如下(drivers/base/platform.c):
static int platform_match(struct device * dev, struct device_driver * drv)
{
struct platform_device *pdev = container_of(dev, struct platform_device, dev);
return (strncmp(pdev->name, drv->name, BUS_ID_SIZE) == 0); //通过设备和驱动的成员name进行比较,相同则匹配成功
}
bus_register函数调用后,就会在用户空间生成platform相关文件(拓扑结构),在开发板上执行如下命令:
sys/bus/platform/devices里用来存放的是platform设备,/sys/bus/platform/drivers里用来存放的是platform驱动。从这里我们可以看出,一个完整的platform设备驱动包含两大部分:
(1) 注册平台设备platform_device(platform_device_register函数)
(2) 注册平台驱动platform_driver(platform_driver_register函数)
接下来围绕这两大函数深入分析platform设备驱动。
2.2 注册platform平台设备
2.2.1 函数原型
注册与卸载platform平台设备函数原型如下(drivers/base/platform.c):
int platform_device_register(struct platform_device * pdev) //注册platform设备
void platform_device_unregister(struct platform_device * pdev) //卸载platform设备
2.2.2 相关数据结构
传入的数据结构platform_device定义如下(include/linux/platform_device.h):
struct platform_device {
const char * name; //设备的名字,这将代替device->dev_id,用作sys/device下显示的目录名
u32 id; //设备id,用于给插入给该总线并且具有相同name的设备编号,如果只有一个设备的话填-1
struct device dev; //device结构
u32 num_resources; //资源的数目
struct resource * resource; //资源
};
参数含义:
name:设备的名字,这将代替device->dev_id,用作sys/device下显示的目录名。
id:设备id,用于给插入给该总线并且具有相同name的设备编号,如果只有一个设备的话填-1。
dev:device结构。
num_resources:资源的数目。
resource:资源,该设备的资源描述,由struct resource(include/linux/ioport.h)结构抽象。
其中struct resource结构也是我们platform平台设备的重点,用于存放设备的资源信息,如IO地址、中断号等。定义如下:
struct resource {
resource_size_t start; //资源的起始地址
resource_size_t end; //资源的结束地址
const char *name;
unsigned long flags; //资源的类型
struct resource *parent, *sibling, *child;
};
2.2.3 函数调用过程
platform_device_register //注册platform设备
platform_device_add //添加platform设备
device_add //添加设备,将platform_device结构信息存到device结构里
bus_add_device //添加设备到platform总线上的dev链表
bus_attach_device //设备在platform总线上进行匹配
device_attach
bus_for_each_drv //依次匹配platform总线上的drv链表成员
__device_attach
driver_probe_device
drv->bus->match(dev, drv) //调用platform_bus_type的.match函数进行匹配
add_dev
2.3 注册platform平台驱动
2.3.1 函数原型
注册与卸载platform平台驱动函数原型如下(drivers/base/platform.c):
int platform_driver_register(struct platform_driver *drv) //注册platform驱动
void platform_driver_unregister(struct platform_driver *drv) //卸载platform驱动
2.3.2 相关数据结构
传入的数据结构platform_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 (*suspend_late)(struct platform_device *, pm_message_t state);
int (*resume_early)(struct platform_device *);
int (*resume)(struct platform_device *);
struct device_driver driver;
};
struct platform_driver结构和struct device_driver非常类似,提供probe、remove、suspend、resume等回调函数。
2.3.3 函数调用过程
platform_driver_register //注册platform驱动
driver_register
bus_add_driver //添加驱动到platform总线上drv链表
driver_attach
bus_for_each_dev //依次匹配platform总线上的dev链表成员
__driver_attach
driver_probe_device
drv->bus->match(dev, drv) //调用platform_bus_type的.match函数进行匹配
module_add_driver
2.4 总结
当调用platform_device_register(或platform_driver_register)注册platform_device(或platform_driver)时,首先会将其加入platform总线上,依次匹配platform总线上的platform_driver(或platform_device),实现原理是通过设备和驱动的成员.name进行比较,相同则匹配成功,然后调用platform_driver的.probe函数。其中platform_device存放设备资源(硬件息息相关代码,易变动),platform_driver则使用资源(比较稳定的代码),这样当改动硬件资源时,我们的上层使用资源的代码部分几乎可以不用去改动。
3. 编写代码
接下来以实际例子来使用platform总线设备驱动。硬件相关部分请看二、Linux驱动之简单编写字符设备。
platform设备程序led_dev.c代码如下:
#include <linux/module.h>
#include <linux/version.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/interrupt.h>
#include <linux/list.h>
#include <linux/timer.h>
#include <linux/init.h>
#include <linux/serial_core.h>
#include <linux/platform_device.h>
/* 分配/设置/注册一个platform_device */
static struct resource led_resource[] = {
[0] = {
.start = 0x56000050,
.end = 0x56000050 + 8 - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = 5,
.end = 5,
.flags = IORESOURCE_IRQ,
}
};
static void led_release(struct device * dev)
{
}
static struct platform_device led_dev = {
.name = "myled", //platform_device的名字
.id = -1, //如果有相同名字的设备,则需要这个设备编号区分,只有该名字的设备就填-1
.num_resources = ARRAY_SIZE(led_resource),
.resource = led_resource, //platform_device的资源
.dev = {
.release = led_release,
},
};
static int led_dev_init(void)
{
platform_device_register(&led_dev); //注册platform_device
return 0;
}
static void led_dev_exit(void)
{
platform_device_unregister(&led_dev); //卸载
}
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_AUTHOR("LVZHENHAI");
MODULE_LICENSE("GPL");
platform驱动程序led_drv.c代码如下:
#include <linux/module.h>
#include <linux/version.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/cdev.h>
/* 分配/设置/注册一个platform_driver */
static int major;
static struct cdev led_cdev;
static struct class *cls;
static volatile unsigned long *gpio_con;
static volatile unsigned long *gpio_dat;
static int pin;
static int led_open(struct inode *inode, struct file *file)
{
/* 配置为输出 */
*gpio_con &= ~(0x3<<(pin*2));
*gpio_con |= (0x1<<(pin*2));
return 0;
}
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
int val;
int n;
n = copy_from_user(&val, buf, count);
if (val == 1)
{
// 点灯
*gpio_dat &= ~(1<<pin);
}
else
{
// 灭灯
*gpio_dat |= (1<<pin);
}
return 0;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = led_open,
.write = led_write,
};
static int led_probe(struct platform_device *pdev)
{
struct resource *res;
int result;
dev_t devid;
/* 根据platform_device的资源进行ioremap */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); //得到资源
gpio_con = ioremap(res->start, res->end - res->start + 1);
gpio_dat = gpio_con + 1;
res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
pin = res->start;
/* 注册字符设备驱动程序 */
printk("led_probe, found led\n");
devid = MKDEV(major, 0); //从主设备号major,次设备号0得到dev_t类型
if (major)
{
result=register_chrdev_region(devid, 1, "myled"); //注册字符设备
}
else
{
result=alloc_chrdev_region(&devid, 0, 1, "myled"); //注册字符设备
major = MAJOR(devid); //从dev_t类型得到主设备
}
if(result<0)
return result;
cdev_init(&led_cdev, &led_fops);
cdev_add(&led_cdev, devid, 1);
cls = class_create(THIS_MODULE, "myled");
class_device_create(cls, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
return 0;
}
static int led_remove(struct platform_device *pdev)
{
/* 卸载字符设备驱动程序 */
/* iounmap */
printk("led_remove, remove led\n");
class_device_destroy(cls, MKDEV(major, 0));
class_destroy(cls);
cdev_del(&led_cdev);
unregister_chrdev_region(MKDEV(major, 0), 1);
iounmap(gpio_con);
return 0;
}
struct platform_driver led_drv = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "myled", //必须与platform_device的.name相同
}
};
static int led_drv_init(void)
{
platform_driver_register(&led_drv); //注册platform_driver
return 0;
}
static void led_drv_exit(void)
{
platform_driver_unregister(&led_drv); //卸载
}
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_AUTHOR("LVZHENHAI");
MODULE_LICENSE("GPL");
应用程序led_test.c代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
/* led_test on
* led_test off
*/
int main(int argc, char **argv)
{
int fd;
int val = 1;
fd = open("/dev/led", O_RDWR);
if (fd < 0)
{
printf("can't open!\n");
}
if (argc != 2)
{
printf("Usage :\n");
printf("%s <on|off>\n", argv[0]);
return 0;
}
if (strcmp(argv[1], "on") == 0)
{
val = 1;
}
else
{
val = 0;
}
write(fd, &val, 4);
return 0;
}
Makefile代码如下:
KERN_DIR = /work/system/linux-2.6.22.6 //内核目录
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += led_drv.o
obj-m += led_dev.o
3. 测试
内核:linux-2.6.22.6
编译器:arm-linux-gcc-3.4.5
环境:ubuntu9.10
将led_dev.c、led_drv.c、led_text.c、Makefile三个文件放入网络文件系统内,在ubuntu该目录下执行:
make
arm-linux-gcc -o led_text led_text.c
在挂载了网络文件系统的开发板上进入相同目录,执行“ls”查看:
装载驱动:
insmod led_dev.ko
insmod led_drv.ko
输出信息(装载驱动时调换顺序也一样):
运行测试程序:
./led_test on或者./led_test off
就能看到led灯打开或者熄灭了。
4. 程序说明
当led_dev.ko先被装载时,调用platform_device_register函数注册platform_device,从platform总线的drv链表上查找,是否有.name成员相同的(“myled”)platform_driver被注册了,发现有则调用platform_driver的.probe函数,当led_drv.ko先被装载也是如此的过程。当其中之一被卸载时,都会调用platform_driver的.remove函数。
led_dev.c负责描述硬件资源, led_drv.c则使用资源,实现分离。
5. 相关知识点
5.1 Platform Device提供的API
/* include/linux/platform_device.h */
extern void arch_setup_pdev_archdata(struct platform_device *);
extern struct resource *platform_get_resource(struct platform_device *,unsigned int, unsigned int);
extern int platform_get_irq(struct platform_device *, unsigned int);
extern struct resource *platform_get_resource_byname(struct platform_device*,unsigned int,const char *);
extern int platform_get_irq_byname(struct platform_device *, const char *);
extern int platform_add_devices(struct platform_device **, int);
extern struct platform_device *platform_device_register_full(const struct platform_device_info *pdevinfo);
static inline struct platform_device *platform_device_register_resndata(struct device *parent, const char *name, int id,const struct resource *res, unsigned int num,const void *data, size_t size)
static inline struct platform_device *platform_device_register_simple(const char *name, int id,const struct resource *res, unsigned int num)
static inline struct platform_device *platform_device_register_data(struct device *parent, const char *name, int id,const void *data, size_t size)
extern struct platform_device *platform_device_alloc(const char *name, int id);
extern int platform_device_add_resources(struct platform_device *pdev,const struct resource *res,unsigned int num);
extern int platform_device_add_data(struct platform_device *pdev,const void *data, size_t size);
extern int platform_device_add(struct platform_device *pdev);
extern void platform_device_del(struct platform_device *pdev);
extern void platform_device_put(struct platform_device *pdev);
arch_setup_pdev_archdata,设置platform_device变量中的archdata指针。
platform_get_resource、platform_get_irq、platform_get_resource_byname、platform_get_irq_byname,通过这些接口,可以获取platform_device变量中的resource信息,以及直接获取IRQ的number等等。
platform_device_register_full、platform_device_register_resndata、platform_device_register_simple、platform_device_register_data,其它形式的设备注册。调用者只需要提供一些必要的信息,如name、ID、resource等,Platform模块就会自动分配一个struct platform_device变量,填充内容后,注册到内核中。
platform_device_alloc,以name和id为参数,动态分配一个struct platform_device变量。
platform_device_add_resources,向platform device中增加资源描述。
platform_device_add_data,向platform device中添加自定义的数据(保存在pdev->dev.platform_data指针中)。
platform_device_add、platform_device_del、platform_device_put,其它操作接口。
5.2 Platform Driver提供的API
extern int platform_driver_probe(struct platform_driver *driver,int (*probe)(struct platform_device *));
static inline void *platform_get_drvdata(const struct platform_device *pdev)
static inline void platform_set_drvdata(struct platform_device *pdev,void *data)
platform_driver_registe、platform_driver_unregister,platform driver的注册、注销接口。
platform_driver_probe,主动执行probe动作。
platform_set_drvdata、platform_get_drvdata,设置或者获取driver保存在device变量中的私有数据。
5.3 懒人API
extern struct platform_device *platform_create_bundle(struct platform_driver *driver, int (*probe)(struct platform_device *),struct resource *res, unsigned int n_res,const void *data, size_t size);
只要提供一个platform_driver(要把driver的probe接口显式的传入),并告知该设备占用的资源信息,platform模块就会帮忙分配资源,并执行probe操作。对于那些不需要热拔插的设备来说,这种方式是最省事的了。