An FPGA And Embedded Engineering Blog

Category: Uncategorized

Driver development for Zephyr RTOS and nrfConnect – Part 1

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!

Exploring the nrf52-DK – Timers

This first post dives into one of the most fundamental peripherals of any microprocessor: The timer. Let us examine this part very closely. All the information was gathered from looking through an example file and the datasheet for the nRF52832.

The code with the examples can be found in the following github gist: https://gist.github.com/kschoos/535c6703ddfd06e26c5c1092d8513df4

To start, we need to find an example that makes use of the timer. After some digging I found the peripherals/ppi example to be a good candidate. Reading through the main function and going from there shows the general pattern that we should follow if we were to use this peripheral:

– Generate timer config (This looks interesting so we make a mental note)
– Initialize timer with that config and pass event handler callback
– Convert a period in ms to a number of ticks (A quick glance at the function reveils that it does what we’d expect)
– Configure the compare register (This also looks interesting)
– Enable the timer

The event handler is just a function that is called every time a certain event is triggered by the timer. We will see this in more depth shortly.

Now that we have a broad overview, let’s dig deeper into the different bits. The constant NRFX_TIMER_DEFAULT_CONFIG pops into view and it’s interesting to see what a “default config” for such a timer contains. We follow its definition and see that it is made up of more constants that are provided by the sdk_config.h.

The first parameter is the frequency. The maximum frequency is 16 MHz  and it can be reduced by setting this parameter. Interesting to note is that, when a frequency below or equal to 1MHz is chosen, the slower CLK PCLK1M (1 MHz internal clock)  is chosen instead of PCLK16M (16 MHz internal clock), in order to reduce power consumption. This should be kept in mind when choosing the right timer values for certain tasks.

Next, mode can be either Timer or Counter. When mode is configured to be “Timer”, the module acts like an actual timer. The timer’s internal counter register is incremented for every tick. The time between ticks is the period given by the timer frequency. However, when setting the mode to “Counter”, the internal counter register is incremented whenever the COUNT-task is triggered. We will see an example of both.

Bit width sets the width of the variable that represents the number of ticks that have passed. This can be 8, 16, 24 or 32 bit wide. We can easily compute the largest timeline that these constraints allow. For example 2^32 / 31.25 kHz = 137439 seconds (38 hours and 10 minutes) is the longest possible timeline when using the slowest frequency (remember, that this puts constraints on the accuracy of the timer). When using the full 16MHz we only get 4 minutes and 28 seconds worth of timeline. We can extend these timelines by keeping track of time itself, updating a variable in software whenever an the timer is about to overrun.

IRQ priority sets the interrupt priority. Nested interrupts are a thing in ARM Cortex-M4, so the priority indicated, by which other interrupts the interrupt service routine for timer interrupts can be … interrupted.

Now that we know the settings, which describe a timer, let us see what this mysterious nrf_drv_timer_extended_compare function is doing.

Every timer in the nRF52 has multiple capture/compare registers associated with them. Timers 0 to 2 have 4, timers 3 and 4 have 6 so called CC-registers. The concepts of these functionalities is straight-forward and will be briefly explained here:

“Capture” means, that the current value of the timeline is written into the specified CC-register. Capturing can be used for example for time-stamping certain events and interrupts or for precisely measuring the time between two instants, as setting a capture task has such a small amount of overhead. In order to use a certain CC-register in capture mode, simply call the function nrfx_timer_capture with the timer and channel you are interested in. This will trigger the CAPTURE-Task and return the captured value. The function nrfx_timer_capture_get is a wrapper that simply returns the captured value for a certain channel. This function should be used when you do not want to capture the value again, but just want to read from it. Whenever the CAPTURE-task is triggered via the PPI (Programmable Peripheral Interface), this function must be used, as we don’t want to trigger the CAPTURE-Task again when reading the value.

“Compare” means, that an event occurs whenever the timeline is equal to the value in that CC-register. The timer counts up to the value specified in the CC-register and triggers an interrupt when it reaches that value. This can be useful when you want periodic things to happen, such as an LED blinking or a sensor measurement being taken. However, when implementing such functionality, make sure to keep your ISRs as short as possible and defer all complex processing to later times. Timer interrupts  are also used by preemptive schedulers: A timer interrupt occurs every X milliseconds and checks which task is supposed to run next. To setup a compare timer interrupt, simply call nrfx_timer_compare or nrfx_timer_extended_compare with a timer instance, an appropriate channel, the value at which the interrupt should be triggered as well as a flag, if interrupts should be enabled or disabled. In addition, nrfx_timer_extended_compare also takes an nrf_timer_short_mask_t can be added, which allows to specify that the CLEAR- and / or the STOP-Task should be executed, whenever the corresponding COMPARE-event is raised.

The index of the capture/compare register is equivalent to what is called “channel” in the example code.

Now that we have a good overview of what these timers are capable of, it’s time to play around with them. I created three small examples that showcase what we have learned. The gist of these examples is obviously not that this can’t be done differently, but it’s fun to tinker with these kind of things to deepen the understanding.

The examples are based on the example project peripherals/bsp in the nRF5 SDK, because it already includes button and LED initialization as well as UART logging. The only thing we need to import is nrfx_timer.h and nrfx_timer.c. In addition, some fields in the sdk_config.h must be added and reconfigured (The part found under “nrfx_timer – TIMER peripheral driver”). After this is done, our playground is ready for action.

There are 3 tasks implemented, which can be cycled through by pressing Button 1 on the nRF52-DK.

Task 1:
The first task creates a single counter. Whenever Button 0 is pressed, the counter is incremented by 1 and the value of the counter is displayed in form of its bits by the LEDs. When the compare value of 16 is reached, the counter is supposed to be resetted. This is achieved by setting the mask to NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK in the call to nrfx_timer_extended_compare. The timer is now cleared every time the compare value of channel 0 is reached.

Task 2:
In the second task, we create another timer. This timer increments the first timer (the counter), every time it reaches a certain compare value. The compare value is computed, such that it takes exactly 1 second between events. The result is the display of a binary number which is incremented every second.

Task 3:
The final task contains two timers. The timer from task 2 enables the new timer every time it hits its compare value. The second timer has several compare events attached to it. The datasheet tells us, that we need to use timer 3 or 4 if we need more than 4 compare registers. Instead of computing the compare values ourselves, we use the function nrfx_timer_ms_to_ticks, which does the computation for us, based on the frequency of the timer. Note that we only set the masking shortcut for the last compare registers: We only want to reset the timer once it cycled to the end. We finally disable the timer inside of its own event handler.

This wraps up this first post on timers in the nRF52-DK. We will come back to timers again and again, as they are vital for all kinds of embedded devices.