- PVSM.RU - https://www.pvsm.ru -

Строим свой остров: как создать минимальный Linux для Raspberry Pi

Строим свой остров: как создать минимальный Linux для Raspberry Pi - 1

Ещё три года назад меня просили рассказать, как собрать минимальный 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 [1] собирать мы будем без использования Buildroot [2] или Yocto Project [3], только сделаем его более практичным, так как он будет поддерживать работу с 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 [4] называются upstream-ядрами (часто называют ещё ванильными ядрами). Ядра, которые распространяются с дистрибутивами или разрабатываются создателями платформ, называются downstream-ядрами.

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

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

  • 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 [5]

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

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

  • BSP,

  • toolchain,

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

  • загрузчик,

  • Device Tree.

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

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 [6].

Чтобы снизить издержки на сопровождение 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 [7], если у вас платформа 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, вы можете увидеть здесь [8]

Разнообразие файлов, необходимых для загрузки 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 [9].

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

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

Файл config.txt

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

Мы будем использовать минимальное количество опций. В файле 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 [12].

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

  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 "33c" > $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 [13]

В случае Raspberry Pi 4 нужно изменять конфигурацию загрузчика в EEPROM при помощи утилиты rpi-eeprom-config [14]. Что, в свою очередь, подразумевает предварительную загрузку операционной системы на 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,

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

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

  • Device Tree overlays.

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

  • toolchain,

  • загрузчик,

  • Device Tree,

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

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

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

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

Автор: artyomsoft

Источник [15]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/linux/440799

Ссылки в тексте:

[1] статье по сборке Linux для PC: https://habr.com/ru/companies/ruvds/articles/963400/

[2] Buildroot: https://buildroot.org/

[3] Yocto Project: https://www.yoctoproject.org/

[4] kernel.org: https://kernel.org/

[5] bootlin.com: https://elixir.bootlin.com

[6] аккаунте Raspberry Pi на GitHub: https://github.com/raspberrypi

[7] сайта ARM: https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads

[8] здесь: https://github.com/raspberrypi/firmware

[9] balenaEtcher: https://etcher.balena.io/

[10] здесь: https://www.raspberrypi.com/documentation/computers/config_txt.html

[11] здесь: https://www.raspberrypi.com/documentation/computers/legacy_config_txt.html

[12] моим проектом на GitHub: https://github.com/artyomsoft/robinson-linux

[13] официальной документации к Raspberry Pi: https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#bootcode-bin-uart-enable

[14] изменять конфигурацию загрузчика в EEPROM при помощи утилиты rpi-eeprom-config: https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#raspberry-pi-bootloader-configuration

[15] Источник: https://habr.com/ru/companies/ruvds/articles/971084/?utm_source=habrahabr&utm_medium=rss&utm_campaign=971084