3. PX4系统架构:uORB消息机制、工作队列与任务调度、设备驱动框架
好,咱们进入第三章。这一章可以说是PX4开发的“内功心法”。你写驱动也好,写应用也好,天天都在跟这三样东西打交道:uORB、工作队列、设备驱动框架。我当年刚接触PX4时,最头疼的就是搞不清它们之间的关系。今天咱们就把这层窗户纸捅破。
3.1 uORB消息机制:模块间的“快递员”
先问个问题:PX4里那么多模块——传感器、姿态估计、位置控制、电机输出——它们之间怎么通信?
答案就是uORB。全称是micro Object Request Broker,说白了就是一个轻量级的发布-订阅消息总线。
3.1.1 发布与订阅模型
uORB的核心思想很简单:
- 发布者:把数据塞进一个“话题”(topic)里,比如传感器数据话题
sensor_accel - 订阅者:去订阅这个话题,有新数据时就能收到通知
打个比方:就像你订阅了一个公众号。作者(发布者)更新文章,你(订阅者)就能收到推送。发布者不需要知道谁在看,订阅者也不需要知道作者是谁。这就是解耦。
关键点:uORB是进程间通信(IPC)机制。不同模块可以跑在不同的线程甚至不同的CPU核心上,通过uORB交换数据。
3.1.2 常用API
写代码时,你主要会用这几个函数:
// 发布数据
orb_advert_t orb_advertise(const struct orb_metadata *meta, const void *data);
int orb_publish(const struct orb_metadata *meta, orb_advert_t handle, const void *data);
// 订阅数据
int orb_subscribe(const struct orb_metadata *meta);
int orb_copy(const struct orb_metadata *meta, int handle, void *buffer);
// 检查更新
int orb_check(int handle, bool *updated);
int orb_poll(struct pollfd *fds, int nfds, int timeout);
我个人习惯用 orb_poll 配合工作队列,这样不会阻塞主循环。后面会讲到。
3.1.3 自定义话题
你写驱动时,很可能需要定义自己的话题。比如我做过一个激光雷达驱动,就定义了一个 lidar_distance 话题。
步骤很简单:
- 在
msg/目录下新建一个.msg文件,定义数据结构 - 运行
make,自动生成C/C++头文件 - 在代码里包含头文件,用
ORB_DECLARE声明话题
// 示例:lidar_distance.msg
uint64_t timestamp # 时间戳
float32 distance_m # 距离,单位米
uint8_t quality # 信号质量 0-100
小技巧:话题名字不要超过32个字符,否则会被截断。我曾经因为这个查了半天bug……
3.2 工作队列与任务调度:别让CPU闲着
PX4跑在实时操作系统(RTOS)上,通常是NuttX。任务调度是RTOS的核心能力。但PX4在RTOS之上又封装了一层——工作队列(Work Queue)。
3.2.1 为什么需要工作队列?
你想想看,如果每个驱动都开一个独立线程,系统里可能有几十个线程在跑。线程切换开销大,而且容易出优先级反转之类的问题。
工作队列的思路是:把任务排队,让一个或几个工作线程去处理。就像你去银行办事,不用每个人都找一个柜员,而是排队等柜员叫号。
3.2.2 工作队列的类型
PX4里主要有两个工作队列:
| 队列名称 | 优先级 | 典型用途 |
|---|---|---|
wq_configurations::HPWorkQueue |
高优先级 | 传感器数据读取、控制输出 |
wq_configurations::LPWorkQueue |
低优先级 | 日志记录、参数更新、非实时任务 |
嗯,这里要注意:高优先级队列里的任务不能阻塞太久,否则会影响飞行控制。我见过有人把文件写入操作放在高优先级队列里,结果导致控制循环抖动……
3.2.3 如何创建工作项?
写驱动时,你通常会继承 ScheduledWorkItem 类:
class MyDriver : public ModuleBase<MyDriver>, public ScheduledWorkItem
{
public:
MyDriver() : ScheduledWorkItem("my_driver", wq_configurations::HPWorkQueue) {}
void Run() override {
if (should_exit()) return;
// 这里写你的周期性任务
read_sensor_data();
publish_data();
// 重新调度自己,比如每10ms执行一次
ScheduleDelayed(10_ms);
}
private:
void read_sensor_data() { /* ... */ }
void publish_data() { /* ... */ }
};
关键点:Run() 函数会被工作队列的线程调用。你不需要自己创建线程,工作队列会帮你调度。
避坑指南:我曾经在 Run() 里调用了 usleep(),结果整个工作队列都被阻塞了。记住:工作项里不要用阻塞调用。如果需要延时,用 ScheduleDelayed() 代替。
3.3 设备驱动框架:写驱动的“标准模板”
PX4的设备驱动框架,说白了就是一套约定俗成的代码结构。你按照这个结构写,别人一看就懂,而且能自动适配PX4的启动、停止、参数管理等机制。
3.3.1 驱动的基本结构
一个典型的PX4驱动包含这几个部分:
- 类定义:继承
ModuleBase和ScheduledWorkItem - 启动/停止接口:实现
start()、stop()、status()等静态方法 - 参数处理:使用
param_t读取用户配置 - uORB发布:把数据发布到标准话题
- 硬件访问:通过I2C、SPI或UART与传感器通信
3.3.2 一个UART驱动的骨架
咱们这课程主要讲UART设备,所以我给个UART驱动的骨架:
class UartSensor : public ModuleBase<UartSensor>, public ScheduledWorkItem
{
public:
UartSensor() :
ScheduledWorkItem("uart_sensor", wq_configurations::HPWorkQueue)
{
// 打开UART设备
_uart_fd = ::open(UART_DEVICE_PATH, O_RDWR | O_NONBLOCK);
// 配置波特率、数据位等
configure_uart();
}
~UartSensor() {
if (_uart_fd >= 0) ::close(_uart_fd);
}
// 模块接口
static int start(int argc, char *argv[]);
static int stop(int argc, char *argv[]);
static int status(int argc, char *argv[]);
void Run() override {
if (should_exit()) return;
// 读取UART数据
uint8_t buf[64];
int nread = ::read(_uart_fd, buf, sizeof(buf));
if (nread > 0) {
// 解析协议
parse_data(buf, nread);
// 发布到uORB
publish_sensor_data();
}
// 继续调度
ScheduleDelayed(10_ms);
}
private:
int _uart_fd{-1};
void configure_uart() { /* 设置波特率、停止位等 */ }
void parse_data(uint8_t *data, int len) { /* 协议解析 */ }
void publish_sensor_data() { /* orb_publish */ }
};
3.3.3 模块注册与命令行
为了让你的驱动能通过 uart_sensor start 这样的命令启动,需要注册模块:
// 在文件末尾
static int uart_sensor_main(int argc, char *argv[])
{
return UartSensor::main(argc, argv);
}
// 在CMakeLists.txt中注册
px4_add_module(
MODULE drivers/uart_sensor
MAIN uart_sensor
STACK_MAIN 2048
STACK_MAX 4096
SRCS
uart_sensor.cpp
DEPENDS
platform__uorb
)
个人经验:我建议你在 status() 方法里打印一些调试信息,比如接收到的数据包数量、错误计数等。这在现场排查问题时特别有用。
3.4 三者关系:一张图说清楚
好,咱们用一张图来总结uORB、工作队列和设备驱动框架的关系:
从这张图可以看得很清楚:
- 硬件层在最底下,你的驱动通过UART/SPI/I2C去读写它
- 驱动层跑在工作队列里,周期性执行
Run()方法 - 驱动解析完数据后,通过uORB发布出去
- 上层的应用模块订阅这些话题,拿到数据做进一步处理
说白了,你的驱动就是硬件和上层应用之间的翻译官。工作队列决定了翻译官什么时候干活,uORB决定了翻译官把消息传给谁。
核心要点:写PX4驱动,你只需要关注三件事:
- 怎么读写硬件(UART/SPI/I2C)
- 怎么解析数据(协议)
- 怎么发布到uORB(话题定义)
剩下的——线程调度、任务优先级、模块生命周期——框架都帮你搞定了。
好,这一章就到这里。记住这张图,后面写驱动时你会反复用到它。
公众号:蓝海资料掘金营,微信deep3321