FreeRTOS:任务(Tasks)与调度基础

这里把上位机RTOS的坑填上:

FreeRTOS 中的“任务”究竟是什么

一句话定义:

任务 = 一段永不返回的 C 函数 + 一块独立的栈 + 一份内核管理结构(TCB)

在 FreeRTOS 里:

  • 每个任务都有自己的栈(不是共用)

  • 每个任务都有一个 TCB(Task Control Block)

  • 调度器只做一件事:

    在“就绪态”的任务中,选优先级最高的那个运行

你可以把它理解成:

while (system_running) {
    选出最高优先级的 Ready 任务
    切换上下文
    让它跑一会儿
}

任务 ≠ 函数调用

  • 函数:调用 → 返回
  • 任务:创建 → 运行 → 阻塞 / 就绪 → 再运行(通常永不返回)

这就是 RTOS 和裸机 while(1) 的本质差异。


2. xTaskCreate —— 创建任务

2.1 API 原型

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,
    const char * const pcName,
    const configSTACK_DEPTH_TYPE usStackDepth,
    void *pvParameters,
    UBaseType_t uxPriority,
    TaskHandle_t *pvCreatedTask
);

2.2 每个参数的含义

参数 含义
pvTaskCode 任务入口函数(不能返回)
pcName 任务名字,仅用于调试
usStackDepth 任务私有栈大小
pvParameters 传给任务的参数
uxPriority 任务优先级(数值越大越高)
pvCreatedTask 输出任务句柄(可为 NULL)
  • 需要注意的是——xTaskCreate 只负责创建
  • 任务真正开始跑,是在 vTaskStartScheduler() 之后

3. vTaskStartScheduler —— 调度器启动

void vTaskStartScheduler(void);

这是一道分水岭 API

  • 调用之前:
    • 你在“普通 C 世界”
    • 只是注册了一堆任务
  • 调用之后:
    • CPU 控制权交给调度器
    • main 函数理论上不再返回
int main(void)
{
    xTaskCreate(...);
    xTaskCreate(...);

    vTaskStartScheduler();

    // 走到这里 = 调度器启动失败
    for (;;);
}

如果在 host 模拟环境里看到 main 返回,99% 是内存或配置问题


4. vTaskDelay —— 相对延时

void vTaskDelay(TickType_t xTicksToDelay);

把当前任务从 Ready → Blocked,至少阻塞这么多个 tick

vTaskDelay(pdMS_TO_TICKS(500));

特点

  • 延时是相对的
  • 会随着代码执行时间产生时间漂移

适合:

  • 日志打印
  • 非严格周期任务

5. vTaskDelayUntil —— 绝对时间延时(周期任务)

void vTaskDelayUntil(
    TickType_t *pxPreviousWakeTime,
    TickType_t xTimeIncrement
);
void PeriodicTask(void *arg)
{
    TickType_t last = xTaskGetTickCount();

    for (;;)
    {
        printf("tick = %lu\n", xTaskGetTickCount());
        vTaskDelayUntil(&last, pdMS_TO_TICKS(1000));
    }
}

和 vTaskDelay 的根本区别

API 是否漂移 适用场景
vTaskDelay 普通等待
vTaskDelayUntil 不会 严格周期任务

这是实时系统里非常重要的一对 API


6. vTaskDelete —— 删除任务

void vTaskDelete(TaskHandle_t xTaskToDelete);
  • 删除指定任务
  • NULL删除自己
vTaskDelete(NULL);

常见用途:

  • 初始化任务跑完就退出
  • 异常检测任务自杀

⚠️ 实时系统中不建议频繁创建 / 删除任务(碎片 + 不确定性)。


7. 任务栈 & TCB —— 为什么你必须关心它

每个任务都有:

  • 私有栈(函数调用、局部变量、中断上下文)
  • TCB(状态、优先级、栈指针等)

7.1 栈用多了会怎样?

  • 覆盖别的内存
  • 行为诡异
  • 调试地狱

7.2 查看栈使用:uxTaskGetStackHighWaterMark

UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);
UBaseType_t water = uxTaskGetStackHighWaterMark(NULL);
printf("stack left = %u\n", water);

含义:

历史最小剩余栈空间

  • 越小说明越危险
  • 接近 0 必炸

8. 优先级与调度规则

8.1 基本规则

  1. 优先级高的任务一定先跑
  2. 高优先级 Ready → 立刻抢占
  3. 同优先级 → 时间片轮转(若开启)

8.2 一个非常重要的现象:优先级反转

现象描述(先不解决)

  • 低优先级任务 L 占着资源
  • 高优先级任务 H 等这个资源,被阻塞
  • 中优先级任务 M 疯狂运行

结果:

H 被 M 间接“压制”

这就是后面互斥锁必须存在的根本原因。


9. 实验 / 作业代码(完整)

9.1 实验目标

  • 3 个任务
  • 3 个优先级
  • 用打印解释调度顺序

9.2 作业代码

#include <stdio.h>
#include "FreeRTOS.h"
#include "task.h"

void TaskHigh(void *arg)
{
    for (;;)
    {
        printf("[H] run\n");
        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

void TaskMid(void *arg)
{
    for (;;)
    {
        printf("[M] run\n");
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void TaskLow(void *arg)
{
    for (;;)
    {
        printf("[L] run, stack=%u\n",
               (unsigned)uxTaskGetStackHighWaterMark(NULL));
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

int main(void)
{
    xTaskCreate(TaskHigh, "H", 256, NULL, tskIDLE_PRIORITY + 3, NULL);
    xTaskCreate(TaskMid,  "M", 256, NULL, tskIDLE_PRIORITY + 2, NULL);
    xTaskCreate(TaskLow,  "L", 256, NULL, tskIDLE_PRIORITY + 1, NULL);

    vTaskStartScheduler();
    for (;;);
}

各位可以在自己的上位机环境中试一下 😃

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐