Linux и дерево устройств

Использование данных модели дерева устройств в Linux

Аннотация

В этой статье описывается использование дерева устройств в ядре Linux. Обзор формата дерева устройств можно найти на странице "Использвание дерева устройств" на сайте devicetree.org (Device_Tree_Usage)


Содержание

Дерево устройств (DT)
1. История
2. Модель данных
2.1 Обзор с высокого уровня
2.2 Идентификация платформыn
2.3 Конфигурация времени выполения
2.4 Добавление устройств
Приложение A: AMBA устройства

Дерево устройств (DT)

The "Дерево открытых встроенных программ", или просто "Дерево устройств" (DT), структуры данных и язык для описания аппаратуры. Более строго, это описание аппаратуры, которое читает операционная система и ей становятся ненужными детальные описания аппратуры.

Структурно, DT является деревом, или ациклическим графом с именованными узлами, которые могут иметь произвольное количество свойств, инкапсулирующих произвольные данные. Существует так же механизм создания произвольных связей от одного узла к другому, вне зависимости от естественное структуры дерева.

С концептуальной точки зрения, определён общий набор соглашений использования, называемых 'bindings', которые определят, как структуры данных в дереве описывают типичные характеристики аппаратуры, включая шины данных, линии прерывания, соединения с GPIO и периферийные устройства.

Насколько возможно, аппратура описывается с использованием уже существующих "bindings" для максимального использования поддрежки уже существующего кода, но так как свойства и имена узлов являются простым текстом, то легко расширить существующие "bindings", или создавать новые, определяя новые узлы и свойства. Однако, будьте осторожны, создавая новые "bindings" без того, что бы предварительно не узнать подробнее о существующих. В настоящий момент существует два различных и несовместимых "bindings" для шины i2c, что стало возможным из-за того, что новые "bindings" создавались без первоначального ислледования того, как устройства i2c уже нумеровались в существующих системах.

1. История

Первоначальный вариант DT был создан Open Firmware как часть метода коммуникакции для передачи данных от Open Firmware к клиентским программам (и операционным системам). ОС использовали DT для построения топологии аппаратуры во время выполнения и таким образом, опеспечивают поддержку большинства доступных аппаратных средств, без информации о подробностях низкого уровня (предполагается, что драйверы доступны для всех типов устройств).

Так как Open Firmware чаще всего используется на платформах PowerPC и SPARC, то поддержка Дерева устройств на этих платформах, существует уже давно.

В 2005 году, когда в Linux для PowerPC началась большая чистка и слияние 32-битных и 64-битных версий, было принято решение потребовать поддержку DT на всех платформах PowerPC, за исключением тех, которые не используют Open Firmware. Для этого, было создано представление дерева устройств, называемым Плоским Деревом Устройств (Flattened Device Tree - FDT), которое должно было передаваться ядру как большой двоичный объект (BLOB) не требуя реальной реализации Open Firmware. U-boot, kexec и другие загрузчики были модифицированы для поддержки как передачи Device Tree Binary (DTB), так и модификации DTB во время загрузки. Дерево устройств было так же добавлено в оболочку загрузчика PowerPC (arch/powerpc/boot/*), так что DTB может быть обёрнуто в образ ядра, для обеспечения загрузки существующими встроенными программами, которые ничего не знают о DT.

Какое-то время назад, инфраструктура FDT была обобщена для использования на всех архитектурах. К моменту написания этой статьи, 6 магистральных архитектур (arm, microblaze, mips, powerpc, sparc, и x86) и одна второстепенная (nios) имели некоторый уровень поддержки дерева устройств.

2. Модель данных

Если вы никогда ранее не читали страничку Device_Tree_Usage, сходите и прочитайте сейчас. Хорошо, я подожду!

2.1 Обзор с высокого уровня

Наиболее важной вещью, которую необходимо понять, является то, что DT - это просто структура данных, которая описывает аппаратуру. Ничего магического в этом нет, и это не изгоняет магическим образом все проблемы конфигурации аппаратуры. Для этого был предоставлен язык, для разрыва связи между аппаратурой на материнской плате и драйверами устройств в ядре Linux (или любых других операционных систем аналогичного типа). Использование этой технологии позволяет сделать поддержку устройств и платы, управляемой данными; принимая решения о настройке аппаратуры на основе данных, передаваемых ядру, взамен жесткого кода, выбираемого для каждой машины.

В идеале, настройка платформы, управляемая данными, должна в результате уменьшить объём дублирования кода и облегчить поддержку широкого круга аппаратуры одним образом ядра.

Linux использует данные DT для трёх главных целей:

  1. идентификация платформы,

  2. во время исполнения, и

  3. размещение устройств.

2.2 Идентификация платформыn

Самым важным и используемым прежде всего, является то, что данные из DT идентифицируют конкретную машину. В идеале, конкретная платформа не будет основой ядра, так как все детали этой платформы будут точно описаны в DT согласованным и надёжным способом. Аппаратура не идеально, но, тем не менее, ядро должно идентифицировать машину на ранней стадии загрузки, так что бы была возможность запустить машино-зависимые настройки.

В большинстве случаев, идентификация машины неуместна и ядро, взамен этого, будет выбирать настроечный код на основе ЦП ядра или SoC (Система на чипе). Например, при загрузке ARM, настройка arch/arm/kernel/setup.c, который вызывает setup_arch(), который, в свою очередь, вызывает setup_machine_fdt() в arch/arm/kernel/devicetree.c, которая просматривает таблицу machine_desc и выбирает ту строчку в machine_desc, которая наилучшим образом соответствует данным из DT. Наилучшее соответствие определяется путём просмотра свойства 'compatible' в корне DT и сравнении его со списком dt_compat в структуре machine_desc.

Свойство 'compatible' содержит отсортированный список строк, начинающихся с точного наименования машины, за которым следует не обязательный список совместимых плат, отсортированных от наиболее совместимой к менее совместимым. Например, свойство 'compatible' корня DT для "TI BeagleBoard" и его наследника "BeagleBoard xM" должно выглядеть следующим образом:

	compatible = "ti,omap3-beagleboard", "ti,omap3450", "ti,omap3";
	compatible = "ti,omap3-beagleboard-xm", "ti,omap3450", "ti,omap3";

Здесь "ti,omap3-beagleboard-xm" точно задаёт имя модели и, дополнительно, объявляются совместимые с ней OMAP 3450 SoC, и omap3 семейство SoCs. Вы видите, что список отсортирован от наиболее конкретного элемента (точное соответствие) к наименее конкретному (SoC семейство).

Проницательный читатель заметит, что Beagle xM должен так же объявлять о совместимости с исходной Beagle board. Однако, надо предупредить об опасности делать так на уровне платы, так как это обычно верхний уровень изменений платы, даже в рамках одной линии продукта и трудно точно определить, что означает, когда одна плата объявляется совместимой с другой. На верхнем уровне лучше проявить осторожность и не объявлять о совместимости одной платы с другой. Отметим исключение, когда одна плата является носителем другой, например, когда модуль ЦП к плате носителя.

Ещё одно замечание по поводу значений 'compatible'. Любые строки, используемые в свойстве 'compatible' должны быть документированы как это показано. Добавляйте описания для строк 'compatible' в файлы Documentation/devicetree/bindings.

Возвращаясь к ARM, видим, что для каждой строки в machine_desc, ядро проверяет, имеется ли любое значение из списка dt_compat свойстве 'compatible'. Если совпадение найдено, то строка machine_desc становится кандидатом для управления машиной. После просмотра всей таблицы setup_machine_fdt() возвращает "наиболее совместимую" строку machine_desc, основываясь на том, какая строка в свойстве 'compatible' соответсвует более точно значению из machine_desc. Если такого совпадения не найдено, то возвращается значение NULL.

Рассуждая вне этой схемы, мы видим, что в большинстве случаев, единственный machine_desc может обслуживать большое количество плат, если все они используют один SoC, или одно семейство SoC. Однако, инвариантность здесь будет иметь некоторые исключения. когда конкретная плата потребует специального кода настройки, который бесполезен в общем случае. Специальные случаи должны управляться явной проверкой причиняющих беспокойство плат в общем коде настройки, но делаться это должно очень быстро, что бы не стать опасным или неуправляемым, если есть более чем пара таких случаев.

Вместо этого, список совместимости позволяет родительскому machine_desc обеспечить поддержку широкого набора плат путём указания "менее совместимых" значений в списке dt_compat. В вышеприведённом примере, поддержка родительской платы может потребовать совместимости с "ti,omap3" или "ti,omap3450" и если будет обнаружена ошибка родительской beagleboard, то на раннем этапе загрузки потребуются специальные обходные коды, для чего можно добавить новый элемент в machine_desc, который реализует обходной код только для машин на основе "ti,omap3-beagleboard".

На PowerPC используется слегка модифицированная схема, в соответствии с которой, для каждого элемента machine_desc вызывается probe() и используется первый вызов, вернувший TRUE. Однако, такой подход не принимает в рапсчёт приоритет списка совместимости, и не должен использоваться при поддержке новых архитектур.

2.3 Конфигурация времени выполения

В большинстве случаев, DT будет единственным методом передачи данных от встроенных программ в ядро Linux, а так же получения доступа к конфигурационным данным, вроде строковых параметров загрузки ядра и места расположения initrd во время выполения загрузки ОС.

Большинство этих данных содержится в узле /chosen и для загрузки Linux это должно вглядеть каким-то подобным образом:

	chosen {
		bootargs = "console=ttyS0,115200 loglevel=8";
		initrd-start = <0xc8000000>;
		initrd-end = <0xc8200000>
	};

Свойство 'bootargs' содержит аргументы ядра, а свойства 'initrd-*' задают адрес и размер двоичного образа (blob) initrd. Выбранный узел может (опционально) содержать произвольное количество дополнительных свойств, содержащих конфигурационные данные, специфичные для платыормы.

На раннем этапе загрузки, код настройки архитектуры несколько раз делает вызов of_scan_flat_dt() различными вспомогательными callback-функциями для разбора данных дерева устройств до того, как настроена страничная организация памяти. Код функции of_scan_flat_dt() сканирует дерево устройств, используя функцию helper для выделения информации, необходимой на ранних этапах загрузки. Обычно функция-helper early_init_dt_scan_chosen() используется для разбора выбранных узлов, включая параметры ядра, функция early_init_dt_scan_root() - для инициализации адресного пространства DT модели и функция early_init_dt_scan_memory() для определения размера и местоположения доступных RAM.

На ARM, функция setup_machine_fdt() отвечает за ранее сканирование DT, после выбора корректного machine_desc которое поддерживает плату.

2.4 Добавление устройств

После того, как плата идентифицирована, и после того, как обработаны данные ранней конфигурации, инициализация ядра может начать работу в обычном режиме. В некоторой точке этого процесса, вызывается функция unflatten_device_tree() для преобразования данных из DT в более эффективное представление времени выполения. Такое преобразование будет выполнятся ещё и при вызове машинно-специфичных установочных обработчиков, подобных machine_desc .init_early(), .init_irq() и .init_machine() на ARM. В оставшейся части этой главы используются примеры из реализации на ARM, но все архитектуры будут делать эти вещи намного лучше, если они будут использовать DT.

Как можно догадаться из названия, .init_early(), используется во всех машинно-специфичных настройках, которые необходимо выполнить начале процесса загрузки, а .init_irq() используется для настройки обработки прерываний. Использование DT не требует изменять поведение ни одной из этих двух функций. Если представлено DT, то функции, как .init_early() так и .init_irq() имеют возможность вызвать любую функций запроса DT (функции of_* (Open Firmware) в заголовках include/linux/of*.h для получения дополнительных данных о платформе.

Наиболее интересным обработчиком в контексте DT, является функция .init_machine(), которая напрямую отвечает за заполение Linux моделей устройств данными о платформе. Исторически, это было реализовано встроенных платформах путём определения статических структур clock, platform_devices и других данных в файле платы support .c и регистрации их, в основном, в .init_machine(). При использовании DT, взамен жесткого кодирования статических устройств для каждой платформы, список устройств может быть получен при разборе DT и динамического выделения структур описания устройств.

В простейшем случае, .init_machine() несёт ответственность только за регистрацию блока platform_devices. Концепция platform_devices используется Linux для мапирования памяти илди устройств В/В, которые не могут быть обнаружены аппаратурой и для "составных" или "виртуальных" устройств (большинство из них рассматриваются позднее). Поскольку в DT нет понятия "устройство платформы", то можно приблизительно сказать, что такие устройства - это устройства, узлы которых находятся под корнем DT, или являются дочерними в узлаш шин, мапированных на память простейшим образом.

Подошло время рассмотреть пример. Здесь показана часть дерева устройств для платы NVIDIA Tegra.

/{
	compatible = "nvidia,harmony", "nvidia,tegra20";
	#address-cells = <1>;
	#size-cells = <1>;
	interrupt-parent = <&intc>;

	chosen { };
	aliases { };

	memory {
		device_type = "memory";
		reg = <0x00000000 0x40000000>;
	};

	soc {
		compatible = "nvidia,tegra20-soc", "simple-bus";
		#address-cells = <1>;
		#size-cells = <1>;
		ranges;

		intc: interrupt-controller@50041000 {
			compatible = "nvidia,tegra20-gic";
			interrupt-controller;
			#interrupt-cells = <1>;
			reg = <0x50041000 0x1000>, < 0x50040100 0x0100 >;
		};

		serial@70006300 {
			compatible = "nvidia,tegra20-uart";
			reg = <0x70006300 0x100>;
			interrupts = <122>;
		};

		i2s1: i2s@70002800 {
			compatible = "nvidia,tegra20-i2s";
			reg = <0x70002800 0x100>;
			interrupts = <77>;
			codec = <&wm8903>;
		};

		i2c@7000c000 {
			compatible = "nvidia,tegra20-i2c";
			#address-cells = <1>;
			#size-cells = <0>;
			reg = <0x7000c000 0x100>;
			interrupts = <70>;

			wm8903: codec@1a {
				compatible = "wlf,wm8903";
				reg = <0x1a>;
				interrupts = <347>;
			};
		};
	};

	sound {
		compatible = "nvidia,harmony-sound";
		i2s-controller = <&i2s1>;
		i2s-codec = <&wm8903>;
	};
};

Во время работы .init_machine() коду поддержки платы Tegra будет необходимо просмотреть DT и решить какой узел использовать для создания platform_devices. Однако, просматривая дерево, сразу понять, какой вид устройства представляет узел и даже, вобще - представляет ли этот узел устройство. Узлы /chosen, /aliases, и /memory являются информационными узлами, не описывают устройств (несмотря на спорность этого утверждения, память, тем не менее, должна рассматриваться как устройство). Наследниками узла /soc устройства, мапированные на память, но codec@1a является i2c устройством и узел sound представляет не устройство, а скорее то, как другие устройства соединены вместе, для создания аудио системы. Я знаю, чем является каждое устройство, поэтому я хорошо понимаю платы, но как сделать, что бы ядро знало, что делать с каждым узлом ?

Хитрость заключается в том, что ядро начинает с корня дерева и просматривает те узлы, которые имеют свойство 'compatible'. Прежде всего, обычно предполагается. что любой узел, имеющий свойство 'compatible', представляет устройство некоторого типа и, во вторых, можно предположить, что любой узел, расположенный в корне, либо напрямую присоединён к шине процессора, либо это составное системное устройство, которое не может быть описано любым другим способом. для каждого из этих узлов Linux выделяет и регистрирует platform_device, которое в дальнейшем может быть связано с platform_driver.

Почему предполагается, что использование platform_device для этих узлов является безопасным ? Потому, что исходя из модели устройства в Linux, предполагается, что его устройства являются потомками контроллера шины. Например, каждый i2c_client является наследником i2c_master. Каждое spi_device является наследником шины SPI. Аналогично для шин USB, PCI, MDIO и т.д. Эта же самая иерархия ообнаруживается и в DT, в котором узел устройства I2C появляется как наследник узла шины I2C. Точно так же для SPI, MDIO, PCI и т.д. Только те устройства, которые не требуют родительского устройства конуретного типа, являются platform_deviceamba_device, но об этом немного позднее), которые будут успешно существовать в корне Linux дерева /sys/devices. Более того, если узел DT находится в корне дерева, то возможно, наилучшим решением будет регистрация его как platform_device.

Код поддержки платы в Linux вызывает of_platform_populate(NULL, NULL, NULL) для того, что бы избежать обнаружения устройств в корне дерева. Все параметры равны NULL, так как когда работа идёт в корне дерева, нет необходимости в указании начального узла, указателя на структуру родительского устройства и мы не используем таблицу соответствия. Если необходимо только зарегистрировать устройства на плате, .init_machine() может быть совсем пуста, за исключением вызова of_platform_populate().

В примере Tegra это поясняется для узлов /soc и /sound, но что можно сказать о наследниках узлов SoC? Должны ли они тоже регистрироваться как platform_device? Для поддержки DT в Linux? общим правилом является такое поведение, когда устройства наследники регистрируются драйверами родительских устройств, во время выполнения функции .probe() этого драйвера. Так например, драйвер шины I2C будет регистрировать i2c_client для каждого дочернего узла, а драйвер SPI будет регистрировать его spi_device children и аналогично для шин других типов. В соответствии с этой моделью, драйвер должен быть написан таким образом, что он привязывается к узлу SoC и просто регистрирует platform_devices для каждого из своих наследников. Код поддержки платы будет выделять и регистрировать устройство SoC , драйвер устройства SoC будет (теоретически) будет привязываться к SoC устройству и регистрировать platform_device для /soc/interrupt-controller, /soc/serial, /soc/i2s, и /soc/i2c время выполения функции .probe(). Ведь просто, правда ?

Фактически, на некоторых platform_device регистрация наследников отключается, так как многие platform_device являются общими образами, и код поддержки DT отражает это и делает вышеприведённый код простейшим. Следующий аргумент в пользу of_platform_populate() в том. что таблица of_device_id tableБ и любые узлы, соотвествующие строке в этой таблице, будут получать их дочерние узлы уже зарегистрированными. В случае tegra, код мог бы выглядеть приблизительно так:

static void __init harmony_init_machine(void)
{
	/* ... */
	of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);
}

"simple-bus" определена в спецификации ePAPR 1.0 как свойство, имеющее смысл простой шины, мапированной на память, так что код of_platform_populate() должен быть написан, исходя из предположения, что только узлы, совместимые с простейшей шиной, будут подключаться. Однако, мы передаем их аргумент, для того, что бы код поддержки платы мог всегда переопределить поведение по умолчанию.

Примечание

Необходимо обсудить устройств i2c/spi/etc

Приложение A: AMBA устройства

Устройство ARM Primecells является некоторой разновидностью устройств, присоединяемых к шине ARM AMBA, которая предоставляет некоторую поддержку обнаружения устройств и управления питанием. В Linux стркутура amba_device и тип amba_bus_type используются для пердставления устройства Primecells. Однако, небольшая проблема заключается в том, что не все устройства на шине AMBA являются устройствами Primecells и в Linux типичной ситуацией будет, когда оба экземляра - amba_device и platform_device являются наследниками одного сегмента шины.

Когда используется DT, это создаёт проблемы для of_platform_populate() так как необходимо принять решение о том, будет ли выполняться регистрация обох устройств - platform_device и amba_device или одного из них. Эта неприятная сложность при создании модели устройства не велика, но решения. исключающие её, не слишком распространены. Если узел совместим с "arm,amba-primecell", то вызов of_platform_populate() будет регистрировать его как amba_device вместо platform_device.