1. 程式人生 > >基於platform匯流排的驅動分析

基於platform匯流排的驅動分析

1.platform匯流排基本概念

  • 裝置和驅動若基於裝置驅動模型,則它們通常都需要掛接在一種總線上。匯流排相對於裝置和驅動,可謂是“媒人”擔當
  • 對於本身依附於 USB、 I2C、SPI 等的裝置而言,這自然不是問題。但是很多的裝置(比如led)實際並不依附於匯流排,沒了“媒人”,裝置和驅動怎麼產生聯絡呢?於是核心為這些可憐的裝置發明了一種虛擬的匯流排——platform(平臺匯流排)
  • 掛接在platform上的裝置和驅動,就稱之為platform_device,和platform_driver

2.platform匯流排驅動工作流程

這裡寫圖片描述

  • 提供並註冊platform_device/裝置節點
  • 提供並註冊platform_driver
  • 當platform匯流排內的mach函式會不停的匹配driver和device(老核心是根據driver內的id、name元素;新核心是根據of_match_table中的compatible)
  • 一旦匹配成功,則呼叫driver的probe(探測)函式開始正式執行驅動程式碼

3.platform匯流排驅動的獨立性和適應性

一個platform匯流排驅動程式可以對應多個裝置,並且裝置的變化也不會影響驅動。這是如何實現的呢?

  • 簡單的說,這是一種類似傳參的機制。裝置將底層資訊(比如暫存器資訊、使用到的中斷號、裝置名稱等)傳遞給驅動,驅動本身程式碼不用變,只需要根據引數操作底層,便可適應裝置的變化
  • 現代驅動設計理念就是演算法和資料分離,驅動原始碼中不攜帶資料,只負責演算法(對硬體的操作方法),這樣最大程度保持驅動的獨立性和適應性
  • 具體的實現方法是:老核心中,platform_device包含了一個device結構體,其內部有一個 void *platform_data; 這個有點類似於給使用者提供的自留地,使用者可以在裡面存放各種底層資訊。當driver的probe(探測)函式執行時,platform_device會作為引數傳進去,這樣驅動就能夠間接的得到這個 void *platform_data,從而據此操作硬體;新核心則直接在裝置節點屬性中存放資料,驅動通過API讀取節點裡的資料

4.老核心下platform匯流排驅動的編寫方法

下面,以led驅動為例項,分析怎麼使用platform來寫驅動
這裡寫圖片描述

  • 根據上圖的流程,首先應該進入mach-xxx.c完成platform裝置的註冊。
    • 第一步:建立適用於我們裝置的platform_data型別(為自留地設計一種格式)
    • 第二步:為一個具體裝置例項化一個platform_data,用來存放該類裝置的底層資訊
    • 第三步:建立一個具體platform裝置(例項化一個platform_device),並把各種資訊和platform_data填充入該裝置
    • 第四步:把platform裝置丟到專門存放platform_device的陣列中,開機時系統會註冊陣列中所有裝置
  • 先來看看mach-xxx.c中的情況,如果我們要寫新的platform_device,要注意mach-xxx.c內有沒有重複功能的。在該mach-xxx.c搜尋“platform”,尋找專門存放platform_device的陣列,發現裡面並沒有led,看來我們要自己從頭開始寫了
/*sjh_add*/

/*第一步:建立一個適用於我們裝置的platform_data型別*/
struct s5pv210_led_platdata {
    unsigned int         gpio;
    unsigned int         flags;
    char            *name;
    char            *def_trigger;
};

/*第二步:為一個具體裝置例項化一個platform_data*/
static struct s5pv210_led_platdata x210_led1_pdata = {
    .gpio       = S5PV210_GPJ0(3),
    .flags      = NULL,
    .name       = "led1",
    .def_trigger    = NULL,
};

/*第三步:例項化一個platform_device,正式建立裝置*/
static struct platform_device x210_led1 = {
    .name       = "s5pv210_led",//要和platform驅動中的名字對應
    .id     = 1,
    .dev        = {
    .platform_data  = &x210_led1_pdata,//底層資訊
    },
};

/* 第四步:把我們的platform_device新增進陣列,開機時系統會註冊陣列中所有裝置*/
static struct platform_device *smdkc110_devices[] __initdata = {
/*sjh_add*/
    &x210_led1,
#ifdef CONFIG_FIQ_DEBUGGER
    &s5pv210_device_fiqdbg_uart2,
#endif

...
/*該陣列很長,後面就不貼了*/
  • 然後我們還可以嘗試新增多個裝置,讓一個驅動對應多個led,我們只需重複第二、第三步即可。值得注意的是,驅動和裝置的匹配是靠name元素的,所以務必保證這幾個裝置的name元素都要和驅動的name元素相同,並且id不同就行了(如果name和id都一樣……那就分不清了……)
  • 接著開始驅動部分的編寫
/*自留地格式,提供給probe和release函式,讓它們可以解析platdata*/
extern struct s5pv210_led_platdata {
    unsigned int         gpio;
    unsigned int         flags;
    char            *name;
    char            *def_trigger;
};

static int s5pv210_led_probe(struct platform_device *pdev)
{
        /*匯入自留地格式s5pv210_led_platdata,這樣我們才能解析引數*/
        struct s5pv210_led_platdata *pdata = pdev->dev.platform_data;
        int ret = -1;

        /*這是一個例子,我們如何通過傳入的引數獲得led的gpio編號資訊*/
        gpio_num = pdata->gpio;

        /*各種註冊、初始化操作*/
        ...

        return 0;
}

/*解除安裝模組將觸發remove函式*/
static int s5pv210_led_remove(struct platform_device *pdev)
{
        struct s5pv210_led_platdata *pdata = pdev->dev.platform_data;

        /*各種登出操作*/
    return 0;
}

/*定義我們的platform_driver。注意name要和platform_device中相同*/
static struct platform_driver s5pv210_led_driver = {
    .probe      = s5pv210_led_probe,
    .remove     = s5pv210_led_remove,
    .driver     = {
        .name       = "s5pv210_led",
        .owner      = THIS_MODULE,
    },
};

/*模組與解除安裝載入函式,在裡面分別新增platform驅動的註冊和解除安裝函式*/
static int __init s5pv210_led_init(void)
{
        return platform_driver_register(&s5pv210_led_driver);
}

static void __exit s5pv210_led_exit(void)
{
        platform_driver_unregister(&s5pv210_led_driver);
}

module_init(s5pv210_led_init);
module_exit(s5pv210_led_exit);

//模組描述資訊
MODULE_LICENSE("GPL");
MODULE_AUTHOR("taurenking");
MODULE_DESCRIPTION("S5PV210 LED driver");
MODULE_ALIAS("S5PV210 LED driver");         
  • 驅動方面要注意的地方是傳參的問題,對於probe函式,傳進來的是指向platform_device型別例項的指標,而我們需要的底層資訊在platform_device中的dev內的platform_data中,前面的那幅圖可以很好的表明這個結構
  • 為了解析這個引數,還需要extern一個s5pv210_led_platdata到本檔案,它是我們led裝置的platform_data型別(因為這個型別是我們自己寫在mach-xxx.c中的,沒有定義在標頭檔案中,故編譯器找不到),讓編譯器知道格式,才能夠解析
  • 在解析引數前,先讓匯入自留地的格式struct s5pv210_led_platdata *pdata = pdev->dev.platform_data;,這樣編譯器才能知道格式,我們才能解析引數
  • 那麼其他硬體操作函式怎麼獲得底層資訊呢?它們又沒有platform_data這種引數,我們其實在檔案頭部定義幾個全域性變數就能解決,在probe函式種解析底層資訊,然後賦值給全域性變數,比如前面程式碼中的gpio_num = pdata->gpio;,硬體操作函式呼叫全域性變數即可獲得裝置的底層資訊,例如
static void s5pv210_led_set(struct led_classdev *led_cdev,
                enum led_brightness value)
{
        if (value == 0) {
            gpio_set_value(gpio_num, 1);
        }else{
            gpio_set_value(gpio_num, 0);
        }
}

5.老核心下廠商風格的platform匯流排驅動

有時,我們需要去分析一些由廠商實現的platform匯流排驅動,比如framebuffer。此類platform匯流排驅動本質上和我們之前分析的完全相同,只是在細節上略有區別罷了

  • 以framebuffer為例,在mach-xxx中,我們發現在專門存放platform_device的陣列中,有一個和fb有關的platform裝置s3c_device_fb,它被定義在arch/arm/plat-xxx/devs.c內:
struct platform_device s3c_device_fb = {
    .name         = "s3cfb",
    .id       = -1,
    .num_resources    = ARRAY_SIZE(s3cfb_resource),
    .resource     = s3cfb_resource,
    .dev          = {
        .dma_mask       = &fb_dma_mask,
        .coherent_dma_mask  = 0xffffffffUL
    }
};
  • 不難發現,裡面並沒有填充platform_data。這意味著該裝置沒有platform_data嗎?顯然不是的,就在s3c_device_fb的下面,定義了一個函式s3cfb_set_platdata,該函式明顯是用來為platform裝置填充platform_data的
    這裡寫圖片描述
  • 看一下該函式的reference,發現在mach-xxx裡的smdkc110_machine_init函式中被呼叫了,並且傳進去一個貌似是LCD硬體資訊的引數:
    這裡寫圖片描述
    搜尋ek070tn93_fb_data,發現這個結構體內果然包括了LCD所有的硬體資訊。看來,這個結構體就是platform_data的靈魂
static struct s3c_platform_fb ek070tn93_fb_data __initdata = {
    .hw_ver = 0x62,
    .nr_wins = 5,
    .default_win = CONFIG_FB_S3C_DEFAULT_WINDOW,
    .swap = FB_SWAP_WORD | FB_SWAP_HWORD,

    .lcd = &ek070tn93,
    .cfg_gpio   = ek070tn93_cfg_gpio,
    .backlight_on   = ek070tn93_backlight_on,
    .backlight_onoff    = ek070tn93_backlight_off,
    .reset_lcd  = ek070tn93_reset_lcd,
};
  • 說到底,廠商搞的這一出,其實就是繞了幾個圈子,把platform裝置和platform_data分開定義了,本質和我們之前的那種直接填充的方法沒有什麼不同

6.新核心下platform匯流排驅動的編寫方法

  • 對於驅動本身來說,新核心下的變化倒不大;主要是platform裝置不再需要在mach-xxx中註冊,而是直接以節點形式定義在裝置樹中。platform裝置可以直接定義在dts的根節點內,比如imx6dl-hummingboard.dts內的ir-receiver裝置
/ {
    model = "SolidRun HummingBoard DL/Solo";
    compatible = "solidrun,hummingboard", "fsl,imx6dl";

    ir_recv: ir-receiver {
        compatible = "gpio-ir-receiver";
        gpios = <&gpio1 2 1>;
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_hummingboard_gpio1_2>;
    };

/*後面一堆程式碼就省略了*/

  • 驅動程式將直接和裝置樹裡的裝置節點進行配對,是通過裝置節點中的compatible(相容性)來與裝置節點進行配對的。具體方法是定義一個of_match_table,只要裡面的compatible與裝置節點裡的compatible相同,那麼就觸發probe函式
/*驅動中定義的of_match_table*/
static struct of_device_id gpio_ir_recv_of_match[] = {
    { .compatible = "gpio-ir-receiver", },
    { },
};

/*of_match_table被繫結到driver結構體內*/
static struct platform_driver gpio_ir_recv_driver = {
    .probe  = gpio_ir_recv_probe,
    .remove = gpio_ir_recv_remove,
    .driver = {
        .name   = GPIO_IR_DRIVER_NAME,
        .owner  = THIS_MODULE,
        .of_match_table = of_match_ptr(gpio_ir_recv_of_match),
    },
};