Maker.io main logo

Introduction to Zephyr Part 5: Devicetree Bindings

118

2025-04-03 | By ShawnHymel

Microcontrollers ESP32

In previous installments, we’ve looked at how Zephyr uses Devicetree to describe hardware, and how you can navigate Devicetree syntax and structure. We also touched briefly on “bindings.” Bindings connect the abstract hardware descriptions in the Devicetree source (DTS) files to the driver code that makes devices actually work.

In this tutorial, we’ll look at Devicetree bindings in the context of enabling and reading from an Analog-to-Digital Converter (ADC) on the ESP32-S3 platform. By walking through this example, you’ll gain a practical understanding of how Zephyr’s bindings files map Devicetree properties to driver code. You’ll also learn how to customize your DTS overlays and reference binding-defined properties in your C application.

All example code and solutions for this series can be found in this repository: https://github.com/ShawnHymel/introduction-to-zephyr/

What Are Devicetree Bindings?

Devicetree files (.dts or .overlay) describe hardware in a structured way. However, the system still needs to know how to interpret these descriptions.

Bindings are the link between this abstract hardware description and the low-level driver code. They are typically defined in YAML files placed in the zephyr/dts/bindings directory. Each binding file corresponds to one or more compatible strings. A compatible string might look like "espressif,esp32-adc". Inside the corresponding binding file, you’ll find documentation on what properties that node can have, their types, default values, and what’s required vs. optional.

When you run west build, Zephyr’s build system uses these binding files to:

  • Validate your DTS and overlay files.
  • Generate C header files that map Devicetree nodes, properties, and configurations into macros and structs that driver code and your application can reference.

As a result, changing something in your DTS configuration automatically propagates to your code through these auto-generated headers and macros. This keeps your code clean, maintainable, and easy to adapt when you switch boards or peripherals.

Note that bindings files are unique to Zephyr—Linux does not use them to map Devicetree nodes to driver code. Zephyr reads them at compile time to enforce node properties in the Devicetree (interface) and to determine which driver files to use.

I recommend this tutorial if you need a good starter guide and reference for YAML syntax.

Examining the ADC Bindings for ESP32-S3

Let’s look at the ESP32-S3 ADC as our example. On the ESP32-S3, the ADCs are documented in Zephyr’s Devicetree files under something like &adc0 or &adc1. The compatible string for the ESP32 ADC controller might be something like "espressif,esp32-adc".

Finding the Binding Files

You’ll find the ADC binding files in zephyr/dts/bindings/adc/. There should be a generic ADC binding (such as adc-controller.yaml) and a SoC-specific binding (espressif,esp32-adc.yaml), among others.

  • adc-controller.yaml: Defines the standard properties for an ADC controller in Zephyr. Includes properties like #io-channel-cells and standard properties for defining channels. This file creates a common interface for ADC configurations.
  • espressif,esp32-adc.yaml: Extends the generic ADC binding with properties specific to the ESP32 series.

These YAML files use a schema that lists required and optional properties, their types, and how the driver will interpret them.

For example, espressif,esp32-adc.yaml might include:

  • A compatible: "espressif,esp32-adc" line, ensuring that any node with this compatible string uses this binding. Note that this string is what Zephyr looks for when matching the compatible string in the Devicetree, not the filename! The filename matching is by convention but not enforced.
  • Properties like unit and channel-count that are specific to the ESP32 ADC design.
  • Inclusion of adc-controller.yaml to inherit common ADC properties and constraints.

Important Properties

Some key properties defined by these bindings include:

  • reg: Indicates the memory address or bus address of the ADC peripheral.
  • status: Should be "okay" to enable the ADC, or "disabled" otherwise. Note that Linux has other status values, but Zephyr sticks to these two (for now).
  • zephyr,gain, zephyr,reference, zephyr,vref-mv, and zephyr,resolution: Define how the ADC should interpret input signals and what reference voltage or resolution to use. These are per-channel properties when you create channel sub-nodes.

By reading these binding files, you learn how to properly configure each node and what the driver expects. For instance, zephyr,gain and zephyr,reference might be enums defined in the binding file, ensuring that your DTS picks from a valid set of options (like ADC_GAIN_1_4 or ADC_REF_INTERNAL).

As previously mentioned, these properties create an interface you must adhere to when defining a Devicetree node that uses this particular compatible. Zephyr’s build system will throw an error if you forget to add a required property defined in the bindings file or use the wrong type of property.

Hardware Connections

For this demonstration, we will be using a 10k potentiometer connected to pin 1 on the ESP32-S3-DevKitC. Here is a Fritzing diagram showing all of the connections we will use throughout this series:

Introduction to Zephyr Part 5: Devicetree Bindings

Creating a Devicetree Overlay for the ADC

The ESP32S3 has two analog-to-digital (ADC) controllers: ADC1 and ADC2. You can read more about these controllers in section 39.3 of the ESP32S3 technical reference manual. Zephyr, by convention, uses zero-based indexing for nodes, so ADC1 is adc0 in the Devicetree and ADC2 is adc1.

We’re going to enable the ADC1 (&adc0) and configure one of its channels. Let’s say we want to read from GPIO1, which maps to ADC1 channel 0 on the ESP32-S3.

Project Setup

Create a new project directory structure:

Copy Code
/workspace/apps/05_demo_adc/
├─ boards/
│  └─ esp32s3_devkitc.overlay
├─ src/
│  └─ main.c
├─ CMakeLists.txt
└─ prj.conf
 

CMakeLists.txt:


Copy Code
cmake_minimum_required(VERSION 3.20.0)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(adc_demo)

target_sources(app PRIVATE src/main.c)

This is a standard (minimum) boilerplate setup for a Zephyr application.

prj.conf

This tells Zephyr to include ADC support. Without it, the ADC driver wouldn’t be compiled in, and you’d see errors.

boards/esp32s3_devkitc.overlay

Copy Code
/{
    aliases {
        my-adc = &adc0;
        my-adc-channel = &adc0_ch0;
    };
};

// ADC1 (adc0) channel 0 is connected to GPIO1
&adc0 {
    status = "okay";
    #address-cells = <1>;
    #size-cells = <0>;

    adc0_ch0: channel@0 {
        reg = <0>;
        zephyr,gain = "ADC_GAIN_1_4";
        zephyr,reference = "ADC_REF_INTERNAL";
        zephyr,vref-mv = <3894>;
        zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
        zephyr,resolution = <12>;
    };
};
 

Let’s break this down:

  • We add an alias my-adc that points to &adc0, and my-adc-channel that points to adc0_ch0. Aliases allow our application code to easily reference these nodes without remembering the full path.
  • We ensure status = "okay" to enable the ADC hardware.
  • We set #address-cells = <1>; and #size-cells = <0>; to match what the ADC binding expects for channel sub-nodes.
  • We create a sub-node channel@0 (with reg = <0>) representing channel 0 of this ADC. The zephyr,gain, zephyr,reference, and zephyr,vref-mv properties match what we learned from the binding files. They describe how the ADC driver should configure the channel:
    • ADC_GAIN_1_4 might mean we use an attenuation that allows for a larger input voltage range.
    • ADC_REF_INTERNAL means we’re using the internal reference voltage.
    • zephyr,vref-mv = <3894> sets the effective reference voltage in millivolts, factoring in attenuation. In this example, we’ve chosen ~3.9 V to match the internal configuration of ESP32’s ADC with an 11 dB attenuation (about gain of 1/4 or more precisely 1/3.54).
    • zephyr,resolution = <12> sets a 12-bit resolution, meaning the ADC returns values between 0 and 4095.

If something is off, the binding files and Zephyr’s build system will complain during compilation. This ensures you use valid properties and configurations.

src/main.c

Copy Code
#include <stdio.h>‎#include <zephyr/kernel.h>‎#include <zephyr/drivers/adc.h>‎// Settings‎
static const int32_t sleep_time_ms = 100;‎

‎// Get Devicetree configurations‎#define MY_ADC_CH DT_ALIAS(my_adc_channel)‎
static const struct device *adc = DEVICE_DT_GET(DT_ALIAS(my_adc));‎
static const struct adc_channel_cfg adc_ch = ADC_CHANNEL_CFG_DT(MY_ADC_CH);‎

int main(void)‎
‎{‎
‎    int ret;‎
‎    uint16_t buf;‎
‎    uint16_t val_mv;‎
‎    int32_t vref_mv;‎

‎    // Get Vref (mV) from Devicetree property‎
‎    vref_mv = DT_PROP(MY_ADC_CH, zephyr_vref_mv);‎

‎    // Buffer and options for ADC (defined in adc.h)‎struct adc_sequence seq = {‎
‎        .channels = BIT(adc_ch.channel_id),‎
‎        .buffer = &buf,‎
‎        .buffer_size = sizeof(buf),‎
‎        .resolution = DT_PROP(MY_ADC_CH, zephyr_resolution)‎
‎    };‎

‎    // Make sure that the ADC was initialized‎if (!device_is_ready(adc)) {‎
‎        printk("ADC peripheral is not ready\r\n");‎
‎        return 0;‎
‎    }‎

‎    // Configure ADC channel‎
‎    ret = adc_channel_setup(adc, &adc_ch);‎
‎    if (ret < 0) {‎
‎        printk("Could not set up ADC\r\n");‎
‎        return 0;‎
‎    }‎

‎    // Do forever‎while (1) {‎

‎        // Sample ADC‎
‎        ret = adc_read(adc, &seq);‎
‎        if (ret < 0) {‎
‎            printk("Could not read ADC: %d\r\n", ret);‎
‎            continue;‎
‎        }‎

‎        // Calculate ADC value (mV)‎
‎        val_mv = buf * vref_mv / (1 << seq.resolution);‎

‎        // Print ADC value‎
‎        printk("Raw: %u, mV: %u\r\n", buf, val_mv);‎

‎        // Sleep‎
‎        k_msleep(sleep_time_ms);‎
‎    }‎
‎}‎
 

Let’s examine the key concepts in the code:

  • DEVICE_DT_GET(DT_ALIAS(my_adc)) uses the alias my-adc defined in the overlay. This returns a device struct representing our ADC hardware.
  • ADC_CHANNEL_CFG_DT(MY_ADC_CH) macro uses the properties defined in the my-adc-channel node (like gain, reference, resolution) to create an adc_channel_cfg structure automatically.
  • DT_PROP(MY_ADC_CH, zephyr_vref_mv) fetches the zephyr,vref-mv property right from Devicetree, giving the code a clean and dynamic way to access hardware configuration values.

No hardcoded addresses, no platform-specific #defines. If we change the attenuation or the reference voltage in the DTS overlay later, the code adapts automatically after rebuilding.

Building and Running the Example

To build and run:

Copy Code
west build -p always -b esp32s3_devkitc/esp32s3/procpu -- -DDTC_OVERLAY_FILE=boards/esp32s3_devkitc.overlay
 

This command:

  • Selects the esp32s3_devkitc/esp32s3/procpu board variant.
  • Forces a pristine build (-p always) to ensure we pick up changes in the overlay.
  • Specifies our custom overlay file.

Once built, flash the firmware onto your ESP32-S3 board. Note that if you are using the Docker image to build your Zephyr applications, this next step must be performed on your host machine (not in the Docker container). Change <PORT> to the port connected to your ESP32-S3-DevKitC.

Copy Code
python -m esptool --port "<PORT>" --chip auto --baud 921600 --before default_reset --after ‎hard_reset write_flash -u --flash_size detect 0x0 ‎workspace/apps/05_adc_demo/build/zephyr/zephyr.bin
 

Then connect to the serial monitor:

Copy Code
python -m serial.tools.miniterm "<PORT>" 115200‎
 

You should see raw ADC values and calculated voltage appearing. Try turning the potentiometer knob to watch the values change.

Introduction to Zephyr Part 5: Devicetree Bindings

Note that the ESP32’s ADC is notoriously inaccurate. You can read more about those inaccuracies (and how to programmatically offset those inaccuracies) here: https://www.luisllamas.es/en/esp32-adc/

How Bindings Enable This Magic

All of this dynamic configuration works because of the binding files. The espressif,esp32-adc.yaml and generic adc-controller.yaml files told Zephyr’s build system how to interpret the zephyr,gain, zephyr,vref-mv, and other properties. It also enforced that reg = <0> for our channel node is valid, and guided the macros used by DT_ALIAS() and DT_PROP().

Without bindings, you’d have to hardcode many platform-specific details in your code. You’d lose the flexibility and consistency that Devicetree brings. Bindings are like a contract between your DTS and the driver code: they define what’s allowed what each property means, and how to translate those properties into driver configuration. In other words, they define Devicetree node interfaces.

Generalizing Beyond ADC

The approach we used here isn’t unique to ADCs. Any peripheral—UART, I2C, SPI, PWM—follows a similar pattern:

  1. Find the compatible string for the device (e.g. "espressif,esp32-uart").
  2. Locate the corresponding binding file in zephyr/dts/bindings.
  3. Review required and optional properties in the binding file to understand what you can set in your DTS or overlay.
  4. Write or update your DTS/overlay to include these properties.
  5. Use Devicetree macros (like DT_PROP(), DT_NODELABEL(), DT_ALIAS()) in your C code to access these properties.
  6. Configure and use the driver code as per Zephyr’s APIs, confident that your properties match what the driver expects.

As you become more familiar with the process, you’ll rely less on guesswork. The binding files become a source of truth, guiding you in how to set up each device. Instead of searching datasheets and hoping your code aligns with the driver’s assumptions, you let Devicetree and bindings ensure correctness.

Be aware that this assumes your driver code has been provided for you! In the next episode, we will examine creating our own device driver.

Challenge: Combining ADC With Other Peripherals

For a more advanced project, try controlling PWM brightness of an LED based on the ADC input (like a trimpot). Use the same techniques:

  • Find the PWM binding file (e.g. espressif,esp32-ledc.yaml for the ESP32 LEDC peripheral).
  • Configure a PWM node in the DTS overlay.
  • Use the ADC reading from this tutorial as input to adjust the PWM duty cycle in your code.

When done, you should be able to adjust the potentiometer knob to control the LED brightness:

%%%cose-up of knob controlling LED

This challenge will reinforce how bindings unify the entire hardware configuration and help you build a highly configurable embedded application without scattering board-specific details throughout your code.

Going Further

In this post, we saw how bindings files defined Devicetree node interfaces and connected those nodes to the appropriate device driver code. If you’d like to dive more into the Devicetree, bindings, and the ESP32S3 ADC, I recommend the following resources:

In the next tutorial, we will write our own device driver so you can see how Kconfig, the Devicetree, and bindings files all work together to create portable application and driver code.

制造商零件编号 ESP32-S3-DEVKITC-1-N32R8V
ESP32-S3-WROOM-2-N32R8V DEV BRD
Espressif Systems
制造商零件编号 3533
GRAPHIC DISPLAY TFT RGB 0.96"
Adafruit Industries LLC
制造商零件编号 1782
MCP9808 TEMP I2C BREAKOUT BRD
Adafruit Industries LLC
制造商零件编号 3386P-1-103TLF
TRIMMER 10K OHM 0.5W PC PIN TOP
Bourns Inc.
制造商零件编号 1825910-6
SWITCH TACTILE SPST-NO 0.05A 24V
TE Connectivity ALCOSWITCH Switches
制造商零件编号 CF14JT220R
RES 220 OHM 5% 1/4W AXIAL
Stackpole Electronics Inc
制造商零件编号 LTL-4224
LED RED CLEAR T-1 3/4 T/H
Lite-On Inc.
制造商零件编号 PRT-14284
JUMPER M/M 4" 26AWG 30PCS
SparkFun Electronics
制造商零件编号 FIT0096
BREADBRD TERM STRIP 3.20X2.00"
DFRobot
制造商零件编号 DH-20M50055
USB AM TO USB MICRO, USB 2.0 - 1
Cvilux USA
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.