4. Zephyr 传感器子系统详解
好,咱们进入第一个真正的传感器驱动实战。我选BME280,原因很简单——这芯片太常见了,温湿度气压三合一,价格便宜,资料也多。你在智能家居、气象站、环境监测项目里十有八九会碰到它。
这一章,我会带你从头搭一个完整的I2C驱动。从总线配置、设备树节点编写,到驱动初始化、读写数据,最后把温湿度气压读出来。嗯,咱们不搞虚的,直接上手。
4.1 I2C总线配置:先让总线跑起来
写驱动之前,得先把I2C总线配好。Zephyr里配I2C,说白了就是两步:在设备树里使能控制器,在Kconfig里打开驱动。
我个人习惯先查板子的原理图,看BME280挂在哪组I2C引脚上。比如我手头的nRF52840 DK,I2C1默认在P0.27和P0.26上。那设备树里就得这么写:
// boards/arm/nrf52840dk_nrf52840.overlay
&i2c1 {
compatible = "nordic,nrf-twim";
status = "okay";
clock-frequency = <I2C_BITRATE_FAST>; // 400kHz
bme280@76 {
compatible = "bosch,bme280";
reg = <0x76>;
label = "BME280";
};
};
这里有个坑——地址0x76还是0x77?看BME280的SDO引脚电平。SDO接地是0x76,接VDDIO是0x77。我遇到过好几次,板子焊好了死活读不到数据,最后发现是地址搞反了。所以,先量一下SDO电平,再写地址。
4.2 设备树节点编写:把硬件描述清楚
设备树节点不只是写个地址就完事了。我建议把BME280的初始化参数也放进去,这样驱动启动时可以直接读设备树,不用硬编码。
来看一个更完整的节点:
bme280@76 {
compatible = "bosch,bme280";
reg = <0x76>;
label = "BME280";
status = "okay";
// 过采样配置
osr-t = <2>; // 温度过采样x2
osr-p = <16>; // 气压过采样x16
osr-h = <1>; // 湿度过采样x1
// 滤波器配置
filter = <4>; // IIR滤波器系数
// 工作模式
mode = "normal"; // 正常模式,持续测量
};
你可能会问:这些参数非得放设备树吗?我的经验是,能放设备树的就别硬编码。为啥?因为换板子时改设备树比改C代码安全多了,不会不小心把别的逻辑改坏。
| 参数 | 设备树属性 | 可选值 | 说明 |
|---|---|---|---|
| 温度过采样 | osr-t | 0, 1, 2, 4, 8, 16 | 0表示跳过测量 |
| 气压过采样 | osr-p | 0, 1, 2, 4, 8, 16 | 过采样越高噪声越低 |
| 湿度过采样 | osr-h | 0, 1, 2, 4, 8, 16 | 湿度只有这6档 |
| IIR滤波器 | filter | 0, 2, 4, 8, 16 | 0关闭滤波器 |
| 工作模式 | mode | sleep, forced, normal | normal持续测量 |
4.3 驱动初始化:从设备树到运行时结构
设备树写好了,接下来是驱动代码。Zephyr的驱动模型要求你实现一个init函数,在系统启动时被调用。
先定义设备数据结构:
struct bme280_data {
const struct device *i2c_dev;
uint16_t i2c_addr;
uint8_t osr_t;
uint8_t osr_p;
uint8_t osr_h;
uint8_t filter;
// 校准参数
uint16_t dig_T1;
int16_t dig_T2, dig_T3;
uint16_t dig_P1;
int16_t dig_P2, dig_P3, dig_P4, dig_P5, dig_P6, dig_P7, dig_P8, dig_P9;
uint8_t dig_H1, dig_H3;
int16_t dig_H2, dig_H4, dig_H5, dig_H6;
};
然后写init函数:
static int bme280_init(const struct device *dev)
{
struct bme280_data *data = dev->data;
const struct bme280_config *cfg = dev->config;
// 1. 获取I2C设备
data->i2c_dev = device_get_binding(cfg->i2c_name);
if (!data->i2c_dev) {
return -ENODEV;
}
// 2. 读取校准参数
if (bme280_read_calibration(dev) < 0) {
return -EIO;
}
// 3. 配置过采样和模式
if (bme280_configure(dev) < 0) {
return -EIO;
}
// 4. 软复位,确保芯片处于已知状态
bme280_soft_reset(dev);
printk("BME280 initialized at 0x%02x\n", data->i2c_addr);
return 0;
}
这里有个细节:先复位再配置。我踩过这个坑——芯片上电后可能处于不确定状态,直接写配置寄存器可能不生效。所以init里先发复位命令(0xB6到0xE0寄存器),等10ms再配置。
4.4 I2C读写操作:核心数据通路
I2C读写是驱动的灵魂。Zephyr提供了两套API:i2c_write_read和i2c_burst_read。我个人偏爱i2c_burst_read,因为它一次调用就能连续读多个寄存器,效率高。
来看读温度的例子:
static int bme280_read_temperature(const struct device *dev, int32_t *temp)
{
struct bme280_data *data = dev->data;
uint8_t buf[3];
uint8_t reg = 0xFA; // 温度数据起始寄存器
int32_t adc_T;
// 一次读取3字节温度数据
if (i2c_burst_read(data->i2c_dev, data->i2c_addr, reg, buf, 3) < 0) {
return -EIO;
}
// 组合成20位ADC值
adc_T = ((int32_t)buf[0] << 12) |
((int32_t)buf[1] << 4) |
((int32_t)buf[2] >> 4);
// 用校准参数补偿
*temp = bme280_compensate_temperature(data, adc_T);
return 0;
}
写寄存器更简单:
static int bme280_write_reg(const struct device *dev, uint8_t reg, uint8_t val)
{
struct bme280_data *data = dev->data;
uint8_t buf[2] = {reg, val};
return i2c_write(data->i2c_dev, buf, 2, data->i2c_addr);
}
注意看,写操作是把寄存器地址和数据放在同一个buffer里发出去。这是I2C的标准做法——先发地址字节,再发数据字节。
4.5 数据补偿算法:从ADC到物理量
BME280的ADC原始值不能直接用,得用芯片自带的校准参数做补偿。这些参数在芯片出厂时烧录,每次上电都要读出来。
温度补偿公式(BME280数据手册第23页):
static int32_t bme280_compensate_temperature(struct bme280_data *data, int32_t adc_T)
{
int32_t var1, var2, T;
var1 = ((((adc_T >> 3) - ((int32_t)data->dig_T1 << 1))) *
((int32_t)data->dig_T2)) >> 11;
var2 = (((((adc_T >> 4) - ((int32_t)data->dig_T1)) *
((adc_T >> 4) - ((int32_t)data->dig_T1))) >> 12) *
((int32_t)data->dig_T3)) >> 14;
data->t_fine = var1 + var2;
T = (data->t_fine * 5 + 128) >> 8;
return T; // 单位:0.01摄氏度
}
看着有点晕是吧?我第一次看也懵。说白了就是一组多项式拟合,把ADC值映射到真实温度。你不需要理解每步数学推导,照着数据手册抄就行。但要注意变量类型——BME280的中间计算容易溢出,int32是底线。
4.6 完整驱动框架图
下面这张图把整个驱动的数据流和模块关系画清楚了。你可以看到从应用层到底层硬件的完整路径。
4.7 完整读取流程
把上面所有模块串起来,一次完整的数据读取流程是这样的:
- 应用层调用:
sensor_sample_fetch(dev)触发一次采样 - 驱动层判断:检查芯片状态,确认数据就绪
- I2C读取:连续读取温度(0xFA-0xFC)、气压(0xF7-0xF9)、湿度(0xFD-0xFE)寄存器
- ADC组合:把各字节拼成20位或16位ADC值
- 补偿计算:用校准参数做多项式补偿,得到物理量
- 数据返回:通过
sensor_channel_get返回给应用
4.8 避坑指南
最后,把我这些年踩过的坑列出来,你遇到了直接对照排查:
- 地址错误:SDO引脚电平决定地址,0x76或0x77,别猜,用万用表量
- 校准参数未读:每次上电必须重新读取校准参数,它们不会自动加载
- 模式配置顺序:先配过采样和滤波器,再设模式。反过来可能不生效
- 数据未就绪:读数据前检查状态寄存器的测量完成位(bit 3),别读旧数据
- I2C时钟延展:BME280在测量时会拉低SCL,确保你的I2C控制器支持时钟延展