Pull to refresh

Embedded Linux для начинающих (Часть первая)

Level of difficultyEasy
Reading time10 min
Views14K

Однажды по работе мне прилетела задача по сборке и запуску Linux на одноплатном ПК. Тогда я, будучи разработчиком ПО для микроконтроллеров, встал в небольшой ступор — задачка явно не решалась установкой IDE и нажатием в ней кнопки «Собрать проект». Гугл помог узнать о том, что существует некий Buildroot. В материалах по теме всё выглядело довольно просто: скачай, настрой, дерни пару команд, загрузи результат на одноплатник — и можно запускать! Получается, процесс не многим сложнее установки дистрибутива Linux или Windows на обычный ПК? Конечно же, нет. Ведь если у тебя в руках кастомный одноплатник неизвестного китайского бренда, а не BeagleBone или Raspberry Pi, то зарыться в Buildroot придётся с головой...

Информации в сети много, но вся она разрозненная — и в основном на английском. Чтобы понять, что за что отвечает и где что настраивается, приходится перелопачивать тонны материалов. И далеко не факт, что после этого в голове всё сложится в понятную картину.

Так что, пройдя долгий и тернистый путь накопления знаний, их систематизации и применения на практике, я решил создать этот цикл статей. В нём я постараюсь максимально понятно и подробно рассказать о том, что такое Embedded Linux в целом и Buildroot в частности.

Я буду описывать усреднённый сценарий работы System on Chip (SoC) с Embedded Linux (EL), чтобы не погружаться в детали, характерные для отдельных чипов. А поскольку статьи рассчитаны на новичков, чей уровень владения Linux мне заранее неизвестен, некоторые моменты буду объяснять, возможно, слишком подробно.

Оглавление

1. Введение

1.1. Что такое System on Chip?

Если описывать максимально просто, SoC — это микросхема, содержащая в себе процессор, интерфейсы и аппаратные блоки. У тех, кто знаком с микроконтроллерами, закономерно возникнет вопрос: «А в чём тогда разница? Разве микроконтроллер — не то же самое?»

И действительно, любой микроконтроллер тоже содержит в себе всё перечисленное. Но главное отличие не столько в железе, сколько в подходе к программированию и уровне абстракции, с которым работает разработчик. Чтобы понять, где заканчивается микроконтроллер и начинается SoC, давайте сравним три основных подхода к созданию встроенных решений — от простейших к более сложным:

  1. Микроконтроллер без ОС

    Это самое дешёвое и простое решение, идеально подходящее для узкоспециализированных задач — например, управления сервоприводом или считывания данных с датчика. Такие системы отличаются очень низким энергопотреблением и высокой скоростью отклика.

    Программирование таких микроконтроллеров — это почти всегда «низкий уровень»: прямой доступ к регистрам, минимальные обёртки, если таковые вообще есть. Иногда единственное, что у вас есть в распоряжении — это заголовочный .h файл с описанием регистров и datasheet. Каждая конкретная модель микроконтроллера имеет свой набор периферии и регистров, поэтому код, как правило, сильно платформозависим.

    Исключением здесь можно назвать, например, HAL от STM, который позволяет относительно безболезненно переносить код с чипа на чип в рамках линейки STM32.

  2. Микроконтроллер с RTOS

    Следующий шаг — это использование ОС реального времени (RTOS), таких как FreeRTOS или SYS-BIOS. Здесь появляется многозадачность (не путать с многопоточностью), драйверы и абстракции над железом.

    Такая система может выполнять «тяжелые» задачи с меньшим количеством накладных расходов для разработчика, ведь наличие ОС позволяет разрабатывать куда более сложный, но в тоже время гибкий, код. Например, разбить взаимодействия с датчиками на разные задачи, которыми будет управлять планировщик задач.

    Код становится менее зависимым от конкретной платформы — большую часть можно повторно использовать на разных микроконтроллерах, если они поддерживаются данной RTOS. Однако ограничения всё ещё есть: вы по-прежнему работаете в условиях ограниченных ресурсов, часто с ограниченным пространством памяти и без полноценной файловой системы.

  3. System on Chip

    На этом уровне начинается настоящая «взрослая» жизнь: многоядерные процессоры, мегабайты оперативной памяти, гигабайты хранилища, аппаратные модули обработки видео, различные интерфейсы, такие как HDMI, Bluetooth и USB — и всё это в одном чипе.

    Со стороны, SoC может выглядеть как просто «мощный микроконтроллер», но на практике это уже полноценный компьютер, на который можно поставить Embedded Linux.

    С этого момента разработчик работает с регистрами не напрямую, а через ядро Linux, драйверы, DeviceTree-файлы и огромный стек инструментов — от компилятора до менеджера пакетов.

    Что особенно важно — код становится максимально переносимым. Всё, что не связано напрямую с конкретным чипом (например, управление GPIO) может быть одинаковым на разных SoC. Аппаратные различия абстрагируются на уровне ядра Linux и DeviceTree.

    Непереносимой частью обычно остаётся SDK от производителя — набор специфичных библиотек для работы с уникальными аппаратными блоками конкретного SoC.

    Стоит уточнить, что речь идёт про те SoC, которые рассчитаны на работу с полноценной ОС. Существуют и более простые, например ESP32, которые ближе по духу к микроконтроллерам и нередко работают без Linux.

Таким образом SoC — это не просто «продвинутый микроконтроллер», а архитектурно совсем другой класс устройств. Это решение для задач, где нужны ресурсы, многопоточность, полноценная ОС и комплексная обработка данных.

Главное отличие — в уровне программного обеспечения, которое идёт «в комплекте» с железом.

Если микроконтроллер — это устройство, где программист управляет железом напрямую, то SoC — это уже платформа, где программист работает через полноценную ОС и драйверную модель.

1.2. Что такое Embedded Linux?

EL — это не конкретный дистрибутив и даже не продукт. Это целая концепция: идея использовать ядро Linux и связанный с ним стек встраиваемого программного обеспечения для управления устройствами, отличными от классических компьютеров.

Обычно EL включает в себя:

  • Загрузчик

  • Ядро Linux

  • Корневую файловую систему

  • Утилиты, библиотеки, службы

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

Возникает логичный вопрос: а зачем что-то собирать вручную, если можно взять готовый дистрибутив вроде Debian или Ubuntu?

Вот ключевые отличия:

  1. Поддержка аппаратной платформы

    Не под каждый SoC существует готовый образ Linux. Даже если вы найдёте нечто совместимое, это вовсе не означает, что оно запустится «из коробки» или будет работать корректно с вашим железом.

  2. Аппаратные возможности могут быть не раскрыты

    У многих SoC есть специфические аппаратные блоки: видеоускорители, нейронные сопроцессоры, модули аппаратного шифрования и прочее. В стандартных дистрибутивах такие вещи чаще всего просто не активированы, либо вовсе не поддерживаются.

  3. Избыточность и ресурсоёмкость

    Настольные дистрибутивы содержат огромное количество фоновых служб, универсальных драйверов и библиотек. Всё это потребляет ресурсы — память, процессорное время и энергию. А у встраиваемых решений ресурсы почти всегда ограничены. EL позволяет убрать всё лишнее, оставив только то, что действительно нужно.

Таким образом, Embedded Linux — это тот же самый Linux, но не в виде универсального дистрибутива, а в виде конструктора, собранного под конкретную задачу. Это система, в которой под контролем оказывается всё: от ядра до последнего скрипта автозапуска. Она создаётся под конкретное устройство, работает эффективно, стабильно и выполняет ровно ту работу, для которой задумывалась — ничего лишнего.

Тем не менее, это не значит, что все компоненты EL аналогичны компонентам, используемым в классических ПК. Например, в обычной системе загрузка идёт через BIOS и GRUB, а в SoC— через набор специализированных загрузчиков. Так что, чтобы разобраться, как устроена такая система и что в ней собирать, давайте посмотрим, из каких компонентов она состоит и как происходит процесс загрузки.

2. Как устроен Embedded Linux изнутри?

2.1. Процесс загрузки от включения до входа в систему

Пожалуй, продолжу традицию сравнений — так разница становится особенно наглядной.

2.1.1. Процесс загрузки ПК

Начнём с привычного многим процесса загрузки ПК:

  1. После подачи питания запускается базовая прошивка — BIOS (или UEFI на современных платформах). Она проверяет наличие и работоспособность основных компонентов: процессора, оперативной памяти и т.д. Далее происходит опрос устройств, подключённых к материнской плате. Если на одном из них обнаружен загрузочный сектор, это устройство попадает в очередь загрузки.

  2. BIOS (или UEFI) копирует загрузочный сектор (например, MBR) с первого подходящего устройства в оперативную память и передаёт управление коду, содержащемуся в этом секторе. Этот код называется загрузчиком первого этапа.

  3. Этот загрузчик, зная, где находится его продолжение (обычно в /boot), загружает основной загрузчик — чаще всего это GRUB. GRUB отображает меню (если задано) и передаёт управление ядру Linux (Kernel), указанному в конфигурации.

  4. Ядро монтирует корневую файловую систему (RootFS), указанную в параметрах загрузки, и запускает процесс init.

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

2.1.2. Процесс загрузки SoC

Теперь взглянем на загрузку встраиваемой системы на базе SoC:

  1. После подачи питания стартует Primary Program Loader (PPL) — загрузчик, находящийся в ROM. Он инициализирует базовую периферию, например, Static RAM (SRAM), и начинает поиск следующего этапа — Secondary Program Loader (SPL). Если он найден, его код выгружается в SRAM, и PPL передаёт ему управление.

    У загрузчика первого уровня нет конкретного термина. Он и Boot ROM и ROM code и internal bootloader и PPL. Но далее я буду использовать термин PPL

  2. SPL инициализирует более сложную периферию, в частности, RAM. Далее он ищет следующий этап — Third Program Loader (TPL) или сразу Kernel. Найденный код загружается в RAM, управление передаётся ему.

  3. TPL (если он есть) проводит дополнительную инициализацию оборудования и ищет ядро. Обнаружив его, загружает в RAM и передаёт управление.

  4. Kernel перенастраивает периферию согласно Device Tree, монтирует RootFS и запускает процесс Init. Дальше, наличие отличий от обычного ПК зависит от того, что именно содержится в RootFS и Init.

Необходимо отметить, что каждый этап не вызывает следующий, а именно передает управление. Вернуться назад невозможно — только перезапустить всё устройство и пройти путь заново.

2.1.3 Сходства и ключевые отличия

На первый взгляд, процессы загрузки похожи. Да и выглядят одинаково запутанно. Отчасти, так и есть. Но!

В ПК множество задач решают микроконтроллеры: например, память и шины инициализируются «сами» — BIOS лишь координирует их работу. Каждый узел системы автономен и содержит собственную прошивку. Загрузка ОС в этом случае — скорее организация взаимодействия между независимыми компонентами, чем ручное включение каждого.

В SoC всё иначе. Это монолитный чип без отдельных контроллеров или лишнего пространства — всё должно быть максимально компактным, надёжным и понятным. Задача пробудить систему полностью ложится на её плечи. Поэтому и нужен каскад загрузчиков, каждый из которых решает свою задачу.

  • PPL — минимальный, максимально надёжный и неизменяемый код в ROM. Он выполняет лишь базовую инициализацию и запускает следующий этап. Его компактность делает его легко проверяемым и надёжным. Повредить его — значит убить всю систему: чип станет кирпичом.

  • SPL — получает чуть больше возможностей: в частности, может инициализировать RAM. Но SRAM слишком мала, чтобы вместить сложную логику, и потому SPL обычно нужен только как мост к следующему шагу.

  • TPL — уже работает в полноценной RAM и может позволить себе роскошь сценариев: например, откуда загружать ОС, как отобразить процесс загрузки, как реагировать на подключённые устройства и т.д.

Такая архитектура не усложняет — она защищает. Если сбой произошёл на уровне SPL или TPL, загрузку можно перехватить, загрузиться с резервного носителя и восстановить систему. Главное — предусмотреть такую возможность. А вот сбой в PPL означает полный отказ устройства. Поэтому он неизменяем и максимально прост.

2.2. Состав Embedded Linux

Итак, мы узнали, с чем нам предстоит работать. Предлагаю рассмотреть каждый компонент подробнее.

2.2.1. Bootloader

Для загрузки операционной системы на SoC чаще всего используется U-Boot — мощный и универсальный загрузчик с открытым исходным кодом. Именно с ним мы и будем работать. Вот его преимущества:

  • Поддерживает множество архитектур и SoC-платформ

  • Позволяет загружать ядро и RootFS с самых разных источников: USB, SD-карты, SATA-диска, по сети, из NAND/NOR-памяти

  • Может выполнять сценарии: например, пробовать сначала загрузку с USB, затем с eMMC и только потом — по сети

  • Поддерживает интерактивный режим (через консоль) и авто-загрузку по таймеру

  • Имеет гибкую конфигурацию через переменные среды

Скачать U-Boot можно с официального репозитория.

Если SPL не помещается в SRAM, его можно «разбить» на части — подробнее об этом было в разделе про загрузку SoC

2.2.2. Kernel

Это и есть сам Linux — ядро, которое управляет железом, абстрагирует ресурсы и обеспечивает взаимодействие между программами.

Скачать ядро можно с официального репозитория или с форков, адаптированных под конкретный SoC. Например, для Rockchip-процессоров есть репозиторий linux-rockchip от проекта Armbian, содержащий патчи, Device Tree-файлы и конфигурации под различные платы.

2.2.3. RootFS

RootFS — это не просто набор каталогов. Это полноценное окружение, в котором есть:

  • Утилиты вроде init, sh, ifconfig

  • Конфигурационные файлы

  • Зависимости для запуска программ

В отличие от ядра и загрузчика, RootFS не собирается как бинарник — он создаётся как структура каталогов. Вы можете создать RootFS вручную (например, скопировав нужные бинарники и библиотеки из хост-системы), но это требует аккуратности и глубокого понимания устройства системы.

2.3. Инструменты и средства сборки Embedded Linux

Итак, «представителей» рассмотрели. Возможно, кто-то даже полез смотреть исходники. Но не советую спешить, ведь нам осталось разобраться со средствами и инструментами сборки.

2.3.1. Toolchain

Toolchain — набор компиляторов, заголовков и утилит, ориентированных на целевую архитектуру, а именно:

  • Кросс-компиляторы gcc, g++, ld, as — всё необходимое для сборки кода

  • Заголовочные файлы ядра Linux (Linux kernel headers)

  • Библиотеки и заголовки libc, libstdc++, libm и другие системные библиотеки

  • Отладочные утилиты, анализаторы, системные хелперы

Главное требование к тулчейну — соответствие архитектурам Host (Где происходит компиляция) и Target (Где будет запускаться код) систем.

Скачать можно уже готовый, а можно собирать самому (Например, при помощи CrossTool-NG). Второй вариант заслуживает отдельного цикла статей, поэтому представим, что мы можем использовать только готовые решения.

Их можно скачать с сайта-производителя вашего устройства или сторонних сайтов производителей тулчейнов (Например, Linaro).

Ну, еще можно собрать самому...

Стоп, разве я не сказал, что мы так делать не будем? Скажем так, я слукавил. Собирать и настраивать тулчейн мы будем, но не сами, а попросим об этом инструмент сборки.

2.3.2. Инструменты сборки

Чтобы не собирать всё вручную, обычно используют специальные сборочные системы. Наиболее популярны две: 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, углубимся в структуру каждого компонента, научимся их настраивать и собирать. Разберем способы решения ошибок во время сборки и попробуем запустить собранный образ.

На этом первая статья из цикла подошла к концу. Спасибо за уделенное время! Еще увидимся!

Tags:
Hubs:
+61
Comments38

Articles