3. 开发环境搭建与工程初始化

各位好,我是老周。今天我们来啃Zephyr传感器驱动里最核心的四个API家族。说实话,我刚接触这套框架时也被绕得有点晕——Sensor API、Trigger API、Attribute API、Channel API,名字长得像,功能又交叉。但用熟了你会发现,这四套API其实就是驱动与上层应用之间的“四根管道”。

我个人习惯把传感器驱动想象成一个黑盒子:Channel API定义你能读到什么数据,Attribute API定义你能怎么配置它,Trigger API定义数据什么时候来,而Sensor API则是把这些串起来的调度中枢。下面我们一个一个拆开看。

一句话总结:Sensor API是入口,Channel API是数据出口,Attribute API是配置入口,Trigger API是事件通知机制。

3.1 Sensor API:驱动的“总调度员”

Sensor API是应用层与驱动交互的第一道门。它定义在include/zephyr/drivers/sensor.h里,核心就是一组函数指针结构体——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 (*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负责触发一次采样,channel_get负责把采样结果读出来,attr_set/attr_get负责配置参数,trigger_set负责注册中断回调。

我在项目中遇到过一个问题:某款加速度计在sample_fetch里同时做了采样和滤波,导致调用channel_get时数据已经变了。后来我强制在sample_fetch里只触发硬件采样,把数据锁在驱动内部缓冲区,channel_get再从缓冲区读——这才稳定下来。

我的习惯:sample_fetch只做“触发+等待”,channel_get只做“拷贝+转换”。千万别把业务逻辑混进去,否则调试时你会疯的。

3.2 Channel API:数据到底长什么样?

Channel API说白了就是定义“传感器能输出哪些物理量”。Zephyr用enum sensor_channel来枚举所有可能的通道,从加速度到气压,从温度到步数,应有尽有。

通道枚举 物理量 典型传感器
SENSOR_CHAN_ACCEL_X/Y/Z 三轴加速度 (m/s²) 加速度计
SENSOR_CHAN_GYRO_X/Y/Z 三轴角速度 (rad/s) 陀螺仪
SENSOR_CHAN_MAGN_X/Y/Z 三轴磁场强度 (Gauss) 磁力计
SENSOR_CHAN_PRESS 气压 (kPa) 气压计
SENSOR_CHAN_TEMP 温度 (°C) 温度传感器
SENSOR_CHAN_HUMIDITY 相对湿度 (%) 湿度传感器
SENSOR_CHAN_LIGHT 光照强度 (lux) 环境光传感器
SENSOR_CHAN_PROX 接近检测 (0/1) 接近传感器

你想想看,一个六轴IMU需要同时暴露加速度和角速度通道。在channel_get实现里,我通常用switch-case来分发:

static int my_sensor_channel_get(const struct device *dev,
                                  enum sensor_channel chan,
                                  struct sensor_value *val)
{
    struct my_sensor_data *data = dev->data;

    switch (chan) {
    case SENSOR_CHAN_ACCEL_X:
        sensor_value_from_double(val, data->accel_x);
        break;
    case SENSOR_CHAN_ACCEL_Y:
        sensor_value_from_double(val, data->accel_y);
        break;
    case SENSOR_CHAN_ACCEL_Z:
        sensor_value_from_double(val, data->accel_z);
        break;
    case SENSOR_CHAN_GYRO_X:
        sensor_value_from_double(val, data->gyro_x);
        break;
    /* ... 其他通道 ... */
    default:
        return -ENOTSUP;
    }
    return 0;
}

这里有个细节:struct sensor_value是Zephyr定义的通用数值结构,包含val1和val2两个整型字段,分别表示整数部分和小数部分。驱动里必须用sensor_value_from_double或sensor_value_from_int来填充,上层应用再用sensor_value_to_double解析。

我曾经踩过的坑:直接往sensor_value里写浮点数!Zephyr的sensor_value是定点数格式,val1是整数部分,val2是百万分之一的小数部分。你直接写float进去,上层读出来就是乱码。

3.3 Attribute API:怎么调参数?

Attribute API负责传感器的“旋钮和开关”。比如设置采样率、量程、滤波带宽、工作模式等。Zephyr用enum sensor_attribute来定义通用属性,驱动也可以自定义扩展属性。

常见的通用属性包括:

  • SENSOR_ATTR_SAMPLING_FREQUENCY:采样频率
  • SENSOR_ATTR_FULL_SCALE:量程范围
  • SENSOR_ATTR_LOW_PASS_FILTER:低通滤波截止频率
  • SENSOR_ATTR_OFFSET:偏移校准值
  • SENSOR_ATTR_CALIB_TARGET:校准目标值

我在移植BMA456加速度计时,发现它的量程设置不是直接写数值,而是通过寄存器位域组合实现的。于是我在attr_set里做了个映射表:

static const struct {
    uint16_t range;   /* 单位:g */
    uint8_t reg_val;
} range_map[] = {
    { 2,  0x00 },
    { 4,  0x01 },
    { 8,  0x02 },
    { 16, 0x03 },
};

static int bma456_attr_set(const struct device *dev,
                            enum sensor_channel chan,
                            enum sensor_attribute attr,
                            const struct sensor_value *val)
{
    if (attr != SENSOR_ATTR_FULL_SCALE)
        return -ENOTSUP;

    double target = sensor_value_to_double(val);
    for (int i = 0; i < ARRAY_SIZE(range_map); i++) {
        if (fabs(target - range_map[i].range) < 0.1) {
            /* 写寄存器 */
            return i2c_reg_write_byte(dev, REG_RANGE,
                                      range_map[i].reg_val);
        }
    }
    return -EINVAL;
}

嗯,这里要注意:attr_set的chan参数可以指定通道,比如只设置加速度通道的量程,而不影响陀螺仪。如果你的传感器不支持通道级配置,直接忽略chan参数就行。

3.4 Trigger API:数据来了,快接!

Trigger API是传感器驱动里最“高级”的部分。它允许应用注册一个回调函数,当特定事件发生时(比如数据就绪、阈值触发、自由落体检测),驱动自动调用这个回调。

Zephyr定义了struct sensor_trigger来描述触发条件:

struct sensor_trigger {
    enum sensor_trigger_type type;
    enum sensor_channel chan;
};

type可以是:

  • SENSOR_TRIG_DATA_READY:数据就绪中断
  • SENSOR_TRIG_THRESHOLD:阈值触发(上/下阈值)
  • SENSOR_TRIG_DELTA:变化量触发
  • SENSOR_TRIG_FREEFALL:自由落体检测
  • SENSOR_TRIG_TAP:敲击检测

驱动实现trigger_set时,需要做三件事:

  1. 配置硬件中断寄存器(使能对应中断源)
  2. 保存回调函数指针到驱动私有数据中
  3. 配置GPIO中断引脚(如果传感器通过INT引脚通知MCU)

中断服务例程(ISR)里通常这样写:

static void my_sensor_isr(const struct device *dev,
                           struct gpio_callback *cb,
                           uint32_t pins)
{
    struct my_sensor_data *data = dev->data;
    const struct sensor_trigger *trig = &data->trigger;

    if (data->handler) {
        data->handler(dev, trig);
    }
}

我个人习惯在ISR里只做最轻量的事——调用回调,然后立即返回。真正的数据读取和业务逻辑放在回调函数里,由应用层决定什么时候调用sample_fetch和channel_get。

避坑指南:我曾经在ISR里直接调了sample_fetch,结果因为I2C通信耗时太长,导致系统中断延迟飙升。后来我把sample_fetch挪到回调的工作队列里,问题才解决。

3.5 四套API的协作关系

光讲概念不够直观,我画了一张图来说明它们怎么配合工作:

Sensor 驱动四套 API 协作关系图 应用层 (Application) Sensor API 入口 Channel API channel_get() Attribute API attr_set() / attr_get() Trigger API trigger_set() 硬件传感器 (I2C/SPI/GPIO) 协作流程说明: 1. 应用层通过 Sensor API 的 sample_fetch() 触发采样 2. 通过 Channel API 的 channel_get() 读取数据 3. 通过 Attribute API 的 attr_set() 配置采样率/量程等参数 4. 通过 Trigger API 的 trigger_set() 注册中断回调,实现事件驱动

从这张图可以看得很清楚:Sensor API是唯一的入口,应用层通过它调用sample_fetch,然后内部再分发到Channel、Attribute、Trigger三个子模块。这三个子模块最终都操作硬件寄存器。

3.6 实际移植中的常见问题

最后分享几个我在移植传感器驱动时遇到的典型问题,希望能帮你少走弯路:

问题 原因 解决方案
sample_fetch返回成功但数据全零 硬件采样未完成就读取 在sample_fetch里加入忙等待或延时
channel_get返回乱码 字节序不对或sensor_value格式错误 检查寄存器字节序,使用sys_le16_to_cpu等转换函数
attr_set不生效 寄存器写保护未解除 查看数据手册,先写解锁序列再配置
中断触发但回调不执行 GPIO中断配置错误或回调未保存 检查GPIO引脚复用和中断触发边沿

嗯,其实这些坑说穿了都不复杂,但第一次遇到时确实能卡你好几天。我的建议是:先让sample_fetch+channel_get跑通,再搞Attribute,最后搞Trigger。一步一步来,别想一口吃成胖子。

重要提醒:Trigger API的handler是在中断上下文执行的。不要在handler里做任何阻塞操作(如I2C通信、内存分配、打印调试信息)。如果非要做,用k_work_submit丢到系统工作队列里去。