Skip to main content

Devicetree Overlays Explained

With a real ESP32 board as the example.

Zephyr uses devicetree (DTS) to describe hardware to the kernel — which pins are I2C, which address a sensor lives at, what the pin configuration is. If you've never seen it before, it looks like structured config but behaves more like a type system: the compatible property binds a DTS node to a specific C driver.

This is the mental model that took me the longest to build. Here it is, compressed.


The three-layer model

There are three DTS inputs that Zephyr merges at build time:

SoC DTS Board DTS Your overlay
(esp32s3.dtsi) + (esp32s3_devkitc.dts) + (boards/esp32s3_devkitc_esp32s3_procpu.overlay)
| | |
+---------------------+--------------------------+
|
Final device tree
  • SoC DTS: defines all peripherals on the chip (i2c0, spi1, uart0, etc.) with vendor defaults. You never edit this.
  • Board DTS: enables the peripherals the board uses and maps them to physical pins. The DK files live in boards/espressif/esp32s3_devkitc/ in the Zephyr repo.
  • Your overlay: overrides and additions specific to your application. This is what you write.

A minimal I2C sensor overlay

boards/esp32s3_devkitc_esp32s3_procpu.overlay
&i2c0 {
status = "okay";
clock-frequency = <I2C_BITRATE_STANDARD>;

sht31: sht3xd@44 {
compatible = "sensirion,sht3xd";
reg = <0x44>;
};
};

Breaking it down:

&i2c0 — the & means "reference an existing node" (defined in the board DTS). You're not creating a new i2c0, you're modifying the one the board defines.

status = "okay" — Zephyr's convention for "this peripheral is in use." Nodes default to disabled. If you forget this, your driver won't initialize and you'll see i2c0: could not get binding.

clock-frequency = <I2C_BITRATE_STANDARD> — expands to 100000 (100 kHz). The macro is defined in zephyr/drivers/i2c.h. Use I2C_BITRATE_FAST for 400 kHz.

sht3xd@44 — the node name. The @44 is the unit address, which must match reg. It's hex, so @44 = 0x44.

compatible = "sensirion,sht3xd" — the binding string. Zephyr looks up dts/bindings/sensor/sensirion,sht3xd.yaml to validate this node and generate the matching device struct. This is how the DTS node gets connected to the C driver.

reg = <0x44> — the I2C address. For 7-bit addresses, write the 7-bit value (not the 8-bit read/write shifted form). The driver handles the shift.


Getting the device in C

After the DTS is set up, retrieving the device in your application:

/* Option 1: by node label (preferred) */
static const struct device *sht31 = DEVICE_DT_GET(DT_NODELABEL(sht31));

/* Option 2: by compatible string (if there's only one) */
static const struct device *sht31 = DEVICE_DT_GET_ONE(sensirion_sht3xd);

/* Always check it initialized */
if (!device_is_ready(sht31)) {
LOG_ERR("SHT31 not ready — check overlay and CONFIG_SHT3XD=y");
return -ENODEV;
}

The DEVICE_DT_GET macro resolves at compile time — if the node doesn't exist in the DTS, you get a build error, not a runtime NULL. This is one of Zephyr's nicest properties.


Common mistakes

1. Forgetting status = "okay"

The peripheral exists in the board DTS but is disabled. Your overlay enables it by adding status = "okay". Without it: device never initializes, device_is_ready() returns false.

2. Mismatched compatible and driver Kconfig

compatible = "sensirion,sht3xd" requires CONFIG_SHT3XD=y in prj.conf. If the config is missing, the driver isn't compiled and the device won't bind. The error looks like: sht3xd: binding not found.

3. Node address doesn't match reg

sht3xd@44 with reg = <0x45> is a DTS validation error. They must match. The @ address is decorative (Zephyr uses reg for actual addressing) but they must be identical.

4. Using 8-bit I2C address instead of 7-bit

The SHT31's datasheet says "I2C address 0x88 (write) / 0x89 (read)." In DTS, use the 7-bit form: 0x44. Zephyr's I2C drivers shift the address themselves.


Checking what the build sees

west build -b esp32s3_devkitc/esp32s3/procpu -- -DOVERLAY_CONFIG=overlay.conf
cat build/zephyr/zephyr.dts # merged final DTS

The zephyr.dts file in the build directory is the fully resolved device tree. Grep for your node name to confirm it's present and has the right properties:

grep -A10 "sht3xd" build/zephyr/zephyr.dts