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接口,完全够用。

I2C关键特性:
  • 主从架构,飞控通常是主机
  • 7位或10位地址,每个设备唯一
  • 支持多主机,但实际很少用
  • 需要上拉电阻(4.7kΩ典型值)
避坑指南: 我曾经遇到过I2C总线锁死的问题。原因是某个从设备异常拉低了SDA线。解决办法是在驱动中加入总线恢复机制——连续发送9个时钟脉冲,让从设备释放总线。

3.1.2 SPI:四根线,高速率

SPI用四根线:SCLK(时钟)、MOSI(主机输出从机输入)、MISO(主机输入从机输出)、CS(片选)。每个从设备独占一根CS线。速度可以跑到几十MHz。

我建议:IMU(加速度计、陀螺仪)这类需要高更新率的传感器,必须用SPI。PX4默认的IMU驱动,比如BMI088、ICM-20602,都是跑SPI的。为什么?因为IMU数据通常需要1kHz以上的更新率,I2C根本扛不住。

注意: SPI没有标准的应答机制。你发送数据后,从设备是否收到,全靠你自己在协议层判断。我踩过这个坑——某次SPI通信时,CS信号毛刺导致数据错位,飞控姿态解算直接炸了。

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 数据读取流程

标准的读取流程分三步:

  1. 触发测量:写寄存器启动一次ADC转换
  2. 等待就绪:轮询状态寄存器或等待中断
  3. 读取结果:连续读取多个寄存器,拼成完整数据

我建议:读取时尽量用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 本章知识体系

下面这张图,是我自己总结的传感器驱动开发知识体系。你可以把它当作一个检查清单——写驱动时,对照着看,哪一步漏了。

传感器驱动开发知识体系 总线协议选择 I2C:低速、多设备、2线 SPI:高速、独占片选、4线 PX4驱动模板 init() → probe() → cycle() 继承cDev,实现三个核心方法 数据读取流程 触发测量 → 等待就绪 → 读取结果 连续读模式提升效率 校准流程 零偏校准 → 灵敏度校准 → 温度补偿 校准顺序影响最终精度 发布到uORB sensor_accel/sensor_gyro/sensor_baro 每个环节都踩过坑,每个坑都是经验

嗯,这一章的内容就到这里。总线协议选型、驱动模板结构、数据读取和校准流程,这三块是传感器驱动开发的基石。你在实际项目中,肯定会遇到各种奇怪的问题——总线锁死、数据跳变、校准失败。别慌,对照着上面的知识体系,一步步排查,总能找到原因。


专注资料整理