物联网 Nordic nRF54L15-DK 开发板(基于 Zephyr 系统)—— 开发自定义驱动及配套 API

Nordic nRF Connect 软件开发套件(SDK)所基于的 Zephyr 实时操作系统(RTOS),采用了驱动程序与应用程序编程接口(API)高度解耦的设备驱动模型。该模型允许开发人员在不修改上层应用代码的前提下,直接替换底层驱动的实现逻辑,这也是 Zephyr RTOS 的一大优势特性。

本演示将说明如何创建一个自定义应用程序编程接口 (API),如何使用自定义参数配置 Zephyr 设备树 (DeviceTree),并最终以 Nordic nRF54L15-DK 开发板 开发套件为例,展示如何在驱动程序和应用程序中使用这些内容。

本示例的核心目标是开发一款自定义 Zephyr 设备驱动,用于实现开发板上 LED 灯的周期性闪烁功能。具体实现效果包括:通过通用输入输出端口(GPIO)控制 LED 周期性闪烁,且闪烁周期可直接在设备树中进行配置。

设备树中需配置以下两个核心参数:

  1. LED 对应的 GPIO 引脚
  2. LED 闪烁周期

同时,要求能够在 main.c 应用代码中动态修改闪烁周期。该自定义驱动需对外提供以下两个 API 函数:

blink_set_period_ms – To establish the blinking period.
blink_off – To deactivate the LED entirely.

本示例的项目代码基于 Zephyr RTOS 应用程序模板进行开发,项目目录结构如下:

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

具体开发步骤如下:

一、创建驱动绑定文件

需要为该驱动创建一个绑定文件,用于定义设备驱动的相关参数。

1.1 创建绑定文件 blink-gpio-leds.yaml

dts/bindings 目录下新建文件 blink-gpio-leds.yaml,并添加如下代码,用于声明设备树兼容性名称,同时引入绑定文件的基础配置。

compatible: "blink-gpio-led"

include: base.yaml

1.2 在绑定文件中添加 LED GPIO 引脚属性

在上述文件中补充以下代码,添加 led-gpios 属性配置。

properties:
  led-gpios:
    type: phandle-array
    required: true
    description: GPIO-controlled LED.

led-gpios 属性将用于指定连接目标 LED 灯的 GPIO 引脚。

1.3 在绑定文件中添加闪烁周期属性

在文件中继续补充以下代码,添加 blink-period-ms 属性,用于配置 LED 闪烁周期。

blink-period-ms:
    type: int
    description: Initial blinking period in milliseconds.

该参数是控制 Zephyr 驱动工作逻辑的关键配置项,后续步骤中将通过 API 实现该参数的动态修改。

二、为 “blink” 驱动类开发配套 API

接下来需要为该驱动创建专属驱动类,通过在 custom_driver_module/include 目录下的 blink.h 文件中编写代码,为该类提供通用 API 接口。

2.1 在驱动类中定义 API 结构体

编辑自定义驱动模块目录下的 include/blink.h 文件,创建名为 blink_driver_api 的 API 结构体。在该结构体中定义一个函数指针,用于指向闪烁周期修改函数。同时,需添加 __subsystem 前缀,告知工具链该结构体是一个设备驱动 API。

__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 为驱动类开发公有 API 函数

main.c 应用程序将通过调用该函数,实现对任意同类型驱动实例的控制。不同驱动实例虽调用相同的 API 函数,但具体执行逻辑由各自的驱动实现代码决定。API 函数命名需遵循 z_impl_<函数名> 的格式规范。

此处需调用两个辅助宏定义:

  • DEVICE_API_IS(class, device):用于校验设备是否属于指定驱动类,本示例中用于校验调用函数的设备是否属于 blink 驱动类
  • DEVICE_API_GET(class, device):用于获取指定设备驱动类的 API 实例指针,本示例中用于获取驱动定义的 blink_driver_api 实例
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 为 API 函数声明添加 __syscall 前缀,生成用户态封装接口

__syscall int blink_set_period_ms(const struct device *dev,
				  unsigned int period_ms);

2.4 开发 blink_off() API 函数,用于停止 LED 闪烁

调用前文定义的函数,实现 LED 关闭功能:

static inline int blink_off(const struct device *dev)
{
	return blink_set_period_ms(dev, 0);
}

2.5 在头文件末尾添加系统调用头文件

所有声明了系统调用的头文件,都必须在文件末尾引入自动生成的专用头文件:

#include <syscalls/blink.h>

2.6 配置构建系统,指定系统调用声明文件路径

修改自定义驱动模块根目录下的 CMakeLists.txt 文件,将驱动类头文件路径添加至系统调用头文件列表中:

zephyr_syscall_include_directories(include)

三、将 gpio_led 驱动归属到自定义 blink 驱动类

前文已完成 blink 自定义驱动类的定义,接下来需要在 custom_driver_module/drivers/blink 目录下的 gpio_led.c 文件中编写代码,使 gpio_led 驱动实现该类的 API 接口。

3.1 创建驱动数据结构体

LED 闪烁功能将在定时器回调函数中实现,需先定义如下数据结构体:

struct blink_gpio_led_data {
	struct k_timer timer;
};

3.2 在 drivers/blink/gpio_led.c 中定义驱动配置结构体

struct blink_gpio_led_config {
	struct gpio_dt_spec led;
	unsigned int period_ms;
};

3.3 将 blink_gpio_led_set_period_ms 函数关联至驱动 API

本示例的驱动功能代码已提前编写完成,只需配置 API 结构体,即可在应用程序 main.c 中调用。此处需使用 DEVICE_API(class, function) 宏定义,将指定函数绑定至对应的设备驱动类。

在本示例中,需创建一个 blink 子系统类的结构体实例,并将 blink_gpio_led_set_period_ms 函数关联到驱动 API 的 .set_period_ms 成员:

static DEVICE_API(blink, blink_gpio_led_api) = {
	.set_period_ms = &blink_gpio_led_set_period_ms,
};

四、定义设备实例

完成自定义设备的定义,将 API 结构体与配置结构体关联到设备定义结构体的对应字段中。

4.1 定义数据结构体实例模板

BLINK_GPIO_LED_DEFINE 宏定义中添加如下代码:

static struct blink_gpio_led_data data##inst;                          \               \

4.2 创建配置结构体实例模板

在步骤 1 中,我们已创建包含 led_gpiosblink_period_ms 两个字段的绑定文件,现在需要通过这两个字段从设备树中读取配置参数。

  • led 参数的类型为 gpio_dt_spec,需确保设备树节点中存在对应的 led-gpios 属性。本示例将通过 GPIO_DT_SPEC_INST_GET() 宏定义解析并转换该参数。
  • period_ms 参数将通过 DT_INST_PROP_OR() 宏定义从设备树节点中读取。若未读取到该参数,则默认赋值为 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 声明设备定义模板

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 定义驱动初始化优先级

drivers/blink/Kconfig 文件中配置 BLINK_INIT_PRIORITY 选项,默认值设置为 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.1 在设备树中创建 blink_gpio_leds 设备节点

app/boards 目录下创建一个板级设备树叠加文件 <board_target>.overlay,文件名需与 Nordic nRF54L15-DK 开发板的型号对应。

在该叠加文件中定义一个 blink_led 节点,并配置 led-gpiosblink-period-ms 两个参数:

  • led-gpios:指定开发板上的一个 LED 灯对应的 GPIO 引脚(本示例中使用 nRF54L15 DK 开发板的 2.9 引脚)
  • blink-period-ms:设置 LED 闪烁周期(本示例中设置为 1 秒,即 1000 毫秒)

最后,将节点的兼容性属性设置为 blink-gpio-led

叠加文件的完整内容如下:

/ {
	blink_led: blink-led {
		compatible = "blink-gpio-led";
		led-gpios = <&gpio2 9 GPIO_ACTIVE_HIGH>;
		blink-period-ms = <1000>;
	};
};

5.2 在应用程序中启用 blink 驱动

prj.conf 配置文件中添加如下代码,启用 blink 驱动:

CONFIG_BLINK=y

5.3 在 main.c 应用程序中调用自定义 blink API,动态修改 LED 闪烁周期

main.c 文件中添加如下代码:

 /* 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));
     }

完成上述代码添加后,main.c 应用程序的完整代码如下:

/*
 * 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;
 }

在完成上述代码编写后,在项目根目录下执行以下命令构建 Zephyr 应用程序。构建过程中会输出大量日志信息,若构建成功,最终输出的日志内容如下:

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

最后,通过 USB 线将 Nordic nRF54L15-DK 开发板连接至电脑主机,执行以下命令将应用程序烧录至开发板:

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.

将本 Zephyr RTOS 设备驱动示例程序烧录至 Nordic nRF54L15-DK 开发板后,LED0 灯的闪烁周期将每隔 1 秒变化一次。

本示例详细演示了如何基于 Nordic nRF54L15-DK 开发板,开发自定义应用程序编程接口(API)、在 Zephyr 设备树中配置自定义参数,以及在驱动程序和应用程序中调用相关接口与参数。

希望本教程能作为入门指南,助力开发者掌握基于 Zephyr RTOS 开发自定义驱动及配套 API 的方法。Nordic nRF54L15-DK 开发板是开发低功耗物联网 Zephyr RTOS 可复用应用的理想平台,该开发板可在 DigiKey 网站购买。祝您开发顺利!