Linux驅動開發(十):設備樹下的platform平臺設備驅動

簡介

在今年五月份我在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的資源部分也有很多不太清除的點,所有說還需要繼續努力

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章