STM32 з нуля без HAL: Device Tree, три UART і Python на борту. Частина 7
Серія «STM32 з нуля без HAL» • Місяць 3 • Тижні
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 ми писали прямо в регістри:
#define USART1_BASE 0x40013800 #define USART1_CR1 (*(volatile uint32_t *)(USART1_BASE + 0x0C)) RCC_APB2ENR |= (1 << 14); // тактування USART1
Ми знали адресу кожного регістра, бо працювали з одним конкретним чіпом. Але Linux — універсальне ядро. Воно працює на тисячах різних плат. Звідки йому знати, що на нашому Luckfox UART2 сидить за адресою 0xff4c0000, а не 0×40013800 як на STM32?
Відповідь проста — це Device Tree, файл, який описує все залізо на платі: які є контролери, за якими адресами, які переривання використовують, які піни зайняті. Ядро читає цей файл при завантаженні і знає з чим працювати.
Структура файлів: як матрьошка
Відкриваємо DTS нашої плати і бачимо три include:
// 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: хто ми такі
/ {
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:
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 0×100> — базова адреса і розмір блоку регістрів. На STM32 ми писали #define USART1_BASE 0×40013800. Та сама ідея, тільки в 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 описує фізичні піни
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 кодом:
// 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 потрапляє на піни
Перевіряємо всі варіанти в pinctrl:
$ 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:
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 нашої плати бачимо:
&uart3 { status = "disabled"; };
&uart4 { status = "disabled"; };
Обидва вимкнені. Дивимось на pinout Luckfox — UART3 виведений на піни
Що робимо? Змінюємо один рядок:
/* UART3_M1 */
&uart3 {
status = "okay";
};
Pinctrl вже прописаний в базовому rv1106.dtsi (pinctrl-0 = <&uart3m1_xfer>), тому більше нічого додавати не треба.
Збираємо і прошиваємо
# Збираємо ядро (включає компіляцію DTS) cd ~/luckfox-pico ./build.sh kernel # Копіюємо boot.img на плату scp -o PubkeyAuthentication=no output/image/boot.img \ [email protected]:/tmp/ # Прошиваємо тільки boot розділ (mtd3) і перезавантажуємо ssh -o PubkeyAuthentication=no [email protected] \ "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 0×08000000 на STM32 — та ж ідея, тільки через Linux підсистему.
Повна розмітка MTD на Luckfox:
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 запит і бачимо:
$ 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 для максимальної гнучкості:
/* UART1_M1 */
&uart1 { status = "okay"; };
/* UART3_M1 */
&uart3 { status = "okay"; };
/* UART4_M1 */
&uart4 { status = "okay"; };
Перезбираємо, прошиваємо, і тепер:
$ 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), піни
U3 (BMP180 #2), піни
U11 (гребінка), піни
Піни
Крім UART, на гребінці U11 є повний SPI0 (піни

SIM800L конектор — половинка UART
Конектор SIM800L (піни
Зсувний регістр — GPIO bit-bangSN74HC595N підключений до пінів 4, 5, 9. На Luckfox ці піни не збігаються з SPI контролером, тому апаратний SPI не вийде. Але GPIO bit-bang працює чудово — для 8 реле швидкості вистачить з головою.
Думаю це чудова тема для окремої статті — як керувати зсувним регістром з Linux через sysfs.
Частина 4. Підключаємо STM32 — перший міст

Коли паяєш дорожки на платі і перевіряєш пін за піном, потрібна максимальна концентрація. Ніяких паразитних сигналів, тільки чистий UART.
Фізичне підключення
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.
// 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:
# Налаштувати 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 скрипт:
#!/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:
# Основний 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:
$ 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, звернув увагу на цікаву ноду:
npu@ff660000 {
compatible = "rockchip,rknpu";
...
};
NPU — Neural Processing Unit. На нашій $15 платі є нейроприскорювач на 0.5 TOPS. Перевіряємо:
$ 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 до працюючої системи керування STM32 за допомогою Linux:
• Розібрали 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
10 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівСпоріденні файли, з розширеннями: dtb, dts, dtsi, dtc compiler «dtc parameters».
Про компілятор у вас нічоґо не знaйшоy, а без цьоґо взаґалі, я впевнений, ніхто нічоґо не зрозуміє.
Very important : І як фінальний файл binary (dtb) використовувати. Я це робив через Лінукс софтлінки до модулів. І там ще е дуже велика ніша як їх використати.
Треба доповнити обов’азково.
— Для мене особисто, коли з цим працював залишилось незрозумілим різниця між файлами з розширеннями dts i dtsi.
Дякую!
DTS і DTSI та ж різниця що .c і .h в C:
.dtsi — це include-файл (бібліотека з описом SoC),
.dts — файл конкретної плати який включає потрібні .dtsi і перевизначає налаштування. Описував в статті як аналогію з HAL: rv1106.dtsi це як hal.h, а .dtsплати це як `main.c`.
Стосовно компіляція dtc, то:
в Luckfox SDK відбувається автоматично через ./build.sh kernel.
Але можна і вручну — dtc -I dts -O dtb -o output.dtb input.dts (компіляція),
dtc -I dtb -O dts -o readable.dts firmware.dtb (декомпіляція, щоб подивитись що всередині). Детальніше в шпаргалках dts_commands_cheatsheet.md
VsWorks RTOS:
Може комусь допоможе мій досвід: окремо компілюють кожний файл з описом dts, фінальний файл dtb за допомоґою софтлінк лінкують до усіх модулів. Потім іде білдінґ всієї систeми.
Дякую за доповнення! Цікаво що Device Tree використовується і в VxWorks отже DTS/DTB став стандартом не тільки для Linux, а і для embedded. Buildroot/Luckfox SDK компіляція DTS відбувається автоматично під час ./build.sh kernel, але принцип той самий — dtc компілює .dts -> .dtb, і далі DTB йде в boot image.
Ще важливе доповнення:
як правило все дженерек. Але для конкретних бордів треба робити перегляд, додавати чого не вистачає і перекомпілювати все. Займає також час. Я це робив для таких Samsung’s Exynos Auto V920, Xilinx/ZynqMP, iMX8 families.
Дякую, за важливе доповнення, у вас неймовірно багатий досвід
Занадто олдскульно. Чи не ліпше було взяти розшируювач GPIО з SPI або І2С?
а там що boot.img 3 в 1: ядро, файлова система, і девайс трі?
Файлова система (rootfs) сидить окремо на mtd6.
Тому коли ми змінюємо DTS і перезбираємо ядро — прошиваємо тільки mtd3 (4MB), а не весь образ (250MB+). Rootfs залишається на місці з усіма файлами, Python скриптами, налаштуваннями.
Якщо потрібно оновити і rootfs — тоді повний образ через USB: sudo upgrade_tool uf output/image/update.img
Може й краще, мав напрацювання з іншої поделки, вщяв що було