Zephyr tutorial 102 – Device tree

Zephyr tutorial 102 – Device tree

Javad Rahimipetroudi (Mind embedded software consultant)
Javad Rahamipetroudi
24/01/2024

So far we have learned how to print a simple Hello World application into the serial output. However, any microcontroller without peripherals is a useless device. In this tutorial, we will learn how to introduce various types of peripherals in our project.

Device tree

As it is stated in the Devicetree documentation page in Linux kernel docs:

“The DT was originally created by Open Firmware as part of the communication method for passing data from Open Firmware to a client program (like to an operating system). An operating system used the Device Tree to discover the topology of the hardware at runtime, and thereby support a majority of available hardware without hard coded information (assuming drivers were available for all devices).”

However, after a while due to its flexibility and power, it found its place in the world of embedded systems.

Zephyr uses the device tree concept to manage peripherals in a board or MCU. Just consider a device description header file for a typical MCU (in this case STM32):

/* Internal SRAMs size */
#define SRAM1_SIZE               (0x30000UL)    /*!< SRAM1=192k */
#define SRAM2_SIZE               (0x10000UL)    /*!< SRAM2=64k  */
#define SRAM3_SIZE               (0x80000UL)    /*!< SRAM3=512k */
#define SRAM4_SIZE               (0x04000UL)    /*!< SRAM4=16k  */

/* External memories base addresses - Not aliased */
#define FMC_BASE                 (0x60000000UL) /*!< FMC base address                                   */
#define OCTOSPI2_BASE            (0x70000000UL) /*!< OCTOSPI2 memories accessible over AHB base address */
#define OCTOSPI1_BASE            (0x90000000UL) /*!< OCTOSPI1 memories accessible over AHB base address */

#define FMC_BANK1                FMC_BASE
#define FMC_BANK1_1              FMC_BANK1
#define FMC_BANK1_2              (FMC_BANK1 + 0x04000000UL)

/* Flash, Peripheral and internal SRAMs base addresses - Non secure */
#define FLASH_BASE_NS            (0x08000000UL) /*!< FLASH (up to 2 MB) non-secure base address         */
#define SRAM1_BASE_NS            (0x20000000UL) /*!< SRAM1 (192 KB) non-secure base address             */
#define SRAM2_BASE_NS            (0x20030000UL) /*!< SRAM2 (64 KB) non-secure base address              */
#define SRAM3_BASE_NS            (0x20040000UL) /*!< SRAM3 (512 KB) non-secure base address             */
#define SRAM4_BASE_NS            (0x28000000UL) /*!< SRAM4 (16 KB) non-secure base address              */
#define PERIPH_BASE_NS           (0x40000000UL) /*!< Peripheral non-secure base address                 */

This excerpt of header code from stm32u585xx.h looks familiar to most developers who have worked with MCUs.

Now, consider you are developing a code that the final hardware is not (fully) specified yet. For example, we are not sure on which MCU the application will be deployed. If we develop the code based on specific hardware, we are in trouble. So, what is the solution?

That is where Zephyr comes in. It uses a device tree and driver API to make an isolation layer between the application and the underlying hardware. In this case, rather than adapting our application for different hardware, we describe the hardware specs in a DT format file. We write only one version of the application using Zephyr’s generic headers. The magic happens with the build command: you specify which application to build, and to which target hardware.

Let’s consider a basic DT for the B-U585I board:

/dts-v1/;
#include "b_u585i_iot02a-common.dtsi"

/ {
    model = "STMicroelectronics B-U585I-IOT02A discovery kit";
    compatible = "st,b-u585i-iot02a";

    chosen {
        zephyr,console = &usart1;
        zephyr,shell-uart = &usart1;
        zephyr,sram = &sram0;
        zephyr,flash = &flash0;
        zephyr,code-partition = &slot0_partition;
    };

    aliases {
        led0 = &green_led_1;
        led1 = &red_led_1;
        sw0 = &user_button;
    };
};

&flash0 {
    partitions {
        compatible = "fixed-partitions";
        #address-cells = <1>;
        #size-cells = <1>;
        boot_partition: partition@0 {
            label = "mcuboot";
            reg = <0x00000000 DT_SIZE_K(64)>;
        };
        slot0_partition: partition@10000 {
            label = "image-0";
            reg = <0x00010000 DT_SIZE_K(416)>;
        };
.
.
.
    };
};

As we can see, rather than hardcoding the hardware specs into our application body, we put it in a human-readable format.

Now, just consider that we have designed a custom IoT board that has different types of sensors and actuators. We call it Mindboard. The Mindboard has the following specs:Mindboard overview

  • An I2C based Temperature sensor
  • An I2C EEPROM
  • A SPI WiFi module
  • Two LED indicators
  • Two relay actuators

Now, let’s write a simple device tree for the Mindboard. First, create a file with your board name and .overlay extension in the board directory of your project. In our case, it will be b_u585i_iot02a.overlay because the Mindboard was designed based on the B-U585I-IOT02 development kit. Ideally we should create a custom board rather than an overlay, but an overlay is a very quick way to get started.

Humidity sensor

Let’s start with the Humidity sensor. According to the circuit diagram, the HTS221 sensor is connected to I2C2. Furthermore, according to the datasheet, the sensor slave address is 0x5f. (Note that the address we need to use here doesn’t include the R/W bit.) So we added the sensor configuration file as follows:

/ {
    &i2c2 {
        hts221@5f {
            compatible = "st,hts221";
            reg = <0x5f>;
            status = "okay";
        };
    };
};

Flash memory

Mindboard - EEPROMNext, we will add the EEPROM flash memory. According to the schematic, the flash memory is also connected to the I2C. Furthermore, E0 is connected to ground while E1 and E2 are connected to VCC.

To calculate the device slave address, we can refer to the below table from the M24256 datasheet. Dropping the R/W bit, this gives us 0x56 as address.

EEPROM datasheet - I2C address

Please note that we need the compatible string field to point which device is going to be used with this driver. In this case, we are using the M24256-DF from STMicroelectronics, which is compatible with the AT24.

aliases {
eeprom-0 = &eeprom0;
};
    
&i2c2 {
    eeprom0:e2prom@56 {
            	compatible = "atmel,at24";
            	reg = <0x56>;
            	status = "okay";
            	size = <32768>;
            	pagesize = <64>;
            	address-width = <16>;
            	timeout = <5>;
    	};

};

WiFi module

Mindboard - WiFiNow, we will add the WiFi SPI module. By looking at the schematic, we see that the wireless  module is connected to SPI2.
In the below device tree, we have defined the pin controllers for the SPI2 and also the chip select pin. There is only a single slave device on this SPI bus so we can just use reg=<0> to identify the slave. If more SPI devices are connected to the bus, their corresponding PIN number should be added to cs-gpios of the bus, and the reg field of each module should be set accordingly.

&spi2 {
        pinctrl-0 = <&spi2_nss_pb12 &spi2_sck_pd1
                     &spi2_miso_pd3 &spi2_mosi_pd4>;
        cs-gpios = <&gpiof 12 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
        pinctrl-names = "default";
        status = "okay";

        wifi: wifi@0 {
             compatible = "mxchip,emw3080";
             label = "WIFI Module";
             reg = <0>;

             spi-max-frequency = <4000000>;
     };
};

LEDs

Mindboard - LEDsThe two LEDs can be simply enabled by gpios:

leds {
    compatible = "gpio-leds";
    green_led_1: led_1 {
        gpios = <&gpioh 7 GPIO_ACTIVE_LOW>;
        label = "User LD7";
    };
    red_led_1: led_3 {
        gpios = <&gpioh 6 GPIO_ACTIVE_LOW>;
        label = "User LD6";
    };
};

The pins PH6 and PH7 are already configured as GPIO by default, so no pinctrl setting is needed.

Relays

Assuming that the two Relays are connected to the GPIO pins PH10, PH11, we modify the device tree overlay as follows:

/ {
        relays {
                compatible = "gpio-relays";
                door_relay: rel_1 {
                        gpios = <&gpioh 10 GPIO_ACTIVE_LOW>;
                        label = "Entrance door relay";
                };
                light_relay: rel_2 {
                        gpios = <&gpioh 11 GPIO_ACTIVE_LOW>;
                        label = "Light relay";
                };
        };
};

Conclusion

With the device tree, it is easy to specify what the board looks like. The syntax takes a little getting used to, but there are plenty of examples that can be used as inspiration. Also, the device tree properties that can be set for each device are very well documented.

The actual code to use the device is isolated in a device driver, which is matched using the compatible string. Often a device driver already exists, and the Zephyr application only needs to use the generic API to access the device. In the next example, we will show an example of that using a light sensor.

Presentations

Drop the docs and embrace the model with Gaphor Fosdem '24 - Frank Van Bever 20 March, 2024 Read more
How to update your Yocto layer for embedded systems? ER '23 -Charles-Antoine Couret 28 September, 2023 Read more
Tracking vulnerabilities with Buildroot & Yocto EOSS23 conference - Arnout Vandecapelle 12 July, 2023 Read more
Lua for the lazy C developer Fosdem '23 - Frank Van Bever 5 February, 2023 Read more
Exploring a Swedish smart home hub Fosdem '23 - Hannah Kiekens 4 February, 2023 Read more
prplMesh An Open-source Implementation of the Wi-Fi Alliance® Multi-AP (Arnout Vandecappelle) 25 October, 2018 Read more

 

News