Hacking the Linux Device Tree
The Device Tree framework allows hardware to be described in an operating system dependent manner. The Linux kernel has been using device tree’s since the days of the PowerPC architecture. Since about 2012, every major architecture has supported this standard. In this guide I will be focusing on the Raspberry Pi platform because of its popularity in the maker community.
Getting started with an LED
My hardware additions are simple. I have simpley wired a red led to Raspberry Pi (rpi) gpio pins 17 and 9 (GND). A python script confirms that the led is working.
#!/usr/bin/python3
from gpiozero import LED
from time import sleep
led = LED(17)
while True:
led.on()
sleep(1)
led.off()
sleep(1)
So far, no modifications have been made to the device tree. The goal of this exercise is to use the Linux led subsystem to flash a heartbeat on the attached led. A secondary goal is to add another led node to /sys/class/leds 1. Again, I should be able to accomplish this by modifying the device tree while writing no custom kernel code.
Device Tree Overlays
Device trees are complex. Any number of hardware combinations may exist. With a rpi this may be any combination of processor, HAT, and gpio modules. Device tree overlays are the best way to add support for different permutations of hardware.
Here is an example of a fragment for a rpi. You can read more about fragments in the raspberry pi documentation.
// Enable the i2s interface
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2835";
fragment@0 {
target = <&i2s>;
__overlay__ {
status = "okay";
test_ref = <&test_label>;
test_label: test_subnode {
dummy;
};
};
};
};
Fragments can be loaded at boot-time or runtime. At boot-time, the rpi bootloader is smart enough to combine overlays to
produce a single flattened device tree for the kernel. At runtime, the dtoverlay
command can be used to load new
overlays. For example, the command sudo dtoverlay -v gpio-led gpio=17 label=hbled trigger=heartbeat
does exactly
what I want to do. This command loads a device tree overlay called “gpio-led” with parameters that set the gpio pin,
label, and kernel trigger. I will note, device tree parameters are not part of the standard device tree file format! The
raspberry pi bootloader and dtoverlay
command can parse this non-standard information, but other systems
may not be able to. The gpio-led overlay, along with all other overlays, is stored in /boot/overlays
. To state
again, all of this information is system specific to the rpi!
My Custom Overlay
At this point I know that my led works correctly, and I know that the kernel can do what I want with no modification. As
an exercise, I want to write my own overlay that successfully mimics the behavior of the system overlay. To reiterate my
goals, the led needs to be connected on rpi pin 17, the led must show up in /sys/class/leds
, and the led must begin a
heartbeat sequence when the overlay is loaded. In this exercise, since device tree parameters are non-standard, I will
forgo making the overlay configurable. Each piece of the hardware will be hard-coded in the device tree.
Here is the completed overlay. Before reading the overlay, I recommend looking at this page on elinux.org. It does a very good job of explaining the device tree syntax that I will only briefly touch on here.
/* Raspberry pi heartbeat led
*
* Configuration:
* GPIO Pin: 17, output, active high (GPIO_ACTIVE_HIGH)
* Led color: Red (LED_COLOR_ID_RED)
* Function: heartbeat (LED_FUNCTION_HEARTBEAT)
*
* Alec Matthews <[email protected]> */
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2835"; // Base RPI processor version
fragment@0 {
target = <&gpio>;
__overlay__ {
led_pin: led_pin {
// Setup the gpio pin
brcm,pins = <17>; // pin 17
brcm,function = <1>; // output
};
};
};
fragment@1 {
target-path = "/";
__overlay__ {
// a new node must be created for drivers to be reloaded
leds@0 {
compatible = "gpio-leds"; // load the correct driver
pinctrl-names = "default"; // name our only state
pinctrl-0 = <&led_pin>; // set up the pin for state 0; our
// only one.
status = "okay"; // enable the device.
led {
// These constants are defined in the linux kernel
color = <1>; // red
function = "heartbeat";
linux,default-trigger = "heartbeat";
gpios = <&gpio 17 0>; // gpio pin 17, active high
};
};
};
};
};
Now to break it down. Starting with the outer layer, then moving in.
/dts-v1/;
/plugin/;
/ {
};
This is the parent node of our overlay. It specifies the device tree version, and that it’s a plugin.
compatible = "brcm,bcm2835"; // Base RPI processor version
fragment@0 {
};
fragment@1 {
};
These lines define the base compatible system and overlay fragments. For the rpi brcm,bcm2835
is the correct base. The
compatible tag at this level is used to ensure that the following nodes are compatible with the base driver system. Each
fragment@n
declaration is resolved at load-time to modify the base device tree. The names must be unique, so they are
given a _unitaddress. The unit address is the number following the @ symbol. A unit address is often the address of
the actual hardware item on a bus or in memory. In this case, there is no bus, but the unit address is still used to
make the name unique.
fragment@0 {
target = <&gpio>;
__overlay__ {
led_pin: led_pin {
// Setup the gpio pin
brcm,pins = <17>; // pin 17
brcm,function = <1>; // output
};
};
};
Inside fragment@0 is an overlay definition for the gpio node. The gpio node is defined by the raspberry pi foundation in
the board specific device tree file. This overlay definition sets pin 17 as an output. led_pin
is labeled since it is
referenced again in the other fragment.
fragment@1 {
target-path = "/";
__overlay__ {
// a new node must be created for drivers to be reloaded
leds@0 {
compatible = "gpio-leds"; // load the correct driver
pinctrl-names = "default"; // name our only state
pinctrl-0 = <&led_pin>; // set up the pin for state 0; our
// only one.
status = "okay"; // enable the device.
led {
// These constants are defined in the linux kernel
color = <1>; // red
function = "heartbeat";
linux,default-trigger = "heartbeat";
gpios = <&gpio 17 0>; // gpio pin 17, active high
};
};
};
};
This node contains the majority of the important configuration. It first creates a new node, uniquely named leds@0
.
While it is possible to modify the existing leds node the driver is not reloaded to correctly reflect the changes.
Creating a new node causes the driver to be instantiated correctly automatically when the overlay is enabled. The
compatible = "gpio-leds";
tag informs the kernel which driver should be loaded. The driver is then responsible for
reading the remaining parameters and configuring itself accordingly. The pinctrl
tags inform the kernel where in the
device tree the gpio configuration is stored. Information on the pinctrl subsystem can be found in the kernel
documentation. This allows the kernel gpio subsystem to correctly configure the gpio before the gpio-leds driver
attempts to use them. The status
tag informs the kernel that this node is active and should be controlled by the
kernel.
The next node, led
, contains configuration information for the
kernel led subsystem. Each value under this node is
defined in a device tree schema. In this case, the schema is located in the
kernel documentation. For using the
device tree, this documentation is critical. It maps how drivers read information from the tree at runtime. Each value
is explained in full and examples are given. These schema are written for each supported device driver. Including the
previously mentioned pinctrl subsystem.
Once you have this device tree overlay it can be compiled with the device tree compiler. The command
dtc -@ -I dts -O dtb -o heartbeat.dtbo heartbeat_led.dts
compiles our device tree source file to a binary file while,
at the same time, respecting the fact that some nodes will be undefined (due to this being an overlay). After
compilation, copy the binary to the appropriate directory sudo cp heartbeat.dtbo /boot/overlays/
. Once it is copied,
then it is ready to be loaded in to the kernel sudo dtoverlay heartbeat
. If all goes well, the led should begin to
flash in a heartbeat pattern.
This is probably one of the most contrived examples for adding a device to an embedded Linux system. Real hardware devices are often connected to a bus like PCI, I2C, or SPI. In the future, I would like to do a more complex example with a device on a bus; perhaps an I2C gpio expander.
/sys/class/leds is actually a collection of symlinks to the real /sys/devices/platform/leds/leds/* directories. Symlinks are used to sort actual control files into device classes to make them easy to find.
[return]