Plug and /pray/ play
ОС и компьютер. Роман с камнем
Когда компьютеры были большими, а программы маленькими, никто особенно не задумывался над ответами на вопросы: Какова конфигурация компьютера? Какие устройства и как подключены? Да, собственно, этого и не нужно было делать. Компьютеры обслуживали сильно специальные люди — они собирали компьютер у заказчика, подключали периферию и настраивали операционную систему на работу с оной. Несмотря на то, что люди были специальными, по нынешним меркам все было организовано довольно просто. Любой самостоятельный компонент компьютера (процессор, память, шина, порты ввода/вывода и т. д.) поставлялся с кипой бумажной документации, в которой описывалось, какие интерфейсы используются, какие адреса, прерывания необходимы для работы того или иного устройства. Если возникали конфликты адресов/прерываний, то, как правило, в устройстве были переключатели, позволявшие в определенных пределах менять настройки. А когда это не помогало, то специальные люди не чурались паяльника и разрешали конфликт, так сказать, «по месту».
Все это хорошо, но как потом ОС с этим разбиралась? Да никак — операционную систему «генерировали», т. е. по факту компилировали и собирали, часто на этом же самом компьютере. Для старта поставлялся набор перфолент/лент и даже дисков (правда, размером с небольшую летающую тарелку). Перед началом процесса сборки, как минимум, нужно привести в рабочее состояние процессорный блок, память, как минимум один терминал и что-нибудь из периферии — например, накопитель на магнитной ленте. Далее следовал достаточно длительный процесс сборки — вопросы и ответы и никаких меню! Если вы думаете, что понимаете, о чем это, ведь вы собирали ядро Linux через make config, а не через make menuconfig, то вы сильно заблуждаетесь. По нормативам процесс сборки ОС мог занимать несколько дней. Вот вам и «плуг-энд-плей»!
Не будем заострять внимание на том, что и начальный загрузчик мог быть введен либо с панели управления компьютера, либо через перфоленту (или даже с компакт кассеты). Наш основной предмет — распознавание компьютером (его ОС) собственной конфигурации.
Тем не менее, большие машины задолго до персональных компьютеров научились «плуг-энд-плею». Однако, вопрос распознавания конфигурации не стоял так остро — номенклатура периферии была небольшой, выпускалась часто самим производителем компьютеров, да и самих компьютеров было относительно немного, и на всех хватало «специальных людей».
С появлением персональных компьютеров, и, в частности, с появлением открытой архитектуры IBM PC картина поменялась. Во-первых, компьютеров стало существенно больше, соответственно — перестало хватать «специальных людей». Во-вторых, периферию стали выпускать «все желающие», и номенклатура периферии выросла.
Если мы вспомним, как выходили из положения в эпоху ранних IBM PC, то это не сильно отличалось от «больших» машин. Как минимум один листок документации с описанием «джамперов», файл конфигурации драйвера для возможности настройки драйвера на конкретную конфигурацию оборудования. Следует сделать оговорку — это не относится к устройствам, смонтированным на материнской плате. Про них все знает основной BIOS системы.
Правда такое подходило не для всех устройств. Некоторые устройства обзавелись собственной периферией. Например, дисковый контроллер. К нему мало того, что можно было подключать накопители разного объема, так еще и от разных производителей. Т.е. мы уже имеем не плоскую картину мира, а некое дерево. В IBM PC предусмотрен механизм цепочки BIOS-ов (который сохранился до наших дней). В определенном диапазоне адресного пространства выделен участок, в который могут встраиваться BIOS-ы того или иного периферийного устройства. Основной BIOS ПК передает управление на начало этого адреса, и далее, по цепочке, выполняется инициализации тех устройств, о которых системе не было известно до момента установки устройства в разъем (ISA например).
«Прокрустово ложе» такого механизма сильно сдерживало «интеллект» периферийных устройств — «окно» для собственного BIOS-а небольшое, скорость той же шины ISA уже недостаточная. По мере усложнения периферии компьютера, делались различные попытки — создавались новые шины (EISA, VLB и т. п., о которых уже никто не помнит), усложнялся основной BIOS ПК (он быстро вырос с десятков килобайт то мегабайт). Но требовался радикальный шаг. И он был сделан с введением стандарта на новую шину PCI. Было решено — раз вы (периферийные устройства) такие умные, вот и расскажите основному BIOS-у о том, что вы умеете и как с вами общаться, а рассказывать вы будете только тем протоколом, который описан в стандарте PCI. Опустим всю шумиху и трескотню, которая сопровождала появление «Plug-n-play». Важно, что шаг был сделан. Plug-n-play некоторое время притирался, дорабатывался, расширялся, и теперь мы его знаем как спецификацию ACPI.
Кратко — ACPI позволяет основному BIOS-у собрать информацию обо всех устройствах на шине PCI (PCIe) и поместить в специальную таблицу ACPI. Сведения об устройствах не PCI, о которых BIOS «знает» (например, процессор, память и т. д.), также помещаются в эту таблицу. В свою очередь BIOS предоставляет к этой таблице унифицированный интерфейс. И далее операционная система может пользоваться этим знанием, самостоятельно не занимаясь изысканиями.
Все это хорошо и замечательно в мире настольных компьютеров и серверов, имеющих BIOS и поддерживающих PCI. Но не все такие «умные». Встраиваемые системы, даже по современным меркам, все еще весьма ограничены в ресурсах. А часто таким системам и не нужно быть слишком «умными». Тем не менее, на них тоже работают операционные системы и программы, которым важно знать, что к ним подключили. Частично вопрос снимается использованием, например, шины USB, которая имеет собственный протокол распознавания устройств и требуемых им ресурсов. Но и шина USB тоже часто избыточна (или, наоборот, недостаточна). Так как же быть со встраиваемыми системами?
«Бонсай» для встраиваемых систем
Проблема описания оборудования давно циркулировала в среде специалистов по вычислительной технике, и ее решения находил для себя кто как мог. Как гласит история, изложенная на сайте kernel.org — изначально описание Devicetree (для краткости DT) было придумано для Open Firmware как способ передачи данных между, собственно, Open Firmware и программами (читай — операционными системами). И так как Open Firmware в основном использовался на платформах PowerPC и SPARC, то его поддержка попала в Linux для этих платформ. Поддержка была стандартизована в рамках ePAPR, и это описание все еще можно найти в Сети. В разработку оного стандарта уже тогда были вовлечены крупные игроки, такие как Freescale и IBM. Рост популярности Devicetree на других платформах, отличных от PowerPC и SPARC, повлек за собой унификацию поддержки DT в Linux. В конце концов была создана организация devicetree.org, которая и занимается стандартизацией DT в настоящее время. Текущая версия стандарта 0.3 находится в открытом доступе на соответствующем сайте организации. Как сказано на сайте, Devicetree используется в OpenFirmware, в OpenPOWER Abstraction Layer (OPAL), в решениях в рамках Power Architecture Platform Requirements (PAPR) и в виде самостоятельных FDT (flattened device tree, плоского дерева устройств) описаний. В духе времени — документацию, все самые последние версии спецификаций, а также текущий драфт можно найти в официальном репозитории на github.
Хорош Devicetree или плох, мы не будем втягиваться в религиозные войны DT vs. ACPI и т. п. Пока это инструмент и широко распространенный инструмент, то им нужно уметь пользоваться, если ты программист в мире встраиваемых систем.
Быть Тимирязевым. Теория выращивания деревьев
Так как мой опыт работы с Devicetree связан, в основном, с Linux, его и возьмем, например. То есть все, что дальше будет сказано без явного указания, будет относиться к Linux.
Понятно, что описания хранятся в файлах. Файлы DT делятся на:
исходные файлы — с расширением .dts/.dtsi. Принято, что файлы конкретной аппаратной платформы (платы) — это файлы .dts. А общие для нескольких платформ –это .dtsi;
скомпилированные двоичные (которые и будут использоваться ядром) — .dtb;
.h — файлы языка C. Это, как правило, константы, которые являются общими с исходным кодом ядра. Например, те или иные адреса или битовые маски для значений регистров. Это одно из связующих звеньев между ядром и описанием аппаратуры.
Исходные файлы компилируются/декомпилируются утилитой dtc в двоичное, пригодное для загрузки, представление .dtb. Да, если вы потеряли исходники Devicetree, то их можно восстановить той же программой dtc. Как и при любой декомпиляции, часть деталей описания будет утеряна (например, связь с .h файлами деление на .dts/.dtsi). Но это часто лучше, чем ничего.
Описания Devicetree в дереве исходного кода Linux располагаются по подкаталогам boot/dts соответствующей архитектуры в папке /arch.
Документация по применяемым объектам описания DT и их свойствам с примерами располагается в каталоге Documentation/devicetree.
Выше мы упомянули одну связь между исходным кодом ядра и Devicetree — общие .h файлы. Вторая часть — это API для работы с .dtb. Общий код ядра Linux для манипулирования объектами описания располагается в каталоге /drivers/of. Поддержка Devicetree включается в конфигурации ядра параметром: CONFIG_OF=y. При этом отдельные драйверы и/или подсистемы могут иметь собственное расширение функционала методов работы с деревом и опции конфигурации ядра, отвечающие за это. Как правило, такие файлы имеют префикс of_ (of — open firmware) в своем имени или могут называться также незатейливо of.h, of.c. В этих файлах находятся обработчики свойств, специфичных для данной категории устройств. Например, для памяти характерно большое количество специфических параметров тайминга.
Теперь, когда мы знаем где и что искать, мы можем перейти непосредственно к структурам данных, используемых в DT.
Общие принципы
Любое дерево начинается с корня. Возьмем, например, небольшой dts-файл для архитектуры ARM (arch/arm/boot/dts/imx6ull-colibri-eval-v3.dts).
// SPDX-License-Identifier: (GPL-2.0 OR MIT)
/*
* Copyright 2018 Toradex AG
*/
/dts-v1/;
#include "imx6ull-colibri-nonwifi.dtsi"
#include "imx6ull-colibri-eval-v3.dtsi"
/ {
model = "Toradex Colibri iMX6ULL 256MB on Colibri Evaluation Board V3";
compatible = "toradex,colibri-imx6ull-eval", "fsl,imx6ull";
};
Все описания объектов заключены в фигурные скобки. Перед корневым объектом описания стоит символ `/`. Корневой объект может быть только один, и обычно это материнская плата, на которой собраны все остальные элементы. В нашем случае это плата для разработки (evaluation board) компании Toradex для процессора IMX6.
Ключевое слово «model» дает текстовое описание платы, которое может быть прочитано ядром, и потом выдано при опросе конфигурации оборудования.
Другое слово, «compatible», является важным связующим элементом между описанием и работой ядра Linux. Именно по значению этого ключевого слова ядро определяет, с чем имеет дело и подгружает соответствующий модуль (или обращается к нужному драйверу если он уже в памяти). Это слово используется в ядре двумя способами и обрабатывается, соответственно, тоже двумя способами. Для корня описания, т. е. для описания платы — один способ, а для всех остальных другой.
Здесь мы имеем в описании корня совместимость с драйверами основной платы, поддерживающими либо »toradex, colibri-imx6ull-eval», либо »fsl, imx6ull», либо оба вместе. Ядро будет использовать код платы, имеющей в описании совместимости одну из этих строк (или обе сразу).
Декларация совместимости платы может выглядеть так:
static const char * const imx6ul_dt_compat[] __initconst = {
"fsl,imx6ul",
"fsl,imx6ull",
NULL,
};
DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")
.init_irq = imx6ul_init_irq,
.init_machine = imx6ul_init_machine,
.init_late = imx6ul_init_late,
.dt_compat = imx6ul_dt_compat,
MACHINE_END
Здесь мы видим список совместимых плат (imx6ul_dt_compat), который добавляется в код инициализации ядра, в поле .dt_compat.
В свою очередь, чтобы декларировать совместимость, обычный драйвер должен экспортировать непустую таблицу структур типа of_device_id с помощью макроса MODULE_DEVICE_TABLE:
static const struct of_device_id w1_gpio_dt_ids[] = {
{ .compatible = "w1-gpio" },
{ }
};
MODULE_DEVICE_TABLE(of, w1_gpio_dt_ids);
Таблица of_device_id всегда должна заканчиваться пустым элементом.
А что делает C-директива #include здесь? Да то же самое, что и в коде на языке C — позволяет включить содержимое другого файла в контекст компиляции. Вложенность может быть любой, в разумных пределах. В нашем примере в компиляцию добавляются два dtsi-файла с общим описанием для семейства плат на базе IMX6. Следует отметить, что разбиение на .dts/.dtsi файлы служит удобству разработки. [VD2] Никто не мешает все «свалить» в один файл, но следует учитывать, как вас потом будут вспоминать те, кому достанется поддержка такого кода ;-)
Что еще хотелось отметить в данном материале, так это то, каким образом константы, объявленные в h-файлах, связаны с описанием Devicetree. Если мы углубимся по директивам #include далее, в описание компонент, входящих в состав нашей платы, мы обнаружим в файле imx6ull.dtsi директиву #include «imx6ull-pinfunc.h». И мало того, в описании пинов мы увидим:
&pxp {
compatible = "fsl,imx6ull-pxp";
interrupts = ,
;
};
GIC_SPI и IRQ_TYPE_LEVEL_HIGH — это константы, объявленные в h-файле. Таким образом, константы могут быть использованы везде в значениях ключевых слов. В процессе компиляции на их место будет подставлено конкретное значение. Это добавляет гибкости описаниям Devicetree. Эта гибкость может относиться не только, например, к адресам, но и к значениям, инициализируемым в тех или иных регистрах платы. Некоторые чипы позволяют программировать, например, импеданс выходов. Изменив одну константу в описании, иногда можно избежать перепайки целого устройства, если схемотехники ошиблись, и выход чипа не согласован с остальной схемой.
Этим не исчерпывается гибкость описания. Кто-то уже мог заметить, что деление на .dts/.dtsi, на специфическую и общую части, даже вкупе с h-файлами не дает достаточного простора. Например, а что, если в нашей плате один из пинов используется не так, как в общей части описания? Или если одно из устройств из общего описания у нас не используется? Или нам нужно уточнить или расширить описание того или иного устройства? Что теперь, целиком копировать весь файл и менять в нем пару символов? К счастью, нет. Ничего копировать не нужно. В DT есть механизм ссылок, который позволяет вынести детализацию или даже переопределить параметры устройства. Собственно, пример такой ссылки уже приведен немного выше — это конструкция »&pxp { … }». Использование `&` перед именем объекта позволяет еще раз вернуться и добавить/изменить значения параметров. То есть, где-то в описании есть объект с именем pxp, там он встроен в иерархию, соединен с соответствующей шиной и пр. И в другом файле или в другом месте того же файла добавлена ссылка, которая расширяет и конкретизирует первоначально определенный объект. В нашем примере это указание на строку совместимости и список прерываний, используемых устройством. Причем, таких возвратов может быть любое количество. Есть где проявить творчество, если вы проектируете Devicetree на целую линейку родственных плат.
В контексте определения и переопределения значений параметров стоит вспомнить еще одно ключевое слово, широко используемое в Devicetree — это параметр объекта «status». Он показывает, используется устройство или нет. Может принимать значения «okay» и «disabled». Типичным использованием является добавление «status = «disabled»;» при первоначальном описании объекта, и последующее «включение» устройства в ссылке на него:
/ {
soc {
gpmi: nand-controller@1806000 {
compatible = "fsl,imx6q-gpmi-nand";
…
status = "disabled";
};
};
…
&gpmi {
status = "okay";
};
Интересно то, что символ `&` имеет еще одно применение. Можно задать значение параметра как ссылку на другой объект. Например, вот так:
&usdhc1 {
pinctrl-names = "default", "state_100mhz", "state_200mhz";
pinctrl-0 = <&pinctrl_usdhc1>;
pinctrl-1 = <&pinctrl_usdhc1_100mhz>;
pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
...
Немаловажный вопрос, который мы еще не осветили — это вопрос адресации. А какого размера адрес? В DT размер адреса задается с помощью ключевого слова »#address-cells», а размер описателя длины определяет »#size-cells». Оба имеют беззнаковые 32-битные значения. То есть число в этих параметрах указывает, сколько 32-битных слов (ячеек — cells) должно быть использовано для описания адреса и размера регистра. Второе определяет размер значения адресуемого объекта в тех же величинах, т. е. в 32-х битных словах. Например:
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ocram: sram@900000 {
compatible = "mmio-sram";
reg = <0x00900000 0x20000>;
ranges = <0 0x00900000 0x20000>;
#address-cells = <1>;
#size-cells = <1>;
};
...
Регистры reg устройства ocram размером 32-а бита (#size-cells = <1>; ) имеют 32-х битный начальный адрес 0×00900000 (#address-cells = <1>; ) и длину 0×20000. Удобно, что эти размерности сохраняются для всех дочерних объектов объекта, на уровне которого они были определены. Длина может быть опущена, тогда это описание одиночного регистра. Таких наборов для свойства reg может быть несколько. В общем случае, для устройств, не отображаемых в память, набор может еще включать выбор чипа (chipselect number): <выбор_чипа смещение длина>.
Что важно, кроме размерности — сама адресация. Адрес в параметре reg задается относительно адресного пространства устройства, в котором он описан. Чтобы указать размещение устройства и, соответственно, его регистров (блоков памяти) в адресном пространстве процессора на той или иной шине используется параметр ranges. В нашем примере выше трансляция 1:1, и параметр ranges можно было бы оставить пустым. Приведем еще один пример с трансляцией адресов на шине:
external-bus {
#address-cells = <2>;
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000 // Chipselect 1, Ethernet
1 0 0x10160000 0x1000 // Chipselect 2, i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash
ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x1000000>;
};
};
Здесь мы имеем на некой шине три устройства. Диапазоны адресов на этой шине, занимаемые каждым из устройств, описаны в параметре ranges. Первое устройство — сетевой контроллер, и с ним все более-менее ясно — его управляющие регистры размещаются с относительного адреса 0 и с абсолютного на шине external_bus 0×10100000. Третье устройство — флеш-память. Управляется аналогично — отображает свои регистры, начиная с адреса 0×30000000. А вот второе устройство представляет собой контроллер еще одной шины — i2c. Его регистры отображаются в окно 0×10160000–0×10160fff. Однако устройства на i2c не являются отображаемыми в память устройствами, поэтому для них параметр ranges не задается. Более того, отсутствие этого параметра указывает, что доступ к дочерним устройствам может быть выполнен только через программирование родительского устройства. Таким образом, часы rtc с адресом 58 будут доступны через только через контроллер i2c.
Картина описания будет неполной, если мы не снабдим наш процессор и плату питанием, тактовым генератором, и не наделим их способностью обрабатывать прерывания.
Управление частотой и питанием
В современных системах имеется множество источников тактирования, да еще и разные подсистемы могут тактироваться от нескольких разных генераторов. Это может весьма запутать дело. Стоит обратить пристальное внимание на разделы руководства по тому или иному чипу, чтобы правильно описать сами генераторы и их потребителей. Иначе, легко можно получить неработоспособную ОС, или даже испортить оборудование.
Рассмотрим еще один фрагмент описания платформы.
/ {
compatible = "rockchip,rk3568";
cpus {
#address-cells = <2>;
#size-cells = <0>;
cpu0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a55";
reg = <0x0 0x0>;
clocks = <&scmi_clk 0>;
enable-method = "psci";
operating-points-v2 = <&cpu0_opp_table>;
};
...
};
cpu0_opp_table: cpu0-opp-table {
compatible = "operating-points-v2";
opp-shared;
opp-408000000 {
opp-hz = /bits/ 64 <408000000>;
opp-microvolt = <900000 900000 1150000>;
clock-latency-ns = <40000>;
};
opp-600000000 {
opp-hz = /bits/ 64 <600000000>;
opp-microvolt = <900000 900000 1150000>;
};
...
};
firmware {
scmi: scmi {
compatible = "arm,scmi-smc";
arm,smc-id = <0x82000010>;
shmem = <&scmi_shmem>;
#address-cells = <1>;
#size-cells = <0>;
scmi_clk: protocol@14 {
reg = <0x14>;
#clock-cells = <1>;
};
};
};
...
Здесь описывается процессор и режимы его работы. Генератор тактовой частоты задается ключевым словом »clocks». В нашем случае это объект »scmi_clk». Мы к нему вернемся немного позже. Обратим внимание на строку, начинающуюся с «operating-points-v2 = …». Не секрет, что современные процессоры умеют менять частоту (и, соответственно, производительность) в зависимости от необходимости. Изменение частоты, как правило, связано с соответствующим изменением напряжения питания, ключевое слово »operating-points-v2» как раз и задает таблицу соответствия частоты и напряжения питания для разных режимов работы процессора.
Вернемся к объекту »smci_clk», у нас он описан в разделе »firmware» и подключен к шине smci. SCMI (System Control and Management Interface) — это протокол управления различными аспектами аппаратной платформы, в частности, частотой (производительностью) и питанием. У нашего генератора есть один управляющий регистр с адресом 0×14 на шине smci. Следовательно, мы можем управлять генератором и питанием процессора через драйвер шины, отвечающий за устройства, совместимые с «arm, scmi-smc». Осталось отметить, что протокол SMCI реализуется во многих случаях прошивкой SoC или платы, о чем нам и говорит ключевое слово »firmware» в описании.
В системе могут быть и другие источники частоты:
xin24m: xin24m {
compatible = "fixed-clock";
clock-frequency = <24000000>;
clock-output-names = "xin24m";
#clock-cells = <0>;
};
Заданный таким образом генератор может использоваться в соответствующих устройствах, например, для управления частотой на выходе HDMI:
hdmiphy: phy@ff430000 {
compatible = "rockchip,rk3328-hdmi-phy";
reg = <0x0 0xff430000 0x0 0x10000>;
interrupts = ;
clocks = <&cru PCLK_HDMIPHY>, <&xin24m>, <&cru DCLK_HDMIPHY>;
clock-names = "sysclk", "refoclk", "refpclk";
clock-output-names = "hdmi_phy";
#clock-cells = <0>;
nvmem-cells = <&efuse_cpu_version>;
nvmem-cell-names = "cpu-version";
#phy-cells = <0>;
status = "disabled";
};
У кого-то может возникнуть иллюзия, что мы «программируем» конфигурацию аппаратной платформы. Нет, с помощью всех этих средств мы только описываем то, что было придумано проектировщиками аппаратуры. Поэтому, когда мы говорим «может использоваться» — это означает, что в спецификации платформы сказано, что для работы выхода HDMI используется генератор фиксированной частоты. Который мы и описываем, а «может» здесь относится только к тому, что один и тот же объект средствами Devicetree можно задать по-разному.
Мы уже коснулись того, как описывается управление питанием процессора. Тем не менее, разные подсистемы мало того, что могут иметь разные источники питания, так эти источники могут быть разного напряжения и быть программируемыми. В контексте DT устройства управления питанием — это такие же устройства, подключаемые к различным шинам управления. Устройство управления питанием образует домен, в который могут входить одно или несколько устройств, питаемых данной, условно говоря, микросхемой. Для указания принадлежности питания к тому или иному домену используется ключевое слово «power-domains». Это связь многие ко многим. Как одно и то же устройство может иметь несколько источников питания (принадлежать к нескольким доменам), так и один источник может питать несколько устройств.
Прерывания
Для обработки прерываний нам нужны три вещи: контроллер прерываний, область его действия и собственно прерывание конкретного устройства. Фрагменты реального dts-файла для ARMV8 иллюстрируют эти составные части. Устройство с именем «gic» является контроллером прерываний, об этом говорит использование ключевого слова «interrupt-controller». У него, как и у любого устройства, есть строка совместимости, позволяющая выбрать правильный драйвер. Есть управляющие регистры. И даже есть собственное прерывание, используемое для работы с данным чипом. Здесь же появилось новое ключевое слово »#interruprt-cells». Аналогично с »#address-cells» оно указывает, сколько 32-битных слов (ячеек — cells) используется для описания прерывания устройства. В нашем случае это три. Напомним, что символьные константы вроде GIC_PPI, как правило, попадают в описание DT из соответствующих h-файлов.
Итак, контроллер прерываний у нас есть. Область его действия определяет ключевое слово «interrupt-parent», в котором содержится соответствующая ссылка на него.
И собственно устройство timer. Оно способно генерировать четыре разных прерывания. Каждое из прерываний описывается тремя значениями, каждое из которых имеет собственное назначение. Первое — это флаг, указывающий на вид прерывания — совместно используемое или нет. Как правило используется в ARM и влияет на трансляцию номера прерывания в этой архитектуре. Второе значение — это номер прерывания. Последнее — тип прерывания, по уровню, по фронту, по срезу и инверсный сигнал или нет.
/ {
compatible = "rockchip,rk3368";
interrupt-parent = <&gic>;
...
gic: interrupt-controller@ffb71000 {
compatible = "arm,gic-400";
interrupt-controller;
#interrupt-cells = <3>;
#address-cells = <0>;
reg = <0x0 0xffb71000 0x0 0x1000>,
<0x0 0xffb72000 0x0 0x2000>,
<0x0 0xffb74000 0x0 0x2000>,
<0x0 0xffb76000 0x0 0x2000>;
interrupts = ;
};
...
timer {
compatible = "arm,armv8-timer";
interrupts = ,
,
,
;
};
Итого
Используемые в Linux ключевые слова Devicetree стандартизованы далеко не все. Стандартизованы в том смысле, что их описание содержится в спецификации Devicetree. С этой точки зрения спецификация — скорее набор общих рекомендаций, и никто не мешает добавлять в описание своих устройств свои собственные параметры — ключевые слова, и их обработчики в код Linux. Но не стоит этим злоупотреблять: прежде чем добавить «новое» ключевое слово, стоит просмотреть документацию Linux Documentation/devicetree на предмет описания других устройств данного класса. И если уж ваше устройство имеет совсем уникальные свойства, то и в документации Linux, и в спецификации Devicetree содержатся рекомендации, как лучше подобрать имя такому параметру. В таком подходе есть и неприятность — от одной версии Linux к другой версии далеко не факт, что ключевые слова для описания того или иного драйвера сохранились. Поэтому первое, что стоит сделать при портировании нового ядра на платформу — сверить имеющиеся .dts/.dtsi-файлы с драйверами текущей версии ядра.
Что почитать?
Мы захватили только самую верхушку информации, циркулирующей в Сети на тему Devicetree, чтобы показать, что не боги горшки обжигают, и нет ничего загадочного или суперсложного в этой технологии. Полезными источниками информации будут:
· Сайт организации devicetree.org https://www.devicetree.org/ и сама «Devicetree Specification». Release v0.3, расположенная на Github https://github.com/devicetree-org/devicetree-specification
· Документация Linux, относящаяся к Devicetree https://www.kernel.org/doc/html/latest/devicetree/usage-model.html
· И конечно же документация актуальная для вашей версии ядра Linux из папки Documentation/devicetree.
Удачной вам селекционной работы!