十一、Linux驱动之platform总线设备驱动

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_resourceplatform_get_irqplatform_get_resource_bynameplatform_get_irq_byname,通过这些接口,可以获取platform_device变量中的resource信息,以及直接获取IRQnumber等等。
    platform_device_register_fullplatform_device_register_resndataplatform_device_register_simpleplatform_device_register_data,其它形式的设备注册。调用者只需要提供一些必要的信息,如name、ID、resource等,Platform模块就会自动分配一个struct platform_device变量,填充内容后,注册到内核中。
    platform_device_alloc,以nameid为参数,动态分配一个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(要把driverprobe接口显式的传入),并告知该设备占用的资源信息,platform模块就会帮忙分配资源,并执行probe操作。对于那些不需要热拔插的设备来说,这种方式是最省事的了。

猜你喜欢

转载自blog.csdn.net/qq_36576792/article/details/84550047