第七章:驱动与抽象篇

在裸机时代,你是硬件的“独裁者”,你直接读写寄存器 (*REG = 0x55)。 在 Zephyr 时代,你是系统的“指挥官”,你通过对象模型标准接口来指挥硬件。

这一章的目标: 让你彻底忘掉寄存器,掌握 Zephyr 的“万物皆设备”哲学。

🎭 7.1 核心哲学:Zephyr 设备模型 (Device Model)

1. 为什么不能直接操作寄存器?

  • 可移植性灾难: 如果你直接写了 STM32_GPIOA->ODR,你的代码就死在了 STM32 上。换 NXP?重写吧。

  • 资源冲突: 两个线程同时操作一个寄存器,没有内核仲裁,会导致状态错乱。

  • 电源管理: 内核需要知道设备是否在使用,以便自动挂起空闲设备省电。直接操作寄存器会绕过 PM (Power Management) 机制。

2. struct device:内核眼中的“对象”

在 Zephyr 源码 (include/zephyr/device.h) 中,每个硬件外设在运行时都由一个 struct device 结构体实例表示。它包含三个核心部分:

  1. name (名字): 用于调试和查找。

  2. config (只读配置): 指向存储在 Flash (ROM) 中的配置结构体。

    • 内容: 寄存器物理基地址、中断号、I2C 地址、时钟频率等(数据来源:DTS)。

  3. data (运行时数据): 指向存储在 RAM 中的状态结构体。

    • 内容: 信号量、互斥锁、回调函数链表、当前的驱动状态(正在发送/空闲)。

  4. api (虚函数表): [最关键] 指向一组标准函数的指针。

3. 调用栈解剖 (Call Stack Anatomy)

当你调用 gpio_pin_set(dev, ...) 时,发生了什么?

// 1. 应用层 (你的代码)
gpio_pin_set(dev, pin, 1);

// 2. 子系统层 (include/zephyr/drivers/gpio.h)
// 这是一个内联函数,它直接通过 API 指针跳转
static inline int gpio_pin_set(const struct device *dev, ...) {
    const struct gpio_driver_api *api = (const struct gpio_driver_api *)dev->api;
    return api->pin_set(dev, ...); // (修正)
}

// 3. 驱动层 (drivers/gpio/gpio_stm32.c)
// 真正的干活代码,操作寄存器
static int gpio_stm32_pin_set(...) {
    // 写 STM32 的 BSRR 寄存器
}

结论: Zephyr 的驱动调用开销极小(就是一次函数指针跳转),但换来了极大的解耦。

🤝 7.2 标准起手式:_dt_specdevice_is_ready

这是现代 Zephyr (3.x+) 唯一推荐的写法。拒绝使用 device_get_binding

1. _dt_spec (设备树规格结构体)

仅仅拿到 struct device * 是不够的。

  • 对于 GPIO,你需要:设备指针 + 引脚号 + 标志位 (Active Low/Pull Up)。

  • 对于 I2C/SPI,你需要:总线设备指针 + 从机地址 + 频率。

Zephyr 发明了 _dt_spec 结构体来打包这些信息。

2. [专家级代码模板] 初始化检查

90% 的新手 Bug 都是因为没有检查 device_is_ready

/* 1. 获取 Spec (编译时) */
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);

void main(void) {
    /* 2. 检查设备就绪 (运行时)
     * 为什么会失败?
     * - Kconfig 没开驱动 (CONFIG_GPIO=n)
     * - DTS 里 status = "disabled"
     * - 硬件初始化失败 (比如 PLL 锁相环没起来)
     */
    if (!gpio_is_ready_dt(&led)) { // (修正) gpio_is_ready_dt()
        // [专家建议] 不要只 return,要打印日志!
        // LOG_ERR("Device %s is not ready!", led.port->name);
        return;
    }
    
    // ... 安全地使用设备
}

⚡ 7.3 GPIO 驱动深度解析:从轮询到中断

1. 标志位 (Flags) 的艺术

gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE) 中,GPIO_OUTPUT_ACTIVE 是什么意思?

  • GPIO_OUTPUT: 配置为输出。

  • GPIO_ACTIVE: 逻辑电平

    • 如果 DTS 里定义了 GPIO_ACTIVE_LOW (低电平亮灯)。

    • 你代码里写 gpio_pin_set_dt(&led, 1) (设为逻辑 1/开启)。

    • 驱动层会自动把物理引脚拉低 (0V)!

    • 价值: 你的 C 代码永远不需要关心“高电平有效”还是“低电平有效”,代码逻辑永远是正向的。

2. 中断与回调 (Interrupts & Callbacks) - 重难点

Zephyr 的 GPIO 中断处理非常独特,它使用单链表回调机制

为什么? 一个 GPIO 控制器(如 Port A)通常管理 16 或 32 个引脚,但往往共享同一个硬件中断向量(IRQ)。内核需要知道是哪个引脚触发了中断,并调用对应的用户函数。

[专家级实战代码]: (假设 button 已经通过 GPIO_DT_SPEC_GET 获取)

/* 必须是全局或静态的,因为它是链表节点,不能在栈上被销毁 */
static struct gpio_callback button_cb_data;

/* 回调函数 (Running in ISR Context!) */
void button_pressed(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
    // 警告:这是中断上下文!
    // ❌ 禁止:k_sleep(), k_mutex_lock(), I2C 读写, printk (如果没开 Deferred Log)
    // ✅ 推荐:k_sem_give() (通知线程), gpio_pin_toggle() (极简操作)
    
    // 怎么区分是哪个引脚触发的?
    if (pins & BIT(button.pin)) { 
        // 处理逻辑...
    }
}

void init_button(void) {
    
    // 1. 配置中断参数 (边缘触发)
    gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE);

    // 2. 初始化回调节点
    // 将 button_pressed 函数与 BIT(button.pin) 关联
    gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin));

    // 3. 将节点挂载到驱动的回调链表中
    gpio_add_callback(button.port, &button_cb_data);
}

📡 7.4 UART 驱动:异步接收的正确姿势

UART 发送 (printk) 很简单,但接收很难。

  • 轮询 (poll_in): 甚至不能用在生产环境,会丢数据。

  • 中断 (interrupt): 生产环境标准做法。

[架构师模式] Ring Buffer + ISR 在高速串口通信(如 GPS 或 WIFI 模组)中,直接在中断里处理数据是自杀行为。 标准架构: ISR (中断) -> 存入 Ring Buffer -> 信号量通知 -> 线程读取解析。

/* 简化的 Ring Buffer 伪代码思路 */
// (假设已定义 dev, app_ringbuf 和 rx_data_sem)

// 1. 只有在中断里才能调用这些 API
void serial_cb(const struct device *dev, void *user_data)
{
    uint8_t c;
    
    // 检查是否是 RX 中断
    if (!uart_irq_update(dev)) return;

    if (uart_irq_rx_ready(dev)) {
        // 必须循环读,因为硬件 FIFO 可能有多个字节
        while (uart_fifo_read(dev, &c, 1) == 1) {
            // 【关键】极速存入环形缓冲区
            ring_buf_put(&app_ringbuf, &c, 1);
        }
        // 通知处理线程
        k_sem_give(&rx_data_sem);
    }
}

void app_thread(void) {
    // 2. 开启中断
    uart_irq_callback_user_data_set(dev, serial_cb, NULL);
    uart_irq_rx_enable(dev);

    while(1) {
        k_sem_take(&rx_data_sem, K_FOREVER);
        // 从 buffer 取数据处理,这里可以慢悠悠地跑
        // process_data_from_ringbuf();
    }
}

🚌 7.5 总线通信:I2C 与 SPI 的“大杀器”

1. I2C:i2c_write_read 的妙用

I2C 设备最常用的操作是:写寄存器地址,然后读回数据。 如果在多线程环境下,你先调用 i2c_write 再调用 i2c_read,中间可能会被其他线程打断(插入别的 I2C 操作),导致时序错误(Restart 信号丢失)。

Zephyr 提供了原子操作:

// (假设 spec 已经通过 I2C_DT_SPEC_GET 获取)
uint8_t reg = 0x05;
uint8_t val;
// 在一次总线事务中完成:START -> Write(Reg) -> RESTART -> Read(Val) -> STOP
// 中间绝对不会被插队。
i2c_write_read_dt(&spec, &reg, 1, &val, 1);

2. SPI:复杂的 spi_buf_set

SPI 是全双工的,Zephyr 的 SPI API 比较“啰嗦”,因为它支持 Scatter-Gather (分散/聚合) DMA。

实战模板:

// (假设 spi_spec 已经通过 SPI_DT_SPEC_GET 获取)

/* 准备发送数据 */
uint8_t tx_data[] = {0x01, 0x02};
struct spi_buf tx_b = {.buf = tx_data, .len = sizeof(tx_data)};
struct spi_buf_set tx_bufs = {.buffers = &tx_b, .count = 1};

/* 准备接收缓冲区 (SPI 必须同时收发) */
uint8_t rx_data[2];
struct spi_buf rx_b = {.buf = rx_data, .len = sizeof(rx_data)};
struct spi_buf_set rx_bufs = {.buffers = &rx_b, .count = 1};

/* 传输!自动管理 CS 片选引脚 */
spi_transceive_dt(&spi_spec, &tx_bufs, &rx_bufs);

🔧 7.6 进阶话题:Pin Control (pinctrl)

在 STM32CubeMX 里,你通过鼠标把 PA9 设为 USART1_TX。在 Zephyr 里,这是由 Pin Control 子系统管理的。

这是 DTS 的一部分:

/* 硬件定义文件 (pinctrl.dtsi) */
&pinctrl {
    /* 定义状态 'default':UART 正常工作 */
    uart0_default: uart0_default {
        group1 {
            psels = <NRF_PSEL(UART_TX, 0, 6)>, <NRF_PSEL(UART_RX, 0, 8)>;
        };
    };

    /* 定义状态 'sleep':低功耗时,把引脚断开以省电 */
    uart0_sleep: uart0_sleep {
        group1 {
            psels = <NRF_PSEL(UART_TX, 0, 6)>, <NRF_PSEL(UART_RX, 0, 8)>;
            low-power-enable;
        };
    };
};

/* 设备节点 */
&uart0 {
    /* 引用上面的引脚配置 */
    pinctrl-0 = <&uart0_default>;
    pinctrl-1 = <&uart0_sleep>;
    pinctrl-names = "default", "sleep";
};

专家视角: 你通常不需要手动调用 pinctrl API。Zephyr 的驱动程序(UART, I2C 等)会在初始化时自动应用 "default" 状态,在进入休眠时自动应用 "sleep" 状态。这就是 Zephyr 电源管理强大的基础。

🚨 7.7 避坑指南 (The Troubleshooting Guide)

  1. 中断里系统崩了 (Kernel Panic)

    • 原因: 你在回调函数里调用了 k_mutex_locki2c_write (这些函数会睡眠)。

    • 解决: 只有 k_sem_give, k_msgq_put (带 K_NO_WAIT), gpio_set 是在 ISR 里安全的。

  2. SPI/I2C 读不到数据

    • 原因 1: CS/Addr 错。检查 _dt_spec 里的地址。

    • 原因 2: 引脚复用错。检查 pinctrl 定义,是不是把 TX/RX 接反了,或者 MISO/MOSI 搞错了。

    • 原因 3: 时钟没开。有些 SoC 需要在 Kconfig 显式开启总线时钟。

  3. Device not ready 即使 Kconfig 开了

    • 原因: 初始化优先级。如果你在 POST_KERNEL 阶段尝试访问一个在 APPLICATION 阶段才初始化的设备,就会报错。检查驱动的 CONFIG_XX_INIT_PRIORITY

🌟 第七章总结

通过这一章,你已经掌握了:

  1. 设备模型: struct device_dt_spec 是操作硬件的唯一凭证。

  2. GPIO: 如何优雅地处理 Active Level 和中断回调。

  3. 通信总线: 如何使用标准的 I2C/SPI/UART API,以及背后的 ISR/RingBuffer 模式。

  4. PinCtrl: 硬件引脚复用的底层逻辑。

你现在已经具备了**“裸写驱动”的能力。但 Zephyr 的强大之处在于它还有丰富的“中间件”**。

第八章:子系统篇,我们将不再关注底层引脚,而是去看看 Zephyr 提供的文件系统、日志系统和 Shell 命令行。那才是让产品真正“好用”的关键。

 

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐