Embedded Linux для начинающих (Часть первая)
- понедельник, 7 июля 2025 г. в 00:00:11
Однажды по работе мне прилетела задача по сборке и запуску Linux на одноплатном ПК. Тогда я, будучи разработчиком ПО для микроконтроллеров, встал в небольшой ступор — задачка явно не решалась установкой IDE и нажатием в ней кнопки «Собрать проект». Гугл помог узнать о том, что существует некий Buildroot. В материалах по теме всё выглядело довольно просто: скачай, настрой, дерни пару команд, загрузи результат на одноплатник — и можно запускать! Получается, процесс не многим сложнее установки дистрибутива Linux или Windows на обычный ПК? Конечно же, нет. Ведь если у тебя в руках кастомный одноплатник неизвестного китайского бренда, а не BeagleBone или Raspberry Pi, то зарыться в Buildroot придётся с головой...
Информации в сети много, но вся она разрозненная — и в основном на английском. Чтобы понять, что за что отвечает и где что настраивается, приходится перелопачивать тонны материалов. И далеко не факт, что после этого в голове всё сложится в понятную картину.
Так что, пройдя долгий и тернистый путь накопления знаний, их систематизации и применения на практике, я решил создать этот цикл статей. В нём я постараюсь максимально понятно и подробно рассказать о том, что такое Embedded Linux в целом и Buildroot в частности.
Я буду описывать усреднённый сценарий работы System on Chip (SoC) с Embedded Linux (EL), чтобы не погружаться в детали, характерные для отдельных чипов. А поскольку статьи рассчитаны на новичков, чей уровень владения Linux мне заранее неизвестен, некоторые моменты буду объяснять, возможно, слишком подробно.
Если описывать максимально просто, SoC — это микросхема, содержащая в себе процессор, интерфейсы и аппаратные блоки. У тех, кто знаком с микроконтроллерами, закономерно возникнет вопрос: «А в чём тогда разница? Разве микроконтроллер — не то же самое?»
И действительно, любой микроконтроллер тоже содержит в себе всё перечисленное. Но главное отличие не столько в железе, сколько в подходе к программированию и уровне абстракции, с которым работает разработчик. Чтобы понять, где заканчивается микроконтроллер и начинается SoC, давайте сравним три основных подхода к созданию встроенных решений — от простейших к более сложным:
Микроконтроллер без ОС
Это самое дешёвое и простое решение, идеально подходящее для узкоспециализированных задач — например, управления сервоприводом или считывания данных с датчика. Такие системы отличаются очень низким энергопотреблением и высокой скоростью отклика.
Программирование таких микроконтроллеров — это почти всегда «низкий уровень»: прямой доступ к регистрам, минимальные обёртки, если таковые вообще есть. Иногда единственное, что у вас есть в распоряжении — это заголовочный .h
файл с описанием регистров и datasheet. Каждая конкретная модель микроконтроллера имеет свой набор периферии и регистров, поэтому код, как правило, сильно платформозависим.
Исключением здесь можно назвать, например, HAL от STM, который позволяет относительно безболезненно переносить код с чипа на чип в рамках линейки STM32.
Микроконтроллер с RTOS
Следующий шаг — это использование ОС реального времени (RTOS), таких как FreeRTOS или SYS-BIOS. Здесь появляется многозадачность (не путать с многопоточностью), драйверы и абстракции над железом.
Такая система может выполнять «тяжелые» задачи с меньшим количеством накладных расходов для разработчика, ведь наличие ОС позволяет разрабатывать куда более сложный, но в тоже время гибкий, код. Например, разбить взаимодействия с датчиками на разные задачи, которыми будет управлять планировщик задач.
Код становится менее зависимым от конкретной платформы — большую часть можно повторно использовать на разных микроконтроллерах, если они поддерживаются данной RTOS. Однако ограничения всё ещё есть: вы по-прежнему работаете в условиях ограниченных ресурсов, часто с ограниченным пространством памяти и без полноценной файловой системы.
System on Chip
На этом уровне начинается настоящая «взрослая» жизнь: многоядерные процессоры, мегабайты оперативной памяти, гигабайты хранилища, аппаратные модули обработки видео, различные интерфейсы, такие как HDMI, Bluetooth и USB — и всё это в одном чипе.
Со стороны, SoC может выглядеть как просто «мощный микроконтроллер», но на практике это уже полноценный компьютер, на который можно поставить Embedded Linux.
С этого момента разработчик работает с регистрами не напрямую, а через ядро Linux, драйверы, DeviceTree-файлы и огромный стек инструментов — от компилятора до менеджера пакетов.
Что особенно важно — код становится максимально переносимым. Всё, что не связано напрямую с конкретным чипом (например, управление GPIO) может быть одинаковым на разных SoC. Аппаратные различия абстрагируются на уровне ядра Linux и DeviceTree.
Непереносимой частью обычно остаётся SDK от производителя — набор специфичных библиотек для работы с уникальными аппаратными блоками конкретного SoC.
Стоит уточнить, что речь идёт про те SoC, которые рассчитаны на работу с полноценной ОС. Существуют и более простые, например ESP32, которые ближе по духу к микроконтроллерам и нередко работают без Linux.
Таким образом SoC — это не просто «продвинутый микроконтроллер», а архитектурно совсем другой класс устройств. Это решение для задач, где нужны ресурсы, многопоточность, полноценная ОС и комплексная обработка данных.
Главное отличие — в уровне программного обеспечения, которое идёт «в комплекте» с железом.
Если микроконтроллер — это устройство, где программист управляет железом напрямую, то SoC — это уже платформа, где программист работает через полноценную ОС и драйверную модель.
EL — это не конкретный дистрибутив и даже не продукт. Это целая концепция: идея использовать ядро Linux и связанный с ним стек встраиваемого программного обеспечения для управления устройствами, отличными от классических компьютеров.
Обычно EL включает в себя:
Загрузчик
Ядро Linux
Корневую файловую систему
Утилиты, библиотеки, службы
На выходе мы получаем самостоятельную операционную систему, но собранную строго под нужды конкретного устройства — будь то маршрутизатор, промышленный контроллер или, например, умная колонка.
Возникает логичный вопрос: а зачем что-то собирать вручную, если можно взять готовый дистрибутив вроде Debian или Ubuntu?
Вот ключевые отличия:
Поддержка аппаратной платформы
Не под каждый SoC существует готовый образ Linux. Даже если вы найдёте нечто совместимое, это вовсе не означает, что оно запустится «из коробки» или будет работать корректно с вашим железом.
Аппаратные возможности могут быть не раскрыты
У многих SoC есть специфические аппаратные блоки: видеоускорители, нейронные сопроцессоры, модули аппаратного шифрования и прочее. В стандартных дистрибутивах такие вещи чаще всего просто не активированы, либо вовсе не поддерживаются.
Избыточность и ресурсоёмкость
Настольные дистрибутивы содержат огромное количество фоновых служб, универсальных драйверов и библиотек. Всё это потребляет ресурсы — память, процессорное время и энергию. А у встраиваемых решений ресурсы почти всегда ограничены. EL позволяет убрать всё лишнее, оставив только то, что действительно нужно.
Таким образом, Embedded Linux — это тот же самый Linux, но не в виде универсального дистрибутива, а в виде конструктора, собранного под конкретную задачу. Это система, в которой под контролем оказывается всё: от ядра до последнего скрипта автозапуска. Она создаётся под конкретное устройство, работает эффективно, стабильно и выполняет ровно ту работу, для которой задумывалась — ничего лишнего.
Тем не менее, это не значит, что все компоненты EL аналогичны компонентам, используемым в классических ПК. Например, в обычной системе загрузка идёт через BIOS и GRUB, а в SoC— через набор специализированных загрузчиков. Так что, чтобы разобраться, как устроена такая система и что в ней собирать, давайте посмотрим, из каких компонентов она состоит и как происходит процесс загрузки.
Пожалуй, продолжу традицию сравнений — так разница становится особенно наглядной.
Начнём с привычного многим процесса загрузки ПК:
После подачи питания запускается базовая прошивка — BIOS (или UEFI на современных платформах). Она проверяет наличие и работоспособность основных компонентов: процессора, оперативной памяти и т.д. Далее происходит опрос устройств, подключённых к материнской плате. Если на одном из них обнаружен загрузочный сектор, это устройство попадает в очередь загрузки.
BIOS (или UEFI) копирует загрузочный сектор (например, MBR) с первого подходящего устройства в оперативную память и передаёт управление коду, содержащемуся в этом секторе. Этот код называется загрузчиком первого этапа.
Этот загрузчик, зная, где находится его продолжение (обычно в /boot
), загружает основной загрузчик — чаще всего это GRUB. GRUB отображает меню (если задано) и передаёт управление ядру Linux (Kernel), указанному в конфигурации.
Ядро монтирует корневую файловую систему (RootFS), указанную в параметрах загрузки, и запускает процесс init.
Всё, что происходит от этого момента до появления экрана входа в систему, зависит от конкретного дистрибутива и настроек: может выполняться настройка сети, запуск графического окружения и прочие задачи.
Теперь взглянем на загрузку встраиваемой системы на базе SoC:
После подачи питания стартует Primary Program Loader (PPL) — загрузчик, находящийся в ROM. Он инициализирует базовую периферию, например, Static RAM (SRAM), и начинает поиск следующего этапа — Secondary Program Loader (SPL). Если он найден, его код выгружается в SRAM, и PPL передаёт ему управление.
У загрузчика первого уровня нет конкретного термина. Он и Boot ROM и ROM code и internal bootloader и PPL. Но далее я буду использовать термин PPL
SPL инициализирует более сложную периферию, в частности, RAM. Далее он ищет следующий этап — Third Program Loader (TPL) или сразу Kernel. Найденный код загружается в RAM, управление передаётся ему.
TPL (если он есть) проводит дополнительную инициализацию оборудования и ищет ядро. Обнаружив его, загружает в RAM и передаёт управление.
Kernel перенастраивает периферию согласно Device Tree, монтирует RootFS и запускает процесс Init. Дальше, наличие отличий от обычного ПК зависит от того, что именно содержится в RootFS и Init.
Необходимо отметить, что каждый этап не вызывает следующий, а именно передает управление. Вернуться назад невозможно — только перезапустить всё устройство и пройти путь заново.
На первый взгляд, процессы загрузки похожи. Да и выглядят одинаково запутанно. Отчасти, так и есть. Но!
В ПК множество задач решают микроконтроллеры: например, память и шины инициализируются «сами» — BIOS лишь координирует их работу. Каждый узел системы автономен и содержит собственную прошивку. Загрузка ОС в этом случае — скорее организация взаимодействия между независимыми компонентами, чем ручное включение каждого.
В SoC всё иначе. Это монолитный чип без отдельных контроллеров или лишнего пространства — всё должно быть максимально компактным, надёжным и понятным. Задача пробудить систему полностью ложится на её плечи. Поэтому и нужен каскад загрузчиков, каждый из которых решает свою задачу.
PPL — минимальный, максимально надёжный и неизменяемый код в ROM. Он выполняет лишь базовую инициализацию и запускает следующий этап. Его компактность делает его легко проверяемым и надёжным. Повредить его — значит убить всю систему: чип станет кирпичом.
SPL — получает чуть больше возможностей: в частности, может инициализировать RAM. Но SRAM слишком мала, чтобы вместить сложную логику, и потому SPL обычно нужен только как мост к следующему шагу.
TPL — уже работает в полноценной RAM и может позволить себе роскошь сценариев: например, откуда загружать ОС, как отобразить процесс загрузки, как реагировать на подключённые устройства и т.д.
Такая архитектура не усложняет — она защищает. Если сбой произошёл на уровне SPL или TPL, загрузку можно перехватить, загрузиться с резервного носителя и восстановить систему. Главное — предусмотреть такую возможность. А вот сбой в PPL означает полный отказ устройства. Поэтому он неизменяем и максимально прост.
Итак, мы узнали, с чем нам предстоит работать. Предлагаю рассмотреть каждый компонент подробнее.
Для загрузки операционной системы на SoC чаще всего используется U-Boot — мощный и универсальный загрузчик с открытым исходным кодом. Именно с ним мы и будем работать. Вот его преимущества:
Поддерживает множество архитектур и SoC-платформ
Позволяет загружать ядро и RootFS с самых разных источников: USB, SD-карты, SATA-диска, по сети, из NAND/NOR-памяти
Может выполнять сценарии: например, пробовать сначала загрузку с USB, затем с eMMC и только потом — по сети
Поддерживает интерактивный режим (через консоль) и авто-загрузку по таймеру
Имеет гибкую конфигурацию через переменные среды
Скачать U-Boot можно с официального репозитория.
Если SPL не помещается в SRAM, его можно «разбить» на части — подробнее об этом было в разделе про загрузку SoC
Это и есть сам Linux — ядро, которое управляет железом, абстрагирует ресурсы и обеспечивает взаимодействие между программами.
Скачать ядро можно с официального репозитория или с форков, адаптированных под конкретный SoC. Например, для Rockchip-процессоров есть репозиторий linux-rockchip от проекта Armbian, содержащий патчи, Device Tree-файлы и конфигурации под различные платы.
RootFS — это не просто набор каталогов. Это полноценное окружение, в котором есть:
Утилиты вроде init, sh, ifconfig
Конфигурационные файлы
Зависимости для запуска программ
В отличие от ядра и загрузчика, RootFS не собирается как бинарник — он создаётся как структура каталогов. Вы можете создать RootFS вручную (например, скопировав нужные бинарники и библиотеки из хост-системы), но это требует аккуратности и глубокого понимания устройства системы.
Итак, «представителей» рассмотрели. Возможно, кто-то даже полез смотреть исходники. Но не советую спешить, ведь нам осталось разобраться со средствами и инструментами сборки.
Toolchain — набор компиляторов, заголовков и утилит, ориентированных на целевую архитектуру, а именно:
Кросс-компиляторы gcc, g++, ld, as — всё необходимое для сборки кода
Заголовочные файлы ядра Linux (Linux kernel headers)
Библиотеки и заголовки libc, libstdc++, libm и другие системные библиотеки
Отладочные утилиты, анализаторы, системные хелперы
Главное требование к тулчейну — соответствие архитектурам Host (Где происходит компиляция) и Target (Где будет запускаться код) систем.
Скачать можно уже готовый, а можно собирать самому (Например, при помощи CrossTool-NG). Второй вариант заслуживает отдельного цикла статей, поэтому представим, что мы можем использовать только готовые решения.
Их можно скачать с сайта-производителя вашего устройства или сторонних сайтов производителей тулчейнов (Например, Linaro).
Ну, еще можно собрать самому...
Стоп, разве я не сказал, что мы так делать не будем? Скажем так, я слукавил. Собирать и настраивать тулчейн мы будем, но не сами, а попросим об этом инструмент сборки.
Чтобы не собирать всё вручную, обычно используют специальные сборочные системы. Наиболее популярны две: Buildroot и Yocto Project.
Yocto Project
Yocto — это мощная и гибкая среда сборки Embedded Linux. Позволяет:
Собирать ядро, загрузчик, RootFS и тулчейн
Включать в систему поддержку пакетных менеджеров (opkg, rpm, dpkg)
Точно управлять версионностью, патчами и конфигурацией
Однако у Yocto довольно высокий порог вхождения. Он использует собственную систему рецептов (bitbake), множество слоёв и понятий. Взамен вы получаете максимальную гибкость и контроль.
Buildroot
Buildroot проще: это набор Makefile, Kconfig и bash-скриптов, собирают минимальную, но готовую к работе систему Linux. Как и Yocto, позволяет собрать ядро, загрузчик, RootFS и тулчейн. Из плюсов:
Низкий порог вхождения
Простой интерфейс (menuconfig)
Защищенная от изменений в Runtime система — RootFS по умолчанию read-only, встроить пакетный менеджер без танцев с бубном возможно, но крайне сложно и не рекомендуется.
Именно Buildroot мы будем использовать в этом руководстве. Его можно скачать с официального репозитория.
На этом вводную часть можно закончить и перейти от теории к практике.
Итак, мы прошлись по основным терминам, узнали, что такое SoC, EL, как он загружается, чем отличается от обычного дистрибутива, из каких компонентов состоит и чем все это собирать.
В следующих статьях мы настроим рабочее окружение при помощи Docker, углубимся в структуру каждого компонента, научимся их настраивать и собирать. Разберем способы решения ошибок во время сборки и попробуем запустить собранный образ.
На этом первая статья из цикла подошла к концу. Спасибо за уделенное время! Еще увидимся!