3、传感器驱动开发(I2C):I2C总线协议回顾、PX4 I2C驱动框架、编写一个I2C温度传感器驱动
各位同学,欢迎来到第三章。
这一章我们聊聊I2C传感器驱动。说实话,I2C在飞控里用得特别多——气压计、磁力计、部分IMU,都是走I2C的。你想想看,一个飞控板上可能挂着四五个I2C设备,如果驱动写得不好,总线冲突、数据错乱、启动失败……这些坑我都踩过。
今天我们就拿一个温度传感器来练手。从协议回顾,到PX4的驱动框架,再到手写一个完整驱动,一步步来。
3.1 I2C总线协议回顾
先快速过一遍I2C的核心要点。如果你已经很熟了,可以跳过这节。但我建议你还是扫一眼,因为后面写驱动时,很多细节会跟协议强相关。
3.1.1 物理层与电气特性
- 两线制:SCL(时钟线)和SDA(数据线),都是开漏输出,需要上拉电阻。
- 地址:7位或10位地址。飞控里99%是7位地址,比如0x76、0x77。
- 速率:标准模式100kHz,快速模式400kHz。PX4默认用400kHz,但有些传感器扛不住,得降速。
3.1.2 通信时序
I2C的通信流程其实很简单:
- 主机发送START条件(SCL高时SDA从高变低)
- 主机发送7位地址 + 读写位(0写,1读)
- 从机回复ACK(拉低SDA)
- 传输数据:每字节后跟一个ACK
- 主机发送STOP条件(SCL高时SDA从低变高)
嗯,这里要注意:读操作中间通常要加一个RESTART条件,而不是STOP再START。很多新手在这里写错,导致读出来的数据全是0xFF。
3.1.3 寄存器读写模式
大多数I2C传感器都采用寄存器映射的方式。写一个寄存器:
START + 设备地址(写) + ACK + 寄存器地址 + ACK + 数据 + ACK + STOP
读一个寄存器:
START + 设备地址(写) + ACK + 寄存器地址 + ACK +
RESTART + 设备地址(读) + ACK + 数据 + NACK + STOP
说白了,就是先告诉传感器你要读哪个寄存器,然后再重新发起一次读操作。我刚开始写驱动时,忘了加RESTART,结果读回来的数据永远是上一个寄存器的值。排查了半天,最后用逻辑分析仪才看到问题。
3.2 PX4 I2C驱动框架
PX4的I2C驱动框架,核心就两个东西:I2C基类和I2CSPIDriver基类。我们重点讲I2C基类,因为它是所有I2C驱动的根。
3.2.1 I2C基类
定义在 src/lib/drivers/device/i2c.h 里。关键成员:
_device_id:设备ID,用于uORB消息标识_address:7位I2C地址_bus:总线编号(比如内部总线是1,外部是2)_retries:重试次数,默认3次
核心方法就两个:
int transfer(const uint8_t *send, unsigned send_len,
uint8_t *recv, unsigned recv_len);
int read_reg(uint8_t reg, uint8_t *data, unsigned len);
int write_reg(uint8_t reg, uint8_t data);
transfer是底层接口,直接操作I2C总线。read_reg和write_reg是封装好的便捷函数。我个人习惯是直接用read_reg和write_reg,除非你需要自定义时序。
3.2.2 驱动生命周期
一个标准的PX4 I2C驱动,生命周期是这样的:
- 探测(probe):读取芯片ID寄存器,确认设备存在
- 初始化(init):配置传感器工作模式、量程、采样率
- 循环读取(cycle):定时读取数据,发布到uORB
- 错误处理:通信失败时重试或重启
关键点: probe阶段一定要做。我见过有人跳过probe,结果驱动加载到了错误的地址上,数据全是乱的。probe就是你的第一道防线。
3.2.3 调度方式
PX4支持两种调度方式:
- 轮询模式:在
cycle()里主动调用ScheduleNow()或ScheduleDelayed() - 中断模式:传感器通过DRDY引脚通知,驱动在中断处理中读取
温度传感器一般用轮询就够了,100ms读一次,完全够用。
3.3 编写一个I2C温度传感器驱动
好,理论说完了,我们动手写一个。选一个常见的温度传感器——TMP102。它便宜、简单、寄存器少,非常适合教学。
3.3.1 传感器简介
| 参数 | 值 |
|---|---|
| I2C地址 | 0x48(AD0接地时) |
| 温度寄存器 | 0x00,16位,左对齐 |
| 配置寄存器 | 0x01,控制转换模式 |
| 分辨率 | 12位(0.0625°C) |
| 转换时间 | 典型26ms |
3.3.2 驱动文件结构
在 src/drivers/temperature/tmp102/ 下创建:
CMakeLists.txt:构建文件tmp102.hpp:头文件tmp102.cpp:实现文件
3.3.3 头文件设计
#pragma once
#include <drivers/device/i2c.h>
#include <uORB/topics/sensor_temperature.h>
class TMP102 : public device::I2C
{
public:
TMP102(int bus, int address);
virtual ~TMP102() = default;
int init() override;
int probe() override;
void cycle() override;
private:
int _measure();
float _convert_raw_to_celsius(uint16_t raw);
int _read_reg(uint8_t reg, uint16_t &val);
int _write_reg(uint8_t reg, uint16_t val);
int _retries{3};
orb_advert_t _temperature_pub{nullptr};
};
注意看,我继承了device::I2C,然后重写了init()、probe()和cycle()。这是PX4驱动最标准的写法。
3.3.4 probe实现
int TMP102::probe()
{
uint16_t id = 0;
int ret = _read_reg(0x00, id); // 读温度寄存器,看能否通信
if (ret != PX4_OK) {
PX4_ERR("TMP102 probe failed: no ACK");
return -EIO;
}
// TMP102没有专门的ID寄存器,我们通过读温度值来验证
// 如果读回来的值在合理范围内,认为设备存在
if (id == 0xFFFF || id == 0x0000) {
PX4_ERR("TMP102 probe failed: invalid data");
return -ENODEV;
}
PX4_INFO("TMP102 detected on bus %d", _bus);
return PX4_OK;
}
这里有个小技巧:TMP102没有芯片ID寄存器,所以用读温度值来验证。如果读回来全是0或全是1,说明总线有问题或者地址不对。
3.3.5 init实现
int TMP102::init()
{
int ret = I2C::init(); // 调用基类init,设置总线速率等
if (ret != PX4_OK) {
return ret;
}
// 配置寄存器:连续转换模式,12位分辨率
uint16_t config = 0x60A0; // 连续转换,CR1=0, CR0=0
ret = _write_reg(0x01, config);
if (ret != PX4_OK) {
PX4_ERR("TMP102 config failed");
return ret;
}
// 启动循环,100ms间隔
ScheduleOnInterval(100000); // 单位微秒
return PX4_OK;
}
嗯,这里要注意:ScheduleOnInterval的参数是微秒。100ms就是100000微秒。我刚开始写的时候写成了100,结果驱动跑得跟疯了一样,CPU占用率直接拉满。
3.3.6 cycle实现
void TMP102::cycle()
{
uint16_t raw_temp = 0;
int ret = _read_reg(0x00, raw_temp);
if (ret != PX4_OK) {
PX4_ERR("TMP102 read failed, retrying...");
return;
}
float temp_c = _convert_raw_to_celsius(raw_temp);
// 发布到uORB
sensor_temperature_s report{};
report.temperature = temp_c;
report.timestamp = hrt_absolute_time();
if (_temperature_pub == nullptr) {
_temperature_pub = orb_advertise(ORB_ID(sensor_temperature), &report);
} else {
orb_publish(ORB_ID(sensor_temperature), _temperature_pub, &report);
}
}
float TMP102::_convert_raw_to_celsius(uint16_t raw)
{
// TMP102数据是12位左对齐,右移4位得到12位值
int16_t temp_raw = static_cast<int16_t>(raw) >> 4;
return temp_raw * 0.0625f;
}
3.3.7 私有读写函数
int TMP102::_read_reg(uint8_t reg, uint16_t &val)
{
uint8_t buf[2] = {0};
int ret = read_reg(reg, buf, 2);
if (ret == PX4_OK) {
val = (buf[0] << 8) | buf[1];
}
return ret;
}
int TMP102::_write_reg(uint8_t reg, uint16_t val)
{
uint8_t buf[2] = {
static_cast<uint8_t>(val >> 8),
static_cast<uint8_t>(val & 0xFF)
};
return write_reg(reg, buf, 2);
}
这里我封装了一层,把16位寄存器的读写包装成函数。这样上层代码更干净,也方便以后扩展。
3.4 驱动调试与验证
驱动写完了,怎么验证对不对?我一般按这个顺序来:
- i2cdetect:先确认I2C地址对不对。在nsh终端运行
i2cdetect -b 1,看0x48有没有出现。 - 手动读写:用
i2cget和i2cset直接操作寄存器,验证通信正常。 - 驱动加载:insmod驱动,看probe是否成功。
- uORB监听:用
listener sensor_temperature看数据是否更新。 - 对比验证:用另一个温度计(比如BMP280的温度)对比,看差值是否在合理范围内。
3.5 本章小结
这一章我们做了三件事:
- 回顾了I2C协议的核心要点,特别是寄存器读写时序
- 分析了PX4的I2C驱动框架,重点讲了I2C基类和生命周期
- 手写了一个TMP102温度传感器驱动,从probe到cycle完整实现
说实话,I2C驱动在PX4里算是比较简单的。但简单不代表可以马虎。probe一定要做,错误处理一定要有,数据转换一定要验证。这些习惯养成了,后面写SPI驱动、UART驱动,都会顺手很多。
好,这一章就到这里。代码我已经放在课程仓库里了,你们可以拉下来编译试试。有问题随时在群里问我。