[Из песочницы] Опыт создания сборок Linux под одноплатники с поддержкой обновлений
Введение
На данный момент, на рынке представлен большой ассортимент одноплатников на любой вкус по приемлемой цене.
Как правило, различные сборки от производителей, предназначены для оценки платформы и являются отправной точкой нового проекта, поэтому не всегда подходят под конкретные задачи. В задачах где требуется высокая надежность, перед разработчиком встает вопрос, как доработать дистрибутив и потом не поплатиться за это полной переработкой образа и системы обновления.
На просторах интернета почти нет информации о том какая должна быть релизная сборка и как реализовать ее обновление, поэтому разработчик вынужден придумывать «велосипед» или пользоваться собственными наработками, которые далеко не всегда проверены на 100%.
Поскольку я принимаю участие в разработке ПО для различных Linux устройств (моё портфолио можно нагуглить по слову develinux) и дополнительно являюсь автором проекта 11-parts, мне регулярно приходится сталкиваться не только созданием сборок, но и разработкой механизмов обновлений через WEB или USB flash.
В этой статье я хочу поделиться своим опытом и знаниями в соответствующих направлениях.
Требования к сборке
В процессе разработки сборок и обновлений под различные устройства я для себя выделил несколько требований:
- сборка не должна повреждаться при внезапном выключении питания;
- сборка должна быстро грузиться;
- загрузчик должен работать безотказно;
- сборка должна поддерживать обновление.
Эти требования я постараюсь подробнее изложить ниже, а после этого опишу 3 подхода в разбивке образов на разделы и их обновлениях.
Сборка не должна повреждаться при внезапном выключении питания
Кому нужно устройство, которое перестает работать после десятой перезагрузки? Никому! Если взять готовые дистрибутивы (а их очень мало для embedded), то без напильника из коробки они все весьма ненадежные в этом плане. Я очень хорошо запомнил проект, где использовал ubuntu под imx6, файловые системы на карте повреждались, иногда с десятой перезагрузки, иногда с сороковой, зависело от звезд на небе. Проект спасла ФС aufs. Дело в том, что ubuntu не предназначена для readonly, и всегда что-то должна писать. Похожую ситуацию помню в другом проекте, где использовалась yocto на SD карте. Вообще, так к сведению, SD карты это самый безобразный тип накопителя, который выходит из строя быстрее всех, гораздо надежнее emmc и nand. Если используется SD карта, то желательно на нее писать как можно реже во время работы, алгоритмы фонового переноса секторов очень непредсказуемы, я работал с десятками различных SD карт мировых брендов и не нашел ни одной карты, которую мог бы порекомендовать.
Но у SD карт есть несколько преимуществ, они доступны, недороги и удобны в отладке ПО.
К чему это я… А, вот к чему — корневая ФС должна быть readonly, никаких записей в нее быть не должно во время работы. Вы, наверно, задумаетесь: как так? Миллионы Андроид устройств всегда что-то пишут и не выходят из строя. Верно, но это все потому, что большинство устройств с Android, во-первых, имеют аккумулятор, а во-вторых, корневая ФС оформлена в виде ramdisk, а раздел system — readonly.
Если система должна быть надежной, то всякие рюшечки с установкой пакетов в корневую ФС могут многое подпортить. В качестве ФС рекомендую squashfs. Работает быстро, ничего писать не может, экономит место за счет сжатия…
А как же сохранение конфигов, загрузка файлов и т.д. спросите вы?
А вот для этого нужно создавать отдельные разделы RW. Если планируете писать в NAND, то рекомендую проверенный вариант — UBIFS. Если в NOR, то jffs2. Если писать на другой накопитель, то рекомендую ext4, raiserfs, btrfs, лучшей ФС среди них отметить не могу, т.к. с всеми были различные проблемы.
При этом всегда перед монтированием rw разделов, обязательно нужно проверять разделы на ошибки с помощью fsck подобных утилит.
Сборка должна быстро грузиться
Скорость загрузки устройства влияет на общий юзабилити. В некоторых задачах время загрузки не более 30 сек, в некоторых допустимо, 5 минут. Для себя я выработал время до 1 минуты, чем меньше тем лучше. Ждать загрузки более одной минуты слишком долго, может показаться что устройство зависло, поэтому если есть возможность сократить время, то лучше ее использовать.
Загрузчик должен работать безотказно
Загрузчик — это то, без чего не запустится сборка. В последнее время я часто наблюдаю за тем, как производители одноплатников облегчают разработку, выкладывая демо для SD карты с описанием как в нее прописать загрузчик или готовый образ с загрузчиком, который элементарно заливается командой dd. А что если SD карта зависнет? Такое же не редкость. Лично на моей практике карты частенько отваливались. Вот так работаешь за платой несколько часов, пишешь софт, бац и все… начинают сыпаться ошибки в ядре, карта отвалилась. А что, если это устройство, которое должно работать в полях без перезагрузки? И кстати, перезагрузка включая watchdog не всегда оживляют зависшую карту, у карты нет сигнала сброса, это не emmc, конечно же это больше вопрос к схемотехнике платы, если на плате стоит сброс питания карты, то это спасет, но это не везде. На некоторых платах помогает только передергивание питания или карты. Основываясь на своем опыте, я не рекомендую хранить загрузчик на накопителе с основной сборкой, если в процессе работы на накопитель производится запись. Если система на накопитель с загрузчиком ничего не пишет, а такое редко бывает, то пожалуйста. На моем опыте, в режиме readonly файловая система деформировалась только из-за аппаратных ошибок, но не программных.
Загрузчик следует хранить в надежном месте, на надежном накопителе, например в отдельной NOR или EEPROM микросхеме. Ниже пример модуля на базе чипа imx6ull, с SPI NOR для хранения загрузчика.
Сборка должна поддерживать обновление
Без обновления никуда… Я участвовал во многих проектах и никогда к сдаче работ не получалось идеального ПО. Всегда обнаруживается ошибка или требуется доработка функционала. Нужно понимать, пока люди пишут ПО они, будут ошибаться, пока люди пользуются устройством, они будут хотеть чуть большего. В 90% случаях, отсутствие продуманной системы обновления может привести как к головной боли производителя, так и к краху всего проекта. Например, разработана система видеонаблюдения для транспорта, система установлена по всей России и вдруг оказывается маркетологи недооценили рынок и не предусмотрели потоковое вещание, да к тому же еще и обнаружилось несколько ошибок в прошитом ПО, плюс потребитель начинает поглядывать в сторону конкурентов, потому что у них как раз есть то чего нет в купленном устройстве… Да, да, на чужом огороде клубника вкуснее и погода лучше (психология).
Что делать в такой ситуации? Если обновление поддерживается, то вариантов решений масса, ошибки можно исправить, потоковое вещание доработать, а функционал подстроить под потребителя, выдать потребителю прошивку с инструкцией и все. Но если оно не поддерживается, то производителя ждут большие приключения с командировками сервисных инженеров вплоть до замены устройств.
Система обновления в устройстве должна быть продумана до мелочей и протестирована на 100%. Одна ошибка в этой части превратит железо в кирпич, поэтому никаких допусков и исключений быть не должно.
Процесс обновления должен быть стоек к выключению питания, и ни при каких обстоятельствах не должен испортить устройство.
Обзор подходов в разбивке образов на разделы с учетом дальнейших обновлений
Из множества подходов могу порекомендовать 3 типа, которые лично реализовывал. Это не все подходы, их объем выходит за рамки этой статьи. Все 3 типа имеют недостатки и далеко не идеальны, но, как мне кажется, близки к золотой середине здравого смысла.
Подход №1
Самый простой и доступный способ:
На накопитель, например SD карту, кладется образ, который прошивается из u-boot во встроенный накопитель устройства, например NAND flash.
В u-boot для этого нужно подготовить скрипты.
Из плюсов — это самый простой тип обновления, разработка которого займет максимум 1 день.
Недостатки этого подхода в отсутствии визуализации процесса и в очень ограниченных возможностях загрузчика, т.е. никакую сложную логику стандартными средствами не наворотить, если конечно же не придумать свою команду в u-boot (но это уже другой тип обновления, язык C великая сила). Этот способ не предназначен для обновлений через WEB — обеспечить контроль целостности прошиваемого образа проблематично, в ряде случаев размер сборки не должен превышать размера ОЗУ.
Кроме того, в некоторых задачах требуется сохранять настройки при обновлении, а это, при таком подходе, реализовать не просто.
Подход №2
Самый надежный и защищенный способ из рассмотренных, но самый сложный. Рекомендую этот способ использовать в особо ответственных разработках, т.к. он защищает как от битых образов, так и от физического повреждения основного накопителя, поскольку в схеме используется дополнительный.
В подходе используется минимальная сборка (ramdisk размером 8–16Мб) и основная. Ramdisk — это сжатый архив, поэтому сборка на 16Мб физически будет в несколько раз меньше.
Цель минимальной сборки — оценить основную сборку и загрузить ее.
Ramdisk размещается вместе с ядром и скриптами u-boot в FIT image.
Зачем FIT image и что он дает? FIT image — это формат, который поддерживается u-boot. Он обеспечивает целостность всех составляющих (ядро, dts, ramdisk, скрипты). Распаковка FIT image осуществляется в u-boot, причем, если не сойдется контрольная сумма, u-boot откажется его грузить. Это удобно, т.е. не нужно самим заботиться о контроле целостности, не нужно записывать несколько файлов по отдельности или придумывать свои образы, все делает FIT image. Обычно FIT image занимает 7–20Мб, его следует записать на отдельный высоконадежный накопитель, например, в qspi nor flash. Основную сборку можно хранить в более дешевой и ненадежной памяти, например, NAND flash. Поскольку основная работа будет происходить в основной сборке, то именно она повредится первой. В этом случае на помощь придет отдельный накопитель с минимальной rootfs.
Процесс загрузки.
u-boot загружает скрипт, который пытается использовать FIT обновления (FIT2), а затем FIT заводской прошивки (FIT1).
Если FIT2 нет или нарушена его целостность, проверка fit завершится с ошибкой и u-boot загрузит первый FIT (FIT1). Если FIT обновления (FIT2) есть, и он не битый, то грузится его ramdisk который проверяет rootfs обновления (Rootfs2).
Если Rootfs2 битый, то скрипты удалят FIT обновления (FIT2), затем после перезагрузки будет загружен заводской образ состоящий из FIT (FIT1) и Rootfs1.
Процесс обновления.
Образ обновления содержит FIT, rootfs и различную информацию о сборке, включая контрольные суммы всех его составляющих. Информация о сборке используется в ходе обновления для контроля целостности и совместимости.
Ход обновления по шагам:
- проверка образа на совместимость с железом и ПО,
- проверка целостности образа в файле обновления,
- копирование Rootfs2 из файла обновления в заранее подготовленный раздел,
- проверка целостности скопированного образа в разделе,
- копирование FIT2 в соответствующий раздел,
- перезагрузка.
Если в процессе произойдет сбой, отсутствие или повреждение FIT2 не испортит систему, т.к. u-boot просто откажется его использовать и загрузит заводской образ. Поэтому в ходе обновления целостность FIT2 не проверяется.
После обновления, новая сборка будет размещена на основном накопителе в виде FIT2 и Rootfs2.
Данный способ стоек к механическим повреждениям накопителя и ошибкам ФС.
В случае критических неисправностей, запустится заводской образ, где сработает ПО восстановления, которое, например, может перепроверить NAND, загрузить прошивку из сети с использованием протокола SSH, а затем ее записать.
Я привел всего лишь пример восстановления, вариантов много. В этом подходе процессом восстановления рулит полноценный Linux, который может все…, а не загрузчик как в первом варианте.
Подход №3
Этот тип обновления используется почти во всех проектах 11-parts, поскольку очень неплохо себя зарекомендовал.
Обновление подойдет под любые размеры сборок, для любых типов накопителей. В отличие от предыдущего типа, здесь SPI NOR используется только для u-boot, поэтому имеет меньший размер и меньшую стоимость, 1 Мбайт хватит вполне.
Этот тип обновления не требует отдельной сборки ramdisk, а значит экономится программистское время на его разработку и поддержку в будущем.
В примере используется накопитель SD карта, но может быть и NAND с использованием UBIFS, без разницы. В этом подходе отсутствует проверка Rootfs RO перед загрузкой, если сборка повредится, то система так и не узнает что она повредилась и будет ее грузить по кругу. Здесь предполагается, что данные в разделе RO никаким образом не могут быть изменены, этот подход исключает физическую неисправность накопителя. Если накопитель физически не исправен, то устройство нужно нести в сервис центр, никакого самовосстановления не предусмотрено. Это та цена, которую приходится платить за увеличение скорости разработки, удешевление поддержки и удешевление элементной базы, но она оправдана. Зачем страховаться от того, что почти никогда не произойдет.
Логика загрузки и обновления такая же как в предыдущем подходе.
В случае загрузки вначале u-boot грузит FIT обновления (FIT2), если его нет или нарушена целостность, u-boot грузит первый FIT (FIT1), стартует сборка прошитая на заводе и так до тех пор пока не обновят систему. Когда систему обновят появиться FIT2 и Rootfs2. В этом случае при загрузке устройства первым стартует FIT обновления (FIT2). В скриптах u-boot, которые хранятся в каждом FIT, должно быть прописано какую rootfs монтировать.
Shared partition RW
На диаграммах везде присутствует блок shared partition, это группа разделов для записи. Любые записи производятся только туда. Shared partition изображен как один раздел для наглядности. На самом деле их 3: два маленьких для конфигов, работающие в зеркале, и один большой для всего остального. Дополнительно рекомендую при обновлении часть конфигов сохранять, что удобно, например, если вы настроите сеть и обновитесь, настройки сети перенастраивать не потребуется.
Подведем итог
В статье рассмотрены три вида сборок с поддержкой обновления, все проверены мной лично, можно их смело использовать в проектах.
На данный момент я использую только последние два, поскольку они больше всего подходят под требования. Для наглядности, вы можете ознакомиться с примерами устройств, где используются эти типы обновлений (подробности в портфолио 11-parts):
- повторитель RS485 через 4G/WiFi/LAN,
- плата управления контроллером промышленного дисплея 4K V-By-One,
- комплексная система управления климатом ангара,
- видео контроллер промышленного дисплея 2DisplayPort-LVDS,
- система контроля линий,
- VPN шлюз.
Если моя статья окажется полезной и интересной, я готов далее делится опытом и проверенными техническими решениями в области embedded linux на этой площадке.
Всем спасибо.
Горчаков Илья
telegram: develinux