Есть момент при загрузке системы, который большинство разработчиков никогда не видят напрямую. Ядро опознало устройство, драйвер готов взять его под управление, но прежде чем это произойдёт, нужно передать в железо двоичный блоб с микропрограммой. Без неё адаптер Wi-Fi молчит, видеокарта не показывает картинку, звуковой чип не издаёт ни звука. Именно этот тихий момент передачи данных обеспечивает firmware loading framework ядра Linux.
Многие устройства достаточно сложны, чтобы требовать собственную прошивку для полноценной работы. Часть из них ожидает предварительно запрограммированную постоянную память, подключённую напрямую. Но другие предоставляют механизм получения прошивки от программного обеспечения при каждой загрузке. Второй вариант дешевле в производстве и значительно удобнее при обновлениях. Именно под него ядро предоставляет стандартизированный API, позволяющий драйверу запросить файл прошивки, получить его содержимое в памяти и передать устройству без единого системного вызова из пространства пользователя.
Как устроена цепочка от файла до устройства
Прежде чем разбирать API, полезно понять, как ядро ищет файлы прошивок. Бинарные блобы хранятся в /lib/firmware/. Именно туда пакет linux-firmware раскладывает сотни файлов при установке. Некоторые производители именуют файлы по версиям: iwlwifi-9260-th-b0-jf-b0-46.ucode, другие используют фиксированные имена без версии. Порядок поиска фиксирован: сначала ядро проверяет прямой путь в файловой системе, затем путь из параметра модуля firmware_class.path, и только потом обращается к запасным механизмам.
Важный нюанс касается initramfs. Если драйвер скомпилирован прямо в ядро и запрашивает прошивку в методе probe(), корневая файловая система может быть ещё не смонтирована в тот момент. Для таких случаев прошивку включают непосредственно в образ initramfs или встраивают в само ядро через опцию CONFIG_EXTRA_FIRMWARE.
Проверить, какие прошивки были загружены на работающей системе, можно через dmesg:
dmesg | grep -i firmware
# Пример вывода:
# [ 12.860701] iwlwifi 0000:03:00.0: firmware: requesting iwlwifi-9260-th-b0-jf-b0-46.ucode
# [ 12.949384] iwlwifi 0000:03:00.0: loaded firmware version 46.5786ed88.0
Если прошивка не найдена, строка вывода меняется на сообщение об ошибке с кодом -2 (ENOENT). Это первое место, куда смотрят при отладке.
Синхронный и асинхронный варианты request_firmware
Центральная функция фреймворка называется request_firmware(). Она блокирует вызывающий поток до тех пор, пока прошивка не будет найдена или не вернётся ошибка. Сигнатура проста, но за ней скрыта серьёзная логика поиска:
int request_firmware(const struct firmware **fw, const char *name,
struct device *dev);
Параметр name задаёт имя файла прошивки, которое также используется как переменная $FIRMWARE в событиях udev. Параметр dev связывает запрос с конкретным устройством в модели устройств ядра. После успешного вызова указатель fw указывает на структуру с полями data и size, содержащими образ прошивки в памяти.
Типичный цикл работы с прошивкой выглядит так:
static int my_driver_probe(struct platform_device *pdev)
{
const struct firmware *fw;
int ret;
ret = request_firmware(&fw, "vendor/my_device.bin", &pdev->dev);
if (ret) {
dev_err(&pdev->dev, "Failed to load firmware: %d\n", ret);
return ret;
}
/* Передаём данные устройству */
ret = my_device_upload_firmware(pdev, fw->data, fw->size);
release_firmware(fw);
return ret;
}
Вызов release_firmware() в конце обязателен. Без него память, выделенная под образ прошивки, не освобождается. Это не просто утечка: при повторной загрузке модуля или перезапуске устройства проблема накапливается.
Синхронный вариант удобен, но несёт риск для встроенных драйверов: долгое ожидание прошивки в методе probe() увеличивает время загрузки всей системы. Асинхронный вариант request_firmware_nowait() снимает это ограничение. Драйвер передаёт колбэк-функцию и возвращает управление немедленно. Когда прошивка найдена или поиск завершился ошибкой, ядро вызывает переданный колбэк:
static void my_fw_callback(const struct firmware *fw, void *context)
{
struct my_device *dev = context;
if (!fw) {
dev_err(dev->dev, "Firmware not available\n");
return;
}
my_device_upload_firmware(dev, fw->data, fw->size);
release_firmware(fw);
}
/* В probe(): */
ret = request_firmware_nowait(
THIS_MODULE,
FW_ACTION_UEVENT, /* разрешить uevent-уведомления */
"vendor/my_device.bin",
&pdev->dev,
GFP_KERNEL,
my_device_context,
my_fw_callback
);
Флаг FW_ACTION_UEVENT разрешает ядру отправить uevent-уведомление в пространство пользователя. Это важно ещё по одной причине: именно флаг uevent управляет работой кэша прошивок при suspend/resume, о чём речь пойдёт дальше.
Специализированные варианты API под конкретные задачи
Помимо базовых request_firmware() и request_firmware_nowait(), фреймворк предоставляет несколько функций под специфические нужды. Каждая закрывает свой сценарий, и знание этих вариантов экономит время при разработке драйверов.
firmware_request_nowarn() работает идентично request_firmware(), но не печатает предупреждение в журнал ядра, если файл не найден. Это полезно для опциональных прошивок, отсутствие которых не является ошибкой. Хороший пример: дополнительные модули расширения функциональности, которые поставляются не во всех конфигурациях продукта.
request_firmware_into_buf() позволяет загрузить прошивку напрямую в заранее выделенный буфер, не выделяя память внутри фреймворка. Это особенно ценно для устройств с жёсткими требованиями к расположению данных в памяти, например, для DMA-передачи:
void *dma_buf = dma_alloc_coherent(dev, FW_SIZE, &dma_addr, GFP_KERNEL);
ret = request_firmware_into_buf(&fw, "vendor/dma_fw.bin",
dev, dma_buf, FW_SIZE);
/* fw->data теперь указывает на dma_buf */
firmware_request_platform() добавляет ещё один уровень поиска: если прямое обращение к файловой системе не дало результата, функция ищет встроенную копию прошивки в основной прошивке платформы, например в UEFI. Это актуально для интегрированных периферийных устройств, описание которых зашито в BIOS производителем.
Наконец, firmware_request_cache() решает специфическую задачу устройств, которые не нуждаются в повторной загрузке прошивки при перезагрузке системы, но требуют её наличия при выходе из suspend. Вместо полного запроса эта функция лишь помечает прошивку как нужную для кэширования, не выделяя память под образ.
Кэш прошивок и работа в цикле suspend/resume
Suspend и resume создают нетривиальную проблему. При пробуждении системы некоторые устройства требуют повторной загрузки прошивки. Но в момент resume корневая файловая система может быть ещё недоступна. Читать с диска нечего, а устройство уже требует прошивку.
Фреймворк решает это через механизм кэширования. Ядро запоминает все образы прошивок, запрошенных через uevent-совместимые вызовы API, и удерживает их в памяти на время сна системы. При пробуждении драйвер получает прошивку из кэша мгновенно, без обращения к диску.
Кэш контролируется опцией CONFIG_FW_CACHE. При этом важно понимать ограничение: кэш работает только для запросов, у которых активирован флаг uevent. Вызовы request_firmware_into_buf() кэш не поддерживают вовсе, потому что фреймворк в этом случае не владеет буфером с данными.
Чтобы проверить, активна ли опция кэша в текущей конфигурации ядра:
grep CONFIG_FW_CACHE /boot/config-$(uname -r)
# CONFIG_FW_CACHE=y
Для встроенных систем с ограниченным объёмом RAM кэш прошивок может оказаться расточительным. В таких случаях CONFIG_FW_CACHE отключают намеренно, но тогда драйвер обязан самостоятельно заботиться о повторной загрузке при resume через колбэк в блоке управления питанием.
Сжатые прошивки и встраивание блобов в ядро
Начиная с ядра 5.3, фреймворк поддерживает загрузку сжатых файлов прошивок. Это позволяет сократить занимаемое место в файловой системе и немного ускорить чтение с медленных носителей. Поддерживаются форматы xz и zstd (начиная с ядра 5.19).
Опции ядра, управляющие сжатыми прошивками:
Device Drivers --->
Generic Driver Options --->
Firmware loader --->
[*] Enable compressed firmware support
[*] Enable XZ-compressed firmware support
[*] Enable ZSTD-compressed firmware support
Для xz-сжатия есть важное ограничение: файл должен быть сжат без блочного контрольного суммирования или с типом проверки crc32. Стандартная команда упаковки:
xz -C crc32 -k iwlwifi-9260-th-b0-jf-b0-46.ucode
# Создаст файл iwlwifi-9260-th-b0-jf-b0-46.ucode.xz
Ядро ищет сначала несжатый файл, затем .xz, затем .zst. Это позволяет при необходимости заменить сжатую версию несжатой без изменения драйвера.
Второй вариант, актуальный для систем без initramfs и без корневой файловой системы в начале загрузки, - встраивание прошивок прямо в образ ядра. Опция CONFIG_EXTRA_FIRMWARE принимает список имён файлов через пробел, а CONFIG_EXTRA_FIRMWARE_DIR указывает на каталог, где их искать при сборке:
CONFIG_EXTRA_FIRMWARE="vendor/device_a.bin vendor/device_b.bin"
CONFIG_EXTRA_FIRMWARE_DIR="/lib/firmware"
Встроенные прошивки доступны ядру немедленно, ещё до монтирования любой файловой системы. Цена - увеличение размера образа ядра ровно на объём встроенных файлов.
Резервные механизмы и отладка через sysfs
Когда прямой поиск в файловой системе не находит нужный файл, фреймворк может активировать резервный механизм через sysfs. Ядро создаёт временное устройство и отправляет uevent в пространство пользователя. Предполагается, что пользовательский процесс отреагирует на это событие и загрузит образ прошивки через специальный интерфейс.
Исторически этим занимался udev, но поддержку загрузки прошивок из udev убрали ещё в 2014 году в systemd v217. Сегодня большинство дистрибутивов отключают CONFIG_FW_LOADER_USER_HELPER_FALLBACK, полагаясь исключительно на прямой поиск в файловой системе. Тем не менее интерфейс sysfs полезен для отладки и для нестандартных provisioning-сценариев в embedded:
# Ручная загрузка через sysfs (для отладки и provisioning)
MY_FW_DIR=/lib/firmware/
echo 1 > /sys/$DEVPATH/loading
cat $MY_FW_DIR/$FIRMWARE > /sys/$DEVPATH/data
echo 0 > /sys/$DEVPATH/loading
Переменные $DEVPATH и $FIRMWARE автоматически передаются в окружение uevent-скрипта. Запись 1 в атрибут loading открывает приём данных, запись 0 сигнализирует об успешном завершении. Запись -1 отменяет загрузку и немедленно возвращает ошибку в драйвер.
Таймаут ожидания ответа через sysfs настраивается в секундах:
cat /sys/class/firmware/timeout
# 60
echo 30 > /sys/class/firmware/timeout
Понимание всей этой цепочки, от структуры request_firmware() до кэша при suspend и резервного sysfs-интерфейса, отличает разработчика, способного написать надёжный драйвер для реального железа, от того, кто лишь копирует чужие примеры. Фреймворк загрузки микропрограмм выглядит скромно снаружи, но внутри держит на себе стабильную работу сотен драйверов в каждой Linux-системе мира.