Что такое 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 для семейства TI J722S/AM67A — то самое описание, которое плата 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 кГц, и снизу добавляется настоящее устройство — расширитель GPIO TCA6424 по адресу 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 в память, загружает ядро и при запуске передаёт ему адрес блоба в регистре. На 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 — первое место, куда смотреть, когда чего-то нет: всё вытекает из одной и той же идеи.
По теме
- цепочка загрузки встраиваемого Linux — как загрузчик передаёт управление ядру
- Что такое DFU? — прошивка по USB, когда другого пути не осталось
Источники
- Platform Devices and Drivers — почему периферия SoC не перечислима так, как PCI и USB, и как её регистрирует код платы.
- How To Write Linux PCI Drivers — таблица
pci_device_idи вызовprobe()для совпавших устройств. - USB Device Drivers — сопоставление драйверов по идентификатору на стороне USB.
- Devicetree Specification — формальное определение формата.
- Linux and the Devicetree — как ядро читает дерево и сопоставляет драйверы.
- Writing Devicetree Bindings — граница между описанием железа и настройкой драйвера.
- dtc — Device Tree Compiler — репозиторий компилятора и его использование.
- U-Boot: Devicetree Control — как U-Boot работает с device tree.
- Booting ARM Linux — в каком регистре загрузчик передаёт блоб ядру.
- Devicetree Overlay Notes — как overlay применяются во время работы.
- Device Tree Reference (eLinux) — справочник сообщества с разобранными примерами.
Комментарии