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