Nordic nRF Connect 软件开发套件(SDK)所基于的 Zephyr 实时操作系统(RTOS),采用了驱动程序与应用程序编程接口(API)高度解耦的设备驱动模型。该模型允许开发人员在不修改上层应用代码的前提下,直接替换底层驱动的实现逻辑,这也是 Zephyr RTOS 的一大优势特性。
本演示将说明如何创建一个自定义应用程序编程接口 (API),如何使用自定义参数配置 Zephyr 设备树 (DeviceTree),并最终以 Nordic nRF54L15-DK 开发板 开发套件为例,展示如何在驱动程序和应用程序中使用这些内容。
本示例的核心目标是开发一款自定义 Zephyr 设备驱动,用于实现开发板上 LED 灯的周期性闪烁功能。具体实现效果包括:通过通用输入输出端口(GPIO)控制 LED 周期性闪烁,且闪烁周期可直接在设备树中进行配置。
设备树中需配置以下两个核心参数:
- LED 对应的 GPIO 引脚
- 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_gpios 和 blink_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-gpios 和 blink-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 网站购买。祝您开发顺利!
