The Zephyr RTOS that the Nordic nRF Connect Software Development Kit (SDK) is based on, has a highly decoupled device driver model from its Application Programming Interface (API), which allows designers to switch out the low-level driver implementation without modifying the top application, this is one of the nice features that Zephyr RTOS has. Before proceeding with this demo please follow the Installation of Nordic Zephyr Development Tools in Linux.
This demo will illustrate how to create a custom Application Programming Interface (API), configure the Zephyr DeviceTree with customized parameters, and finally, use these in the driver and application using the Nordic nRF54L15-DK development kit,
The goal of this demo is to create a customized Zephyr device driver used to periodically blink an LED in the board. This demo will show how to get an LED (via General Purpose Input/Output port) device to blink periodically. Also this period will be configurable from the DeviceTree.
The DeviceTree has the following 2 relevant parameters:
- LED’s GPIO pin
- The period of the blinking mechanism
Also we need to be able to change the blinking period from the main.c application. The device driver will provide the following 2 API’s functions:
blink_set_period_ms – To establish the blinking period.
blink_off – To deactivate the LED entirely.
We start from the Zephyr RTOS application template,
template/
├─── app/
| ├─── boards/
│ ├─── src/
│ │ └─── main.c
│ ├──prj.conf
| └──CMakeLists.txt
|
└─── custom_driver_module/
├─── drivers/
│ ├── blink/
│ │ ├──gpio_led.c
│ │ ├──CMakeLists.txt
│ │ └───Kconfig
│ ├──CMakeLists.txt
│ └───Kconfig
├─── dts/
├─── include/
│ └───blink.h
├─── zephyr/
│ └───module.yml
├──CMakeLists.txt
└──Kconfig
Follow the next steps,
1. Create the driver binding.
We need a binding file for the driver to define the device driver parameters.
1.1 Create the binding file blink-gpio-leds.yaml.
Create the file blink-gpio-leds.yaml in the dts/bindings directory, and add the following lines to declare the DeviceTree compatibility name, and include the base for the binding.
compatible: "blink-gpio-led"
include: base.yaml
1.2 Include a property for the LED GPIO to the binding.
Include the property led-gpios by creating the following lines to the file.
properties:
led-gpios:
type: phandle-array
required: true
description: GPIO-controlled LED.
the led-gpios property is going to be used to determine the GPIO pin used to connect to the appropaite LED.
1.3 Include a property for the blinking period to the proper binding.
Include the property blink-period-ms for the blink period with the lines shown below.
blink-period-ms:
type: int
description: Initial blinking period in milliseconds.
This is the important parameter used to control the behavior of the Zephyr driver. In the next steps, we will update the API to be able to change its value.
2. Establish the API’s for the “blink” driver class.
At this point, create a class for the driver. A custom class will be added by providing a common API for a selected collection of device drivers. This is done using the file blink.h found in custom_driver_module/include folder.
2.1 Create the API structure in the driver class
Edit the include/blink.h file in the custom driver module directory and create the API structure blink_driver_api. At this point, include a pointer to the function that changes the blinking period. Proceed to let the toolchain know that this structure is a device driver API by using the proper __subsystem prefix.
__subsystem struct blink_driver_api {
/**
* @brief Configure the LED blink period.
*
* @param dev Blink device instance.
* @param period_ms Period of the LED blink in milliseconds, 0 to
* disable blinking.
*
* @retval 0 if successful.
* @retval -EINVAL if @p period_ms can not be set.
* @retval -errno Other negative errno code on failure.
*/
int (*set_period_ms)(const struct device *dev, unsigned int period_ms);
};
2.2 Implement a public API function for the driver class.
The main.c program will be able to call this function from an instance of any driver that belongs to this class. Although they share same API function, the behavior will be specific to a certain driver’s implementation. The API function should follow the naming structure z_impl_<function_name>.
Now use an additional macro helper:
DEVICE_API_IS(class, device)– This verifies if the device is a particular class. In this case, it verifies if the function was called with a device whose driver is theblinkclass.
DEVICE_API_GET(class, device)– Obtains the pointer to the API instance of a specific device class. Here, it gives access to theblink_driver_apiinstance defined by the driver
static inline int z_impl_blink_set_period_ms(const struct device *dev,
unsigned int period_ms)
{
__ASSERT_NO_MSG(DEVICE_API_IS(blink, dev));
return DEVICE_API_GET(blink, dev)->set_period_ms(dev, period_ms);
}
2.3 Provide the user space wrapper with the prefix __syscall before the API function declaration
__syscall int blink_set_period_ms(const struct device *dev,
unsigned int period_ms);
2.4 Create an API function blink_off() to deactivate the blinking process
Using a previously defined function with the prefix,
static inline int blink_off(const struct device *dev)
{
return blink_set_period_ms(dev, 0);
}
2.5 Add the syscall header at the end of the header file
Any header file that declares system calls must include as a requirement a special generated header at the very bottom of the header file.
#include <syscalls/blink.h>
2.6 Let the build system know where to find the syscalls declaration
Add the driver class header to the corresponding syscalls list by changing the CMakeLists.txt file in the root of the custom driver module:
zephyr_syscall_include_directories(include)
3. Define the gpio_led driver to belong to the custom driver class blink.
Since the custom driver class: blink is already defined. The device driver will implement the API of the class. This will be incorporated in the file gpio_led.c found in custom_driver_module/drivers/blink.
3.1 Now, the driver’s data structure is created
The LED blinking action is going to be performed in the timer’s callback,
struct blink_gpio_led_data {
struct k_timer timer;
};
3.2 Now, define the configuration structure for the driver in drivers/blink/gpio_led.c
struct blink_gpio_led_config {
struct gpio_dt_spec led;
unsigned int period_ms;
};
3.3 Glue the blink_gpio_led_set_period_ms function to the driver API function
Since driver functions are already prepared for this demo, at this point there is only a need to configure the API structures to use them properly in the application main.c. At this moment, use the DEVICE_API(class, function) macro, that assigns a selected function to a particular device driver class.
In this demo, now create a structure instance of the blink (subsystem) class and connect the blink_gpio_led_set_period_ms function to .set_period_ms as part of the driver API,
static DEVICE_API(blink, blink_gpio_led_api) = {
.set_period_ms = &blink_gpio_led_set_period_ms,
};
4. Define the device.
Proceed to define the custom device. Assign the API and the configuration structures to the proper fields in the device definition structure.
4.1 Define the data structure instance template.
Place the following code in the BLINK_GPIO_LED_DEFINE macro
static struct blink_gpio_led_data data##inst; \
4.2 Create the configuration structure instance template
In the previous step 1, a binding containing the fields led_gpios and blink_period_ms was created. Use these fields to get configuration parameters from the DeviceTree.
- As
ledparameter is of typegpio_dt_specexpect to have the corresponding property (led-gpios) in the devicetree node. The macroGPIO_DT_SPEC_INST_GET()will parse and convert the parameter for this demo.
- The
period_msparameter, will search for a parameter with this name in the DeviceTree node usingDT_INST_PROP_OR(). If nothing is found, it will get the value of 0.
static const struct blink_gpio_led_config config##inst = { \
.led = GPIO_DT_SPEC_INST_GET(inst, led_gpios), \
.period_ms = DT_INST_PROP_OR(inst, blink_period_ms, 0U), \
};
4.3 Declare the device definition template
DEVICE_DT_INST_DEFINE(inst, blink_gpio_led_init, NULL, &data##inst, \
&config##inst, POST_KERNEL, \
CONFIG_BLINK_INIT_PRIORITY, \
&blink_gpio_led_api);
4.4 Define the driver’s init priority level
Establish the Kconfig BLINK_INIT_PRIORITY in drivers/blink/Kconfig
Set the default value to KERNEL_INIT_PRIORITY_DEVICE
config BLINK_INIT_PRIORITY
int "Blink device drivers init priority"
default KERNEL_INIT_PRIORITY_DEVICE
help
Blink device drivers init priority.
5. Use the custom driver in the application.
Add a node to the DeviceTree with the binding we defined earlier in the demo.
5.1 Establish a blink_gpio_leds device node in the DeviceTree.
Include an overlay file <board_target>.overlay in app/boards, with the name corresponding to the Nordic nRF54L15-DK development kit,
In the overlay file, define a blink_led node, and add the led-gpios and blink-period-ms parameters. For the led-gpios use one of the LEDs populated in the board (in this demo pin 2.9 for the nRF54L15 DK). For the blink-period-ms, establish a blink period, for example 1 second (1000 ms).
Finally, set the node compatibility to blink-gpio-led.
The overlay file should look like this,
/ {
blink_led: blink-led {
compatible = "blink-gpio-led";
led-gpios = <&gpio2 9 GPIO_ACTIVE_HIGH>;
blink-period-ms = <1000>;
};
};
5.2 Activate the blink driver in the application
Activate the blink driver in the main.c application by adding the following line to the prj.conf file
CONFIG_BLINK=y
5.3 Use the custom blink API from the driver to update the blinking period from the main.c application
Add the following code into main.c application,
/* Use custom API to turn LED off */
int ret = blink_off(blink);
if (ret < 0) {
LOG_ERR("Could not turn off LED (%d)", ret);
return 0;
}
while (1) {
/* When LED is constantly enabled - start over with high blinking period*/
if (period_ms == 0U) {
period_ms = BLINK_PERIOD_MS_MAX;
} else {
period_ms -= BLINK_PERIOD_MS_STEP;
}
printk("Setting LED period to %u ms\n",
period_ms);
/* Use custom API to change LED blinking period*/
blink_set_period_ms(blink, period_ms);
k_sleep(K_MSEC(1000));
}
At this point, the application source code main.c is as follows,
/*
* Copyright (c) 2021 Nordic Semiconductor ASA
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <blink.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(DigiKey Coffee Cup, LOG_LEVEL_INF);
#define BLINK_PERIOD_MS_STEP 100U
#define BLINK_PERIOD_MS_MAX 1000U
int main(void)
{
/* Start blinking - slow*/
unsigned int period_ms = BLINK_PERIOD_MS_MAX;
LOG_INF("Zephyr Example Application");
const struct device * blink = DEVICE_DT_GET(DT_NODELABEL(blink_led));
if (!device_is_ready(blink)) {
LOG_ERR("Blink LED not ready");
return 0;
}
/* STEP 5.3 Use the custom blink API from the driver to change the blinking period */
/* Use custom API to turn LED off */
int ret = blink_off(blink);
if (ret < 0) {
LOG_ERR("Could not turn off LED (%d)", ret);
return 0;
}
while (1) {
/* When LED is constantly enabled - start over with high blinking period*/
if (period_ms == 0U) {
period_ms = BLINK_PERIOD_MS_MAX;
} else {
period_ms -= BLINK_PERIOD_MS_STEP;
}
LOG_INF("Setting LED period to %u ms",
period_ms);
/* Use custom API to change LED blinking period*/
blink_set_period_ms(blink, period_ms);
k_sleep(K_MSEC(1000));
}
return 0;
}
Now build the Zephyr application as follows from the project top level folder, after many arguments displayed in the build process, here are the last ones if it completed the build process as expected,
digikey_coffee_cup # west build app -b nrf54l15dk/nrf54l15/cpuapp
....
....
....
-- Configuring done (8.8s)
-- Generating done (0.1s)
-- Build files have been written to: /digikey_coffee_cup/app/build
-- west build: building application
[1/159] Preparing syscall dependency handling
[3/159] Generating include/generated/zephyr/version.h
-- Zephyr version: 4.2.99 , build: v4.2.0-5624-gfb7a74ebbd0a
[159/159] Linking C executable zephyr/zephyr.elf
Memory region Used Size Region Size %age Used
FLASH: 39476 B 1428 KB 2.70%
RAM: 6728 B 188 KB 3.49%
IDT_LIST: 0 GB 32 KB 0.00%
Generating files from /digikey_coffee_cup/app/zephyr/zephyr.elf for board: nrf54l15dk
Finally, flash the application after connecting the Nordic nRF54L15-DK development kit via the USB to the host computer as follows,
digikey_coffee_cup # west flash
-- west flash: rebuilding
ninja: no work to do.
-- west flash: using runner nrfutil
-- runners.nrfutil: reset after flashing requested
Using board 001057777221
-- runners.nrfutil: Flashing file: /digikey_coffee_cup/zephyr/zephyrproject/zephyr/driver/appl/build/zephyr/zephyr.hex
-- runners.nrfutil: Connecting to probe
-- runners.nrfutil: Programming image
-- runners.nrfutil: Verifying image
-- runners.nrfutil: Reset
-- runners.nrfutil: Board(s) with serial number(s) 1057777221 flashed successfully.
After flashing the Device Driver Zephyr RTOS demo to the Nordic nRF54L15-DK development kit, LED0 blinking period will change every 1 second as shown in the next video,
This demo showed how to create a custom Application Programming Interface (API), configuring the Zephyr DeviceTree with customized parameters, using these in the driver and application using the Nordic nRF54L15-DK development kit,
Hopefully this will serve as an introductory session to help in the process of creating a custom Zephyr RTOS drivers with a custom APIs. The Nordic nRF54L15-DK development kit is an excellent avenue to develop new power constrained IoT Zephyr RTOS reusable applications, and is available at DigiKey. Have a nice day!
This article is also available in spanish here.
Este artículo esta disponible en español aquí.
