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的传感器子系统,说白了就是三层架构:
- 应用层:调用传感器API获取数据,做业务逻辑
- 驱动层:实现具体的传感器读写、配置、触发
- 硬件抽象层:通过设备树和总线API,屏蔽硬件差异
为什么要这么分?你想想看,如果每个传感器驱动都直接操作I2C寄存器,换一个MCU就得重写一遍——那得多痛苦。Zephyr的做法是:驱动只关心传感器本身的协议,总线操作交给框架。
我画了一张图,帮你理解这个分层关系:
这张图你看懂了吗?应用层只管调用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接口,标准化驱动开发
- 设备树与绑定:硬件描述与代码分离,跨平台移植的基石