191. Встраиваемый Linux: когда он нужен и из чего собран образ
Микроконтроллер выполняет вашу программу целиком: она и есть система.
Прибор на Linux устроен иначе — там между железом и вашей программой стоит
ядро, а сама программа лишь одна из многих. Этот раздел о том, когда переход
оправдан и из каких частей собирается образ, который заливают в память платы.
Опыты — в образе stm-linux:1 (Debian 12, gcc 12, dtc 1.6.1, cpio 2.13);
числа в тексте — из прогонов, сверяет _verify/linux191_check.py.
Как это называется в документации
Оригинал Перевод system on chip (SoC) система на кристалле: ядро, память, контроллеры в одном корпусе memory management unit (MMU) блок управления памятью: виртуальные адреса вместо физических bootloader загрузчик device tree (DTS, DTB) дерево устройств: описание платы для ядра, текст и двоичный вид root filesystem (rootfs) корневая файловая система initramfs корень в оперативной памяти, распакованный ядром при старте init первый процесс системы, PID 1
191.1 Когда микроконтроллера мало
Переход на Linux — не повышение в классе, а смена набора задач. Считать шаги мотора и держать 20 кГц ШИМ удобнее без операционной системы общего назначения; хранить гигабайты, говорить по TCP с шифрованием и показывать графику на экране — удобнее с ней.
| Что нужно прибору | Микроконтроллер | Прибор на Linux |
|---|---|---|
| реакция за микросекунды, предсказуемо | да, это его работа | нет: планировщик и вытеснение мешают |
| запуск за миллисекунды | да | нет: старт ядра и служб — секунды |
| сеть с TLS, обновления по воздуху, база данных | тяжело, вручную | штатно, готовыми пакетами |
| файловая система на гигабайты | внешняя, своим кодом | штатно |
| экран, браузер, видео | почти нет | штатно |
| цена и потребление | копейки и микроамперы | дороже и заметно прожорливее |
Главное техническое ограничение простое: обычному ядру Linux нужен блок
управления памятью. У Cortex-M его нет. Вариант ядра без него существует
и загружается на STM32F429, F746 и H743 с внешней SDRAM на несколько
мегабайт, но без защиты памяти и без fork() — для изделий это экзотика.
F407 ему не подходит: контроллера SDRAM у него нет, а 192 КБ своего ОЗУ
ядру мало. В приборах на Linux стоят камни с блоком управления памятью —
серии MP1 или процессоры общего назначения (Cortex-A).
Отсюда частая и удачная схема: два вычислителя на одной плате. Микроконтроллер держит реальное время — опрос датчиков, управление, сторожевой таймер (раздел 12); процессор на Linux занимается сетью, хранением и экраном, а разговаривают они по UART, SPI или Ethernet (разделы 16, 18, 105). Прошивку микроконтроллера при этом часто обновляет как раз Linux-половина.
191.2 Четыре части образа
Образ прибора — не один файл, а четыре независимые части. Это важно практически: каждую собирают и обновляют отдельно.
| Часть | Что делает | Чем собирается |
|---|---|---|
| загрузчик | поднимает память и тактирование, читает ядро, передаёт ему аргументы | U-Boot, реже свой |
| ядро | владеет железом, даёт процессам память, файлы, сокеты | исходники ядра под вашу архитектуру |
| дерево устройств | говорит ядру, что именно стоит на этой плате | dtc из текста .dts |
| корневая ФС | всё остальное: init, библиотеки, ваша программа |
Buildroot, Yocto или готовый дистрибутив |
Ядро одно на всё семейство плат, а дерево устройств — своё на каждую. В этом и смысл разделения: перевесили датчик с одной шины на другую — пересобрали дерево, ядро не трогали.
Строка аргументов от загрузчика к ядру (bootargs) — тоже часть образа.
В опыте она такая:
console=ttyS0,115200 root=/dev/mmcblk0p2 rw
Где консоль (какой UART и скорость), где корень (второй раздел карты памяти) и как его монтировать. Если прибор «загрузился, но молчит», первым делом смотрят именно сюда.
191.3 Дерево устройств: плата отдельно от ядра
Дерево устройств — текстовый файл: узлы, у узлов свойства. Компилятор dtc
превращает его в двоичный .dtb, который читает ядро.
/ {
compatible = "handbook,demo-board";
#address-cells = <1>;
#size-cells = <1>;
memory@40000000 {
device_type = "memory";
reg = <0x40000000 0x08000000>; /* 128 МБ */
};
i2c0: i2c@44e0b000 {
compatible = "ti,omap4-i2c";
reg = <0x44e0b000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
clock-frequency = <100000>; /* DS1307 — только стандартный режим, раздел 17 */
rtc@68 {
compatible = "dallas,ds1307";
reg = <0x68>; /* тот же адрес, что и в разделе 17.2 */
};
};
};
Читается это так же, как схема: i2c@44e0b000 — контроллер по такому
физическому адресу; вложенный rtc@68 — микросхема на шине с адресом
0x68, тем самым семибитным адресом из раздела 17.2. Свойство compatible
— ключ, по которому ядро выбирает драйвер; reg — адрес, clock-frequency
— скорость шины. Она 100 кГц, потому что DS1307 работает только
в стандартном режиме; для 400 кГц ставят часы, которые его умеют, например
DS3231 (maxim,ds3231). #address-cells и #size-cells говорят, сколько
ячеек в reg у вложенных узлов: у шины I²C адрес — одна ячейка, длины нет.
Без них dtc выдаёт предупреждения, а reg = <0x68> разбирается неверно.
Опыт: плата из примера, пять узлов периферии.
| Что | Значение |
|---|---|
текст .dts |
2218 байт |
двоичное дерево .dtb |
1515 байт |
| узлов в дереве | 13 |
первые байты .dtb |
d00dfeed — магическое число дерева |
| скорость I²C после сборки | 0x186a0 = 100 000 Гц |
| адрес часов на шине | 0x68 |
| память | 0x40000000, длина 0x8000000 — 128 МБ |
Обратная сборка dtc -I dtb -O dts возвращает текст из двоичного файла —
это главный приём отладки: смотрят не то, что написали, а то, что реально
лежит в образе.
⚠️ Дерево описывает плату, а не ваши желания. Ядро не проверяет, что микросхема действительно стоит по этому адресу: оно доверяет дереву. Ошибка в
regили вcompatibleдаёт не сообщение об ошибке, а молчащее устройство — как ошибка в адресе I²C на микроконтроллере (раздел 17.2).
191.4 Корень в памяти: initramfs
Самый маленький работающий корень — архив, который ядро распаковывает
в память и запускает из него /init. Собирается он в два шага: каталог →
архив cpio → сжатие.
/* init.c — первый процесс системы */
int main(void)
{
struct utsname u;
uname(&u);
printf("init: pid=%d ядро=%s\n", getpid(), u.release);
mount("proc", "/proc", "proc", 0, NULL);
for (;;) {
pause();
}
}
gcc -static -O2 -o root/init init.c
( cd root && find . | sort | cpio -o -H newc --quiet > initramfs.cpio )
gzip -9 -n -c initramfs.cpio > initramfs.cpio.gz
| Что | Размер |
|---|---|
init, собранный статически |
762 688 байт (745 КБ) |
архив cpio (init и три пустых каталога) |
763 392 байта |
он же, сжатый gzip -9 |
321 КБ |
| записей в архиве | 5 |
Три четверти мегабайта на программу в двадцать строк — это стандартная
библиотека glibc, влитая в файл статически; с маленькой библиотекой C
(musl, которую Buildroot предлагает на выбор) тот же файл выходит во много
раз меньше. Отсюда два обычных для встраиваемых систем приёма: маленькая
библиотека C и один многоцелевой исполняемый файл на все команды.
В опыте это busybox: 1 982 256 байт (1,9 МБ) — и 269 команд в нём,
от sh и ls до ip и wget. Корень с ним целиком — 1004 КБ в сжатом
виде, и это уже система, в которой можно работать руками.
PID 1 — особенный процесс. Он запускается первым, усыновляет осиротевшие процессы (раздел 136.4) и не имеет права завершиться: выход
initдля ядра — паника. Поэтому в примере вечныйpause().
191.5 Чем собирают образ
Корневую файловую систему руками не собирают. Выбор обычно между тремя подходами, и он определяется не вкусом, а тем, кто потом сопровождает прибор.
| Подход | Что даёт | Чем платите |
|---|---|---|
| Buildroot | один конфигурационный файл, образ на десятки мегабайт, сборка с нуля за минуты | пересборка всего при каждом изменении, нет управления пакетами на приборе |
| Yocto | воспроизводимые сборки, слои, свой дистрибутив и обновления | входной порог и время сборки |
| готовый дистрибутив (Debian, Alpine, Armbian) | привычные пакеты, обновления из сети | размер и лишние службы, меньше контроля над стартом |
Практический порядок: макет собирают на готовом дистрибутиве, чтобы проверить железо и драйверы; серийный прибор переводят на Buildroot или Yocto, когда нужен предсказуемый образ, время старта и обновление целиком.
191.6 Грабли
Ядро загрузилось, консоли нет. В bootargs не тот console=.
Проверять по строке, которую реально передал загрузчик, а не по той,
что в файле конфигурации.
Дерево собрано, устройство не появилось. Смотреть три вещи по порядку:
есть ли узел в обратной сборке .dtb, есть ли status = "okay",
есть ли в ядре драйвер с таким compatible.
Прибор виснет сразу после «Freeing unused kernel memory». Ядро
не нашло или не смогло запустить /init: не тот путь, не тот формат
исполняемого файла (динамический без библиотек в образе), нет прав
на выполнение.
init завершился — паника ядра. Первый процесс не имеет права выходить.
Образ распух на ровном месте. Отладочные символы и статическая сборка
всего подряд: 20 строк на C дают 745 КБ, а busybox на 269 команд —
1,9 МБ. Считать размер образа стоит с первого дня, а не перед выпуском.
191.7 Итоги
- Linux нужен там, где важны сеть, хранение и экран; микроконтроллер — там, где важна предсказуемая реакция. Часто на плате стоят оба.
- Обычному ядру нужен блок управления памятью. Вариант без него грузится на F429, F746 и H743 с внешней SDRAM, но в изделиях это экзотика, а F407 ему не подходит.
- Образ состоит из четырёх частей: загрузчик, ядро, дерево устройств и корневая ФС; обновляются они независимо.
- Дерево устройств — описание платы:
compatibleвыбирает драйвер,regзадаёт адрес. В опыте 2218 байт текста дали 1515 байт.dtbи 13 узлов. - Самый маленький корень —
initramfs: статическийinitна 745 КБ даёт сжатый архив 321 КБ;busyboxна 269 команд — 1004 КБ, и это уже рабочая система. - Образы собирают Buildroot или Yocto; готовый дистрибутив удобен на макете.
Полные исходники
init.c — статический init для initramfs.
/* Первый процесс системы для раздела 191.4: статический init для initramfs.
* Ничего не делает, кроме как показывает, что он PID 1 и что корень уже смонтирован. */
#include <stdio.h>
#include <sys/mount.h>
#include <sys/utsname.h>
#include <unistd.h>
int main(void)
{
struct utsname u;
uname(&u);
printf("init: pid=%d ядро=%s\n", getpid(), u.release);
mount("proc", "/proc", "proc", 0, NULL);
printf("init: /proc смонтирован, дальше — служба прибора\n");
for (;;) {
pause();
}
}
Дальше: раздел 192 «Периферия из программы: gpiochip, i2c-dev, spidev, sysfs».