4. 传感器驱动开发(SPI):从协议到实战

各位同学,欢迎来到第四章。这一章我们聊聊SPI传感器驱动。

说实话,SPI在嵌入式世界里太常见了。气压计、IMU、磁力计,很多传感器都走SPI。我这些年调试过的SPI设备,少说也有几十种。踩过的坑嘛...嗯,后面慢慢聊。

4.1 SPI总线协议回顾

先快速过一下SPI的基础。这东西说白了就是四根线:

  • SCLK — 时钟线,由主机控制
  • MOSI — 主机输出,从机输入
  • MISO — 从机输出,主机输入
  • CS — 片选,低电平有效

SPI是同步全双工的。什么意思?就是主机发数据的同时,从机也在发数据。你发一个字节,同时也会收到一个字节。这个特性在驱动开发中很重要,我后面会提到。

4.2 SPI的四种模式

SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定:

模式 CPOL CPHA 采样沿
模式0 0 0 上升沿采样
模式1 0 1 下降沿采样
模式2 1 0 下降沿采样
模式3 1 1 上升沿采样

我个人习惯先看数据手册,确认传感器支持哪种模式。大部分气压计和IMU都用模式0或模式3。选错了模式,读出来的数据全是乱的。我曾经因为这个排查了整整一个下午,最后发现是模式配错了。

4.3 PX4 SPI驱动框架

PX4的SPI驱动框架,说白了就是一套封装好的接口。你不需要直接操作寄存器,框架帮你搞定了底层的收发逻辑。

核心类就两个:

  • SPI — 底层硬件抽象,负责收发数据
  • SPI::Device — 设备抽象,封装了片选、频率、模式等参数

驱动开发时,你主要和SPI::Device打交道。它的构造函数长这样:

SPI::Device(const char *name,
            const char *bus,
            uint32_t frequency,
            spi_mode_e mode,
            uint8_t cs_gpio);

参数说明:

  • name — 设备名称,比如"baro_ms5611"
  • bus — 总线编号,比如"SPI1"、"SPI2"
  • frequency — SPI时钟频率,单位Hz
  • mode — SPI模式,0~3
  • cs_gpio — 片选引脚号
小提示:频率不是越高越好。我见过有人把气压计的SPI频率设到10MHz,结果数据全是乱的。查了手册才发现,这颗芯片最高只支持5MHz。所以,先看手册,再设参数。

4.4 编写一个SPI气压计驱动

好,理论说完了。我们动手写一个实际的驱动。以MS5611气压计为例。

MS5611这颗芯片很经典。它支持SPI和I2C两种接口。我们这里用SPI。

4.4.1 驱动结构

一个完整的SPI驱动,通常包含这几个文件:

  • MS5611.hpp — 头文件,定义类和方法
  • MS5611.cpp — 实现文件
  • MS5611_SPI.cpp — SPI接口实现

我习惯把SPI相关的代码单独放一个文件。这样如果以后要加I2C版本,只需要加一个MS5611_I2C.cpp就行,核心逻辑不用动。

4.4.2 初始化流程

初始化SPI设备,第一步是创建SPI::Device对象:

// MS5611_SPI.cpp
#include "MS5611.hpp"

MS5611::MS5611(const char *bus, uint32_t frequency, spi_mode_e mode, uint8_t cs_gpio)
    : _dev(new SPI::Device("ms5611", bus, frequency, mode, cs_gpio))
{
    // 构造函数
}

然后复位传感器:

bool MS5611::init()
{
    // 复位命令: 0x1E
    uint8_t cmd = 0x1E;
    _dev->write(&cmd, 1);

    // 等待复位完成
    usleep(10000); // 10ms

    // 读取校准系数
    readCalibration();

    return true;
}
注意:复位后一定要等足够的时间。MS5611的复位时间典型值是2.8ms,但我习惯等10ms。为什么?因为不同批次、不同温度下,复位时间会有差异。等久一点,更稳妥。

4.4.3 读取数据

MS5611的读取流程分两步:

  1. 发送转换命令(气压或温度)
  2. 等待转换完成,读取结果

代码实现:

bool MS5611::readPressure(float &pressure)
{
    // 发送气压转换命令 (0x48 对应OSR=4096)
    uint8_t cmd = 0x48;
    _dev->write(&cmd, 1);

    // 等待转换完成 (OSR=4096时约8.22ms)
    usleep(10000);

    // 读取ADC结果 (0x00)
    uint8_t reg = 0x00;
    uint8_t buf[3];
    _dev->writeRead(&reg, 1, buf, 3);

    // 组装24位数据
    uint32_t raw = ((uint32_t)buf[0] << 16) |
                   ((uint32_t)buf[1] << 8)  |
                   (uint32_t)buf[2];

    // 用校准系数计算实际气压值
    pressure = calculatePressure(raw);

    return true;
}

这里有个细节:writeRead函数同时完成写和读。因为SPI是全双工的,你发命令字节的同时,从机也在发数据。但MS5611的协议是先发命令,再读结果,所以分两次操作。

4.4.4 注册到PX4驱动框架

驱动写好了,怎么让PX4认识它?需要注册到驱动框架里:

// 在ms5611_main.cpp中
extern "C" __EXPORT int ms5611_main(int argc, char *argv[]);

int ms5611_main(int argc, char *argv[])
{
    if (argc < 2) {
        PX4_ERR("usage: ms5611 start|stop|status");
        return 1;
    }

    if (strcmp(argv[1], "start") == 0) {
        // 创建驱动实例
        MS5611 *dev = new MS5611("SPI1", 5000000, SPIDEV_MODE0, GPIO_CS_MS5611);
        if (dev->init()) {
            // 注册到uORB
            dev->start();
            PX4_INFO("MS5611 started");
        } else {
            PX4_ERR("MS5611 init failed");
            delete dev;
        }
    }
    // ...
}
核心要点:驱动注册到PX4后,数据会通过uORB消息发布。其他模块(比如EKF)订阅这个消息,就能拿到气压数据了。

4.5 避坑指南

写SPI驱动,有几个坑我经常遇到:

  • 片选时序不对 — 有些芯片要求CS在传输前后保持高电平一段时间。我遇到过一颗气压计,CS拉低后必须等1us才能开始传数据。手册里写得很隐蔽,差点没看到。
  • 字节序问题 — MS5611的数据是大端模式,但有些芯片是小端。读出来数据不对,先检查字节序。
  • 转换时间不够 — 气压计的ADC转换需要时间。等得太短,读出来的是上次的数据。我曾经因为这个,气压值一直不变,还以为是传感器坏了。

4.6 本章知识结构图

下面这张图,帮你理清SPI驱动开发的整体脉络:

SPI传感器驱动开发知识结构 SPI协议基础 PX4 SPI驱动框架 驱动实现 四线制:SCLK/MOSI/MISO/CS 四种模式:CPOL/CPHA 全双工同步通信 SPI::Device类 write/read/writeRead uORB消息发布 初始化:复位+校准 数据读取:命令+等待+读取 注册到PX4框架 常见坑点:片选时序 / 字节序 / 转换时间

这张图把SPI驱动开发的三个层次串起来了。从协议基础,到框架封装,再到具体实现。你写驱动时,脑子里要有这张图。


专注资料整理