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

Ещё три года назад меня просили рассказать, как собрать минимальный 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 для встраиваемых систем несколько сложнее, чем для PC, так как начальная загрузка Linux отличается от одной платформы к другой, а сами платформы могут сильно различаться. Но если вы разберётесь с Linux на одной платформе, собрать и запустить Linux на другой вам будет гораздо проще.
Ядро Linux имеет открытый исходный код, который называется Linux Kernel Source Tree. Любой желающий может скачать, изменить и собрать этот код.
Различают два основных вида ядер Linux: upstream и downstream-ядра. Версии ядер от Линуса Торвальдса и на сайте kernel.org [4] называются upstream-ядрами (часто называют ещё ванильными ядрами). Ядра, которые распространяются с дистрибутивами или разрабатываются создателями платформ, называются downstream-ядрами.
В версиях ядер легко запутаться, поэтому я советую начать изучение исходного кода с версий, которые находятся на главной странице сайта kernel.org [4].
Там находится актуальный исходный код из веток нескольких категорий:
mainline,
stable,
longterm,
linux-next.
Downstream-ядра, пос��авляемые в составе BSP (Board Support Package), как правило, основаны на upstream-ветках stable или longterm (LTS). Использование этих веток позволяет вендорам получить предсказуемую и поддерживаемую базу, поверх которой накладываются платформо-специфичные патчи и драйверы.
Мы возьмём за основу stable ветку 6.12.y и соберём на основе кода из этой ветки upstream-ядро и downstream-ядро.
При исследовании upstream-ядер также удобно исследовать исходный код Linux на сайте bootlin.com [5]
При портировании встраиваемого Linux на конкретную платформу ключевыми понятиями являются:
BSP,
toolchain,
кросс-компиляция,
загрузчик,
Device Tree.
Я рассчитываю, что другие понятия, такие как корневая файловая система, ядро, пользовательские приложения и программы вам знакомы уже по созданию минимального Linux для PC [1].
Бо́льшая часть кода ядра 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-ядро.
Платформы, на которых запускается 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 выполняется в четыре стадии:
Выполнение кода в BootROM. Этот код записывается один раз производителем и изменить его нельзя. Основная задача кода — найти на доступных устройствах вторичный загрузчик.
Выполнение кода вторичного загрузчика. На этой стадии инициализируется контроллер DRAM, ищется и запускается основной загрузчик.
Выполнение кода основного загрузчика. Этот загрузчик находит и загружает ядро операционной системы.
Выполнение загрузки ядра операционной системы.
Выделение отдельных вторичного и основного загрузчика необходимо из-за того, что контроллер 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.
Важное отличие большинства встраиваемых систем от 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,
сборку пользовательских приложений и программ,
создание образа, в котором располагаются они и другие, необходимые для работы файлы (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-карты следующий:
Создать файл образа.
Разметить файл образа разделами.
Получить блочные loop-уcтройства для разделов.
Отформатировать каждый из разделов.
Смонтировать файловые системы.
Заполнить файловые системы разделов содержимым.
Размонтировать файловые системы и освободить loop-устройства.
В Linux существует несколько способов сделать это. Каждый из способов имеет свои достоинства и недостатки. Мы будем использовать способ, который подходит для работы внутри Docker-контейнера. К сожалению, Docker-контейнер нужно стартовать с опцией --privileged.
Полученный образ можно записать на SD-карту при помощи команды dd или используя balenaEtcher [9].
Как уже говорилось ранее, передать параметры загрузчику Raspberry Pi можно при помощи двух файлов: config.txt и cmdline.txt.
Описание опций файла config.txt можно посмотреть здесь [10] и здесь [11].
Мы будем использовать минимальное количество опций. В файле config.txt можно хранить конфигурации для нескольких моделей Raspberry Pi при помощи так называемых фильтров.
Например, опции, которые вы укажете после [pi3], будут использоваться, если загрузка осуществляется на Raspberry Pi 3. После [pi4] указываются опции для Raspberry Pi 4.
Файл 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. Если у вас нет желания набирать команды вручную, то можете воспользоваться моим проектом на GitHub [12].
Создаём 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.
Собираем Docker-образ:
sudo docker build -t robinson-linux-builder .
Запускаем контейнер:
В 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
Создаём директорию, где будет выполняться сборка:
# mkdir /build && cd /build
Скачиваем 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
Конфигурируем ядро:
# 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
Собираем ядро:
# make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
Скачиваем downstream-ядро:
cd /build
# git clone --depth=1 -b rpi-6.12.y https://github.com/raspberrypi/linux.git linux-downstream
Конфигурируем ядро:
# 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
Собираем ядро:
# make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
Скачиваем busybox:
# cd /build
# wget -q https://busybox.net/downloads/busybox-1.37.0.tar.bz2
# tar xjf busybox-1.37.0.tar.bz2
Конфигурируем 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
Создаём структуру папок корневой файловой системы:
# mkdir -p ../rootfs/{bin,sbin,etc,proc,sys,usr/{bin,sbin},dev,run,tmp,var}
Собираем BusyBox:
# make CROSS_COMPILE=aarch64-linux-gnu- CONFIG_STATIC=y CONFIG_PREFIX=../rootfs -j$(nproc) install
Создаём свой минимальный 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-карты:
# cd /workspace
# dd if=/dev/zero of=robinson-linux.img bs=1M count=1024 status=progress
Размечаем образ:
# 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%
Создаём 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"
Форматируем разделы:
# mkfs.vfat -F 32 $BOOT_PARTITION
# mkfs.ext4 -F $ROOT_PARTITION
Монтируем файловые системы разделов
# 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"
Файл 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, чтобы можно было задать разные командные строки для ядер
Файлы 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
Устанавливаем 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
Устанавливаем ядра:
# 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
Устанавливаем 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/
Устанавливаем корневую файловую систему:
# cp -ra /build/rootfs/. $TMP_ROOT
Проверяем, что мы сформировали необходимую структуру в файловых системах.
# tree $TMP_BOOT
# tree $TMP_ROOT
Размонтируем файловые системы:
# umount $TMP_ROOT
# umount $TMP_BOOT
Удаляем loop-устройства:
# kpartx -dv $LOOP_DEVICE
# losetup -d $LOOP_DEVICE
Выполняем следующие операции из операционной системы (не из 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-диске.
Наш минимальный Linux для взаимодействия с пользователем использует UART.
Использование UART — один из основных способов отладки и взаимодействия с большинством встраиваемых систем. На платах присутствуют контакты TX, RX и GND, через которые можно получить доступ к:
сообщениям загрузчика,
сообщениям ядра Linux,
интерактивной Linux-консоли.
Для подключения понадобится USB-UART адаптер и терминальная программа:
в Linux: picocom, minicom, screen,
в Windows: PuTTY.
USB-UART-адаптеры бывают на различных контроллерах и в разных форм-факторах. Я использовал китайский, как на фото выше.
Уровни логических сигналов TX/RX на плате должны совпадать с уровнями USB-UART адаптера. Большинство одноплатных компьютеров (и Raspberry Pi также) используют 3.3 В, тогда как некоторые конвертеры могут выдавать 5 В — такое подключение может повредить плату. Обязательно проверьте напряжения на сигнальных проводах вашего адаптера перед подключением к Raspberry Pi.
В Raspberry Pi 3 и Raspberry Pi 4 UART выведен на пины GPIO. Распиновка и схема подключения приведены ниже. Нас интересуют пины 6, 8, 10.
В Raspberry Pi 3 и Raspberry Pi 4 существует две реализации UART — 8250 и PL011. Они отличаются доступными скоростями передачи и управляющими регистрами. Я не хочу усложнять статью, поэтому мы используем только 8250.
Подключаем USB-UART к Raspberry Pi
Запускаем эмулятор терминала
$ sudo picocom -b 115200 /dev/ttyUSB0
Включаем Raspberry Pi.
Наблюдаем загрузку.
UART часто используется для простейшей отладки работы ядра Linux. Ниже я приведу сведения, которые, возможно, помогут вам, если «что-то пойдёт не так».
Для включения логирования в проприетарном загрузчике 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.
Если ядро упадёт до того, как проинициализируется 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
Нажмите здесь для печати.