Embedded HandbookПриложения
Все разделы › XXIV. Встраиваемый Linux
· уровень средний · 9 мин чтения
Открыть в интерактивном справочнике

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 Четыре части образа

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

Четыре части образа прибора на Linux: загрузчик, ядро, дерево устройств и корневая файловая система

Часть Что делает Чем собирается
загрузчик поднимает память и тактирование, читает ядро, передаёт ему аргументы 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 возвращает текст из двоичного файла — это главный приём отладки: смотрят не то, что написали, а то, что реально лежит в образе.

Дерево устройств платы: корень с compatible handbook,demo-board, узел memory по 0x40000000 на 128 МБ, контроллер i2c по 0x44e0b000 с драйвером ti,omap4-i2c и частотой 100 000, внутри него rtc по адресу 0x68 с драйвером dallas,ds1307; рядом числа опыта — dts 2218 байт, dtb 1515 байт, 13 узлов, магия d00dfeed

⚠️ Дерево описывает плату, а не ваши желания. Ядро не проверяет, что микросхема действительно стоит по этому адресу: оно доверяет дереву. Ошибка в 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 Итоги

Полные исходники

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».

Открыть в интерактивном справочнике