4、设备驱动模型:设备驱动模型架构、设备结构体、驱动API、设备绑定

Zephyr RTOS 的设备驱动模型是一套统一的软件抽象层,它将硬件外设与上层应用解耦。理解该模型是进行驱动开发的核心前提。本章将深入剖析设备驱动模型的四层架构、核心数据结构 device、标准驱动 API 以及设备与驱动的绑定机制。

4.1 设备驱动模型架构

Zephyr 的设备驱动模型采用分层架构,从上至下依次为:应用层、API 层、驱动层和硬件层。这种分层设计保证了代码的可移植性和模块化。

架构核心思想: 应用通过标准化的 API 调用驱动,驱动通过设备结构体与硬件交互,设备树(DT)负责描述硬件拓扑和配置。

下图展示了设备驱动模型的四层架构及其数据流关系:

Zephyr 设备驱动模型四层架构 Zephyr 设备驱动模型架构 应用层 (Application) 调用标准 API:uart_fifo_fill(), gpio_pin_set(), i2c_write() API 层 (Driver API) struct uart_driver_api, struct gpio_driver_api, struct i2c_driver_api 驱动层 (Driver Instance) 设备结构体实例 + 驱动初始化函数 + 硬件操作函数 硬件层 (Hardware) UART 外设、GPIO 引脚、I2C 控制器、SPI 总线 数据流 应用 ↓ 调用 API ↓ 映射 驱动 ↓ 操作 硬件
架构优势: 应用层无需关心底层硬件细节,更换 SoC 或外设时,只需更换驱动层实现,API 层保持不变,极大提升了代码复用性。

4.2 设备结构体 (struct device)

在 Zephyr 中,每个硬件外设都由一个 struct device 实例表示。该结构体是驱动模型的核心,它聚合了设备的所有关键信息。

struct device 定义在 include/zephyr/device.h 中,主要成员如下:

成员 类型 描述
name const char * 设备名称,用于在系统中唯一标识设备
config const void * 指向设备配置数据的指针(编译时确定,通常来自设备树)
api const void * 指向驱动 API 结构体的指针
data void * 指向设备运行时数据的指针(如状态、缓冲区)
state atomic_t 设备状态标志(初始化状态、电源状态等)
关键设计: config 和 api 是 const 类型,存储在只读区域(ROM/Flash),而 data 存储在可读写区域(RAM)。这种分离设计节省了 RAM 空间,并保证了配置和 API 的安全性。

设备实例通常通过宏 DEVICE_DEFINE 或 DEVICE_DT_DEFINE 在编译时静态定义。以下是一个典型的设备实例定义示例:

// 设备配置数据(来自设备树,存储在 ROM)
struct uart_nrfx_config uart0_config = {
    .base = DT_REG_ADDR(DT_NODELABEL(uart0)),
    .irq_num = DT_IRQN(DT_NODELABEL(uart0)),
};

// 设备运行时数据(存储在 RAM)
struct uart_nrfx_data uart0_data = {0};

// 驱动 API 结构体(存储在 ROM)
struct uart_driver_api uart_nrfx_api = {
    .poll_in = uart_nrfx_poll_in,
    .poll_out = uart_nrfx_poll_out,
    .fifo_fill = uart_nrfx_fifo_fill,
    .fifo_read = uart_nrfx_fifo_read,
};

// 定义设备实例
DEVICE_DEFINE(uart0, "UART_0", &uart0_init, NULL,
              &uart0_data, &uart0_config,
              POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEVICE,
              &uart_nrfx_api);

4.3 驱动 API

驱动 API 是一组函数指针的集合,定义在 struct xxx_driver_api 中。它为上层应用提供了标准化的操作接口。每种设备类型(UART、GPIO、I2C、SPI 等)都有自己独立的 API 结构体。

以下以 UART 和 GPIO 为例,展示典型的驱动 API 结构:

设备类型 API 结构体 关键函数指针
UART struct uart_driver_api poll_in, poll_out, fifo_fill, fifo_read, configure
GPIO struct gpio_driver_api pin_configure, pin_set, pin_get, port_get_raw, port_set_masked_raw
I2C struct i2c_driver_api transfer, configure, recover_bus
SPI struct spi_driver_api transceive, configure

应用层通过 device_get_binding() 获取设备句柄后,即可通过 API 结构体调用驱动函数:

// 获取 UART 设备句柄
const struct device *uart_dev = device_get_binding("UART_0");
if (uart_dev == NULL) {
    return -ENODEV;
}

// 通过 API 结构体发送数据
const struct uart_driver_api *api = uart_dev->api;
api->poll_out(uart_dev, 'A');
注意事项: 驱动 API 结构体中的函数指针必须由驱动开发者实现并赋值。如果某个功能不支持,应设置为 NULL,并在调用时进行有效性检查。

4.4 设备绑定

设备绑定是指将设备树中描述的硬件节点与驱动代码中的 struct device 实例关联起来的过程。Zephyr 通过设备树(Devicetree)和宏展开机制实现自动绑定。

绑定流程分为以下步骤:

  1. 设备树描述: 在 .dts 文件中定义硬件节点,包含寄存器地址、中断号、时钟等属性。
  2. 宏展开: 使用 DEVICE_DT_DEFINE 宏,该宏会自动从设备树中提取配置信息,生成 struct device 实例。
  3. 编译时注册: 所有设备实例在编译时被放入一个特殊的链接器段(.device 段),形成一个设备列表。
  4. 运行时初始化: 系统启动时,内核按优先级遍历设备列表,调用每个设备的初始化函数。
  5. 应用获取: 应用通过 device_get_binding() 按名称查找已初始化的设备。

下图展示了从设备树到设备实例的完整绑定流程:

设备绑定流程 设备绑定流程:从设备树到设备实例 设备树 (.dts) uart0: uart@40002000 { reg = <0x40002000 0x1000>; } 宏展开 DEVICE_DT_DEFINE 提取配置生成实例 设备列表 .device 链接器段 编译时注册 运行时初始化 按优先级调用 init 设置 state 为就绪 应用获取设备 device_get_binding() 绑定关键宏 DEVICE_DT_DEFINE() DEVICE_DT_INST_DEFINE() DT_INST() / DT_NODELABEL()
最佳实践: 推荐使用 DEVICE_DT_DEFINE 系列宏进行设备绑定,它们会自动处理设备树兼容性检查、配置提取和优先级排序,减少手动错误。

4.5 设备绑定示例:UART 驱动

以下是一个完整的 UART 驱动绑定示例,展示了从设备树到驱动实现的完整链路:

// 1. 设备树节点(board.dts)
// uart0: uart@40002000 {
//     compatible = "nordic,nrf-uarte";
//     reg = <0x40002000 0x1000>;
//     interrupts = <39 1>;
//     status = "okay";
// };

// 2. 驱动头文件(uart_nrfx.h)
struct uart_nrfx_config {
    uint32_t base;
    uint32_t irq_num;
};

struct uart_nrfx_data {
    uint8_t rx_buffer[256];
    uint32_t rx_count;
};

// 3. 驱动实现(uart_nrfx.c)
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>

static int uart_nrfx_init(const struct device *dev)
{
    const struct uart_nrfx_config *cfg = dev->config;
    struct uart_nrfx_data *data = dev->data;
    
    // 硬件初始化代码
    printk("UART init at base 0x%x\n", cfg->base);
    return 0;
}

static int uart_nrfx_poll_out(const struct device *dev, unsigned char c)
{
    // 发送一个字节
    return 0;
}

static int uart_nrfx_poll_in(const struct device *dev, unsigned char *c)
{
    // 接收一个字节
    return 0;
}

static const struct uart_driver_api uart_nrfx_api = {
    .poll_out = uart_nrfx_poll_out,
    .poll_in = uart_nrfx_poll_in,
};

// 4. 设备绑定宏
#define UART_NRFX_INIT(n) \
    static const struct uart_nrfx_config uart##n##_config = { \
        .base = DT_REG_ADDR(DT_NODELABEL(uart##n)), \
        .irq_num = DT_IRQN(DT_NODELABEL(uart##n)), \
    }; \
    static struct uart_nrfx_data uart##n##_data; \
    DEVICE_DT_DEFINE(DT_NODELABEL(uart##n), \
                     uart_nrfx_init, NULL, \
                     &uart##n##_data, \
                     &uart##n##_config, \
                     POST_KERNEL, \
                     CONFIG_KERNEL_INIT_PRIORITY_DEVICE, \
                     &uart_nrfx_api)

// 5. 实例化设备
UART_NRFX_INIT(0);
关键点总结:
  • 设备结构体是驱动模型的核心,包含 config、data、api 三个关键指针。
  • 驱动 API 通过函数指针结构体实现多态,每种设备类型有独立的 API 定义。
  • 设备绑定通过设备树和宏展开自动完成,开发者只需关注驱动逻辑实现。
  • 使用 DEVICE_DT_DEFINE 宏可以简化绑定过程,并确保与设备树的一致性。