第三章 实时操作系统(RTOS)基础:FreeRTOS任务调度、中断管理、信号量与互斥锁
各位同学,咱们今天聊聊RTOS。说实话,我刚入行那会儿,对RTOS也是又爱又恨。爱的是它能让复杂的多任务变得井井有条,恨的是稍不留神就踩坑。尤其是分拣机这种高速设备,一个任务调度延迟几毫秒,可能就导致包裹分错位置。嗯,咱们今天就把FreeRTOS这几个核心机制掰开揉碎了讲。
3.1 任务调度:谁先跑,谁后跑?
任务调度说白了就是操作系统决定「现在该执行哪个任务」。FreeRTOS用的是抢占式调度,优先级高的任务先跑。但这里有个坑——优先级不是越高越好。
FreeRTOS的任务状态有四种:运行态、就绪态、阻塞态、挂起态。我习惯用一张表来理解它们:
| 状态 | 说明 | 典型场景 |
|---|---|---|
| 运行态 | 正在占用CPU | 当前执行的任务 |
| 就绪态 | 可以运行,但CPU被别的任务占着 | 等待高优先级任务释放CPU |
| 阻塞态 | 等待某个事件(延时、信号量、队列等) | vTaskDelay() 等待10ms |
| 挂起态 | 被vTaskSuspend()暂停,不参与调度 | 调试时暂停某个任务 |
任务调度的核心函数是 vTaskStartScheduler()。启动调度器后,FreeRTOS会根据优先级和状态决定谁跑。我个人习惯在创建任务时,把关键任务(比如处理传感器数据的任务)优先级设高,但不要超过 configMAX_PRIORITIES-1,留点余量给中断。
3.2 中断管理:别让中断抢了任务的饭碗
中断是RTOS里最「霸道」的机制。它优先级最高,来了就打断当前任务。但中断服务程序(ISR)里不能调用所有FreeRTOS API,这是个容易忽略的点。
FreeRTOS提供了一组带 FromISR 后缀的API,专门给中断用。比如:
xQueueSendFromISR()— 在中断中发送队列数据xSemaphoreGiveFromISR()— 在中断中释放信号量vTaskNotifyGiveFromISR()— 在中断中通知任务
为什么不能用普通API?因为普通API可能会引起任务切换,而中断里做任务切换需要特殊处理。FreeRTOS通过 portYIELD_FROM_ISR() 来安全地触发上下文切换。
举个例子,分拣机上的光电传感器触发中断:
// 中断服务程序
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 读取传感器状态
uint32_t sensor_data = read_sensor();
// 通过队列发送给处理任务
xQueueSendFromISR(xSensorQueue, &sensor_data, &xHigherPriorityTaskWoken);
// 如果有更高优先级的任务被唤醒,立即切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
注意那个 xHigherPriorityTaskWoken 变量。如果队列接收端任务的优先级比当前被中断的任务高,这个变量会被设为pdTRUE,然后 portYIELD_FROM_ISR 会触发一次任务切换。嗯,这个机制很巧妙,保证了实时性。
3.3 信号量与互斥锁:任务间的「交通信号灯」
信号量和互斥锁都是用来同步任务或保护共享资源的。但它们的用法完全不同,我刚开始也搞混过。
3.3.1 二值信号量:事件通知的好帮手
二值信号量就像一个旗子,只有0和1两种状态。它常用于「通知任务某个事件发生了」。比如中断来了,释放信号量,任务收到信号量后开始处理数据。
// 创建二值信号量
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();
// 任务A:等待信号量
void TaskA(void *pvParameters) {
while(1) {
// 等待信号量,超时时间portMAX_DELAY表示一直等
if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
// 处理数据
process_data();
}
}
}
// 中断中释放信号量
void ISR_Handler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
xSemaphoreGive 后变为1。如果连续Give两次,第二次会被忽略,因为已经是1了。这就是「二值」的含义。
3.3.2 计数信号量:管理有限资源
计数信号量可以记录多个事件。比如分拣机有5个缓冲区,每放入一个包裹,信号量加1;每取走一个包裹,信号量减1。信号量的值就是可用缓冲区的数量。
// 创建计数信号量,初始值5,最大值5
SemaphoreHandle_t xSemaphore = xSemaphoreCreateCounting(5, 5);
// 放入包裹
void put_package() {
// 如果信号量>0,表示有可用缓冲区
if(xSemaphoreTake(xSemaphore, 100) == pdTRUE) {
// 放入缓冲区
buffer_add();
} else {
// 缓冲区满了,处理异常
handle_overflow();
}
}
// 取走包裹
void get_package() {
// 取出包裹后释放信号量
package_t pkg = buffer_remove();
xSemaphoreGive(xSemaphore);
}
3.3.3 互斥锁:保护共享资源
互斥锁和二值信号量很像,但有个关键区别:互斥锁有「优先级继承」机制。什么意思呢?假设低优先级任务拿了锁,高优先级任务来抢锁,系统会临时提升低优先级任务的优先级,防止「优先级反转」。
使用互斥锁很简单:
// 创建互斥锁
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
// 任务中访问共享资源
void task_use_shared_resource(void *pvParameters) {
while(1) {
// 拿锁
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 安全访问共享资源
shared_variable++;
// 释放锁
xSemaphoreGive(xMutex);
}
}
}
3.4 实际应用:分拣机上的任务设计
咱们把前面讲的串起来,看看分拣机上怎么用FreeRTOS。一个典型的分拣机控制程序,我一般会设计这么几个任务:
| 任务名称 | 优先级 | 功能 | 同步方式 |
|---|---|---|---|
| 传感器中断处理 | 最高(中断) | 读取传感器数据,通知任务 | 二值信号量 |
| 分拣控制任务 | 高 | 根据传感器数据控制分拣臂 | 接收队列数据 |
| 通信任务 | 中 | 与上位机通信,上报状态 | 互斥锁保护共享数据 |
| 显示任务 | 低 | 更新LCD显示 | 无,空闲时运行 |
你看,中断处理优先级最高,但只做最少的操作。分拣控制任务优先级高,保证实时响应。通信任务中等,显示任务最低。这样设计,系统既实时又稳定。
好了,这一章就到这里。下一章咱们聊聊内存管理——别小看它,嵌入式系统里内存用不好,程序跑着跑着就崩了。