Як модифікувати bootloader, додати власну команду і прошити без Maskrom — з живої системи через MTD.
* * *
Навіщо чіпати U-Boot?
Якщо ви запустили Luckfox Pico Pro і побачили рядки що мигтять у терміналі — ви вже бачили U-Boot. Він живе між увімкненням живлення і появою Linux shell. Більшість людей ніколи його не чіпає. Та в мене вже руки сверблять — дізнатись, як воно працює.
До того ж, embedded розробник повинен розуміти весь стек: від першого такту процесора до userspace. U-Boot — це C проект. Він компілюється, патчується і прошивається як будь-який інший код.
У цій статті ми зробимо три конкретні речі:
- змінимо bootdelay — щоб мати час зловити консоль без акробатики з Ctrl+C
- додамо checkboard() — власний рядок при старті через механізм weak функцій
- напишемо нову shell команду mystatus — 20 рядків C, нуль магії
І прошиємо все це прямо з живої Linux системи — без Maskrom, без USB кабелю.
⚠ Ця стаття про Luckfox Pico Pro з RV1106 і U-Boot 2017.09 (Rockchip fork). Якщо у вас інша плата — концепти ті самі, шляхи до файлів будуть різні.
* * *
◆ Частина 1. Що таке U-Boot і де він живе
Boot sequence на Luckfox
RV1106 — це Cortex-A7 з Rockchip ROM всередині. При увімкненні живлення процесор не знає де Linux, де файлова система, і взагалі нічого. Він знає тільки одне: прочитати код з фіксованої адреси в ROM і виконати його.
ROM code — це перший ступінь. Він ініціалізує мінімум заліза і шукає наступний завантажувач на SPI NAND (або eMMC, SD — залежно відBootROM straps). На Luckfox це SPI NAND.
Далі ланцюжок виглядає так:
|
1 2 3 4 5 6 7 8 9 |
ROM code (вбудований в чіп, незмінний) ↓ SPL / miniloader (ініціалізує DDR RAM) ↓ U-Boot proper (наш bootloader) ↓ Linux kernel ↓ init → userspace (ваш shell) |
U-Boot — третій ступінь. До нього RAM вже ініціалізована SPL, і U-Boot може завантажити kernel з NAND в RAM і передати керування.
MTD партиції: де що лежить
Вся SPI NAND на Luckfox розбита на MTD партиції. Подивитись можна так:
|
1 2 3 4 5 6 7 8 9 10 |
$ cat /proc/mtd dev: size erasesize name mtd0: 00040000 00020000 "env" mtd1: 00040000 00020000 "idblock" mtd2: 00080000 00020000 "uboot" mtd3: 00400000 00020000 "boot" mtd4: 01e00000 00020000 "oem" mtd5: 00a00000 00020000 "userdata" mtd6: 0d200000 00020000 "rootfs" |
mtd2 — це наш U-Boot, 512KB. Саме туди ми будемо прошивати. mtd1 (idblock) — це SPL з DDR init, його краще не чіпати без причини.
◆ MTD = Memory Technology Device. Це Linux підсистема для роботи з flash пам’яттю. /dev/mtd2 — символьний пристрій для прямого запису, /dev/mtdblock2 — блочний (як диск). Для прошивки bootloader використовуємо символьний.
* * *
Частина 2. Де живе код U-Boot
Структура Luckfox SDK
Luckfox SDK — це великий репозиторій де все вже зібрано: buildroot, kernel, u-boot, rkbin (бінарники від Rockchip). U-Boot живе тут:
|
1 |
~/luckfox-pico/sysdrv/source/uboot/u-boot/ |
Поруч є rkbin — це закриті бінарники від Rockchip (DDR init, TrustZone). Ми їх не чіпаємо, вони використовуються при пакуванні фінального uboot.img.
Defconfig і конфіг плати
U-Boot конфігурується через Kconfig — так само як Linux kernel. Для Luckfox Pico Pro defconfig знаходиться тут:
|
1 |
configs/luckfox_rv1106_uboot_defconfig |
Конфіг плати (board-специфічний хедер):
|
1 2 |
include/configs/rv1106_common.h include/configs/evb_rv1106.h |
І board-специфічний C файл:
|
1 |
board/rockchip/evb_rv1106/evb_rv1106.c |
Як збирається uboot.img
Luckfox SDK має скрипт build.sh з окремою ціллю для U-Boot:
|
1 2 |
cd ~/luckfox-pico ./build.sh uboot |
Після збірки готовий образ з’являється тут:
|
1 |
output/image/uboot.img |
Це вже запакований FIT image з підписом Rockchip — не просто бінарник U-Boot, а контейнер з SPL, U-Boot proper і TrustZone. Саме його ми прошиватимемо в mtd2.
⚠ ./build.sh uboot збирає тільки U-Boot і займає ~2 хвилини. ./build.sh all — весь SDK включно з rootfs, це може зайняти 20-40 хвилин. Для наших патчів достатньо першого.
* * *
Частина 3. Три патчі
Патч 1: bootdelay — дати собі час
За замовчуванням Luckfox стартує з bootdelay=0. Це означає що U-Boot не чекає взагалі — він одразу завантажує Linux без паузи на prompt. Щоб зловити консоль треба було тримати Ctrl+C ще до натискання RESET, не питайте як я до цього дійшов ).
Змінюємо один рядок в defconfig:
|
1 2 3 4 5 |
# configs/luckfox_rv1106_uboot_defconfig # Було: CONFIG_BOOTDELAY=0 # Стало: CONFIG_BOOTDELAY=3 |
Або однією командою:
|
1 2 |
sed -i 's/CONFIG_BOOTDELAY=0/CONFIG_BOOTDELAY=3/' \ configs/luckfox_rv1106_uboot_defconfig |
Тепер при старті буде 3 секунди:
|
1 2 3 |
Hit key to stop autoboot('CTRL+C'): 3 Hit key to stop autoboot('CTRL+C'): 2 Hit key to stop autoboot('CTRL+C'): 1 |
◆ bootdelay — це змінна оточення U-Boot. Її можна також змінити з консолі: setenv bootdelay 5 && saveenv. Але зміна в defconfig гарантує що після скидання env вона повернеться до вашого значення, а не до 0.
Патч 2: checkboard() — власний рядок при старті
U-Boot виводить інформацію при старті через ланцюжок функцій ініціалізації. Одна з них — show_board_info(), яка в свою чергу викликає checkboard(). Обидві оголошені як __weak в
|
1 2 3 4 5 6 |
common/board_info.c: // common/board_info.c int __weak checkboard(void) { return 0; // порожня реалізація за замовчуванням } |
__weak означає що компілятор візьме цю версію тільки якщо ніде більше немає визначення з таким іменем. Якщо ми додаємо свою checkboard() в board-специфічний файл — компілятор тихо замінює weak версію нашою. Ніяких #ifdef, ніяких патчів core коду.
Додаємо в кінець board/rockchip/evb_rv1106/evb_rv1106.c:
|
1 2 3 4 5 6 |
int checkboard(void) { printf("Board: Luckfox Pico Pro (custom build by Alex)\n"); printf("Series: STM32 з нуля без HAL — article 6\n"); return 0; } |
Результат при старті:
|
1 2 3 |
Model: Luckfox Pico Pro Max Board: Luckfox Pico Pro (custom build by Alex) Series: STM32 з нуля без HAL — article 6 |
◆ Model: береться автоматично з DTB — поле model в device tree. Board: — це вже наш checkboard(). Два різних механізми, два різних рядки, один boot log.
⚠ Не визначайте checkboard() в двох місцях одночасно — лінкер видасть помилку ‘multiple definition’. weak замінюється тільки одним strong визначенням.
Патч 3: команда mystatus — власний shell в U-Boot
U-Boot має вбудований командний інтерпретатор. Кожна команда — це окремий .c файл в директорії cmd/. Подивіться на version.c — це найпростіший шаблон:
|
1 2 3 4 5 6 7 8 9 10 11 |
// cmd/version.c (спрощено) #include <common.h> #include <command.h> static int do_version(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[]) { printf(display_options_get_banner(false, buf, sizeof(buf))); return 0; } U_BOOT_CMD(version, 1, 1, do_version, "print version", ""); |
Паттерн простий: функція do_xxx() + макрос U_BOOT_CMD(). Макрос реєструє команду в спеціальній секції .u_boot_list, звідки U-Boot збирає всі доступні команди при ініціалізації.
Створюємо cmd/mystatus.c:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/* * mystatus — custom command for Luckfox Pico Pro * Part of 'STM32 з нуля без HAL' article series */ #include <common.h> #include <command.h> static int do_mystatus(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[]) { DECLARE_GLOBAL_DATA_PTR; printf("\n=== Luckfox Pico Pro — System Status ===\n"); printf("U-Boot version : 2017.09 (custom patch)\n"); printf("CPU : RV1103/RV1106 (Cortex-A7)\n"); printf("DRAM : %lu MiB\n", gd->ram_size / 1024 / 1024); printf("Boot device : SPI NAND\n"); printf("Author : Alex (STM32 без HAL series)\n"); printf("=========================================\n\n"); return 0; } U_BOOT_CMD( mystatus, 1, 0, do_mystatus, "show Luckfox system status", " — prints CPU, RAM, boot device info" ); |
Реєструємо в cmd/Makefile — одним рядком:
|
1 |
obj-y += mystatus.o |
obj-y означає ‘завжди компілювати’, на відміну від obj-$(CONFIG_CMD_SOMETHING) де команда ввімкнена тільки якщо є відповідний CONFIG_ прапор. Для нашого випадку obj-y простіше.
|
1 |
◆ DECLARE_GLOBAL_DATA_PTR — це макрос який дає доступ до глобальної структури gd (global data). В ній U-Boot зберігає все що визначив при ініціалізації: розмір RAM, частоту CPU, стан периферії. gd->ram_size — реальний розмір RAM знайдений SPL при DDR init. |
* * *
◆ Частина 4. Збірка і прошивка
Збірка
Після всіх трьох патчів — збираємо:
|
1 2 |
cd ~/luckfox-pico ./build.sh uboot 2>&1 | tail -20 |
Успішна збірка виглядає так:
|
1 2 3 |
Image(no-signed, version=0): uboot.img (FIT with uboot, trust...) is ready pack uboot.img okay! [build.sh:info] Running build_uboot succeeded. |
Новий uboot.img лежить в output/image/uboot.img.
Прошивка з живої системи через MTD
Ось де стає цікаво. Більшість інструкцій кажуть: переведи плату в Maskrom (BOOT + USB), прошивай через upgrade_tool. Це працює, але якось все таки незручно — треба фізичний доступ до плати, USB кабель, і вся процедура займає певний час.
Але якщо Linux на платі вже запущений і є SSH — можна прошити U-Boot прямо по живому через MTD:
|
1 2 3 4 |
# Копіюємо новий uboot.img на плату scp output/image/uboot.img root@192.168.1.125:/tmp/ # Прошиваємо mtd2 = uboot партиція ssh root@192.168.1.125 'flashcp -v /tmp/uboot.img /dev/mtd2' |
flashcp — утиліта з пакету mtd-utils. Вона робить три кроки автоматично:
|
1 2 3 |
Erasing blocks: 2/2 (100%) Writing data: 256k/256k (100%) Verifying data: 256k/256k (100%) |
Після цього — reboot, і новий U-Boot запускається.
⚠ Прошивка U-Boot по живому — це ризик. Якщо живлення пропаде під час запису в mtd2, плата може не завантажитись. На Luckfox відновлення можливе через Maskrom + upgrade_tool, але це вже інша пригода. Робіть при стабільному живленні.
⚠ flashcp перевіряє розмір файлу перед записом. Якщо uboot.img більший за партицію mtd2 — він відмовить і викине помилку. 256KB образ в 512KB партицію — норм.
Перевірка в U-Boot консолі
Підключаємо UART (UART2 на Luckfox, 115200 baud) і дивимось boot log:
|
1 |
minicom -b 115200 -D /dev/ttyUSB0 |
При старті бачимо:
|
1 2 3 4 5 6 7 |
HASH(c): OK Some drivers failed to bind Model: Luckfox Pico Pro Max Board: Luckfox Pico Pro (custom build by Alex) Series: STM32 з нуля без HAL — article 6 Net: eth0: ethernet@ffa80000 Hit key to stop autoboot('CTRL+C'): 3 |
Натискаємо будь-яку клавішу — потрапляємо в U-Boot shell. Перевіряємо нову команду:
|
1 2 3 4 5 6 7 8 9 |
=> mystatus === Luckfox Pico Pro — System Status === U-Boot version : 2017.09 (custom patch) CPU : RV1103/RV1106 (Cortex-A7) DRAM : 128 MiB Boot device : SPI NAND Author : Alex (STM32 без HAL series) ========================================= |
128 MiB — реальне значення з gd->ram_size, яке SPL визначив при ініціалізації DDR. Це не константа в коді, а дані з живої системи.
* * *
◆ Частина 5. Що всередині uboot.img
FIT image
uboot.img — це не просто бінарник U-Boot. Це FIT (Flattened Image Tree) контейнер — той самий формат що використовується для Linux kernel + DTB. Всередині:
|
1 2 3 4 |
uboot.img ├── u-boot.bin (скомпільований U-Boot proper) ├── u-boot-spl.bin (Secondary Program Loader) └── trust.img (TrustZone / OP-TEE від Rockchip) |
SPL — це мінімальний завантажувач розміром ~50KB. Він ініціалізує DDR RAM і завантажує повний U-Boot. TrustZone — закритий бінарник від Rockchip для безпечного завантаження, ми його не чіпаємо.
Rockchip запаковує все це своїм скриптом після компіляції. Як бачимо ./build.sh uboot це не просто make, а ще й запаковка у фінальний образ.
Чому 256KB якщо партиція 512KB
mtd2 має розмір 512KB (0x80000), але наш uboot.img — 256KB. Це нормально. flashcp записує рівно стільки скільки є в файлі, решта партиції залишається як є (або заповнена 0xFF після erase). U-Boot при старті читає тільки потрібні байти.
◆ Якщо ви коли-небудь захочете прочитати поточний U-Boot з плати: dd if=/dev/mtd2 of=/tmp/uboot_backup.img bs=256k count=1. Корисно зробити бекап перед першим патчем.
* * *
◆ Підводні камені які зустрів
UART1 vs UART2: plug-and-pray
На Luckfox Pico Pro U-Boot console виведеео на UART2 (фізичні піни 1-2 на основному гребінці), а не на UART1. Документація про це мовчить. Якщо підключились до UART1 і бачите сміття або тишу — просто переключіть на сусідній роз’єм.
minicom Hardware Flow Control
За замовчуванням minicom вмикає Hardware Flow Control (RTS/CTS). CH340 адаптер підтримує тільки TX/RX без flow control — через це термінал або мовчить або показує сміття.
|
1 2 3 |
# В minicom: Ctrl+A → O → Serial port setup → F (Hardware Flow Control) → No # Або одразу при запуску через конфіг: minicom -b 115200 -D /dev/ttyUSB0 -o |
bootdelay=0 і Ctrl+C трюк
Якщо хочете зловити U-Boot prompt без зміни коду — є спосіб: затисніть Ctrl+C в minicom ще до натискання RESET на платі. U-Boot при старті перевіряє чи є символ в UART буфері, і якщо є Ctrl+C — зупиняється. Але це потребує точного таймінгу і не завжди спрацьовує.
Набагато надійніше просто змінити CONFIG_BOOTDELAY=3 як ми і зробили.
upgrade_tool і Maskrom
upgrade_tool очікує що плата вже в Rockusb режимі. Maskrom і Rockusb — різні режими. Якщо upgrade_tool каже ‘No found any rockusb device’ — потрібно спочатку відправити download.bin через Maskrom щоб плата перейшла в Rockusb:
|
1 2 3 |
sudo upgrade_tool DB output/image/download.bin # Після цього: sudo upgrade_tool UB output/image/uboot.img |
Але якщо плата вже запущена і є SSH — flashcp через MTD простіше і швидше.
BRLTTY і CH340 на Ubuntu 22.04
Ubuntu 22.04 встановлює BRLTTY (Braille display daemon) який перехоплює CH340 адаптер. Симптом: /dev/ttyUSB0 з’являється і одразу зникає, або permission denied. Вирішення:
|
1 2 3 4 |
sudo apt remove brltty # Або тільки відключити: sudo systemctl stop brltty sudo systemctl disable brltty |
⚠ Це класична пастка Ubuntu 22.04 для всіх хто працює з CH340, CP2102, FTDI адаптерами. Якщо /dev/ttyUSB0 не з’являється після підключення — перша підозра завжди BRLTTY.
* * *
Замість висновку
Ми пройшли весь шлях від де ‘U-Boot це чорна скринька’ до ‘U-Boot це C проект який я можу патчити і прошивати по живому на пряму із системи за 5 хвилин’.
Три патчі показали три різних підходи до модифікації:
- defconfig — один рядок змінює поведінку всього завантаження
- __weak функції — розширюємо поведінку без зміни core коду
- cmd/ + U_BOOT_CMD() — додаємо нову команду в 20 рядків
І прошивка через MTD з живої системи — це те що відрізняє ‘я читав про embedded’ від ‘я це робив’.
Наступна стаття — Device Tree. Будемо читати DTS файли Luckfox, розуміти compatible і reg, і робити перший overlay. Та не переживай — після U-Boot це буде легко.