Raspberry Pi:Настройка/Деревья устройств, оверлеи и параметры

Материал из Онлайн справочника
Перейти к навигацииПерейти к поиску

Перевод: Максим Кузьмин
Проверка/Оформление/Редактирование: Мякишев Е.А.


Деревья устройств, оверлеи и параметры[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, чтобы загрузить оттуда поддерживаемый оверлей. Это происходит автоматически, безо всякого вмешательства пользователя. Есть несколько способов сообщить, что ядро использует дерево устройств:

  1. Сообщение ядра при загрузке будет содержать значение, указывающее на тип платы (вроде «Raspberry Pi 2 Model B»), а не на тип процессора (вроде «BCM2709»).
  2. Некоторое время спустя может появиться еще одно сообщение ядра, спрашивающее «No ATAGs?»
  3. Есть раздел /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.

См.также

Внешние ссылки