日期 :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_LICENSE、MODULE_AUTHOR 等模块信息宏都以同一方式生效:编译期在 ELF 的 .modinfo 段生成 key=value 格式的字符串。MODULE_FIRMWARE 沿用这一方式,以 firmware 为 key 记录固件名。固件名本身即最终形态,无需翻译或汇总,因此一行宏就是全部机制。声明链与加载链相互独立,如图 1 所示。
图 1:声明链与加载链相互独立
两条链路唯一的交点是名字字符串本身:声明记录的名字必须与加载请求的名字逐字符一致,一致性纯靠约定维系。
2.3 声明与请求的名字必须一致
request_firmware() 传入的名字决定运行时读取的文件,MODULE_FIRMWARE() 传入的名字决定构建工具看到的文件名。两者必须逐字符一致,且都是相对固件搜索路径的相对路径。宏不做任何一致性检查,imx-sdma 的写法有两点值得注意:
- 声明的是各 SoC 设备树的约定名 ------
imx6qdl.dtsi、imx6sl.dtsi、imx6ul.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_INFO(include/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.dep、modules.alias、modules.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/ 下以固件名创建一个设备,暴露 loading 与 data 两个属性文件。内核原本要读文件的固件改由用户态直接提供:任意用户态进程向 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 的异步固件加载时序
固件文件不存在的影响推迟到使用时才显现:部分外设的通道被标记为必须使用 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 维护者遵守同一命名约定。
七、设计哲学小结
- 机制复杂度由数据要走多远决定 。
firmware=条目只被构建工具读取,一行宏即是全部机制------无需翻译,无需汇总数据库,内核运行时没有任何读取者。数据流通的链路越长,中间环节越多,机制也就越重。 - 沿用既有结构优于新建机制 。
.modinfo段与 modinfo 工具在固件依赖出现之前早已存在,记录一种新依赖只需占用一个新的 key,不必为固件引入新的 ELF 段或新的工具。 - 没有检查的约定依靠缩减范围来降低风险。宏不校验声明与实际请求是否一致,错误只在固件未进入镜像时才暴露。
附录 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.