1. 程式人生 > >十一、Linux驅動之platform匯流排裝置驅動

十一、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操作。對於那些不需要熱拔插的裝置來說,這種方式是最省事的了。