Introduction

In this tutorial we will look at how we can create a device driver for Zephyr RTOS.
Zephyr RTOS already supports a lot of different devices and it may already have drivers for your target. If that’s not the case, you came to the right place. We will find out how we can create device drivers for Zephyr RTOS in a streamlined manner. The device that we will be targeting is the MS8607 sensor by TE connectivity.

We will put the driver together with the source code of our main application. This is the simplest form of implementing the driver and it will serve us well. When projects become larger or if multiple projects depend on the same device, it may become necessary to move the driver out, however for small to medium projects, having the driver right there with the rest of the source is completely sufficient.

The Bare Minimum

The Necessary Files

After generating a hello-world project in VSCode using nrf Connect, the first thing we need to do is add several directories and files, such that our tree looks like the following (ignoring some of the pre-generated files):


├───dts
│   └───bindings
│       └───drivers
│           └───example,test-driver.yaml
└───src
    └───driver
    │    └───test_driver.c
    │    └───test_driver.h
    └───main.c
    └───CMakeLists.txt
    └───prj.conf
    └───nrf52dk_nrf52832.overlay

The attentive reader will see that this is built for the nrf52-DK featuring an nrf52832.

CMakeLists Entry

Before we forget it, let’s quickly add the c file we just added to the CMakeLists.txt file. Simply add the line


target_sources(app PRIVATE src/driver/test_driver.c)

to the bottom of the file and it will be included when building the project.

The Devicetree Bindings

Next, let’s create the simplest possible devicetree binding file. Edit dts/bindings/drivers/example,test-driver.yaml to contain the following:

description: Driver Test

compatible: "example,test-driver"

include: base.yaml

This will create a compatible that we will be able to use in our device tree. By including base.yaml our driver automatically inherits some important properties, such as status and compatible, which you will have come across already when you have used Zephyr RTOS before.

The Driver Source

We will create a multi-instance driver. Like this, it is possible to have multiple instances of the same device in our project.

Let us fill out the barebones for src/driver/test_driver.c next:

#include <zephyr/device.h>

#define DT_DRV_COMPAT example_test_driver

static int driver_init(const struct device *dev) {
    printk("Initializing driver!\n");
}

#define ZEPHYR_DRIVER_TEST_INST(inst) \
    DEVICE_DT_INST_DEFINE(  \
        inst,        \
        driver_init, \
        NULL, \
        NULL, \
        NULL, \
        APPLICATION, \
        CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \
        NULL);

DT_INST_FOREACH_STATUS_OKAY(ZEPHYR_DRIVER_TEST_INST)

The line …

#define DT_DRV_COMPAT example_test_driver

… registers our driver with the compatible we defined earlier. Commas and hyphens are replaced by underscores and everything needs to be lowercase.

Next, a macro is defined that will allow for an instance to be initalized. Actually, the last line …

DT_INST_FOREACH_STATUS_OKAY(ZEPHYR_DRIVER_TEST_INST)

… will run this macro for each instance that is added to the devicetree that has a status of okay. You will want to look at the documentation for DEVICE_DT_INST_DEFINE to see, what the different entries stand for. A word of caution though: If you have several instances of the same device, the value for inst will not have any relationship with the ordering of the devices in the devicetree file, nor to the memory address of that device, so you should never rely on that directly.

We set the driver_init function as an initialization function for our driver and we choose to initialize the driver at APPLICATION level, which allows us to see the output of the printk function.

Now only a last change needs to be done before we can build, flash and reap the fruits of our work!

The Devicetree Overlay

In the file nrf52dk_nrf52832.overlay (or whatever board you are using), create an instance for our newly created driver in the soc node:


/ {
    soc {
        drivertest: driver_test0 {
            compatible = "example,test-driver";
            status = "okay";
        };
    };
};

Build and Flash

That’s it for the first part! Build the code by hitting Ctrl+Alt+B. You will probably come across the line…

node '/soc/driver_test0' compatible 'example,test-driver' has unknown vendor prefix 'example'

… which is due to the fact that there is no vendor called example in the file zephyr/dts/bindings/vendor-prefixes.txt in your zephyr installation. This is not an error though and everything compiles fine anyway.

After that, flash the code to your device and you’ll be greeted by the wonderful line…

Initializing driver!

We didn’t even have to add any code to main.c yet our driver is initialized and ready to be used. Next, we will look at adding a bit more functionality to the driver and making it actually useful.

Conclusion

That concludes the first part of the driver development! In the next part, we will look at how to choose an API for our driver and make it actually useful. Stay tuned!