1. 课程导入与学习路线

各位同学,欢迎来到这门课的第一章。今天咱们聊聊Zephyr RTOS和传感器生态的底层关系。说实话,我刚开始接触Zephyr时,也被它的设备树和驱动模型绕晕过。但搞懂了这套体系,你会发现——嗯,它其实挺优雅的。

1.1 Zephyr RTOS:不止是个“小Linux”

Zephyr RTOS是Linux基金会旗下的开源实时操作系统。很多人叫它“小Linux”,我个人觉得不太准确。Linux是为通用计算设计的,而Zephyr从骨子里就是为资源受限的嵌入式设备打造的。

它的核心特点,我列几个关键点:

  • 可裁剪内核:最小配置只要几KB RAM,跑在Cortex-M0上都没问题
  • 丰富的驱动框架:传感器、显示、无线协议栈,开箱即用
  • 设备树支持:硬件描述与代码分离,跨平台移植方便
  • 安全机制:支持TLS、DTLS、安全启动,适合物联网场景

我记得第一次用Zephyr做项目,是在一个STM32L4的板子上挂三个传感器。当时最头疼的不是写驱动,而是搞清楚设备树怎么配。后来发现,只要理解了它的分层思想,其实比裸机开发清爽得多。

核心认知:Zephyr不是把Linux搬进MCU,而是用类似Linux的设计哲学,为嵌入式场景重新造了一套轮子。设备树、驱动模型、Kconfig,这些概念都来自Linux,但实现上更轻量、更实时。

1.2 传感器子系统架构:三层分离

Zephyr的传感器子系统,说白了就是三层架构:

  1. 应用层:调用传感器API获取数据,做业务逻辑
  2. 驱动层:实现具体的传感器读写、配置、触发
  3. 硬件抽象层:通过设备树和总线API,屏蔽硬件差异

为什么要这么分?你想想看,如果每个传感器驱动都直接操作I2C寄存器,换一个MCU就得重写一遍——那得多痛苦。Zephyr的做法是:驱动只关心传感器本身的协议,总线操作交给框架。

我画了一张图,帮你理解这个分层关系:

Zephyr 传感器子系统三层架构 应用层 sensor_sample_fetch() / sensor_channel_get() 驱动层 struct sensor_driver_api sample_fetch / channel_get / attr_set / trigger_set 硬件抽象层 设备树绑定 / 总线API (I2C/SPI/GPIO) struct i2c_dt_spec / struct spi_dt_spec 驱动只关心传感器协议,总线操作和引脚配置由设备树和框架处理 换MCU只需改设备树,驱动代码零改动

这张图你看懂了吗?应用层只管调用API,驱动层实现具体逻辑,硬件抽象层负责和MCU打交道。每一层只做自己的事,互不干扰。

1.3 Sensor Driver模型:API与回调

Zephyr的传感器驱动模型,核心是struct sensor_driver_api这个结构体。它定义了一组标准接口:

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 (*trigger_set)(const struct device *dev,
                       const struct sensor_trigger *trig,
                       sensor_trigger_handler_t handler);
};

这四个函数指针,就是驱动必须实现的“四件套”:

接口 功能 调用时机
sample_fetch 触发一次采样,数据存入驱动内部缓冲区 每次需要新数据时
channel_get 从缓冲区读取指定通道的数据 fetch之后调用
attr_set 配置传感器参数(采样率、量程等) 初始化或运行时调整
trigger_set 设置中断触发条件(数据就绪、阈值等) 需要异步通知时

个人经验:写驱动时,我习惯先把sample_fetch和channel_get实现好,这两个是基础。触发功能可以后面再加。曾经有个项目,我一开始就把触发写得特别复杂,结果调试了三天才发现——其实用轮询就够了。别过度设计。

1.4 设备树与传感器绑定:硬件描述的艺术

设备树(Device Tree)是Zephyr里最让我“又爱又恨”的部分。爱的是它让硬件配置变得清晰,恨的是——刚开始学的时候,那语法真让人头大。

简单说,设备树就是用文本描述硬件:MCU有哪些外设、挂载了什么传感器、引脚怎么连的。Zephyr在编译时把这些信息解析成C结构体,驱动通过API读取。

一个典型的传感器设备树节点长这样:

/ {
    sensors {
        compatible = "i2c-devices";
        bme280@76 {
            compatible = "bosch,bme280";
            reg = <0x76>;
            label = "BME280";
            status = "okay";
        };
    };
};

这里的关键字段:

  • compatible:驱动匹配的关键字,驱动里用DEVICE_DT_DEFINE宏绑定
  • reg:I2C地址或SPI片选号
  • label:设备名称,应用层通过它获取设备句柄
  • status:设为"okay"才启用,设为"disabled"则跳过

驱动里怎么获取这些信息?看这段代码:

#define DT_DRV_COMPAT bosch_bme280

static int bme280_init(const struct device *dev)
{
    const struct bme280_config *cfg = dev->config;
    /* cfg->i2c 来自设备树绑定 */
    if (!device_is_ready(cfg->i2c.bus)) {
        return -ENODEV;
    }
    /* 读取设备树中的reg地址 */
    uint16_t addr = cfg->i2c.addr;
    /* 初始化传感器... */
    return 0;
}

DEVICE_DT_DEFINE(DT_INST(0, bosch_bme280),
                 bme280_init,
                 NULL,
                 &bme280_data,
                 &bme280_config,
                 POST_KERNEL,
                 CONFIG_SENSOR_INIT_PRIORITY,
                 &bme280_driver_api);

避坑指南:我曾经在一个项目里,设备树配好了,驱动也写了,但传感器就是没反应。查了两天,最后发现是status = "okay"写成了status = "ok"——Zephyr不认这个。记住,设备树里状态必须是"okay",少一个字母都不行。

1.5 绑定文件:驱动与设备树的桥梁

绑定文件(Binding)是设备树和C代码之间的“翻译官”。它告诉Zephyr:这个传感器的设备树节点里,哪些字段是必须的,哪些是可选的,类型是什么。

绑定文件放在dts/bindings/sensor/目录下,以YAML格式编写。以BME280为例:

# dts/bindings/sensor/bosch,bme280.yaml
description: Bosch BME280 temperature, humidity, pressure sensor

compatible: "bosch,bme280"

include: [i2c-device.yaml]

properties:
    label:
        type: string
        required: true
    reg:
        type: int
        required: true
    measurement-mode:
        type: string
        required: false
        enum:
            - "forced"
            - "normal"

绑定文件的作用:

  • 定义compatible字符串,驱动通过它匹配设备
  • 声明属性类型和是否必须
  • 支持继承(include),复用通用总线配置

我个人建议,写新传感器驱动时,先写绑定文件。为什么?因为绑定文件定义了驱动和设备的“契约”,有了它,后面的代码实现才有依据。我见过不少开发者先写驱动再补绑定,结果发现设备树里少配了字段,又回头改驱动——浪费时间。

1.6 本章小结

这一章我们走了一遍Zephyr传感器生态的骨架:

  • Zephyr RTOS:为嵌入式物联网设计的实时操作系统,可裁剪、驱动丰富
  • 传感器子系统:三层架构,应用-驱动-硬件抽象,各司其职
  • Sensor Driver模型:四个核心API接口,标准化驱动开发
  • 设备树与绑定:硬件描述与代码分离,跨平台移植的基石