Иногда старые проекты дают о себе знать в самый неожиданный момент — так случилось и с моим Linux GPIO Daemon. Коллеги из департамента методик и автоматизации тестирования в YADRO заинтересовались разработкой, и я наконец решил довести его до ума. Расскажу о демоне, который реагирует на события линий: текстовым сообщением об изменении состояния в сокет либо запуском скрипта. Это аналог incron-ng, только мониторит он не файлы, а линии GPIO. А в конце обсудим, как найти и затем не терять нужный нам gpiochip.
Этот текст — небольшое отступление от моей серии статей, хотя тематика осталась прежней: GPIO.
Сначала функционал был очень простым — следить за изменением состояния заданных линий и отправлять текст в порт:
DI;8879.302130419;00;1 DI;8909.538474510;00;0
Такой подход также позволяет специалисту читать логи с экрана «глазом», что может пригодиться, если нужно отслеживать медленный процесс. Например, наблюдать за проезжающим на скорости 10–15 км/ч составом и смотреть, как работают датчики колеса в режиме реального времени.
Проект я выделил из более крупного и функционального решения, в которое значительный вклад внес мой бывший коллега.
Функционал постепенно рос и сейчас включает:
поддержку как старого GPIO SYSFS, так и нового UAPI GPIO;
поиск линии по глобальному номеру или по смещению относительно gpiochip;
назначение пользовательских меток для передачи в скрипт или сокет по событию;
поддержка событий high, low или both;
конфигурируемый поллинг состояния, если события по каким-то причинам не поддерживаются (например, отсутствуют прерывания на gpiochip);
отправку событий с временной меткой по сокету всем подключенным клиентам;
исполнение другой программы по событию.

Проект и сборка
В свое время проект Linux GPIO Daemon вынужденно переехал c GitHub на GitFlic, а уведомить о переезде у меня не было возможности.
Единственная внешняя зависимость — библиотека libconfuse, которая широко распространена и доступна в большинстве дистрибутивов. Сборка выполняется максимально просто с помощью Makefile:
# Просто make gpiod $ make # Или, если нужна статическая сборка (libconfuse должен быть собран со static-libs) gpiod $ make CFLAGS="-static"
Все! Сборка максимально простая, ведь сама прошивка, в которую он был включен, состояла только из BusyBox и нужных простых программ с минимумом зависимостей. Сам gpiod — это классический UNIX-демон.
Использование c сокетом
В простейшем случае это мониторинг линий и передача событий в сокет:
$ sudo /sbin/modprobe gpio-mockup gpio_mockup_ranges=-1,8 $ sudo cat /sys/kernel/debug/gpio | grep gpio-mockup-A | awk -F: '{ print $1 }' gpiochip1 # Меняем gpiochip0 в конфиге на полученный выше gpiod $ sed "s/gpiochip0/gpiochip1/g" etc/gpiod.conf.uapi > etc/test.conf gpiod $ cat etc/test.conf # (optional) to specific address, can be overriden by cmd args [default=127.0.0.1] listen = 127.0.0.1 # (optional) listen on port, can be overriden by cmd args [default=1500] port = 1500 # (optional) poll interval in msec, can be overriden by cmd args [default = 50] poll = 50 gpio { # facility to use for this gpio sysfs or uapi [default = uapi] if not explicitly provided a global facility will be used facility = uapi_v2 # (optional) gpiochip name [nodefault] gpiochip = gpiochip1 # (optional) local mapping [default=system] local = 0 # label for passing via libpack i.e. WD0, WD1, ..., WDN label = WD0 # rising, falling, both, polled [default=both] edge = both # swith gpio pin to ACTIVE LOW mode active_low = true }
Запустим gpiod без демонизации и с высоким уровнем болтливости, то есть с максимальной детализацией логов:
gpiod $ sudo ./gpiod -l 7 -n -c etc/test.conf Not daemonizing! Using config file: etc/test.conf gpiod: Couldn't open tabs directory: /etc/gpiod.d/ for reading gpiod: Failed loading tabs (No such file or directory). gpiod: failed opening /sys/class/gpio with [2] : No such file or directory gpiod: Could not initialize sysfs (No such file or directory).
Теперь нужно подключить потребителя. Я не стал добавлять в демон механизм защиты с авторизацией, так как есть много способов обеспечить нужный уровень безопасности с помощью внешних инструментов — например, через SSH-туннель с авторизацией по ключу.
$ nc localhost 1500 # забегаем чуть-чуть вперед, так как эти строки — реакция на манипуляции с gpio-mockup DI;8879.302130419;00;1 DI;8909.538474510;00;0
И наконец, генерируем нужные события:
# echo 1 > /sys/kernel/debug/gpio-mockup/gpiochip1/0 # echo 0 > /sys/kernel/debug/gpio-mockup/gpiochip1/0
Конфигурирование линий
Для идентификации линий возможны разные комбинации.
В случае sysfs можно использовать:
system— самый простой способ: указывается системный номер линии из/sys/class/gpio/export.gpiochip-label+local— этот способ лучше, так как вы будете меньше зависеть от порядка инициализации и версии ядра (об этом мы поговорим чуть позже).
С UAPI можно использовать:
gpiochip+local— са��ый простой способ, но у него тот же недостаток, что и у system для sysfs.gpiochip-label+local— надежный и безопасный способ, при котором вы не будете зависеть от порядка инициализации и версии ядра.name— метку, присвоенную в DTS/ACPI. У этого способа есть дополнительное преимущество: на практике можно использовать одинаковые имена линий на разных устройствах.
Использование gpiotabs
gpiotabs запускают скрипт или программу в ответ на событие линии. Например, нажали кнопку — запустили nginx.
События и реакция прописываются в файлах, которые по умолчанию расположены в /etc/gpio.d/. Пример из репозитория:
WD0 EDGE_RISING /tmp/test.sh -n --version -v 23 -s test $@ $% $& $t WD1 EDGE_RISING /tmp/test.sh $@ WD2 EDGE_BOTH /tmp/test.sh $% WD3 EDGE_RISING /tmp/test.sh $& WD4 EDGE_RISING /tmp/test.sh $t WD5 EDGE_BOTH,NO_LOOP /tmp/sleep_test.sh 10 WD6 EDGE_BOTH,ONESHOT /tmp/test.sh $t WD7 EDGE_RISING /tmp/sleep_test.sh 100 WD8 EDGE_FALLING /tmp/sleep_test.sh 100
WD0–7 в примере — это ярлык, который присваивается линии в конфигурационном файле. Затем идут событие и модификаторы:
EDGE_RISING— выполнить скрипт только по переднему фронту;EDGE_FALLING— только по заднему фронту;EDGE_BOTH— и по переднему и по заднему соответственно;NO_LOOP— не выполнять, пока не завершился процесс, запущенный ранее, то есть пока не отработалSIGCHLD+waitpid();ONESHOT— выполнить только один раз, после чего gpiod игнорирует все события на этой линии до полученияSIGHUP.
Далее указан полный путь к программе относительно корня файловой системы.
Наконец, прописаны разные аргументы, которые нужно передать. Все незнакомые аргументы передаются как есть, а знакомые заменяются:
$@— метка, то есть WD0–7 в нашем примере;$%— событие в текстовом виде, то есть RISE/FALL;$&— логическое состояние линии после события, то есть 0 или 1;$t— временная метка события. В режиме GPIO UAPI ее присваивает ядро, а при работе через sysfs — сам демон после высчитывания нового значения.
Тесты
Тесты, к сожалению, нужно запускать через sudo, так как нужны права на modprobe и доступ к /dev/gpiochipN или /sys/class/gpio:
gpiod $ sudo make tests make -C tests make[1]: Entering directory '/home/maquefel/workshop/gpiod-test-suite/gpiod/tests' for t in gpiod_hook_tests.bats gpiod_sysfs_tests.bats gpiod_uapi_tests.bats ; do \ bats $(realpath $t) ; \ done gpiod_hook_tests.bats ✓ hook test args count ✓ hook test passing label arg ✓ hook test passing event arg ✓ hook test passing event numerical arg ✓ hook test passing event timestamp arg ✓ hook test spurios launch prevention ✓ hook test rising event ✓ hook test falling event ✓ hook test both event ✓ hook test oneshot ✓ hook test spawning multiply children 11 tests, 0 failures gpiod_sysfs_tests.bats ✓ sysfs both interrupt test - sysfs rising interrupt test (skipped: gpio-mockup currently doesn't handle separate rise/fall and treat them as both) - sysfs falling interrupt test (skipped: gpio-mockup currently doesn't handle separate rise/fall and treat them as both) ✓ sysfs both polled test ✓ sysfs active_low test ✓ sysfs chip by label 6 tests, 0 failures, 2 skipped gpiod_uapi_tests.bats ✓ uapi both interrupt test ✓ uapi rising interrupt test ✓ uapi falling interrupt test ✓ uapi both polled test ✓ uapi chip by label ✓ uapi pin by label 6 tests, 0 failures
Установка и init-скрипты
Команда $ make DESTDIR= prefix=/usr install установит:
gpiod в
/usr/sbin,gpiod.conf.example — в
/etc/gpiod.
Можно дополнительно установить скрипт sysv:
$ make DESTDIR= install_sysvinit
Кросс-компиляция
Если уже есть скомпилированный libconfuse, то можно просто передать префикс через переменную CROSS_COMPILE. Например:
$ make CROSS_COMPILE=riscv64-unknown-linux-gnu-
Если libconfuse собирался отдельно, то процедура будет немного сложнее:
$ CFLAGS="-I../libconfuse/src/ -static" LDFLAGS="-L../build/libconfuse/src/.libs/" make CROSS_COMPILE=riscv64-unknown-linux-gnu-
В этом случае я обычно использую статическую сборку.
Buildroot
Я сделал пакет для Buildroot, который позволяет автоматически скомпилировать демон и включить его в состав кастомной прошивки одним кликом. Пакет можно найти в моем репозитории. Отправлять его в апстрим Buildroot я пока не планирую.
Поиск и идентификация конкретного gpiochip
Осталось решить одну проблему: связать представление sysfs/gpiochip с реальными физическими ножками чипа. Многие ее старательно обходят, но факт остается фактом: никто не гарантирует того же порядка номеров линий (в случае sysfs) или gpiochip (в случае UAPI). Он может зависеть от конкретной версии ядра или меняться при изменении конфигурации машины. Например, подключили переходник FTDI, и нумерация поехала — в переходнике тоже есть GPIO.
Как тогда именовать линии или порты, если поставляемый dtb, а вместе с ним и буты менять не очень хочется, но облегчить перенос конфигурации от ревизии к ревизии было бы очень удобно.
Для начала нужно найти конкретный gpiochip одним из трех способов:
знаем физический адрес нашего чипа,
берем чип из актуальной инструкции для платы или SoC,
эмпирическим методом находим тот, который работает так, как нам надо.
Затем смотрим физический адрес чипа:
# ls -la /sys/bus/gpio/devices/gpiochip*/of_node lrwxrwxrwx 1 root root 0 Jan 1 00:00 /sys/bus/gpio/devices/gpiochip0/of_node -> ../../../../../../firmware/devicetree/base/ahb/apb/gpio@1e780000 lrwxrwxrwx 1 root root 0 Jan 1 00:00 /sys/bus/gpio/devices/gpiochip1/of_node -> ../../../../../../firmware/devicetree/base/ahb/apb/gpio@1e780800
У gpiochip0 физический адрес — 1e780000, у gpiochip1 — 1e780800. Можно провести дополнительную проверку, посмотрев iomem:
# cat /proc/iomem [...] 1e780000-1e7803ff : 1e780000.gpio gpio@1e780000 1e780800-1e780fff : 1e780800.gpio gpio@1e780800 [...]
Найденный адрес позволяет идти дальше минимум двумя путями: первый позволяет не менять буты, а второй предполагает, что лучше поменять.
EUDEV
В случае с Device Tree физический адрес будет фигурировать как в пути устройства, так и в имени узла (node) dt.
Приведу вывод для aspeed2600/gpio0 в качестве примера:
# udevadm info /dev/gpiochip0 P: /devices/platform/ahb/ahb:apb/1e780000.gpio/gpiochip0 N: gpiochip0 E: DEVNAME=/dev/gpiochip0 E: DEVPATH=/devices/platform/ahb/ahb:apb/1e780000.gpio/gpiochip0 E: DEVTYPE=gpio_chip E: MAJOR=254 E: MINOR=0 E: OF_COMPATIBLE_0=aspeed,ast2600-gpio E: OF_COMPATIBLE_N=1 E: OF_FULLNAME=/ahb/apb/gpio@1e780000 E: OF_NAME=gpio E: SUBSYSTEM=gpio
Тогда можно сформулировать правило для udev, например такое:
# cat /etc/udev/rules.d/99-aspeed-gpio.rules SUBSYSTEM=="gpio", ENV{OF_FULLNAME}=="/ahb/apb/gpio@1e780000", KERNEL=="gpiochip*", SYMLINK+="gpioA"
При перезагрузке правил udev создаст ссылку /dev/gpioA:
# udevadm control --reload-rules # udevadm trigger -c add -s gpio # ls -la /dev/gpioA lrwxrwxrwx 1 root root 9 Jan 1 00:06 /dev/gpioA -> gpiochip0
Все достаточно просто. Если у нас несколько плат, то можно создать правила для каждой или попробовать сделать общее правило с более жесткими параметрами. Но у данного подхода есть недостатки.
Во-первых, к каждому чипу, скорее всего, придется применить индивидуальный подход: не забываем, что у нас есть gpiochips поверх SPI, I2C, USB и так далее. Наконец у нас есть ACPI, например:
udevadm info /dev/gpiochip0 P: /devices/platform/AMDIF031:00/gpiochip0 M: gpiochip0 R: 0 U: gpio T: gpio_chip D: c 254:0 N: gpiochip0 L: 0 E: DEVPATH=/devices/platform/AMDIF031:00/gpiochip0 E: DEVNAME=/dev/gpiochip0 E: DEVTYPE=gpio_chip E: MAJOR=254 E: MINOR=0 E: SUBSYSTEM=gpio
Во-вторых, такой способ не позволяет идентифицировать каждую линию по отдельности. Это может быть полезно, если одно и то же ПО запущено на разных платах, а функционал один и тот же: «LedGREEN», «LedRED» и так далее.
Device tree
Можно модифицировать Device Tree. Тогда нужно найти узел в Device Tree, который соответствует нашему gpiochip. Есть два способа. Первый — посмотреть оригинальный dts, который используется при разгрузке:
$ cat linux/arch/arm/boot/dts/aspeed/aspeed-g6.dtsi [...] gpio0: gpio@1e780000 { #gpio-cells = <2>; gpio-controller; compatible = "aspeed,ast2600-gpio"; reg = <0x1e780000 0x400>; interrupts = <GIC_SPI 40 IRQ_TYPE_LEVEL_HIGH>; gpio-ranges = <&pinctrl 0 0 208>; ngpios = <208>; clocks = <&syscon ASPEED_CLK_APB2>; interrupt-controller; #interrupt-cells = <2>; }; [...]
Если оригинальный dts недоступен или вы сомневаетесь в его оригинальности, то можно найти конкретную запись в работающей системе:
# find /sys/firmware/devicetree/ -name '*1e780000' /sys/firmware/devicetree/base/ahb/apb/gpio@1e780000
А затем превратить ее в читаемый текст (на большинстве дистрибутивов п��кет так и называется: «dtc»):
# dtc -f -I fs -O dts /sys/firmware/devicetree/base/ahb/apb <stdout>: ERROR (name_properties): /: "name" property is incorrect ("gpio" instead of base node name) Warning: Input tree has errors, output forced /dts-v1/; [...] gpio@1e780000 { compatible = "aspeed,ast2600-gpio"; clocks = <0x02 0x35>; gpio-controller; gpio-ranges = <0x14 0x00 0x00 0xd0>; #interrupt-cells = <0x02>; interrupts = <0x00 0x28 0x04>; ngpios = <0xd0>; phandle = <0x37>; reg = <0x1e780000 0x400>; #gpio-cells = <0x02>; interrupt-controller; }; [...]
Также можно получить текущий dts целиком, если в качестве аргумента указать команде полный путь к devicetree: /sys/firmware/devicetree/base/.
Путь к узлу можно построить вручную или воспользоваться утилитой fdtgrep из пакета u-boot-tools:
# dtc -f -I fs -O dtb /sys/firmware/devicetree/base > dts.dtb # fdtgrep -g "aspeed,ast2600-gpio" dts.dtb / { ahb { apb { gpio@1e780000 { }; gpio@1e780800 { }; }; }; }
Тогда нужный путь будет: /ahb/apb/gpio@1e780000.
Инъекция через U-Boot Proper
Один из вариантов внести изменения в существующий dtb — через консольную строку U-Boot Proper. Способ на первый взгляд простой, но абсолютно непредсказуемый по временным затратам. Например, можно было бы поступить так:
fdt addr 0x80800000 fdt resize 0x1000 fdt set /ahb/apb/gpio@1e780000 gpio-line-names A fdt set /ahb/apb/gpio@1e780000 label gpioA
Мы внесли изменения в dtb, но с поправкой на некоторые неудобства — например, необходимость перечислять все линии по порядку:
# cat /proc/device-tree/ahb/apb/gpio@1e780000/label gpioA # cat /proc/device-tree/ahb/apb/gpio@1e780000/gpio-line-names A
Дело в том, что:
способов загрузки много: FIT Image, TFTP, загрузка с раздела или по адресу;
многие нужные команды могут отсутствовать в штатно поставляемом загрузчике;
формат компонентов может не подойти.
Например, для ASPEED ast2600 пришлось добавлять CONFIG_CMD_BOOTZ и CONFIG_CMD_XIMG и перекомпилировать U-Boot Proper, чтобы были доступны команды imxtract и bootz:
imxtract 0x20100000 kernel-1 0x80001000 imxtract 0x20100000 fdt-1 0x80800000 # imxtract 0x20100000 ramdisk-1 0x80900000 fdt addr 0x80800000 fdt resize 0x1000 fdt set /ahb/apb/gpio@1e780000 gpio-line-names A fdt set /ahb/apb/gpio@1e780000 label gpioA bootz 0x80001000 0x80900000 0x80800000
Но даже этого оказалось недостаточно, так как bootz не понимает формата ramdisk, который по умолчанию включен в FIT Image. Пришлось обходными путями подгрузить ramdisk в формате U-Boot напрямую в память.
Но не проще ли добавить запись в dts?
Может сложиться так, что все необходимое уже есть, так что проверить возможность такого способа будет полезно.
Модифицируем dts
Если мы модифицируем оригинальный dts, то можно не использовать полный путь, а оперировать его меткой:
$ cat linux/arch/arm/boot/dts/aspeed/aspeed-ast2600-evb.dts [...] &gpio0 { label = "gpioA"; gpio-line-names = "gpioA0", "gpioA1", "gpioA2", "gpioA3", "gpioA4"; }; &gpio1 { label = "gpioB"; gpio-line-names = "gpioB0", "gpioB1", "gpioB2", "gpioB3", "gpioB4"; };
Тогда:
# cat /proc/device-tree/ahb/apb/gpio@1e780000/label gpioA # cat /proc/device-tree/ahb/apb/gpio@1e780000/gpio-line-names gpioA0gpioA1gpioA2gpioA3gpioA4
DTB Overlay
Допустим, нам нужно присвоить свойства "label" и "gpio-line-names" для gpiochip0. Соответствующий ему overlay выглядит так:
/dts-v1/; /plugin/; &{/ahb/apb/gpio@1e780000} { label = "gpioA"; gpio-line-names = "", "", "", "", "", "", "", "myLabel", "", "", "", "", "", "", "", ""; };
Но как его применить? Подгрузить его в работающее ванильное ядро нельзя. Есть поддерживаемая серия патчей of: overlay: Add DT-Overlay configfs interface для динамической инъекции. Но если мы готовы патчить и собирать ядро то, на мой взгляд, проще сразу модифицировать Device Tree.
Впрочем, я не исключаю, что многие BSP (Board Supply Package) уже включают этот патч по умолчанию, как это было (а может и есть до сих пор) с TI am335x BeagleBone, где с помощью инъекций осуществлялась поддержка плат-расширений. Но если это не так, то этот способ ничуть не лучше предыдущего: нам не обойтись без U-Boot Proper, другого загрузчика, который может модифицировать dtb перед передачей его ядру.
Вывод по использование Device tree
В свое время я решил, что проще и надежнее править BSP, а точнее — входящий в его состав Device Tree. Для серийных продуктов тотальный контроль и сборка из исходников — это аксиома, а вот для любителей могут возникнуть сложности в зависимости от их навыков.
Labels
Метку gpiochip label с включенным в ядре CONFIG_GPIO_SYSFS или CONFIG_GPIO_SYSFS_LEGACY для ядер >= v6.16 можно посмотреть так:
# cat /sys/class/gpio/gpiochip512/label 1e780000.gpio
Или так (gpiodetect — это часть пакета libgpiod):
# gpiodetect gpiochip0 [1e780000.gpio] (208 lines) gpiochip1 [1e780800.gpio] (36 lines)
Проблема меток в том, что они присваиваются драйвером, а у каждого драйвера свое мнение на этот счет:
linux $ git describe --tags v6.16.12-1-gbf01a42579a8 # drivers/gpio/gpio-aspeed-sgpio.c +601 gpio->chip.label = dev_name(&pdev->dev); # drivers/gpio/gpio-tps68470.c +135 tps68470_gpio->gc.label = "tps68470-gpio"; # drivers/gpio/gpio-loongson-64bit.c +284 .label = "ls3a6000_gpio", # drivers/gpio/gpio-loongson-64bit.c +165 lgpio->chip.label = lgpio->chip_data->label;
Метки тоже можно использовать, но тогда их надо знать заранее, так как общей схемы нет. К тому же я очень сильно сомневаюсь, что есть какие-то гарантии, что их внезапно не захотят поменять.
Заключение
Проект gpiod родился из необходимости иметь простой и надежный инструмент для мониторинга GPIO-событий во встраиваемых системах. Постепенно он оброс функционалом, который оказался востребован в реальных проектах:
поддержка обоих механизмов работы с GPIO (sysfs и uapi),
гибкое конфигурирование линий,
возможность запуска скриптов по событиям и отправка данных в сокет.
Однако главная проблема, с которой часто сталкивается любой разработчик embedded-систем, — ненадежность нумерации gpiochip. От перезагрузки к перезагрузке, от версии к версии ядра, при подключении дополнительных устройств порядок нумерации может меняться. И если для прототипа можно позволить себе жестко прописать /dev/gpiochip0, то для серийного продукта такой подход не подойдет. Думаю, мне хотя бы отчасти удалось раскрыть эту тему.
Что касается самого демона gpiod, то он остается инструментом для своих, но при этом достаточно гибким, чтобы закрыть 95% задач по мониторингу GPIO. Он не требует десятков зависимостей, прост в кросс-компиляции и легко встраивается в образы на базе Buildroot. А благодаря поддержке gpiotabs может не только сигнализировать о событиях, но и самостоятельно на них реагировать, выполняя пользовательские скрипты.
Я буду рад новым пользователям и готов помочь с адаптацией конфигурации под конкретные платформы, задавайте вопросы в комментариях.
Статья и проект посвящается Александру Сергеевичу Колонтаеву, который был моим прекрасным другом, учителем и коллегой.