Што е Device Tree?

August 7, 2026 · посетители

Ако некогаш си се обидел да подигнеш Linux на вградена плочка, порано или подоцна наидуваш на датотека што завршува на .dts. Обично во најлошиот можен момент: плочката не подига, на екранот нема ништо, а некој ти вели „погледни го device tree-то". Овој текст е за тоа што е таа датотека, зошто постои и што всушност бараш во неа.

Кој Проблем Го Решава

На десктоп компјутер јадрото го наоѓа хардверот речиси само. PCI и USB се формално специфицирани, набројливи магистрали (1). Уредите се претставуваат со идентификатор на производител и на производ; драјверот ги наведува поддржаните идентификатори во табела, а јадрото ја повикува функцијата probe() на тој драјвер за секој уред што одговара на некој запис (2). На USB страната истото работи преку своја табела на идентификатори (3). Никој не мора однапред никому ништо да соопштува.

Вградените плочки ја немаат таа удобност. Документацијата на јадрото ја повлекува границата сосема јасно: периферијата вградена во систем-на-чип виси на магистрали со минимална инфраструктура, „за разлика од големите формално специфицирани како PCI или USB", а заедничкото за тие уреди е директното адресирање од магистралата на процесорот (1). Еден I²C сензор или контролер за екран закачен на SPI не се претставува; тој едноставно седи таму, на таа адреса, поврзан на таа прекинска линија. Единствениот начин јадрото да дознае за него е некој да му каже.

Долго време тој „некој" беше самиот код. За секоја плочка постоеше датотека во дрвото на јадрото што опишуваше во C кој уред каде е поврзан — јадрото и денес го документира овој пат, каде кодот специфичен за плочката ги регистрира уредите еден по еден (1). На ARM овие датотеки се намножија толку што во 2011 Линус Торвалдс ја расчлени состојбата во едно писмо, велејќи дека ARM кодот оди во насока што не се одржува. Излезот што се прифати беше device tree.

Идејата

Device tree е податочна датотека што опишува како е поврзан хардверот. Не е код — не се извршува и не се компајлира во јадрото. Тоа е опис: на оваа адреса има контролер, ја користи оваа прекинска линија, се напојува од овој такт, а под него висат овие уреди.

Самиот формат почива на формална спецификација (4). Се вика дрво затоа што навистина е дрво. Почнува од коренски јазол, од него се разгрануваат магистралите, а од нив уредите закачени на тие магистрали — верно пресликувајќи го физичкото поврзување на хардверот.

Наместо измислен пример, еве еден вистински. Јазолот подолу е од k3-am62p-main.dtsi во U-Boot дрвото за фамилијата J722S/AM67A на TI — токму описот што една AM67A плочка навистина го чита при подигање:

main_i2c0: i2c@20000000 {
    compatible = "ti,am64-i2c", "ti,omap4-i2c";
    reg = <0x00 0x20000000 0x00 0x100>;
    interrupts = <GIC_SPI 161 IRQ_TYPE_LEVEL_HIGH>;
    #address-cells = <1>;
    #size-cells = <0>;
    power-domains = <&k3_pds 102 TI_SCI_PD_EXCLUSIVE>;
    clocks = <&k3_clks 102 2>;
    clock-names = "fck";
    status = "disabled";
};

Секоја линија овде значи нешто. reg ја дава адресата на контролерот во мемориската мапа — 0x20000000, со должина 0x100. Адресите се запишани како парови броеви затоа што адресниот простор е 64-битен; првиот број ги носи горните 32 бита. interrupts кажува која прекинска линија се користи; имиња како GIC_SPI и IRQ_TYPE_LEVEL_HIGH доаѓаат од вклучени заглавни датотеки — значи, иако изворот на device tree е чиста податочна опис, сепак поминува низ C препроцесорот.

Вреди да се забележи дека compatible носи две вредности: "ti,am64-i2c" и "ti,omap4-i2c". Списокот се чита од најспецифичното кон најопштото. Јадрото прво бара драјвер специфичен за am64, а ако не најде, се враќа на постариот и поопшт omap4 драјвер (5). Така нов чип може да работи без некој да напише драјвер посебно за него.

clocks и clock-names кажуваат од каде уредот го добива тактот: излез 2 на уредот 102 кај &k3_clks, што драјверот ќе го најде под името "fck". power-domains по иста логика го поврзува доменот на напојување. Ова не се поставки на фреквенција — тие го опишуваат поврзувањето, кој такт каде оди.

Последната линија е важна: status = "disabled". Производителот на чипот ја опишува периферијата, но ја остава исклучена, зашто не може да знае дали таа I²C магистрала воопшто се користи на твојата плочка. Вклучувањето е работа на .dts на самата плочка. Во датотеката на плочката J722S EVM тоа изгледа вака:

&main_i2c0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&main_i2c0_pins_default>;
    clock-frequency = <400000>;

    exp1: gpio@23 {
        compatible = "ti,tca6424";
        reg = <0x23>;
        gpio-controller;
        #gpio-cells = <2>;
    };
};

Водечкото &main_i2c0 е референца кон горниот јазол — не создава нов јазол, туку пишува врз постоечкиот. status се вклучува, пиновите се доделуваат, брзината се поставува на 400 kHz, а под него се додава вистински уред: TCA6424 GPIO проширувач што седи на адреса 0x23 на I²C магистралата. Бидејќи контролерот декларирал #address-cells = <1>, таа адреса се пишува со еден единствен број, за разлика од сопствениот reg на контролерот.

Вака device tree работи во пракса: производителот го опишува SoC-от еднаш, а ти кажуваш што е поврзано на твојата плочка.

Својството што навистина е важно е compatible. Тука гледа јадрото за да спари драјвер со парче хардвер. Драјверот вели „компатибилен сум со ti,tca6424", јазол во device tree ја носи истата низа, и јадрото ги спојува двата. Затоа печатните грешки паѓаат тивко: ако низата не се совпадне, нема грешка — уредот едноставно никогаш не се појавува.

Вреди да се има предвид: device tree опишува хардвер, а не конфигурација на драјвер. Водичот на јадрото за пишување binding-и особено ја нагласува оваа граница (6). Во пракса линијата постојано се замаглува и е најмногу расправаната тема на списоците на јадрото — „дали ова е навистина својство на хардверот, или е твоја преференца?"

Датотеките

Ќе видиш три наставки и лесно е да се измешаат (11).

.dts е изворот што го пишуваш, формата читлива за човек. .dtsi е фрагмент што други датотеки можат да го вклучат — „i" е од include. Производителот на чипот обично го опишува целиот SoC во .dtsi; ти пишуваш кратка .dts за својата плочка, ја вклучуваш и додаваш само она што е специфично за неа.

.dtb е компајлираната форма — device tree blob. Тоа е она што јадрото навистина го чита. Компајлерот е dtc (7):

dtc -I dts -O dtb -o my-board.dtb my-board.dts

Работи и во обратна насока, што е непроценливо кога бркаш проблем. Ако имаш .dtb што работи, можеш да го отвориш и да погледнеш внатре:

dtc -I dtb -O dts -o decoded.dts my-board.dtb

На систем во работа device tree може да се прочита и директно од датотечниот систем. Јадрото тука го изложува дрвото што навистина го користи:

ls /proc/device-tree/

Ова е најбрзиот одговор на прашањето „дали датотеката што ја уредив е датотеката што се вчита?" Изненадувачки често, одговорот е не.

Што Се Случува При Подигање

Редоследот е ваков: подигнувачот — обично U-Boot (8) — го вчитува .dtb во меморија, го вчитува јадрото, а потоа, при стартувањето, му ја предава адресата на blob-от во регистар. На ARM тој регистар и условите што подигнувачот мора да ги исполни се утврдени во документот за подигање на јадрото (9). Пред да подигне било кој драјвер, јадрото го чита тоа дрво, ги извлекува уредите од него и ги спарува драјверите според compatible низите.

Практичната последица е дека јадрото и device tree можат да се ажурираат одвоено. Додавање сензор ретко значи повторно градење на јадрото; обично е доволно да се ажурира .dtb. Но тоа сече на двете страни — бидејќи се независни, можат да се разидат. Подигни ново јадро со стар .dtb и плочката тивко ќе се крене нецелосна, а треба време да се сфати зошто.

Постои и идејата за overlay: фрагменти што се применуваат врз постоечко дрво во време на извршување (10). Тоа се случува кога приклучуваш HAT на Raspberry Pi — главното дрво останува какво што е, а одозгора се положува мал фрагмент. Корисно кога основната плочка е секогаш иста и се менува само приклучениот модул.

Кога Нешто Не Работи

Дефектите во device tree имаат своевидна непријатност, зашто обично не произведуваат никаква грешка. Уредот едноставно го нема. Одењето по овој редослед штеди време.

Почни со проверка дали уредот воопшто бил создаден. Дали јазолот е присутен под /proc/device-tree? Ако не е, проблемот не е во датотеката што ја уредуваше — најверојатно се вчитува погрешен .dtb, или јазолот е исклучен едно ниво погоре со status = "disabled". Ова второто се случува постојано: периферијата обично доаѓа оневозможена во .dtsi датотеките на производителот, а се очекува ти да ја вклучиш една по една со status = "okay" во .dts на својата плочка.

Ако јазолот е таму, но ниту еден драјвер не се закачил, стави го окото на compatible низата. Најди ја of_device_id табелата во изворот на драјверот и спореди ги низите знак по знак. Доволна е една запирка, една цртичка.

И уште нешто: предупредувањата на dtc доаѓаат исклучени во повеќето проекти, а кажуваат навистина корисни работи. Несовпаѓања меѓу адресата и reg, недостасувачки #address-cells — токму оној вид дефект што при подигање се претвора во тивок отказ. Вреди предупредувањата да останат вклучени при градење.

За Крај

Она што го отежнува сфаќањето на device tree е тоа што никој однапред не кажува што всушност е тој. Личи на конфигурациска датотека, но не е; личи на драјвер, но ниту тоа. Најкорисната рамка е да се мисли за него како договор меѓу хардверот и софтверот: како е поврзана плочката, запишано на јазик што јадрото го разбира.

Штом седне во таа рамка, останатото следи само. Зошто compatible е толку критичен, зошто .dtb живее одвоено од јадрото, зошто /proc/device-tree е првото место каде се гледа кога нешто недостасува — сè произлегува од истата идеја.

Поврзани текстови

Извори

  1. Platform Devices and Drivers — зошто периферијата на SoC не е набројлива како PCI и USB, и како кодот специфичен за плочката ја регистрира.
  2. How To Write Linux PCI Drivers — табелата pci_device_id и повикот на probe() за совпаднатите уреди.
  3. USB Device Drivers — спарување драјвери според идентификатор на USB страната.
  4. Devicetree Specification — формалната дефиниција на форматот.
  5. Linux and the Devicetree — како јадрото го чита дрвото и ги спарува драјверите.
  6. Writing Devicetree Bindings — границата меѓу опишување хардвер и конфигурирање драјвер.
  7. dtc — Device Tree Compiler — складиштето на компајлерот и неговата употреба.
  8. U-Boot: Devicetree Control — како U-Boot се справува со device tree.
  9. Booting ARM Linux — со кој регистар подигнувачот му го предава blob-от на јадрото.
  10. Devicetree Overlay Notes — како overlay-ите се применуваат при извршување.
  11. Device Tree Reference (eLinux) — заедничка референца со разработени примери.

Коментари