Серія “STM32 з нуля без HAL” • Місяць 3 • Тижні 1-2
Luckfox Pico Pro як мозок, STM32 як м’язи — перша справжня embedded Linux архітектура
Про що ця стаття
Привіт! Ми продовжуємо серію про STM32 без HAL. В минулих статтях ми розібрали Buildroot, зібрали свій Linux-образ, навчились додавати пакети і розібрали U-Boot. Тепер час для наступного кроку — Device Tree.
Років 10 тому я робив домашню автоматизацію на Arduino Pro Mini з W5500 Ethernet шилдом. Паяв модулі, підключав RJ45, писав скетчі. Щоб мати Ethernet на мікроконтролері — потрібен був окремий чіп, окрема плата, окремий стек бібліотек.

А тепер Luckfox Pico Pro за $15 має Ethernet вбудований, повноцінний Linux з Python, SSH, веб-сервером, і ще NPU для нейромереж зверху. Шлях від паяння Ethernet модуля до вбудованого мережевого стеку — це і є еволюція embedded розробки. І ця серія статей — спроба цю еволюцію пройти і задокументувати.

Розум. Код. Характер. І кожен байт — це крок до перемоги. З таким девізом ми йдемо далі.
Device Tree — це те, без чого не працює жоден embedded Linux пристрій на ARM. Але коли я вперше відкрив DTS файл, то побачив купу рядків з hex-адресами і подумав: “мда, з розбігу не зазкочити”. А потім згадав, що на STM32 я писав ті ж самі hex-адреси руками — і все стало на місця.
В цій статті ми:
• Розберемо DTS файл Luckfox Pico Pro рядок за рядком
• Порівняємо кожну DTS властивість з тим, що ми робили на STM32 bare-metal
• Змінимо один рядок в DTS — і отримаємо новий serial порт
• Підключимо STM32 до Luckfox через UART
• Напишемо Python скрипт, який керує STM32 з Linux
• І трохи зазирнемо в NPU — що це за звір на борту нашої плати
Частина 1. Device Tree — що це і навіщо
Проблема: Linux не знає що на платі
На STM32 ми писали прямо в регістри:
|
1 2 3 |
#define USART1_BASE 0x40013800 #define USART1_CR1 (*(volatile uint32_t *)(USART1_BASE + 0x0C)) RCC_APB2ENR |= (1 << 14); // тактування USART1 |
Ми знали адресу кожного регістра, бо працювали з одним конкретним чіпом. Але Linux — універсальне ядро. Воно працює на тисячах різних плат. Звідки йому знати, що на нашому Luckfox UART2 сидить за адресою 0xff4c0000, а не 0x40013800 як на STM32?
Відповідь — Device Tree. Це файл, який описує все залізо на платі: які контролери є, за якими адресами, які переривання використовують, які піни зайняті. Ядро читає цей файл при завантаженні і знає з чим працювати.
Структура файлів: як матрьошка
Відкриваємо DTS нашої плати і бачимо три include:
|
1 2 3 4 |
// rv1106g-luckfox-pico-pro-max.dts #include "rv1106.dtsi" // SoC: всі UART, SPI, I2C #include "rv1106-evb.dtsi" // базова EVB конфігурація #include "rv1106-luckfox-pico-pro-max-ipc.dtsi" // специфіка Luckfox |
Це як в нашому HAL: rv1106.dtsi — це hal.h де описані всі можливості чіпа, а DTS плати — це main.c де ми обираємо що використовувати.
Root node: хто ми такі
|
1 2 3 4 |
/ { model = "Luckfox Pico Pro Max"; compatible = "rockchip,rv1103g-38x38-ipc-v10", "rockchip,rv1106g3"; }; |
model — людська назва плати. compatible — список від конкретного до загального. Ядро спочатку шукає драйвер для rv1103g-38×38-ipc-v10, не знайшло — пробує rv1106g3. Як fallback.
UART2 в DTS vs USART1 на STM32
Ось опис UART2 в базовому rv1106.dtsi:
|
1 2 3 4 5 6 7 8 9 |
uart2: serial@ff4c0000 { compatible = "rockchip,rv1106-uart", "snps,dw-apb-uart"; reg = <0xff4c0000 0x100>; interrupts = <GIC_SPI 27 IRQ_TYPE_LEVEL_HIGH>; clock-frequency = <24000000>; clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>; pinctrl-0 = <&uart2m1_xfer>; status = "disabled"; }; |
Тепер порівняємо з тим, що ми робили на STM32 в нашому hal_uart.c:
reg = <0xff4c0000 0x100> — базова адреса і розмір блоку регістрів. На STM32 ми писали #define USART1_BASE 0x40013800. Та сама ідея, тільки в DTS ядро зробить ioremap() замість прямого доступу.
interrupts = <GIC_SPI 27 …> — UART2 використовує переривання 27 на GIC. На STM32 ми знали що USART1 = IRQ37 і писали NVIC_ISER1 |= (1 << 5). Тут замість NVIC — GIC (бо Cortex-A7 замість Cortex-M3), але ідея та сама.
clock-frequency = <24000000> — вхідна частота 24MHz. У нас на STM32 було 8000000UL / baud для розрахунку BRR. Тут ядро зробить аналогічний розрахунок, тільки з 24MHz.
clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2> — два клоки від CRU. Як ми робили RCC_APB2ENR |= (1 << 14) для ввімкнення тактування USART1. Ядро зробить це автоматично через clock framework.
pinctrl-0 = <&uart2m1_xfer> — мультиплексор пінів. Як ми ставили PA9 в alternate function (0xB) для TX. Тут ядро зробить це через pinctrl підсистему.
status = “disabled” — за замовчуванням вимкнений. В базовому DTSI ВСІ контролери disabled. Плата вмикає тільки те що реально підключено. Як на STM32 — периферія є в чіпі, але без тактування не працює.
⚠ Бачите 0xff4c0000? Ця ж адреса з’являється в bootargs ядра: earlycon=uart8250,mmio32,0xff4c0000. DTS описує адресу, bootargs використовує її для ранньої консолі. Коло замкнулося.
Pinctrl: як DTS описує фізичні піни
|
1 2 3 4 5 |
uart2m1_xfer: uart2m1-xfer { rockchip,pins = <1 RK_PB3 2 &pcfg_pull_up>, // UART2 RX <1 RK_PB2 2 &pcfg_pull_up>; // UART2 TX }; |
Формат: <bank pin function pull_config>. Bank 1, Port B pin 2, alternate function 2 = UART2 TX, підтяжка вгору. Порівняйте з нашим STM32 кодом:
|
1 2 |
// STM32: PA9 = TX = Alternate Function Push-Pull gpio_init(PIN_PA9, 0xB); |
Та ж ідея — ми ставили PA9 в alternate function для UART TX. Тут DTS каже: “GPIO1_B2 — це alternate function 2, що означає UART2 TX”. Тільки на STM32 ми знали це з даташиту і прописували вручну, а тут ядро читає DTS і робить все автоматично.
M0, M1, M2 — варіанти пінів, але не свобода
M1 в назві — це варіант розводки пінів (mux variant). RV1106 дозволяє вивести один і той же UART на різні фізичні піни. На STM32F103 аналог — AFIO_MAPR remap (наприклад USART1 можна перекинути з PA9/PA10 на PB6/PB7).
Але є важливий нюанс: ви не можете довільно призначити будь-який пін на будь-яку функцію. Варіанти M0/M1/M2 — це фіксовані комбінації, зашиті в кремнії чіпа. Можна обрати тільки з того що виробник передбачив.
Ми це з’ясували на практиці. На нашій платі конектор SIM800L потрапляє на піни 11-12. Пін 11 — це UART0_TX_M1 (є TX). А пін 12 — SPI0_CS0_M0 (це вже зовсім інша периферія, не RX). Виникло питання: а може є варіант M0 або M2 де UART0_RX потрапить на потрібний пін?
Перевіряємо всі варіанти в pinctrl:
|
1 2 3 4 5 6 |
$ grep -A8 "uart0m" rv1106-pinctrl.dtsi uart0m0_xfer: TX = GPIO0_A1, RX = GPIO0_A0 // не на гребінці uart0m1_xfer: TX = GPIO2_B1, RX = GPIO2_B0 // TX на піні 11, // але RX не виведений! uart0m2_xfer: TX = GPIO4_A1, RX = GPIO4_A0 // не на гребінці |
Жоден варіант не кладе UART0_RX на пін 12. TX є, а RX фізично не доступний поруч. Software UART (bit-bang RX) на Linux — це не варіант: планувальник може в будь-який момент забрати CPU на інший процес, і ми втрачаємо біти. На bare-metal STM32 можна крутитись в tight loop і ловити кожен біт з мікросекундною точністю, але Linux — не RTOS.
⚠ Висновок: коли обираєте плату або проектуєте свою PCB для Luckfox — спочатку перевірте pinctrl в DTS. Варіанти M0/M1/M2 фіксовані, і якщо потрібний пін не виведений на гребінку — ніякий DTS overlay не допоможе.
Для SIM800L рішення просте — підключаємо через інший конектор (U3 → UART1), де TX і RX поруч. А конектор SIM800L використовуємо для чогось що потребує тільки TX.
Консоль: FIQ Debugger
Цікава знахідка: ми шукали де вмикається UART2 для консолі і не знайшли звичайного status = “okay”. Виявилось, Rockchip використовує FIQ Debugger:
|
1 2 3 4 |
fiq-debugger { compatible = "rockchip,fiq-debugger"; status = "okay"; }; |
FIQ (Fast Interrupt Request) — переривання з вищим пріоритетом ніж звичайний IRQ. Rockchip перехоплює UART2 через FIQ, щоб консоль працювала навіть коли ядро зависло. Тому консоль — це ttyFIQ0, а не ttyS2.
⚠ Це означає що UART2 зайнятий. Для зв’язку зі STM32 потрібно використовувати інший UART — і ось тут починається найцікавіше.
Частина 2. Один рядок в DTS — і все працює
Знаходимо вільні UART
В DTS нашої плати бачимо:
|
1 2 |
&uart3 { status = "disabled"; }; &uart4 { status = "disabled"; }; |
Обидва вимкнені. Дивимось на pinout Luckfox — UART3 виведений на піни 19-20 (конектор U4 на нашій платі), UART4 — на піни 6-7 (гребінка U11). Ідеально.
Що робимо? Змінюємо один рядок:
|
1 2 3 4 |
/* UART3_M1 */ &uart3 { status = "okay"; }; |
Pinctrl вже прописаний в базовому rv1106.dtsi (pinctrl-0 = <&uart3m1_xfer>), тому більше нічого додавати не треба.
Збираємо і прошиваємо
|
1 2 3 4 5 6 7 8 9 10 11 |
# Збираємо ядро (включає компіляцію DTS) cd ~/luckfox-pico ./build.sh kernel # Копіюємо boot.img на плату scp -o PubkeyAuthentication=no output/image/boot.img \ root@192.168.1.125:/tmp/ # Прошиваємо тільки boot розділ (mtd3) і перезавантажуємо ssh -o PubkeyAuthentication=no root@192.168.1.125 \ "flashcp /tmp/boot.img /dev/mtd3 && reboot" |
⚠ Чому flashcp і mtd3? На Luckfox flash пам’ять поділена на MTD розділи. MTD (Memory Technology Devices) — це підсистема Linux для роботи з flash напряму. Boot розділ (ядро + DTB) сидить на mtd3. flashcp стирає блок і записує нове ядро. Це як st-flash write firmware.bin 0x08000000 на STM32 — та ж ідея, тільки через Linux підсистему.
Повна розмітка MTD на Luckfox:
|
1 2 3 4 5 6 7 |
mtd0 = env (256K) ← U-Boot змінні mtd1 = idblock (256K) ← Rockchip ID block mtd2 = uboot (512K) ← U-Boot mtd3 = boot (4M) ← ядро + DTB ☆ ми прошиваємо сюди mtd4 = oem (30M) ← OEM дані mtd5 = userdata (10M) ← дані mtd6 = rootfs (210M) ← файлова система (UBIFS) |
Перевіряємо результат
Після ребуту — один SSH запит і бачимо:
|
1 2 3 4 5 |
$ cat /proc/device-tree/serial@ff4d0000/status okay $ ls /dev/ttyS* /dev/ttyS3 |
Один рядок в DTS — і з’явився /dev/ttyS3. Ядро побачило status = “okay”, завантажило драйвер snps,dw-apb-uart, зробило ioremap(0xff4d0000), налаштувало pinctrl, і створило пристрій. Все те що ми робили на STM32 руками за десятки рядків коду — одна декларативна властивість.
Вмикаємо всі три UART
Раз вже розібрались — вмикаємо одразу три UART для максимальної гнучкості:
|
1 2 3 4 5 6 7 8 |
/* UART1_M1 */ &uart1 { status = "okay"; }; /* UART3_M1 */ &uart3 { status = "okay"; }; /* UART4_M1 */ &uart4 { status = "okay"; }; |
Перезбираємо, прошиваємо, і тепер:
|
1 2 |
$ ls /dev/ttyS* /dev/ttyS1 /dev/ttyS3 /dev/ttyS4 |
Три serial порти для периферії. Підключай три STM32, або STM32 + SIM800L + щось ще.
Частина 3. Плата Pi Pico Sim800 — як макетка для Luckfox

У мене є кастомна плата Pi Pico Sim800 v3.1 — розроблена під Raspberry Pi Pico для IoT проекту з SIM800L і реле (про цей проект — окрема стаття на DOU). Luckfox Pico Pro має такий же форм-фактор і гребінку 2×20, тому вставляється замість Pi Pico.

Але є нюанс — піни мають різні функції. На Pi Pico пін 19 — це GP14 (I2C SDA), а на Luckfox — це UART3_TX. Тому я зробив повний маппінг цих двох плат може комусь теж буде корисно.
Raspberry Pi Pi Pico

LuckFox pico

Що реально розпаяно на платі

Плата — це по суті breakout board з конекторами. Єдине що розпаяно постійно — зсувний регістр SN74HC595N на пінах 4, 5, 9 (DATA, LATCH, CLOCK), з якого виведено 8 реле. Решта — знімні конектори для модулів: SIM800L, BMP180, PIR.
Маппінг конекторів
Ключова таблиця — які конектори плати відповідають яким UART на Luckfox:
U4 (BMP180 #1), піни 19-20 → UART3 — основний для STM32
U3 (BMP180 #2), піни 21-22 → UART1 — другий STM32 або SIM800L
U11 (гребінка), піни 6-7 → UART4 — додаткова периферія
Піни 1-2 → UART2 (FIQ debugger) — зайнятий консоллю
Крім UART, на гребінці U11 є повний SPI0 (піни 14-17) — для дисплеїв, flash, тощо.

SIM800L конектор — половинка UART
Конектор SIM800L (піни 11-12) потрапляє на UART0_TX і SPI0_CS0 на Luckfox. TX є, а RX на сусідньому піні немає. Software UART на Linux, як на Ардуїно робити ризиковано бо планувальник може забрати CPU і буде втрачено біти. Тому у моєму випадку для SIM800L краще використати інший конектор — наприклад U3 (UART1).
Зсувний регістр — GPIO bit-bangSN74HC595N підключений до пінів 4, 5, 9. На Luckfox ці піни не збігаються з SPI контролером, тому апаратний SPI не вийде. Але GPIO bit-bang працює чудово — для 8 реле швидкості вистачить з головою.

Це тема для окремої статті — як керувати зсувним регістром з Linux через sysfs.
Частина 4. Підключаємо STM32 — перший міст

Ніяких паразитних сигналів, тільки чистий UART.
Фізичне підключення
|
1 2 3 |
Luckfox U4 pin 19 (UART3_TX) → STM32 PA10 (USART1_RX) Luckfox U4 pin 20 (UART3_RX) ← STM32 PA9 (USART1_TX) GND ↔ GND |
Обидві плати 3.3V — конвертер рівнів не потрібен. Ті самі три дроти що раніше йшли до CH340 USB-TTL адаптера, тепер йдуть прямо в Luckfox.
STM32 сторона — нічого не міняємо
На Blue Pill працює той самий код з нашого HAL — uart_init(9600), uart_gets(), uart_puts(). STM32 не знає і не повинен знати що на іншому кінці замість minicom тепер Linux. Для нього це просто UART.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// main.c на STM32 — без змін з попередніх статей hal_init(); uart_init(9600); gpio_init(PIN_PC13, OUTPUT); while (1) { uart_gets(buf, 16); if (buf[0] == 'o' && buf[1] == 'n') { gpio_write(PIN_PC13, LOW); uart_puts("LED on\r\n"); } } |
Linux сторона — перший тест
З Luckfox по SSH:
|
1 2 3 4 5 6 |
# Налаштувати raw режим stty -F /dev/ttyS3 9600 raw -echo # Відправити команду і побачити відповідь echo -ne "on\r" > /dev/ttyS3 cat /dev/ttyS3 |
LED на Blue Pill загорівся, в терміналі з’явилось “LED on”. Luckfox керує STM32. Мозок командує м’язами.
Частина 5. Python контролер — робимо по-людськи
echo в /dev/ttyS3 працює, але це не серйозно. Пишемо нормальний Python скрипт:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#!/usr/bin/env python3 import serial, sys, time def send_command(ser, cmd): ser.write((cmd + "\r").encode()) time.sleep(0.1) response = "" while ser.in_waiting: response += ser.read(ser.in_waiting).decode(errors="ignore") time.sleep(0.05) return response.strip() ser = serial.Serial("/dev/ttyS3", 9600, timeout=2) # Одна команда або інтерактивний режим if len(sys.argv) > 1: print(send_command(ser, sys.argv[1])) else: while True: cmd = input("stm32> ").strip() if cmd == "quit": break resp = send_command(ser, cmd) if resp: print(f" ← {resp}") |
Скрипт підтримує змінну оточення STM32_PORT для роботи з різними UART:
|
1 2 3 4 5 6 7 8 |
# Основний UART (U4) python3 stm32_controller.py on # Через UART1 (U3) STM32_PORT=/dev/ttyS1 python3 stm32_controller.py blink 3 # Через UART4 (U11) STM32_PORT=/dev/ttyS4 python3 stm32_controller.py off |
Тестуємо blink — три рази блимнути LED:
|
1 2 3 4 5 6 7 8 9 |
$ python3 stm32_controller.py blink 3 Блимаємо 3 разів... [1/3] ON → LED on [1/3] OFF → LED off [2/3] ON → LED on [2/3] OFF → LED off [3/3] ON → LED on [3/3] OFF → LED off Готово! |
Linux керує STM32 через Python. Це вже не мигання LED — це архітектура. Мозок (Luckfox з Linux) дає команди м’язам (STM32 з bare-metal прошивкою).
Частина 6. А що тут за NPU?
Поки розбирались з DTS, звернули увагу на цікаву ноду:
|
1 2 3 4 |
npu@ff660000 { compatible = "rockchip,rknpu"; ... }; |
NPU — Neural Processing Unit. На нашій $15 платі є нейроприскорювач на 0.5 TOPS. Перевіряємо:
|
1 2 3 4 5 6 7 8 9 |
$ ls /dev/rknpu /dev/rknpu $ dmesg | grep -i npu RKNPU ff660000.npu: Initialized RKNPU driver v0.9.2 $ find /oem -name '*.rknn' $ ls /oem/usr/lib/librknn* /oem/usr/lib/librknnmrt.so |
Драйвер працює, C бібліотека є. А в SDK Luckfox знаходимо проект rk_smart_door з готовими моделями нейромереж: детекція облич, розпізнавання, визначення повороту голови. Python binding, правда, немає — тільки C API через rknn_api.h.
Це відкриває цікаву перспективу: камера на Luckfox знімає, NPU розпізнає обличчя, через UART дає команду STM32, а той вмикає реле через зсувний регістр. Повний ланцюжок від камери до фізичної дії — на двох платах за $20.
Але це вже тема наступної статті. Поки що залишаємо NPU як інтригу.
Філософія: SOLID для заліза

Є спокуса одразу будувати щось велике — розумний будинок, систему відеоспостереження, автономного робота. Але великі проекти вбивають мотивацію, бо результат далеко, а проблеми — ось вони, прямо зараз.
Ми в цій серії дотримуємось іншого підходу, який нагадує принцип єдиної відповідальності з SOLID: одна задача — один проект. Кожен проект маленький, закінчений і самодостатній. Але з інтерфейсами для з’єднання з іншими.
Подивіться що ми вже маємо:
• STM32 HAL — bare-metal бібліотека з чистим UART інтерфейсом. Не знає нічого про Linux.
• Buildroot образ — Linux система з Go сервером, SSH, Python. Не знає нічого про STM32.
• Device Tree — один рядок status = “okay” з’єднує ці два світи через /dev/ttyS3.
• Python контролер — абстракція над UART. send_command(“on”) і все.
• Плата Pi Pico Sim800 — фізична абстракція. Конектори як інтерфейси: встроми модуль і він працює.
Кожен шматочок можна використати окремо. Але разом вони починають складатись в систему: IoT розетка з SMS-керуванням (Pi Pico + SIM800L) → IP-камера зі стрімом (Luckfox) → Linux керує STM32 через UART → NPU розпізнає обличчя і вмикає реле.
Не треба будувати ракету одразу. Треба будувати модулі з правильними інтерфейсами — і одного дня вони самі складуться в ракету.
Підсумок
За два тижні ми пройшли від “DTS — це китайська грамота” до працюючої системи де Linux керує STM32:
• Розібрали DTS свого пристрою і побачили ті ж концепції що на STM32 bare-metal
• Змінили один рядок — отримали три нових UART
• Підключили STM32 до Luckfox і написали Python контролер
• Зрозуміли як працює MTD, flashcp, і часткова прошивка
• Зазирнули в NPU і побачили що там чекає
Далі — GStreamer для відеостріму і детекція облич через NPU. Камера вже працює (стрім ми налаштували раніше), тепер додамо до нього мізки.
А якщо хочете побачити звідки ростуть ноги всієї цієї екосистеми — ось попередні проекти:
• IoT PowerHub — Pi Pico + SIM800L + реле + MQTT — dou.ua/forums/topic/56126/
Та сама плата Pi Pico Sim800, тільки з Pi Pico замість Luckfox. Реле, зсувний регістр, GSM — все що ми сьогодні маппили на нові піни.
• Luckfox Pico Pro — від ATmega8 до відеостріму — dou.ua/forums/topic/58244/
Перше знайомство з Luckfox, камера, MJPEG стрім. Тепер додаємо до цього STM32 і NPU.
Бачите патерн? Маленькі проекти з правильними інтерфейсами поступово складаються в щось більше. Може колись і на Автобота комп’ютерний зір прикрутимо — камера є, NPU є, UART до мотор-контролера теж є. Всі модулі на місці.
Довідники
dts_theory_guide.md — теорія DTS з порівнянням STM32 bare-metal на кожному кроці
dts_commands_cheatsheet.md — практичні команди для пошуку, перегляду, компіляції і діагностики DTS
Репозиторій: github.com/pipicosim800-maker/stm32F103