Using Linux Device Trees for Fun and Profit

Written by Mike Mogenson, LeafLabs, Member of Technical staff

 

How does a computer know which parts it is composed of? How does it know what hardware and peripherals are connected? For a desktop computer, many things like storage drives and attached USB devices are discoverable on boot. But for embedded systems, a lot of the hardware is connected via non-discoverable protocols such as SPI, UART, and GPIO. The kernel, which controls the hardware, needs to be told what devices are attached and how to talk to them.

One option is to hard code this information into the kernel. This is what the Linux kernel did for some time. However, let’s say I have a configuration with an LED on GPIO 1 and you have a configuration with a push button on that pin. Both of these configurations have to be written into the kernel and there needs to be a way to select the desired configuration at startup. This was done by having the bootloader pass the kernel a value called the machine type integer on boot. This wasn’t a good long term or scalable solution. Distributing hardware configurations as part of the kernel source meant individual's specific board configurations were shared with millions of Linux users.

A new way for describing attached computer hardware, called the device tree, was adopted by the Linux kernel in version 3.7.

The device tree uses a C-like language to describe hardware and its attributes using a tree structure of nodes and properties. It is saved in a plain text file with extension “dts”. An example dummy device tree is shown below:

/dts-v1/;

/ {
    my_node {
        my_stand_alone_property;        /* C style comments */
        my_cell_property = <1 2 0x3>;   /* can be mixed types */
        my_string_property = "i am a string";
        my_string_list_property = "first string", "second string";

        my_child_node {
            my_single_cell_property = <1>;
            my_hex_array_property = [01 23 AB CD]; /* no ‘0x’ */
        };
    };
};

Right now, we're going to use a device tree to help create a niche, single purpose Linux IoT gadget to solve a specific problem of mine: I need a way to log and remotely view the temperature and humidity of my basement.

I’ve selected the Olinuxino Nano embedded Linux board to perform this task. The design is open source with available data sheets and schematics and it uses the Freescale iMX233 processor that is supported by the mainline Linux kernel. The Linux kernel also has a dht11 kernel module that supports the popular DHT22 humidity sensor. We should only need to hook the DHT22 sensor up to our board and modify the device tree to inform the iMX233 of the newly attached hardware. We can leverage the massive amount of work that has been put into the Linux kernel and write very little code ourselves. Let’s get started.

The Olinuxino Nano embedded Linux board

First, connect the DHT22 sensor to the Olinuxino Nano. The DHT22 needs 3.3V power, GND, and a data line, pulled high. The UEXT connector on the Olinuxino has 3.3V and GND pins in addition to a pin labeled CS_UEXT. This pin is connected to a pull-up resistor and is not used by any other driver. We’ll cut and resolder jumper CSS/CSH on the bottom of the board to bring out CS_UEXT to the UEXT connector.

A DHT22 sensor attached to the UEXT connector

Next, flash a Linux image to a Micro SD card used as the boot media for the Olinuxino. I’m using OpenWrt, which is a distribution targeting low powered, embedded hardware. It has a build configuration for the Olinuxino boards and uses a modern 4.4 series kernel. OpenWrt can be configured and built from scratch or a pre-built image for an Olinuxino board is available here.

Once, the Micro SD card is prepared, mount it on a desktop computer and explore the contents.

The Micro SD card has two partitions: one 8 MB FAT16 partition used by the bootloader, and one 48 MB ext4 partition used for the rootfs. The following two files are located in the bootloader partition:

$ ls
imx23-olinuxino.dtb    uImage

‘uImage’ is the U-Boot bootloader binary executable. ‘imx23-olinuxino.dtb’ is a device tree blob. The plain text ‘dts’ device tree needs to be compiled into a binary blob that can be read by the bootloader. The device tree blob can also be decompiled back into text to be inspected and modified. Install the device tree compiler, decompile the device tree blob, and take a look:

$ apt-get install device-tree-compiler
$ dtc -I dtb -O dts imx23-olinuxino.dtb > imx23-olinuxino.dts

The newly created ‘imx23-olinuxino.dts’ file is too large to print in its entirety here, but here is the last node:

leds {
    compatible = "gpio-leds";
    user {
        label = "green";
        gpios = <0x13 0x1 0x0>;
    };
};

Here we have a node labeled “leds” which has a property saying it is compatible with the “gpio-leds” driver. If we reference the documentation for the gpio-leds driver here, we see that each subnode (in our case “user”) is a controllable LED which appears as an entry in the ‘/sys/class/leds/’ directory with a name specified by the “label” property.

The only required property is the “gpios” cell property which specifies, in order, the GPIO bank, the GPIO pin offset, and whether the LED is active high (0x0) or active low (0x1). The GPIO bank is specified with a pointer handle (or phandle in device tree language) to a GPIO bank node. If we search for “phandle = <0x13>” in the ‘dts’ file we find the node for Freescale MXS compatible GPIO controller:

gpio@2 {
    compatible = "fsl,imx23-gpio", "fsl,mxs-gpio";
    interrupts = <0x12>;
    gpio-controller;
    #gpio-cells = <0x2>;
    interrupt-controller;
    #interrupt-cells = <0x2>;
    linux,phandle = <0x13>;
    phandle = <0x13>;
};

This node is GPIO bank 2. If we look at Section 37-6 of the iMX233 reference manual, offset 1 of GPIO bank 2 is labeled as “SSP1_DETECT/GPMI_CE3N/USB_ID”. This is the same label on the Olinuxino schematic for the pin connected to LED1. Everything looks correct!

Let’s make a modification and test the device tree. The “gpio-leds” driver has a property called “linux,default-trigger”. The available options for this property, listed here, are “backlight”, “default-on”, “heartbeat”, “disk-activity”, “idle-disk”, and "timer”. Add a “heartbeat” trigger to the LED node and recompile the device tree:

leds {
    compatible = "gpio-leds";
    user {
        label = "green";
        gpios = <0x13 0x1 0x0>;
        linux,default-trigger = “heartbeat”;
    };
};

$ dtc -I dts -O dtb imx23-olinuxino.dts > imx23-olinuxino.dtb

Insert the Micro SD card, boot the board, and observe the green LED pulsing. You’ve made an LED blink using only a device tree!

It’s time to tackle the humidity sensor. According to the dht11 kernel driver device tree documentation, the only required properties for a humidity sensor node are a “compatible” string with the value “dht11” and a “gpios” cell similar to the “gpio-leds” driver.

Before the new device tree node can be added, we need to find the GPIO bank and offset that correspond to the DHT22 data pin. From the Olinuxino schematic, CS_UEXT is connected to a pin labeled “GPMI_RDY0/SSP2_DETECT”. In the iMX233 reference manual, this label corresponds to GPIO bank 0 and offset 19. Now, let’s look in the ‘dts’ file for the phandle value for GPIO bank 0. This ends up being 0x11 for node “gpio@0”. Lastly, the DHT22 data pin is active high, so our humidity sensor node will look like the following:

humidity_sensor {
    compatible = "dht11";
    gpios = <0x11 19 0x0>;
};

Add this to the end of the device tree dts file, recompile, and boot. The bootloader reads the new device tree blob, finds the “humidity_sensor” node, matches the “compatible” string to the “dht11” kernel driver, loads that module, and passes the GPIO pin to the kernel module. If everything goes well, the “dht11” kernel driver should create an “iio:device0” entry in the ‘/sys/bus/iio/devices/’ directory. Navigate to the device and read the temperature and humidity:

$ cd /sys/bus/iio/devices/iio:device0
$ ls
dev                           name                    subsystem
in_humidityrelative_input     of_node                 uevent
in_temp_input                 power
$ cat in_humidityrelative_input
19200      # 19.2% humidity
$ cat in_temp_input
23300      # 23.3 degrees Celsius

Congratulations! A non-discoverable piece of hardware is now available through the standard Linux sysfs interface.

With this new sensor, we can post temperature and humidity measurements to the Internet. This short shell script logs data to a Sparkfun data stream:

#!/bin/sh

PHANT_PRIVATE_KEY=<your private key here>
PHANT_PUBLIC_KEY=<your public key here>

# read temperature and humidity values
HUMID=$(cat /sys/bus/iio/devices/iio:device0/in_humidityrelative_input)
TEMP=$(cat /sys/bus/iio/devices/iio:device0/in_temp_input)

# check to see if they are numbers
if [ "$HUMID" -eq "$HUMID" ] && [ "$TEMP" -eq "$TEMP" ]; then

    echo humidity = $HUMID
    echo temperature = $TEMP

    # post data to data.sparkfun.com
    curl -k --header "Phant-Private-Key: $PHANT_PRIVATE_KEY" \
            --data "humidity=$HUMID" \
            --data "temperature=$TEMP" \
            "https://data.sparkfun.com/input/$PHANT_PUBLIC_KEY"
fi

Once the measurements are online, we can plot them and view them from anywhere. Below is a simple HTML page showing real time humidity and temperature measurements from the DHT22 sensor and Olinuxino board.

The stand-alone HTML page can be viewed here. All of the sources for this project are posted in this repository. The full specifications for the device tree language are here and the Linux kernel device tree documentation is available here.