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;
}
...