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