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时钟频率,单位Hzmode— SPI模式,0~3cs_gpio— 片选引脚号
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;
}
4.4.3 读取数据
MS5611的读取流程分两步:
- 发送转换命令(气压或温度)
- 等待转换完成,读取结果
代码实现:
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(®, 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;
}
}
// ...
}
4.5 避坑指南
写SPI驱动,有几个坑我经常遇到:
- 片选时序不对 — 有些芯片要求CS在传输前后保持高电平一段时间。我遇到过一颗气压计,CS拉低后必须等1us才能开始传数据。手册里写得很隐蔽,差点没看到。
- 字节序问题 — MS5611的数据是大端模式,但有些芯片是小端。读出来数据不对,先检查字节序。
- 转换时间不够 — 气压计的ADC转换需要时间。等得太短,读出来的是上次的数据。我曾经因为这个,气压值一直不变,还以为是传感器坏了。
4.6 本章知识结构图
下面这张图,帮你理清SPI驱动开发的整体脉络:
这张图把SPI驱动开发的三个层次串起来了。从协议基础,到框架封装,再到具体实现。你写驱动时,脑子里要有这张图。