4. 传感器驱动模型:PX4驱动分层与设备注册探测
好,咱们今天聊聊PX4的传感器驱动模型。说实话,这部分内容是我当年刚接触PX4时最头疼的一块。代码结构看着挺清晰,但真要自己写一个驱动,总觉得无从下手。后来我花了一周时间,把整个I2C磁力计的驱动流程走了一遍,才算真正摸透了。
说白了,PX4的驱动模型就是一套标准化的“接入流程”。不管你是I2C、SPI还是UART的传感器,都得按照这个流程来。这样做的好处很明显——代码复用率高,维护起来也省心。
4.1 PX4驱动分层架构
先看整体架构。PX4的驱动分三层,我习惯这么理解:
- 总线层(Bus):负责和硬件打交道。读写寄存器、控制片选、处理中断,这些都是它的活。
- 设备层(Device):封装具体的传感器操作。比如读陀螺仪数据、配置加速度计量程。
- 驱动层(Driver):对接PX4的uORB消息系统。把传感器数据打包成话题,供飞控使用。
嗯,这里要注意:三层之间是单向依赖的。驱动层调用设备层,设备层调用总线层。反过来不行。
核心原则:总线层只负责“怎么读写”,设备层负责“读写什么”,驱动层负责“数据怎么用”。各司其职,别越界。
4.2 总线接口:I2C、SPI、UART
这三种总线,我在项目中都踩过坑。一个一个说。
4.2.1 I2C总线
I2C的特点是引脚少(SCL、SDA),但速度慢。PX4里I2C驱动通常用于磁力计、气压计这类低速传感器。
我曾经在一个项目中,I2C总线老是超时。查了半天,发现是总线电容太大,上拉电阻没选对。后来换了4.7kΩ的电阻,问题解决。
// I2C设备注册示例
class BMI088_I2C : public BMI088
{
public:
BMI088_I2C(const I2CSPIDriverConfig &config);
// ...
};
// 在探测函数中
int BMI088_I2C::probe()
{
// 读取芯片ID
uint8_t id = 0;
int ret = read(REG_CHIP_ID, &id, 1);
if (ret != OK || id != EXPECTED_ID) {
PX4_ERR("chip id mismatch: %02x", id);
return -EIO;
}
return OK;
}
4.2.2 SPI总线
SPI速度快,全双工,适合IMU这类高速传感器。但SPI的接线多(MOSI、MISO、SCLK、CS),每个设备还要独占一个片选。
我记得有一次调试SPI陀螺仪,数据总是跳变。用示波器一看,MISO线上的信号有毛刺。原来是SPI时钟频率设得太高,降了一档就好了。
小技巧:SPI通信时,片选信号拉低后,最好加一小段延时再开始传数据。有些传感器对片选建立时间有要求。
4.2.3 UART总线
UART常用于GPS、激光雷达这类外部设备。PX4里UART驱动相对简单,主要是配置波特率、数据位、停止位。
你想想看,UART没有时钟线,全靠双方约定速率。所以波特率必须一致,否则数据全是乱码。我刚开始做时,就犯过这个低级错误。
4.3 设备注册与探测流程
这部分是驱动开发的核心。PX4有一套标准的探测流程,我画了个图,方便你理解:
这个流程看着简单,但每个步骤都有讲究。我重点说说probe()函数。
4.3.1 probe()函数怎么写
probe()的核心任务就是确认设备是否存在、通信是否正常。通常的做法是读芯片ID寄存器。
int MySensor::probe()
{
// 第一步:复位传感器(可选)
write(REG_RESET, 0xB6);
usleep(10000); // 等10ms
// 第二步:读芯片ID
uint8_t chip_id = 0;
int ret = read(REG_CHIP_ID, &chip_id, 1);
if (ret != OK) {
PX4_ERR("read chip id failed");
return -EIO;
}
// 第三步:验证ID
if (chip_id != MY_SENSOR_CHIP_ID) {
PX4_ERR("chip id mismatch: got 0x%02x, expected 0x%02x",
chip_id, MY_SENSOR_CHIP_ID);
return -EINVAL;
}
PX4_INFO("sensor detected, chip id: 0x%02x", chip_id);
return OK;
}
避坑指南:我曾经在probe()里忘了加延时,结果复位后立刻读ID,读回来的全是0xFF。后来加了10ms延时,问题解决。记住:传感器复位需要时间,别急着读。
4.3.2 设备注册的完整流程
从module_start()到设备真正开始工作,中间还有几个关键步骤:
- 解析参数:从启动脚本中获取总线号、设备地址、频率等参数。
- 创建驱动实例:调用new操作符,分配内存。
- 调用init():初始化硬件,配置寄存器。
- 调用probe():确认设备存在。
- 注册到调度器:让驱动进入PX4的任务调度循环。
- 开始数据采集:周期性读取传感器数据,发布到uORB。
嗯,这里有个细节:init()和probe()的顺序。有些驱动把初始化放在probe()里,有些分开。我个人习惯把硬件配置放在init()里,probe()只做通信验证。这样职责更清晰。
4.4 实战:写一个I2C磁力计驱动
光说不练假把式。咱们以AK09916磁力计为例,看看完整的驱动怎么写。
// ak09916_i2c.cpp
class AK09916_I2C : public AK09916
{
public:
AK09916_I2C(const I2CSPIDriverConfig &config);
virtual ~AK09916_I2C() = default;
static I2CSPIDriverBase *instantiate(const I2CSPIDriverConfig &config,
int runtime_instance);
static int print_usage(const char *reason);
private:
int probe() override;
int init() override;
// 数据采集
void RunImpl() override;
};
// 探测函数
int AK09916_I2C::probe()
{
// AK09916的I2C地址是0x0C
uint8_t who_am_i = 0;
int ret = read(REG_WIA, &who_am_i, 1);
if (ret != OK || who_am_i != 0x48) {
PX4_ERR("AK09916 not found: WIA=0x%02x", who_am_i);
return -EIO;
}
PX4_INFO("AK09916 detected");
return OK;
}
// 初始化函数
int AK09916_I2C::init()
{
int ret = I2C::init(); // 先调父类init
if (ret != OK) {
return ret;
}
// 配置传感器:连续测量模式,100Hz
write(REG_CNTL2, 0x10);
usleep(10000);
return OK;
}
// 数据采集
void AK09916_I2C::RunImpl()
{
// 读取磁力计数据
uint8_t buf[6];
read(REG_HXL, buf, 6);
// 解析数据
int16_t mx = (buf[1] << 8) | buf[0];
int16_t my = (buf[3] << 8) | buf[2];
int16_t mz = (buf[5] << 8) | buf[4];
// 发布到uORB
sensor_mag_s mag_data = {};
mag_data.x = mx;
mag_data.y = my;
mag_data.z = mz;
mag_data.timestamp = hrt_absolute_time();
_mag_pub.publish(mag_data);
}
个人经验:写驱动时,我习惯在probe()成功和失败时都打印日志。这样调试时一眼就能看出设备有没有被正确识别。别小看这几行日志,关键时刻能救命。
4.5 总结一下
PX4的驱动模型,说白了就是一套标准化的“接入流程”。不管什么传感器,都得走probe() -> init() -> RunImpl()这条路。你只要把这三步写好,驱动就基本成型了。
我刚开始做驱动开发时,总想着“我这个传感器特殊,得用特殊方法”。后来发现,老老实实按PX4的套路来,反而最省事。框架已经帮你处理好了调度、同步、错误处理这些麻烦事,你只需要关注传感器本身的操作。
嗯,今天就聊到这儿。下次咱们聊聊传感器校准,那又是另一番天地了。
公众号:蓝海资料掘金营,微信deep3321