4. SPI驱动框架:PX4设备驱动模型、SPI总线驱动接口、SPI设备注册流程
好,咱们进入第四章。这一章我打算把PX4的SPI驱动框架彻底讲透。你可能会问,为什么非要搞懂这个框架?说白了,你写的驱动最终要挂到PX4这个操作系统里跑,不懂它的规矩,代码写得再漂亮也跑不起来。
我个人习惯,学一个新平台的驱动框架,先看它的「骨架」——也就是设备驱动模型。然后再看「血管」——总线接口怎么用。最后看「心脏怎么跳」——设备怎么注册进去。咱们就按这个顺序来。
4.1 PX4设备驱动模型:从顶层看下去
PX4基于NuttX实时操作系统,它的设备驱动模型其实继承自NuttX的框架。但PX4在上面又封装了一层,专门针对飞控场景做了优化。
整个模型分三层:
- 应用层:你写的飞行控制算法、传感器融合模块。它们不直接操作硬件。
- 设备驱动层:咱们现在要写的SPI驱动就在这层。它负责把硬件寄存器操作封装成标准接口。
- 总线层:SPI、I2C、UART这些总线控制器驱动。它们管理物理传输。
嗯,这里要注意:PX4里每个传感器设备,在文件系统中都对应一个设备节点。比如 /dev/icm20689。应用层通过POSIX标准的 open()、read()、ioctl() 来访问它。你想想看,这样设计的好处是什么?
好处就是:上层代码根本不用关心底层是SPI还是I2C。我曾在项目中把一颗IMU从SPI改成了I2C接口,上层代码一行没改,只换了驱动文件。这就是模型的力量。
核心概念:PX4驱动模型 = 设备节点 + 文件操作接口 + 总线抽象
4.2 SPI总线驱动接口:底层怎么干活
SPI总线驱动,说白了就是负责在MOSI/MISO线上收发数据的那层代码。PX4里,SPI总线驱动已经由芯片厂商(STM32、NXP等)实现了,我们不需要重写。但我们必须知道它提供了哪些接口。
最重要的接口就这几个:
| 接口函数 | 作用 | 我常用的场景 |
|---|---|---|
SPI_TRANSFER() |
执行一次SPI传输(全双工) | 读写传感器寄存器 |
SPI_SELECT() |
控制片选信号 | 多设备分时访问 |
SPI_LOCK() |
锁定总线,防止多任务冲突 | 中断中访问SPI时必用 |
SPI_SETFREQUENCY() |
设置SPI时钟频率 | 不同传感器速率不同 |
SPI_SETMODE() |
设置SPI模式(CPOL/CPHA) | 匹配传感器时序要求 |
我曾经踩过一个坑:某款气压计要求SPI模式3(CPOL=1, CPHA=1),我默认用了模式0,结果读出来的数据全是0xFF。查了两天才发现是时序不匹配。所以,拿到一个新传感器,第一件事就是看数据手册里的时序图,确认SPI模式。
小技巧:在调试SPI通信时,我习惯先用逻辑分析仪抓波形。看片选是否拉低、时钟是否稳定、数据位是否对齐。比看代码快多了。
4.3 SPI设备注册流程:把驱动挂进系统
好,现在到了最关键的环节——怎么把你的SPI设备驱动注册到PX4里。这个过程我拆成四步,每一步都有对应的代码。
第一步:定义设备类
在PX4里,每个SPI设备驱动都是一个C++类,继承自 SPI 基类。比如:
class ICM20689 : public SPI {
public:
ICM20689(const char *name, const int bus, const uint32_t device);
virtual ~ICM20689();
virtual int init() override;
virtual int read(unsigned address, void *data, unsigned count) override;
virtual int write(unsigned address, void *data, unsigned count) override;
private:
int probe(); // 检测设备是否存在
};
注意看,read() 和 write() 是虚函数。这意味着你可以重写它们,实现自定义的读写逻辑。我一般会在 read() 里加一个调试打印,方便看原始数据。
第二步:实现探测函数
probe() 函数用来确认传感器是否在线。通常的做法是读一个芯片ID寄存器:
int ICM20689::probe() {
uint8_t id = 0;
int ret = read(REG_WHO_AM_I, &id, 1);
if (ret != OK) {
return -EIO;
}
if (id != ICM20689_WHO_AM_I) {
return -ENODEV;
}
return OK;
}
嗯,这里有个细节:read() 函数内部会自动处理SPI的读写操作。你不需要手动调用 SPI_TRANSFER(),基类已经封装好了。
第三步:实现初始化函数
init() 函数负责配置传感器的工作参数。比如设置量程、采样率、滤波器等:
int ICM20689::init() {
int ret = SPI::init(); // 先调用基类初始化
if (ret != OK) return ret;
ret = probe(); // 检测设备
if (ret != OK) return ret;
// 配置传感器寄存器
write(REG_PWR_MGMT_1, 0x80); // 复位
usleep(10000);
write(REG_PWR_MGMT_1, 0x01); // 唤醒
write(REG_CONFIG, 0x00); // 设置滤波器
write(REG_GYRO_CONFIG, 0x18); // 2000dps
write(REG_ACCEL_CONFIG, 0x18);// 16g
return OK;
}
我建议在 init() 里加一个状态检查循环。比如写一个寄存器后,读回来验证是否写成功。有些传感器上电后需要几十毫秒才能稳定,不加延时直接配置会失败。
第四步:注册到系统
最后一步,在驱动的入口函数里创建设备实例并注册:
extern "C" __EXPORT int icm20689_main(int argc, char *argv[]);
int icm20689_main(int argc, char *argv[]) {
if (argc < 2) {
PX4_ERR("usage: icm20689 start|stop|status");
return -1;
}
if (strcmp(argv[1], "start") == 0) {
// 创建驱动实例
ICM20689 *dev = new ICM20689(SPI_BUS_SENSOR_1, SPIDEV_ICM20689);
if (dev == nullptr) {
PX4_ERR("failed to allocate memory");
return -1;
}
// 初始化并注册到设备文件系统
if (dev->init() != OK) {
PX4_ERR("device init failed");
delete dev;
return -1;
}
// 注册到PX4的uORB主题
dev->Schedule();
return 0;
}
// ... stop, status 处理
}
注意最后一行 dev->Schedule()。这是PX4驱动框架的特色——它会把驱动注册到一个调度器里,周期性调用 Run() 函数来采集数据。你不需要自己写while循环。
避坑指南:我曾经在注册时忘记调用 SPI::init(),结果基类没有正确配置GPIO和时钟,设备一直报错。记住:子类的 init() 必须调用父类的 init(),这是规矩。
4.4 知识体系总览
为了让你更直观地理解整个框架,我画了一张图。它展示了从应用层到硬件层的完整调用链路:
从这张图可以清楚看到:应用层通过POSIX接口访问设备节点,设备节点背后是驱动类,驱动类调用SPI总线接口,最终操作硬件。每一层各司其职,耦合度很低。
我个人觉得,理解这个框架比背代码更重要。框架是固定的,代码是活的。你只要掌握了「设备类 + 探测 + 初始化 + 注册」这个四步流程,任何SPI传感器都能套用。
实战建议:刚开始写驱动时,不要急着写完整功能。先写一个最简单的 probe(),确认能读到芯片ID。这一步通了,后面就顺了。我每次移植新传感器都是这么干的。
好了,这一章的内容就这些。记住:框架是骨架,接口是血管,注册流程是心脏。三样都通了,你的SPI驱动就能在PX4里活起来。
公众号:蓝海资料掘金营,微信deep3321