request_firmware()分析——加載在用戶空間的固件

  一些不帶內置存儲的設備,依賴於驅動預加載的固件才能運行,傳統做法是將固件二進制碼作爲一個數組編譯進驅動代碼,如下所示:

static const unsigned char my_firmware[] = {
    0x00, 0x01, 0x02,
    0x01, 0x03, 0x04,
    0xff, 0xef, 0xda,
    ......
};

這種方法圖一時省力,卻爲後續的維護升級帶來了麻煩,每次設備廠商發佈新的固件,都需要重新編譯驅動模塊或整個內核。

  針對這種情況,在內核中其實早有解決辦法,request_firmware()是一套成熟的固件加載方案:將固件以二進制文件形式存儲於文件系統之中,在內核啓動後再從用戶空間將固件傳遞至內核空間,解析固件獲得參數,最後加載至硬件設備。
  
  但是requeset_firmware()必須等到文件系統掛載後才能得到用戶空間的固件,固件加載時間落後於傳統方法。是想要更早的加載時間還是更方便的後續維護?需要驅動開發者根據實際情況去選擇

  在《Linux設備驅動程序》和《Linux設備驅動開發詳解》中對request_firmware()的介紹都是寥寥數語,在本文中,我對自己實際使用該接口的一些經驗和網上零落的資料進行了總結,希望可以幫助更多的驅動開發者。 


固件加載相關內核API介紹

#include <linux/firmware.h>

int request_firmware(const struct firmware **fw, const char *name,
             struct device *device);
參數 描述
fw 用於保存申請到的固件
name 固件名(非路徑名)
device 申請固件的設備結構體

  如果申請固件成功,函數返回0;申請固件失敗,返回一個負數值(如-EINVAL、-EBUSY)。申請固件成功後,fw指向如下結構體:

struct firmware {
    size_t size;
    const u8 *data;
    struct page **pages;

    /* firmware loader private fields */
    void *priv;
};

  在調用request_firmware()時,函數將在 /sys/class/firmware 下創建一個以設備名爲目錄名的新目錄,其中包含 3 個屬性:
  1. loading ,當固件加載時被置1,加載完畢被置0,如果被置-1則終止固件加載;
  2. data,內核獲取固件接口,當loading被置1時,用戶空間通過該屬性接口傳遞固件至內核空間;
  3. device ,符號鏈接,鏈接至/sys/devices/下相關設備目錄。

  當sysfs接口創建完畢,udevd會配合將固件通過sysfs節點寫入內核。在內核空間中獲取到固件後,應該對其進行校驗檢查,然後再解析加載至設備,最後通過如下API釋放firmware 結構體:

void release_firmware(const struct firmware *fw);

使用request_fimware()前的準備

  在使用requeset_firmware()之前,要在menuconfig之中勾選相應配置:

Symbol: FW_LOADER [=y]
  │ Type  : tristate
  │ Prompt: Userspace firmware loading support
  │   Location:
  │     -> Device Drivers
  │ (1)   -> Generic Driver Options
  │   Defined at drivers/base/Kconfig:77                                                                                                         

或者直接在.config中配置:

// .config

......

CONFIG_FW_LOADER=y

......

  在內核中CONFIG_FW_LOADER選項默認關閉,如果不進行手動配置,requset_firmware()會被預編譯爲一個內聯函數(不做任何操作,直接返回-EINVAL):

// include/linux/firmware.h

#if defined(CONFIG_FW_LOADER) || (defined(CONFIG_FW_LOADER_MODULE) && defined(MODULE))
int request_firmware(const struct firmware **fw, const char *name,
             struct device *device);
......
#else
static inline int request_firmware(const struct firmware **fw,
                   const char *name,
                   struct device *device)
{
    return -EINVAL;
}
......
#endif

  
  在上文介紹request_firmware()時提到第二個參數name爲固件名,而非路徑名,那麼內核是怎麼在沒有文件路徑的情況下找到固件呢?答案其實是內核已經在fw_path數組中指定了路徑名,我們只需要將固件置於相對應目錄下即可:

// driver/base/firmware_class.c

static const char * const fw_path[] = {
    fw_path_para,
    "/system/etc/firmware",
    "/lib/firmware/updates/" UTS_RELEASE,
    "/lib/firmware/updates",
    "/lib/firmware/" UTS_RELEASE,
    "/lib/firmware",
};

大部分情況下固件置於/system/etc/firmware目錄,如果需要指定自定義目錄,則需要在fw_path數組中添加目錄路徑名。


requset_firmware()——固件加載實例分析

  正如linus所說“talk is cheap, show me the code”,接下來我們以requset_firmware()等系列內核api爲基礎設計一個加載用戶空間固件的方案,具體流程如下:
  
1、生成固件“my_frimware.bin”並置於自定義目錄/data/my_firmware”下,
然後在fw_path數組中添加目錄路徑名:

// driver/base/firmware_class.c

static const char * const fw_path[] = {
    fw_path_para,
    "/data/my_firmware",
    "/system/etc/firmware",
    "/lib/firmware/updates/" UTS_RELEASE,
    "/lib/firmware/updates",
    "/lib/firmware/" UTS_RELEASE,
    "/lib/firmware",
};

2、request_firmware()函數如果在probe()中使用會一直阻塞至文件系統掛載獲取固件後,因此我們在驅動中將其封裝爲sysfs節點,以便在文件系統掛載後調用:

// driver/test_driver.c
......

static ssize_t load_fw_store(struct device *dev,   
                    struct device_attribute *attr,   
                    const char *buf, size_t count)
{
#define FIRMWARE_NAME "/data/my_firmware/my_firmware.bin"

    int ret;
    unsigned long value;
    const struct firmware *fw = NULL;

    if (kstrtoul(buf, 10, &value))
        return -EINVAL;

    if (value == 1) {
        /* 申請用戶空間固件 */
        ret = request_firmware(&fw, FIRMWARE_NAME, dev);
        if (ret) 
            return -ENOENT;

        /* 加載固件至硬件設備 */
        load_fw(fw);

        /* 釋放firmware結構體 */
        release_firmware(fw);
    }

    return count;
}

// sys/devices/platform/test/load_fw
static DEVICE_ATTR(load_fw, S_IWUGO, NULL, load_fw_store);  

......

static int test_probe(struct platform_device *pdev)
{
    int ret;

    ......

    /* 創建sysfs節點 */
    ret = device_create_file(pdev, &dev_attr_load_fw);
    if (ret)
        return -EINVAL;

    ......

    return 0;
}

......

3、由於固件位於/data分區,所以在init.rc中“on post-fs-data”後添加對sysfs節點的操作,實現開機/data分區掛載後即開始自動加載固件:

// system/core/rootdir/init.rc
......
on post-fs-data
write sys/devices/platform/test/load_fw 1
......

requeset_firmware_nowait()——以異步方式加載固件

  在上一個實例中我們用sysfs+request_firmware()實現了加載用戶空間固件的方案,但request_firmware()是以同步方式運行的,如果在調用requset_firmware()的上下文中不能睡眠,我們應該選擇另一個異步api來加載掛件:

int request_firmware_nowait(
    struct module *module, bool uevent,
    const char *name, struct device *device, gfp_t gfp, void *context,
    void (*cont)(const struct firmware *fw, void *context));
參數 描述
module 模塊名
uevent
name 固件名
device 申請固件的設備結構體
gfp 內核內存分配標誌位
context 私有數據指針
cont 回調函數

深入requeset_firmware_nowait()函數內部,我們發現其實是以工作隊列方式來實現異步工作方式:

int
request_firmware_nowait(
    struct module *module, bool uevent,
    const char *name, struct device *device, gfp_t gfp, void *context,
    void (*cont)(const struct firmware *fw, void *context))
{
    struct firmware_work *fw_work;

    fw_work = kzalloc(sizeof (struct firmware_work), gfp);
    if (!fw_work)
        return -ENOMEM;

    fw_work->module = module;
    fw_work->name = name;
    fw_work->device = device;
    fw_work->context = context;
    fw_work->cont = cont;
    fw_work->uevent = uevent;

    if (!try_module_get(module)) {
        kfree(fw_work);
        return -EFAULT;
    }

    get_device(fw_work->device);
    /* 初始化工作隊列 */
    INIT_WORK(&fw_work->work, request_firmware_work_func);
    /* 調用工作隊列 */
    schedule_work(&fw_work->work);
    return 0;
}

瞭解了方法requset_firmware_nowait()的異步原理之後,我們對上例的test_driver.c進行一些小小的改動就能實現異步加載固件:

// driver/test_driver.c

......

/* requset_firmware_nowait()回調函數 */
void test_requset_fw_callback(const struct firmware *fw, void *context)
{  
    struct device *dev = context;

    if (fw != NULL) {
        /* 加載固件至硬件設備 */    
        load_fw(fw, dev);
        /* 釋放firmware結構體 */
        release_firmware(fw);
    }
}

static ssize_t load_fw_store(struct device *dev,   
                    struct device_attribute *attr,   
                    const char *buf, size_t count)
{
#define FIRMWARE_NAME "/data/my_firmware/my_firmware.bin"

    int ret;
    unsigned long value;
    const struct firmware *fw = NULL;

    if (kstrtoul(buf, 10, &value))
        return -EINVAL;

    if (value == 1) {
        /* 申請用戶空間固件 */
        ret = request_firmware_nowait(THIS_MODULE, 1, FIRMWARE_NAME, \
                dev, GFP_KERNEL, dev, test_requset_fw_callback);
        if (ret) 
            return -ENOENT;
    }

    return count;
}

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