ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux驱动开发:固件firmware的声明与加载机制详解

Linux驱动开发:固件firmware的声明与加载机制详解 做嵌入式或者Linux驱动开发的朋友十有八九都遇过这么一幕板子刚起来dmesg里飘着一行“Direct firmware load for xxx failed with error -2”然后对应的网卡、蓝牙或者某个外设就是起不来。这个报错说的就是firmware没加载成功。firmware固件这玩意儿在驱动开发里经常被一句话带过可真到了自己的设备需要下载固件的时候不少人会栽在“声明”和“加载”这两个环节上。这篇文章是Linux驱动基础系列的第三篇专门把firmware的声明与加载这件事从头到尾捋一遍包括request_firmware系列API怎么选、MODULE_FIRMWARE宏是干嘛的、内核怎么找到固件文件、以及最常见的几个报错怎么排查。适合刚入门驱动开发、或者正在调自己第一块需要下载固件的板子的同学参考。1. 为什么要关心firmware——先搞清楚它是什么1.1 firmware在驱动开发里到底扮演什么角色很多刚接触驱动的朋友会把firmware和driver搞混觉得“我写了驱动设备就能动了”。实际上对相当一部分设备来说驱动只是“管理员的说明书”而真正干活的是设备内部跑的固件程序。举个最直观的例子一块WiFi网卡芯片它内部有一颗完整的微控制器MCU上电后这颗MCU需要一个基础运行环境——操作系统、协议栈、射频校准参数这些东西体积不小不可能全部烧在芯片的ROM里。所以芯片出厂只带一个最小的bootloader真正的功能固件放在主机端文件系统里。驱动初始化的时候把固件下载到设备设备才能进入正常工作状态。类似的还有GPU比如部分amd、mali显卡驱动、FPGA通过JTAG或者SPI加载bitstream、各种带DSP的音频编解码芯片、USB转串口芯片里的部分型号以及大量物联网设备里的协处理器。这里有个非常实际的问题为什么内核不直接把firmware数据编进驱动二进制非要搞一套“声明 加载”的机制原因有几层。首先是体积很多固件是几百KB甚至几MB全都编进内核会让镜像膨胀得厉害。其次是版权不少固件是闭源的二进制blob厂商只允许以独立文件形式分发不允许你混进GPL的内核代码里。第三是更新需求固件经常要修复bug、增加无线信道表如果编死在驱动里升级一次驱动就得重新编译整个模块。把固件放在 /lib/firmware 目录下独立维护无论是根文件系统更新还是initramfs打包都灵活得多。1.2 什么时候需要自己加载firmware如果是做芯片原厂BSPfirmware加载流程通常已经写在SDK里了。但如果你是在做产品、做板卡定制或者调试一个非主流设备那大概率要自己动手。常见需要自己处理固件加载的场景有这么几类设备内部有MCU/DSP上电后处于bootloader状态等待主机通过SPI、I2C、USB或者自定义总线把固件灌进去。这类设备最典型驱动probe之后第一件事就是请求固件、下载固件、等设备重启、再发启动命令。FPGA逻辑加载。现在很多板卡用FPGA做协议加速驱动里需要读取bitstream文件并写入FPGA配置接口。有些框架比如FPGA Manager已经封装好了但底层还是靠request_firmware一类的机制。设备树里甚至可以直接用firmware-name属性声明固件名驱动拿属性值去请求固件。内核已经支持这类设备但厂商提供的固件文件需要手工安装。这个严格来说不算“你自己写加载代码”但你得知道固件被弄到哪个目录、模块报错时怎么定位。我的建议是拿到一份不熟悉的驱动源码先搜一下有没有 request_firmware、request_firmware_nowait、request_firmware_direct 这几个函数再配合 modinfo 看模块声明的固件文件清单基本就能判断这个设备是不是需要额外装固件。这个习惯能帮你少走很多弯路。2. 驱动的firmware声明从代码层面开始2.1 request_firmware 系列接口怎么选Linux内核提供了完整的固件加载API头文件在 include/linux/firmware.h。核心数据结构是 struct firmware里面最重要的两个成员是 data 和 size分别指向固件数据的首地址和字节大小。最常用的是同步接口int request_firmware(const struct firmware **fw, const char *name, struct device *device);调用的时候传入固件文件名和设备指针函数会阻塞直到固件加载完成。成功后 *fw 指向一个需要你手动释放的结构体release_firmware(fw);这是最简单的用法但有个坑request_firmware 是同步的会阻塞当前线程。如果在probe里直接调用而固件文件刚好在慢速存储比如NFS挂载的根文件系统上或者用户空间还没有准备好提供固件整个驱动加载就可能卡住几十秒甚至超时连带着启动流程都受影响。如果不想在probe里久等或者需要并行加载多个固件就用异步版本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));这个函数会立刻返回0表示已经排队真正的加载过程在内核工作队列里执行。加载完成后回调函数 cont 会被调用注意此时 fw 可能为 NULL表示加载失败必须判空。还有两个使用频率稍低的接口值得知道。request_firmware_direct 和 request_firmware 类似但它会跳过用户空间helper回退路径只有直接从文件系统读到文件才成功。request_firmware_into_buf 则可以把固件直接加载到驱动预先分配好的缓冲区里适合固件数据要下载到DMA缓冲区、不希望多一次拷贝的场景。我整理了一张选型参考表接口是否阻塞适用场景注意点request_firmware阻塞probe流程简单、文件系统稳定不要在高锁粒度持有锁时调用request_firmware_nowait非阻塞启动流程敏感、多固件并行加载回调必须判空注意生命周期request_firmware_direct阻塞明确只需要直接文件加载加载不到就失败没有回退request_firmware_into_buf阻塞固件需要直接进DMA/预分配缓冲缓冲区大小要先确认2.2 一种更省事的声明方式MODULE_FIRMWARE 宏说完了加载API再来说标题里的另一个关键词——“声明”。很多驱动只写了 request_firmware 请求固件却忘了在模块里声明自己依赖哪些固件文件。这样做的直接后果是modinfo 查不到固件清单udev/hotplug 的固件自动搜索机制没法用initramfs 打包工具也不知道该把这个驱动对应的固件收进去。结果就是换个机器环境跑固件文件没被装进初始内存盘设备死活起不来。正确的做法是在模块里加一句MODULE_FIRMWARE(iwlwifi-8000C-36.ucode);MODULE_FIRMWARE 的作用是把这个固件文件名写进模块的.modinfo段里。编译完加载模块后执行 modinfo 就能看到类似这样的输出$ modinfo iwlwifi | grep firmware firmware: iwlwifi-9000-pu-b0-jf-b0-46.ucode firmware: iwlwifi-9000-pu-b0-jf-b0-45.ucode ...这些信息会被 depmod 收集起来生成/lib/modules/$(uname -r)/modules.firmware 之类的映射文件。之后无论是 udev 还是 mkinitramfs、dracut 这类 initramfs 工具都能根据这个映射表自动找到对应的固件文件并打包。所以我的建议是只要驱动会请求固件就必须加 MODULE_FIRMWARE这不仅仅是规范问题而是实打实影响固件能被自动分发的问题。如果一个驱动可能用到多个版本的固件就写多个 MODULE_FIRMWARE 宏。文件名要带上相对路径比如 “rtl_nic/rtl8168g-3.fw”这样安装脚本会把它放到 /lib/firmware/rtl_nic/ 目录下加载时内核再去找同样的相对路径。2.3 内核配置与firmware路径那些事光有驱动代码还不够内核的固件加载子系统和文件系统的配合也很重要。首先内核要开启 CONFIG_FW_LOADER这是固件加载机制的主开关。大多数发行版默认是m或y但如果你自己裁剪内核很容易把这个选项漏掉然后发现 request_firmware 每次都返回 -ENOENT甚至连代码都编不进去。与固件加载相关的配置项还有CONFIG_FW_LOADER_USER_HELPER启用传统的用户空间helper方式加载固件很多精简系统已经关闭。CONFIG_FW_LOADER_USER_HELPER_FALLBACK启用sysfs回退机制当直接从文件系统加载失败时内核会在 /sys/class/firmware 下创建节点通过uevent通知用户空间让用户空间把固件内容写入sysfs属性。这个机制在纯initramfs环境里仍然有用但也可能引入60秒超时等待的坑。CONFIG_FW_LOADER_COMPRESS_XZ / CONFIG_FW_LOADER_COMPRESS_ZSTD让内核支持加载 .xz 或 .zst 压缩格式的固件文件。启用后如果找不到原始文件内核会尝试找 name.xz 或 name.zst。默认情况下固件文件放在 /lib/firmware 目录下。留意一下这些路径通常是发行版默认配置的实际生效路径可以通过模块参数查看cat /sys/module/firmware_class/parameters/path如果你把固件放在自定义目录比如 /lib/firmware/mycompany可以临时改这个sysfs节点也可以在启动参数里加 firmware_class.path/lib/firmware/mycompany不过自定义目录要非常谨慎因为改动影响全局所有固件查找。3. firmware加载机制与实操过程3.1 加载全过程从request_firmware到用户空间把加载机制拆开看request_firmware 并不是简单打开一个文件读一遍而是经历了几层判断。整个流程大致是驱动调用 request_firmware内核进入 drivers/base/firmware_loader 子系统。首先检查这个固件是否已经在“内置固件表”里编译进内核的固件后面会讲如果在直接返回内置镜像。然后尝试直接从文件系统读取。查找路径依次是 firmware_class.path 指定的路径、/lib/firmware、/lib/firmware/$(uname -r) 等。如果内核开启了压缩固件支持还会尝试查找 name.xz 或 name.zst。如果直接加载失败并且开启了 CONFIG_FW_LOADER_USER_HELPER_FALLBACK内核会在sysfs下创建固件加载节点发送uevent等待用户空间程序通常是udev规则把固件数据写入 /sys/class/firmware/xxx/data。整个过程有一个超时限制默认60秒左右超时后会报错。加载成功后内核把固件数据封装成 struct firmware 返回给驱动。所以你在启动日志里经常能看到这两种信息“Direct firmware load for xxx failed with error -2”表示直接从文件系统读文件失败了“Falling back to sysfs fallback for: xxx”表示内核准备走回退流程。error -2 就是 -ENOENT意思是“文件不存在”。3.2 直接编译进内核的firmwarebuilt-in固件有些场景不适合把固件放在根文件系统里。比如设备在根文件系统挂载之前就需要初始化像是网卡驱动做成built-in、根文件系统本身要通过这个网卡从NFS启动或者你希望减少对用户空间的依赖。这时候可以用内核内置固件功能。在 .config 里配置CONFIG_FW_LOADERy CONFIG_EXTRA_FIRMWAREmydev-fw.bin CONFIG_EXTRA_FIRMWARE_DIR/lib/firmwareCONFIG_EXTRA_FIRMWARE 指定要编进内核的固件文件名多个文件用空格分隔CONFIG_EXTRA_FIRMWARE_DIR 指定编译时从哪个目录读取这些文件。内核构建系统会把固件转成二进制数组链接进内核镜像驱动里照样用 request_firmware 请求内核会在内置固件表里找到并返回。这个方法确实省事但别滥用。固件二进制会被完整地带进内核镜像几MB的固件会让内核镜像明显变大。而且如果固件是闭源的把它编进内核甚至可能引发许可证问题。我一般只在两种场景用一是启动早期必须用的设备二是开发调试阶段图省事。产品量产时还是建议走 /lib/firmware 的标准路径。3.3 一个完整的驱动示例与加载验证下面用一个最小的平台驱动例子演示完整流程。假设设备树里有一个 mycompany,mydev 兼容节点驱动probe时要从文件中加载 mydev-fw.bin 固件然后下载给设备。同步版本#include linux/module.h #include linux/platform_device.h #include linux/firmware.h #include linux/of.h #define FW_NAME mydev-fw.bin static int mydev_probe(struct platform_device *pdev) { const struct firmware *fw NULL; int ret; ret request_firmware(fw, FW_NAME, pdev-dev); if (ret) { dev_err(pdev-dev, request firmware failed: %d\n, ret); return ret; } dev_info(pdev-dev, firmware loaded, size %zu bytes\n, fw-size); /* * 到这里就可以把 fw-data 里的内容按设备的下载协议 * 通过 SPI/I2C/寄存器写入设备比如逐字节发送或者 * 触发DMA搬运然后再发启动命令。 */ release_firmware(fw); return 0; } static int mydev_remove(struct platform_device *pdev) { return 0; } static const struct of_device_id mydev_of_match[] { { .compatible mycompany,mydev }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, mydev_of_match); static struct platform_driver mydev_driver { .probe mydev_probe, .remove mydev_remove, .driver { .name mydev, .of_match_table mydev_of_match, }, }; module_platform_driver(mydev_driver); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(Firmware loading demo driver); MODULE_LICENSE(GPL); MODULE_FIRMWARE(FW_NAME);如果probe不想被同步阻塞改成异步版本static void mydev_fw_cb(const struct firmware *fw, void *context) { struct platform_device *pdev context; if (!fw) { dev_err(pdev-dev, async firmware load failed\n); return; } dev_info(pdev-dev, async firmware loaded, size %zu bytes\n, fw-size); /* 执行固件下载 */ release_firmware(fw); } static int mydev_probe(struct platform_device *pdev) { return request_firmware_nowait(THIS_MODULE, true, FW_NAME, pdev-dev, GFP_KERNEL, pdev, mydev_fw_cb); }在开发机上验证的步骤一般是这几步把固件文件放到 /lib/firmware/ 下。注意文件名要跟驱动请求的名字完全一致大小写、路径都不能错。编译模块并加载观察 dmesgsudo cp mydev-fw.bin /lib/firmware/ make -C /lib/modules/$(uname -r)/build M$(pwd) modules sudo insmod mydev.ko dmesg | tail用 modinfo 检查模块声明的固件清单modinfo mydev.ko | grep firmware正常的话 dmesg 里会出现类似 “mydev mydev.0: firmware loaded, size 10240 bytes” 的日志。如果固件没找到就会是开头那种 “Direct firmware load ... failed with error -2”。4. 常见问题与排查技巧实录4.1 firmware文件明明存在却加载失败这是我在社区见得最多的一类问题文件明明放在 /lib/firmware 下了驱动还是报 -2。这时候别急着怀疑内核bug按下面顺序排查。先看路径。firmware_class.path 参数如果被改过内核就不会去默认路径找。检查一下cat /sys/module/firmware_class/parameters/path如果这个路径不是 /lib/firmware那固件可能放错目录了。再看内核版本。较新内核如果开了 CONFIG_FW_LOADER_COMPRESS_XZ驱动请求名字为 foo.bin 时内核会自动尝试加载 foo.bin.xz。但如果你直接在 /lib/firmware 放了一个 foo.bin.xz而内核没开对应压缩支持就会加载失败。可以用 lsinitramfs 查看initramfs里有没有打包固件也可以直接用 find 确认文件在不在目标rootfs里find /lib/firmware -name mydev-fw.bin -o -name mydev-fw.bin.*然后是权限和文件系统类型。固件文件最好让 root 可读权限 644 一般没错。如果根文件系统是NFS、FUSE这类确保文件系统支持seek和read某些特殊挂载方式下固件读取也可能出问题。最后还有个很容易忽略的点如果驱动是从 initramfs 阶段加载的而你只把固件放到了磁盘的 /lib/firmware没有更新 initramfs那启动早期一样会报错。用 dracut 或 update-initramfs 重新生成一下初始内存盘把固件打进去。4.2 request_firmware_nowait回调不执行异步固件加载最大的问题是“回调没被调用”。排查这个问题首先要确认 request_firmware_nowait 的返回值是不是0。如果返回了负数说明请求根本没排进去。返回0只代表请求被接受不代表固件已经加载成功。第二个常见原因是没有保持设备引用。异步加载期间如果设备被拔掉、probe失败或者模块被卸载回调可能永远不会执行或者回调里访问的设备指针已经失效。稳妥的写法是在probe里用 get_device 拿到设备引用回调里处理完再 put_device。模块卸载时要确保回调不会访问已释放的内存必要时用 completion 等待回调结束。第三个原因是回调上下文不适合做某些操作。request_firmware_nowait 的回调运行在内核工作队列上下文可以睡眠但不能持有自旋锁或者中断上下文才能用的资源。如果回调里要操作硬件寄存器先确认你的设备访问函数是不是依赖进程上下文。我之前踩过的一个坑是为了在回调里复用probe里的一个缓冲区直接把栈上地址传给了context结果回调执行的时候栈早没了。记住context 必须指向生命周期足够长的对象最好是设备私有数据结构千万别传局部变量地址。4.3 设备驱动probe阶段卡住或超时同步 request_firmware 在probe里调用最怕碰到“永远等不到结果”的情况。直接加载模式下如果文件不存在内核很快会返回错误不会卡。真正会卡的是开启了 sysfs fallback 的场景内核在 /sys/class/firmware 下创建节点等用户空间写入固件数据如果用户空间根本没有对应的udev规则或者脚本内核会一直等到超时默认60秒才报错。所以如果发现某个驱动加载要卡一分钟左右基本可以断定是触发了fallback等待。两种解法一是确保固件文件确实放在默认路径下让直接加载成功根本不给fallback机会。二是如果你非常确定不需要用户空间回退直接用 request_firmware_direct这样即使失败也立即返回错误不会卡在那等超时。另外在实际产品里很多驱动会故意把固件加载放在probe之外比如注册一个晚于probe的工作队列或者等网络/存储子系统就绪后再加载。这种设计虽然绕但能极大提高启动鲁棒性。我自己做产品时凡是启动早期就需要的非关键外设固件都倾向用 request_firmware_nowait让加载异步化避免拖慢系统启动。5. 一点个人体会做驱动时间长了我越来越觉得firmware这块属于“会者不难难者不会”的典型。它不像中断、并发那样需要大量理论铺垫更多是流程细节和工程习惯的问题。我个人的习惯是驱动里只要涉及到请求固件必定带上 MODULE_FIRMWARE每次发布驱动包都要附上固件文件清单部署文档里明确写出固件应该放到哪个目录、校验MD5值。这些看起来琐碎但在量产维护阶段能省下大量沟通成本。最后再分享一个小技巧调试固件加载问题时可以在内核启动参数里加了 firmware_class.path 之后配合 dmesg 里 grep 一下 firmware把加载路径和失败原因一次看全。有些发行版还会把 udev 固件规则打印到 systemd 日志里注意看的话能拿到比 dmesg 更多的线索。希望这篇能帮你把firmware的声明和加载理顺下次再遇到 -2 报错心里有底。
返回列表