3. RTOS基础:FreeRTOS任务调度、优先级反转与解决、中断管理、任务间通信
各位同学,咱们今天聊聊FreeRTOS。说实话,我刚开始做嵌入式那会儿,对RTOS是又爱又怕。爱的是它能帮我管理复杂的任务,怕的是调度起来各种玄学问题。但用久了你会发现,RTOS其实就是一套规则,你摸透了它的脾气,它就是你最得力的助手。
3.1 任务调度:谁先跑,谁后跑?
FreeRTOS的任务调度,说白了就是决定哪个任务该上CPU跑。它用的是抢占式调度,配合时间片轮转。
抢占式调度是什么意思?就是高优先级的任务一旦就绪,立马把低优先级的踢下去。我有个项目是做电机控制,电流环任务优先级最高,必须毫秒级响应。有一次我忘了把通信任务优先级调低,结果通信任务一卡,电流环就被抢了,电机直接抖成筛子。嗯,从那以后我再也不敢乱设优先级了。
时间片轮转呢?就是同优先级的任务,每人分一个时间片,轮流跑。比如两个任务都是中等优先级,每个跑10ms,然后切换。这里有个坑——时间片别设太短,否则切换开销太大;也别太长,否则实时性变差。我个人习惯设5-10ms,具体看你的系统时钟节拍。
看个简单例子:
// 创建两个任务
xTaskCreate(vTask1, "Task1", 1000, NULL, 2, NULL);
xTaskCreate(vTask2, "Task2", 1000, NULL, 2, NULL);
void vTask1(void *pvParameters) {
for(;;) {
// 任务1的代码
vTaskDelay(pdMS_TO_TICKS(10)); // 主动让出CPU
}
}
void vTask2(void *pvParameters) {
for(;;) {
// 任务2的代码
vTaskDelay(pdMS_TO_TICKS(10));
}
}
注意那个vTaskDelay,它会让任务进入阻塞态,把CPU让给别的任务。如果你不加这个,同优先级的任务会一直跑,另一个永远没机会。我曾经见过一个新手写的代码,两个任务都没加延时,结果一个任务跑完了另一个还没开始——这就是典型的“饿死”现象。
3.2 优先级反转:一个经典的坑
优先级反转,这词听着挺吓人,其实道理很简单。就是低优先级任务拿着一个资源,高优先级任务等着用,结果中优先级任务插进来把低优先级任务抢了,高优先级任务反而被“反转”成最低优先级。
我举个例子。假设有三个任务:
- 任务A:高优先级,需要访问共享资源
- 任务B:中优先级,纯计算任务
- 任务C:低优先级,拿着共享资源
运行过程是这样的:
- 任务C先跑,拿到了共享资源(比如一个互斥锁)。
- 任务A就绪,抢占了C。但A需要资源,发现被C拿着,只好阻塞等待。
- 任务B就绪,它优先级比C高,直接抢占了C。C被挂起,资源拿不回来。
- 任务A还在等资源,但C被B压着跑不了。A明明优先级最高,却只能干等。
这就是优先级反转。我在一个数据采集项目中遇到过,高优先级的采集任务被堵了整整200ms,数据全丢了。查了半天才发现是中间有个打印任务在捣乱。
3.3 怎么解决?优先级继承与优先级天花板
FreeRTOS提供了两种方案:优先级继承和优先级天花板。说白了就是让低优先级任务临时“升级”,别让高优先级等太久。
优先级继承:当高优先级任务等资源时,持有资源的低优先级任务临时提升到高优先级。等它释放资源,再降回去。这样中优先级任务就抢不了它了。
优先级天花板:更粗暴一点。谁拿到资源,优先级直接提到一个预设的“天花板”值。这个天花板要高于所有可能使用该资源的任务优先级。
在FreeRTOS里,用互斥锁(Mutex)就能自动启用优先级继承机制。信号量(Semaphore)不行,它没有这个功能。
重要提醒:用互斥锁代替二值信号量来保护共享资源。互斥锁自带优先级继承,能有效防止优先级反转。信号量虽然轻量,但在这个场景下不安全。
// 创建互斥锁
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
void HighPriorityTask(void *pvParameters) {
for(;;) {
// 请求互斥锁
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 访问共享资源
// ...
xSemaphoreGive(xMutex);
}
}
}
void LowPriorityTask(void *pvParameters) {
for(;;) {
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 访问共享资源
// ...
xSemaphoreGive(xMutex);
}
}
}
我的经验:互斥锁的等待时间别设成portMAX_DELAY,万一死锁了系统就卡死了。我习惯设一个超时值,比如100ms,超时了就报错,方便调试。
3.4 中断管理:别在中断里干重活
中断管理是RTOS的命脉。FreeRTOS里,中断服务函数(ISR)要遵循一个铁律:快进快出。你想想看,中断是来打断正常任务的,你在里面待太久,其他任务怎么办?
我见过有人把整个数据处理逻辑都写在ISR里,结果中断一触发,系统响应慢了10倍。正确的做法是:ISR里只做最紧急的事,比如读寄存器、清标志位,然后把数据通过队列或信号量发给任务去处理。
FreeRTOS提供了中断安全版本的API,比如xQueueSendFromISR、xSemaphoreGiveFromISR。这些函数不能在普通任务里用,反之亦然。搞混了会出大问题。
// 中断服务函数
void USART_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint8_t data;
// 读取数据
data = USART->DR;
// 发送到队列(中断安全版本)
xQueueSendFromISR(xDataQueue, &data, &xHigherPriorityTaskWoken);
// 如果有更高优先级的任务被唤醒,立即切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
注意:portYIELD_FROM_ISR这个宏很关键。如果队列操作唤醒了一个高优先级任务,它会触发一次上下文切换,让那个任务立即执行。否则,中断返回后可能还是跑原来的低优先级任务,实时性就差了。
3.5 任务间通信:队列、信号量、互斥锁
任务之间怎么传数据?怎么同步?FreeRTOS给了我们三件宝:队列、信号量、互斥锁。
3.5.1 队列:任务间的数据管道
队列就是个先进先出的缓冲区。一个任务往里放数据,另一个任务往外取。我最常用它来传递传感器数据、命令包等。
// 创建队列,每个元素4字节,长度10
QueueHandle_t xQueue = xQueueCreate(10, sizeof(uint32_t));
// 发送任务
void SenderTask(void *pvParameters) {
uint32_t value = 0;
for(;;) {
value++;
xQueueSend(xQueue, &value, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 接收任务
void ReceiverTask(void *pvParameters) {
uint32_t received;
for(;;) {
if(xQueueReceive(xQueue, &received, portMAX_DELAY) == pdTRUE) {
// 处理数据
printf("Received: %lu\n", received);
}
}
}
队列有个好处:它自带阻塞机制。队列满了,发送任务会阻塞;队列空了,接收任务会阻塞。你不用自己写轮询,省心多了。
3.5.2 信号量:轻量级的同步工具
信号量分两种:二值信号量和计数信号量。
- 二值信号量:就像一把钥匙,只能一个人拿。常用于中断和任务之间的同步。比如中断来了,给信号量,任务拿到后处理。
- 计数信号量:可以有多把钥匙。比如管理一个资源池,有5个缓冲区,信号量初始值就是5。任务拿一个,减1;放回去,加1。
// 二值信号量:中断同步
SemaphoreHandle_t xBinarySem = xSemaphoreCreateBinary();
void ISR_Handler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xBinarySem, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void ProcessingTask(void *pvParameters) {
for(;;) {
// 等待中断信号
if(xSemaphoreTake(xBinarySem, portMAX_DELAY) == pdTRUE) {
// 处理中断数据
}
}
}
3.5.3 互斥锁:保护共享资源
互斥锁和信号量很像,但有个关键区别:互斥锁支持优先级继承,而且谁拿谁放,不能在不同任务间传递。它专门用来保护共享资源,防止数据竞争。
我之前说过,用互斥锁代替二值信号量来保护资源。但要注意,互斥锁有递归版本(xSemaphoreCreateRecursiveMutex),允许同一个任务多次获取同一个锁。这在函数嵌套调用时很有用。
| 通信方式 | 用途 | 特点 | 典型场景 |
|---|---|---|---|
| 队列 | 数据传递 | FIFO,支持多元素 | 传感器数据、命令包 |
| 二值信号量 | 同步 | 轻量,无优先级继承 | 中断通知任务 |
| 计数信号量 | 资源管理 | 可计数,无优先级继承 | 管理缓冲区池 |
| 互斥锁 | 资源保护 | 有优先级继承,谁拿谁放 | 保护共享变量、外设 |
避坑指南:我曾经在一个项目里用二值信号量保护共享内存,结果高优先级任务被堵了,查了两天才发现是优先级反转。换成互斥锁后,问题立刻解决。所以,保护资源请用互斥锁,别图省事用信号量。
好了,这一章的内容就到这儿。FreeRTOS的调度、反转、中断、通信,说白了就是一套规则。你理解了规则,就能用好它。下一章咱们聊聊实时控制算法的具体实现,到时候会用到今天讲的知识点。