Linux驱动基础(三):firmware的声明与加载

日期 :2026-09-19 · 参考源码版本:Linux 5.x
版权声明 本文为原创技术文章,著作权归作者所有。未经作者书面授权,严禁任何形式的转载、摘编、复制、建立镜像、商业使用

写在前面

NXP 的 i.MX 系列 SoC 集成了一个可编程 DMA 控制器(SDMA,Smart DMA) :控制器内部带一个小型处理器,每种外设的数据搬运各由一段脚本完成------串口收发、音频接口等都有各自的脚本,主机驱动只需通过通道号请求搬运,具体的外设协议由脚本处理。一部分脚本固化在控制器的 ROM 中;ROM 中的版本随芯片出厂而固定,不能修改,后续新增外设支持、修正脚本缺陷时,更新版脚本以固件文件 的形式发布,安装在 /lib/firmware/imx/sdma/ 路径下。i.MX6 平台的设备树为 SDMA 节点写明了文件名:

dts 复制代码
/* arch/arm/boot/dts/imx6qdl.dtsi */
sdma: sdma@20ec000 {
    compatible = "fsl,imx6q-sdma", "fsl,imx35-sdma";
    /* ... */
    fsl,sdma-ram-script-name = "imx/sdma/sdma-imx6q.bin";
};

驱动在探测时读出这个文件名,调用 request_firmware_nowait() 把脚本读入内核、下载到控制器的 RAM。模块由此产生了一类新的依赖:.ko 文件对用户态文件的依赖。模块工具链能自动解析的只有模块之间的符号引用,模块运行时要读取哪些文件,工具链无从得知;而嵌入式构建系统在裁剪根文件系统时恰恰需要知道这份文件清单,遗漏固件文件时,模块加载正常,用到该固件的设备在运行中失效。清单由驱动声明:

c 复制代码
/* drivers/dma/imx-sdma.c */
#if IS_ENABLED(CONFIG_SOC_IMX6Q)
MODULE_FIRMWARE("imx/sdma/sdma-imx6q.bin");
#endif
#if IS_ENABLED(CONFIG_SOC_IMX7D)
MODULE_FIRMWARE("imx/sdma/sdma-imx7d.bin");
#endif

本文剖析这条链路:宏如何把固件名记入 .modinfo 段,request_firmware() 运行时沿什么路径找到文件,imx-sdma 如何以异步接口加载脚本、校验格式并在固件文件不存在时回退 ROM 版本,以及 modinfo 与构建工具如何利用这些声明。

阅读本文你将了解:

  • 固件为什么以独立文件分发,模块对用户态文件的依赖为何需要专门机制申报
  • MODULE_FIRMWARE 如何以 .modinfo 段记录依赖,一行宏即完成声明
  • request_firmware() 的搜索路径、异步变体 request_firmware_nowait(),以及失败后的 sysfs 回退
  • imx-sdma 如何校验并下载固件,固件文件不存在时如何回退 ROM 脚本
  • 机制的局限:imx-sdma 的固件名来自设备树,静态声明为何覆盖不全

一、问题:模块对用户态文件的依赖

部分外设的控制器内嵌处理器,驱动为处理器准备一段程序,设备才能工作:或初始化控制器的功能单元(如无线网卡的固件),或实现设备特定的数据搬运流程(如 SDMA 的脚本)。这类程序由厂商以二进制文件提供;因授权方式与内核源码不同,不并入内核源码树,而是单独分发,安装到 /lib/firmware/ 下。驱动源码中只出现文件名,文件本身留在用户态。

模块的依赖有两种。一种是模块之间的符号引用:一个模块使用了另一个模块导出的符号,depmod 扫描每个 .ko 的符号表即可自动生成依赖数据库 modules.dep。另一种是本文讨论的:模块运行时要读取 /lib/firmware/ 下的固件文件,这种依赖在 .ko 文件中不留任何痕迹,工具链无从发现,必须由驱动用 MODULE_FIRMWARE 显式声明。

固件依赖若无人处理,故障是隐性的------模块加载成功、设备却不能完全工作。构建系统与问题定位因此都需要这份固件清单:

需求方 需要知道什么 用途
嵌入式构建系统 目标根文件系统需包含哪些固件 将驱动对应的固件收入镜像
问题定位 某驱动会请求哪些文件名 对照 /lib/firmware/ 排查缺失

维护这份信息有几种可能的方式:

  • 各工具自行维护对照表:需求方各自维护,必然与驱动演进脱节
  • 从源码解析 request_firmware() 调用点:固件名经常由运行期信息拼接而成,静态分析不可行
  • 驱动在模块信息中自行申报:数据随驱动源码同演进,工具只读取不维护

内核选择第三种。问题只剩一个:驱动以什么形式"申报"?

二、核心设计思想

2.1 声明与加载的分离

该机制由两个动作构成,二者不可互相替代:

  • MODULE_FIRMWARE------声明 。编译期在 .modinfo 段记录一条字符串,除此之外不做任何事。它不触发、也不影响加载
  • request_firmware------加载。运行时由驱动在合适的时机调用,固件加载器从文件系统读取文件

声明面向构建工具,加载面向内核,两条链路互不依赖:内核运行时从不读取 firmware= 条目 ------加载完全由驱动代码中的 request_firmware() 调用触发,声明只是把加载涉及的文件名提供给构建工具。

2.2 以 .modinfo 段记录依赖

MODULE_LICENSEMODULE_AUTHOR 等模块信息宏都以同一方式生效:编译期在 ELF 的 .modinfo 段生成 key=value 格式的字符串。MODULE_FIRMWARE 沿用这一方式,以 firmware 为 key 记录固件名。固件名本身即最终形态,无需翻译或汇总,因此一行宏就是全部机制。声明链与加载链相互独立,如图 1 所示。

图 1:声明链与加载链相互独立

graph TB subgraph BUILD[构建期 声明链] A[MODULE_FIRMWARE 声明] B[.modinfo 段 firmware= 条目] C[modinfo 工具] D[嵌入式构建 问题定位] A --> B --> C --> D end subgraph RUNTIME[运行时 加载链] E[probe 读设备树固件名] F[request_firmware_nowait] G[fw_path 路径表搜索] H[固件下载进 SDMA 控制器] E --> F --> G --> H end

两条链路唯一的交点是名字字符串本身:声明记录的名字必须与加载请求的名字逐字符一致,一致性纯靠约定维系。

2.3 声明与请求的名字必须一致

request_firmware() 传入的名字决定运行时读取的文件,MODULE_FIRMWARE() 传入的名字决定构建工具看到的文件名。两者必须逐字符一致,且都是相对固件搜索路径的相对路径。宏不做任何一致性检查,imx-sdma 的写法有两点值得注意:

  • 声明的是各 SoC 设备树的约定名 ------imx6qdl.dtsiimx6sl.dtsiimx6ul.dtsi 等文件为 SDMA 节点写的都是 imx/sdma/sdma-imx6q.bin,声明与设备树字符串保持一致
  • 声明以 IS_ENABLED(CONFIG_SOC_IMX6Q) 等条件裁剪------只声明当前构建配置下实际可能请求的文件,不为未启用的平台申报

三、宏的实现

c 复制代码
/* include/linux/module.h */
/* Optional firmware file (or files) needed by the module
 * format is simply firmware file name.  Multiple firmware
 * files require multiple MODULE_FIRMWARE() specifiers */
#define MODULE_FIRMWARE(_firmware) MODULE_INFO(firmware, _firmware)

MODULE_INFO(tag, info) 展开到 __MODULE_INFOinclude/linux/moduleparam.h):

c 复制代码
/* include/linux/moduleparam.h */
#define __MODULE_INFO(tag, name, info)                    \
	static const char __UNIQUE_ID(name)[]                 \
		__used __section(".modinfo") __aligned(1)         \
		= __MODULE_INFO_PREFIX __stringify(tag) "=" info

每次调用在 .modinfo 段生成一个独立的字符串常量 。imx-sdma 的两条声明即两个 firmware=imx/sdma/... 字符串。注释中"format is simply firmware file name"值得注意:声明的就是传给 request_firmware() 的名字,其中可以含子目录(imx/sdma/ 前缀对应文件系统的 /lib/firmware/imx/sdma/)。

__MODULE_INFO_PREFIX 在模块形态下为空,在内建(=y)形态下为 KBUILD_MODNAME "."。前缀的用途在内建形态下才显现:所有内建模块的目标文件被链接进同一个 vmlinux,各模块的 .modinfo 条目汇入同一段,若不加区分,条目之间无法分辨归属------virtio.firmware=... 这样的前缀把每条信息标记回所属模块。模块形态下每个 .ko 是独立的 ELF 文件,.modinfo 段天然只含本模块的条目,前缀为空即可。

内建形态还带来一个后果:vmlinux 不是 .ko,modinfo 不能像对模块那样直接读取。构建系统在链接 vmlinux 时以 objcopy 把整个 .modinfo 段抽取为独立文件(scripts/link-vmlinux.sh):

sh 复制代码
# scripts/link-vmlinux.sh
${OBJCOPY} -j .modinfo -O binary vmlinux.o modules.builtin.modinfo

该文件随模块安装到 /lib/modules/<内核版本>/ 下,modinfo 对内建模块即由此取得信息------实测 modinfo virtio 输出 filename: (builtin)。对模块形态,modinfo 则直接读 .ko.modinfo 段:

console 复制代码
$ modinfo -F firmware imx-sdma
imx/sdma/sdma-imx6q.bin

-F firmware 只取 firmware= 条目,一条声明一行。注意这与 depmod 无关:depmod 生成 modules.depmodules.aliasmodules.softdep 等,firmware= 不在其中------需要时对模块执行一次 modinfo 即可。

四、运行时:request_firmware 的加载路径

4.1 搜索路径与直接加载

request_firmware() 是同步接口:阻塞直到固件就绪或失败。内部路径 _request_firmware() 先查内建固件 ------CONFIG_EXTRA_FIRMWARE 列出若干固件文件名,构建系统把这些文件直接嵌入 vmlinux,fw_get_builtin_firmware() 先在嵌入的文件中查找,命中则不再访问文件系统;未命中则进入 fw_get_filesystem_firmware(),沿固定的路径表逐个尝试:

c 复制代码
/* drivers/base/firmware_loader/main.c */
static char fw_path_para[256];
static const char * const fw_path[] = {
	fw_path_para,                       /* firmware_class.path 模块参数 */
	"/lib/firmware/updates/" UTS_RELEASE,
	"/lib/firmware/updates",
	"/lib/firmware/" UTS_RELEASE,
	"/lib/firmware"
};

路径表自上而下有三层含义:

  • fw_path_para 最优先 :内核命令行参数 firmware_class.path= 可指定自定义搜索目录
  • updates 目录优先于正式目录,带内核版本号的目录优先于不带的 :为特定内核版本覆盖某固件时,直接放入 updates 目录即可,无需改动正式目录下的同名文件
  • 逐个路径以 kernel_read_file_from_path_initns() 读取 :在 init 进程的挂载命名空间中打开文件,用户态对 /lib/firmware/ 的替换对内核直接可见

若内核启用了 CONFIG_FW_LOADER_COMPRESS,未命中时还会再尝试同名 .xz 文件并解压。

4.2 失败路径、sysfs 回退与异步变体

所有路径未命中时,_request_firmware() 打印 dmesg 中最常见的固件故障线索:

c 复制代码
/* drivers/base/firmware_loader/main.c */
dev_warn(device, "Direct firmware load for %s failed with error %d\n",
	 name, ret);

打印之后才轮到回退机制firmware_fallback_sysfs()/sys/class/firmware/ 下以固件名创建一个设备,暴露 loadingdata 两个属性文件。内核原本要读文件的固件改由用户态直接提供:任意用户态进程向 loading 写 1 表示开始写入,把固件内容写入 data,再向 loading 写 0 表示写入完成,内核即以收到的内容充当固件;60 秒内没有完成写入则超时失败。回退并非无条件开启:仅当内核配置 CONFIG_FW_LOADER_USER_HELPER_FALLBACK=y,或调用方为 request_firmware_nowait(uevent=false) 时生效;多数系统不启用回退,文件读取失败即向驱动返回错误码。

request_firmware_nowait() 是同步接口的异步变体:把同一套加载路径放入工作队列执行,加载完成后调用驱动注册的完成函数------imx-sdma 注册的是 sdma_load_firmware()。它解决两个问题------probe 不被文件读取阻塞,以及调用点不允许睡眠。

4.3 imx-sdma:异步加载、校验与 ROM 回退

imx-sdma 用的正是异步接口。probe 完成基础初始化后,从设备树读出固件名并排入异步加载:

c 复制代码
/* drivers/dma/imx-sdma.c */
static int sdma_get_firmware(struct sdma_engine *sdma,
		const char *fw_name)
{
	return request_firmware_nowait(THIS_MODULE,
			FW_ACTION_UEVENT, fw_name, sdma->dev,
			GFP_KERNEL, sdma, sdma_load_firmware);
}

/* sdma_probe() 末尾 */
ret = of_property_read_string(np, "fsl,sdma-ram-script-name",
			      &fw_name);
if (ret) {
	dev_warn(&pdev->dev, "failed to get firmware name\n");
} else {
	ret = sdma_get_firmware(sdma, fw_name);
	...
}

FW_ACTION_UEVENT 表示加载失败时通过 uevent 通告用户态。回调 sdma_load_firmware() 在工作队列中执行,分两条路径:

固件文件不存在fw 为 NULL)------回退 ROM:

c 复制代码
/* drivers/dma/imx-sdma.c */
if (!fw) {
	dev_info(sdma->dev, "external firmware not found, using ROM firmware\n");
	return;
}

控制器 ROM 中固化的脚本继续承担数据搬运,驱动照常工作。固件文件不存在因此不是致命错误,代价只是用不上更新版本的脚本。

固件就绪------校验并下载。固件文件并非裸二进制,而是带文件头的镜像:

c 复制代码
/* drivers/dma/imx-sdma.c */
#define SDMA_FIRMWARE_MAGIC 0x414d4453    /* "SDMA" */

struct sdma_firmware_header {
	u32	magic;
	u32	version_major;
	u32	version_minor;
	u32	script_addrs_start;
	u32	num_script_addrs;
	u32	ram_code_start;
	u32	ram_code_size;
};

回调逐项校验文件头:校验通过后,sdma_load_script() 把 RAM 镜像下载进控制器,sdma_add_scripts() 记录各脚本的入口地址,最后 sdma->fw_loaded = true。完整时序如图 2 所示。

图 2:imx-sdma 的异步固件加载时序

sequenceDiagram participant P as sdma_probe participant W as 工作队列 participant L as 固件加载器 participant FS as 文件系统 participant C as sdma_load_firmware P->>P: 读设备树 fsl,sdma-ram-script-name P->>W: request_firmware_nowait 排入工作队列 Note over P: probe 不等待固件加载先行返回 W->>L: _request_firmware L->>L: 查内建固件 CONFIG_EXTRA_FIRMWARE loop fw_path 路径表 L->>FS: kernel_read_file_from_path_initns end alt 命中 L-->>C: 回调 fw 指向固件数据 C->>C: 校验 magic 与版本 下载脚本 fw_loaded 置位 else 未命中 L-->>C: 回调 fw 为 NULL C->>C: 回退 ROM 内固化脚本 end

固件文件不存在的影响推迟到使用时才显现:部分外设的通道被标记为必须使用 RAM 脚本(is_ram_script),这类通道在 fw_loaded 为假时直接拒绝传输:

c 复制代码
/* drivers/dma/imx-sdma.c */
if (!sdmac->sdma->fw_loaded && sdmac->is_ram_script) {
	dev_warn_once(sdmac->sdma->dev, "sdma firmware not ready!\n");
	goto err_out;
}

依赖这些通道的外设(如串口)数据搬运失效------模块加载成功、设备树匹配成功,固件加载失败的影响直到此时才暴露。

五、构建期:谁读取 firmware= 声明

modinfo -F firmware 是读取声明的基本途径,需求方各自在其上构建:

  • 嵌入式构建系统 :生成目标根文件系统镜像时,先扫描镜像内将安装的 .ko,对每个模块执行 modinfo -F firmware 得到固件名清单,再把清单对应的文件复制进镜像的 /lib/firmware/
  • 问题定位 :设备工作异常时,先用 modinfo 列出驱动会请求的文件名,再对照 /lib/firmware/ 确认文件是否存在;dmesg 中 Direct firmware load for xxx failed with error -2 的排查由此起步

内建(=y)驱动的声明经 modules.builtin.modinfo 导出,读取方式相同;若系统不希望依赖任何用户态文件,还可在构建内核时以 CONFIG_EXTRA_FIRMWARE 把固件直接嵌入 vmlinux------那是加载器最先查询的路径。

六、机制的边界

宏声明的是编译期确定的字符串常量,两条边界由此产生。

边界一:驱动代码中不含固件名时,声明只能申报约定名。用一个具体场景说明。i.MX6 平台上,imx-sdma 运行时要读的文件名写在设备树里,而不是驱动代码里:

c 复制代码
/* 驱动代码:名字从设备树读出,驱动自己不知道它是什么 */
ret = of_property_read_string(np, "fsl,sdma-ram-script-name", &fw_name);
ret = sdma_get_firmware(sdma, fw_name);      /* 请求名为 fw_name 的固件 */

/* 设备树:名字写在这里 */
fsl,sdma-ram-script-name = "imx/sdma/sdma-imx6q.bin";

同一份驱动模块运行在哪个平台、读到什么名字,编译驱动时一概不知。那 MODULE_FIRMWARE("imx/sdma/sdma-imx6q.bin") 里这个名字是从哪来的?------不是从驱动代码推断的,而是依据设备树的既定取值:i.MX6 系列各 dtsi 为该属性写的值固定为这个字符串,驱动与 dtsi 的维护者共同遵守这一约定,驱动据此把约定名写进声明。

边界二:声明不参与加载,写错也不会被发现。宏不做任何检查,名字写错不会有任何报错,后果只是该固件不被收入镜像。imx-sdma 的声明之所以与设备树一致,靠的是驱动与 dtsi 维护者遵守同一命名约定。

七、设计哲学小结

  1. 机制复杂度由数据要走多远决定firmware= 条目只被构建工具读取,一行宏即是全部机制------无需翻译,无需汇总数据库,内核运行时没有任何读取者。数据流通的链路越长,中间环节越多,机制也就越重。
  2. 沿用既有结构优于新建机制.modinfo 段与 modinfo 工具在固件依赖出现之前早已存在,记录一种新依赖只需占用一个新的 key,不必为固件引入新的 ELF 段或新的工具。
  3. 没有检查的约定依靠缩减范围来降低风险。宏不校验声明与实际请求是否一致,错误只在固件未进入镜像时才暴露。

附录 A:延伸阅读

  • include/linux/module.h --- MODULE_FIRMWARE定义
  • include/linux/moduleparam.h --- .modinfo 段机制
  • drivers/dma/imx-sdma.c --- 贯穿全文的实例
  • arch/arm/boot/dts/imx6qdl.dtsi --- fsl,sdma-ram-script-name 属性的约定用法
  • Documentation/devicetree/bindings/dma/fsl-imx-sdma.txt --- SDMA 设备树绑定文档

版权声明(重申) 本文为原创技术文章,著作权归作者所有。未经作者书面授权,严禁任何形式的转载、摘编、复制、建立镜像、商业使用

Copyright © 2026. All rights reserved. Unauthorized reproduction, distribution, or commercial use is strictly prohibited.

相关推荐
云计算练习生2 小时前
什么是系统调用?为什么程序访问硬件必须经过它
linux·windows·操作系统·系统调用·操作系统原理
玄芯散人3 小时前
【筑基·059】Linux命令行入门:工程师的操作系统
linux·操作系统·命令行
半仙白桑3 小时前
内核篇第二讲:Linux exec 函数族详解
linux·linux驱动
AR-26710-3 小时前
Linux Day7——建组/用户、umask、Python脚本
linux·python
吴声子夜歌4 小时前
Shell编程实例——内务及管理任务(一)
linux·运维·shell
byte轻骑兵4 小时前
【BlueZ 】input 模块:蓝牙鼠标/键盘等输入设备的基础适配逻辑
linux·人工智能·bluez·电脑蓝牙·嵌入式蓝牙
吴声子夜歌5 小时前
Shell编程实例——高级脚本编程(一)
linux·运维·网络·shell
Huangjin007_6 小时前
【Linux 系统篇(二十一)】进程(九):进程等待 wait/waitpid、status状态参数
linux·运维·服务器
螺蛳粉 螺蛳粉6 小时前
mysql的备份与恢复
linux·运维·数据库·mysql