When you start working with nRF Connect SDK, one of the first hurdles you’ll encounter is something called the Device Tree (DT). It might look intimidating at first glance — with its unfamiliar .dts
, .dtsi
, and .overlay
files — but it’s one of the most powerful tools in the SDK. Once you understand it, you’ll unlock a level of portability and flexibility in your embedded applications that would otherwise require endless conditional code and pin definitions.
In this article, we’ll explore:
- What Device Tree is and why it’s important
- The structure of DT: hierarchy, nodes, properties, and overlays
- Practical examples of working with onboard LEDs and buttons
- Real-world use cases like enabling UART or configuring I2C sensors
By the end, you’ll not only understand how to read a Device Tree file but also how to use it effectively in your nRF Connect SDK projects.
What is Device Tree (DT) and Why is it Important?
At its core, a Device Tree is a structured data format that describes the hardware configuration of a board. It answers questions like:
- Which peripherals exist on this chip or board?
- What pins are LEDs, buttons, or UART TX/RX connected to?
- Which I2C bus is an external sensor attached to?
- Which devices should be enabled, and which should remain disabled?
Instead of embedding all this information directly in your C code, the Device Tree places it in a hardware description file that is processed during the build. This separation brings several benefits:
1. Hardware Abstraction and Portability
Imagine you wrote a blinking LED application for the nRF52840 DK. The LED might be on P0.13
. But if you switch to a custom board where the LED is wired to P0.25
, your code shouldn’t need to change. With DT, you only update the board configuration (via an overlay) — your application continues to reference led0
in the same way.
2. Cleaner Application Code
Without DT, application code would be littered with hardcoded pin numbers, addresses, and magic constants. DT ensures those details live in a centralized place, and your code simply queries “give me the GPIO for led0.”
3. Shared Configuration for SDK and Drivers
Drivers in Zephyr (the RTOS used in nRF Connect SDK) rely heavily on DT. When you enable a UART in an overlay, you’re not just telling your app about it — you’re informing the driver subsystem as well. This ensures consistency and reduces duplication.
In short, DT makes your applications portable, maintainable, and less error-prone.
Device Tree Structure: Hierarchy, Nodes, and Properties
The Device Tree format looks unusual if you’ve never seen it before, but once you learn its building blocks it becomes straightforward.
Hierarchy
DT is hierarchical, similar to a filesystem. At the root level (/
) you have top-level definitions, and inside are nodes representing peripherals.
Example:
/ {
model = "Nordic nRF52840 DK";
compatible = "nordic,nrf52840-dk-nrf52840";
leds {
compatible = "gpio-leds";
led0: led_0 {
gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
label = "Green LED 0";
};
};
};
Here, /
is the root node, and inside it is a child node leds
which contains another node led_0
.
Nodes
A node represents a device or peripheral. Each node has a name and can have an optional label for referencing in code.
Example:
led0: led_0 {
gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
label = "Green LED 0";
};
led0
→ label (used in code via macros likeDT_ALIAS(led0)
)led_0
→ node name- The node contains properties like
gpios
andlabel
Properties
Each node is made up of properties, which describe configuration. Properties can be:
- Strings →
label = "Green LED 0";
- Integers →
current-speed = <115200>;
- Boolean flags →
status = "okay";
- Phandles (references to other nodes) →
gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
Properties often align with what the driver needs to know. For instance, a UART driver needs to know baud rate, pin configuration, and whether the device is enabled.
Aliases
Aliases provide shortcuts to commonly used devices. For example, instead of referencing /leds/led_0
, you can simply use DT_ALIAS(led0)
in your code. These aliases are defined in the board DTS or can be overridden in an overlay.
Overlays
Overlays are how you customize a board’s DT configuration for your application. Each board comes with a default .dts
file describing its hardware. Instead of editing this directly, you create an .overlay
file in your app directory.
Overlays can:
- Enable or disable devices
- Change pin assignments
- Add new devices (like sensors)
Example overlay (boards/nrf52840dk_nrf52840.overlay
):
&uart1 {
status = "okay";
current-speed = <115200>;
pinctrl-0 = <&uart1_default>;
pinctrl-names = "default";
};
This activates UART1 and sets its configuration.
Example 1: Using DT to Blink an LED
Let’s walk through a simple LED blink using DT.
Device Tree Snippet for LED
From the nRF52840 DK DT file:
leds {
compatible = "gpio-leds";
led0: led_0 {
gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
label = "Green LED 0";
};
};
Application Code
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/kernel.h>
#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
void main(void)
{
if (!device_is_ready(led.port)) {
return;
}
gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
while (1) {
gpio_pin_toggle_dt(&led);
k_sleep(K_MSEC(500));
}
}
Key points:
DT_ALIAS(led0)
finds the node labeledled0
.GPIO_DT_SPEC_GET()
extracts port, pin, and flags into a struct.- The rest is standard GPIO handling.
Example 2: Reading Button Input with DT
Buttons are defined similarly in DT:
buttons {
compatible = "gpio-keys";
button0: button_0 {
gpios = <&gpio0 11 GPIO_ACTIVE_LOW>;
label = "Button 0";
};
};
Application code:
#define SW0_NODE DT_ALIAS(sw0)
static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(SW0_NODE, gpios);
void main(void)
{
if (!device_is_ready(button.port)) {
return;
}
gpio_pin_configure_dt(&button, GPIO_INPUT);
while (1) {
int val = gpio_pin_get_dt(&button);
if (val == 0) {
printk("Button pressed!\n");
}
k_sleep(K_MSEC(100));
}
}
Here, the application doesn’t care which pin the button is connected to — DT handles that.
Example 3: Adding a Custom I2C Sensor
Let’s say you’re adding a temperature sensor over I2C to your custom board. Instead of hardcoding the bus and address, you extend the DT with an overlay:
&i2c1 {
status = "okay";
temp_sensor: lm75@48 {
compatible = "national,lm75";
reg = <0x48>;
};
};
Your application can now reference temp_sensor
using DT_NODELABEL(temp_sensor)
and work with the driver directly. If you move the sensor to another bus, you only update the overlay.
Tips for Working with Device Tree
- Use
west build -t devicetree
to generate and inspect the processed Device Tree (build/zephyr/zephyr.dts
). This helps confirm your overlays are applied correctly. - Leverage
DT_NODELABEL()
,DT_ALIAS()
, andDT_PATH()
macros depending on how you want to reference a node. - Remember
status = "disabled";
can turn off unused peripherals, saving power and build size. - Start simple: Experiment with LEDs and buttons before jumping into complex buses and external devices.
Wrapping Up
The Device Tree in nRF Connect SDK is more than just a configuration file — it’s the backbone of how your application, drivers, and hardware talk to each other.
By understanding DT hierarchy, nodes, properties, and overlays, you can:
- Write applications that are portable across boards
- Cleanly separate hardware definitions from application logic
- Enable and configure peripherals without modifying your C code
- Scale your projects with less maintenance overhead
Whether you’re blinking an LED, reading a button, or bringing up a custom I2C sensor, Device Tree will be central to your workflow in nRF Connect SDK. Mastering it early will pay dividends in every embedded project you build.