3、实时操作系统(RTOS)基础:FreeRTOS任务调度、中断管理、信号量与互斥锁
好,我们直接进入正题。这一章讲的是RTOS,具体说就是FreeRTOS。很多刚入行的朋友觉得RTOS很神秘,其实说白了,它就是一个帮你管理CPU时间的管家。你想想看,在显微镜系统里,既要处理图像采集,又要响应按键,还要控制电机对焦,单靠一个超级循环,迟早要出问题。
3.1 任务调度:谁先跑,谁后跑
任务调度是RTOS的核心。我习惯把任务想象成排队打饭的人。CPU就是那个打饭阿姨,一次只能服务一个人。FreeRTOS用的是抢占式调度,什么意思?就是优先级高的人可以插队。
核心概念:每个任务都有一个优先级,数值越大优先级越高。高优先级任务就绪时,会立即抢占低优先级任务的CPU使用权。
来看一个典型的任务创建代码:
// 创建图像采集任务,优先级设为5
xTaskCreate(
ImageCaptureTask, // 任务函数
"Image Capture", // 任务名称
1024, // 栈深度(字)
NULL, // 参数
5, // 优先级
NULL // 任务句柄
);
// 创建电机控制任务,优先级设为3
xTaskCreate(
MotorControlTask,
"Motor Control",
512,
NULL,
3,
NULL
);
我在项目中遇到过一个问题:图像采集任务优先级设得太高,结果电机控制任务一直抢不到CPU,导致对焦动作卡顿。后来我把电机控制任务的优先级也提到4,才解决了。嗯,这里要注意,优先级不是越高越好,要平衡。
3.2 中断管理:紧急事件的处理
中断,说白了就是CPU正在干活,突然有人敲门说「有急事」。比如显微镜的触发信号来了,必须立刻响应。
FreeRTOS的中断管理有个重要原则:中断服务函数(ISR)里尽量少做事。为什么?因为ISR会打断任务调度,你如果在里面做太多事,整个系统的实时性就崩了。
我的习惯:ISR里只做两件事——清除中断标志,然后通过队列或信号量通知任务去处理。这样ISR执行时间极短,任务调度不受影响。
举个例子:
// 中断服务函数
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 清除中断标志
EXTI_ClearITPendingBit(EXTI_Line0);
// 发送信号量通知任务
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
// 如果有更高优先级任务就绪,立即切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
我曾经踩过一个坑:在ISR里直接调用了printf打印调试信息。结果打印一次要好几毫秒,系统直接卡死。后来才意识到,ISR里绝对不能做阻塞操作。
3.3 信号量:资源的钥匙
信号量,你可以把它想象成一把钥匙。任务想用某个资源,就得先拿到钥匙。用完了再还回去。
FreeRTOS提供了两种信号量:
| 类型 | 特点 | 典型用途 |
|---|---|---|
| 二值信号量 | 只有0和1两种状态 | 中断与任务同步 |
| 计数信号量 | 可以有多个资源 | 管理缓冲池 |
二值信号量最常用的场景就是中断通知任务。比如图像采集完成,中断发信号量,采集任务收到信号量就开始处理数据。
// 创建二值信号量
xSemaphoreHandle xSemaphore = xSemaphoreCreateBinary();
// 任务中等待信号量
void ImageCaptureTask(void *pvParameters)
{
while(1)
{
// 等待信号量,超时时间设为portMAX_DELAY
if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE)
{
// 处理图像数据
ProcessImage();
}
}
}
注意:信号量创建后默认是「未给出」状态。第一次使用前,记得先调用xSemaphoreGive()给一个信号量,否则任务会一直等下去。我刚开始用的时候就犯过这个错,查了半天才发现。
3.4 互斥锁:防止数据打架
互斥锁和信号量有点像,但有个关键区别:互斥锁有优先级继承机制。什么意思?你想想看,如果低优先级任务拿着锁,高优先级任务在等锁,那高优先级任务就被阻塞了。这就是经典的「优先级反转」问题。
互斥锁的优先级继承能解决这个问题:当高优先级任务等待锁时,持有锁的低优先级任务会临时提升到高优先级,尽快释放锁。
// 创建互斥锁
xSemaphoreHandle xMutex = xSemaphoreCreateMutex();
// 使用互斥锁保护共享数据
void SetMotorPosition(int position)
{
// 获取互斥锁
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE)
{
// 安全地修改共享变量
g_MotorPosition = position;
// 释放互斥锁
xSemaphoreGive(xMutex);
}
}
我个人建议:能用互斥锁就别用二值信号量做互斥。虽然功能上差不多,但互斥锁的优先级继承能避免很多坑。我在一个老项目里见过用二值信号量做互斥,结果系统偶尔卡死,查了三天才发现是优先级反转。
3.5 实际应用中的避坑指南
讲几个我实际项目中踩过的坑:
- 栈空间不够:任务栈大小设得太小,会导致栈溢出。我习惯给每个任务至少512字(2048字节)的栈空间,复杂任务给1024字。
- 中断优先级配置:FreeRTOS要求中断优先级不能高于configMAX_SYSCALL_INTERRUPT_PRIORITY,否则中断里调不了FreeRTOS的API。这个配置搞错了,系统直接跑飞。
- 死锁:两个任务互相等待对方释放锁,就死锁了。解决办法是:所有任务按相同的顺序获取锁,或者用超时机制。
调试小技巧:FreeRTOS提供了vTaskList()和vTaskGetRunTimeStats()两个函数,可以打印所有任务的状态和CPU占用率。我每次调试新系统,第一件事就是看这两个输出,能快速定位问题。
好了,这一章的内容就这些。任务调度、中断管理、信号量和互斥锁,是FreeRTOS的四大基石。你把这些搞明白了,大部分嵌入式系统的并发问题都能应对。下一章我们讲内存管理,到时候再聊。