request_firmware()——Linux固件子系統接口函數分析

1. 概述

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

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

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

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

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

2. 固件加載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);

3. 使用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_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數組中添加目錄路徑名。

4. 固件加載實例分析

正如linus所說“talk is cheap, show me the code”,接下來我們以requset_firmware()等系列內核api爲基礎設計一個加載用戶空間固件的方案,具體流程如下:

  • 生成固件“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",
};
  • 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;
}
...
  • 由於固件位於/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
...

5. 以異步方式加載固件

在上一個實例中我們用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;
}
...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章