3. 开发环境搭建与工程初始化
各位好,我是老周。今天我们来啃Zephyr传感器驱动里最核心的四个API家族。说实话,我刚接触这套框架时也被绕得有点晕——Sensor API、Trigger API、Attribute API、Channel API,名字长得像,功能又交叉。但用熟了你会发现,这四套API其实就是驱动与上层应用之间的“四根管道”。
我个人习惯把传感器驱动想象成一个黑盒子:Channel API定义你能读到什么数据,Attribute API定义你能怎么配置它,Trigger API定义数据什么时候来,而Sensor 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再从缓冲区读——这才稳定下来。
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解析。
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时,需要做三件事:
- 配置硬件中断寄存器(使能对应中断源)
- 保存回调函数指针到驱动私有数据中
- 配置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。
3.5 四套API的协作关系
光讲概念不够直观,我画了一张图来说明它们怎么配合工作:
从这张图可以看得很清楚: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。一步一步来,别想一口吃成胖子。
k_work_submit丢到系统工作队列里去。