在 FreeRTOS 中,任务抢占机制是其实时性的核心特性之一,它基于优先级驱动的抢占式调度(Preemptive Scheduling)
一、配置configUSE_PREEMPTION
在FreeRTOS的配置文件中有 configUSE_PREEMPTION
选项:
#define configUSE_PREEMPTION 1
- 将此选型设置为 1 即 开启 抢占式调度模式
- 将此选型设置为 0 即 关闭 抢占式调度模式(协作调度模式)
二、FreeRTOS抢占调度机制
抢占式调度模式下,当有更高优先级任务就绪时,当前任务会被抢占。然而,这种抢占通常发生在任务主动调用可能引发调度的函数(如延迟、等待信号量等)或者系统滴答中断(tick interrupt)时。
在我们的观察下,这种抢占可能是瞬间的。所以我们觉得当高优先级任务由阻塞态或暂停态转为就绪态时,系统会直接保存上下文并且转到高优先级的任务内执行相应代码。
但是在系统内部,这种抢占并不是立刻发生的,如果忽略这“微小的时差”也有可能造成严重的运行错误。就比如需要高优先级任务立刻去处理串口接收缓存内的数据时,需要较快的响应,否则就可能造成串口接收缓存溢出。
总结一下就是:
- 优先级抢占机制 :
当一个高优先级任务进入就绪状态(例如,通过中断唤醒、信号量释放等),并不会立即切换到该任务,需要等待调度触发的时机。 - 调度触发时机 :
系统节拍中断(Tick Interrupt): 通常为1ms,需要根据configTICK_RATE_HZ
来进行设置。 - 主动触发调度 :
当其他任务调用阻塞式API(如 xQueueSend()、vTaskDelay())时主动让出CPU。
任务显式调用 taskYIELD() 强制调度。该函数会直接启用任务调度,不用检查SysTick时基,同时也有中断函数版本。
中断服务程序(ISR)中唤醒高优先级任务后,可能触发上下文切换(通过 portYIELD_FROM_ISR())。
三、示例Demo
下面的Demo仅为示范使用,实际情况要比这个会更复杂。
在该示例工程中,设置了Demo_A_Task和Demo_B_Task两个任务,A的优先级比B高。同时设置了一个按键。
标志位一共有四种:
- SysTick中断标志位 :systick_flag
- 任务A标志位 :Demot_A_flag
- 任务B标志位 :Demot_B_flag
- 按键按下标志 :Key_cnt_flag
情况1:
- DemoA需要任务通知才能解除阻塞并且置位后进入死循环
- DemoB每次置位操作后进入10ms的阻塞状态
- 任务通知由按键触发外部中断来发送。
任务函数:
#include "rtos.h"
TaskHandle_t StartTask_Handler;
TaskHandle_t Demo_task_A_Handler;
TaskHandle_t Demo_task_B_Handler;
uint8_t systick_flag = 0; // SysTick中断标志位
uint8_t Demot_A_flag = 0; // 任务A标志位
uint8_t Demot_B_flag = 0; // 任务B标志位
uint8_t Key_cnt_flag = 0; // 按键按下标志
void start_task(void *pvParameters)
{
//进入临界区
taskENTER_CRITICAL();
xTaskCreate((TaskFunction_t )Demo_task_A, //DEMO_A
(const char* )"Demo_task_A",
(uint16_t )Demo_task_A_STK_SIZE,
(void* )NULL,
(UBaseType_t )3,
(TaskHandle_t* )&Demo_task_A_Handler);
xTaskCreate((TaskFunction_t )Demo_task_B, //DEMO_B
(const char* )"Demo_task_B",
(uint16_t )Demo_task_B_STK_SIZE,
(void* )NULL,
(UBaseType_t )1,
(TaskHandle_t* )&Demo_task_B_Handler);
//删除开始任务
vTaskDelete(StartTask_Handler);
//退出临界区
taskEXIT_CRITICAL();
}
void Demo_task_A(void* pvParameters)
{
uint32_t ulNotificationValue;
for(;;)
{
xTaskNotifyWait( 0, // 清除所有bit位(初始条件)
EVENT_KEY_BIT0, // 清除bit位0(退出条件)
&ulNotificationValue, // 接收到的数值
portMAX_DELAY ); // 无限等待
Demot_A_flag = 1;
Demot_B_flag = 0;
while(1);
}
}
void Demo_task_B(void* pvParameters)
{
for(;;)
{
Demot_A_flag = 0;
Demot_B_flag = 1;
vTaskDelay(10);
}
}
外部中断服务函数:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if(GPIO_Pin == GPIO_PIN_4)
{
Key_cnt_flag = 1;
xTaskNotifyFromISR( Demo_task_A_Handler, // 目标任务句柄
EVENT_KEY_BIT0, // 设置位0
eSetBits, // 动作为按位设置(不影响其他位)
&xHigherPriorityTaskWoken);
}
}
现象 :
上图可以看出,虽然按键按下后发送Demo_A的任务通知,Demo_A任务进入就绪态。但是并没有立刻切换到任务A,而是等到了一个新的Tick中断后,调度器才进行任务的切换来运行任务A。那假如任务A是串口接收处理函数,而在 0.x Tick 时间段内又有大量的数据发送到串口的接收缓存中会发生什么呢?显然,会造成串口接收溢出错误,同时有大量数据丢失,这在某些应用场景中是非常致命的错误。
情况2:
其他代码不变,在外部中断回调函数的末尾加入portYIELD_FROM_ISR
函数。
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if(GPIO_Pin == GPIO_PIN_4)
{
Key_cnt_flag = 1;
xTaskNotifyFromISR( Demo_task_A_Handler, // 目标任务句柄
EVENT_KEY_BIT0, // 设置位0
eSetBits, // 动作为按位设置(不影响其他位)
&xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
现象 :
上图可以看到,当按键按下时,外部中断发送了任务通知,在退出中断后,调度器立刻切换了上下文,执行了任务A的代码片段。该动作响应非常快,也没有等待下一个Tick中断的到来。
局部放大分析 :
经过局部放大可见,在按键按下到任务A开始执行,只用了不到 3微秒 的时间,在这期间系统的动作大致可以分为以下几步:
- 按键按下触发外部中断
- 中断回调函数向任务A发送任务通知
- 中断函数即将退出时,调用
portYIELD_FROM_ISR
宏定义,告诉调度器立即进行任务状态的查询 - 调度器进行任务查询发现高优先级任务A已就绪
- 调度器保存任务B的上下文(寄存器、堆栈指针等)到其任务控制块(TCB)
- 恢复任务A的上下文,从任务A上次阻塞或切换出的位置继续执行
假设任务A的内容是对串口接收的数据进行处理,假设串口波特率为115200(8n1),那么:
- 接收 1bit 数据所需时间为 1000000微秒 ÷ 115200bit ≈ 8.68 微秒/bit
- 接收一帧数据所需时间约为 86 微秒
上述计算还省略了一帧数据从移位寄存器转移到RDR寄存器所需的时间,即使这样,任务A的切换动作大约2.7微秒也是绰绰有余的。更不用提一些高级的芯片还具备硬件上的FIFO缓存区。
四、portYIELD函数
在 FreeRTOS 中,portYIELD() 是一个关键宏,用于手动触发任务调度。它的核心作用是强制调度器立即检查是否有更高优先级的任务就绪,并执行上下文切换(如果满足抢占条件)。
1. portYIELD() 的作用
- 手动触发上下文切换:调用 portYIELD() 会强制调度器立即检查任务就绪列表,如果存在更高优先级任务或同优先级任务需要时间片轮转,则切换任务。
- 不依赖中断或阻塞:与自动调度(如 Tick 中断或任务阻塞)不同,portYIELD() 允许在任务代码中主动让出 CPU。
2. 使用场景
- 协作式多任务处理:在协作式调度(需配置 configUSE_PREEMPTION=0)中,任务需主动调用 portYIELD() 让出 CPU。
- 优化实时性:即使启用抢占式调度(configUSE_PREEMPTION=1),若任务执行长循环且不阻塞,插入 portYIELD() 可减少高优先级任务的响应延迟。
- 同步共享资源访问:在未使用互斥量的情况下,通过 portYIELD() 避免长时间占用共享资源。
更详细的内容请参考下面链接:
FreeRTOS中高优先级无法打断低优先级任务的原因