3、传感器驱动基础:理解I2C/SPI总线协议、PX4传感器驱动模板、数据读取与校准流程
好,咱们进入传感器驱动的正题。这一章,我会带你从底层总线协议开始,一路走到PX4的传感器驱动模板,最后聊聊数据读取和校准的那些坑。说白了,传感器就是飞控的「眼睛」和「耳朵」,驱动写不好,飞控就是瞎子。
3.1 I2C与SPI总线协议:选对总线,事半功倍
先说说I2C和SPI。这两个是嵌入式世界最常用的板级通信协议。我在PX4驱动开发中,90%的传感器都跑在这两条总线上。
3.1.1 I2C:两根线,多设备
I2C用两根线:SCL(时钟)和SDA(数据)。所有设备挂在同一条总线上,靠地址区分。优点是引脚少,布线简单。缺点是速度慢,标准模式只有100kHz,快速模式400kHz。
我个人的习惯:气压计、温度传感器这类更新率要求不高的,用I2C。比如MS5611气压计,我就在项目里用过I2C接口,完全够用。
- 主从架构,飞控通常是主机
- 7位或10位地址,每个设备唯一
- 支持多主机,但实际很少用
- 需要上拉电阻(4.7kΩ典型值)
3.1.2 SPI:四根线,高速率
SPI用四根线:SCLK(时钟)、MOSI(主机输出从机输入)、MISO(主机输入从机输出)、CS(片选)。每个从设备独占一根CS线。速度可以跑到几十MHz。
我建议:IMU(加速度计、陀螺仪)这类需要高更新率的传感器,必须用SPI。PX4默认的IMU驱动,比如BMI088、ICM-20602,都是跑SPI的。为什么?因为IMU数据通常需要1kHz以上的更新率,I2C根本扛不住。
3.1.3 选型对比
| 特性 | I2C | SPI |
|---|---|---|
| 引脚数 | 2(SCL+SDA) | 4(SCLK+MOSI+MISO+CS) |
| 速度 | 100kHz~1MHz | 1MHz~80MHz |
| 多设备 | 地址区分,最多127个 | 片选区分,受GPIO限制 |
| 通信方式 | 半双工 | 全双工 |
| 典型应用 | 气压计、温度传感器 | IMU、磁力计、外部Flash |
3.2 PX4传感器驱动模板:从零开始写一个驱动
好,协议懂了,接下来看看PX4的驱动怎么写。PX4的传感器驱动有固定的模板结构。你想想看,如果每个驱动都自己造轮子,那维护起来得多痛苦?
3.2.1 驱动文件结构
一个典型的PX4传感器驱动包含以下文件:
xxx.cpp:主驱动文件,包含初始化、数据读取、发布逻辑xxx.hpp:头文件,定义类、寄存器地址、参数xxx_params.c:参数定义文件(可选)CMakeLists.txt:编译配置
我记得第一次写PX4驱动时,对着模板看了半天。后来发现,核心就是继承cDev基类,实现init()、probe()、cycle()三个方法。
3.2.2 核心代码骨架
下面是一个简化版的SPI传感器驱动模板。注意看注释,这些都是我实际项目中总结出来的关键点。
// 头文件包含
#include <px4_platform_common/px4_config.h>
#include <px4_platform_common/spi.h>
#include <drivers/drv_sensor.h>
// 传感器类定义
class MySensor : public cDev
{
public:
MySensor(const char *name, const int bus, const uint32_t device);
virtual ~MySensor();
virtual int init() override; // 初始化:配置寄存器、校准
virtual int probe() override; // 探测:验证传感器是否存在
virtual int cycle() override; // 循环:读取数据并发布
private:
int _measure(); // 触发测量
int _collect(); // 收集数据
int _publish(); // 发布到uORB
int _fd; // 文件描述符
uint8_t _reg_addr; // 寄存器地址
float _data[3]; // 原始数据缓冲区
};
// 初始化函数
int MySensor::init()
{
// 1. 打开SPI设备
_fd = px4_open(SPI_BUS, O_RDWR);
if (_fd < 0) {
PX4_ERR("SPI open failed");
return -1;
}
// 2. 配置SPI参数(时钟、模式、位宽)
// 我习惯用SPI模式3,因为大多数IMU都支持
px4_ioctl(_fd, SPI_IOC_WR_MODE, SPI_MODE_3);
px4_ioctl(_fd, SPI_IOC_WR_BITS_PER_WORD, 8);
px4_ioctl(_fd, SPI_IOC_WR_MAX_SPEED_HZ, 10*1000*1000); // 10MHz
// 3. 读取设备ID,验证连接
uint8_t id = read_reg(REG_WHO_AM_I);
if (id != EXPECTED_ID) {
PX4_ERR("Device ID mismatch: 0x%02x", id);
return -1;
}
// 4. 配置传感器(设置量程、滤波器等)
write_reg(REG_CTRL1, 0x0F); // 使能所有轴
return OK;
}
// 数据读取函数
int MySensor::cycle()
{
// 触发一次测量
_measure();
// 等待数据就绪(实际项目中用中断或轮询)
px4_usleep(1000);
// 读取数据
_collect();
// 发布到uORB
_publish();
return OK;
}
probe()函数时,一定要做设备ID校验。我曾经遇到过SPI总线上的两个传感器地址冲突,导致数据错乱。加上ID校验后,问题立刻定位。
3.3 数据读取与校准流程:从原始值到可用数据
驱动写好了,数据也能读出来了。但原始数据能用吗?不能。你想想看,传感器出厂时都有偏差,温度变化也会影响读数。所以必须校准。
3.3.1 数据读取流程
标准的读取流程分三步:
- 触发测量:写寄存器启动一次ADC转换
- 等待就绪:轮询状态寄存器或等待中断
- 读取结果:连续读取多个寄存器,拼成完整数据
我建议:读取时尽量用SPI的连续读模式。比如读取加速度计的X、Y、Z轴数据,一次发送读命令+6个字节,比三次单字节读取快得多。
3.3.2 校准流程
校准说白了就是消除系统误差。PX4的校准流程分为两部分:
| 校准类型 | 方法 | 存储位置 |
|---|---|---|
| 零偏校准 | 静止时采集N组数据,取平均值 | 参数文件(如SENS_ACC_XOFF) |
| 灵敏度校准 | 使用已知参考值,计算比例因子 | 参数文件(如SENS_ACC_XSCALE) |
| 温度补偿 | 在不同温度下采集数据,拟合曲线 | EEPROM或参数文件 |
calibrated_value = (raw_value - offset) * scale其中offset是零偏值,scale是灵敏度系数。
3.3.3 实战中的校准坑
我曾经在某个项目中,加速度计校准后数据还是不准。排查了两天,最后发现是温度补偿没做。传感器在-10°C和40°C下的零偏能差出5%。从那以后,我所有驱动都强制加入温度补偿逻辑。
另一个常见问题是校准顺序。PX4的校准流程是:先做陀螺仪零偏,再做加速度计零偏,最后做加速度计灵敏度。顺序错了,校准结果就是错的。为什么?因为陀螺仪零偏会影响姿态估计,而姿态估计又会影响加速度计的重力分量计算。
3.4 本章知识体系
下面这张图,是我自己总结的传感器驱动开发知识体系。你可以把它当作一个检查清单——写驱动时,对照着看,哪一步漏了。
嗯,这一章的内容就到这里。总线协议选型、驱动模板结构、数据读取和校准流程,这三块是传感器驱动开发的基石。你在实际项目中,肯定会遇到各种奇怪的问题——总线锁死、数据跳变、校准失败。别慌,对照着上面的知识体系,一步步排查,总能找到原因。