Device Tree — probably the part that can get the most complicated when you’re starting with Zephyr, and it’s something you need to dive into from the beginning, because working with microcontrollers means working with hardware and peripheral configuration. In this post, I’ll explain a bit about how the different Device Tree files that come with a Zephyr board are organized and its use.

I’m not going to talk about concepts — for that you already have the official pages: https://docs.zephyrproject.org/latest/build/dts/index.html

Instead, let’s get straight to the point. DeviceTree is used by Zephyr to describe the hardware and its configuration. For example, if we want to specify that a UART port will have a speed of 9600 baud and one stop bit, we write the following:

&usart2 {
    status = "okay";
    current-speed = <9600>;
    stop-bits = "1";
};

The options we see such as status, current-speed, and stop-bits are called properties, and their names, types, and optional values can be found in files called bindings. Here’s an example for the UART port:

...
  current-speed:
    description: |
      Initial baud rate setting for UART. Defaults to standard baudrate of 115200 if not specified.
    default: 115200

  stop-bits:
    description: |
      Sets the number of stop bits. Defaults to standard of 1 if not specified.
    default: "1"

Each manufacturer whose microcontroller family is supported by Zephyr includes, within the operating system code, .dtsi files that describe the peripherals present in the microcontroller (the SoC). You can find these files in the zephyr/dts directory. For example, the dtsi file for the STM32G0B1RE microcontroller is located in:

 ── zephyr
    └── dts
        └── arm
            ├── armv6-m.dtsi
            └── st
                └── g0
                    ├── stm32g0b1Xe.dtsi
                    ├── stm32g0b1.dtsi
                    ├── stm32g071.dtsi
                    ├── stm32g051.dtsi
                    ├── stm32g031.dtsi
                    ├── stm32g030.dtsi
                    └── stm32g0.dtsi

In the stm32g0.dtsi file we can find the description of the peripherals that come with this family, written as nodes with their respective properties. Below, we can see the node that describes the UART1 peripheral, and from its properties we can tell that it is disabled, and that its physical address is 0x40013800 + 0x400, among other details.

usart1: serial@40013800 {
	compatible = "st,stm32-usart", "st,stm32-uart";
	reg = <0x40013800 0x400>;
	clocks = <&rcc STM32_CLOCK(APB1_2, 14U)>;
	resets = <&rctl STM32_RESET(APB1H, 14U)>;
	interrupts = <27 0>;
	status = "disabled";
};

It is very common for manufacturers to reuse DeviceTrees from other sub-families and to create compositions through several files, since—just like in C—these files can be included.


In the case of ST, the complete DeviceTree for the STM32G0B1RE is composed of up to seven .dtsi files, plus the armv6-m.dtsi file, which contains the descriptions for the SysTick and NVIC peripherals. Down below you can see what the stm32g0b1Xe.dtsi include hierarchy looks like:

── stm32g0b1Xe.dtsi
    └── stm32g0b1.dtsi
        └── stm32g071.dtsi
            └── stm32g051.dtsi
                └── stm32g031.dtsi
                    └── stm32g030.dtsi
                        └── stm32g0.dtsi
                            └── armv6-m.dtsi

When you compile your code for a Zephyr project, you must do it using a board as the base, not a SoC. Zephyr already includes DeviceTrees that describe the peripherals present on each board, and some of them are already configured. You can find these in the zephyr/boards directory. Once again, here is the path for a Nucleo G0 board, and these are the files that include the microcontroller’s .dtsi files.

 ── zephyr
    └── boards
        └── st
            └── nucleo_g0b1re
                └── nucleo_g0b1re.dts

Here is an example of the description of the LED that comes on this board and is already defined in the nucleo_g0b1re.dts file. It is connected to port A, pin 5.

leds: leds {
	compatible = "gpio-leds";
	green_led_1: led_4 {
    	gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>;
    	label = "User LD4";
	};
};

A third layer of the DeviceTree is used in our application, which is called an overlay. Here we can modify configurations that already exist in the DTS and DTSI files, as well as add new descriptions and settings. By default, Zephyr looks for these files in the following locations with the following names (where BOARD is the board name as defined in Zephyr). Well, there are other locations too, but you can check them here:

── application
    ├── boards
        └── <BOARD>.overlay
    ├── <BOARD>.overlay
    └── app.overlay

You can also specify its location using the DTC_OVERLAY_FILE variable, either in your CMakeLists.txt file or on the command line when building.

west build -b nucleo_g0b1re -- -DDTC_OVERLAY_FILE=enable-ser.overlay

Each time you run west build, a file called build/zephyr/zephyr.dts will be created in your build folder. This file contains the final DeviceTree, which combines the three levels of files mentioned earlier. This file is the one that the Device Tree Compiler uses to generate a header file called build/zephyr/include/generated/devicetree_generated.h.

Config the serial port

Let’s look at an example of how to use these three levels of DeviceTree files to see how to configure the serial port. If we open the file corresponding to the microcontroller, we can find the node serial@40004400, which describes Serial Port 1, and it is assigned the label uart2.

stm32g0b1.dtsi

usart2: serial@40004400 {
	compatible = "st,stm32-usart", "st,stm32-uart";
	reg = <0x40004400 0x400>;
	clocks = <&rcc STM32_CLOCK(APB1, 17U)>;
	resets = <&rctl STM32_RESET(APB1L, 17U)>;
	interrupts = <28 0>;
	status = "disabled";
};

In the file corresponding to the board, we can see that a reference is made to that node using its label, and other properties that were not in the previous file—such as the baud rate and the assigned pins—are modified and configured.

nucleo_g0b1re.dts

&usart2 {
	pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>;
	pinctrl-names = "default";
	current-speed = <115200>;
	status = "okay";
};

In the overlay corresponding to our application, we modify the baud rate and configure new options such as stop bits and data bits. We only need to reference the node label and set the new values for the properties we want to modify.

app.overlay

&usart2 {
    current-speed = <9600>;
    stop-bits = "1";
    data-bits = <8>;
};

When we build, in the generated file we can see the node with the properties used across all three levels, and in the comments it shows which file each property was configured in.

/* node '/soc/serial@40004400' defined in deps/zephyr/dts/arm/st/g0/stm32g0.dtsi:245 */
usart2: serial@40004400 {
	compatible = "st,stm32-usart",
	             "st,stm32-uart";                  /* in deps/zephyr/dts/arm/st/g0/stm32g0.dtsi:246 */
	reg = < 0x40004400 0x400 >;                    /* in deps/zephyr/dts/arm/st/g0/stm32g0.dtsi:247 */
	clocks = < &rcc 0x3c 0x20000 >;                /* in deps/zephyr/dts/arm/st/g0/stm32g0.dtsi:248 */
	resets = < &rctl 0x591 >;                      /* in deps/zephyr/dts/arm/st/g0/stm32g0.dtsi:249 */
	interrupts = < 0x1c 0x0 >;                     /* in deps/zephyr/dts/arm/st/g0/stm32g0.dtsi:250 */
	pinctrl-0 = < &usart2_tx_pa2 &usart2_rx_pa3 >; /* in deps/zephyr/boards/st/nucleo_g0b1re/nucleo_g0b1re.dts:101 */
	pinctrl-names = "default";                     /* in deps/zephyr/boards/st/nucleo_g0b1re/nucleo_g0b1re.dts:102 */
	status = "okay";                               /* in deps/zephyr/boards/st/nucleo_g0b1re/nucleo_g0b1re.dts:104 */
	current-speed = < 0x2580 >;                    /* in app/app.overlay:8 */
	stop-bits = "1";                               /* in app/app.overlay:9 */
	data-bits = < 0x8 >;                           /* in app/app.overlay:10 */
};

We can look at another file generated when we build, called devicetree_generated.h. Here, the properties are converted into macros, which will later be used by Zephyr in their respective drivers.

#define DT_N_S_soc_S_serial_40004400_P_current_speed 9600
...
#define DT_N_S_soc_S_serial_40004400_P_stop_bits "1"
...
#define DT_N_S_soc_S_serial_40004400_P_data_bits 8  

We’ll stop here for now, since the purpose of this post is only to explain the files in which the nodes that make up the DeviceTree of your application are defined. In future posts, we will go deeper into other types of details. By the way, if you want to remove nodes that are already defined without modifying the DTS/DTSI files, you can do so in your overlay as follows:

/ {
    aliases {
        /delete-property/ led0;
        /delete-property/ sw0;
    };

    /delete-node/ leds;
    /delete-node/ buttons;    
};

Here’s the structure of the DeviceTree files if, like me, you are also using the Nordic nRF54L15DK board.

└── zephyr
    ├── boards
        └── nordic
            └── nrf54l15dk
                ├── nrf54l15dk_nrf54l15_cpuapp.dts
                ├── nrf54l_05_10_15_cpuapp_common.dtsi
                ├── nrf54l15dk_common.dtsi
                └── nrf54l15dk_nrf54l_05_10_15-pinctrl.dtsi
    └── dts
        └── arm
            ├── armv6-m.dtsi
            └── nordic
                ├── nrf541l5_cpuapp.dtsi
                └── nrf54l_05_10_15_cpuapp.dtsi
        └── vendor
            └── nordic
                ├── nrf54l15.dtsi               //ram controllers
                ├── nrf54l_05_10_15.dtsi        //peripherals
                └── nrf_common.dtsi

Well, I hope this simple post helps you better understand the structure of the DeviceTree files, which can be difficult to grasp when you’re starting out with Zephyr. However, you’ll soon find that they are very easy to use. In another post, we will explain a bit about the binding files.