FreeRTOS(3)——任务挂起和恢复
本文最后更新于20 天前,其中的信息可能已经过时,如有错误请发送邮件到big_fw@foxmail.com

FreeRTOS(3)——任务挂起和恢复

在 FreeRTOS 中,任务除了创建、删除、阻塞、就绪这些状态外,还可以被主动挂起。挂起可以理解为“暂停”:任务还存在,任务控制块和栈也还在,只是调度器不会再选择它运行;等到后续调用恢复函数后,它又可以重新回到就绪态。

这一点和删除任务不同。删除任务之后,任务不能再被恢复;挂起任务则只是临时停止执行,适合用来控制某个任务在特定条件下暂停和继续。

一、任务挂起与恢复 API

本节主要用到三个 API:

API 函数描述调用位置
vTaskSuspend()挂起任务任务中
vTaskResume()恢复被挂起的任务任务中
xTaskResumeFromISR()在中断中恢复被挂起的任务中断服务函数中

带有 FromISR 后缀的函数表示它专门用于中断服务函数。任务中恢复和中断中恢复的写法不同,尤其是中断中恢复任务时,还需要根据返回值决定是否请求一次任务切换。

二、STM32CubeIDE API 与 FreeRTOS 原生 API 对照

在 STM32CubeIDE 或 STM32CubeMX 生成的 FreeRTOS 工程中,经常会看到 osThread... 这一类 API。它们不是 FreeRTOS 的原生 API,而是 CMSIS-RTOS 封装层。CMSIS-RTOS 再往下,最终还是会调用 FreeRTOS 的任务管理接口。

CubeIDE 工程中常见两种 CMSIS-RTOS 版本:

  • CMSIS-RTOS v1:句柄类型常见为 osThreadId
  • CMSIS-RTOS v2:句柄类型常见为 osThreadId_t

它们和 FreeRTOS 原生 API 的关系可以这样理解:

功能STM32CubeIDE / CMSIS-RTOS v1STM32CubeIDE / CMSIS-RTOS v2FreeRTOS 原生 API
启动调度器osKernelStart()osKernelStart()vTaskStartScheduler()
创建任务osThreadCreate()osThreadNew()xTaskCreate() / xTaskCreateStatic()
删除任务osThreadTerminate()osThreadTerminate()vTaskDelete()
挂起任务osThreadSuspend()osThreadSuspend()vTaskSuspend()
恢复任务osThreadResume()osThreadResume()vTaskResume()
中断中恢复任务不建议直接使用 CMSIS 线程恢复接口不建议直接使用 CMSIS 线程恢复接口xTaskResumeFromISR()
任务延时osDelay()osDelay()vTaskDelay()
周期延时无直接等价常用接口osDelayUntil()vTaskDelayUntil()
主动让出 CPUosThreadYield()osThreadYield()taskYIELD()
获取当前任务句柄osThreadGetId()osThreadGetId()xTaskGetCurrentTaskHandle()

如果是在 CubeIDE 生成的 MX_FREERTOS_Init() 中写任务,可能会看到类似下面的写法:

osThreadId_t task1Handle;

const osThreadAttr_t task1_attributes = {
  .name = "task1",
  .priority = (osPriority_t) osPriorityNormal,
  .stack_size = 128 * 4
};

task1Handle = osThreadNew(task1, NULL, &task1_attributes);

对应到 FreeRTOS 原生写法,大致就是:

TaskHandle_t task1_handler;

xTaskCreate(
   task1,
   "task1",
   128,
   NULL,
   2,
   &task1_handler
);

这里要注意一个细节:CMSIS-RTOS v2 的 osThreadAttr_t.stack_size 通常按字节填写,所以示例中写成 128 * 4;FreeRTOS 原生 xTaskCreate()usStackDepthStackType_t 的个数填写,所以这里写 128。如果 STM32 是 32 位 MCU,一个 StackType_t 通常是 4 字节。

任务挂起和恢复的对应关系最直接:

/* STM32CubeIDE / CMSIS-RTOS */
osThreadSuspend(task1Handle);
osThreadResume(task1Handle);

/* FreeRTOS 原生 API */
vTaskSuspend(task1_handler);
vTaskResume(task1_handler);

不过,中断中恢复任务要特别注意:osThreadResume() 是线程级接口,不适合直接在中断服务函数中调用。需要在中断中恢复被挂起任务时,优先使用 FreeRTOS 原生的 xTaskResumeFromISR()。如果只是中断通知任务去处理某件事,通常更推荐用任务通知、信号量或事件标志,而不是挂起/恢复任务。

三、挂起任务:vTaskSuspend()

函数原型如下:

void vTaskSuspend(TaskHandle_t xTaskToSuspend);

参数说明:

参数描述
xTaskToSuspend待挂起任务的任务句柄

使用这个函数前,需要将 FreeRTOSConfig.h 中的 INCLUDE_vTaskSuspend 配置为 1

调用 vTaskSuspend() 后,无论被挂起任务的优先级有多高,它都不会再被调度器执行,直到后续被恢复。如果传入参数为 NULL,表示挂起当前正在运行的任务,也就是任务自己挂起自己。

vTaskSuspend(task1_handler);  // 挂起 task1
vTaskSuspend(NULL);           // 挂起当前任务

挂起之后,任务不会因为延时到期、优先级较高等原因自动恢复,必须显式调用恢复函数。

四、任务中恢复:vTaskResume()

函数原型如下:

void vTaskResume(TaskHandle_t xTaskToResume);

参数说明:

参数描述
xTaskToResume待恢复任务的任务句柄

vTaskResume() 用于在普通任务上下文中恢复一个被挂起的任务,同样需要将 INCLUDE_vTaskSuspend 配置为 1

一个容易忽略的点是:任务无论被 vTaskSuspend() 挂起多少次,只需要调用一次 vTaskResume() 就可以恢复。它不是计数型挂起,不会要求“挂起几次就恢复几次”。

被恢复的任务会先进入就绪态,至于是否马上运行,则取决于它的优先级以及当前调度情况。

vTaskResume(task1_handler);

五、中断中恢复:xTaskResumeFromISR()

函数原型如下:

BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume);

参数说明:

参数描述
xTaskToResume待恢复任务的任务句柄

返回值说明:

返回值描述
pdTRUE恢复任务后需要进行任务切换
pdFALSE恢复任务后不需要进行任务切换

使用这个函数时,需要打开两个宏:

#define INCLUDE_vTaskSuspend        1
#define INCLUDE_xTaskResumeFromISR 1

中断服务函数里恢复任务时,不能直接调用 vTaskResume(),而要调用 xTaskResumeFromISR()。如果返回值为 pdTRUE,说明被恢复任务的优先级足够高,需要在退出中断前请求一次上下文切换。

核心写法如下,完整的中断初始化和标志位清除可以放到 GitHub 工程里:

BaseType_t xYieldRequired;

xYieldRequired = xTaskResumeFromISR(task1_handler);

if (xYieldRequired == pdTRUE)
{
   portYIELD_FROM_ISR(xYieldRequired);
}

这里还有一个和 Cortex-M 中断优先级相关的注意点:如果中断服务函数中要调用 FreeRTOS API,那么该中断的优先级不能高于 FreeRTOS 所能管理的最高中断优先级。对于 STM32 来说,任务优先级通常是数字越大优先级越高,而 NVIC 中断优先级是数字越小优先级越高,这两个方向不要混在一起。

六、实验设计

本实验的目的,是熟悉任务挂起与恢复相关的三个 API:

  • vTaskSuspend()
  • vTaskResume()
  • xTaskResumeFromISR()

实验中可以设计四个任务:

任务功能
start_task周期打印串口信息,作为系统仍在运行的心跳
task1每隔 1s 控制 RGB LED 显示蓝色
task2每隔 1.2s 控制 RGB LED 显示绿色,作为被挂起的目标任务

按键逻辑可以这样安排:

按键操作
KEY挂起 task2或恢复task2

这样可以同时验证任务上下文和中断上下文中的恢复方式。

七、关键代码

完整工程代码我就不全部贴在正文里了,后续可以放到 GitHub,然后在这里补上链接:

完整代码:Hui404/FreeRTOS_3

这里正文只保留实验的核心调用关系。start_task 创建 task1task2 后,先调用 vTaskSuspend(task1_handler) 挂起 task1,因此系统开始运行后只有 task2 会正常闪烁。后续按键触发恢复逻辑后,task1 再重新进入就绪态。

核心流程如下:

  1. 创建 start_task,然后启动调度器。
  2. start_task 中创建 task1task2
  3. 调用 vTaskSuspend(task1_handler) 挂起 task1
  4. 普通按键任务中可以调用 vTaskResume(task1_handler) 恢复 task1
  5. 外部中断中应调用 xTaskResumeFromISR(task1_handler) 恢复 task1
  6. 如果 xTaskResumeFromISR() 返回 pdTRUE,则调用 portYIELD_FROM_ISR() 请求任务切换。

这样正文重点放在 API 使用逻辑上,完整的工程初始化、GPIO 配置、任务创建细节和 LED 翻转代码可以放在 GitHub 中查看。

八、挂起函数内部实现

理解 vTaskSuspend() 的内部流程,可以帮助我们明白“挂起”到底改变了什么。

大致流程如下:

  1. 通过传入的任务句柄获取任务控制块;如果传入 NULL,则表示当前任务。
  2. 将任务从原来的状态列表中移除,例如就绪列表或阻塞列表。
  3. 如果任务同时挂在事件列表中,也要从事件列表中移除。
  4. 将任务插入到挂起任务列表 xSuspendedTaskList
  5. 如果调度器正在运行,需要更新下一次阻塞超时时间,防止被挂起任务刚好是下一个超时任务。
  6. 如果挂起的是当前任务,并且调度器正在运行,则需要触发一次任务切换。

也就是说,挂起任务的核心动作不是释放内存,而是把任务从调度器正常会扫描的列表中移走,放入挂起列表中。只要它还在挂起列表里,调度器就不会选中它运行。

九、恢复函数内部实现

vTaskResume() 的核心流程如下:

  1. 判断待恢复任务不能是当前正在运行的任务。
  2. 判断任务是否真的在挂起列表中。
  3. 如果在挂起列表中,就将任务从挂起列表移除。
  4. 将该任务重新加入就绪列表。
  5. 如果被恢复任务的优先级不低于当前任务,则可能触发一次任务切换。

xTaskResumeFromISR() 的逻辑类似,但因为它运行在中断上下文中,所以会多做几件事:

  1. 关闭 FreeRTOS 可管理的中断,保存进入前的 BASEPRI 值。
  2. 判断待恢复任务是否处于挂起列表。
  3. 如果调度器没有被挂起,则把任务从挂起列表移到就绪列表,并根据优先级设置返回值。
  4. 如果调度器处于挂起状态,则先把任务插入等待就绪列表,等调度器恢复后再统一处理。
  5. 恢复进入函数前保存的 BASEPRI 值。
  6. 返回是否需要任务切换。

所以在中断函数中调用它时,不能忽略返回值。返回 pdTRUE 时,要配合 portYIELD_FROM_ISR(),这样高优先级任务才能在中断退出后尽快运行。

十、使用注意事项

使用任务挂起与恢复时,记住下面几点:

  1. vTaskSuspend()vTaskResume() 都需要 INCLUDE_vTaskSuspend1
  2. xTaskResumeFromISR() 还需要 INCLUDE_xTaskResumeFromISR1
  3. vTaskSuspend(NULL) 表示挂起当前任务。
  4. 任务被挂起多次后,只需要恢复一次就能重新进入就绪态。
  5. 中断中恢复任务要使用 xTaskResumeFromISR(),并根据返回值决定是否调用 portYIELD_FROM_ISR()
  6. CubeIDE 中的 osThreadSuspend()osThreadResume() 对应 FreeRTOS 原生的 vTaskSuspend()vTaskResume()
  7. CMSIS-RTOS 的线程恢复接口不适合直接放在中断服务函数中使用,中断场景应使用 xTaskResumeFromISR() 或其他 FromISR 类同步 API。
  8. 挂起和恢复更适合做任务运行状态控制,不建议把它当作复杂事件同步机制;任务通知、信号量、队列通常更适合做任务间同步。

十一、总结

任务挂起和恢复可以让我们主动控制某个任务是否参与调度。挂起任务时,FreeRTOS 会把任务从就绪或阻塞等状态列表中移除,放入挂起列表;恢复任务时,再把它从挂起列表移回就绪列表。

在普通任务中恢复任务,用 vTaskResume();在中断中恢复任务,用 xTaskResumeFromISR()。两者最大的区别是:中断中恢复任务需要处理返回值,并在必要时请求任务切换。

这组 API 的使用并不复杂,但要特别注意调用上下文和配置宏。只要把“任务中调用普通 API,中断中调用 FromISR API”这个原则记牢,任务挂起与恢复就会清晰很多。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇