Bootstrap

FreeRTOS中如何让阻塞的高优先级任务快速响应


在 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中高优先级无法打断低优先级任务的原因

;