http://ll.anew.su/personal/html/
Итак, начну с описания той системы, для которой мне понадобились оверлеи, а также с того, что это вообще такое.
Во-первых. Речь идет о плате Orange Pi plus 2, которая используется в качестве домашнего headless сервера. И поскольку это таки headless-сервер, начальный доступ к нему осуществляется через TTL to USB адаптер. Ну а после... конечно по ssh.
Во-вторых, на момент написания статьи, эта система работает под управлением Slackware Linux current с mainline ядром 4.19.86. Никакие армбианы или прочие дебианоподобные системы меня не устраивают. Поэтому только хардкор. А это значит, что придется не просто кликать мышкой согласно инструкции, но и как-то скрипеть заржавевшими от старости мозгами.
В-третьих. Все манипуляции проводятся на уже работающей системе, т.е. сборка ядра, оверлеев и U-Boot - выполняется прямо на той самой плате, без использования всяческих тулчейнов и виртуалок. Действительно, зачем нам вся эта клоунада с тулчейнами, если у нас есть куча готовых сборок Armbian или OpenELEC, годных для компиляции ядра и т.п.?
Все, что нам надо, это рабочая системы на SD-карте с какой-нибудь Armbian, в которую, если надо, придется установить средства разработки и собрать свежий U-Boot. Затем разметить как надо внутреннюю флешку, установить на нее загрузчик и распаковать в корневой раздел необходимый для работы системный минимум, так называемую, slackware miniroot filesystem.
После этого, можно выполнить chroot в новый корневой раздел, предварительно забиндев (mount --bind) такие важные каталоги как /dev, /sys и /proc. После этого с помощью менеджера пакетов Slackware - доустанавливать весь необходимый софт. При желании, можно установить даже ядро! И не заниматься его компиляцией. Но скорее всего, наши оверлеи с таким ядром не заработают! А это значит, прощай звук и часы! Так что придется все-таки собирать свое.
Кстати, не надо забывать про fstab. При отсутствии в fstab записи о правильном корневом разделе - ядро не сможет смонтировать корневую файловую систему, и в лучшем случае, Вы получите single user mode со смонтированной в read-only корневой файловой системой. Если такое случилось, необходимо перемонтировать ФС в режиме read-write:
И уже после этого, поправить fstab. По умолчанию, в файле /etc/fstab из архива slack-current-miniroot - пусто.
Необходимо прописть в него что-то типа:
Здесь, все зависит от параметров вашего железа и используемой под рут ФС.
Но, статья не про то, как установить Slackware Linux на Orange PI plus 2, а про то, как заставить работать оверлеи с mainline ядром. Впрочем, возможно, когда-то я напишу статью и на эту тему.
Эта плата "живет" у меня уже достаточно давно, но нормально работать с mainline ядром я заставил ее только сейчас (Ноябрь 2019).
Естественно, меня не устраивало, что ни звук, ни часы, не работаю. Поэтом я решил заняться этим вплотную. До этого, разбираться было просто лень, но зря я что ли тратился на все эти, пусть и не дорогие, но полезные RTC и i2s DAC?
Для тех, перед кем стоит примерно такая же задача, эта статья может оказаться полезной, так как значительно сэкономит время и поможет вникнуть в суть. Особенно трудно приходится тем, кто совсем не знаком ни с системами на чипах (SoC - System on Chip), ни с деревом устройств, ни тем более с оверлеями. А если такой человек еще и новичок в мире Linux, то задача может оказаться и вовсе невыполнимой.
Теперь, необходимо сказать, что такое device tree и его оверлеи.
Традиционно, платы SoC не имеют системы BIOS/UEFI - любезно предоставляющей ядру ОС сведения об оборудовании на котором оно работает.
Поэтому информация о платформе, хранится в виде бинарного файла (DTB - Device Tree Blob) с описанием аппаратной части и параметров ее работы, а так же, что для нас очень важно, той самой периферии, подключенной к плате.
Таким образом, одно и то же ядро, может работать с разными платформами. Разница будет лишь в самом файле описания устройств. Для каждой платы, он будет свой.
Но, если файл с описанием устройств конкретной платформы присутствует в коде ядра, то описание подключенного к портам ввода-вывода оборудования - там отсутствует. Поэтому, даже если мы повесим физически наш i2s ЦАП на шину, ядро никак на это не отреагирует, так как ничего не знает о подключенном устройстве. Так каким же образом можно сообщить ядру, о существующем у нас оборудовании?
Правильно, чтобы исправить ситуацию, существуют оверлеи (Device Tree Overlays).
Естественно, перед тем как состояться в двоичной ипостаси, файл дерева устройств существует в текстовом варианте. То же, касается и оверлеев. Обычно, текстовый файл дерева устройств, имеет расширение DTS (Device Tree Source) и, как следует из названия, является древовидной структурой. Корневой узел в дереве, традиционно обозначается как "/".
Далее, следуют узлы (ветви) дерева, состоящие из названия узла и его параметров. Узлы могут содержать дочерние узлы, а также связи с другими узлами дерева. Узлы в свою очередь состоят из имени параметра и его значения, т.е. традиционной гетерогенной пары: ключ/значение. Каждый узел описывает параметры работы конкретного устройства, шины, контроллера, процессора и т.п., а также включает или выключает устройство для доступа к нему из ОС.
Кроме того, узел может иметь метку (label) и так называемый фандл (phandle) - уникальный в пределах всего дерева устройств, числовой идентификатор. Компилятор дерева устройств, может интерпретировать метку как фандл или же как полный путь к узлу дерева. Все зависит от конкретной ситуации.
Более подробно рассказывать о структуре дерева устройств не берусь, ибо отсутствие вменяемой документации на русском языке, а также моя лень, никак не способствуют получению обширных знаний в этом вопросе. То есть я далеко не специалист в этой области. А сами оверлеи, по большей части, были мной найдены на форуме Armbian, ссылки на который будут в конце статьи.
Также следует упомянуть, что основной текстовый файл дерева устройств, может включать в себя другие текстовые файлы устройств, подключаемые в основную структуру дерева через директиву include. Обычно такие файлы имеют расширение dtsi (device tree source include). Т.е. фактически, код дерева устройств может быть "размазан" по нескольким текстовым файлам, впоследствии компилирующихся в один бинарный. Именно это мы и наблюдаем в исходных кодах ядра Linux.
При сборке бинарного файла устройств, параметры узлов с одинаковым именем, накладываются друг на друга, при этом действительным значением параметра остается то, которое было объявлено последним.
Из исходных кодов, текстовый файл дерева устройств собирается собственным компилятором - DTC (Device Tree Compiler).
В поставку Slackware, входит пакет с компилятором DT и необходимыми инструментами, поэтому устанавливаем его, он нам понадобится в ближайшем будущем для сборки оверлеев.
От рута (или через sudo) выполняем:
Готово.
Остановлюсь на особенностях сборки собственно ядра Линукс.
Во-первых. Ядро должно быть собрано с поддержкой оверлеев. Т.е. при конфигурировании ядра, надо включить эту опцию (CONFIG_OF_OVERLAY=y).
Во-вторых. При сборке необходимо собрать драйвер звуковой карты (snd_soc_simple_card) и драйвер часов реального времени (rtc_ds1307). У меня эти драйверы собраны в виде модулей.
В-третьих. Для сборки основного дерева устройств нашей платформы, недостаточно просто "сказать":
Да, эта команда соберет нам дерево устройств, но с оверлеями, оно работать не будет!
При попытке использовать такое дерево устройств с оверлеями, U-Boot выдаст нечто вроде:
failed on fdt_overlay_apply(): FDT_ERR_NOTFOUND
base fdt does did not have a /__symbols__ node
make sure you've compiled with -@
Поэтому, для сборки правильного DTB, надо указать компилятору соответствующий флаг:
С таким флагом, в файле DTB будет создана таблица символов __symbols__ - где перечислены все соответствия между метками и полными путями узлов в дереве. Эти метки нужны для правильной работы оверлеев.
После компиляции DTB-файла - помещаем его в каталог /boot/dtb, или туда, где он будет найден скриптом U-Boot.
На самом деле, эта команда собирает сразу несколько dtb-файлов, для тех плат, которым соответствует конфигурация ядра. В моем случае, собираются все DTB-файлы плат с Allwinner H3 SoC. Поэтому среди прочих DTB-файлов, необходимо выбрать свой, соответствующий Вашей плате. Если главный файл дерева устройств будет неправильным, ядро не запустится.
В-четвертых. Если Вы все же решили собирать ядро сами, а не использовать какое-нибудь от Армбиан и Ко - может возникнуть проблема с зависанием системы. Зависание вызывает планировщик управления частотой процессора. Вероятно, в момент переключения с одной частоты на другую одного из ядер (так как подвисает лишь одно ядро).
Эта проблема обсуждалась на форуме Armbian и сообщения ядра выглядят примерно так:
INFO: rcu_sched self-detected stall on CPU
INFO: rcu_sched detected stalls on CPUs/tasks:
2-...: (1 ticks this GP) idle=18a/1/0 softirq=332295/332295 fqs=0
0-...: (0 ticks this GP) idle=dbe/1/0 softirq=475103/475103 fqs=0
При этом, сообщения появляются не всегда, а у платы отваливается ssh или сеть, часто плата наглухо виснет.
А явно может указывать на проблему, один из процессов "kworker" - постоянно занимающий процессорное время без всяких причин, чего в норме быть не должно.
Возможно, у меня данная проблема возникла из-за того, что для сборки своего ядра, я за основу использовал .config от ядра Армбиан.
Люди с форума лечили этот баг принудительным фиксированием частоты процессора в конфигурации планировщика.
Я сделал по-другому, просто установив в ядре минимальную частоту процессора.
В menuconfig:
И там выбрав "powersave governor" вместо "ondemand". Т.е. специально занизив частоту процессора на минимум. Огромная вычислительная мощь 4х ядер Allwinner H3 мне ни к чему, так как это все же скромный headless сервер и даже на этой частоте, нагрузка на процессор составляет не более 10% в пике. Обычно же процессор простаивает на 99%. Исключение составляет процесс сборки ядра или еще чего-то ресурсоемкого. Но это бывает довольно редко, так что можно и подождать на 5 минут дольше. Еще один плюс такого подхода - мы получаем довольно холодный камень, не нуждающийся в активном охлаждении.
Таким же образом, можно заставить работать процессор и на максимальной частоте (примерно 1ГГц). Для этого надо выбрать "performance" в "Default CPUFreq governor".
С такими параметрами система также работает замечательно, стабильно, и без зависаний. SoC особо не греется. А функция изменения частоты процессора на лету, нам ни к чему, ибо процессор потребляет и так не особо много, да еще и питается от сети. В любом случае, ядро всегда можно пересобрать :)
По ядру все. Успехов! Собираем его, желательно с ключом -j 4, чтобы не ждать до зеленых веников, собираем и устанавливаем модули, не забываем собрать uInitrd и подправить boot.cmd. Но об этом дальше.
Как уже было сказано выше, файл дерева устройств или оверлея - это обычный текстовый файл, содержащий описание какого-то устройства, сразу нескольких устройств или изменения для уже существующих узлов. Этот файл компилируется так же, как и основной файл дерева устройств.
В нашем случае, оверлей для i2s DAC, описывает к каким пинам GPIO будет подключена плата ЦАП, активирует выключенную по умолчанию шину i2s, создает устройство самой звуковой карты и указывает ядру, какой драйвер использовать для работы с этим ЦАП.
Т.е. на основной файл дерева устройств, накладывается файл оверлея устройств. Таким образом, оверлей модифицирует основной файл дерева устройств, и позволяет ядру узнать о новом подключенном оборудовании.
Сам оверлей для включения i2s выглядит так:
/dts-v1/;
/plugin/;
/ {
compatible = "allwinner,sun8i-h3";
fragment@0 {
target-path = "/soc/pinctrl@1c20800";
__overlay__ {
i2s0_pins: i2s0-pins {
pins = "PA18", "PA19", "PA20", "PA21";
function = "i2s0";
};
};
};
fragment@1 {
target = <&i2s0>;
__overlay__ {
pinctrl-0 = <&i2s0_pins>;
pinctrl-names = "default";
sound-dai = <&i2s0_out>;
status = "okay";
};
};
fragment@2 {
target-path = "/";
__overlay__ {
i2s0_out: i2s0-out {
#sound-dai-cells = <0>;
simple-audio-card,format = "i2s";
compatible = "linux,spdif-dit";
};
sound_i2s {
simple-audio-card,name = "i2s DAC";
compatible = "simple-audio-card";
simple-audio-card,format = "i2s";
simple-audio-card,mclk-fs = <256>;
status = "okay";
simple-audio-card,cpu {
sound-dai = <&i2s0>;
};
codec_dai: simple-audio-card,codec {
sound-dai = <&i2s0_out>;
};
};
};
};
};
Сохраняем его в произвольный файл.
Запись fragment@0 - означает, что это часть кода номер 1, который добавляется (или в который вносится правка) в конкретное место дерева устройств. Путь к нужному узлу указывается с помощью директив target или target-path, со ссылкой на метку или фандл узла.
status = "okay" - означает, что устройство включено и будет использоваться, т.е. будет доступно для ОС. По умолчанию, шина i2s в основном файле дерева устройств - выключена.
Параметр compatible - указывает ядру, какое именно оборудование использует описываемое устройство. В случае правильного указания названия аппаратной части, ядро автоматически выполнит загрузку нужного драйвера и устройство успешно подключится к системе.
Компилируем наш оверлей следующей командой:
На выходе получаем бинарный файл оверлея, с именем i2s.dtbo.
Но это еще не все. Чтобы попусту не перезагружать нашу плату для проверки правильности структуры оверлея, проверим его на совместимость с нашим основным деревом устройств, так сказать, не отходя от кассы.
Для этого выполним:
В случае успешного наложения оверлея, на выходе будем иметь файл test.dtb - который является бинарной версией слияния нашего основного файла устройств с оверлеем.
Если в результате выполнения этой команды нет ошибок, можно переходить к конфигурированию U-Boot, дабы указать ему на наш новоиспеченный оверлей и слить его с основным древом устройств при загрузке. Об этом будет рассказано ниже.
Указанное ниже, выполнять совсем не обязательно, но для проверки результата, так сказать, в явном виде, воспользуемся командой:
Эта команда позволяет увидеть изменения в текстовом виде, т.е. применился ли оверлей вообще.
Точно так же, поступаем с оверлеем для шины i2c и часов реального времени на микросхеме DS3231.
Собственно сам оверлей:
/dts-v1/;
/plugin/;
/ {
compatible = "allwinner,sun8i-h3";
fragment@0 {
target = <&i2c0>;
__overlay__ {
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
ds3231: ds1307@68 {
compatible = "maxim,ds3231";
reg = <0x68>;
status = "okay";
};
};
};
fragment@1 {
target-path = "/soc/i2c@1c2b000";
__overlay__ {
status = "disabled";
};
};
};
Фрагмент 0 включает шину i2c 0 и описывает подсоединенное к ней устройство RTC DS3231 (загружается модуль rtc_ds1307 для часов реального времени).
Фрагмент 1 - выключает шину i2c 1 (status = "disabled"). Это делается потому, что шина 1 i2c - использует те же пины на гребенке GPIO, что и шина i2s - для которой мы назначили пины сами. Если этого не сделать, наша звуковая карта не заработает, так как пины будут заняты под шину i2c.
Компилируем наш оверлей как в примере выше и помещаем его бинарную версию в каталог /boot/overlays, ну, или туда, где его найдет наш U-Boot.
Вообще, все оверлеи для конкретной платы, можно держать в одном файле, но если надо будет включить или выключить одно из устройств на плате - то придется пересобирать весь оверлей. Поэтому принцип одно устройство - один оверлей, является хорошим тоном, когда оверлей можно просто отключить или включить в конфигурационном файле загрузчика.
Как говорится, без U-Boot, и... ядро грузить нечем.
Почему статья про оверлеи, но речь теперь пойдет про U-Boot?
Да потому, что этот загрузчик ядра, широко используемый для систем на чипе, имеет прямое отношение к нашим оверлеям и самому дереву устройств. Именно он его загружает в память, "патчит" оверлеями и передает на "съедение" ядру Linux.
Как собрать U-Boot?
В этом нет ничего сложного, в сети достаточно документации и руководств по сборке и установке U-Boot.
Например, можно посмотреть здесь: https://linux-sunxi.org/U-Boot
Но если вкратце:
Берем свежий исходный код U-Boot с сайта разработчиков: ftp://ftp.denx.de/pub/u-boot/
Отлично. Wget нам в помощь.
Распаковываем. Конфигурируем сообразно нашему девайсу. Собираем.
Устанавливаем с помощью dd на нашу внутреннюю флешку (или sd-карту), с которой мы и будем загружать всю систему.
Однако, предположим, что U-Boot у нас уже собран и установлен. Так что едем дальше.
Теперь о главном. А именно, о файле boot.cmd.
Файл boot.cmd, содержит директивы U-Boot в текстовом виде, и после соответствующей правки, собирается в boot.scr - основной скрипт U-Boot. Т.е. при загрузке, U-Boot выполняет именно его.
Но опять же, статья про оверлеи, а не про загрузчик ядер.
Поэтому двигаемся вперед.
Не стесняемся и берем boot.cmd из дистрибутива Armbian, меняем его в соответствии с нашими задачами. Например, обязательно надо указать, на каком разделе нашей eMMC находится корневая файловая система, иначе ядро запаникует и скуксится. Также комментируем или удаляем те строки, которые нам не нужны.
И вот здесь, пришло время указать загрузчику, что надо загрузить наши оверлеи.
В файле boot.cmd от Армбиан, достаточно примеров загрузки оверлеев. Мы лишь указываем откуда собственно, брать наши.
Выглядит это так:
if load ${devtype} ${devnum} ${load_addr} ${prefix}overlays/i2s.dtbo; then
echo "Applying i2s overlay"
fdt apply ${load_addr} || setenv overlay_error "true"
fi
Т.е. наш оверлей для шины i2s и ЦАПа, находится в каталоге /boot/overlays и имеет имя i2s.dtbo.
При неудачной загрузке оверлея, переменная "overlay_error" принимает значение "true" и дальше обработчик скрипта отменяет этот испорченный оверлей, выполняя чистую загрузку дерева устройств.
Для оверлея часов, выполняем то же самое, изменив имя оверлея на нужный.
Опосля всех издевательств над boot.cmd мы его таки собираем в скрипт boot.scr и кладем (или ложим?) в каталог /boot:
На этом, оставляем U-Boot и иже с ним в покое, так как полагаем, что свою задачу он будет выполнять исправно.
Это самая простая и приятная часть работы.
Подключить модуль часов и модуль звуковой карты к плате Orange Pi - не составляет труда.
Для начала посмотрим на распиновку нашей GPIO гребенки.
Например здесь: https://ua3nbw.ru/all/i2s-slave-orangepi/.
Подключаем нашу звуковуху, согласно означенных в оверлее пинов. С этим все.
Соответственно, модуль часов подключаем на пины PA11 и PA12 - точно так, как у нас прописано в основном DTB файле.
Не забываем, что мы всегда можем просмотреть наш бинарный DTB-файл с помощью утилиты fdtdump и таки узреть, что же там реально есть, а чего нет и на каких пинах, что должно висеть.
Перезагружаем машину, проверяем результат.
Выполняем i2cdetect -y 0:
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- 57 -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- UU -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
Часы работают. Символы "UU" показывают, что драйвер для часов (rtc_ds1307) успешно загружен, а сами часы у нас инициализировались как rtc1.
Для синхронизации системного времени с нашими новыми часами, в файл /etc/rc.d/rc.local можно добавить такую строчку:
Выполняем aplay -l:
card 0: Codec [H3 Audio Codec], device 0: CDC PCM Codec-0 [CDC PCM Codec-0]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 1: DAC [i2s DAC], device 0: 1c22000.i2s-dit-hifi dit-hifi-0 [1c22000.i2s-dit-hifi dit-hifi-0]
Subdevices: 1/1
Subdevice #0: subdevice #0
Видим наш i2s DAC в списке. lsmod показывает, что драйвер snd_soc_simple_card - загружен. Порядок. Можно запускать cmus!
Обсуждение оверлеев для i2s DAC на форуме Armbian: https://forum.armbian.com/topic/2820-i2s-audio-on-cubietruck-with-mainline-need-help/
Распиновка гребенки GPIO: https://ua3nbw.ru/all/i2s-slave-orangepi/
Подключение часов на DS3231 к Orange Pi: http://roboforum.ru/forum2/topic18247.html
Я не отношу себя к особо грамотным линуксоидам, поэтому за допущенные технические ошибки в тексте, прошу извинить.
На этом все, успешного плавания!
С уважением, Илья aka ll.