4、实时操作系统(RTOS)基础:FreeRTOS任务调度、中断管理、信号量与消息队列
各位同学,今天我们聊聊飞控系统的“心脏”——实时操作系统。说白了,飞控就是一台不停做决策的机器,它必须在规定时间内完成所有任务。如果晚了,飞机可能就翻了。我最早接触飞控时,用的是裸机循环,后来发现根本扛不住复杂任务,才转向了 FreeRTOS。今天我就把这块的核心内容掰开揉碎讲给你听。
4.1 任务调度:谁先跑,谁后跑?
FreeRTOS 的任务调度,核心就是“优先级抢占”。什么意思?高优先级的任务可以随时打断低优先级的任务。我习惯把任务分成几个等级:
- 紧急任务:比如传感器数据采集、姿态解算,优先级最高。
- 常规任务:比如控制输出、通信处理,优先级中等。
- 后台任务:比如日志记录、状态显示,优先级最低。
你想想看,如果姿态解算被日志打印打断了,那飞机还能稳吗?肯定不行。所以调度策略必须清晰。
核心机制:每个任务都有一个优先级(0~configMAX_PRIORITIES-1),数值越大优先级越高。调度器永远运行当前就绪态中优先级最高的任务。
举个例子,我写过一个飞控任务创建代码:
// 创建姿态解算任务(优先级最高)
xTaskCreate(
vAttitudeTask, // 任务函数
"Attitude", // 任务名称
256, // 栈深度
NULL, // 参数
5, // 优先级(最高)
NULL // 任务句柄
);
// 创建通信任务(优先级中等)
xTaskCreate(
vCommTask,
"Comm",
128,
NULL,
3,
NULL
);
// 创建日志任务(优先级最低)
xTaskCreate(
vLogTask,
"Log",
64,
NULL,
1,
NULL
);
这里要注意,栈深度一定要算好。我曾经因为栈给太小,导致任务跑飞,查了整整两天才找到原因。嗯,建议每个任务至少给 128 字,复杂任务给 256 字以上。
4.2 中断管理:别让中断“喧宾夺主”
中断是 RTOS 的“快车道”。传感器数据来了,必须立刻响应。但中断服务程序(ISR)里不能做太多事,否则会阻塞其他中断。我个人的经验是:ISR 里只做最轻量的操作,比如设置一个标志位,或者通过消息队列把数据扔给任务处理。
FreeRTOS 提供了专门的中断安全 API:
xQueueSendFromISR():在中断中发送消息xSemaphoreGiveFromISR():在中断中释放信号量portYIELD_FROM_ISR():在中断中触发任务切换
警告:千万不要在 ISR 中调用 vTaskDelay() 或 xQueueReceive() 这类阻塞函数!这会导致系统崩溃。我见过有人这么干,结果整个飞控直接死机。
举个例子,陀螺仪数据中断处理:
void vGyroISR(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint32_t ulData;
// 读取传感器数据
ulData = readGyro();
// 通过队列发送给任务
xQueueSendFromISR(xGyroQueue, &ulData, &xHigherPriorityTaskWoken);
// 如果接收任务优先级更高,触发切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
你看,ISR 里只做了两件事:读数据、发队列。剩下的解算工作交给任务去处理。这样既保证了实时性,又不会阻塞系统。
4.3 信号量:任务间的“红绿灯”
信号量,说白了就是一把锁。用来保护共享资源,或者同步任务执行顺序。FreeRTOS 支持两种信号量:
| 类型 | 用途 | 典型场景 |
|---|---|---|
| 二进制信号量 | 任务同步 | 中断通知任务处理数据 |
| 计数信号量 | 资源管理 | 控制多个外设访问 |
我遇到过一个问题:两个任务同时写一个全局变量,结果数据全乱了。后来用二进制信号量保护起来,问题就解决了。
// 创建信号量
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();
// 任务A:写数据
void vTaskA(void *pvParameters) {
while(1) {
// 等待信号量
if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
// 写共享数据
writeSharedData();
// 释放信号量
xSemaphoreGive(xSemaphore);
}
}
}
// 任务B:读数据
void vTaskB(void *pvParameters) {
while(1) {
if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
readSharedData();
xSemaphoreGive(xSemaphore);
}
}
}
小技巧:信号量的超时时间别设成 portMAX_DELAY,除非你确定不会死锁。我习惯设成 100ms,超时后打印错误日志,方便调试。
4.4 消息队列:任务间的“快递员”
消息队列是 FreeRTOS 中最常用的通信方式。它像一个管道,一个任务往里扔数据,另一个任务从里面取数据。队列是 FIFO(先进先出)的,保证数据顺序不乱。
我设计飞控时,用消息队列串联了所有模块:
- 传感器中断 → 队列1 → 姿态解算任务
- 姿态解算任务 → 队列2 → 控制输出任务
- 控制输出任务 → 队列3 → 通信任务
这样每个任务只关心自己的输入输出,耦合度很低。改一个模块,其他模块不用动。
// 创建队列(每个元素32字节,队列深度10)
QueueHandle_t xQueue = xQueueCreate(10, sizeof(uint32_t));
// 发送任务
void vSenderTask(void *pvParameters) {
uint32_t ulValue = 100;
while(1) {
// 发送数据,超时100ms
if(xQueueSend(xQueue, &ulValue, pdMS_TO_TICKS(100)) != pdPASS) {
// 发送失败,队列满了
vLogError("Queue full!");
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// 接收任务
void vReceiverTask(void *pvParameters) {
uint32_t ulReceived;
while(1) {
// 接收数据,阻塞等待
if(xQueueReceive(xQueue, &ulReceived, portMAX_DELAY) == pdPASS) {
// 处理数据
processData(ulReceived);
}
}
}
关键点:队列深度要根据数据产生速率来定。传感器 1kHz 输出,队列深度至少 10,否则容易丢数据。我一般留 50% 余量。
4.5 知识体系总览
下面这张图是我自己画的,把 FreeRTOS 的核心模块串起来了。你看一眼就能明白它们之间的关系:
这张图你看懂了吗?中断是“触发器”,任务调度是“大脑”,信号量和队列是“血管”。四者配合,才能让飞控系统稳定运行。
4.6 避坑指南
最后,我分享几个实战中踩过的坑:
- 栈溢出:任务栈给太小,运行一段时间就崩了。建议用
uxTaskGetStackHighWaterMark()监控栈使用情况。 - 优先级反转:低优先级任务占着资源,高优先级任务等不到。用互斥信号量(带优先级继承)可以解决。
- 中断嵌套:FreeRTOS 默认中断嵌套深度有限,别在 ISR 里调用太多 API。
- 队列满:发送数据时一定要检查返回值,否则数据丢了都不知道。
我的习惯:每个任务都加一个看门狗喂狗操作。如果任务卡死,看门狗会复位系统。这在飞控中特别重要,毕竟飞机不能在空中死机。
好了,FreeRTOS 的基础就讲到这里。记住,理论是死的,实践才是硬道理。回去把代码跑一遍,遇到问题再回来翻翻这节课。