Devicetree is a hierarchical description of the hardware that Zephyr processes during compilation to generate the configuration used by drivers and the application.
If you come from Arduino, PIC, or directly programming microcontrollers in C, you might be used to this:
#define LED_PIN 13
#define BTN_PIN 5
int main(void) {
pinMode(LED_PIN, OUTPUT);
// ...
return 0;
}It seems harmless, right? Well, in Zephyr this is a prohibited practice. Or at least, it’s a practice that goes against the entire philosophy of the system.
In Zephyr, the C source code should not know which physical pin a component is connected to. You will never see a pin number “hardcoded” in a professional Zephyr application.
To achieve this total abstraction, Zephyr borrows a technology from the Linux Kernel: Devicetree.
What is Devicetree?
Devicetree (DTS) is a hierarchical data structure that describes the hardware of a board. It is a text file (with .dts or .dtsi extension) that lists all physical components: the CPU, memory, buses (I2C, SPI), GPIO controllers, and the peripherals connected to them (sensors, LEDs, buttons).
Think of Devicetree as the architectural blueprint of the board.
- The C code defines the logic (what to do).
- The Devicetree defines the context (where things are).
Thanks to Devicetree, you can compile the same C code for different boards without changing the .c file, as long as they all provide the aliases, peripherals, and capabilities that the application expects.
Why not use #define PIN 13?
Suppose you write a driver for a temperature sensor connected via I2C.
The old way:
You write I2C_SDA_PIN 21 in your code. If tomorrow you change the PCB design and the sensor moves to pin 15, you have to go into the C code, find that line, change it, and recompile. If you have 20 sensors, it’s a mess.
The Zephyr way:
Your C code says: “System, give me the pointer to the device named ‘sensor_temp’”.
The system consults Devicetree, finds sensor_temp on the I2C0 controller at address 0x40, and returns the configured device.
If you change the PCB, you only update the .dts file. The C code doesn’t even know. Total decoupling.
Anatomy of a .dts file
Devicetree language looks like a mix of JSON and C. It is organized into Nodes and Properties.
Let’s look at a simplified example of what an LED looks like in a Devicetree:
/ {
model = "My IoT Board";
compatible = "my-company,my-board";
/* Alias node for easier access */
aliases {
led0 = &my_led;
};
leds {
compatible = "gpio-leds";
/* LED definition */
my_led: led_0 {
gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
label = "Status Red LED";
};
};
};Let’s analyze this, as it contains the essence of Zephyr:
- Root (
/): Everything starts at the root. - Nodes (
leds,led_0): Represent devices or groupings. - Labels (
my_led:): These are internal DTS “variables”. They are used to reference this node from elsewhere. - Properties (
gpios = ...): Here, the actual connection is described.&gpio0: Reference to the port 0 controller.13: The pin number.GPIO_ACTIVE_LOW: A “flag” indicating the LED turns on with negative logic (0V).
Notice GPIO_ACTIVE_LOW. In Arduino, you would have to manage in your C code whether writing a HIGH or a LOW turns on the LED. In Zephyr, the driver reads this property and handles it by itself. Your code only says “Turn on”, and the driver knows if that means setting a 1 or a 0.
The compilation flow: generated macros
It is essential to understand that the .dts file is not read at runtime. The microcontroller does not parse text.
During compilation (when you run west build):
The preprocessor takes all .dts and .dtsi files (from the board, the processor, and yours) and merges them.
The Devicetree compiler (dtc) verifies everything is correct.
Generation script: Zephyr converts that tree into a giant C header file called devicetree_generated.h.
This generated file contains thousands of unintelligible macros that translate the tree’s information into C constants.
For example, our LED from before becomes something like this (simplified):
#define DT_N_S_leds_S_led_0_GPIO_PIN 13
#define DT_N_S_leds_S_led_0_GPIO_FLAGS (GPIO_ACTIVE_LOW)And that’s why in your C code you use macros like DT_ALIAS(led0) or GPIO_DT_SPEC_GET, which internally resolve these automatic definitions.
Nodes, properties, labels, and aliases
To work with Devicetree, you need to distinguish these four terms:
- Node: A
{ ... }block in the DTS file. Represents a device (e.g., a UART controller). - Property: Key-value pairs within the node (e.g.,
current-speed = <115200>;). - Label: The name followed by a colon (e.g.,
my_uart:) before the node. It’s a unique pointer within the DTS. - Alias: A “friendly” name (like
led0oruart-console) that maps to a complex node. We use them to avoid going crazy looking for nested nodes.
Devicetree allows a significant part of the hardware to be configuration rather than C logic. To add a sensor or change a property without modifying the original board definition, we will use overlays, which we will cover in the next article.