3. 传感器驱动模型:设备驱动模型、设备树、驱动API与数据回调
好,咱们进入第三章。这一章我打算把Zephyr的传感器驱动模型给你拆开揉碎了讲清楚。说实话,我刚接触Zephyr那会儿,也被它这套东西绕得有点晕。但后来我发现,一旦你理解了它的设计哲学,剩下的就是套路了。
3.1 设备驱动模型(DDM)—— 一切皆设备
Zephyr的设备驱动模型,说白了就是一套统一的管理框架。它把所有的硬件外设都抽象成「设备」,然后通过标准接口来操作它们。你想想看,不管是温度传感器、加速度计,还是I2C、SPI总线,在Zephyr眼里都是一个个的device结构体。
这个模型的核心是struct device。每个设备实例都包含:
- 名称:字符串标识,比如
"bme280" - 配置数据:硬件相关的静态信息,比如I2C地址、GPIO引脚号
- 运行时数据:设备状态、缓存等可变信息
- API函数表:指向具体操作函数的指针
我个人的习惯是,把DDM理解成一个「设备注册中心」。所有驱动在初始化时,都会调用DEVICE_DEFINE宏把自己注册进去。系统启动后,你通过device_get_binding()就能找到它。
核心要点:DDM让驱动开发变成了「填空」—— 你只需要填好配置、实现好API函数,剩下的框架帮你搞定。
3.2 设备树(Devicetree)—— 硬件描述的语言
设备树,嗯,这是很多Zephyr新手的第一道坎。我记得我第一次看.dts文件时,心里想的是:「这什么玩意儿?一堆尖括号和逗号。」
其实设备树就是用来描述硬件拓扑的。它告诉系统:你有哪些外设?挂在哪个总线上?地址是多少?中断号是什么?
一个典型的传感器节点长这样:
/ {
i2c0: i2c@40003000 {
compatible = "nordic,nrf-twim";
reg = <0x40003000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
bme280: bme280@76 {
compatible = "bosch,bme280";
reg = <0x76>;
label = "BME280";
};
};
};
这里的关键是compatible属性。它像一把钥匙,把硬件节点和驱动代码匹配起来。驱动里用DT_INST_FOREACH_STATUS_OKAY宏就能遍历所有匹配的设备。
我的经验:调试设备树时,最常用的命令是west build -t menuconfig查看配置,然后用gen_isr_tables生成的日志确认中断是否正确。我曾经花了一整天,最后发现只是设备树里少写了一个逗号——嗯,这种坑踩过一次就记住了。
3.3 驱动API接口 —— 标准化的操作方式
Zephyr为传感器驱动定义了一套标准的API接口。你不需要自己发明轮子,照着接口实现就行。
核心API就这几个:
| 函数 | 作用 |
|---|---|
sensor_sample_fetch() |
从传感器读取原始数据 |
sensor_channel_get() |
获取某个通道的转换后数据 |
sensor_attr_set() |
设置传感器属性(如采样率、精度) |
sensor_attr_get() |
读取传感器当前属性 |
举个例子,读取BME280的温度和湿度:
struct device *dev = device_get_binding("BME280");
struct sensor_value temp, humidity;
sensor_sample_fetch(dev);
sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &temp);
sensor_channel_get(dev, SENSOR_CHAN_HUMIDITY, &humidity);
printf("温度: %d.%06d °C\n", temp.val1, temp.val2);
printf("湿度: %d.%06d %%\n", humidity.val1, humidity.val2);
你看,代码非常简洁。不管底层是I2C还是SPI,上层调用方式完全一样。这就是标准化的好处。
注意:sensor_value结构体用两个整数表示浮点数。val1是整数部分,val2是小数部分(百万分之一)。别搞反了,我见过有人把val2当整数用,结果数据差了100万倍。
3.4 数据回调机制 —— 让数据主动来找你
轮询方式简单,但效率低。你想想看,如果CPU每秒去问传感器100次:「有数据了吗?」—— 大部分时间都在空转。
回调机制就不一样了。传感器准备好数据后,通过中断通知CPU。驱动里注册一个回调函数,数据一来,系统自动调用它。
实现回调的步骤:
- 在设备树中配置中断引脚
- 驱动里实现中断处理函数
- 注册
sensor_trigger_set()设置触发条件 - 在回调中处理数据
代码示例:
static void sensor_trigger_handler(struct device *dev,
struct sensor_trigger *trigger)
{
struct sensor_value data;
sensor_sample_fetch(dev);
sensor_channel_get(dev, SENSOR_CHAN_ACCEL_X, &data);
printk("加速度X: %d\n", data.val1);
}
void main(void)
{
struct device *dev = device_get_binding("LIS2DH");
struct sensor_trigger trig = {
.type = SENSOR_TRIG_DATA_READY,
.chan = SENSOR_CHAN_ACCEL_XYZ,
};
sensor_trigger_set(dev, &trig, sensor_trigger_handler);
while(1) {
k_sleep(K_SECONDS(1));
}
}
这里要注意,回调函数是在中断上下文执行的。别在里面做耗时操作,比如打印大量日志或者申请内存。我曾经在回调里调了printk,结果系统直接卡死——中断嵌套把自己玩崩了。
避坑指南:回调里只做最轻量级的工作。如果需要复杂处理,用工作队列(work queue)把任务丢到线程上下文去执行。
3.5 把它们串起来 —— 一个完整的驱动流程
好了,我们把今天讲的东西串一下。一个传感器从硬件到应用,经历了这么几步:
- 设备树:描述硬件在哪里、怎么连接
- 驱动初始化:通过DDM注册设备,配置硬件
- API接口:提供标准化的数据访问方式
- 回调机制:实现高效的数据通知
我个人觉得,Zephyr这套模型最大的优点就是「解耦」。应用层开发者不需要知道底层是I2C还是SPI,驱动开发者不需要关心数据最终被谁消费。各司其职,互不干扰。
下一章,我们会动手写一个真实的传感器驱动。到时候你会发现,理解了今天这些概念,写驱动其实就是照着模板填代码。