3. 传感器驱动开发基础
传感器驱动开发,说白了就是让Zephyr内核和你的传感器芯片能「对上话」。我刚开始接触Zephyr的Sensor子系统时,第一反应是——这API怎么这么多?但用熟了就会发现,核心就那么几个东西:驱动接口、触发模式、通道属性、数据回调。今天咱们就把这些掰开揉碎了讲清楚。
3.1 Sensor驱动接口:从probe到fetch
每个传感器驱动都必须实现一组标准接口。Zephyr用struct sensor_driver_api来定义这组契约。我习惯把这个结构体叫做「驱动身份证」——内核不关心你底层是I2C还是SPI,它只认这组函数指针。
// 核心驱动接口结构体
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 (*attr_get)(const struct device *dev,
enum sensor_channel chan,
enum sensor_attribute attr,
struct sensor_value *val);
int (*trigger_set)(const struct device *dev,
const struct sensor_trigger *trig,
sensor_trigger_handler_t handler);
};
这里我重点说三个:
- sample_fetch:发起一次采样。你可以传
SENSOR_CHAN_ALL一次拿所有通道,也可以指定单个通道。我在项目里踩过坑——有些传感器fetch全部通道时耗时太长,后来改成按需fetch,功耗降了30%。 - channel_get:从驱动内部缓冲区拿数据。注意,它不触发硬件操作,只是把上次fetch的结果转出来。
- trigger_set:配置中断触发。这个后面单独讲。
关键点:sample_fetch和channel_get是分离的。fetch负责「去硬件拿」,channel_get负责「把数据给应用」。这种分离设计让驱动可以批量采样、按需读取,非常灵活。
3.2 触发模式 vs 轮询模式:什么时候用哪个?
这是传感器开发里最常被问的问题。我直接给结论:
| 对比维度 | 轮询模式 | 触发模式 |
|---|---|---|
| CPU占用 | 高(一直问「数据好了没?」) | 低(数据好了才通知) |
| 响应延迟 | 取决于轮询间隔 | 微秒级(硬件中断) |
| 代码复杂度 | 简单,while循环搞定 | 需要配置中断、回调、GPIO |
| 适用场景 | 温度/湿度等慢变信号 | 加速度计/陀螺仪等运动检测 |
轮询模式的代码长这样:
// 轮询模式示例:每100ms读一次温度
while (1) {
sensor_sample_fetch(dev, SENSOR_CHAN_ALL);
sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &val);
printk("温度: %d.%d°C\n", val.val1, val.val2);
k_sleep(K_MSEC(100));
}
触发模式则需要先注册中断:
// 触发模式示例:加速度计检测到运动时回调
static void accel_trigger_handler(const struct device *dev,
const struct sensor_trigger *trig)
{
struct sensor_value val;
sensor_sample_fetch(dev, SENSOR_CHAN_ALL);
sensor_channel_get(dev, SENSOR_CHAN_ACCEL_XYZ, &val);
// 处理数据...
}
// 配置触发
struct sensor_trigger trig = {
.type = SENSOR_TRIG_DATA_READY,
.chan = SENSOR_CHAN_ACCEL_XYZ,
};
sensor_trigger_set(dev, &trig, accel_trigger_handler);
我的经验:电池供电的设备,能用触发就别用轮询。我曾经在一个穿戴项目里用轮询读加速度计,电池两天就耗光了。换成触发模式后,续航直接拉到两周。
3.3 Sensor通道与属性:数据怎么组织?
Zephyr用enum sensor_channel来定义数据通道。每个传感器可以输出多个通道的数据。比如一个6轴IMU,通常有:
SENSOR_CHAN_ACCEL_XYZ:三轴加速度SENSOR_CHAN_GYRO_XYZ:三轴陀螺仪SENSOR_CHAN_DIE_TEMP:芯片内部温度
属性(attribute)则是用来配置传感器工作模式的。比如设置采样率、量程、滤波器等:
// 设置加速度计量程为±4g
struct sensor_value range = { .val1 = 4, .val2 = 0 };
sensor_attr_set(dev, SENSOR_CHAN_ACCEL_XYZ,
SENSOR_ATTR_FULL_SCALE, &range);
// 设置输出数据速率为100Hz
struct sensor_value odr = { .val1 = 100, .val2 = 0 };
sensor_attr_set(dev, SENSOR_CHAN_ACCEL_XYZ,
SENSOR_ATTR_SAMPLING_FREQUENCY, &odr);
注意:不是所有传感器都支持所有属性。设置前最好查datasheet,或者先attr_get看看当前值。我遇到过有人给不支持量程设置的传感器设量程,驱动直接返回-ENOTSUP。
3.4 数据回调机制:中断来了怎么办?
触发模式的核心就是回调函数。当传感器硬件产生中断(比如数据准备好、阈值超限),驱动会调用你注册的处理函数。
回调函数的执行上下文很关键:
- 它运行在中断上下文(ISR)中
- 不能调用阻塞函数(如
k_sleep、printk) - 不能做耗时操作(如复杂浮点运算)
- 最佳实践:回调里只做数据搬运,处理逻辑交给线程
我常用的模式是「回调+工作队列」:
// 定义工作项
static struct k_work sensor_work;
// 工作队列处理函数(线程上下文)
static void sensor_work_handler(struct k_work *work)
{
// 这里可以做耗时操作
sensor_sample_fetch(dev, SENSOR_CHAN_ALL);
// 处理数据、更新状态机...
}
// 中断回调(ISR上下文)
static void sensor_irq_handler(const struct device *dev,
const struct sensor_trigger *trig)
{
// 只提交工作项,不做其他事
k_work_submit(&sensor_work);
}
核心原则:ISR里只做「最小必要操作」。把数据解析、滤波、融合等逻辑放到线程里。这能避免中断嵌套导致系统不稳定。
3.5 知识结构总览
下面这张图把Sensor Driver的核心API关系梳理清楚了:
3.6 避坑指南:我踩过的几个坑
- 回调里做printk:我曾经在ISR里直接printk调试,结果系统卡死。后来才知道printk可能触发调度,中断里绝对不能干这事。
- 忘记配置GPIO中断:trigger_set配好了,但GPIO引脚没配成中断模式,传感器死活不触发。检查硬件配置要仔细。
- 通道号搞混:有些传感器ACCEL_XYZ和GYRO_XYZ的通道号是连续的,但索引方式不同。我建议写驱动时先打印所有通道的值确认一下。
- attr_set顺序:部分传感器必须先设量程再设采样率,顺序反了会返回错误。看datasheet的初始化序列很重要。
调试小技巧:用sensor_attr_get把传感器当前配置全部读出来打印,能快速定位配置问题。我每次移植新传感器都先做这个「配置快照」。
嗯,Sensor Driver的核心API就这些。说白了就是三件事:怎么拿数据(fetch/get)、怎么配参数(attr)、怎么响应事件(trigger)。把这套接口用熟了,Zephyr上任何传感器驱动你都能快速上手。