Збираємо U-Boot та запускаємо свій C++ код на Orange Pi Zero 2W
Для одного з експериментів мені знадобилося перевірити, як саме операційна система Linux впливає на швидкість роботи із залізом. Для цього один і той самий алгоритм необхідно протестувати у двох середовищах: під керуванням Linux та в режимі bare-metal (без ОС). Крім того, мною рухав суто спортивний інтерес - було цікаво порівняти програмування мікроконтролерів і "дорослих" 64-бітних багатоядерних процесорів.
Для проведення тестів я обрав процесор Allwinner H618:
По-перше, він надзвичайно доступний. У моїй домашній embedded-лабораторії знайдеться з десяток пристроїв на цьому чипі: від ТВ-боксів до різноманітних SBC (Orange Pi, Walnut Pi тощо). Ще влітку 2025 роки плати на базі H618 можна було придбати за 600–800 грн, але звісно зараз це змінилось і ціни помітно зросли.
По-друге, це баланс потужності та периферії. Маючи на борту чотири ядра Cortex-A53, цей процесор не назвеш "гальмівним". Він достатньо спритний для серйозних завдань. Робота з UART, SPI, I2C або таймерами тут зрозуміла і добре задокументована (якщо не в офіційних мануалах, то в спільноті linux-sunxi).
Щоб запустити свій код на такому залізі, спочатку потрібно зрозуміти, як воно прокидається. На відміну від мікроконтролерів, де виконання коду зазвичай починається одразу з Flash-пам'яті, процес завантаження складних SoC — це багатоступеневий квест.
Після подачі живлення на процесор він починає виконувати програму, яка вшита в BootROM (або MaskROM). Цей код закладено безпосередньо в маску при виготовленні кристала чіпа, тому він не може бути змінений. Головна задача BootROM — знайти SPL (Secondary Program Loader) на зовнішньому носії (SD-карта, eMMC або NAND), завантажити його в SRAM-кеш та передати йому керування.
SPL починає роботу, коли оперативна пам’ять ще не ініціалізована (контролер DRAM не налаштований). Його основними задачами є:
Налаштування критичної периферії (контролер DRAM, тактування/PLL, UART-консоль).
Завантаження в ініціалізовану DRAM наступних компонентів: TF-A (Trusted Firmware-A) та U-Boot.
і далі передача керування на рівень TF-A.
TF-A (раніше це було ATF - ARM Trusted Firmware) відповідає за налаштування безпеки процесора, TrustZone, керування живленням (PSCI) тощо. Це досить об’ємна та складна тема, яка потребує окремого дослідження, тому не будемо заглиблюватися в деталі в межах цього опису. Після завершення ініціалізації безпечного середовища TF-A передає керування до U-Boot.
Що таке U-Boot - це проєкт із відкритим вихідним кодом, який став стандартом для ембедед систем. Спочатку він розроблявся для архітектури PowerPC, але згодом став універсальним завантажувачем для платформ на базі Arm, RISC-V, MIPS та інших.
U-Boot значно розширює можливості взаємодії із залізом:
- Інтерактивний командний рядок (CLI) - дозволяє гнучко налаштовувати сценарії завантаження, перевіряти стан пам'яті та керувати периферією в реальному часі.
- Підтримка додаткової периферії - U-Boot може працювати з пристроями, які зазвичай не підтримуються на рівні BootROM. Наприклад, завантажувати код через USB, Ethernet (NFS/PXE) або з NVMe-накопичувача.
- Робота з файловими системами - він розуміє FAT, ext4 та інші системи, що дозволяє просто скопіювати наш бінарник на картку, а не записувати його в сирі сектори.
Підготовка до збірки U-Boot
Щоб зібрати U-Boot, вам знадобиться Linux у будь-якому вигляді. Це може бути як основна ОС на вашому комп'ютері, так і віртуальна машина або віддалений VPS. Щодо WSL2 в Windows - теоретично це має працювати без проблем, проте я особисто цей варіант ще не перевіряв.
Вже зібраний образ можна записати на MicroSD-картку на будь-якій системі, де
доступна утиліта dd. Наприклад, частину цього матеріалу я писав на десктопі з
Arch Linux, а іншу частину — на MacBook Pro, використовуючи VPS з Ubuntu для самої
компіляції та перекидаючи готовий бінарник для прошивки.
Для Debian/Ubuntu ставимо наступні пакети:
sudo apt update
sudo apt install build-essential git bison flex libssl-dev \
gcc-aarch64-linux-gnu swig python3-dev bc device-tree-compiler \
libgnutls28-dev
Для ArchLinux:
sudo pacman -S base-devel git openssl aarch64-linux-gnu-gcc \
swig python-setuptools dtc bc gnutls
gcc-aarch64-linux-gnu - це компілятор для ARM64.
Збірка Trusted Firmware-A
Хоча код SPL та U-Boot знаходиться в одному репозиторії, TF-A — це окремий проєкт, який потрібно клонувати та збирати окремо. Офіційні вихідні коди доступні на GitHub:
https://github.com/TrustedFirmware-A/trusted-firmware-a.git
Іноді виробники чипів (наприклад, Rockchip) підтримують власні форки TF-A чи U-Boot із патчами, яких ще немає в основній гілці. Проте для H618 нам цілком вистачить стандартного репозиторію TF-A.
Отже, клонуємо репозиторій та переходимо до збірки:
# Клонуємо та заходимо в директорію
git clone https://github.com/TrustedFirmware-A/trusted-firmware-a.git
cd trusted-firmware-a/
# Встановлюємо префікс крос-компілятора
export CROSS_COMPILE=aarch64-linux-gnu-
# Запускаємо збірку. Для H618 використовується платформа sun50i_h616
make PLAT=sun50i_h616 DEBUG=1 bl31
Компіляція пройде досить швидко, після чого ми отримаємо необхідний
файл: build/sun50i_h616/debug/bl31.bin.
Важливо: Якось я помилився з налаштуванням PLAT — чи
то взяв дані з невдалого мануалу, чи просто був неуважним — і вказав
sun50i_a64. Все скомпілювалося без помилок, U-Boot теж
зібрався, але під час завантаження система йшла в астрал одразу після
SPL. Витратив чимало часу, поки помітив цю прикру помилку. Після
перезбірки з правильним параметром нарешті отримав доступ до консолі
U-Boot.
Збираємо SPL та UBoot
Клонуємо офіційний репозиторій з GitHub: https://github.com/u-boot/u-boot
# Клонуємо та переходимо в директорію
git clone https://github.com/u-boot/u-boot
cd u-boot/
# Налаштовуємо змінні середовища для крос-компіляції
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
В U-Boot існують готові конфігурації для різних одноплатних
комп’ютерів, переглянути які можна в папці configs/. Нас
цікавлять конфігурації для плат Orange Pi:
$ ls -1 configs/orangepi*
configs/orangepi-3b-rk3566_defconfig
configs/orangepi-5-max-rk3588_defconfig
...
configs/orangepi_zero2_defconfig
configs/orangepi_zero2w_defconfig
configs/orangepi_zero3_defconfig
configs/orangepi_zero_defconfig
configs/orangepi_zero_plus2_defconfig
configs/orangepi_zero_plus2_h3_defconfig
configs/orangepi_zero_plus_defconfig
Для прикладу я використовуватиму Orange Pi Zero 2W, тому запускаю конфігурацію наступною командою:
make orangepi_zero2w_defconfig
У результаті буде створено файл .config з параметрами
майбутньої збірки. Тепер можна запускати процес компіляції. У параметрі
BL31 необхідно вказати шлях до бінарного файлу TF-A
(Trusted Firmware-A):
make BL31=../trusted-firmware-a/build/sun50i_h616/debug/bl31.bin -j$(nproc)
Після успішного завершення операції у кореневій директорії з’явиться
файл u-boot-sunxi-with-spl.bin:
$ ls -l u-boot-sunxi-with-spl.bin
-rw-r--r-- 1 mrco mrco 890309 Mar 18 19:22 u-boot-sunxi-with-spl.bin
Можливі проблеми:
Image ‘u-boot-sunxi-with-spl’ is missing external blobs and is non-functional: atf-bl31 - ви не вказали або вказали неправильний шлях до файлу
bl31.bin(TF-A).Відсутність утиліт — якщо збірка переривається помилкою “command not found”, перевірте, чи встановлені всі необхідні залежності (swig, python3-dev, bison, flex).
Залежність від версії ОС — у неофіційних форках (я з цим зтикнувся у випадку з Luckfox) часто потрібне специфічне - застаріле оточення. Якщо компіляція не проходить на свіжій системі, найпростішим рішенням буде використання Docker або віртуальної машини зі старішою версією Ubuntu.
Прошивка завантажувача на SD-картку
Allwinner BROM шукає завантажувач (SPL) на картці по фіксованому зміщенню 8 КБ (16 секторі1в).
Обережно! Будьте надзвичайно уважними при виборі цільового диска. Помилка в назві пристрою (/dev/sdX або rdiskN) може призвести до повної втрати даних на вашому основному накопичувачі.
Спочатку визначимо шлях до нашої картки:
## MacOS
diskutil list
## Linux
lsblk
Щоб уникнути конфліктів зі старими таблицями розділів або залишками даних, зануляємо перші 10 МБ картки:
## linux
sudo dd if=/dev/zero of=/dev/sdb bs=1M count=10
## macos
sudo dd if=/dev/zero of=/dev/rdiskN bs=1m count=10 status=progress
Записуємо зібраний файл u-boot-sunxi-with-spl.bin із відступом 8 КБ (seek=8 при блоці 1024 байти):
## macos
sudo dd if=u-boot-sunxi-with-spl.bin of=/dev/rdiskN bs=1024 seek=8 status=progress
## linux
sudo dd if=u-boot-sunxi-with-spl.bin of=/dev/sdb bs=1024 seek=8 conv=fsync
Після завершення запису картку можна витягувати. Оскільки ми не створювали та не монтували файлову систему, додаткове розмонтування (unmount) не потрібне.
Під’єднуємо UART-адаптер до пінів GND, TX та RX (UART0) Orange Pi Zero 2w та відкриваємо UART консоль через screen, швидкість 115200:
screen /dev/ttyXXX 115200
Якщо все зроблено правильно, після подачі живлення має зʼявитись лог успішного завантаження SPL, TF-A та U-Boot:
U-Boot SPL 2026.04-rc4-00006-geefb822fb574 (Mar 18 2026 - 16:21:53 +0200)
DRAM: 2048 MiB
Trying to boot from MMC1
NOTICE: BL31: v2.14.0(debug):sandbox/v2.14-755-g2adf0f434
NOTICE: BL31: Built : 17:13:38, Mar 15 2026
NOTICE: BL31: Detected Allwinner H616 SoC (1823)
NOTICE: BL31: Found U-Boot DTB at 0x4a0b8d68, model: OrangePi Zero 2W
INFO: ARM GICv2 driver initialized
INFO: Configuring SPC Controller
INFO: Probing for PMIC on I2C:
INFO: PMIC: found AXP313
INFO: BL31: Platform setup done
INFO: BL31: Initializing runtime services
INFO: BL31: cortex_a53: CPU workaround for erratum 855873 was applied
INFO: BL31: cortex_a53: CPU workaround for erratum 1530924 was applied
INFO: PSCI: Suspend is unavailable
INFO: BL31: Preparing for EL3 exit to normal world
INFO: Entry point address = 0x4a000000
INFO: SPSR = 0x3c9
INFO: Changed devicetree.
U-Boot 2026.04-rc4-00006-geefb822fb574 (Mar 18 2026 - 16:21:53 +0200) Allwinner Technology
CPU: Allwinner H616 (SUN50I)
Model: OrangePi Zero 2W
DRAM: 2 GiB
Core: 63 devices, 24 uclasses, devicetree: separate
WDT: Not starting watchdog@30090a0
MMC: mmc@4020000: 0
Loading Environment from FAT... Unable to use mmc 0:0...
In: serial@5000000
Out: serial@5000000
Err: serial@5000000
Allwinner mUSB OTG (Peripheral)
Net: using musb-hdrc, OUT ep1out IN ep1in STATUS ep2in
MAC de:ad:be:ef:00:01
HOST MAC de:ad:be:ef:00:00
RNDIS ready
eth0: usb_ether
starting USB...
USB EHCI 1.00
USB OHCI 1.0
Bus usb@5200000: 1 USB Device(s) found
Bus usb@5200400: 1 USB Device(s) found
scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot: 0
=>
Helloworld на C++
Зробимо останній штрих - запустимо програму на C++ у режимі bare-metal, тобто безпосередньо на «залізі» без жодної операційної системи.
Оскільки U-Boot написаний переважно на C та асемблері, раніше нам було достатньо лише C-компілятора. Для збірки коду на C++ під архітектуру Arm64 необхідно встановити додатковий пакет:
sudo apt install g++-aarch64-linux-gnu
Процесор H618 використовує архітектуру Memory-Mapped Input/Output (MMIO). Тому, щоб вивести дані в UART, нам потрібно записати їх за відповідною фізичною адресою в пам’яті.
Щоб дізнатися цю адресу, необхідна офіційна документація. Для чипа H618 її у вільному доступі немає, проте можна використовувати документацію від практично ідентичного процесора H616: H616 User Manual V1.0.
Відповідно до мануалу, базова периферія UART0 має виділений блок пам’яті за адресою (9.2.5. Register List) 0x05000000 - 0x050003FF.
Для базового виводу тексту нам знадобляться лише два регістри:
Transmit Holding Register (THR) (зміщення 0x00): усе, що ми записуємо в цей регістр, буде відправлено в лінію UART.
Line Status Register (LSR) (зміщення 0x14): у ньому нас цікавить 5-й біт — TX Holding Register Empty (THRE). Оскільки процесор працює набагато швидше за UART, перед записом кожного наступного байта в THR необхідно в циклі перевіряти цей біт і чекати, поки буфер передавача не звільниться.
Код для нашого HelloWorld.cpp:
#include <stdint.h>
#define UART0_BASE 0x05000000
#define UART0_THR (*(volatile uint32_t*)(UART0_BASE + 0x00))
#define UART0_LSR (*(volatile uint32_t*)(UART0_BASE + 0x14))
#define LSR_THRE (1 << 5)
void uart_putc(char c) {
while ((UART0_LSR & LSR_THRE) == 0);
UART0_THR = c;
}
void uart_print(const char* str) {
while (*str) {
if (*str == '\n') uart_putc('\r');
uart_putc(*str++);
}
}
extern "C" __attribute__((section(".text.boot"))) void _start() {
uart_print("\n\n");
uart_print("================================\n");
uart_print(" Hello from C++ \n");
uart_print("================================\n");
// Let's wait a bit
for (volatile int i = 0; i < 10000000; i++) {}
uart_print("Returning control back to U-Boot...\n\n");
// return to the U-Boot
return;
}
У процесорах Allwinner H616/H618 початок оперативної пам’яті жорстко прив’язаний до фізичної адреси 0x40000000 (згідно з розділом 3.1 Memory Mapping технічного мануалу).
Будь-яка адреса, менша за цю (наприклад, 0x05000000 для UART), веде не до оперативної пам’яті, а до регістрів периферії або внутрішньої пам’яті чипа (SRAM). Ми не завантажуємо код безпосередньо в 0x40000000, оскільки перші мегабайти DRAM зазвичай уже зарезервовані під потреби U-Boot: там розміщуються таблиці сторінок MMU, глобальний стек та інші службові структури.
Для безпечної роботи необхідний відступ. У світі ARM64 прийнято завантажувати ядро саме зі зміщенням у 2 МБ — за адресою 0x40200000. Ми дотримуватимемося цього стандарту і для нашого Hello World, тож конфігурація скрипта лінкувальника (linker.ld) виглядатиме так:
ENTRY(_start)
SECTIONS
{
. = 0x40200000;
.text : {
*(.text.boot)
*(.text*)
}
.rodata : { *(.rodata*) }
.data : { *(.data*) }
.bss : {
__bss_start = .;
*(.bss*)
__bss_end = .;
}
}
Для збірки використовуємо крос-компілятор. Оскільки ми пишемо для “голого заліза” нам потрібно вимкнути стандартні бібліотеки, обробку винятків та RTTI. Точніше тут справа не в не тому, що процесор їх не тягне, а тому, що в нашому мікро-коді ще немає реалізації (керування пам’яттю та обробки помилок), на який ці функції спираються
# Компіляція об'єктного файлу
aarch64-linux-gnu-g++ -ffreestanding -fno-exceptions -fno-rtti -c helloworld.cpp -o helloworld.o
# Лінкування згідно з нашою картою пам'яті
aarch64-linux-gnu-ld -T linker.ld helloworld.o -o helloworld.elf
# Створення чистого бінарного образу (без заголовків ELF)
aarch64-linux-gnu-objcopy -O binary helloworld.elf helloworld.bin
Оскільки наш U-Boot займає близько 800 КБ на початку диска, ми запишемо наш бінарник на картку із безпечним відступом в 1 МБ. Це рівно 2048 (0x0800) секторів по 512 байт.
sudo dd if=helloworld.bin of=/dev/... bs=512 seek=2048 conv=notrunc
Після подачі живлення зупиняємо автозавантаження натисканням будь-якої клавіші та вводимо наступні команди:
mmc dev 0- вибираємо SD-картку як поточний пристрій.mmc read 0x40200000 0x0800 0x1- зчитуємо 1 сектор із відступу 1 МБ (0x800) в оперативну пам’ять за нашою базовою адресою 0x40200000.go 0x40200000- передаємо керування завантаженому коду.
Результат виконання в UART консолі:
Програму-мінімум виконано! Ми написали та запустили Helloworld на чистому залізі без операційної системи. Звісно завдяки U-Boot який взяв на себе всю складну роботу з ініціалізації контролера пам’яті DRAM та налаштування частот процесора, дозволивши нам зосередитися на логіці програми.



Коментарі
Дописати коментар