Ещё три года назад меня просили рассказать, как собрать минимальный Linux для Raspberry Pi, — и сейчас я выполняю эту просьбу. Несмотря на то, что первоначальной целью Raspberry Pi было создание дешёвого устройства для обучения базовым навыкам программирования, информации о том, как создать минимальный Linux для Raspberry Pi в интернете немного. Я хочу восполнить этот пробел для желающих начать погружение в embedded-разработку.

Linux для встраиваемых систем, включая Raspberry Pi, и Linux для PC имеют ряд различий. Различия касаются используемых загрузчиков, платформо-зависимого кода ядра, файловых систем и прочего. Для встраиваемых систем большое значение имеет Board Support Package (BSP), который обычно сопровождает различные системы на кристалле (System on Chip — SoC) или одноплатные компьютеры (Single Board Computer — SBC).

Чтобы сделать статью интереснее и полезнее, я рассмотрю создание Linux для Raspberry Pi 3 и для Raspberry Pi 4 и укажу на различие этих одноплатных компьютеров в контексте загрузки и сборки ядра Linux. Также мы соберём и запустим downstream и upstream Linux-ядра для Raspberry Pi.

Под Raspberry Pi 3 и Raspberry Pi 4 подразумеваются модели Raspberry Pi 3 Model B и Raspberry Pi 4 Model B соответственно. А обе модели называются в статье Raspberry Pi.

Как и в моей прошлой статье по сборке Linux для PC собирать мы будем без использования Buildroot или Yocto Project, только сделаем его более практичным, так как он будет поддерживать работу с SD-картой.

Такие сборки минимального Linux без Buildroot и Yocto Project мне чем-то напоминают высадку на необитаемый остров, где вы вынуждены минимальным набором инструментов благоустраивать свою жизнь. Да, вашей жизни ничего не угрожает, но определённая закалка в виде полученных базовых знаний остаётся. Поэтому системе Linux, создаваемой в статье, я дал кодовое название Robinson Linux.

Я надеюсь, что после прочтения статьи вам будет гораздо проще собрать Linux для другого одноплатного компьютера, например, Orange Pi.

Кому интересно погрузиться в embedded-разработку, добро пожаловать под кат.

Особенности создания сборок Linux для встраиваемых систем на примере Raspberry Pi

Процессы сборки и запуска Linux для встраиваемых систем несколько сложнее, чем для PC, так как начальная загрузка Linux отличается от одной платформы к другой, а сами платформы могут сильно различаться. Но если вы разберётесь с Linux на одной платформе, собрать и запустить Linux на другой вам будет гораздо проще.

Linux Kernel Source Tree

Ядро Linux имеет открытый исходный код, который называется Linux Kernel Source Tree. Любой желающий может скачать, изменить и собрать этот код.

Различают два основных вида ядер Linux: upstream и downstream-ядра. Версии ядер от Линуса Торвальдса и на сайте kernel.org называются upstream-ядрами (часто называют ещё ванильными ядрами). Ядра, которые распространяются с дистрибутивами или разрабатываются создателями платформ, называются downstream-ядрами.

В версиях ядер легко запутаться, поэтому я советую начать изучение исходного кода с версий, которые находятся на главной странице сайта kernel.org.

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

  • mainline,

  • stable,

  • longterm,

  • linux-next.

Главная страница сайта kernel.org
Главная страница сайта kernel.org

Downstream-ядра, поставляемые в составе BSP (Board Support Package), как правило, основаны на upstream-ветках stable или longterm (LTS). Использование этих веток позволяет вендорам получить предсказуемую и поддерживаемую базу, поверх которой накладываются платформо-специфичные патчи и драйверы.

Мы возьмём за основу stable ветку 6.12.y и соберём на основе кода из этой ветки upstream-ядро и downstream-ядро.

При исследовании upstream-ядер также удобно исследовать исходный код Linux на сайте bootlin.com

Ключевые понятия для сборки и запуска Linux на встраиваемых платформах

При портировании встраиваемого Linux на конкретную платформу ключевыми понятиями являются:

  • BSP,

  • toolchain,

  • кросс-компиляция,

  • загрузчик,

  • Device Tree.

Я рассчитываю, что другие понятия, такие как корневая файловая система, ядро, пользовательские приложения и программы вам знакомы уже по созданию минимального Linux для PC.

Board Support Package (BSP)

Бо́льшая часть кода ядра Linux является платформонезависимой. Платформозависимая часть находится в директории arch/ исходного кода Linux, но этого кода в большинстве случаев недостаточно, чтобы запустить Linux на определённой платформе. Необходим Board Support Package (BSP) — пакет поддержки платформы. Его основная задача — обеспечить корректный старт Linux на целевой аппаратной платформе.

BSP обычно разрабатывается производителем SoC или одноплатного компьютера, но может поддерживат��ся и сообществом. Какого-либо строгого стандарта BSP не существует. По сути, BSP — это набор компонентов, необходимый для загрузки и правильной работы ядра на конкретном устройстве. В состав BSP входят:

  • загрузчик (bootloader),

  • патчи к загрузчику и ядру Linux,

  • Device Tree (DTB),

  • при необходимости — Device Tree overlays,

  • firmware для Wi-Fi, Bluetooth, GPU и других компонентов,

  • скрипты сборки и конфигурационные файлы.

Поиск актуального BSP, в зависимости от платформы, может превратиться в нетривиальную задачу. Но для Raspberry Pi эта задача решается относительно просто. Я не встретил в официальной документации Raspberry Pi упоминания о BSP, но компоненты, которые относятся к BSP, вы можете найти в аккаунте Raspberry Pi на GitHub.

Чтобы снизить издержки на сопровождение downstream-ядра, производители стремятся поместить в upstream как можно больше платформо-специфичного кода. После принятия в upstream этот код сопровождается вместе с ядром Linux, адаптируется к изменениям остального кода ядра и может улучшаться другими разработчиками сообщества, что существенно уменьшает объём работы по поддержке собственных downstream-веток.

Поддержка Raspberry Pi существует в upstream-ядре, но она не такая полная, как в downstream-ядре. Для нашего минимального Linux не нужен весь функционал из downstream-ядра, поэтому мы смело можем использовать как upstream, так и downstream-ядро.

Toolchain и кросс-компиляция

Платформы, на которых запускается Linux, отличаются набором команд (Instruction Set Architecture (ISA)). При компиляции важно понимать, что такое целевая платформа и хост-платформа. Целевая платформа — это та платформа, для которой делается сборка ядра, пользовательских приложений и программ. Хост-платформа — это платформа, на которой эта сборка делается. Кросскомпиляция — компиляция, когда целевая платформа отличается хост-платформы.

Для выполнения кросс-компиляции вам необходимо скачать и установить toolchain для целевой платформы. Toolchain — это набор файлов, позволяющий собрать бинарные файлы для целевой платформы. Необходимый toolchain проще всего установить из репозитория пакетов Debian.

# apt install crossbuild-essential-arm64

Но можно скачать toolchain c сайта ARM, если у вас платформа ARM, или собрать из исходников, как это делается в Buildroot.

Исходный код ядра Linux поддерживает кросс-компиляцию. Чтобы выполнить кросс-компиляцию, необходимо установить две переменных окружения ARCH и CROSS_COMPILE.

В нашем случае:

  • ARCH=arm64;

  • CROSS_COMPILE=aarch64-linux-gnu-.

Переменная ARCH нужна, чтобы в компиляцию включались участки исходного кода, специфичные для архитектуры ARM64, а CROSS_COMPILE, чтобы ядро собиралось при помощи toolchain для ARM64. Toolchain определяется не только ISA целевой платформы, но и её ABI (соглашения о вызовах, формат бинарников, используемую libc и интерфейс с ядром).

Для сборки исходного кода ядра Linux одного toolchain недостаточно — требуется установка дополнительных пакетов на хост-платформу, таких как make, ncurses-dev и др.

Linux собирают на Linux. Чтобы процесс сборки был воспроизводимый, целесообразно сборку осуществлять в Docker-контейнере.

Загрузчик

Загрузчик во встраиваемых системах выполняет ту же задачу, что и в PC — подготавливает систему к запуску ядра операционной системы, загружает и запускает ядро, передавая ему параметры. Во встраиваемых системах часто используется загрузчик U-Boot, но у Raspberry Pi свой проприетарный загрузчик, отличительная черта которого, что он выполняется на графическом ядре SoC Broadcom.

Какие файлы нужны для загрузки Raspberry Pi, вы можете увидеть здесь

Разнообразие файлов, необходимых для загрузки Raspberry Pi, кажется пугающим на первый взгляд, но если отбросить эмоции, то в них можно с лёгкостью ориентироваться.

  • Первичный загрузчик: bootcode.bin (для Raspberry Pi 4 и старше boot firmware находится на EEPROM-чипе).

  • Вторичный загрузчик: start*.elf + fixup*.dat.

  • Два конфигурационных файла: config.txt и cmdline.txt.

  • Device Tree для различных версий Raspberry Pi: *.dtb.

  • Оверлеи для Deviсe Tree: директория overlays.

  • Ядра linux: kernel*.img.

Я не буду рассматривать в статье различные виды загрузки, доступные для Raspberry Pi (сетевая, с внешнего USB-диска), рассмотрю только загрузку с SD-карты с MBR-разметкой.

Файловая система на первом разделе должна быть FAT32 и содержать:

  • файлы firmware,

  • ядра,

  • initramfs (опционально),

  • Device Tree,

  • оверлеи (опционально).

Требований к остальным разделам нет, но обычно файловая система второго раздела — ext4, и он содержит корневую файловую систему Linux.

Загрузка Linux на Raspberry Pi выполняется в четыре стадии:

  1. Выполнение кода в BootROM. Этот код записывается один раз производителем и изменить его нельзя. Основная задача кода — найти на доступных устройствах вторичный загрузчик.

  2. Выполнение кода вторичного загрузчика. На этой стадии инициализируется контроллер DRAM, ищется и запускается основной загрузчик.

  3. Выполнение кода основного загрузчика. Этот загрузчик находит и загружает ядро операционной системы.

  4. Выполнение загрузки ядра операционной системы.

Выделение отдельных вторичного и основного загрузчика необходимо из-за того, что контроллер DRAM изначально не проинициализирован, и уместить весь код загрузчика в кеш CPU или GPU (как в случае Raspberry Pi) невозможно.

Загрузка начинается с того, что загрузчик, который находится в SoC Broadcom и выполняется на GPU, ищет файл bootcode.bin на SD-к��рте или в EEPROM и загружает файл в оперативную память.

Проприетарный загрузчик bootcode.bin выполняется графическим ядром SoC Broadcom, он разбирает конфигурационные параметры в файле config.txt, загружает файлы start*.elf и fixup*.dat и выполняет код.

* в имени файла означает, что эта часть имени файла переменная и зависит от того, в каком режиме осуществляется загрузка.

start*.elf загружает:

  • файл cmdline.txt, содержащий командную строку Linux,

  • ядро Linux (один из файлов kernel*.img, но можно указать и произвольное имя в конфигурации),

  • Device Tree (один из файлов *.dtbo),

  • опционально initramfs,

  • опционально оверлеи к Device Tree (файлы в директории overlays).

Потом загрузчик применяет оверлеи к Device Tree и запускает на CPU ядро Linux, которому передаёт командную строку, initramfs, пропатченное оверлеями Device Tree.

Device Tree

Важное отличие большинства встраиваемых систем от PC в том, что конфигурация платформы хранится не в ACPI-таблицах, а в специальном файле с расширением .dtb (Device Tree Blob). Содержимое этого файла передаётся загрузчиком ядру при запуске.

В официальной прошивке (downstream) используется один базовый *.dtb + десятки overlays (*.dtbo), которые динамически патчат дерево (включают UART, I2C, SPI, звук, камеры и т. д.). Но мы использовать оверлеи не будем.

В upstream-ядре используются следующие Device Tree Blobs:

  • Для Pi 3 Nodel B: bcm2837-rpi-3-b.dtb

  • Для Pi 4 Model B: bcm2711-rpi-4-b.dtb

В downstream-ядре используются следующие Device Tree Blobs:

  • Для Pi 3 Model B: bcm2710-rpi-3-b.dtb

  • Для Pi 4 Model B: bcm2711-rpi-4-b.dtb

Эти файлы лежат в директории arch/arm64/boot/dts/broadcom/ после сборки ядра.

Сборка Linux

Сборка встраиваемого Linux подразумевает:

  • сборку ядра Linux,

  • сборку пользовательских приложений и программ,

  • создание образа, в котором располагаются они и другие, необходимые для работы файлы (dtb, оверлеи, загрузчик, конфигурационные файлы).

Образ обычно состоит из нескольких разделов, требования к которым определяются архитектурой SoC, используемым загрузчиком, требованиями к системе Linux.

Мы будем строить простой образ диска с MBR-разметкой и двумя разделами:

  • раздел с файловой системой FAT32, содержащий:

    • firmware Raspberry Pi,

    • конфигурационные файлы для него,

    • ядро Linux,

    • Device Tree,

  • раздел с файловой системой ext4, содержащий корневую файловую систему с пользовательскими приложениями и программами.

Сборка ядра

Сборка ядра минимального Linux для Raspberry Pi осуществляется на основе конфигурации tiny_config, к которой добавляются опции для необходимых подсистем.

Ограничимся минимумом:

  • Компиляция исходного кода Device Tree (OF_ALL_DTBS).

  • Поддержка платформы Raspberry Pi (RASPBERRYPI_FIRMWARE, BCM2835_MBOX, MAILBOX, ARCH_BCM, ARCH_BCM2835).

  • Поддержка консолей (TTY).

  • Поддержка последовательного порта (SERIAL_OF_PLATFORM, SERIAL_8250_SHARE_IRQ, SERIAL_8250, SERIAL_8250_EXTENDED,SERIAL_8250_BCM2835AUX, SERIAL_8250_CONSOLE).

  • Поддержка специальных файловых систем (DEVTMPFS, DEVTMPFS_MOUNT,TMPFS, PROC_FS, SYSFS).

  • Поддержка исполняемых файлов (BINFMT_ELF, BINFMT_SCRIPT).

  • Поддержка вывода сообщений ядра (PRINTK, PRINTK_TIME).

  • Поддержка DMA(DMA DMADEVICES, DMA_CMA, CMA, DMA_BCM2835, ZONE_DMA, ZONE_DMA32).

  • Поддержка ext4 для корневой файловой системы (BLOCK, EXT4_FS).

  • Поддержка MMC (MMC_BLOCK, MMC, MMC_SDHCI, MMC_SDHCI_PLTFM, MMC_SDHCI_IO_ACCESSORS, MMC_SDHCI_IPROC, MMC_SDHCI_OF_ARASAN, MMC_BCM2835).

Сборка пользовательских приложений и программ

Из всех пользовательских приложений и программ для минимального Linux нам будет достаточно BusyBox. Сборка осуществляется практически так же, как и в случае PC, но выполняется кросс-компиляция и необходимо выключить пару опций.

Создание образа для SD-карты

Алгоритм для сборки образа SD-карты следующий:

  1. Создать файл образа.

  2. Разметить файл образа разделами.

  3. Получить блочные loop-уcтройства для разделов.

  4. Отформатировать каждый из разделов.

  5. Смонтировать файловые системы.

  6. Заполнить файловые системы разделов содержимым.

  7. Размонтировать файловые системы и освободить loop-устройства.

В Linux существует несколько способов сделать это. Каждый из способов имеет свои достоинства и недостатки. Мы будем использовать способ, который подходит для работы внутри Docker-контейнера. К сожалению, Docker-контейнер нужно стартовать с опцией --privileged.

Полученный образ можно записать на SD-карту при помощи команды dd или используя balenaEtcher.

Конфигурирование загрузчика

Как уже говорилось ранее, передать параметры загрузчику Raspberry Pi можно при помощи двух файлов: config.txt и cmdline.txt.

Файл config.txt

Описание опций файла config.txt можно посмотреть здесь и здесь.

Мы будем использовать минимальное количество опций. В файле config.txt можно хранить конфигурации для нескольких моделей Raspberry Pi при помощи так называемых фильтров.

Например, опции, которые вы укажете после [pi3], будут использоваться, если загрузка осуществляется на Raspberry Pi 3. После [pi4] указываются опции для Raspberry Pi 4.

Файл cmdline.txt

Файл cmdline.txt для разных версий Raspberry Pi, а также для upstream- и downstream-ядер может немного различаться, так как по-разному именуется последовательный порт, а для SD-карты по-разному назначается имя устройства.

Для upstream-ядра файл выглядит следующим образом:

earlycon console=ttyS1,115200 root=/dev/mmcblk0p2 rw rootwait

В этом случае ядру передаётся 5 параметров:

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

  • console=ttyS1,115200 — использовать в качестве Linux-консоли последовательный порт ttyS1 со скоростью передачи 115200 бод,

  • root=/dev/mmcblk0p2 — ядро должно монтировать в качестве корневой файловой системы второй раздел на блочном устройстве /dev/mmcblk0,

  • rw — корневая система должна монтироваться в режиме "read-write",

  • rootwait — ядро приостанавливает загрузку, пока не будет смонтирована корневая файловая система.

Действия по сборке Linux для Raspberry Pi

Теперь можно привести список действий по сборке Linux. Если у вас нет желания набирать команды вручную, то можете воспользоваться моим проектом на GitHub.

Подготовка среды для сборки

  1. Создаём Dockerfile для образа на базе Debian, в котором будем производить сборку Linux:

    FROM debian:bookworm
    
    RUN apt update && apt install -y --no-install-recommends \
        build-essential crossbuild-essential-arm64 wget xz-utils cpio flex bison bc file tree \
        ncurses-dev libelf-dev libssl-dev git ca-certificates parted kpartx dosfstools e2fsprogs mount \
        && apt clean \
        && rm -rf /var/lib/apt/lists/*
    
    WORKDIR /workspace
    
    CMD ["/bin/bash"]
    

    Можно обойтись и без создания Dockerfile, но если вы будете экспериментировать со сборкой, использование Docker-образа с предустановленными пакетами сохранит вам время при неоднократной сборке Linux.

  2. Собираем Docker-образ:

    sudo docker build -t robinson-linux-builder .
    
  3. Запускаем контейнер:

    В Linux:

    $ sudo docker run -it --rm --privileged -v $(pwd):/workspace robinson-linux-builder
    

    В Windows:

    > docker run -it --rm --privileged -v %cd%:/workspace robinson-linux-builder
    
  4. Создаём директорию, где будет выполняться сборка:

    # mkdir /build && cd /build
    

Сборка upstream-ядра

  1. Скачиваем upstream-ядро:

    # wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.56.tar.xz
    
    # tar xvf linux-6.12.56.tar.xz --strip-components=1 -C linux-upstream
    
  2. Конфигурируем ядро:

    # cd linux-upstream
    
    # make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- tinyconfig && \
    ./scripts/config \
    -e OF_ALL_DTBS \
    -e TTY \
    -e ARCH_BCM -e ARCH_BCM2835 \
    -e SERIAL_OF_PLATFORM -e SERIAL_8250_SHARE_IRQ -e SERIAL_8250 -e SERIAL_8250_EXTENDED \
    -e SERIAL_8250_BCM2835AUX -e SERIAL_8250_CONSOLE \
    -e DEVTMPFS -e DEVTMPFS_MOUNT -e TMPFS -e PROC_FS -e SYSFS \
    -e BINFMT_ELF -e BINFMT_SCRIPT \
    -e PRINTK -e PRINTK_TIME \
    -e RASPBERRYPI_FIRMWARE \
    -e BCM2835_MBOX -e MAILBOX \
    -e DMADEVICES -e DMA_CMA -e CMA -e DMA_BCM2835 -e ZONE_DMA -e ZONE_DMA32 \
    -e BLOCK -e EXT4_FS \
    -e MMC_BLOCK -e MMC -e MMC_SDHCI -e MMC_SDHCI_PLTFM -e MMC_SDHCI_IO_ACCESSORS -e MMC_SDHCI_IPROC -e MMC_SDHCI_OF_ARASAN -e MMC_BCM2835
    
    # make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- olddefconfig
    
  3. Собираем ядро:

    # make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
    

Сборка downstream-ядра

  1. Скачиваем downstream-ядро:

    cd /build
    
    # git clone --depth=1 -b rpi-6.12.y https://github.com/raspberrypi/linux.git linux-downstream
    
  2. Конфигурируем ядро:

    # cd linux-downstream
    
    # make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- tinyconfig && \
    ./scripts/config \
    -e OF_ALL_DTBS \
    -e TTY \
    -e ARCH_BCM -e ARCH_BCM2835 \
    -e SERIAL_OF_PLATFORM -e SERIAL_8250_SHARE_IRQ -e SERIAL_8250 -e SERIAL_8250_EXTENDED \
    -e SERIAL_8250_BCM2835AUX -e SERIAL_8250_CONSOLE \
    -e DEVTMPFS -e DEVTMPFS_MOUNT -e TMPFS -e PROC_FS -e SYSFS \
    -e BINFMT_ELF -e BINFMT_SCRIPT \
    -e PRINTK -e PRINTK_TIME \
    -e RASPBERRYPI_FIRMWARE \
    -e BCM2835_MBOX -e MAILBOX \
    -e DMADEVICES -e DMA_CMA -e CMA -e DMA_BCM2835 -e ZONE_DMA -e ZONE_DMA32 \
    -e BLOCK -e EXT4_FS \
    -e MMC_BLOCK -e MMC -e MMC_SDHCI -e MMC_SDHCI_PLTFM -e MMC_SDHCI_IO_ACCESSORS -e MMC_SDHCI_IPROC -e MMC_SDHCI_OF_ARASAN -e MMC_BCM2835
    
    # make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- olddefconfig
    
  3. Собираем ядро:

    # make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
    

Сборка пользовательских приложений и программ

  1. Скачиваем busybox:

    # cd /build
    
    # wget -q https://busybox.net/downloads/busybox-1.37.0.tar.bz2
    
    # tar xjf busybox-1.37.0.tar.bz2
    
  2. Конфигурируем BusyBox:

    # cd busybox-1.37.0
    
    # make CROSS_COMPILE=aarch64-linux-gnu- defconfig
    
    # sed -i 's/^CONFIG_SHA256_HWACCEL=y/# CONFIG_SHA256_HWACCEL is not set/' $(BUSYBOX_DIR)/.config
    
    # sed -i 's/^CONFIG_SHA1_HWACCEL=y/# CONFIG_SHA1_HWACCEL is not set/' $(BUSYBOX_DIR)/.config
    
    
  3. Создаём структуру папок корневой файловой системы:

    # mkdir -p ../rootfs/{bin,sbin,etc,proc,sys,usr/{bin,sbin},dev,run,tmp,var}
    
  4. Собираем BusyBox:

    # make CROSS_COMPILE=aarch64-linux-gnu- CONFIG_STATIC=y CONFIG_PREFIX=../rootfs -j$(nproc) install
    
  5. Создаём свой минимальный init-файл и замещаем существующий в rootfs/sbin

    # rm ../rootfs/sbin/init
    
    # cat << 'EOF' > ../rootfs/sbin/init
    #!/bin/sh
    mount -t proc none /proc
    mount -t sysfs none /sys
    mount -t tmpfs tmpfs /run
    mount -t tmpfs tmpfs /tmp
    mdev -s
    TTY=/dev/$(head -1 /sys/class/tty/console/active)
    echo 0 > /proc/sys/kernel/printk
    printf "\033c" > $TTY
    cat << INNER > $TTY
    Welcome to ArtyomSoft Robinson Linux for Raspberry Pi
    TTY: $(basename $TTY)
    Time: $(date)
    Kernel version: $(uname -r)
    INNER
    exec setsid sh -c "exec sh <'$TTY' >'$TTY' 2>'$TTY'"
    EOF
    
    # chmod +x ../rootfs/sbin/init
    
    

Подготовка образа SD-карты

  1. Создаём образ SD-карты:

    # cd /workspace
    
    # dd if=/dev/zero of=robinson-linux.img bs=1M count=1024 status=progress
    
  2. Размечаем образ:

    # parted robinson-linux.img --script mklabel msdos
    
    # parted robinson-linux.img --script mkpart primary fat32 4MiB 1024MiB
    
    # parted robinson-linux.img --script set 1 boot on
    
    # parted robinson-linux.img --script mkpart primary ext4 1024MiB 100%
    
  3. Создаём loop-устройства для разделов:

    # LOOP_DEVICE=$(losetup -f --show robinson-linux.img)
    
    # LOOP_NAME="${LOOP_DEVICE##*/}"
    
    # kpartx -av $LOOP_DEVICE
    
    # BOOT_PARTITION="/dev/mapper/${LOOP_NAME}p1"
    
    # ROOT_PARTITION="/dev/mapper/${LOOP_NAME}p2"
    
  4. Форматируем разделы:

    # mkfs.vfat -F 32 $BOOT_PARTITION
    
    # mkfs.ext4 -F $ROOT_PARTITION
    
  5. Монтируем файловые системы разделов

    # TMP_MOUNT=$(mktemp -d /tmp/rpi-image-XXXXXX)
    
    # TMP_BOOT="$TMP_MOUNT/boot"
    
    # TMP_ROOT="$TMP_MOUNT/root"
    
    # mkdir -p "$TMP_BOOT" "$TMP_ROOT"
    
    # mount "$BOOT_PARTITION" "$TMP_BOOT"
    
    # mount "$ROOT_PARTITION" "$TMP_ROOT"
    

Наполнение файловых систем файлами

  1. Файл config.txt

    kernel=kernel8.img
    os_prefix=upstream/
    arm_64bit=1
    enable_uart=1
    
    [pi3]
    cmdline=cmdline_rpi3.txt
    core_freq=250
    
    [pi4]
    cmdline=cmdline_rpi4.txt
    

    Опция os_prefix=upsteam/ означает, что ядро и файл cmdline.txt будут искаться загрузчиком в директории upstream. В директории upstream находится файл upstream-ядро. Если мы закомментируем эту опцию, то будет загружаться файл downstream-ядра из корня файловой системы.

    Вместо os_prefix можно использовать устаревшую опцию upstream_kernel=1. В этом случае загрузчик будет грузить upstream-ядро, которое должно находиться в директории upstream.

    Опция core_freq=250 необходима для того, чтобы стабильно работал UART на Raspberry Pi 3.

    Мы используем опцию cmdline совместно с фильтрами pi3 и pi4, чтобы можно было задать разные командные строки для ядер

  2. Файлы cmdline*.txt

    # mkdir -p $TMP_BOOT/upstream 
    
    # cat > "$TMP_BOOT/upstream/cmdline_rpi4.txt" <<EOF
    earlycon console=ttyS1,115200 root=/dev/mmcblk1p2 rw rootwait
    EOF
    
    # cat > "$TMP_BOOT/upstream/cmdline_rpi3.txt" <<EOF
    earlycon console=ttyS1,115200 root=/dev/mmcblk0p2 rw rootwait
    EOF
    
    # cat > "$TMP_BOOT/cmdline_rpi4.txt" <<EOF
    earlycon console=serial0,115200 root=/dev/mmcblk0p2 rw rootwait
    EOF
    
    # cat > "$TMP_BOOT/cmdline_rpi3.txt" <<EOF
    earlycon console=serial0,115200 root=/dev/mmcblk0p2 rw rootwait
    EOF
    
  3. Устанавливаем firmware:

    # wget -O firmware.tar.xz https://github.com/raspberrypi/firmware/releases/download/1.20250915/raspi-firmware_1.20250915.orig.tar.xz
    
    # tar -xf firmware.tar.xz --strip-components=2 -C $TMP_BOOT
    
    # rm firmware.tar.xz
    
  4. Устанавливаем ядра:

    # cp /build/linux-upstream/boot/arch/arm64/boot/Image $TMP_BOOT/upstream/kernel8.img
    
    # cp /build/linux-downsream/boot/arch/arm64/boot/Image $TMP_BOOT/kernel8.img
    
  5. Устанавливаем Device Tree:

    # cp /build/linux-upstream/boot/arch/arm64/boot/dts/broadcom/bcm2837-rpi-3-b.dtb $TMP_BOOT/upstream/
    
    # cp /build/linux-upstream/boot/arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb $TMP_BOOT/upstream/
    
    # cp /build/linux-upstream/boot/arch/arm64/boot/dts/broadcom/bcm2710-rpi-3-b.dtb $TMP_BOOT/
    
    # cp /build/linux-upstream/boot/arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb $TMP_BOOT/
    
  6. Устанавливаем корневую файловую систему:

    # cp -ra /build/rootfs/. $TMP_ROOT
    
  7. Проверяем, что мы сформировали необходимую структуру в файловых системах.

    # tree $TMP_BOOT
    
    # tree $TMP_ROOT
    

Освобождение ресурсов

  1. Размонтируем файловые системы:

    # umount $TMP_ROOT
    
    # umount $TMP_BOOT
    
  2. Удаляем loop-устройства:

    # kpartx -dv $LOOP_DEVICE
    
    # losetup -d $LOOP_DEVICE
    

Запись образа Robinson Linux на SD-карту:

Выполняем следующие операции из операционной системы (не из Dockera).

# dd if=robinson-linux.img of=/dev/mmcblk0 bs=10M status=progress

# sync

Значение опции of зависит от того, куда вы поместили карту. /dev/mmcblk0 — встроенный картридер ноутбука. Если карту вы поместили во внешний USB-картридер, это будет /dev/sdX, где X — буква, назначенная блочному устройству. Если у вас в системе есть SATA-диски, используйте команду dd с осторожностью, так как если вы перепутаете SD-карту с SATA-диском, вы уничтожите свои данные на SATA-диске.

Использование UART

Наш минимальный Linux для взаимодействия с пользователем использует UART.

Использование UART — один из основных способов отладки и взаимодействия с большинством встраиваемых систем. На платах присутствуют контакты TX, RX и GND, через которые можно получить доступ к:

  • сообщениям загрузчика,

  • сообщениям ядра Linux,

  • интерактивной Linux-консоли.

Для подключения понадобится USB-UART адаптер и терминальная программа:

  • в Linux: picocom, minicom, screen,

  • в Windows: PuTTY.

USB-UART адаптер
USB-UART адаптер

USB-UART-адаптеры бывают на различных контроллерах и в разных форм-факторах. Я использовал китайский, как на фото выше.

Уровни логических сигналов TX/RX на плате должны совпадать с уровнями USB-UART адаптера. Большинство одноплатных компьютеров (и Raspberry Pi также) используют 3.3 В, тогда как некоторые конвертеры могут выдавать 5 В — такое подключение может повредить плату. Обязательно проверьте напряжения на сигнальных проводах вашего адаптера перед подключением к Raspberry Pi.

В Raspberry Pi 3 и Raspberry Pi 4 UART выведен на пины GPIO. Распиновка и схема подключения приведены ниже. Нас интересуют пины 6, 8, 10.

Распиновка GPIO Raspberry Pi
Распиновка GPIO Raspberry Pi
Подключение USB-UART-адаптера
Подключение USB-UART-адаптера

В Raspberry Pi 3 и Raspberry Pi 4 существует две реализации UART — 8250 и PL011. Они отличаются доступными скоростями передачи и управляющими регистрами. Я не хочу усложнять статью, поэтому мы используем только 8250.

Запуск Robinson Linux на Raspberry Pi

  1. Подключаем USB-UART к Raspberry Pi

  2. Запускаем эмулятор терминала

    $ sudo picocom -b 115200 /dev/ttyUSB0
    
  3. Включаем Raspberry Pi.

  4. Наблюдаем загрузку.

Отладка с помощью UART

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

Включение отладочной информации по UART для загрузчика Raspberry Pi

Для включения логирования в проприетарном загрузчике Raspberry Pi 3 нужно пропатчить файл bootcode.bin:

$ sed -i -e "s/BOOT_UART=0/BOOT_UART=1/" bootcode.bin

Интересно, что этот способ приведён в официальной документации к Raspberry Pi

В случае Raspberry Pi 4 нужно изменять конфигурацию загрузчика в EEPROM при помощи утилиты rpi-eeprom-config. Что, в свою очередь, подразумевает предварительную загрузку операционной системы на Raspberry Pi, так как по-другому rpi-eeprom-config вы не запустите.

# rpi-eeprom-config --edit

BOOT_UART=1

В случае Raspberry Pi 4 для логирования сообщений из start.elf необходимо в файле config.txt добавить параметр uart_2ndstage=1. Загрузчик Raspberry Pi 3 игнорирует uart_2ndstage.

Включение отладочной информации по UART для ядра Linux

Если ядро упадёт до того, как проинициализируется Linux-консоль, вы не увидите никаких сообщений. Чтобы увидеть, что происходило до ошибки и само сообщение kernel panic, используется подсистема earlycon.

Earlycon — удобное средство для выявления ошибок загрузки ядра на ранних стадиях. Оно позволяет выводить сообщения на UART без использования полноценного драйвера UART.
Чтобы earlycon работал, необходимо передать параметр earlycon в командной строке ядра Linux. Значение параметра можно передать там же, а можно с использованием узла в Device Tree.

Вы уже ранее видели earlycon в файле config.txt:

earlycon console=ttyS1,115200 root=/dev/mmcblk1p2 rw rootwait

В этом случае среди прочих параметров ядру указывается, что необходимо использовать раннюю консоль, но параметры (например, какой последовательный порт использовать и скорость) ядро будет брать из узла stdout-path в Device Tree.

Выводы

Ну вот и статья подошла к концу. Спасибо, что дочитали её, надеюсь, вам статья оказалась полезной, а при выполнении команд вы набили шишек и узнали что-нибудь новое, неосвещённое в статье.

Стремясь сделать статью простой и побуждающей к дальнейшему изучению сборки Linux, я сознательно не стал затрагивать ряд важных, но более сложных тем, таких как:

  • HID и USB,

  • framebuffer и framebuffer console,

  • OverlayFS и SquashFS,

  • сети и сетевую загрузку,

  • аппаратное ускорение графики,

  • Device Tree overlays.

Тем не менее в статье рассмотрены ключевые аспекты, на которые стоит обратить внимание при сборке Linux для любой встраиваемой платформы, а не только для Raspberry Pi:

  • toolchain,

  • загрузчик,

  • Device Tree,

  • исходный код ядра Linux,

  • базовые приёмы отладки ядра.

Понимание этих компонентов даёт прочную основу для дальнейшего погружения во встраиваемый Linux и упрощает использование полученных знаний на других SoC и одноплатных компьютерах.

© 2025 ООО «МТ ФИНАНС»