4. 任务状态转换

任务目标

编写一个基于FreeRTOS的演示程序,创建两个任务,通过任务挂起(vTaskSuspend)、恢复(vTaskResume)和延时(vTaskDelay)操作,使用串口打印各任务的状态转换过程,从而直观理解任务在就绪(Ready)、运行(Running)、阻塞(Blocked)和挂起(Suspended)四种状态间的切换逻辑。

核心知识点

  • 就绪态:任务具备运行条件,等待调度器分配CPU。
  • 运行态:任务正在占用CPU执行。
  • 阻塞态:任务因调用延时、等待信号量/队列等而暂时无法运行。
  • 挂起态:任务被vTaskSuspend主动暂停,只能通过vTaskResume恢复。

硬件/环境要求

  • 开发板:STM32F407(或其他Cortex-M4板卡)
  • IDE:STM32CubeIDE 或 Keil MDK
  • FreeRTOS版本:V10.x(CMSIS-OS v1封装或原生API均可)
  • 串口:USART1,波特率115200,用于打印状态信息

完整实现代码

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

/* 串口发送字符串(需根据实际硬件实现) */
extern void Debug_SendString(const char* str);

/* 任务句柄 */
TaskHandle_t xTask1Handle = NULL;
TaskHandle_t xTask2Handle = NULL;

/* 任务1:演示挂起与恢复 */
void vTask1(void *pvParameters)
{
    const char *pcTaskName = "Task1";
    TickType_t xLastWakeTime = xTaskGetTickCount();
    
    for( ;; )
    {
        Debug_SendString("[Task1] Running (Ready->Running)\r\n");
        
        /* 延时500ms,进入阻塞态 */
        vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(500));
        Debug_SendString("[Task1] Blocked (delay 500ms)\r\n");
        
        /* 运行2次后挂起自己 */
        static int count = 0;
        count++;
        if( count == 2 )
        {
            Debug_SendString("[Task1] Suspending self\r\n");
            vTaskSuspend(NULL);  /* 挂起自身,进入挂起态 */
            Debug_SendString("[Task1] Resumed by Task2\r\n");
            count = 0;
        }
    }
}

/* 任务2:控制任务1的挂起与恢复 */
void vTask2(void *pvParameters)
{
    const char *pcTaskName = "Task2";
    TickType_t xLastWakeTime = xTaskGetTickCount();
    
    for( ;; )
    {
        Debug_SendString("[Task2] Running\r\n");
        
        /* 延时1秒 */
        vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(1000));
        
        /* 每2秒挂起Task1一次,再延时1秒后恢复 */
        Debug_SendString("[Task2] Suspending Task1\r\n");
        vTaskSuspend(xTask1Handle);
        
        vTaskDelay(pdMS_TO_TICKS(1000));
        
        Debug_SendString("[Task2] Resuming Task1\r\n");
        vTaskResume(xTask1Handle);
    }
}

int main(void)
{
    /* 硬件初始化(时钟、GPIO、串口等) */
    Hardware_Init();
    
    /* 创建两个任务 */
    xTaskCreate(vTask1, "Task1", 128, NULL, 1, &xTask1Handle);
    xTaskCreate(vTask2, "Task2", 128, NULL, 2, &xTask2Handle);
    
    /* 启动调度器 */
    vTaskStartScheduler();
    
    /* 正常不会执行到这里 */
    for( ;; );
}

代码说明

  • vTaskSuspend(NULL):传入NULL表示挂起当前正在运行的任务自身。
  • vTaskResume(xTask1Handle):恢复指定句柄的任务,使其从挂起态回到就绪态。
  • vTaskDelayUntil:实现固定频率的延时,任务在延时期间处于阻塞态。
  • 任务2优先级高于任务1,因此任务2运行时能抢占任务1。

运行与验证方法

  1. 编译并下载程序到开发板。
  2. 打开串口助手(115200-8-N-1),观察输出日志。
  3. 预期输出序列(关键节点):
[Task1] Running (Ready->Running)
[Task1] Blocked (delay 500ms)
[Task2] Running
[Task1] Running (Ready->Running)
[Task1] Blocked (delay 500ms)
[Task2] Suspending Task1
[Task1] Suspending self
[Task2] Resuming Task1
[Task1] Resumed by Task2

通过判定标准:

  • 串口能观察到Task1在“Running”和“Blocked”间周期性切换。
  • Task2执行“Suspending Task1”后,Task1不再输出任何信息,证明其已进入挂起态。
  • Task2执行“Resuming Task1”后,Task1恢复输出,证明其从挂起态回到就绪态并被调度运行。
  • 整个过程中系统无崩溃、无卡死,两个任务持续交替运行。

状态转换可视化

就绪态 运行态 阻塞态 挂起态 调度器选择 时间片耗尽/抢占 vTaskDelay 延时结束/事件就绪 vTaskSuspend(NULL) vTaskResume(handle) 本案例演示路径:运行→阻塞(延时)→就绪→运行→挂起(自挂)→就绪(恢复)→运行

常见问题排查

  • 任务1挂起后无法恢复:检查vTaskResume传入的句柄是否为xTask1Handle,且任务1确实处于挂起态而非阻塞态。
  • 串口无输出:确认Debug_SendString已正确映射到实际串口发送函数,且串口初始化在任务创建前完成。
  • 任务优先级导致Task1几乎不运行:若Task2使用vTaskDelayUntil且优先级较高,Task1仍能获得运行时间片,因为Task2会周期性阻塞。