Серія “STM32 з нуля без HAL” · Місяць 3 · Тиждень 1
Все що тут описано — перевірено на живому Luckfox Pico Pro (RV1106, Cortex-A7). Якщо ви працювали з STM32 bare-metal — ви вже знаєте 80% того що робить Device Tree, тільки записане іншою мовою.
Що таке Device Tree і навіщо він
На STM32 ви описуєте периферію прямо в коді:
|
1 2 3 |
#define USART1_BASE 0x40013800 #define USART1_CR1 (*(volatile uint32_t *)(USART1_BASE + 0x0C)) RCC_APB2ENR |= (1 << 14); // тактування |
На embedded Linux з ARM SoC (Cortex-A) ядро не знає яка периферія є на платі. Device Tree — це файл що описує апаратуру: які контролери є, за якими адресами, які переривання використовують, які піни зайняті.
Ядро Linux читає Device Tree і робить те що ви робили руками на STM32:
reg→ioremap()(замість#define BASE_ADDR)interrupts→request_irq()(замість NVIC_ISER)clocks→ clock framework (замість RCC_APB2ENR)pinctrl→ pin mux (замість alternate function в GPIOx_CRH)
Структура файлів: DTS → DTSI → DTB
|
1 2 3 4 5 6 7 8 9 10 |
rv1106.dtsi ← SoC: всі UART, SPI, I2C контролери ↑ include rv1106-evb.dtsi ← базова EVB конфігурація ↑ include rv1106-luckfox-pico-pro-max-ipc.dtsi ← специфіка Luckfox (камера, IPC) ↑ include rv1106g-luckfox-pico-pro-max.dts ← плата: вмикає потрібне, задає параметри ↓ компіляція (dtc) rv1106g-luckfox-pico-pro-max.dtb ← бінарний blob, який читає ядро |
Аналогія з C:
.dtsi— якhal.hде описані всі можливості.dts— якmain.cде ви обираєте що використовувати.dtb— як.bin— скомпільований результат
Розширення:
.dts— Device Tree Source (файл плати, верхній рівень).dtsi— Device Tree Source Include (бібліотека, включається в .dts).dtb— Device Tree Blob (скомпільований бінарний файл)
Синтаксис DTS: основні елементи
Ноди (nodes)
DTS — це дерево. Кожна нода описує пристрій або шину.
|
1 2 3 4 5 6 7 8 9 10 |
/ { ← root node (кореневий вузол) model = "Luckfox Pico Pro Max"; compatible = "rockchip,rv1103g-38x38-ipc-v10", "rockchip,rv1106g3"; uart2: serial@ff4c0000 { ← дочірня нода compatible = "rockchip,rv1106-uart", "snps,dw-apb-uart"; reg = <0xff4c0000 0x100>; status = "disabled"; }; }; |
Формат імені ноди: label: name@unit-address
uart2— мітка (label), для посилань через&uart2serial— ім’я (тип пристрою)ff4c0000— unit address (зазвичай = перше значення зreg)
Властивості (properties)
Кожна нода містить властивості у форматі name = value;
| Тип значення | Приклад | Коментар |
|---|---|---|
| Рядок | compatible = "snps,dw-apb-uart"; |
В подвійних лапках |
| Список рядків | compatible = "rockchip,rv1106-uart", "snps,dw-apb-uart"; |
Через кому |
| Число (cell) | reg = <0xff4c0000 0x100>; |
В кутових дужках, 32-біт |
| Посилання (phandle) | clocks = <&cru SCLK_UART2>; |
& — посилання на іншу ноду |
| Порожня (boolean) | disable-wp; |
Наявність = true |
Ключові властивості
compatible — як ядро знаходить драйвер
|
1 2 |
compatible = "rockchip,rv1106-uart", "snps,dw-apb-uart"; |
Список від конкретного до загального. Ядро пробує знайти драйвер по черзі:
rockchip,rv1106-uart— специфічний для RV1106snps,dw-apb-uart— generic DesignWare UART (IP-блок від Synopsys)
Формат: виробник,модель. Якщо перший не знайдений — fallback на другий.
⚠ Gotcha: compatible в root node (/) описує саму плату, не драйвер:
|
1 2 3 4 |
/ { compatible = "rockchip,rv1103g-38x38-ipc-v10", "rockchip,rv1106g3"; }; |
reg — адреси MMIO регістрів
|
1 2 |
reg = <0xff4c0000 0x100>; |
Формат: <base_address size>. UART2 починається з 0xff4c0000, блок регістрів займає 0x100 (256 байт).
Порівняння з STM32:
|
1 2 3 4 5 6 |
// STM32: ви писали це вручну #define USART1_BASE 0x40013800 #define USART1_SR (*(volatile uint32_t *)(USART1_BASE + 0x00)) #define USART1_DR (*(volatile uint32_t *)(USART1_BASE + 0x04)) // DTS: те саме, але декларативно — ядро зробить ioremap() // reg = <0xff4c0000 0x100>; |
⚠ Як читати reg: формат залежить від #address-cells і #size-cells батьківської ноди:
#address-cells = <1>→ адреса = 1 cell (32 біт)#size-cells = <1>→ розмір = 1 cell (32 біт)#size-cells = <0>→ розміру немає (I2C slave адреси)
Приклад для I2C пристрою:
|
1 2 3 4 5 6 7 8 9 |
i2c3: i2c@ff460000 { #address-cells = <1>; #size-cells = <0>; ← дочірні ноди не мають size bmp180@77 { compatible = "bosch,bmp180"; reg = <0x77>; ← це I2C адреса, не MMIO! }; }; |
interrupts — переривання
|
1 |
interrupts = <GIC_SPI 27 IRQ_TYPE_LEVEL_HIGH>; |
Три значення:
GIC_SPI— тип: Shared Peripheral Interrupt (загальне переривання периферії)27— номер перериванняIRQ_TYPE_LEVEL_HIGH— тригер по рівню, активний HIGH
Порівняння з STM32:
|
1 2 3 4 |
// STM32: USART1 = IRQ37 → NVIC_ISER1 біт 5 NVIC_ISER1 |= (1 << 5); / / DTS: те саме, але ядро викличе request_irq() автоматично // interrupts = <GIC_SPI 27 IRQ_TYPE_LEVEL_HIGH>; |
⚠ GIC vs NVIC:
| Cortex-M3 (STM32) | Cortex-A7 (RV1106) | |
|---|---|---|
| Контролер | NVIC | GIC (Generic Interrupt Controller) |
| Типи | IRQ | SPI, PPI, SGI |
| Налаштування | Пряме через регістри | Через Device Tree |
clocks — тактування
|
1 2 |
clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>; clock-names = "baudclk", "apb_pclk"; |
Два клоки від CRU (Clock & Reset Unit):
SCLK_UART2— клок для baud rate генератора (як APB clock на STM32)PCLK_UART2— клок шини APB для доступу до регістрів
Порівняння з STM32:
|
1 2 3 4 |
// STM32: вмикали тактування одним бітом RCC_APB2ENR |= (1 << 14); // USART1 // DTS: ядро зробить clk_prepare_enable() для обох клоків |
status — увімкнення/вимкнення
|
1 2 3 |
status = "disabled"; // вимкнений (за замовчуванням в SoC DTSI) status = "okay"; // увімкнений (плата перевизначає) |
В базовому rv1106.dtsi ВСІ контролери disabled. Файл плати вмикає тільки те що реально використовується.
Порівняння з STM32: як RCC_APB2ENR — периферія є в чіпі, але без тактування не працює. В DTS — є в дереві, але без "okay" ядро не створить пристрій.
pinctrl — мультиплексор пінів
|
1 2 |
pinctrl-names = "default"; pinctrl-0 = <&uart2m1_xfer>; |
Посилання на конфігурацію пінів. uart2m1_xfer визначений в rv1106-pinctrl.dtsi:
|
1 2 3 4 5 6 7 |
uart2m1_xfer: uart2m1-xfer { rockchip,pins = /* uart2_rx_m1 */ <1 RK_PB3 2 &pcfg_pull_up>, /* uart2_tx_m1 */ <1 RK_PB2 2 &pcfg_pull_up>; }; |
Формат: <bank pin function pull_config>
1— GPIO bank 1RK_PB3— Port B, pin 32— alternate function 2 (= UART2 RX)&pcfg_pull_up— внутрішня підтяжка вгору
Порівняння з STM32:
|
1 2 3 4 5 |
// STM32: ставили PA9 в Alternate Function Push-Pull gpio_init(PIN_PA9, 0xB); // DTS: ядро робить те саме через pinctrl підсистему // <1 RK_PB2 2 &pcfg_pull_up> → bank1, PB2, AF2, pull-up |
⚠ M0 vs M1: m1 в назві — варіант розводки пінів. RV1106 дозволяє вивести один і той же UART на різні фізичні піни. На STM32F103 аналог — AFIO_MAPR remap.
Механізм перевизначення (overlay)
DTS плати перевизначає властивості з DTSI за допомогою &label:
|
1 2 3 4 5 6 7 8 9 10 11 |
// В rv1106.dtsi (SoC): uart3: serial@ff4d0000 { compatible = "rockchip,rv1106-uart", "snps,dw-apb-uart"; reg = <0xff4d0000 0x100>; status = "disabled"; ← вимкнений за замовчуванням }; // В rv1106g-luckfox-pico-pro-max.dts (плата): &uart3 { status = "okay"; ← перевизначаємо тільки status }; |
&uart3 — це посилання (reference) на ноду з міткою uart3. Можна перевизначити будь-яку властивість або додати нові дочірні ноди. Решта властивостей (reg, interrupts, clocks) залишаються з DTSI.
Спеціальні ноди
chosen — параметри завантаження
|
1 2 3 4 5 |
chosen { bootargs = "console=ttyFIQ0 root=ubi0:rootfs rootfstype=ubifs"; stdout-path = "serial2:1500000n8"; }; |
Не описує апаратуру — передає параметри ядру. bootargs — командний рядок ядра. U-Boot може доповнювати або перевизначати bootargs перед передачею ядру.
aliases — коротке іменування
|
1 2 3 4 5 6 |
aliases { serial0 = &uart0; serial2 = &uart2; serial3 = &uart3; i2c3 = &i2c3; }; |
Визначає нумерацію пристроїв: serial2 → /dev/ttyS2. Без alias ядро нумерує в порядку виявлення (непередбачувано).
memory — опис RAM
|
1 2 3 |
memory { reg = <0x00000000 0x04000000>; // 64MB RAM }; |
Rockchip FIQ Debugger — особливість консолі
На Luckfox Pico Pro (і більшості Rockchip плат) консоль працює НЕ через звичайний serial драйвер, а через FIQ Debugger:
|
1 2 3 4 5 |
fiq-debugger { compatible = "rockchip,fiq-debugger"; status = "okay"; }; |
FIQ (Fast Interrupt Request) — переривання з вищим пріоритетом ніж IRQ. Rockchip перехоплює UART2 через FIQ щоб консоль працювала навіть коли ядро зависло на рівні IRQ.
Результат:
- Консоль =
ttyFIQ0(неttyS2) - UART2 недоступний як звичайний serial порт
- В bootargs:
console=ttyFIQ0 earlycon=uart8250,mmio32,0xff4c0000— рання консоль до ініціалізації FIQ
⚠ Це означає що для зв’язку зі STM32 потрібно використовувати інший UART (3, 4, або 5).
Bootargs: розбір командного рядка ядра
Реальний приклад з Luckfox Pico Pro:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
earlycon=uart8250,mmio32,0xff4c0000 ← рання консоль (до драйверів) console=ttyFIQ0 ← основна консоль (FIQ debugger) root=ubi0:rootfs ← кореневий розділ rootfstype=ubifs ← файлова система mtdparts=spi-nand0: ← розмітка SPI NAND flash 256K(env), U-Boot environment 256K@256K(idblock), ID block 512K(uboot), U-Boot 4M(boot), ядро 30M(oem), OEM 10M(userdata), дані 210M(rootfs) rootfs rk_dma_heap_cma=66M ← 66MB CMA для камери/NPU |
⚠ 0xff4c0000 в earlycon — та сама адреса що в reg UART2 в DTS.
Карта UART на Luckfox Pico Pro
| UART | Адреса | Статус | Піни (M1) | Використання |
|---|---|---|---|---|
| uart0 | 0xff4a0000 | disabled | — | — |
| uart1 | 0xff4b0000 | disabled | — | — |
| uart2 | 0xff4c0000 | fiq-debugger | Pin 1 (TX), Pin 2 (RX) | Консоль |
| uart3 | 0xff4d0000 | disabled | Pin 19 (TX), Pin 20 (RX) | Вільний → STM32 |
| uart4 | 0xff4e0000 | disabled | Pin 6 (TX), Pin 7 (RX) | Вільний |
| uart5 | 0xff4f0000 | disabled | — | — |
Для зв’язку зі STM32 найзручніший UART3 — два піни поруч, без конфліктів.
Що далі
Тиждень 2: увімкнуємо UART3 в DTS → збираємо ядро → /dev/ttyS3 з’являється → STM32 підключаємо до пінів 19/20 → Linux шле команди, STM32 відповідає.
|
1 2 3 4 5 6 |
// Що додамо в DTS плати: &uart3 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&uart3m1_xfer>; }; |
Довідник створений на основі реального DTS Luckfox Pico Pro (RV1106G, Buildroot 2023.02.6) Серія “STM32 з нуля без HAL” · github.com/pipicosim800-maker/stm32F103