IoT Nordic nRF54L15-DK Zephyr - Creating a custom driver with a custom API

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,

image

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:

  1. LED’s GPIO pin
  2. 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 the blink class.
  • DEVICE_API_GET(class, device) – Obtains the pointer to the API instance of a specific device class. Here, it gives access to the blink_driver_api instance 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 led parameter is of type gpio_dt_spec expect to have the corresponding property (led-gpios) in the devicetree node. The macro GPIO_DT_SPEC_INST_GET() will parse and convert the parameter for this demo.
  • The period_ms parameter, will search for a parameter with this name in the DeviceTree node using DT_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,

image

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í.

2 Likes