這裏寫自定義目錄標題
簡介
在今年五月份我在4412上學習了platform總線設備的驅動編寫,瞭解了引入platform總線的目的以及帶來的方便之處,初步瞭解了Linux內核總線、設備、驅動的總體框架,最近我開始了基於設備樹的驅動開發的學習,所以總結記錄一下在如何使用設備樹來編寫platform設備驅動,再溫習一下關於platform的知識。
關於platform
Linux系統要考慮驅動的可重用性,驅動的分離和分層
所以引入了platform設備驅動,平臺設備驅動
驅動框架
驅動的分隔,將主機驅動和設備驅動分隔開,I2C、SPI等都會採用驅動分隔方式
實際驅動開發中,I2C主機驅動由半導體廠家編寫好,設備驅動也有設備器件廠家編寫好,我們只需要提供設備信息即可,比如連接到了哪個I2C接口、I2C的速率是多少
將設備信息從設備驅動中剝離開來,驅動使用標準方法去獲取到設備信息(比如設備樹),然後根據獲取到的信息來初始化設備
bus、driver、device
總線、驅動、設備是十分關鍵的三個概念
- 當向系統註冊一個驅動時總線會在右邊的設備中尋找,如果有與之匹配的設備就將兩者聯繫起來
- 註冊一個設備時總線會左側的驅動中尋找並聯系,如果有與之匹配的驅動就將兩者聯繫起來
所以說總線的作用就是將驅動和設備匹配起來,作爲兩者中間的一個媒介
驅動的分層
在編寫Linux驅動時,需要編寫file_operations成員函數,還要考慮阻塞、非阻塞、多路複用、SIGIO等複雜的問題。但是當我們面對一個真實的硬件驅動時,總是懶惰的,想做盡可能少的事情,比如按鍵的驅動,我們只想收穫一個按鍵中斷、彙報一個鍵值,所以Linux內核通過驅動分層的方式來幫助我們“偷懶”,儘管file_operations、IO模型是不可或缺的,但是這部分的代碼所有的按鍵都是一樣的、甚至可以說所有的輸入設備都是一樣,所以Linux內核提供了input子系統來幫助我們解決輸入相關的事項,相當於實現了一箇中間件,把那些重複的事情搞定,而我們只要配置底層的與IO相關的,和調用它的上報函數。
同樣的Linux內核不僅僅只有input子系統,與LCD顯示相關的部分抽象出了Framebuffer子系統專門來管理所有與顯示有關的硬件和軟件;與RTC相關的部分抽象出了RTC子系統等等
platform驅動模型
platform總線
首先我們來看一下bus_type 結構體,這個結構體用於描述總線
struct bus_type {
const char *name;
const char *dev_name;
struct device *dev_root;
const struct attribute_group **bus_groups;
const struct attribute_group **dev_groups;
const struct attribute_group **drv_groups;
int (*match)(struct device *dev, struct device_driver *drv);
int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*online)(struct device *dev);
int (*offline)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
int (*num_vf)(struct device *dev);
const struct dev_pm_ops *pm;
const struct iommu_ops *iommu_ops;
struct subsys_private *p;
struct lock_class_key lock_key;
};
match函數就是完成設備和驅動之間的匹配的
就是完成設備和驅動之間匹配的,總線就是使用 match函數來根據註冊的設備來查找對應的驅動,或者根據註冊的驅動來查找相應的設備,因此每一條總線都必須實現此函數。 match函數有兩個參數: dev和 drv,這兩個參數分別爲 device和 device_driver類型,也就是設備和驅動。
platform總線是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,
};
既然總線的作用是匹配驅動和設備,所以我們要重點關注platform_match這個函數
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/* When driver_override is set, only bind to the matching driver */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))//第一種匹配方式,OF類型匹配,設備樹採用的匹配方式
return 1;
/* Then try ACPI style match */
if (acpi_driver_match_device(dev, drv))//第二種匹配方式,ACPI匹配
return 1;
/* Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;//第三種匹配方式,id_table匹配
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);//第四種匹配方式,直接比較驅動和設備的name字段
}
可以看到,platform_match可以使用四種匹配方式
- OF類型匹配,設備樹採用的匹配方式
- ACPI匹配
- id_table匹配
- 直接比較驅動和設備的name字段
platform驅動
首先我們來看一下最基本的platform_driver結構體
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 (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
這個我們是很熟悉的
probe函數,驅動和設備匹配成功以後會執行
driver成員,面向對象,device_driver爲基類,platform_driver集成了這個基類並添加了一些特有的成員變量
id_table表,數組,每個元素爲platform_device_id
struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
我們可以看到在platform_driver 中包含了一個driver對象,它是device_driver 類型的,用於描述設備驅動,所以可以看做是platform_driver 的基類(Linux內核中有很多用到面向對象的思想),我們再來看一下device_driver 結構體
struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const char *mod_name; /* used for built-in modules */
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
enum probe_type probe_type;
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;
const struct dev_pm_ops *pm;
struct driver_private *p;
};
of_device_id類型的of_match_table就是採用設備樹的時候驅動使用的匹配表
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};
compatible非常重要,對於設備樹而言,就是通過設備節點的compatiable屬性值和of_match_table中每個項目的compatible成員變量進行比較,如果有相等的就表示設備和此驅動匹配成功
可以看出在device_driver 結構體中也有probe、remove等函數,而在platform_driver 結構體中相當於是重寫了這些方法,對這些函數進行了新的定義,如果子類中的方法與父類中的方法具有相同的方法名、返回列表和參數表,將會覆蓋原有的方法,這是面向對象的“多態”設計思想,極大的提高了代碼的可重用能力
在編寫 platform驅動的時候,首先定義一個 platform_driver結構體變量,然後實現結構體中的各個成員變量,重點 是實現匹配方法以及 probe函數。當驅動和設備匹配成功以後 probe函數就會執行,具體的驅動程序在 probe函數裏面編寫,比如字符設備驅動等等。
platform設備
有了driver我們還需要platform_device
struct platform_device {
const char *name;
int id;
bool id_auto;
struct device dev;
u32 num_resources;
struct resource *resource;
const struct platform_device_id *id_entry;
char *driver_override; /* Driver name to force a match */
/* MFD cell pointer */
struct mfd_cell *mfd_cell;
/* arch specific additions */
struct pdev_archdata archdata;
};
name 設備名字,要和所使用的platform驅動的name字段相同
num_resources 資源數量,一般爲resource的大小
resource 表示資源,也就是設備信息,比如外設寄存器等,struct resource表示資源
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
start 和 end表示資源的起始和停止信息,對於內存類的資源就表示內存起始和終止地址
name表示資源名字
flags表示資源類型
可選擇的資源類型都定義在ioport.h
引入設備樹後的變化
在以前不支持設備樹的Linux版本中,用戶需要編寫platform_device變量來描述設備信息(一般在平臺文件中寫),然後使用 platform_device_register將驅動註冊到內核中
不再使用時可以通過platform_device_unregister註銷掉對應的platform設備
#ifdef CONFIG_HELLO_CTL
struct platform_device s3c_device_hello_ctl={
.name = "hello_ctl",
.id = -1,
};
#endif
引入設備說以後我們就不用在定義設備了,只需要在設備樹中添加一個節點,如下
gpioled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gpio_leds>;
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
我們添加了一個gpioled 節點,我們要注意它的compatible 屬性"atkalpha-gpioled",在驅動中我們要設置匹配表
static const struct of_device_id led_of_match[] = {
{ .compatible = "atkalpha-gpioled"},
{ /**/ }
};
我們將匹配表設置只有一項設備,就是我們在設備樹中定義的節點
然後定義platform_driver,將匹配表初始化到platform_driver
static struct platform_driver led_driver = {
.driver = {
.name = "im6ul-led",
.of_match_table = led_of_match,
},
.probe = led_probe,
.remove = led_remove,
};
這樣當驅動加載到內核後,就會進行匹配,匹配成功的話纔會執行probe函數,在probe函數中做設備、驅動的初始化。
實驗代碼與分析
實驗代碼
#include <linux/init.h>
#include <linux/ide.h>
#include <linux/delay.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
#include <linux/stat.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/gpio.h>
#include <linux/errno.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/platform_device.h>
#include <asm/io.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#define LEDDEV_CNT 1
#define LEDDEV_NAME "platformled"
#define LEDOFF 0
#define LEDON 1
//設備結構體
struct platled_dev
{
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
int minor;
struct device_node *nd;
int led0;
};
struct platled_dev leddev;//leddev設備
//使用gpio操作開關LED
void led0_switch(u8 sta)
{
if(sta == LEDON)
{
gpio_set_value(leddev.led0, 0);
}
else if(sta == LEDOFF)
{
gpio_set_value(leddev.led0, 1);
}
}
//file_operations的open函數
static int led_open(struct inode *inode,struct file *filp)
{
printk(KERN_EMERG "led_open enter!\n");
filp->private_data = &leddev;
return 0;
}
//file_operations的write函數
static ssize_t led_write(struct file *filp,const char __user *buf,size_t cnt,loff_t *offt)
{
int retvalue;
unsigned char databuf[2];
unsigned char ledstat;
printk(KERN_EMERG "led_write enter!\n");
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0)
{
printk(KERN_EMERG "Kernel write failed!\n");
return -EFAULT;
}
ledstat = databuf[0];
if(ledstat == LEDON)
{
led0_switch(LEDON);
}
else if(ledstat == LEDOFF)
{
led0_switch(LEDOFF);
}
return 0;
}
//file_operations結構體
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
};
//probe函數
static int led_probe(struct platform_device *dev)
{
/*1.create device ID */
if(leddev.major)
{
leddev.devid = MKDEV(leddev.major, 0);
register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
}
else
{
alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);
leddev.major = MAJOR(leddev.devid);
leddev.minor = MINOR(leddev.devid);
}
printk(KERN_EMERG "nemchrdev major=%d,minor=%d \r\n", leddev.major, leddev.minor);
/*2.Init cdev */
leddev.cdev.owner = THIS_MODULE;
cdev_init(&leddev.cdev, &led_fops);
/*3.add cdev*/
cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);
/*4.create class*/
leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
if(IS_ERR(leddev.class))
{
printk(KERN_EMERG "class error!\n");
return PTR_ERR(leddev.class);
}
/*5.create device*/
leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
if(IS_ERR(leddev.device))
{
printk(KERN_EMERG "device error!\n");
return PTR_ERR(leddev.device);
}
leddev.nd = of_find_node_by_path("/gpioled");
if(leddev.nd == NULL)
{
printk(KERN_EMERG "gpioled node can't find!\n");
return -EINVAL;
}
leddev.led0 = of_get_named_gpio(leddev.nd, "led-gpio", 0);
if(leddev.led0 < 0)
{
printk(KERN_EMERG "can't get led-gpio!\n");
return -EINVAL;
}
gpio_request(leddev.led0, "led0");
gpio_direction_output(leddev.led0, 1);
return 0;
}
//remove函數
static int led_remove(struct *dev)
{
gpio_set_value(leddev.led0, 1);
/*Uninstall divece*/
cdev_del(&leddev.cdev);//delete cdev
unregister_chrdev_region(leddev.devid,LEDDEV_CNT);
device_destroy(leddev.class,leddev.devid);
class_destroy(leddev.class);
return 0;
}
//設備匹配表
static const struct of_device_id led_of_match[] = {
{ .compatible = "atkalpha-gpioled"},
{ /**/ }
};
//platform_driver
static struct platform_driver led_driver = {
.driver = {
.name = "im6ul-led",
.of_match_table = led_of_match,
},
.probe = led_probe,
.remove = led_remove,
};
static int __init leddriver_init(void)
{
return platform_driver_register(&led_driver);
}
static void __exit leddriver_exit(void)
{
platform_driver_unregister(&led_driver);
}
module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("GYY");
代碼分析
- 在init函數中調用platform_driver_register,傳入的參數爲一個platform_driver類型的參數,對該參數要進行定義,定義的包括.driver .probe .remove
- 在probe函數中完成設備的註冊及初始化,IO口的註冊及初始化
- 在exit函數中調用platform_driver_unregister,傳入的參數同register函數
- 在probe函數中所做的操作就是以前在init函數中做的操作
關於在probe函數中的操作我們進行以下分析
- 首先調用alloc_chrdev_region創建device_id
- 使用cdev_init初始化cdev結構體,將file_operations結構體寫到cdev中
- 調用cdev_add註冊cdev結構體
- 調用class_create創建一個class
- 調用device_create創建一個字符設備
- 接下來使用of_find_node_by_path獲取設備樹節點,使用of_get_named_gpio獲取GPIO
- 調用gpio_request請求GPIO並使用gpio_direction_output將GPIO初始化爲輸出模式
總結
platform是一種虛擬的總線,主要用於那些沒有實體總線的設備,要注意platform_device並不是與字符設備、塊設備、和網絡設備並列的概念,而是Linux系統提供的一種附加手段,我們通常會把SOC內部集成的I2C、RTC、LCD、看門狗等外設都歸納爲platform_device,而它們本質上都是字符設備。
再一次看platform,確實有了許多新的感受,在4412迷迷糊糊的驅動、設備、總線也有了些基本的認識,但還是有很多可以深入的點的,比如如何選擇驅動設備的匹配方式,如果從設備樹中讀取compatible並進行匹配,關於platform_device的資源部分也有很多不太清除的點,所有說還需要繼續努力