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 

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

你可能感兴趣的:(request_firmware()——Linux固件子系统接口函数分析)