3. Zephyr 设备驱动模型:设备驱动架构、设备树基础、驱动绑定与初始化流程
Zephyr RTOS 的设备驱动模型是其核心抽象层,它将硬件外设与上层应用解耦。理解设备驱动架构、设备树(Device Tree)以及驱动绑定与初始化流程,是开发传感器驱动的基石。本章将深入剖析这三个关键环节,帮助您建立完整的驱动开发知识体系。
3.1 设备驱动架构
Zephyr 的设备驱动模型采用分层与面向对象的设计思想,核心围绕 struct device 结构体展开。每个外设驱动都通过一个 struct device 实例来表示,该实例包含了驱动所需的全部信息。
3.1.1 核心数据结构:struct device
在 include/zephyr/device.h 中定义,struct device 是驱动模型的核心。其主要成员如下:
| 成员 | 类型 | 描述 |
|---|---|---|
name |
const char * |
设备名称,用于在系统中唯一标识该设备。 |
config |
const void * |
指向设备配置数据的指针,通常包含硬件地址、IRQ 号、I2C 地址等静态信息。 |
api |
const void * |
指向驱动 API 结构体的指针,定义了该设备类型支持的操作(如读、写、配置)。 |
data |
void * |
指向设备运行时数据的指针,用于存储设备状态、缓冲区等动态信息。 |
state |
atomic_t |
设备状态标志,用于表示设备是否已初始化、是否就绪等。 |
config 和 data 的分离是 Zephyr 驱动模型的重要设计。配置数据(如 I2C 地址)在编译时确定,存储在只读区域;运行时数据(如采样值、校准偏移)则存储在可读写的 RAM 中。这种设计提高了内存效率和安全性。
3.1.2 驱动 API 结构体
每个设备类(如传感器、GPIO、I2C)都定义了自己的 API 结构体。以传感器驱动为例,其 API 通常定义在 include/zephyr/drivers/sensor.h 中:
struct sensor_driver_api {
int (*sample_fetch)(const struct device *dev,
enum sensor_channel chan);
int (*channel_get)(const struct device *dev,
enum sensor_channel chan,
struct sensor_value *val);
int (*attr_set)(const struct device *dev,
enum sensor_channel chan,
enum sensor_attribute attr,
const struct sensor_value *val);
int (*attr_get)(const struct device *dev,
enum sensor_channel chan,
enum sensor_attribute attr,
struct sensor_value *val);
#ifdef CONFIG_SENSOR_ASYNC_API
int (*trigger_set)(const struct device *dev,
const struct sensor_trigger *trig,
sensor_trigger_handler_t handler);
#endif
};
驱动开发者需要实现这些函数指针,并将其赋值给一个 sensor_driver_api 结构体实例。上层应用通过 device.h 提供的通用接口(如 sensor_sample_fetch())调用时,最终会通过 dev->api 跳转到具体的驱动实现。
3.1.3 驱动架构分层图
下图展示了 Zephyr 设备驱动模型的分层架构,从应用层到硬件层的完整调用链路:
3.2 设备树 (Device Tree) 基础
设备树是一种描述硬件配置的数据结构,在 Zephyr 中用于在编译时静态定义外设的属性和连接关系。它替代了传统的、冗长的宏定义和头文件配置,使得硬件描述更加清晰、可移植。
3.2.1 设备树文件体系
Zephyr 的设备树文件主要分为三类,它们共同构成了完整的硬件描述:
| 文件类型 | 扩展名 | 作用 | 示例 |
|---|---|---|---|
| 芯片级 DTSI | .dtsi |
定义 SoC 内部的外设、中断控制器、时钟等通用信息。 | stm32f4.dtsi |
| 板级 DTS | .dts |
定义具体开发板上的外设实例、引脚复用、I2C 地址等。 | nucleo_f411re.dts |
| 板级 DTSI | .dtsi |
被板级 DTS 包含,用于共享相同硬件配置的板卡。 | nucleo_f4.dtsi |
3.2.2 设备树节点与属性
设备树的基本单元是节点(node),每个节点代表一个硬件设备或总线。节点通过属性(property)来描述其特性。以下是一个 I2C 传感器节点的典型示例:
/ {
soc {
i2c1: i2c@40005400 {
compatible = "st,stm32-i2c-v1";
reg = <0x40005400 0x400>;
clocks = <&rcc>;
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
/* 挂载在 I2C1 总线上的传感器 */
bme280: bme280@76 {
compatible = "bosch,bme280";
reg = <0x76>;
status = "okay";
};
};
};
};
compatible:驱动匹配的关键字符串,用于将设备节点与驱动绑定。reg:设备的地址信息。对于 I2C 设备,表示 7 位从机地址(如0x76);对于内存映射设备,表示基地址和大小。status:设备状态,"okay"表示启用,"disabled"表示禁用。#address-cells和#size-cells:定义子节点中reg属性的编码格式。
3.2.3 设备树到 C 宏的转换
在编译过程中,设备树编译器(DTC)将 .dts 文件转换为 .h 头文件。Zephyr 的构建系统会生成类似 zephyr.dts_compiled.h 的文件,其中包含大量的宏定义。例如,上述 bme280 节点会生成:
#define DT_N_S_soc_S_i2c_40005400_S_bme280_76_BUS_NAME "I2C_1"
#define DT_N_S_soc_S_i2c_40005400_S_bme280_76_REG_ADDR 0x76
#define DT_N_S_soc_S_i2c_40005400_S_bme280_76_COMPATIBLE "bosch,bme280"
#define DT_N_S_soc_S_i2c_40005400_S_bme280_76_STATUS okay
驱动开发中,通常使用 DT_INST 系列宏来简化访问,例如 DT_INST_REG_ADDR(0) 获取第 0 个实例的 reg 地址。
3.3 驱动绑定与初始化流程
驱动绑定是将设备树节点与具体的驱动实现关联起来的过程。Zephyr 通过 DEVICE_DT_DEFINE 宏完成这一工作,并在系统启动时按顺序初始化所有设备。
3.3.1 驱动绑定宏:DEVICE_DT_DEFINE
这是驱动注册的核心宏,通常在驱动源文件的末尾调用。其原型如下:
#define DEVICE_DT_DEFINE(node_id, init_fn, pm_device, data_ptr, cfg_ptr, level, prio, api_ptr)
| 参数 | 描述 |
|---|---|
node_id |
设备树节点的标识符,通常使用 DT_DRV_INST(n) 宏。 |
init_fn |
设备初始化函数指针,在系统启动时被调用。 |
pm_device |
电源管理设备指针,通常为 NULL。 |
data_ptr |
指向运行时数据结构体的指针。 |
cfg_ptr |
指向配置数据结构体的指针,通常从设备树中提取。 |
level |
初始化优先级级别,如 POST_KERNEL、APPLICATION。 |
prio |
同级别内的初始化优先级,数值越小越先执行。 |
api_ptr |
指向驱动 API 结构体的指针。 |
一个典型的传感器驱动绑定示例如下:
/* 定义运行时数据结构 */
struct bme280_data {
int16_t temp;
uint16_t press;
uint16_t hum;
};
/* 定义配置数据结构,从设备树提取 */
struct bme280_config {
uint16_t i2c_addr;
const struct device *bus;
};
/* 定义驱动 API 实例 */
static const struct sensor_driver_api bme280_api = {
.sample_fetch = bme280_sample_fetch,
.channel_get = bme280_channel_get,
};
/* 使用 DEVICE_DT_DEFINE 注册驱动 */
DEVICE_DT_DEFINE(DT_DRV_INST(0),
bme280_init,
NULL,
&bme280_data_0,
&bme280_config_0,
POST_KERNEL,
CONFIG_SENSOR_INIT_PRIORITY,
&bme280_api);
3.3.2 初始化流程
Zephyr 的启动过程会按照 level 和 prio 的顺序,依次调用所有已注册设备的 init_fn。初始化流程分为以下几个阶段:
- EARLY:早期初始化,用于架构相关代码和时钟。
- PRE_KERNEL_1:内核初始化第一阶段,启动调度器前。
- PRE_KERNEL_2:内核初始化第二阶段,启动调度器后。
- POST_KERNEL:内核初始化后,大多数外设驱动在此阶段初始化。
- APPLICATION:应用层初始化,用于依赖外设的应用组件。
POST_KERNEL 级别,并设置合理的优先级(如 CONFIG_SENSOR_INIT_PRIORITY,默认 80)。如果传感器依赖 I2C 总线,必须确保 I2C 控制器驱动的初始化优先级高于传感器驱动。
3.3.3 驱动初始化流程图
下图展示了从设备树解析到驱动初始化完成的完整流程:
3.4 本章小结
本章详细介绍了 Zephyr 设备驱动模型的三大核心要素:
- 设备驱动架构:基于
struct device的分层设计,实现了配置与数据的分离,以及 API 的标准化。 - 设备树基础:通过
.dts文件描述硬件,利用compatible属性实现驱动与设备的绑定。 - 驱动绑定与初始化流程:使用
DEVICE_DT_DEFINE宏注册驱动,系统启动时按优先级顺序调用初始化函数。
status 为 "okay",并正确设置传感器的 reg 地址。初始化函数中应优先获取总线设备指针(通过 DEVICE_DT_GET),再进行传感器复位和配置。