Что касается абсолютного большинства устройств, поддерживаемых в ядре Linux, от вас не требуется почти никакого вмешательства, чтобы пустить их в работу. Пожалуй, устройство потребуется включить (например, при помощи регулятора), отменить команду сброса или обратиться к нескольким конфигурационным регистрам. Но среди устройств есть и достаточно сложные; чтобы в полной мере использовать имеющиеся в них функции или добавлять собственные, для таких устройств требуется специализированная прошивка. Некоторые подобные устройства изначально рассчитаны на подключение заранее запрограммированной персистентной памяти, но в других предусматривается механизм, получающий им подтягивать прошивку из какого-либо приложения. Есть даже такие устройства, в которых одновременно предоставляются обе эти возможности.

Первый вариант, очевидно, проще в разработке: подключаем нужное железо, и с ним прошивка заработает. Но дополнительное оборудование — это издержки (которые зачастую оказываются ключевым фактором), тем более, если вы проектируете встраиваемое устройство. Более того, накатывать новые версии на такую подключённую память порой бывает сложнее, чем обновлять само устройство.

С другой стороны, перспектива программировать собственный инструмент только для того, чтобы загружать прошивку на конкретное устройство – дело не только утомительное, но и опасное. Придётся обращаться к аппаратному обеспечению из пользовательского пространства, вмешиваться в работу драйвера устройства. Причём, даже если производитель предоставляет загрузчик прошивки (такой инструмент редко бывает опенсорсным), то его будет непросто интегрировать с остальной системой, особенно с драйвером устройства. Эта проблема особенно актуальна при работе со встраиваемыми системами. Должен быть более удобный способ решить такую задачу.

К счастью, в ядре предоставляется API для интеграции обновлений прошивки в драйверы устройств. Он обеспечивает автоматизацию обновлений, резервные механизмы и даже управление через sysfs. Не требуется никаких гнусных трюков или проприетарных инструментов!

Примечание: эта тема значительно подробнее рассмотрена в официальной документации. Там разобраны многие другие прикладные аспекты (например, поддержка UEFI), но в этой информации можно утонуть, особенно, если вам ничего не известно о «магии», заложенной в эти механизмы. Ниже резюмированы ключевые моменты, помогающие бегло сориентироваться в данном механизме, а также предложен реальный пример с листингами, взятыми из основной ветки кода ядра.

1. Как это работает?

Базовый механизм очень прост:

1.1. Выборка

Сначала необходимо задать имя файла. Это может быть просто строка, жёстко прописанная в коде драйвера (в некоторых драйверах именно так и сделано), но есть и другие варианты. В нашем примере, как вы сами убедитесь, имя файла — это свойство в дереве устройств. Здесь сразу нарушается устоявшееся правило, согласно которому дерево устройств описывает именно аппаратное обеспечение, зато возрастает гибкость системы. Кстати, есть и другие случаи, в которых правила размываются, например, это касается повсеместно распространённого свойства wakeup-source. Но данная тема выходит за рамки этой статьи, а я от этого решения всё равно отказался, так что не будем на ней останавливаться. Честно говоря, я сразу планировал воспользоваться деревом устройств, потом жёстко прописать имя, но кто-то подсказал мне решение, когда я ещё не успел добавить код в вышестоящую ветку, хехе.

Эту операцию проделаем при помощи следующего вызова:

 request_firmware(&fw, name, dev);

где name — это строка с именем файла, а dev — устройство, для которого мы загружаем прошивку (напр., struct device *dev в драйвере, представляющем устройство, часто внедряется в структуру (struct), специфичную для данного драйвера).

А что такое fw? Это объект, за заполнение которого отвечает API (именно поэтому он передаётся по ссылке):

const struct firmware *fw;

// from include/linux/firmware.h:
struct firmware {
	size_t size;
	const u8 *data;

	* приватные поля загрузчика прошивки */
	void *priv;
};

Как видите, это очень простая структура, здесь нам приходится позаботиться только о самих данных (const u8 *data) и размере структуры (size_t size).

Не требуется указывать расположение файла, поскольку сам API определяет, где он должен быть сохранён:

  • fw_path_para – параметр модуля – по умолчанию пуст, так что игнорируем его

  • /lib/firmware/updates/UTS_RELEASE/

  • /lib/firmware/updates/

  • /lib/firmware/UTS_RELEASE/

  • /lib/firmware/

Разумеется, двоичный файл там требовалось положить заблаговременно, возможно, ещё на этапе сборки ваших rootfs. Если нет веской причины передавать путь как параметр, то часто бывают предпочтительны пути из /lib/firmware.

Если файл удалось найти в каком-то из этих местоположений (список отсортирован по приоритету, при первом же совпадении выборка прекращается), то в fw будут записаны двоичные данные и их размер в байтах. Эта информация понадобится нам, чтобы реализовать механизм обновлений.

Рискованно: также можно было бы включить прошивку в образ Linux (т.e. встроить). Здесь рассказано, как это делается, но будьте осторожны: конечно же, по причинам юридического характера нельзя так включать в образ проприетарную прошивку. Более того, такой подход может быть сопряжён и с некоторыми другими неудобствами — например, придётся пересобирать ядро под новую прошивку, но, может быть, вы из пересборки обойдётесь (если для вас приоритетна скорость разработки, либо вы хотите побыстрее обеспечить доступ к продукту).

1.2. Передача данных

Механизм обновлений очень сильно зависит от конкретных устройств, и API прошивки не может учитывать все нюансы для всех устройств. Напротив, реализовывать обновления вам придётся самостоятельно, следуя руководствам по применению устройств (device application notes). Отправлять прошивку на устройство потребуется по поддерживаемому протоколу (как правило, но не только, I2C и SPI). Не волнуйтесь, обычно механизмы обновления очень просты, не нужно быть семи пядей во лбу, чтобы их понять. Иногда бывает достаточно единственной команды, которая бы запустила пакетную передачу или простого цикла, в рамках которого было бы отправлено множество пакетов.

1.3. Очистка

Как только мы справимся с обновлением прошивки, fw нам больше не понадобится, для релиза данных хватит простого вызова:

release_firmware(fw);

2. Реальный пример: драйвет контроллера TPS6598x PD

Давайте опробуем всё это на практике! На сей раз поэкспериментируем с контроллером TPS65987 PD, драйвер которого рассматривается в моей предыдущей статье (в ней рассказано о данных, специфичных для конкретных устройств). Загружать прошивку для этого устройства требуется всякий раз, когда его включают, поэтому мы интегрируем механизм загрузки в зондирующую функцию драйвера.

Кстати, когда я писал ту статью, выяснилось, что извлечение данных из этого драйвера организовано не лучшим образом. Поэтому я отправил в вышестоящую ветку этот простой патч, чтобы упростить весь процесс. Как видите, писать статьи интересно, а иногда даже полезно . Кстати, я неоднократно контрибьютил в этот драйвер (добавил этот сигнал сброса и исправил несколько багов, например, вот этот, участвовал в портировании драйвера на стабильные ядра). У меня на отправку готово ещё несколько патчей, но в контексте этой статьи наиболее важно, что уже реализована передача прошивки для данного конкретного устройства.

Как мы уже знаем, механизм обновлений зависит от конкретных устройств, а из вышеупомянутой предыдущей статьи известно, как предоставлять в драйвере данные и функции, специфичные для конкретного устройства. Давайте, опираясь на эти глубокие знания, выполним те шаги, что были описаны в предыдущем разделе, но уже с реальным кодом ядра.

2.1. Выборка

Данная часть будет общей в случае с любым устройством, для поддержки которого служит драйвер. Вся разница может заключаться в имени файла, которое (как я упоминал выше) определяется в дереве устройств. Следовательно, можно предусмотреть единую обёртку, через которую мы наполняли бы структуру прошивки и в таком виде предоставлять данные для передачи на устройство. Эта обёртка просто читала бы имя файла и вызывала request_firmware(), как мы поступали ранее:

/*
 * Нас особенно не интересует объект tps, это объемлющая структура, в которой содержится 
 * struct device *dev.
 */
static int tps_request_firmware(struct tps6598x *tps, const struct firmware **fw)
{
	const char *firmware_name;
	int ret;

	// Функция для считывания прошивки из дерева устройств 
	ret = device_property_read_string(tps->dev, "firmware-name",
					  &firmware_name);
	if (ret)
		return ret;

	ret = request_firmware(fw, firmware_name, tps->dev);
	if (ret) {
		dev_err(tps->dev, "failed to retrieve \"%s\"\n", firmware_name);
		return ret;
	}

	// Здесь мы убедимся, что файл не пуст
	if ((*fw)->size == 0) {
		release_firmware(*fw);
		ret = -EINVAL;
	}

	return ret;
}

Именно в этот момент, если никаких ошибок не произошло (примеры ошибок: имя файла не найдено в дереве устройств, файл не найден или пуст) код fw готов к следующему этапу.

2.2 Передача данных

Эта часть зависит от конкретного устройства. В настоящее время драйвер tps6598x поддерживает обновления прошивки для двух разных устройств: tps25750 и tps6598x. Хотя, оба они обмениваются информацией по I2C, действующие в них механизмы обновления совершенно разные. Следовательно, чище всего было бы такое решение: добавлять функции обновления на каждое из устройств отдельно, так, чтобы эти функции могли работать с объектом fw и соответствовать уникальным требованиям. Здесь я покажу (актуальную в рамках данной статьи) часть реализации для tps6598x, так как именно в этом устройстве я разбираюсь лучше. После этого можете сами посмотреть реализацию tps25750, которая вас заинтересует в случае, если ваше устройство поддерживает пакетную передачу (т.е,. отправку всей прошивки как единого целого).

Чтобы tps6598x перешёл в пропатченный режим, требуется команда «start». Чтобы после завершения обновления перейти в нормальный режим эксплуатации, требуется команда «complete». В контексте передачи данных это не важно, поэтому я немного упрощу реальную функцию. Сосредоточимся на предмете этой статьи.

Для передачи данных используется «скачивающая» команда («PTCd»), принимающая до 64 байт данных. Размер прошивки для данного устройства обычно составляет около 16 KB – соответственно, придётся отправить несколько таких команд. Немного утомительно, но при работе с устройством всё равно нет никаких других вариантов, кроме как подключаться к флэш-памяти через SPI. Что ж, далее рассмотрим код цикла, в котором команда отправляет данные:

static int tps6598x_apply_patch(struct tps6598x *tps)
{
	// объявление переменной

	// обёртка с предыдущего шага
	ret = tps_request_firmware(tps, &fw, &firmware_name);
	if (ret)
		return ret;

	// "команда "start": если с ней всё нормально, то может начаться обновление 

	/* ------- САМОЕ ВАЖНОЕ (упрощенно) ------- */
	bytes_left = fw->size;
	while (bytes_left) {
		in_len = min(bytes_left, TPS_MAX_LEN);
		ret = exec_cmd(tps, "PTCd", in_len, fw->data + copied_bytes);
		if (ret) {
			dev_err(tps->dev, "Patch download failed (%d)\n", ret);
			goto release_fw;
		}
		copied_bytes += in_len;
		bytes_left -= in_len;
	}
	/* ------------------------------------------ */

	// команда "complete": если с ней всё хорошо, то обновление прошло успешно

	dev_info(tps->dev, "Firmware update succeeded\n");

	// релиз прошивки, следующий этап ;)

	return ret;
};

Алгоритм очень прост: отправляем fw->размер байт пакетами по 64 байта, взятыми из fw->данные, пока непереданных байт не останется. Если последний пакет окажется меньше 64 байт, отправляется всё, что осталось. Если ничего на отправку не осталось — что ж, вы свободны!

2.3. Очистка

Никаких трюков или фокусов, просто отправляем release_firmware(fw) и метку, чтобы можно было добраться до релиза (операция goto с предыдущего шага):

release_fw:
	release_firmware(fw);

Готово!

3. Что ещё нужно учитывать

Если у вас на примете есть проект, которому пошло бы на пользу взаимодействие с API прошивки ядра – очень внимательно почитайте спецификацию устройства и руководство по прикладному применению, а только затем можете задумываться о реализации. Дело не только в том, что механизм переноса прошивки может быть очень сложен (на самом деле, так только интереснее!), но и потому, что уже на уровне железа могут возникнуть столь серьёзные вызовы, что аппаратное обеспечение придётся проектировать заново. Типичные примеры такого рода — загрузочные последовательности и ножки управления загрузкой, при помощи которых обычно переходят в режим обновления (в tps6598x есть такие ножки).

Кроме того, всегда необходимо учитывать работу со временем. Если драйвер устройства пытается обратиться к файлу, находящемуся во всё ещё не доступной файловой системе, то эта операция не удастся. Почему такое вообще может произойти? Если драйвер начнёт работу до того, как прошивка примонтируется к файловой системе, то, вероятно, вы испытаете это сами. В таких случаях можно либо включить файл с прошивкой прямо в initramfs, либо собрать драйвер устройства как модуль и сохранить его в той же файловой системе. Также можно предусмотреть резервный механизм, который обеспечивал бы срабатывание при помощи sysfs. Существует также дополнительный API под названием Firmware Upload API, чтобы полнее контролировать обновления прошивки через sysfs и отслеживать статус работы. Сам я этим API никогда не пользовался, но, по-видимому, с его помощью вполне можно автоматизировать обновления прошивки из пользовательского пространства. Как я упоминал выше, функционал этого API гораздо шире, чем возможно рассмотреть в одной статье.

Но из того, что приходится учитывать, далеко не все вещи такие неприятные или опасные. API очень хорошо документирован, и в нём могут найтись другие функции, которые подойдут вам гораздо лучше, чем простая функция request_firmware(), рассмотренная в этой статье. Существует несколько альтернативных способов запрашивать прошивку, рассчитанных на другие случаи, все они документированы здесь. Вероятно, вы найдёте более точное решение (может быть, асинхронную версию?). Кроме того, найдётся множество примеров, в которых рассмотрены другие подсистемы ядра (например, для работы с bluetooth или сетевыми/беспроводными системами), которые послужат вам вдохновением.


Читайте также:

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud - в нашем Telegram-канале 

Перейти ↩

Комментарии (4)