Hacking the Linux Device Tree

Posted on May 18, 2022

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.


  1. /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]