2. ChibiOS/RTOS基础:Pixhawk固件底层运行的实时操作系统

做Pixhawk驱动开发,绕不开一个东西——ChibiOS/RTOS。

说白了,它就是飞控固件(ArduPilot和PX4)底层的“管家”。你写的驱动代码,最终都要跑在这个操作系统上面。我刚开始接触Pixhawk时,也想过“能不能跳过RTOS直接操作硬件”?后来发现,这就像盖楼不打地基,迟早要塌。

2.1 为什么Pixhawk选择ChibiOS?

市面上RTOS很多,FreeRTOS、uC/OS、RT-Thread……为什么Pixhawk偏偏选了ChibiOS?

我个人习惯用三个词总结:轻量、实时、可预测。

  • 轻量:ChibiOS内核极小,ROM占用可以控制在几KB。Pixhawk的STM32芯片资源有限,不能浪费。
  • 实时:中断响应时间确定,不会出现“卡一下”的情况。飞控最怕延迟,你想想看,传感器数据晚到1ms,姿态解算就偏了。
  • 可预测:调度策略简单清晰,没有复杂的优先级反转处理。我在项目中遇到过因为RTOS调度抖动导致GPS数据丢包的问题,换成ChibiOS后稳如老狗。

核心认知:ChibiOS不是万能的,但在Pixhawk这个场景下,它是最合适的。它的设计哲学就是“做减法”,把资源留给你的驱动代码。

2.2 线程(Thread)——驱动的基本执行单元

线程是什么?你可以把它想象成一条独立的“任务流水线”。

在Pixhawk里,每个传感器驱动通常跑在一个独立的线程里。比如GPS线程、IMU线程、气压计线程……它们各自忙各自的,互不干扰。

创建线程的代码很简单:

// 定义一个线程栈
static THD_WORKING_AREA(wa_gps_thread, 256);

// 线程入口函数
static THD_FUNCTION(gps_thread, arg) {
    (void)arg;
    while (true) {
        // 读取GPS数据
        gps_read();
        // 让出CPU,别占着不放
        chThdSleepMilliseconds(10);
    }
}

// 启动线程
chThdCreateStatic(wa_gps_thread, sizeof(wa_gps_thread),
                  NORMALPRIO, gps_thread, NULL);

嗯,这里要注意:线程栈大小。我刚开始写驱动时,栈给得太小,结果程序跑着跑着就莫名其妙死机了。调试了两天才发现是栈溢出。建议至少给256字节,如果函数调用层级深,512字节起步。

避坑指南:我曾经在气压计驱动里用了递归函数,栈直接爆了。后来改成迭代,问题解决。记住,RTOS里慎用递归。

2.3 信号量(Semaphore)——线程间的“红绿灯”

信号量,说白了就是一个计数器。用来控制多个线程对共享资源的访问。

在驱动开发中,最常见的用法是:中断服务程序(ISR)发信号,驱动线程收信号

举个例子,SPI数据接收完成时,硬件会触发中断。你在中断里释放一个信号量,驱动线程拿到信号量后就去处理数据。这样中断处理时间极短,不会阻塞其他中断。

// 定义信号量
static binary_semaphore_t spi_sem;

// 初始化
chBSemObjectInit(&spi_sem, false);

// 中断服务程序
void spi_isr(void) {
    // 清除中断标志
    // ...
    // 释放信号量,唤醒驱动线程
    chBSemSignal(&spi_sem);
}

// 驱动线程
static THD_FUNCTION(spi_driver, arg) {
    while (true) {
        // 等待信号量,超时时间设为无限
        msg_t msg = chBSemWaitTimeout(&spi_sem, TIME_INFINITE);
        if (msg == MSG_OK) {
            // 处理SPI数据
            process_spi_data();
        }
    }
}

你可能会问:为什么不用全局变量做标志?因为信号量能解决忙等待的问题。用全局变量,线程要一直轮询检查,浪费CPU。用信号量,线程会进入休眠状态,不占CPU时间。

2.4 互斥锁(Mutex)——保护共享数据

互斥锁和信号量有点像,但用途不同。互斥锁解决的是资源互斥访问问题。

比如,两个线程都要往同一个UART口写数据。如果不加锁,数据会混在一起,变成乱码。

// 定义互斥锁
static mutex_t uart_mutex;

// 初始化
chMtxObjectInit(&uart_mutex);

// 线程A
void thread_a(void) {
    chMtxLock(&uart_mutex);
    uart_send("Hello from A\n");
    chMtxUnlock(&uart_mutex);
}

// 线程B
void thread_b(void) {
    chMtxLock(&uart_mutex);
    uart_send("Hello from B\n");
    chMtxUnlock(&uart_mutex);
}

重要提醒:互斥锁不要和信号量混用。互斥锁有优先级继承机制,能防止优先级反转。而二值信号量没有这个机制。在Pixhawk这种实时性要求高的系统里,保护共享资源请用互斥锁。

我记得有一次,同事用信号量保护I2C总线,结果高优先级线程被低优先级线程阻塞,导致飞控姿态失控。换成互斥锁后,问题消失。

2.5 消息队列(Mailbox)——线程间的“快递员”

消息队列,就是用来在线程之间传递数据的。比信号量更高级,因为它能携带具体的数据内容。

在驱动开发中,我常用消息队列做数据解耦。比如,GPS驱动线程解析出位置数据后,通过消息队列发给导航线程。两个线程不需要知道对方内部实现,只通过队列交互。

// 定义消息队列
static mailbox_t gps_mailbox;
// 消息缓冲区
static msg_t gps_mbox_buffer[8];

// 初始化
chMBObjectInit(&gps_mailbox, gps_mbox_buffer, 8);

// GPS驱动线程发送数据
void gps_driver_thread(void) {
    gps_data_t data;
    while (true) {
        gps_read(&data);
        // 发送消息,超时时间100ms
        msg_t msg = chMBPostTimeout(&gps_mailbox,
                                    (msg_t)&data,
                                    TIME_MS2I(100));
        if (msg != MSG_OK) {
            // 队列满了,处理异常
            handle_overflow();
        }
        chThdSleepMilliseconds(100);
    }
}

// 导航线程接收数据
void navigation_thread(void) {
    msg_t msg;
    while (true) {
        chMBFetchTimeout(&gps_mailbox, &msg, TIME_INFINITE);
        gps_data_t *data = (gps_data_t *)msg;
        update_position(data);
    }
}

这里有个细节:消息队列的深度。我建议根据数据产生速率来定。GPS一般是10Hz,队列深度8就够。但如果传感器速率很高(比如IMU 1kHz),队列深度要相应加大,否则会丢数据。

个人经验:消息队列里的数据尽量用指针传递,不要拷贝整个结构体。尤其是数据量大的时候,拷贝会占用大量CPU时间。我见过有人把一整个图像帧塞进消息队列,结果系统直接卡死。

2.6 知识体系总览

说了这么多,我们来梳理一下。下面这张图展示了ChibiOS核心组件在驱动开发中的关系:

ChibiOS/RTOS 驱动开发核心组件关系图 硬件层(STM32外设:SPI、I2C、UART、GPIO等) 中断服务程序(ISR) ChibiOS/RTOS 核心组件 线程 Thread 信号量 Semaphore 互斥锁 Mutex 消息队列 Mailbox 外设驱动层(GPS、IMU、气压计、磁力计等) 硬件层 中断层 RTOS层 驱动层

从这张图可以看得很清楚:硬件产生中断,中断通过信号量唤醒线程,线程之间用互斥锁保护共享资源,用消息队列传递数据。这就是ChibiOS驱动开发的基本套路。

2.7 总结一下

这一章我们聊了ChibiOS的四个核心概念:

组件 用途 驱动开发中的典型场景
线程(Thread) 独立的任务执行单元 每个传感器一个独立线程
信号量(Semaphore) 中断与线程之间的同步 SPI/I2C数据接收完成通知
互斥锁(Mutex) 保护共享资源 UART/I2C总线互斥访问
消息队列(Mailbox) 线程间传递数据 传感器数据从驱动线程发往应用线程

这些概念,说白了就是RTOS的“四件套”。你掌握了它们,写驱动就有了底气。下一章我们会深入DMA,看看怎么让数据传输更快、更省CPU。但前提是——你得先把这四件套练熟。

一句话总结:线程是骨架,信号量是门铃,互斥锁是门禁,消息队列是快递。把这四个东西用好,你的驱动代码就能在Pixhawk上跑得稳、跑得快。

专注资料整理