/* AUTHOR: Pinus
* Creat on : 2018-10-29
* KERNEL : linux-4.4.145
* BOARD : JZ2440(arm9 s3c2440)
* REFS : 韋東山視頻教程第二期
詳解Linux2.6內核中基於platform機制的驅動模型
*/
概述
分層即把硬件相關和相對穩定的東西給抽出來,並向上提供統一的接口,每一層專注於自己的事情,比如前文的輸入子系統。分離即把硬件相關和相對穩定的東西給分離開來,實際上即是bus-dev-drv模型(平臺總線、平臺設備、平臺驅動)。以最簡單的led驅動爲例,幾乎每個系統都會用到,操作LED的原理是相同的,只是具體LED的物理地址之類的不同,因此不防,把操作之類的放在drv中,把具體的硬件信息放在dev中,這樣只要修改Dev的信息就可以輕鬆移植了,無疑增加的程序的可移植行,節約開發時間,便於修改。
實際查看內核的一些驅動代碼,都是使用了platform bus而不是像前面我們那樣直接用file_operation。
1. 什麼是platform(平臺)總線?
相對於USB、PCI、I2C、SPI等物理總線來說,platform總線是一種虛擬、抽象出來的總線,實際中並不存在這樣的總線。那爲什麼需要platform總線呢?其實是Linux設備驅動模型爲了保持設備驅動的統一性而虛擬出來的總線。因爲對於usb設備、i2c設備、pci設備、spi設備等等,他們與cpu的通信都是直接掛在相應的總線下面與我們的cpu進行數據交互的,但是在我們的嵌入式系統當中,並不是所有的設備都能夠歸屬於這些常見的總線,在嵌入式系統裏面,SoC系統中集成的獨立的外設控制器、掛接在SoC內存空間的外設卻不依附與此類總線。所以Linux驅動模型爲了保持完整性,將這些設備掛在一條虛擬的總線上(platform總線),而不至於使得有些設備掛在總線上,另一些設備沒有掛在總線上。
platform總線相關代碼:driver\base\platform.c 文件
相關結構體定義:include\linux\platform_device.h 文件中
2、platform總線管理下的2員大將
(1)兩個結構體platform_device和platform_driver
對於任何一種Linux設備驅動模型下的總線都由兩個部分組成:描述設備相關的結構體和描述驅動相關的結構體
在platform總線下就是platform_device和platform_driver,下面是對兩個結構體的各個元素進行分析:
platform_device結構體:(include\linux\platform_device.h)
struct platform_device { // platform總線設備
const char * name; // 平臺設備的名字
int id; // ID 是用來區分如果設備名字相同的時候(通過在後面添加一個數字來代表不同的設備,因爲有時候有這種需求)
struct device dev; // 內置的device結構體
u32 num_resources; // 資源結構體數量
struct resource * resource; // 指向一個資源結構體數組
const struct platform_device_id *id_entry; // 用來進行與設備驅動匹配用的id_table表
char *driver_override; /* Driver name to force a match 強制匹配某個驅動 */
/* MFD cell pointer */
struct mfd_cell *mfd_cell;
/* arch specific additions */
struct pdev_archdata archdata; // 自留地 添加自己的東西
};
platform_device結構體中的struct resource結構體分析:
struct resource { // 資源結構體
resource_size_t start; // 資源的起始值,如果是地址,那麼是物理地址,不是虛擬地址
resource_size_t end; // 資源的結束值,如果是地址,那麼是物理地址,不是虛擬地址
const char *name; // 資源名
unsigned long flags; // 資源的標示,用來識別不同的資源
struct resource *parent, *sibling, *child; // 資源指針,可以構成鏈表
};
platform_driver結構體:(include\linux\platform_device.h)
struct platform_driver {
int (*probe)(struct platform_device *); // 這個probe函數其實和 device_driver中的是一樣的功能,但是一般是使用device_driver中的那個
int (*remove)(struct platform_device *); // 卸載平臺設備驅動的時候會調用這個函數,但是device_driver下面也有,具體調用的是誰這個就得分析了
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver; // 內置的device_driver 結構體
const struct platform_device_id *id_table; // 該設備驅動支持的設備的列表 他是通過這個指針去指向 platform_device_id 類型的數組
bool prevent_deferred_probe;
};
(2)兩組接口函數(driver\base\platform.c)
int platform_driver_register(struct platform_driver *); // 用來註冊我們的設備驅動
void platform_driver_unregister(struct platform_driver *); // 用來卸載我們的設備驅動
int platform_device_register(struct platform_device *); // 用來註冊我們的設備
void platform_device_unregister(struct platform_device *); // 用來卸載我們的設備
實驗
目的:實際體驗platform bus 驅動的編寫架構以LED爲例
驅動部分 led_drv.c
/* 分配、設置、註冊一個 platform_driver */
#include <linux/module.h>
#include <linux/version.h>
#include <linux/init.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/console.h>
#include <linux/err.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/device.h> //class_create
#include <linux/fs.h>
#include <asm/io.h>
#include <asm/irq.h>
#include <asm/uaccess.h>
static struct class *led_class;
static struct device *led_dev;
static unsigned int major;
static volatile unsigned long *gpio_con;
static volatile unsigned long *gpio_dat;
static int pin;
static int led_drv_open(struct inode *inode, struct file *file)
{
*gpio_con &= ~(0x3<<(pin*2)); //現將對應位清零
*gpio_con |= (0x1<<(pin*2)); // 01 輸出
return 0;
}
static ssize_t led_drv_write(struct file *file, const char *buf, size_t count, loff_t *pos)
{
char val;
copy_from_user(&val, buf, count); //將數據從用戶空間傳到內核空間 反之copy_to_user
printk("/dev/led: %d\n", val);
if(val==1) {
*gpio_dat &= ~(1<<pin);
}
else {
*gpio_dat |= (1<<pin);
}
return 0;
}
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_drv_open,
.write = led_drv_write,
};
static int led_probe(struct platform_device *pdev)
{
/* 根據device的資源進行 ioremap */
struct resource *res;
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;
/* 註冊字符設備驅動 */
major = register_chrdev(0, "myled", &led_fops);
led_class = class_create(THIS_MODULE, "myled"); // 創建一個類
led_dev = device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); // 創建設備節點"dev/led"
printk("led_probe : find led \n");
return 0;
}
static int led_remove(struct platform_device *pdev)
{
/* 卸載字符設備驅動 */
device_destroy(led_class, MKDEV(major, 0));
class_destroy(led_class);
unregister_chrdev(major, "myled");
/* iounmap */
iounmap(gpio_con);
printk("led_remove : remove led \n");
return 0;
}
static struct platform_driver led_drv = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "myled", //依靠名字來檢索,要和dev保持一致
},
};
static int __init led_drv_init(void)
{
return platform_driver_register(&led_drv);
}
static void __exit led_drv_exit(void)
{
platform_driver_unregister(&led_drv);
}
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");
原理分析
在過去我們在入口函數init裏都會進行初始化註冊設備之類,現在只調用一個函數
platform_driver_register(&led_drv);
其中
static struct platform_driver led_drv = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "myled", //依靠名字來檢索,要和dev保持一致
},
};
上面代碼中實際應用部分其實與以前一樣,也要有file_operation,在前面插了一步platform_register的註冊,下面就是分析一下內核的代碼,大概的流程,代碼太多不適合全放上來,跟着流程走
// 通過宏定義調用另一個程序
platform_driver_register(drv)
|
__platform_driver_register(drv, THIS_MODULE)
|
drv->driver.bus = &platform_bus_type; // 設置設備驅動 掛接在 platform平臺總線下
driver_register(&drv->driver); // 註冊設備驅動
// 其中platform_bus_type是一個bus_type 結構體,其中的匹配函數會在接下來分析時用到
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match, // 匹配驅動和設備
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
driver_register(&drv->driver);
|
other = driver_find(drv->name, drv->bus); // 這個函數其實進行了一個校驗 比對當前的 總線下是否存在名字和現在需要註冊的設備驅動的名字相同的設備驅動
bus_add_driver(drv); // 在總線掛接設備驅動 就是將設備驅動對應的kobj對象與組織建立關係
|
struct bus_type *bus; // 定義一個bus_type 結構體指針
struct driver_private *priv; // 定義一個 driver_private 指針
priv->driver = drv; // 使用 priv->driver 指向 drv
drv->p = priv; // 使用drv->p 指向 priv 這兩步見多了 ,跟之前分析的是一樣的意思 就是建立關係
priv->kobj.kset = bus->p->drivers_kset; // 設置設備驅動對象的父對象( 也就是指向一個 kset ) 父對象就是 /sys/bus/bus_type/drivers/ 這個目錄對應的對象
kobject_init_and_add(&priv->kobj, &driver_ktype, NULL, "%s", drv->name);// 添加kobject 對象到目錄層次中就能夠在 /sys/bus/bus_type/drivers/ 目錄中看到設備驅動對應的文件了
klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers); // 鏈表掛接:priv->knode_bus 掛接到 bus->p->klist_drivers 鏈表頭上去
driver_attach(drv); // 嘗試將驅動綁定到設備 也就是通過這個函數進行設備
module_add_driver(drv->owner, drv);
driver_create_file(drv, &driver_attr_uevent); // 建立屬性文件: uevent
driver_add_groups(drv, bus->drv_groups);
前面的的呢大致就是一些初始化,把當前驅動添加進平臺驅動鏈表啦,創建對應的系統文件之類,platform bus 最關鍵的當然還是如何把驅動和設備關聯起來
/**
* driver_attach - try to bind driver to devices.
嘗試將驅動和設備綁定
* @drv: driver.
*/
int driver_attach(struct device_driver *drv)
{
return bus_for_each_dev(drv->bus, NULL, drv, __driver_attach); // 這個函數的功能就是:依次去匹配bus總線下的各個設備
}
int bus_for_each_dev(struct bus_type *bus, struct device *start, void *data, int (*fn)(struct device *, void *))
{
struct klist_iter i; // 定義一個klist_iter 結構體變量 包含: struct klist 和 struct klist_node
struct device *dev;
int error = 0;
if (!bus || !bus->p)
return -EINVAL;
klist_iter_init_node(&bus->p->klist_devices, &i, (start ? &start->p->knode_bus : NULL)); //這個函數的功能就是將 klist_devices 和 knode_bus填充到 i 變量中
while ((dev = next_device(&i)) && !error) // 依次返回出總線上的各個設備結構體device
error = fn(dev, data); // 對於每一個設備和設備驅動都調用fn這個函數 直道成功 或者全部都匹配不上
klist_iter_exit(&i);
return error;
}
/*
* 作爲參數傳進去,進行驅動和設備匹配識別
*/
static int __driver_attach(struct device *dev, void *data)
{
struct device_driver *drv = data; // 定義一個device_driver 指針
/*
* Lock device and try to bind to it. We drop the error
* here and always return 0, because we need to keep trying
* to bind to devices and some drivers will return an error
* simply if it didn't support the device.
*
* driver_probe_device() will spit a warning if there
* is an error.
*/
if (!driver_match_device(drv, dev)) // 通過這個函數進行匹配 調用總線下的match 函數
return 0;
if (dev->parent) /* Needed for USB */
device_lock(dev->parent);
device_lock(dev);
if (!dev->driver)
driver_probe_device(drv, dev); // 如果匹配成功,調用probe函數
device_unlock(dev);
if (dev->parent)
device_unlock(dev->parent);
return 0;
}
這纔是真正的匹配函數,匹配傳入的驅動和設備是否匹配,可見調用的是總線結構體下的match函數
static inline int driver_match_device(struct device_driver *drv, struct device *dev)
{
return drv->bus->match ? drv->bus->match(dev, drv) : 1;
}
platform->match:就在上文中的platform_bus_type中引用
struct bus_type platform_bus_type = {
.match = platform_match, // 匹配驅動和設備
};
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);//獲得platform_device
struct platform_driver *pdrv = to_platform_driver(drv); //獲取 platform_driver
/* When driver_override is set, only bind to the matching driver 當設置driver_override時,只綁定到匹配驅動程序 */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* Attempt an OF style match first 嘗試一種風格匹配*/
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI style match 然後嘗試ACPI風格匹配 */
if (acpi_driver_match_device(dev, drv))
return 1;
/* Then try to match against the id table 如果id_table存在,就直接匹配id */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match 還沒有就直接匹配 pdev->name drv->name 名字是否形同*/
return (strcmp(pdev->name, drv->name) == 0);
}
通過上面函數,如果匹配成功那麼__driver_attach就會繼續向下執行,調用driver_probe_device函數
driver_probe_device
|
really_probe
|
dev->driver = drv; // 使用 dev->driver 指針去指向 drv 這就使得這兩者建立了一種關係
if (dev->bus->probe) { // 如果總線下的probe函數存在 則調用優先調用這個函數
ret = dev->bus->probe(dev);
} else if (drv->probe) { // 否則調用設備驅動中的probe函數
ret = drv->probe(dev); // 所以由此可知: 總線中的probe函數具有更高的優先級
}
綜上,如果經過驅動初始化過程中和設備經過匹配成功,最終會調用probe函數
設備部分 led_dev.c
#include <linux/module.h>
#include <linux/version.h>
#include <linux/init.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/console.h>
#include <linux/err.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <asm/io.h>
#include <asm/irq.h>
#include <asm/uaccess.h>
/* 分配、設置、註冊一個 platform_device */
static struct resource led_resources[] = {
[0] = {
.start = 0x56000050,
.end = 0x56000050 + 8 - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = 4,
.end = 4,
.flags = IORESOURCE_IRQ,
},
};
void led_release(struct device *dev)
{
// 沒什麼用,但必須存在
}
static struct platform_device myled_dev = {
.name = "myled",
.id = -1,
.num_resources = ARRAY_SIZE(led_resources),
.resource = led_resources,
.dev = {
.release = led_release,
},
};
static int __init led_dev_init(void)
{
return platform_device_register(&myled_dev);
}
static void __exit led_dev_exit(void)
{
platform_device_unregister(&myled_dev);
}
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");
原理分析
【流程】
platform_device_register(&myled_dev)
|
platform_device_add(pdev);
|
pdev->dev.bus = &platform_bus_type;
device_add(&pdev->dev); // 添加到設備鏈表裏
|
bus_probe_device(dev);
|
device_initial_probe
|
__device_attach(dev, true); // 則調用這個函數進行匹配
|
bus_for_each_drv(dev->bus, NULL, &data, __device_attach_driver); // 遍歷總線的鏈表匹配對應的設備驅動
可以看出,接下來的流程與驅動差不多了,最後也是匹配設備與驅動,如果成功那麼也會調用probe函數。
綜上,設備和驅動殊途同歸,其初始化時都是現將自己添加到對應的鏈表,然後和另一個鏈表進行匹配,如果成功那麼就會調用probe函數。而因爲內核中並沒有定義platform的總線probe,因此最終都會調用驅動中的probe。
static int led_probe(struct platform_device *pdev)
{
/* 根據device的資源進行 ioremap */
struct resource *res;
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;
/* 註冊字符設備驅動 */
major = register_chrdev(0, "myled", &led_fops);
led_class = class_create(THIS_MODULE, "myled"); // 創建一個類
led_dev = device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); // 創建設備節點"dev/led"
printk("led_probe : find led \n");
return 0;
}
可以看出在probe函數中我們進行了以往在init中的設備註冊,及一些初始化工作
注意到其中使用了platform_get_resource(pdev, IORESOURCE_MEM, 0);來獲取真正的物理資源,而這些資源都是定義在led_dev.c中的
static struct platform_device myled_dev = {
.name = "myled",
.id = -1,
.num_resources = ARRAY_SIZE(led_resources),
.resource = led_resources, // 資源
.dev = {
.release = led_release,
},
};
/* 分配、設置、註冊一個 platform_device */
static struct resource led_resources[] = {
[0] = {
.start = 0x56000050,
.end = 0x56000050 + 8 - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = 4,
.end = 4,
.flags = IORESOURCE_IRQ,
},
};
可以想象,如果我們想讓這個led驅動程序在另一板子上跑起來,只要根據板子更改led_resources[] 這一個地方就可以了,不用再整個代碼中到處找,可以預見這在其他複雜的驅動裏,能極大提高可以執行,縮短開發週期,避免錯誤。