大家好!我是大聪明-PLUS!
系统程序员迟早都会接触到固件的概念。本文将简要介绍固件的定义、用途以及使用方法。
什么是固件?
这个词通常被翻译为"固件"。世界上许多设备的核心都是微控制器或处理器。为了使设备能够执行其预期功能,必须使用与该微控制器或处理器相匹配的软件对其进行编程。这种软件就叫做"固件"。抱歉在这个网站上需要进行这样的解释,但任何现代微波炉、洗衣机或其他家用电器都包含微控制器,而微控制器又包含固件。
PC 或嵌入式系统(例如 Raspberry Pi)中包含的外围设备也可以基于微控制器构建,并且也必须有固件。笔记本电脑或智能手机中的 Wi-Fi/蓝牙模块就是此类设备的典型例子。此类模块的核心是微控制器,它负责无线电处理并通过 SDIO、PCIe、USB 或 UART 与主机通信。
然而,两者之间存在一个重要的区别:
-
家用电器无需连接任何主机即可独立运行,因此它们的固件存储在非易失性存储器中。每次设备开机时,固件都会立即执行,无需任何其他操作。这是基本要求,因为这些设备本身就具备自给自足的能力。试想一下,如果洗衣机需要连接电脑才能运行,恐怕没人会满意。
-
诸如个人电脑或智能手机等系统中的设备通常都内置了基础固件(本质上是一个引导加载程序),该固件存储在非易失性存储器中。而执行预期功能的主固件则位于主机的文件系统中。主机随后会将此固件加载到设备中(例如,Wi-Fi/蓝牙模块)。
我不知道为什么会选择这种方法,但我推测主要原因是为了方便。就像任何程序一样,固件也可能存在错误。考虑到微控制器开发的特殊性,固件出错几乎是必然的。因此,如果无需使用编程器或将设备置于固件更新模式,就能将固件更新加载到目标设备上,对所有人来说都更加方便。这种方法完全可以实现这一点------只需更新目标文件系统上的固件文件即可。换句话说,只需下载即可。
如何存储固件?
一般来说,有两种选择:
-
将二进制文件存储在设备的文件系统中。
-
将固件以二进制数据数组的形式存储在该设备的驱动程序代码中。
总的来说,第一种方案似乎足够便捷,无需另辟蹊径。但第二种方案依然存在,原因如下。驱动程序会不断地与它所设计连接的设备通信。因此,实现通信协议的代码同时存在于固件和驱动程序中是合乎逻辑的。而在这里,存在几种可能的实现方式。
/* Highest firmware API version supported */`
`#define IWL7260_UCODE_API_MAX 17`
`#define IWL7265_UCODE_API_MAX 17`
`#define IWL7265D_UCODE_API_MAX 29`
`#define IWL3168_UCODE_API_MAX 29`
`/* Lowest firmware API version supported */`
`#define IWL7260_UCODE_API_MIN 17`
`#define IWL7265_UCODE_API_MIN 17`
`#define IWL7265D_UCODE_API_MIN 22`
`#define IWL3168_UCODE_API_MIN 22
然而,更多时候,厂商并不在意这一点,也没有明确声明主机和设备之间使用的协议版本。只有"特定的驱动程序版本 + 特定的固件版本"的组合才能保证正常运行。在这种情况下,"将固件直接存储在驱动程序代码中"的方案就显得合情合理了。这消除了使用不兼容的固件和驱动程序版本的可能性。
#ifdef LOAD_FW_HEADER_FROM_DRIVER`
`#if (defined(CONFIG_AP_WOWLAN) || (DM_ODM_SUPPORT_TYPE & (ODM_AP)))`
`u8` `array_mp_8822b_fw_ap`[] `=` {
`0x22`, `0x88`, `0x00`, `0x00`, `0x1B`, `0x00`, `0x06`, `0x00`,
`0xFF`, `0xFF`, `0xFF`, `0xFF`, `0x00`, `0x00`, `0x00`, `0x00`,
`0x09`, `0x15`, `0x11`, `0x24`, `0xE2`, `0x07`, `0x00`, `0x00`,
...`
也有一些驱动程序同时使用这两种方法。例如,紫光展锐(Unisoc,又名展讯)在其 UWE5622 驱动程序中就采用了这种方法。Makefile 中包含一个宏定义,用于确定使用哪种方法。
#ifdef CONFIG_WCN_DOWNLOAD_FIRMWARE_FROM_HEX`
`/* Some customer (amlogic) download fw from /fw/wcnmodem.bin.hex */`
`WCN_INFO`(`"marlin %s from wcnmodem.bin.hex start!\n"`, `__func__`);
`mfirmware->data` `=` `firmware_hex_buf`;
`mfirmware->size` `=` `FIRMWARE_HEX_SIZE`;
`mfirmware->is_from_fs` `=` `0`;
`mfirmware->priv` `=` `firmware_hex_buf`;
`#else /* CONFIG_WCN_DOWNLOAD_FIRMWARE_FROM_HEX */`
`if` (`marlin_dev->is_btwf_in_sysfs` `!=` `1`) {
`/*`
`* If first power on, download from partition,`
`* else download from backup firmware.`
`*/`
`if` (`marlin_dev->first_power_on_flag` `==` `1`) {
`WCN_INFO`(`"%s from %s%s start!\n"`, `__func__`,
`wcn_fw_path`[`0`], `WCN_FW_NAME`);
`ret` `=` `request_firmware`(`&firmware`, `WCN_FW_NAME`, `NULL`);`
在这种情况下,该文件最初以二进制文件的形式位于驱动程序中,但在汇编过程中被转换为十六进制数组。
如何下载固件
如果我们忽略具体实现细节,那么我们需要:
-
将固件文件从文件系统加载到内存中;
-
解析其头部信息(例如,如果固件损坏或版本错误怎么办);
-
通过设备连接的接口向目标设备传输数据。
如果固件以数组形式存储在代码中,则可以跳到最后一步,文件读取问题不再重要。
简而言之,内核会发出一个调用request_firmware(),在指定的目录集合中搜索具有指定名称的文件。如果找到该文件,则将其内容读入内存并作为内存缓冲区使用。执行必要的操作后,可以通过调用相应的函数释放固件实例release_firmware()。虽然有很多封装层和其他机制简化了使用,但从概念上讲,这就是其底层原理。
这种方法的局限性在于固件必须位于预定义的位置之一。通常情况下,这些位置是[ [ /lib/firmware...
`open()`...`request_firmware()` 上文我们几乎都在讨论从驱动程序(即内核空间)加载固件。然而,这并非强制要求。许多设备的固件是由运行在用户空间的代码加载的。这些设备主要使用 UART 作为其主要通信端口。例如,Wi-Fi/蓝牙模块中的蓝牙部分就是如此(是的,同一个模块中,蓝牙和 Wi-Fi 模块通常使用不同的固件版本)。蓝牙最初的设计是让主机通过 UART 上的 HCI 协议与控制器通信,而这正是 Linux 内核中蓝牙驱动程序的平台无关部分(我指的是内核源代码中 net/bluetooth 目录下的部分)所依赖的。即使 Wi-Fi/蓝牙模块本身通过不同的端口(例如 SDIO)与主机通信,厂商驱动程序也会在 SDIO 上创建一个虚拟 UART。 ``` `config TTY_OVERY_SDIO tristate "Spard TTY Overy SDIO Driver" help Spard tty overy sdio driver` ``` 因此,在这种情况下,我们会自动避免任何内核冲突,可以像使用任何常规驱动程序一样操作固件文件。 ``` static` `int` `qualcomm_load_firmware`(`int` `fd`, `const` `char` `*firmware`, `const` `char` `*bdaddr_s`) { `int` `fw` `=` `open`(`firmware`, `O_RDONLY`); `fprintf`(`stdout`, `"Opening firmware file: %s\n"`, `firmware`); `FAILIF`(`fw` `<` `0`, `"Could not open firmware file %s: %s (%d).\n"`, `firmware`, `strerror`(`errno`), `errno`); `fprintf`(`stdout`, `"Uploading firmware...\n"`);` ``` 如果您是第一次接触 Linux 固件,以上就是您需要了解的全部内容。当然,这个话题还有很多其他方面,但这些都是需要进一步研究的特殊情况,通常在调试更具体的问题时才会变得有趣。因此,我们这里就不赘述了,但我们以后肯定会就其他主题进行探讨。