Let’s take a deep dive into the internals of Zephyr drivers. I’m assuming you already have some experience using the drivers that come preconfigured with your board. We’ll build something simple, step by step — starting with DeviceTree bindings and gradually moving forward until we create our first out-of-tree custom driver.

Let’s implement a very simple driver to control LEDs connected to GPIO pins. Yes, Zephyr already provides the gpio-leds driver, ready to use. But for learning purposes, let’s pretend it doesn’t exist. Below is a simple piece of code that configures a pin as an output and toggles an LED every second.

#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>

/*get the gpios property from node myLed*/
const struct gpio_dt_spec my_led = GPIO_DT_SPEC_GET(DT_NODELABEL(myled), gpios);

int main(void)
{
    gpio_pin_configure_dt(&my_led, GPIO_OUTPUT_INACTIVE);
    while(true)
    {
        gpio_pin_toggle_dt(&my_led);
        k_msleep(1000);
    }
        
	return 0;
}

In our overlay file, we declare a node and specify that the pin we want to use is located on port C, pin 0. If you try to build this project using west, it will fail. The error occurs when attempting to obtain the gpio_dt_spec structure.

/ {
    myLed: myLed{
        gpios = <&gpioc 0 GPIO_ACTIVE_HIGH>;
    };
};

Our DeviceTree node myLed must include a compatible property. In simple terms, we need to bind our node to a DeviceTree binding — an additional description layer that tells Zephyr how this node should be interpreted. Right now, we don’t have such a binding defined. That’s why the build fails.

To fix this, we need to create a new binding file. Let’s create a file called simple-leds.yaml inside a new directory called dts/bindings/ with he following content

# binding description
description: Led conencted to a pin for multipurpose

# binding name (this is the one will be used in the device tree file)
compatible: "simple,leds"

# properties our node will have, in our case there is just one called gpios
# which will be mandatory and their values are in a multi type array
properties:
  gpios:
    type: phandle-array
    required: true
    description: The GPIO pin connected to a simple led.

Now, in the overlay file, add the compatible property to the DeviceTree node using the binding we created. This connects the node to our custom definition. Build the project again to confirm the previous error is gone and the gpio_dt_spec is generated correctly.

/ {
    myLed: myLed{
        compatible = "simple,leds";
        gpios = <&gpioc 0 GPIO_ACTIVE_HIGH>;
    };
};

If we want to declare more nodes of the same type, we can do so. We simply need to repeat the same structure for each additional instance.

/ {
    myLed0: myLed0{
        compatible = "simple,leds";
        gpios = <&gpioc 0 GPIO_ACTIVE_HIGH>;
    };

    myLed1: myLed1{
        compatible = "simple,leds";
        gpios = <&gpioc 1 GPIO_ACTIVE_HIGH>;
    };
};

And your application code would look like the example below. Up to here looks very familiar with what we do when using the gpio-leds driver

#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>

/*get the gpios property from node myLed*/
const struct gpio_dt_spec my_led0 = GPIO_DT_SPEC_GET(DT_NODELABEL(myled0), gpios);
const struct gpio_dt_spec my_led1 = GPIO_DT_SPEC_GET(DT_NODELABEL(myled1), gpios);

int main(void)
{
    gpio_pin_configure_dt(&my_led0, GPIO_OUTPUT_INACTIVE);
    gpio_pin_configure_dt(&my_led1, GPIO_OUTPUT_INACTIVE);
    while(true)
    {
        gpio_pin_toggle_dt(&my_led0);
        gpio_pin_toggle_dt(&my_led1);
        k_msleep(1000);
    }
        
	return 0;
}

But what if you wanted to structure it the same way as gpio-leds and avoid repeating several lines?. In that case, instead of defining each LED node separately in the application, you could group them under a parent node and let the driver iterate over its children

/ {
    myLeds: myLeds{
        compatible = "simple,leds";
        myLed0: myLed0{
            gpios = <&gpioc 0 GPIO_ACTIVE_HIGH>;
        };
        myLed1: myLed1{
            gpios = <&gpioc 1 GPIO_ACTIVE_HIGH>;
        };
    };
};

If you try to build and run it at this point, you’ll notice that it fails and reports errors. This happens because you need to explicitly indicate in your binding that child nodes are allowed. In this structure, multiple child nodes share common properties — such as the compatible property — and the binding must describe that hierarchy properly.

# binding description
description: Led conencted to a pin for multipurpose

# binding name (this is the one will be used in the device tree file)
compatible: "simple,leds"

child-binding:
  # properties our node will have, in our case there is just one called gpio
  # which will be mandatory and their values are in a multi type array
  properties:
    gpio:
      type: phandle-array
      required: true
      description: The GPIO pin connected to a simple led.
💡
If you declare the nodes again using the previous structure, it will not work. That’s because in the binding file you are now specifying that the node must contain child nodes. Binding files not only define the expected properties for a node, but also describe the structure that the node must follow in the DeviceTree hierarchy.

Binding files are where you describe the properties — and even the structure — of your DeviceTree nodes. For example, if you want to define new properties such as:

  • A label to provide a name as a string
  • An integer value to specify a delay
 / {
    myLed: myLed{
        compatible = "simple,leds";
        gpio = <&gpioc 0 GPIO_ACTIVE_HIGH>;
        label = "Blue led";
        delay = <20>;
    };
};

Those properties will only be recognized if they are defined in the corresponding binding file, including their names, data types, and whether they are required or optional.

Regarding the data type, it must be one of the types supported by the Device Tree Compiler (DTC). You can find the official documentation and the list of supported types in the Devicetree Specification, which describes the valid property formats and structures

...
  # properties our node will have (gpio, label and delay)
  properties:
    gpio:
      type: phandle-array
      required: true
      description: The GPIO pin connected to a simple led.
    label:
      type: string
      description: just a name you can use for debugging
    delay:
      type: int
      description: blink delay in milliseconds

By the way, the folder structure of your application would look like the following at the end of our small example.

app
├── app.overlay
├── CMakeLists.txt
├── dts
│   └── bindings
│       └── simple,leds.yaml
├── prj.conf
├── src
│   └── main.c
└── west.yml

Try adding more properties to our driver, or even create a more complex example. Experiment with different node structures, required properties, or multiple instances. That’s the best way to truly understand how bindings and DeviceTree work together. See you in part II