Raspberry Pi:Настройка/Деревья устройств, оверлеи и параметры
Содержание | Введение | Продукты | Операционная система | Настройка | Основы Linux | Аппаратные средства | Неисправности | Типовые проблемы | Часто возникающие вопросы | Библиотеки | Примеры |
Деревья устройств, оверлеи и параметры[1]
В последних версиях ядра и прошивки Raspberry Pi (включая NOOBS и Raspbian) для управления размещением некоторых ресурсов и загрузкой модулей теперь по умолчанию используется дерево устройств (device tree или просто DT). Цель внедрения этого изменения — устранить проблему, связанную с тем, когда множество драйверов спорят друг с другом за ресурсы системы, а также сделать так, чтобы HAT-модули могли выполнять конфигурацию автоматически.
Впрочем, сейчас эту систему нельзя назвать «чистым» деревом устройств, поскольку у Raspberry Pi все еще имеется специальный вспомогательный код, необходимый для инстанцирования некоторых устройств. Однако внешние интерфейсы (I2C, I2S, SPI) и аудиоустройства, которые их используют, теперь должны быть инстанцированы при помощи DTB-файла, который заносится в ядро при помощи загрузчика (start.elf).
Самый главный эффект от использования дерева устройств заключается в том, что теперь система при разрешении спора между драйверами не будет целиком и полностью полагаться на черный список модулей, а будет обращаться к нему лишь в том случае запроса от DTB. Впрочем, чтобы по-прежнему иметь возможность использовать внешние интерфейсы и подключенные к ним периферийные устройства, вам нужно будет добавить в config.txt кое-какие настройки. Ниже — несколько примеров, а более подробно об этом читайте в 3-ей главе.
# Раскомментируйте какой-либо (или сразу все) из указанных тут аппаратных интерфейсов:
#dtparam=i2c_arm=on
#dtparam=i2s=on
#dtparam=spi=on
# раскомментируйте какой-либо из указанных тут аудио-интерфейсов:
#dtoverlay=hifiberry-amp
#dtoverlay=hifiberry-dac
#dtoverlay=hifiberry-dacplus
#dtoverlay=hifiberry-digi
#dtoverlay=iqaudio-dac
#dtoverlay=iqaudio-dacplus
# Раскоментируйте, чтобы включить модуль lirc-rpi:
#dtoverlay=lirc-rpi
# Раскоментируйте, чтобы переписать дефолтные значения для модуля lirc-rpi:
#dtparam=gpio_out_pin=16
#dtparam=gpio_in_pin=17
#dtparam=gpio_in_pull=down
Часть 1. Деревья устройств
Дерево устройств или ДУ (device tree или DT) — это способ описания оборудования системы. Это описание должно включать в себя название базового процессора, а также информацию о памяти и всех периферийных устройствах (внешних и внутренних). ДУ не используется для описания ПО, но если указать в нем список аппаратных модулей, это обычно влечет загрузку драйверов этих модулей. Кроме того, ДУ — это ОС-нейтральный инструмент, поэтому все, что имеет отношение к Linux, в нем быть не должно.
В дереве устройств конфигурация оборудования предстает в виде иерархии нодов. Каждый нод может содержать суб-ноды и свойства. Свойства — это именованные массивы байтов, которые могут содержать строки, числа (от старшего к младшему) и произвольные последовательности битов (и благодаря этому — их любые комбинации). Если привести аналогию с файловой системой, то ноды — это директории, а свойства — это файлы. Расположение нодов и свойств внутри дерева можно описать при помощи «пути», т.е. со слэшами («/») в качестве разделителей между нодами, субнодами и свойствами. Одинарный слэш означает корневой нод.
1.1 Базовый синтаксис DTS
Дерево устройств обычно описывается в текстовом виде, известном как DTS (то есть «device tree source», что можно перевести как «источник данных о дереве устройств»), и сохраняется в файлах с расширением *.dts. Синтаксис DTS-файла — C-образный, т.е. группы строк в нем «скрепляются» скобочками, а в конце каждой строки ставится точка с запятой. Кроме того, точка с запятой ставится и после закрывающих скобочек – думайте об этом не как о функциях, а как о структурах (struct) языка C. Будучи скомпилированным (или, другими словами, переконвертированным), этот DTS-файл становится FDT-файлом (то есть «flattened device tree», что можно перевести как «разжеванные данные о дереве устройств») или, другими словами, DTB-файлом (то есть «device tree blob», что можно перевести как «массив двоичных данных о дереве устройств»), и получает расширение *.dtb.
Ниже — пример простого ДУ в формате DTS:
/dts-v1/;
/include/ "common.dtsi";
/ {
node1 {
a-string-property = "A string";
a-string-list-property = "first string", "second string";
a-byte-data-property = [0x01 0x23 0x34 0x56];
cousin: child-node1 {
first-child-property;
second-child-property = <1>;
a-string-property = "Hello, world";
};
child-node2 {
};
};
node2 {
an-empty-property;
a-cell-property = <1 2 3 4>; /* каждая ячейка — uint32 */
child-node1 {
my-cousin = <&cousin>;
};
};
};
/node2 {
another-property-for-node2;
};
Это дерево содержит:
- заголовок — /dts-v1/
- подключение еще одного DTS-файла — /include/; он обычно имеет расширение *.dtsi — это аналог заголовочному файлу в C (*.h); более подробно об /include/ читайте ниже
- корневой нод — /
- пару дочерних нодов — node1 и node2
- пару «дочек» для node1 — child-node1 и child-node2
- метку (cousin) и отсылку к этой метке (&cousin); подробнее о метках и отсылках читайте ниже
- несколько свойств, разбросанных тут и там по дереву устройств
- повтор нода (/node2); более подробно об /include/ читайте ниже
Свойства — это простые «пары», сформированные по принципу «параметр-значение», где значение может быть либо пустым, либо содержать произвольный поток байтов. И хотя типы данных в структуру DTS-файла не встроены, в нем все же можно использовать несколько самых фундаментальных типов данных.
Текстовые строки (нуль-терминированные — то есть строки, у которых концом считается первый встретившийся нуль-символ), которые ограничиваются двойными кавычками:
строковое-свойство = "строка";
«Ячейки» — 32-битные беззнаковые целые числа, которые ограничиваются угловыми скобочками:
ячейковое-свойство = <0xbeef 123 0xabcd1234>;
Произвольный набор байтов, который ограничивается квадратными скобочками и указывается в шестнадцатеричном виде:
бинарное-свойство = [01 23 45 67 89 ab cd ef];
Смешанные данные, «сцепляемые» вместе посредством запятых:
смешанное-свойство = "строка", [01 23 45 67], <0x12345678>;
Запятые также используются для создания списков строк:
список-строк = "красная рыба", "синяя рыба";
1.2. О директиве /include/
Результат использования директивы /include/ — это простое подключение текстового блока (то есть она схожа с директивой #include из языка C), однако из-за особенностей компилятора дерева устройств этот текстовый блок может стать причиной сразу нескольких изменений. Учитывая то, что у нодов есть названия и «пути», то может случиться, что один и тот же нод появится в DTS-файле (и его подключениях) дважды. Когда это происходит, ноды и свойства объединяются, а свойства взаимозаменяются и переписываются (более ранние значения переписываются более поздними).
Например, во фрагменте выше второе появление /node2 влечет за собой добавление в оригинал нового свойства:
/node2 {
an-empty-property;
a-cell-property = <1 2 3 4>; /* каждая ячейка - uint32 */
another-property-for-node2;
child-node1 {
my-cousin = <&cousin>;
};
};
Следовательно, один и тот же *.dtsi может переписать (или задать по умолчанию) в дереве устройств сразу несколько значений.
1.3. Метки и отсылки
Часто необходимо сделать так, чтобы одна часть ДУ ссылалась на другую, и сделать это можно четырьмя способами:
- Строки-пути. Путь должен говорить сам за себя. То есть, если привести аналогию с файловой системой, то /soc/i2s@7e203000 — это полный путь к I2S-девайсу на BCM2835 и BCM2836. Кроме того, несмотря на то, что в конструировании пути к свойству нет ничего сложного (/soc/i2s@7e203000/status — разве сложно?), стандартные API так не делают. Сначала вам нужно найти нод, а только затем выбрать свойства для этого нода.
- Идентификатор phandle. Это уникальное 32-битное целое число, присвоенное ноду в свойстве phandle (впрочем, исторически так сложилось, что рядом с ним скорее всего будет дублирующее свойство linux,phandle). Значения в этих свойствах пронумерованы последовательно (начиная от «1», т.к. «0» — некорректное значение для phandle) и, как правило, присваиваются ДУ-компилятором, когда он натыкается на отсылку к ноду в целочисленном контексте — обычно в виде метки (см. ниже). Отсылки к нодам, использующим phandle, кодируются просто — в виде соответствующих целочисленных (ячейковых) значений. То есть у них нет какой-либо маркировки, которая указывала бы на то, что их нужно интерпретировать как phandle. Это определяется автоматически — исходя из того, для какой цели они используются.
- Метки. В языке C функция метки заключается в том, чтобы дать название месту в коде, а функция ДУ-метки — в том, чтобы дать название ноду. ДУ-компилятор обращается с отсылками к меткам по-разному: если метка используется в строковом контексте, он конвертирует ее в путь (&node), а если в целочисленном, то в идентификатор phandle (<&node>). Другими словами, когда ДУ проходит компиляцию, то метки в своем первозданном виде там не появляются. Также обратите внимание, что метки не содержат никакой структуры – это просто «бирки», которые позволяют ДУ-компилятору заметить ноды в большом и единообразном пространстве имен.
- Псевдонимы. Они похожи на метки, за исключением того, что когда дерево устройств имеет FDT-вид, они предстают в виде индекса. Они хранятся как свойства в ноде /aliases, где каждое свойство связывает отдельный псевдоним со строкой-путем. Когда ДУ имеет DTS-вид, то нод с псевдонимами в нем есть, однако строки-пути, как правило, представлены не в полном виде, а в виде отсылок к меткам (&node). API, которые взаимодействуют с ДУ и ищут нужный нод при помощи строк-путей, обычно смотрят на самый первый символ и обрабатывают те пути, которые не начинаются со слэша, потому что псевдонимы, которые должны быть переконвертированы в путь в самую первую очередь, используют таблицу /aliases.
1.4. Семантика дерева устройств
Как сконструировать дерево устройств? Как наилучшим образом охватить конфигурацию того или иного оборудования? Это сложные вопросы, требующие долгого и обстоятельного объяснения. В сети доступно много ресурсов на эту тему, и некоторые из которых перечислены ниже, однако данная статья не претендует на то, чтобы охватить всю эту информацию целиком, но все же пару вещей отметить стоит.
Свойства типа compatible — это связь между описанием оборудования и программным драйвером. Когда ОС натыкается на нод со свойством compatible, она тут же заглядывает в свою базу данных для драйверов устройств, чтобы найти наиболее подходящий. В случае с Linux это, как правило, приводит к автоматической загрузке драйвера модуля, но с тем условием, что он имеет соответствующую метку и не занесен в черный список.
Свойства типа status свидетельствуют о том, включен ли девайс или выключен. Если в этом свойстве указано значение ok, okay или никакого значения нет вообще, тогда девайс включен. Если указано значение disabled, то это значит, что девайс, соответственно, выключен. В целях удобства имеет смысл задать у всех девайсов в свойстве status значение disabled, а затем поместить их в файл *.dtsi. Далее мы можем просто подключить этот *.dtsi, а затем задать okay у тех девайсов, которые нам нужны.
Вот несколько статей о дереве устройств: раз, два и три. Для чтения третьей статьи нужна регистрация.
Часть 2. Оверлеи дерева устройств
Современные SoC (то есть «System-on-Chip», что можно перевести как «система на чипе») — это довольно сложные устройства, и ДУ для них может содержать тысячи строк. Если шагнуть еще дальше и поместить этот SoC на плату, которая тоже оснащена множеством компонентов, то ситуация станет еще более запутанной. Чтобы как-то управлять со всем этим (особенно, если разные устройства используют одни и те же компоненты), можно поместить общие элементы в файлы *.dtsi и тем самым получить возможность подключать их из разных DTS-файлов.
Но когда вы имеете дело с системами вроде Raspberry Pi, которые поддерживают разнообразные подключаемые аксессуары (вроде HAT-плат), то проблема усугубляется еще больше. Причем каждая возможная конфигурация требует отдельного ДУ, но поскольку Raspberry Pi доступна в разных версиях (A, B, A+, B+ и Pi 2), а гаджеты требуют использования лишь нескольких GPIO-контактов и поэтому могут друг с другом сосуществовать, то количество возможных комбинаций увеличивается неимоверно.
Что делать в таком случае? Способ есть — описать опциональные компоненты при помощи частичных ДУ, а затем вместе с базовым ДУ использовать их для сборки полного ДУ. Эти частичные, «опциональные» ДУ как раз и называются «оверлеями».
2.1. Фрагменты
Оверлей состоит из нескольких фрагментов, каждый из которых нацелен на какой-то конкретный нод (и его суб-ноды). Хотя эта концепция выглядит довольно просто, сам синтаксис на первый взгляд может показаться довольно странным:
// Включаем интерфейс I2S
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2708";
fragment@0 {
target = <&i2s>;
__overlay__ {
status = "okay";
};
};
};
Строка compatible указывает на то, что мы имеем дело с bcm2708, что является базовой архитектурой для BCM2835. В случае с BCM2836 можно воспользоваться строкой brcm,bcm2709, но до тех пор, пока вы нацеливаетесь на функции процессора ARM, обе архитектуры эквивалентны, благодаря чему можно воспользоваться и строкой brcm,bcm2708. Затем идет первый (и только в этом случае) фрагмент. Фрагменты нумеруются последовательно и начиная с нуля. Если этой последовательности не соблюдать, то в итоге некоторые фрагменты можно просто упустить.
Каждый фрагмент состоит из двух частей — свойства target, идентифицирующего нод, к которому нужно применить этот оверлей, и самого оверлея (__overlay__), тело которого добавлено к целевому моду. К примеру, фрагмент выше можно интерпретировать следующим образом:
/dts-v1/;
/ {
compatible = "brcm,bcm2708";
};
&i2s {
status = "okay";
};
Результатом слияния этого оверлея со стандартным базовым деревом устройств Raspberry Pi (например, bcm2708-rpi-b-plus.dtb) будет включение интерфейса I2S (как видите, в свойстве status стоит okay), но с условием, что ниже будет загружен сам оверлей. Однако если вы попытаетесь скомпилировать этот оверлей при помощи...
dtc -I dts -O dtb -o 2nd-overlay.dtb 2nd-overlay.dts
...то получите следующую ошибку:
Label or path i2s not found
Впрочем, эту ошибку можно было предвидеть, поскольку тут нет отсылки к базовому файлу *.dtb или *.dts, которая позволила бы компилятору найти метку i2s.
Попробовав еще раз, на этот раз — с оригинальным примером...
dtc -I dts -O dtb -o 1st-overlay.dtb 1st-overlay.dts
...мы получим одну из двух ошибок.
Если dtc возвращает ошибку о третьей строчке, это значит, что у него нет расширений, необходимых для работы этого оверлея. Директива /plugin/ — это сигнал компилятору, что ему нужна способность генерировать «связывающую» информацию, чтобы неразрешенные символы могли быть пропатчены позже.
Чтобы установить на Pi соответствующий dtc, впишите следующее:
sudo apt-get install device-tree-compiler
На других платформах у вас есть два варианта. Если вы загрузить исходный код ядра с гитхаба Raspberry Pi, а затем вписать make ARCH=arm dtbs, то система создаст подходящий dtc и поместит его в scripts/dtc. Или же можно вписать нижеследующее, но с поправкой на соответствующую директорию:
wget -c https://raw.githubusercontent.com/RobertCNelson/tools/master/pkgs/dtc.sh
chmod +x dtc.sh
./dtc.sh
Примечание. Этот скрипт загрузит главный исходник, сделает несколько патчей, а затем создаст и установит его. Перед запуском, возможно, имеет смысл отредактировать dtc.sh, чтобы поменять путь для загрузки (в данный момент это ~/git/dtc) и путь для установки (/usr/local/bin).
Если же вы получили ошибку Reference to non-existent node or label "i2s", то все, что нужно сделать — это поменять командную строку, чтобы компилятор перестал ругаться на неразрешенные символы, вписав туда -@:
dtc -@ -I dts -O dtb -o 1st-overlay.dtb 1st-overlay.dts
На этот раз компиляция должна пройти успешно. Если интересно, то содержимое DTB-файла можно разобрать, чтобы посмотреть, что сгенерировал компилятор:
$ fdtdump 1st-overlay.dtb
/dts-v1/;
// magic: 0xd00dfeed
// totalsize: 0x106 (262)
// off_dt_struct: 0x38
// off_dt_strings: 0xe8
// off_mem_rsvmap: 0x28
// version: 17
// last_comp_version: 16
// boot_cpuid_phys: 0x0
// size_dt_strings: 0x1e
// size_dt_struct: 0xb0
/ {
compatible = "brcm,bcm2708";
fragment@0 {
target = <0xdeadbeef>;
__overlay__ {
status = "okay";
};
};
__fixups__ {
i2s = "/fragment@0:target:0";
};
};
Ниже, после подробного описания файловой структуры, как раз находится наш фрагмент. Но будьте внимательны: там, где мы написали &i2s, теперь написано 0xdeadbeef — намек на то, что случилось что-то странное. После этого фрагмента новый нод, __fixups__, что можно перевести как «привязки». Он содержит список свойств, которые связывают названия неразрешенных символов со списком путей к ячейкам во фрагментах, которые нужно пропатчить при помощи идентификаторов phandle целевого нода, раз уж этот нод был размещен. В данном случае — это путь к значению 0xdeadbeef нода target, но фрагменты могут содержать и другие неразрешенные отсылки, которым потребуются дополнительные привязки.
Если вы вписали более сложные фрагменты, то компилятор может сгенерировать еще два дополнительных нода: __local_fixups__ и __symbols__. Первый требуется, если у какого-нибудь фрагмента есть phandle, потому что программе, которая выполняет объединение, нужно убедиться, что phandle-номера уникальны и последовательны. Второй — это объяснение того, как нужно обращаться с неразрешенными символами.
Напомним, в секции 1.3. сказано, что «когда ДУ проходит компиляцию, то метки в своем первозданном виде там не появляются», но если использовать переключатель -@, то это правило не работает. Вместо этого в ноде __symbols__ на каждую метку появляется по свойству, которое связывает метку с путем — в точности как нод aliases. По сути, механизм их работы настолько похож, что, осуществляя процесс разрешения символов, загрузчик Raspberry Pi в отсутствие нода __symbols__ тут же принимается искать нод с псевдонимами. Это полезно, поскольку, имея «качественные» псевдонимы, мы можем использовать старый dtc для создания базовых DTB-файлов.
2.2: Параметры дерева устройств
Чтобы избежать необходимости в использовании множества ДУ-оверлеев (а также в написании DTS-файлов для производителей периферийных устройств), загрузчик Raspberry Pi поддерживает новую функцию — параметры дерева устройств. Она позволяет вносить в ДУ небольшие изменения при помощи именованных параметров — по аналогии с тем, как модули ядра получают параметры от modprobe и командной строки ядра. Параметры можно выявить через базовые DTB-файлы и оверлеи, включая HAT-оверлеи.
Параметры задаются в DTS-файле путем добавления в корень нода __overrides__. Он содержит свойства, чьи названия являются названиями для параметров и чьи значения являются последовательностями, содержащими phandle-номера (отсылки к меткам) для целевых нодов, а также строки, указывающие на целевые свойства. Поддерживаются строковые, целочисленные (ячейковые) и булевы свойства.
2.2.1: Строковые параметры
Строковые параметры объявляются следующим образом:
название = <&метка>,"свойство";
То есть здесь пункты метка и свойство нужно заменить на соответствующие значения. С помощью строковых параметров целевые свойства можно увеличивать, уменьшать и создавать.
Имейте в виду, что к свойствам под названием status отношение особое — если в значении у них указано true, yes, on или ненулевое число, это конвертируется в строку "okay", а если false, no, off или ноль, то в "disabled".
2.2.2: Целочисленные параметры
Целочисленные параметры объявляются следующим образом...
название = <&метка>,"свойство.смещение"; // 8 бит
название = <&метка>,"свойство;смещение"; // 16 бит
название = <&метка>,"свойство:смещение"; // 32 бит
название = <&метка>,"свойство#смещение"; // 64 бит
...где пункты метка, свойство и смещение нужно заменить на соответствующие значения. В данном случае смещение — это значение, которое указывается в байтах (по умолчанию — в десятичном виде) относительно начала свойства. Вид разделителя («.», «;», «:» или «#») определяет размер параметра. Целочисленные параметры должны отсылать к существующей части свойства — с их помощью нельзя увеличивать целевые свойства.
2.2.3: Булевы параметры
ДУ кодирует булевы значения посредством свойств с «нулевой длиной» — если свойство есть, то это true, а если нет, то false. Они объявляются следующим образом:
булево_свойство; // задаем 'булево_свойство' как true
Еще раз отмечаем — для того, чтобы задать булево_свойство как false, его просто не нужно объявлять. Булевы параметры объявляются следующим образом...
название = <&метка>,"свойство?";
...где пункты метка и свойство нужно заменить на соответствующие значения. При помощи булевых параметров свойства можно либо создавать, либо удалять.
2.2.4: Примеры
Вот несколько примеров свойств разных типов вместе с параметрами, которые их модифицируют:
/ {
fragment@0 {
target-path = "/";
__overlay__ {
test: test_node {
string = "hello";
status = "disabled";
bytes = /bits/ 8 <0x67 0x89>;
u16s = /bits/ 16 <0xabcd 0xef01>;
u32s = /bits/ 32 <0xfedcba98 0x76543210>;
u64s = /bits/ 64 < 0xaaaaa5a55a5a5555 0x0000111122223333>;
bool1; // Defaults to true
// bool2 defaults to false
};
};
};
__overrides__ {
string = <&test>,"string";
enable = <&test>,"status";
byte_0 = <&test>,"bytes.0";
byte_1 = <&test>,"bytes.1";
u16_0 = <&test>,"u16s;0";
u16_1 = <&test>,"u16s;2";
u32_0 = <&test>,"u32s:0";
u32_1 = <&test>,"u32s:4";
u64_0 = <&test>,"u64s#0";
u64_1 = <&test>,"u64s#8";
bool1 = <&test>,"bool1?";
bool2 = <&test>,"bool2?";
};
};
2.2.5: Параметры с несколькими целями
В ряде случаев удобно задать одно и то же значение в разных местах дерева устройств. То есть вместо того, чтобы создавать несколько разных параметров, можно взять один параметр и добавить к нему несколько целей, объединив их примерно следующим образом (этот пример взят из оверлея w1-gpio):
__overrides__ {
gpiopin = <&w1>,"gpios:4",
<&w1_pins>,"brcm,pins:0";
...
};
Также стоит отметить, что в одном параметре можно нацелиться на свойства не одного, а сразу нескольких типов. К примеру, параметр enable можно подключить и к строке status, и к ячейке (содержащей единицу или ноль), и к соответствующему булеву свойству.
2.2.6: Другие примеры оверлеев
За другими оверлеями можно обратиться к постоянно растущей коллекции в GitHub-разделе raspberrypi/linux, т.е. тут.
Часть 3. Использование дерева устройств на Raspberry Pi
3.1: Оверлеи и config.txt
На Raspberry Pi немаловажную часть работы с оверлеями выполняет загрузчик (один из файлов start*.elf) — он объединяет их с соответствующим базовым ДУ, а затем заносит полностью разрешенное ДУ в ядро. Базовые ДУ расположены неподалеку от start.elf, в директории FAT (/boot в Linux), и носят названия bcm2708-rpi-b.dtb, bcm2708-rpi-b-plus.dtb, bcm2708-rpi-cm.dtb и bcm2709-rpi-2-b.dtb. Имейте в виду, что модели A и A+ используют, соответственно, варианты «b» и «b-plus». Выбор осуществляется автоматически, благодаря чему один и тот же образ SD-карты можно использовать на разных девайсах.
Примечание. Дерево устройств и ATAGs — это взаимоисключаемы. То есть, если поместить DTB-файл в ядро, которое его не понимает, это приведет к сбою загрузки. Во избежание этого загрузчик проверяет образы ядра на совместимость с ДУ, которая обозначается посредством метки, добавляемой утилитой mkknlimg. Ее можно найти здесь или в директории со скриптами в дереве ядра. Ядро без этой метки рассматривается как несовместимое с ДУ.
Кроме того, теперь загрузчик поддерживает билды, использующие bcm2835_deconfig, и включает для них уже имеющуюся поддержку BCM2835. Эта конфигурация влечет за собой создание ДУ на основе bcm2835-rpi-b.dtb или bcm2835-rpi-b-plus.dtb. Если эти файлы были скопированы вместе с ядром, и если ядро было помечено одной из последних версий mkklimg, то загрузчик будет пытаться загружать один из этих DTB-файлов по умолчанию.
Для того, чтобы управлять ДУ и оверлеями, загрузчик поддерживает несколько новых директив для config.txt:
dtoverlay=acme-board
dtparam=foo=bar,level=42
Эти директивы заставят загрузчика искать в разделе с прошивкой файл overlays/acme-board-overlay.dtb, который Raspbian помещает в раздел /boot. После этого он примется искать параметры foo и level, а затем присвоит им указанные значения.
Кроме того, загрузчик будет искать подсоединенную HAT-плату с программируемой EEPROM, чтобы загрузить оттуда поддерживаемый оверлей. Это происходит автоматически, безо всякого вмешательства пользователя. Есть несколько способов сообщить, что ядро использует дерево устройств:
- Сообщение ядра при загрузке будет содержать значение, указывающее на тип платы (вроде «Raspberry Pi 2 Model B»), а не на тип процессора (вроде «BCM2709»).
- Некоторое время спустя может появиться еще одно сообщение ядра, спрашивающее «No ATAGs?»
- Есть раздел /proc/device-tree с подразделами и файлами, по сути, являющимися точным зеркалом нодов и свойств ДУ.
Используя дерево устройств, ядро будет автоматически искать и загружать модули, которые поддерживают указанные и подключенные к Pi девайсы. Таким образом, создавая для девайса соответствующий оверлей, вы спасаете пользователей от необходимости редактировать /etc/modules, потому что вся конфигурация выполняется в config.txt (этот шаг не нужен даже в случае с HAT). Впрочем, имейте в виду, что многоуровневые модули вроде i2c-dev по-прежнему нужно загружать напрямую.
Обратная сторона этого метода — в том, что устройства самой платформы созданы не будут, пока не будет запроса от DTB-файла. Но в то же время вам уже не нужно заносить в черный список модули, которые обычно загружаются, когда устройства платформы определяются вспомогательным кодом платы. По сути, нынешние образы Raspbian поставляются без файла с черным списком.
3.2: Параметры ДУ
Как говорилось выше, ДУ-параметры — это удобный способ делать небольшие изменения в конфигурации девайса. В данный момент базовые DTB-файлы поддерживают параметры для включения и управления интерфейсами I2C, I2S и SPI без необходимости использования специальных оверлеев. На практике они выглядят следующим образом:
dtparam=i2c_arm=on,i2c_arm_baudrate=400000,spi=on
Обратите внимание, что в одной строчке можно разместить сразу несколько присваиваний, однако не стоит превышать лимит в 80 символов (или 79?), потому что это может кончиться очень плохо.
В результате дефолтный config.txt может содержать примерно такой фрагмент:
# Раскомментируйте одну или все строчки, чтобы включить опциональные аппаратные интерфейсы:
#dtparam=i2c_arm=on
#dtparam=i2s=on
#dtparam=spi=on
Если вы имеете дело с оверлеем, определяющим несколько параметров, то их можно указать либо по одному в каждой строчке...
dtoverlay=lirc-rpi
dtparam=gpio_out_pin=16
dtparam=gpio_in_pin=17
dtparam=gpio_in_pull=down
либо все сразу в одной строчке...
dtoverlay=lirc-rpi:gpio_out_pin=16,gpio_in_pin=17,gpio_in_pull=down
Обратите внимание на символ «:» — он отделяет название оверлея от его параметров. Это одна из поддерживаемых синтаксических вариаций.
Оверлейные параметры действуют до тех пор, пока не будет загружен следующий оверлей. Если параметр с одним и тем же именем есть и в оверлее, и базовом ДУ (вообще, так делать не стоит, это запутывает ситуацию), то предпочтение отдается оверлейному параметру. Впрочем, если вам нужен параметр именно из базового DTB-файла, завершите текущий оверлей следующей строчкой:
dtoverlay=
3.3: Метки и параметры, специфичные для разных версий Raspberry Pi
Платы Raspberry Pi имеют два I2C-интерфейса. Номинально они разделены — один для ARM, а другой для графического ядра (GPU). Почти на всех моделях i2c1 отведен для ARM, а i2c0 — для GPU, где используется для управления камерой и считывания данных с EEPROM-памяти HAT-платы. Впрочем, есть две ранние версии Model B, у которых эти роли реверсированы.
Таким образом, чтобы иметь возможность использовать один набор оверлеев и параметров со всеми версиями Pi, прошивка создает несколько специальных ДУ-параметров. Вот они:
i2c/i2c_arm
i2c_vc
i2c_baudrate/i2c_arm_baudrate
i2c_vc_baudrate
Как видите, тут есть псевдонимы для i2c0, i2c1, i2c0_baudrate и i2c1_baudrate. Параметры i2c_vc и i2c_vc_baudrate рекомендуется использовать лишь в том случае, когда это действительно нужно — например, при программировании EEPROM-памяти HAT-платы. Кроме того, использование i2c_vc может предотвратить определение камеры Pi.
Для тех, кто собственноручно пишет оверлеи, будет полезно узнать, что те же псевдонимы можно применять и к меткам в ДУ-нодах для I2C. Следовательно, это будет выглядеть примерно так:
fragment@0 {
target = <&i2c_arm>;
__overlay__ {
status = "okay";
};
};
Все оверлеи, использующие числовые варианты, будут модифицированы таким образом, чтобы использовать новые псевдонимы.
3.4: HAT-платы и дерево устройств
HAT-плата — это отдельная плата для некоторых версий Raspberry Pi (A+, B+ и Pi 2 B), имеющая встроенную EEPROM-память. EEPROM-память содержит оверлей, необходимый для включения HAT-платы, а этот оверлей, в свою очередь, может содержать параметры.
Прошивка загружает HAT-оверлей автоматически, после загрузки базового DTB-файла. Таким образом, параметры этого оверлея будут доступны, пока не будут загружены другие оверлеи (или пока работа оверлея не будет завершена строчкой dtoverlay=). Если вы по какой-то причине хотите предотвратить загрузку HAT-оверлея, разместите dtoverlay= перед любой директивой dtoverlay и dtparam.
3.5: Поддерживаемые директивы и параметры
Вместо того, чтобы перечислять тут список оверлеев, предлагаем обратиться к README-файлу, который находится в разделе /boot/overlays рядом с DTB-файлами. Его также можно найти на GitHub, куда оперативно добавляются самые последние изменения и дополнения.
Часть 4. Решение проблем и приемы для профи
4.1: Отладка
Загрузчик умеет пропускать отсутствующие оверлеи и некорректные параметры, но при серьезных ошибках (вроде отсутствующего или поврежденного базового DTB-файла или сбоя при слиянии оверлеев) он сделает откат и запустит загрузку Pi без дерева устройств. В таком случае (или если вы, внеся соответствующие настройки, заметили, что система ведет себя не так, как вы ожидали) имеет смысл проверить, делал ли загрузчик какие-либо предупреждения и сообщал ли о каких-либо ошибках:
sudo vcdbg log msg
Дополнительную отладку можно включить, добавив в config.txt строчку dtdebug=1.
Если у ядра не получается загрузиться в ДУ-режиме, то, возможно, из-за того, что у образа ядра нет корректной метки. Проверить наличие метки можно при помощи утилиты knlinfo, а добавить метку — при помощи утилиты mkknlimg. Стоит отметить, что обе утилиты есть в скриптовой директории текущей версии дерева ядра Raspberry Pi.
Кроме того, вы можете создать (в некоторой степени удобочитаемую) репрезентацию текущего состояния ДУ. Сделать это можно следующим образом:
dtc -I fs /proc/device-tree
Это может быть полезно, к примеру, если вам нужно увидеть эффект от слияния оверлеев в одно базовое дерево.
Если модули ядра не загружаются так, как планировалось, проверьте, не находятся ли они в черном списке (который находится по «адресу» /etc/modprobe.d/raspi-blacklist.conf). К слову, если вы используете ДУ, то пользоваться черным списком не обязательно. Если в черном списке ничего нет, вам нужно проверить, корректные ли у этого модуля псевдонимы, поискав в /lib/modules/<version>/modules.alias значение compatible. Если и там нет, то вы, вероятно, упустили либо...
.of_match_table = xxx_of_match
...либо...
MODULE_DEVICE_TABLE(of, xxx_of_match);
Если и это не помогло, то это значит, что сбоит depmod или в целевой файловой системе не установлено обновленных модулей.
4.2: Принудительная загрузка специфического ДУ
Если вам нужны какие-то особенные функции, которые не поддерживаются дефолтным DTB-файлом (например, если вы экспериментируете с «чистым» ДУ — подход, который используется проектом ARCH_BCM2835), или если вы хотите поэкспериментировать с написанием собственного ДУ, то загрузчику можно приказать, чтобы он загрузил альтернативный DTB-файл. Сделать это можно следующим образом:
device_tree=my-pi.dtb
4.3: Отключение использования ДУ
Если вы решили, что ДУ-подход — не для вас (или просто в диагностических целях), вы можете отключить загрузку ДУ, сделав так, чтобы ядро вернулось к работе в старом режиме. Для этого в config.txt нужно добавить следующее:
device_tree=
Стоит отметить, впрочем, что будущие версии ядра эту функцию, возможно, поддерживать больше не будут.
4.4: Сокращения и синтаксические вариации
Загрузчик понимает несколько сокращений. К примеру, вот эти строки...
dtparam=i2c_arm=on
dtparam=i2s=on
...можно сократить до...
dtparam=i2c,i2s
...где i2c — это псевдоним для i2c_arm, а on как бы сам собой разумеется. Кроме того, он принимает и более длинные версии — device_tree_overlay и device_tree_param.
Также есть возможность использовать другие разделители, если вам кажется, к примеру, что символ «=» используется слишком часто. Допустимы следующие варианты:
dtoverlay thing:name=value,othername=othervalue
dtparam setme andsetme='long string with spaces and "a quote"'
dtparam quote="'"
В этих примерах для отделения директивы от оставшейся части строки вместо «=» используется пробел. Кроме того, тут для отделения оверлея от параметров используется двоеточие, а также setme — с учетом того, что дефолтное значение равно 1, true, on или okay.