文章目录[隐藏]
官方文档参考:https://docs.zephyrproject.org/latest/kernel/drivers/index.html#standard-drivers
设备驱动模型
介绍
Zephyr 设备驱动模型用于管理和配置系统中的驱动程序。设备驱动模型负责初始化系统中配置的所有驱动程序。在此模型中,驱动程序在初始化期间填充指向包含其 API 函数指针的结构体的指针。
Zephyr 设备驱动模型如下图所示:
核心思想是“抽象+实例+实现”三层分离:
1. Device Driver APIs(驱动API):定义统一的接口(如 API1、API2、API3),应用层通过这些接口操作硬件,无需关心底层细节。
- Device Driver Instances(驱动实例):每个硬件设备(如每个 LED、每个 UART)在系统中都有一个 struct device 实例,包含设备配置、API指针、私有数据等。每个实例都可以有不同的配置(如不同的引脚、参数)。
-
Device Driver Implementations (驱动实现):具体的驱动实现代码(如 led_gpio.c),实现 API 的具体功能(如点亮、熄灭、设置亮度等)
Zephyr 设备驱动模型与 Linux 设备驱动模型区别:
Zephyr | Linux | |
---|---|---|
空间隔离 | 通常没有严格的用户空间/内核空间隔离,应用层和驱动层都运行在同一地址空间,直接调用驱动API。 | 有用户空间和内核空间,应用层通过系统调用(如ioctl、read、write)与驱动交互,驱动运行在内核态。 |
驱动注册 | 驱动通过DEVICE_DT_INST_DEFINE 等宏注册 | 驱动通过 platform_driver、module_init 等机制注册 |
驱动调用方式 | 应用层直接调用驱动API(如led_on、led_off),API通过结构体函数指针(如led_driver_api)分发到具体实现。 | 应用层通过系统调用访问驱动,驱动实现 file_operations 结构体(open/read/write/ioctl等)。 |
线程与同步 | 多为单线程或RTOS线程模型 | 驱动需考虑多进程/多线程同步、锁机制 |
代码体量与复杂度 | 驱动模型轻量,适合嵌入式和资源受限环境,接口简单直接。 | 驱动模型复杂,支持多种总线、热插拔、权限管理 |
Zephyr 标准驱动程序:
标准驱动程序是指由操作系统内核或官方平台提供的、遵循统一接口规范的设备驱动。它们负责管理和操作常见硬件资源,比如:
- GPIO控制器:通过标准API(如gpio_pin_set、gpio_pin_get)实现引脚电平控制,应用和内核可直接调用,无需关心底层硬件细节。
-
LED驱动:通过标准API(如led_on、led_off、led_set_brightness)控制LED灯状态,保证不同硬件平台下接口一致。
-
串口通信:通过标准API(如uart_tx、uart_rx)实现数据收发,屏蔽硬件差异。
这些驱动程序实现了标准接口,保证不同硬件平台下系统功能的一致性和可移植性。应用和内核可以直接调用这些驱动,无需关心底层硬件细节。
Zephyr 驱动程序核心数据结构
使用 设备初始化宏 在构建时会填充一些数据结构,包含只读部分和运行时可变部分:
struct device {
const char *name;
const void *config;
const void *api;
struct device_state *state;
void *data;
};
config
:存储构建时设置的设备只读静态配置信息。例如,基本内存映射的 IO 地址、IRQ 中断号、引脚号、参数、硬件资源等,通常在编译时生成。
api
: 驱动的接口函数指针表,定义了驱动支持的操作(如 on、off、set_brightness 等),应用层通过它调用具体实现。
state
: 设备实例的通用运行状态信息,设备是否已初始化(initialized)、初始化结果(init_res)等.
data
结构体保存在 RAM 中,设备的动态数据区,用于保存运行时状态、缓存、中间变量等。。
初始化级别
zephyr 可以手动设置驱动在系统中启动过程中参与初始化的阶段顺序和优先级,这样可以保证驱动在合适的阶段初始化,满足依赖关系,比如某些驱动必须在内核启动后才能初始化。
使用 DEVICE_DEFINE()
相关的 API 允许用户指定在启动过程的哪个时间点执行驱动程序的函数。
主要有这三个初始化级别:
PRE_KERNEL_1
:
内核启动前,最早阶段,通常用于时钟等基础驱动。在初始化时不需要依赖其他设备的设备。例如仅依赖于处理器/SOC中现有硬件的设备。这些设备在配置期间无法使用任何内核服务,因为内核服务尚不可用。但是,中断子系统将会被配置,因此可以设置中断。此级别的初始化函数在中断堆栈上运行。PRE_KERNEL_2
:
内核启动前,第二阶段。用于依赖于已初始化设备(作为该PRE_KERNEL_1级别的一部分)的设备。这些设备在配置期间无法使用任何内核服务,因为内核服务尚不可用。此级别的初始化函数在中断堆栈上运行。POST_KERNEL
:
内核启动后,绝大多数外设驱动在此阶段初始化(比如 LED 驱动)。用于在配置期间需要内核服务的设备。此级别的 init 函数在内核主任务的上下文中运行。
在每个初始化级别中,您可以指定一个优先级,相对于同一初始化级别中的其他设备。优先级指定为 0 到 999 之间的整数值;值越低,初始化越早。
延迟初始化
设备的初始化也可以推迟到以后。在这种情况下,Zephyr 不会在启动时自动初始化设备,而是在应用程序调用
device_init()
时初始化。要推迟设备驱动程序的初始化,请在 DTS 文件中将 zephyr,deferred-init
属性添加 到关联的设备节点。例如:
/ {
a-driver@40000000 {
reg = <0x40000000 0x1000>;
zephyr,deferred-init;
};
};
简单驱动编写
官方用例及依赖
参考 Zephyr 文件:
1. 驱动API层 (zephyr/include/zephyr/drivers/led.h
)
2. 驱动实现层 (zephyr/drivers/led/led_gpio.c
)
3. 设备树绑定 (zephyr/dts/bindings/led/gpio-leds.yaml
)
4. Kconfig配置 (zephyr/drivers/led/Kconfig.gpio
)
Zephyr自带的LED测试用例及依赖:
zephyr/
├── drivers/led/
│ ├── Kconfig # LED驱动主配置文件
│ ├── Kconfig.gpio # GPIO LED配置文件
│ └── led_gpio.c # GPIO LED驱动实现
│
├── include/zephyr/drivers/
│ └── led.h # LED驱动API头文件
│
└── dts/bindings/led/
├── gpio-leds.yaml # GPIO LED设备树绑定
└── led-controller.yaml # LED控制器通用绑定
简单驱动编写思路
我们编写一个最简单的 GPIO LED 驱动的编写思路如下:
–
DT_DRV_COMPAT
设备树宏用于匹配设备树节点,通过 compatible 属性值互相关联。–
DEVICE_DT_INST_DEFINE
宏用于把驱动注册到内核中,便于内核经行管理。– 驱动API:具体的 LED 行为操作,提供给应用层使用,通过内核与应用层的 API 关联。
LED 驱动 API:
路径:zephyr\include\zephyr\drivers\led.h
我们只用On、off即可。
API |
---|
led_api_on on |
led_api_off off; |
led_api_blink blink; |
led_api_get_info get_info; |
led_api_set_brightness set_brightness; |
led_api_set_color set_color; |
led_api_write_channels write_channels; |
编写驱动
Zephyr的 GPIO LED 驱动参考路径:zehyr/drivers/led/led_gpio.c
关联驱动与设备树的核心是使用设备树匹配宏,如下:将会与设备树节点中compatible属性值为”gpio-leds”的节点关联。
#define DT_DRV_COMPAT gpio_leds
如果要启用zephyr日志系统,只需要如下引用:具体日志等级可以参考头文件定义去修改。
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(led_gpio, CONFIG_LED_LOG_LEVEL);
关键的数据结构,用于配置 led 引脚个数,led 设备实例:
struct led_gpio_config {
size_t num_leds;
const struct gpio_dt_spec *led;
};
驱动核心宏:LED_GPIO_DEVICE
名字可以自己取。该宏会根据不同的 led 实例展开成不同的配置信息,比如我们有两个 led,及 led0、led1,那么该宏会展开成 LED_GPIO_DIVECE(0)、LED_GPIO_DIVECE(1);
#define LED_GPIO_DEVICE(i) \
\
static const struct gpio_dt_spec gpio_dt_spec_##i[] = { \
DT_INST_FOREACH_CHILD_SEP_VARGS(i, GPIO_DT_SPEC_GET, (,), gpios) \
}; \
\
static const struct led_gpio_config led_gpio_config_##i = { \
.num_leds = ARRAY_SIZE(gpio_dt_spec_##i), \
.led = gpio_dt_spec_##i, \
}; \
\
DEVICE_DT_INST_DEFINE(i, &led_gpio_init, NULL, \
NULL, &led_gpio_config_##i, \
POST_KERNEL, CONFIG_LED_INIT_PRIORITY, \
&led_gpio_api);
DT_INST_FOREACH_STATUS_OKAY(LED_GPIO_DEVICE)
其中还会展开成各自的数据结构 gpio_dt_spec_0[]、gpio_dt_spec_1[];使用 DT_INST_FOREACH_CHILD_SEP_VARGS
宏
– 它会遍历设备树中第 i 个 gpio_leds 设备节点下的所有 gpios 子节点。
– 对每个子节点,调用 GPIO_DT_SPEC_GET 宏,生成一个 gpio_dt_spec 结构体。
– 用逗号 , 分隔,把所有结果拼成一个数组,赋值给 gpio_dt_spec_##i[]。
等价于自动生成:
static const struct gpio_dt_spec gpio_dt_spec_0[] = {
GPIO_DT_SPEC_GET(node0_child0, gpios),
GPIO_DT_SPEC_GET(node0_child1, gpios),
...
};
这个宏让驱动可以自动适配设备树中所有LED子节点的GPIO配置,无需手动枚举,便于多实例管理。
ARRAY_SIZE(gpio_dt_spec_##i)
的作用是计算 gpio_dt_spec_##i 数组的元素个数,即当前 LED 设备实例下有多少个 LED 子节点(多少个 GPIO 控制的 LED 灯)。无需手动填写。
通过 DEVICE_DT_INST_DEFINE
将驱动注册到内核中,其中包含驱动初始化、不同设备的配置信息、初始化阶段、初始化优先级、驱动API等。
最后通过宏DT_INST_FOREACH_STATUS_OKAY(LED_GPIO_DEVICE)
匹配设备树节点中 LED 设备状态为 okay 的节点,经行关联。
接下来实现具体的 LED 操作的驱动 API
完整源代码如下:
#define DT_DRV_COMPAT gpio_leds
/**
* @file
* @brief GPIO driven LEDs
*/
#include <zephyr/drivers/led.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(led_gpio, CONFIG_LED_LOG_LEVEL);
struct led_gpio_config {
size_t num_leds;
const struct gpio_dt_spec *led;
};
static int led_gpio_set_brightness(const struct device *dev, uint32_t led, uint8_t value)
{
const struct led_gpio_config *config = dev->config;
const struct gpio_dt_spec *led_gpio;
if ((led >= config->num_leds) || (value > 100)) {
return -EINVAL;
}
led_gpio = &config->led[led];
return gpio_pin_set_dt(led_gpio, value > 0);
}
static int led_gpio_on(const struct device *dev, uint32_t led)
{
return led_gpio_set_brightness(dev, led, 100);
}
static int led_gpio_off(const struct device *dev, uint32_t led)
{
return led_gpio_set_brightness(dev, led, 0);
}
static int led_gpio_init(const struct device *dev)
{
const struct led_gpio_config *config = dev->config;
int err = 0;
if (!config->num_leds) {
LOG_ERR("%s: no LEDs found (DT child nodes missing)", dev->name);
err = -ENODEV;
}
for (size_t i = 0; (i < config->num_leds) && !err; i++) {
const struct gpio_dt_spec *led = &config->led[i];
if (device_is_ready(led->port)) {
err = gpio_pin_configure_dt(led, GPIO_OUTPUT_INACTIVE);
if (err) {
LOG_ERR("Cannot configure GPIO (err %d)", err);
}
} else {
LOG_ERR("%s: GPIO device not ready", dev->name);
err = -ENODEV;
}
}
return err;
}
static const struct led_driver_api led_gpio_api = {
.on = led_gpio_on,
.off = led_gpio_off,
.set_brightness = led_gpio_set_brightness,
};
#define LED_GPIO_DEVICE(i) \
\
static const struct gpio_dt_spec gpio_dt_spec_##i[] = { \
DT_INST_FOREACH_CHILD_SEP_VARGS(i, GPIO_DT_SPEC_GET, (,), gpios) \
}; \
\
static const struct led_gpio_config led_gpio_config_##i = { \
.num_leds = ARRAY_SIZE(gpio_dt_spec_##i), \
.led = gpio_dt_spec_##i, \
}; \
\
DEVICE_DT_INST_DEFINE(i, &led_gpio_init, NULL, \
NULL, &led_gpio_config_##i, \
POST_KERNEL, CONFIG_LED_INIT_PRIORITY, \
&led_gpio_api);
DT_INST_FOREACH_STATUS_OKAY(LED_GPIO_DEVICE)
编写测试用例
文件结构及依赖:
调用关系
用户在应用层使用内核提供的标准 LED API,内核会自动关联到 LED 驱动 API,去调用具体的 LED 实现函数,最后通过底层 GPIO API 去操作 LED 设备寄存器去控制 LED。
编写yaml
参考Zephyr已存在文件路径:dts/bindings/led/gpio-leds.yaml
我们直接使用,无需在新建。
description: |
This allows you to define a group of LEDs. Each LED in the
compatible: "gpio-leds"
child-binding:
description: GPIO LED child node
properties:
gpios:
type: phandle-array
required: true
label:
type: string
description: |
Human readable string describing the LED. It can be used by an
application to identify this LED or to retrieve its number/index
(i.e. child node number) on the parent device.
编写设备树.overlay
以具体的单板为例,在我的单板上,结合 LED 原理图:确定 LED 引脚为 GPIO25, 即 GPIO0, 25
#include <zephyr/dt-bindings/gpio/gpio.h>
/ {
leds {
compatible = "gpio-leds";
status_led: led_0 {
gpios= <&gpio0 25 GPIO_ACTIVE_HIGH>;
label = "Status LED";
};
};
};
&gpio0 {
status = "okay";
};
配置 prj.conf
启用需要的驱动
# 启用LED驱动
CONFIG_LED=y
# 启用GPIO驱动
CONFIG_GPIO=y
# 启用日志
CONFIG_LOG=y
配置 CMakeList.txt
CMake 是一个跨平台的自动化构建系统工具,用于管理项目的编译、链接和安装过程。
使用命令构建系统的时候会自动调用 CMake,读取 CMakeLists.txt,解析源码、设备树、配置文件等,生成编译系统(如 Ninja 或 Makefile),然后完成编译、链接等所有构建流程。
# 指定CMake最低版本要求
cmake_minimum_required(VERSION 3.20.0)
# 查找并加载Zephyr构建系统(必须)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
# 定义项目名称
project(led)
# 添加主源文件到应用目标
target_sources(app PRIVATE src/main.c)
编写测试应用 main.c
用户可以使用宏 DT_COMPAT_GET_ANY_STATUS_OKAY
来关联设备树节点获取设备信息。
调用 device_is_ready
函数来检查设备是否就绪。
然后使用内核提供的标准 LED API 即 led_on、led_off 来控制 LED 的亮灭。
#include <zephyr/device.h>
#include <zephyr/drivers/led.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(led, CONFIG_LOG_DEFAULT_LEVEL);
/* 获取设备树中兼容字符串为“gpio-leds"且status为okay的节点ID */
#define LED_NODE_ID DT_COMPAT_GET_ANY_STATUS_OKAY(gpio_leds)
int main(void)
{
int err;
/* 获取LED设备 */
const struct device *led_dev = DEVICE_DT_GET(LED_NODE_ID);
if (!device_is_ready(led_dev)) {
LOG_ERR("LED device is not ready\n");
return -1;
}
while(1)
{
/* 点亮LED */
err = led_on(led_dev, 0);
if (err < 0) {
printk("Error turning on LED %d: %d\n", 0, err);
return -1;
}
printk("LED %d turned on\n", 0);
k_sleep(K_MSEC(1000));
/* 熄灭LED */
err = led_off(led_dev, 0);
if (err < 0) {
printk("Error turning off LED %d: %d\n", 0, err);
return -1;
}
printk("LED %d turned off\n", 0);
k_sleep(K_MSEC(1000));
}
return 0;
}
上机测试
可以使用 JLink 配合gdb工具 gdb load 或者 West flash 的方式加载编译后的zephyr.elf文件到板卡运行。
不出意外的话 LED 会循环亮灭。
了解 Heiweilu的小世界 的更多信息
订阅后即可通过电子邮件收到最新文章。