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 v1 | STM32CubeIDE / CMSIS-RTOS v2 | FreeRTOS 原生 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() |
| 主动让出 CPU | osThreadYield() | 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() 的 usStackDepth 按 StackType_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 创建 task1 和 task2 后,先调用 vTaskSuspend(task1_handler) 挂起 task1,因此系统开始运行后只有 task2 会正常闪烁。后续按键触发恢复逻辑后,task1 再重新进入就绪态。
核心流程如下:
- 创建
start_task,然后启动调度器。 - 在
start_task中创建task1和task2。 - 调用
vTaskSuspend(task1_handler)挂起task1。 - 普通按键任务中可以调用
vTaskResume(task1_handler)恢复task1。 - 外部中断中应调用
xTaskResumeFromISR(task1_handler)恢复task1。 - 如果
xTaskResumeFromISR()返回pdTRUE,则调用portYIELD_FROM_ISR()请求任务切换。
这样正文重点放在 API 使用逻辑上,完整的工程初始化、GPIO 配置、任务创建细节和 LED 翻转代码可以放在 GitHub 中查看。
八、挂起函数内部实现
理解 vTaskSuspend() 的内部流程,可以帮助我们明白“挂起”到底改变了什么。
大致流程如下:
- 通过传入的任务句柄获取任务控制块;如果传入
NULL,则表示当前任务。 - 将任务从原来的状态列表中移除,例如就绪列表或阻塞列表。
- 如果任务同时挂在事件列表中,也要从事件列表中移除。
- 将任务插入到挂起任务列表
xSuspendedTaskList。 - 如果调度器正在运行,需要更新下一次阻塞超时时间,防止被挂起任务刚好是下一个超时任务。
- 如果挂起的是当前任务,并且调度器正在运行,则需要触发一次任务切换。
也就是说,挂起任务的核心动作不是释放内存,而是把任务从调度器正常会扫描的列表中移走,放入挂起列表中。只要它还在挂起列表里,调度器就不会选中它运行。
九、恢复函数内部实现
vTaskResume() 的核心流程如下:
- 判断待恢复任务不能是当前正在运行的任务。
- 判断任务是否真的在挂起列表中。
- 如果在挂起列表中,就将任务从挂起列表移除。
- 将该任务重新加入就绪列表。
- 如果被恢复任务的优先级不低于当前任务,则可能触发一次任务切换。
xTaskResumeFromISR() 的逻辑类似,但因为它运行在中断上下文中,所以会多做几件事:
- 关闭 FreeRTOS 可管理的中断,保存进入前的
BASEPRI值。 - 判断待恢复任务是否处于挂起列表。
- 如果调度器没有被挂起,则把任务从挂起列表移到就绪列表,并根据优先级设置返回值。
- 如果调度器处于挂起状态,则先把任务插入等待就绪列表,等调度器恢复后再统一处理。
- 恢复进入函数前保存的
BASEPRI值。 - 返回是否需要任务切换。
所以在中断函数中调用它时,不能忽略返回值。返回 pdTRUE 时,要配合 portYIELD_FROM_ISR(),这样高优先级任务才能在中断退出后尽快运行。
十、使用注意事项
使用任务挂起与恢复时,记住下面几点:
vTaskSuspend()和vTaskResume()都需要INCLUDE_vTaskSuspend为1。xTaskResumeFromISR()还需要INCLUDE_xTaskResumeFromISR为1。vTaskSuspend(NULL)表示挂起当前任务。- 任务被挂起多次后,只需要恢复一次就能重新进入就绪态。
- 中断中恢复任务要使用
xTaskResumeFromISR(),并根据返回值决定是否调用portYIELD_FROM_ISR()。 - CubeIDE 中的
osThreadSuspend()和osThreadResume()对应 FreeRTOS 原生的vTaskSuspend()和vTaskResume()。 - CMSIS-RTOS 的线程恢复接口不适合直接放在中断服务函数中使用,中断场景应使用
xTaskResumeFromISR()或其他FromISR类同步 API。 - 挂起和恢复更适合做任务运行状态控制,不建议把它当作复杂事件同步机制;任务通知、信号量、队列通常更适合做任务间同步。
十一、总结
任务挂起和恢复可以让我们主动控制某个任务是否参与调度。挂起任务时,FreeRTOS 会把任务从就绪或阻塞等状态列表中移除,放入挂起列表;恢复任务时,再把它从挂起列表移回就绪列表。
在普通任务中恢复任务,用 vTaskResume();在中断中恢复任务,用 xTaskResumeFromISR()。两者最大的区别是:中断中恢复任务需要处理返回值,并在必要时请求任务切换。
这组 API 的使用并不复杂,但要特别注意调用上下文和配置宏。只要把“任务中调用普通 API,中断中调用 FromISR API”这个原则记牢,任务挂起与恢复就会清晰很多。








