Custom device tree overlays for Beagle Bone (Black) running nerves
The recent release of the delux library raised my interest in device tree overlays again. delux only works with LEDs known to the led
subsystem in Linux. Like many others I was driving LEDs manually using Circuits.GPIO. However device tree overlays allow you to logically “convert” GPIOs into LEDs — at least from the kernel point of view — not physically of course. This article shows you how to create those overlays and how to integrate them into your custom nerves Beagle Bone system.
Device tree overlay to “convert” GPIOs into LEDs
On many SBC platforms (including the Beagle Bone family) the physically available external pins are muxed. I.e. they can be configured to perform different functions (e.g. GPIO, I2C, PMW, ADC …).
This configuration can be changed during runtime (to a limited extend) using tools like /usr/bin/config-pin
— e.g. configuring a GPIO for input or output or configure pull-up or pull-down resistors).
Device tree overlays are used by the Linux kernel to configure hardware incl. GPIO pins. In addition to properties like input/output use or pull up/down resistors you can also change by which kernel sub system a pin is handled. E.g. the following device tree overlay configures and converts four pins from GPIO use to LED. I.e. from the kernel point of view they are not GPIOs anymore, but LEDs accessible under /sys/class/leds/
:
/dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/am33xx.h>
&{/chosen} {
overlays {
MYFW_GPIO_LEDs.mycompany.com-overlays = __TIMESTAMP__;
};
};
&ocp {
P8_08_pinmux { status = "disabled"; }; /* P8_08: GPIO67, gpio2, 3 */
P8_10_pinmux { status = "disabled"; }; /* P8_10: GPIO68, gpio2, 4 */
P8_12_pinmux { status = "disabled"; }; /* P8_12: GPIO44, gpio1, 12 */
P8_14_pinmux { status = "disabled"; }; /* P8_14: GPIO26, gpio0, 26 */
};
&am33xx_pinmux {
bb_gpio_led_pins: pinmux_bb_gpio_led_pins {
pinctrl-single,pins = <
AM33XX_PADCONF(AM335X_PIN_GPMC_OEN_REN, PIN_OUTPUT_PULLDOWN, MUX_MODE7) /* P8_08: Red LED */
AM33XX_PADCONF(AM335X_PIN_GPMC_WEN, PIN_OUTPUT_PULLDOWN, MUX_MODE7) /* P8_10: Green LED */
AM33XX_PADCONF(AM335X_PIN_GPMC_AD12, PIN_OUTPUT_PULLDOWN, MUX_MODE7) /* P8_12: Blue LED */
AM33XX_PADCONF(AM335X_PIN_GPMC_AD10, PIN_OUTPUT_PULLDOWN, MUX_MODE7) /* P8_14: Yellow LED */
>;
};
};
&{/} {
leds {
pinctrl-names = "default";
pinctrl-0 = <&bb_gpio_led_pins>;
compatible = "gpio-leds";
myfw_red_led {
label = "myfw:red:indicator";
gpios = <&gpio2 03 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
myfw_green_led {
label = "myfw:green:indicator";
gpios = <&gpio2 04 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
myfw_blue_led {
label = "myfw:blue:indicator";
gpios = <&gpio1 12 GPIO_ACTIVE_HIGH>;
/* Blink LED at 4 Hz (125 ms on, off) */
linux,default-trigger = "timer";
default-state = "on";
led-pattern = <125 125>;
};
myfw_yellow_led {
label = "myfw:yellow:indicator";
gpios = <&gpio0 26 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
};
};
Header & includes
/dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/am33xx.h>
The first line of the overlay tells the device tree overlay compiler dtc
which version of the overlay syntax we’re using. The second line informs dtc that this is not a complete device tree but just an add-on/overlay. Lastly we’ll include some defintions/macros from the Linux kernel which whill simplify the overlay later.
Visibility in sysfs
&{/chosen} {
overlays {
MYFW_GPIO_LEDs.mycompany.com-overlays = __TIMESTAMP__;
};
};
This part tell Linux to create a entry called MYFW_GPIO_LEDs.mycompany.com-overlays
under /sys/firmware/devicetree/base/chosen/overlays
if this overlay is loaded/active. The content of the file will be the timestamp of the preprocessor run (__TIMSTAMP__
is actually a C preprocessor macro):
iex(1)> cmd "ls -alh /sys/firmware/devicetree/base/chosen/overlays"
-r--r--r-- 1 root root 9 Aug 11 09:20 name
-r--r--r-- 1 root root 25 Aug 11 09:20 BB-BONE-eMMC1-01-00A0.bb.org-overlays
-r--r--r-- 1 root root 25 Aug 11 09:20 MYFW_GPIO_LEDs.mycompany.com-overlays
-r--r--r-- 1 root root 25 Aug 11 09:20 BB-HDMI-TDA998x-00A0.bb.org-overlays
drwxr-xr-x 3 root root 0 Aug 11 09:20 ..
drwxr-xr-x 2 root root 0 Aug 11 09:20 .
0
iex(2)> cat "/sys/firmware/devicetree/base/chosen/overlays/MYFW_GPIO_LEDs.mycompany.com-overlays"
Wed Aug 10 13:18:16 2022
Disable pin-muxing for On-Chip-Peripherals
&ocp {
P8_08_pinmux { status = "disabled"; }; /* P8_08: GPIO67, gpio2, 3 */
P8_10_pinmux { status = "disabled"; }; /* P8_10: GPIO68, gpio2, 4 */
P8_12_pinmux { status = "disabled"; }; /* P8_12: GPIO44, gpio1, 12 */
P8_14_pinmux { status = "disabled"; }; /* P8_14: GPIO26, gpio0, 26 */
};
This part disables the pin muxing for four GPIO pins.
GPIO pin names
Please note that physical GPIO pins on the pin headers are known by different names by different subsystems. E.g. pin #8 on pin header 8 (P8_08
) is known as GPIO 67
to Linux. It’s also the fourth pin (zero index based) on the third GPIO chip (gpio2, 3
). If you have the GPIO number (e.g. 67
) it’s easy to get the chip/pin number as each chip controls 32 pins. As 67
equals 2 * 32 + 3
GPIO 67 equals gpio2, 3
— that is the fourth pin on the third chip. You can also check the assignments in two PDFs on Derek Molloys site. The column GPIO No.
is the Linux GPIO Number (e.g. 67
). The column Head_pin
is the physical pin (e.g. P8_08
, pin header 8, pin 8). The column Mode7
provides the chip number and pin offset (e.g. gpio2[3]
).
Configuring GPIOs
&am33xx_pinmux {
bb_gpio_led_pins: pinmux_bb_gpio_led_pins {
pinctrl-single,pins = <
AM33XX_PADCONF(AM335X_PIN_GPMC_OEN_REN, PIN_OUTPUT_PULLDOWN, MUX_MODE7) /* P8_08: Red LED */
AM33XX_PADCONF(AM335X_PIN_GPMC_WEN, PIN_OUTPUT_PULLDOWN, MUX_MODE7) /* P8_10: Green LED */
AM33XX_PADCONF(AM335X_PIN_GPMC_AD12, PIN_OUTPUT_PULLDOWN, MUX_MODE7) /* P8_12: Blue LED */
AM33XX_PADCONF(AM335X_PIN_GPMC_AD10, PIN_OUTPUT_PULLDOWN, MUX_MODE7) /* P8_14: Yellow LED */
>;
};
};
This part configures the four pins as GPIOs (MUX_MODE7
) and activates pull-down resistors (PIN_OUTPUT_PULLDOWN
). Please note that this again uses a different pin name! GPIO 67
equals AM335X_PIN_GPMC_OEN_REN
. AMX335X
is the prefix for the Beagle Bone proccessor. PIN_GPMC_OEN_REN
is the uppercase identifier from column Mode0
in the PDFs mentioned above.
Converting GPIOs to LEDs
&{/} {
leds {
pinctrl-names = "default";
pinctrl-0 = <&bb_gpio_led_pins>;
compatible = "gpio-leds";
myfw_red_led {
label = "myfw:red:indicator";
gpios = <&gpio2 03 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
myfw_green_led {
label = "myfw:green:indicator";
gpios = <&gpio2 04 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
myfw_blue_led {
label = "myfw:blue:indicator";
gpios = <&gpio1 12 GPIO_ACTIVE_HIGH>;
/* Blink LED at 4 Hz (125 ms on, off) */
linux,default-trigger = "timer";
default-state = "on";
led-pattern = <125 125>;
};
myfw_yellow_led {
label = "myfw:yellow:indicator";
gpios = <&gpio0 26 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
};
};
This part finally allows us to “convert” GPIOs to LEDs. I.e. The Linux led
subsystem will use GPIO pins to switch LEDs.
&{/} {
leds {
pinctrl-names = "default";
pinctrl-0 = <&bb_gpio_led_pins>;
compatible = "gpio-leds";
---8<-----SNIP-----8<-----SNIP-----8<-----SNIP--
Here we’ll referencing the led
branch root of the device tree (&{/}
). So we’re informing the kernel about LEDs we have … or will have … We’ll also specify to use control the Beagle Bone GPIO pins (<&bb_gpio_led_pins>
). The compatible = "gpio-leds";
line tell the kernel that these “LEDs” will be controlled by GPIOs. Placing this under the led
subsystem together with the compatible
settings leads to the led
subsystem taking care of those “LEDs” by controlling them via GPIO. The only thing missing now is actually defining LEDs and the GPIOs they can be controlld with:
Defining GPIO based LEDs
myfw_red_led {
label = "myfw:red:indicator";
gpios = <&gpio2 03 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
myfw_green_led {
label = "myfw:green:indicator";
gpios = <&gpio2 04 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
myfw_blue_led {
label = "myfw:blue:indicator";
gpios = <&gpio1 12 GPIO_ACTIVE_HIGH>;
/* Blink LED at 4 Hz (125 ms on, off) */
linux,default-trigger = "timer";
default-state = "on";
led-pattern = <125 125>;
};
myfw_yellow_led {
label = "myfw:yellow:indicator";
gpios = <&gpio0 26 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
This finally defines the actual LEDs. Each LED has a logical label which needs to be unique within the device tree (branch) — e.g. myfw_red_led
.
The label
key will define the name of the LED in the sys
filesystem. The general naming scheme is devicename:color:function
as defined in LED handling under Linux with available colors and functions defined in include/dt-bindings/leds/common.h
.
iex(1)> ls "/sys/class/leds"
beaglebone:green:usr0 beaglebone:green:usr1 beaglebone:green:usr2
beaglebone:green:usr3 mmc0:: mmc1::
myfw:blue:indicator myfw:green:indicator myfw:red:indicator
myfw:yellow:indicator
The gpios
key defines which GPIO chip and pin offset is used to drive the LED and if the line is on logical high or low when “active”. E.g. &gpio2 03
is the fourth pin at the third GPIO chip — i.e. GPIO (2 * 32 + 3) = 67
. See LEDs connected to GPIO lines for details.
The default-state
key defines the default behaviour of the LED — e.g. wether it’s on or off.
Please note that you can also define triggers in the device tree. E.g. the blue LED defines linux,default-trigger = "timer";
and led-pattern = <125 125>;
which leads to the blue LED flashing at 4Hz (125ms on, 125ms off) the moment the kernel starts. This can be a pretty good boot indicator. E.g. as long as the the firmare boots the LED is flashing. Once nerves code takes control of the LED (e.g. via delux) the LED is shut off. You can also configure more indication of e.g. disk and network activity (see Common leds properties) or generic complex patterns by using the pattern
trigger (see Pattern format for LED pattern trigger).
Compiling/Installing the Device Tree Overlay (manually)
You can compile and install the device tree overlay manually. Doing so has the small advantage that it doesn’t require a custom nerves system. The basic steps are
- Run your
*.dts
file through the C preprocessor including the matching! kernel sources to resolve includes and macros. - Run the result through the device tree compiler
dtc
. - Copy the resulting
*.dtbo
file to therootfsoverlay/lib/firmware/ directory
in your nerves project.
The following script might be a starting point (untested!):
#!/bin/sh
LINUX_SRCDIR="UNDEFINED" # Please point to the /matching/ kernel sources here
DTS_FILE="myoverlay.dts"
DTBO_FILE="myoverlay.dtbo"
FIRMWARE_DIR="your_nerves_project/rootfs_overlay/lib/firmware"
cpp -I ${LINUX_SRCDIR}/include -I ${LINUX_SRCDIR}/arch -nostdinc -undef -D__DTS__ -x assembler-with-cpp ${DTS_FILE} | \
dtc -Wno-unit_address_vs_reg -I dts -O dtb -b 0 -o ${DTBO_FILE}
cp ${DTBO_FILE} ${FIRMWARE_DIR}
Compiling/Installing the Device Tree Overlay (nerves)
A recent nerves_system_bbb
commit makes it super simple to compile/install custom device tree overlays including automatic inclusion of Linux includes and macros. As of today (Aug, 11th, 2022) this is not yet part of the official nerves_system_bbb
release. So you either need to follow main
or update your package/extra-dts/extra-dts.mk
manually.
#############################################################
#
# extra-dts
#
#############################################################
# Remember to bump the version when anything changes in this
# directory.
EXTRA_DTS_SOURCE =
EXTRA_DTS_VERSION = 0.0.2
EXTRA_DTS_DEPENDENCIES = host-dtc
define EXTRA_DTS_BUILD_CMDS
cp $(NERVES_DEFCONFIG_DIR)/package/extra-dts/*.dts* $(@D)
for filename in $(@D)/*.dts; do \
$(CPP) -I$(@D) -I $(LINUX_SRCDIR)include -I $(LINUX_SRCDIR)arch -nostdinc -undef -D__DTS__ -x assembler-with-cpp $$filename | \
$(HOST_DIR)/usr/bin/dtc -Wno-unit_address_vs_reg -@ -I dts -O dtb -b 0 -o $${filename%.dts}.dtbo || exit 1; \
done
endef
define EXTRA_DTS_INSTALL_TARGET_CMDS
cp $(@D)/*.dtbo $(TARGET_DIR)/lib/firmware
endef
$(eval $(generic-package))
One the updated file is in place (or the next version of nerves_system_bbb
is released) you can simply drop your custom *.dts
file into the package/extra-dts/
directory of your custom system. Nerves will take care of the rest (i.e. compiling the file and putting into /lib/firmware/
in the target filesystem).
Using the Device Tree Overlay
As documented in nerves_system_bbb/Device tree overlays there are two ways to load your custom overlay. You can either load the overlay by conconfiguring U-Boot:
cmd("fw_setenv uboot_overlay_addr7 /lib/firmware/myfw-gpio-leds.dtbo")
Or you can add those settings to fwup_include/provisioning.conf
:
[...]
uboot_setenv(uboot-env, "uboot_overlay_addr7", "/lib/firmware/myfw-gpio-leds.dtbo")
[...]
My take on these options is that I prefer the second one for overlays which should be enabled by default for all targets. The first approach is something where my use-case would be enabling overlays “dynamically” on a per-device basis.
DONE!
That’s it! After building/installing the firmware (and reboot if you used the first/dynamic approach above) your custom device tree overlay is ready/loaded and you can control LEDs by simply writing to sysfs
files:
iex(1)> cd "/sys/class/leds/myfw:red:indicator"
iex(3)> File.write("brightness", "255") # Enable LED
:ok
iex(8)> File.write("brightness", "0") # Disable LED
:ok
It’s of course much more convinient to use the delux library:
iex(1)> Delux.start_link(indicators: %{
...(1)> port: %{green: "myfw:green:indicator"},
...(1)> read: %{blue: "myfw:blue:indicator"},
...(1)> write: %{green: "myfw:yellow:indicator"},
...(1)> error: %{red: "myfw:red:indicator"}})
{:ok, #PID<0.1132.0>}
iex(2)> Delux.render(pid, %{error: Delux.Effects.blink(:red,2)})
And voilà … the red/error LED is blinking with 2Hz!
Tips & Tricks
Which overlay (version) is active?
If you are actively developing your overlay it’s sometimes hard to be sure which version of the overlay is actually active on the device. This is were the string given to chosen section comes into play because you can basically provide any string here. E.g. a version number:
&{/chosen} {
overlays {
MYFW_GPIO_LEDs.mycompany.com-overlays = "Version 0.0.1";
};
};
This can then be easily checked/verified on the device:
iex(1)> cat "/sys/firmware/devicetree/base/chosen/overlays/MYFW_GPIO_LEDs.mycompany.com-overlays"
Version 0.0.1
Rebuilding the firmware after device tree overlay changes
If you are actively developing your overlay always rebuilding the complete system/firmware takes too long. To force buildroot
to pickup changes to your *.dts
files in package/extra-dts
the execute the following in your mix nerves.system.shell
:
$ make extra-dts-dirclean && make
This will force buildroot to clean the extra-dts
build directory and rebuild the system. Based on your setup you might also need to run mix compile
or even mix nerves.artifact
and upload the artifact. Still much faster than rebuilding the whole system.