
在裸机程序中程序的主体通常是main()函数中的一个无限循环。CPU 按照代码顺序执行每一个功能模块例如按键扫描、LED 翻转、串口处理、传感器采集等。只要其中某个函数执行时间过长后面的功能就会被延迟。在 FreeRTOS 中系统不再把所有功能都写在一个主循环中而是把不同功能拆分成多个独立任务。每个任务都有自己的函数入口、任务栈、任务控制块和调度信息。调度器根据任务优先级、阻塞状态和系统节拍来决定哪个任务获得 CPU。本文主要整理 FreeRTOS 中任务的基本形式、任务栈、任务控制块 TCB以及静态创建任务的基本流程。一、裸机中的顺序执行裸机程序中即使两个变量的波形看起来像“同时翻转”本质上仍然是 CPU 按顺序执行代码。下面的代码可以让flag1和flag2按固定频率翻转在 Keil 逻辑分析仪中可以观察到类似方波的效果。#include stdint.h /* volatile 表示变量可能被编译器无法感知的外部环境读取。 在调试或逻辑分析仪观察变量时建议加 volatile防止编译器优化掉变量访问。 */ volatile uint32_t flag1 0; volatile uint32_t flag2 0; /* 简单软件延时函数。 count 越大循环次数越多延时时间越长。 该函数会一直占用 CPU不会主动让出 CPU。 */ static void SoftDelay(uint32_t count) { while (count ! 0U) { count--; } } int main(void) { /* 裸机主循环。 CPU 会按照代码顺序依次执行 flag1 和 flag2 的翻转。 */ while (1) { flag1 1; SoftDelay(10000); flag1 0; SoftDelay(10000); flag2 1; SoftDelay(10000); flag2 0; SoftDelay(10000); } }这段代码虽然能产生两个变量翻转的波形但不是真正的多任务。因为flag1翻转完成后CPU 才会执行flag2的翻转。整个程序仍然是单一执行流。变量说明变量说明flag1用于模拟第一个功能模块的运行状态flag2用于模拟第二个功能模块的运行状态count软件延时循环计数值volatile告诉编译器不要随意优化该变量的读写二、FreeRTOS 中任务的基本形式FreeRTOS 中的任务本质上是一个不会主动返回的 C 函数。当前 FreeRTOS 官方源码中任务函数类型定义为typedef void (* TaskFunction_t)( void * arg );也就是说任务函数的返回值是void参数是一个void *指针。这个定义位于 FreeRTOS 的projdefs.h中。官方task.h也说明任务函数必须以连续循环的形式实现不能从任务函数中返回。FreeRTOS task.h任务函数的基本形式如下/* 任务函数入口。 pvParameters 是创建任务时传入的参数。 参数类型是 void *因此可以传入任意类型数据的地址。 */ static void Task_Entry(void *pvParameters) { /* 如果任务暂时不需要参数可以显式转换为 void 避免编译器提示未使用参数。 */ (void)pvParameters; /* FreeRTOS 任务通常不能返回所以任务主体一般写成无限循环。 */ for (;;) { /* 任务主体代码写在这里。 */ } }函数说明函数或参数说明Task_Entry任务入口函数名pvParameters创建任务时传入的参数(void)pvParameters表示参数暂不使用避免编译警告for (;;)无限循环保证任务不返回三、使用 FreeRTOS 创建两个静态任务FreeRTOS 创建任务有两种方式动态创建和静态创建。动态创建会在创建任务时由系统从堆中分配 TCB 和任务栈静态创建则由应用程序提前定义好 TCB 和任务栈再交给 FreeRTOS 使用。静态创建的好处是内存来源清晰不依赖堆分配更适合对内存确定性要求较高的嵌入式系统。下面代码演示两个任务分别翻转flag1和flag2。#include stdint.h #include FreeRTOS.h #include task.h /* 1. 任务相关配置 */ /* 任务栈大小。 注意FreeRTOS 中任务栈大小通常以 StackType_t 为单位不是字节。 如果 StackType_t 是 4 字节则 128 表示 512 字节栈空间。 */ #define TASK1_STACK_SIZE 128U #define TASK2_STACK_SIZE 128U /* 任务优先级。 数值越大优先级越高。 这里两个任务优先级相同便于观察同优先级任务的轮流运行效果。 */ #define TASK1_PRIORITY 1U #define TASK2_PRIORITY 1U /* 任务延时时间单位为毫秒。 */ #define TASK1_DELAY_MS 100U #define TASK2_DELAY_MS 100U /* 2. 用于观察的全局变量 */ /* flag1 用于观察任务 1 的运行状态。 */ volatile uint32_t flag1 0; /* flag2 用于观察任务 2 的运行状态。 */ volatile uint32_t flag2 0; /* 3. 静态任务内存 */ /* Task1Stack 是任务 1 的栈空间。 静态创建任务时栈空间由应用程序提前定义。 */ static StackType_t Task1Stack[TASK1_STACK_SIZE]; /* Task2Stack 是任务 2 的栈空间。 */ static StackType_t Task2Stack[TASK2_STACK_SIZE]; /* Task1TCB 用于保存任务 1 的任务控制块。 StaticTask_t 是 FreeRTOS 提供给静态任务使用的 TCB 存储类型。 */ static StaticTask_t Task1TCB; /* Task2TCB 用于保存任务 2 的任务控制块。 */ static StaticTask_t Task2TCB; /* Task1Handle 是任务 1 的句柄。 句柄可以理解为任务的标识后续可用于挂起、恢复或删除任务。 */ static TaskHandle_t Task1Handle NULL; /* Task2Handle 是任务 2 的句柄。 */ static TaskHandle_t Task2Handle NULL; /* 4. 任务 1 入口函数 */ static void Task1_Entry(void *pvParameters) { /* 当前示例未使用任务参数。 */ (void)pvParameters; for (;;) { /* flag1 置 1表示任务 1 运行到此处。 */ flag1 1; /* vTaskDelay 会让当前任务进入阻塞态。 阻塞期间 CPU 可以去运行其他就绪任务。 */ vTaskDelay(pdMS_TO_TICKS(TASK1_DELAY_MS)); /* flag1 置 0表示任务 1 执行到另一个状态。 */ flag1 0; /* 再次延时让波形保持低电平一段时间。 */ vTaskDelay(pdMS_TO_TICKS(TASK1_DELAY_MS)); } } /* 5. 任务 2 入口函数 */ static void Task2_Entry(void *pvParameters) { (void)pvParameters; for (;;) { flag2 1; vTaskDelay(pdMS_TO_TICKS(TASK2_DELAY_MS)); flag2 0; vTaskDelay(pdMS_TO_TICKS(TASK2_DELAY_MS)); } } /* 6. 主函数 */ int main(void) { /* 硬件初始化函数。 实际工程中可在这里初始化时钟、GPIO、串口等外设。 */ // Board_Init(); /* 静态创建任务 1。 */ Task1Handle xTaskCreateStatic( Task1_Entry, /* 任务入口函数 */ Task1, /* 任务名称主要用于调试 */ TASK1_STACK_SIZE, /* 任务栈大小单位为 StackType_t */ NULL, /* 任务参数此处不传参 */ TASK1_PRIORITY, /* 任务优先级 */ Task1Stack, /* 任务栈数组 */ Task1TCB /* 任务控制块内存 */ ); /* 静态创建任务 2。 */ Task2Handle xTaskCreateStatic( Task2_Entry, Task2, TASK2_STACK_SIZE, NULL, TASK2_PRIORITY, Task2Stack, Task2TCB ); /* 判断任务是否创建成功。 xTaskCreateStatic 创建成功会返回任务句柄 如果传入的栈或 TCB 指针为空则返回 NULL。 */ configASSERT(Task1Handle ! NULL); configASSERT(Task2Handle ! NULL); /* 启动调度器。 调度器启动后系统开始按照任务优先级和状态进行任务调度。 */ vTaskStartScheduler(); /* 正常情况下调度器启动成功后不会运行到这里。 如果运行到这里通常说明堆、栈或系统配置存在问题。 */ for (;;) { } }四、xTaskCreateStatic() 参数详解xTaskCreateStatic()是 FreeRTOS 的静态任务创建函数。当前官方task.h中给出的函数原型如下TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, const char * const pcName, const configSTACK_DEPTH_TYPE uxStackDepth, void * const pvParameters, UBaseType_t uxPriority, StackType_t * const puxStackBuffer, StaticTask_t * const pxTaskBuffer );参数说明如下参数说明pxTaskCode任务入口函数例如Task1_EntrypcName任务名称主要用于调试和跟踪uxStackDepth任务栈大小单位不是字节而是StackType_tpvParameters传入任务函数的参数uxPriority任务优先级puxStackBuffer静态任务栈数组地址pxTaskBuffer静态任务控制块地址返回值创建成功返回任务句柄失败返回NULL这里最容易混淆的是uxStackDepth。它表示栈数组元素个数而不是字节数。例如在 32 位 Cortex-M 上StackType_t通常是 4 字节。如果uxStackDepth为 128则栈空间约为 512 字节。五、任务栈的作用任务栈用于保存任务运行时的局部变量、函数调用现场、寄存器上下文等信息。每个任务都有自己独立的栈空间这一点是多任务系统和裸机主循环的重要区别。在裸机程序中所有函数调用通常共享同一个主栈。而在 FreeRTOS 中每个任务都有独立栈。任务切换时当前任务的寄存器现场会保存到自己的任务栈中下次任务再次运行时再从自己的栈中恢复现场。简化理解如下任务 1 - 使用 Task1Stack 任务 2 - 使用 Task2Stack 任务 3 - 使用 Task3Stack任务之间的函数调用、局部变量、运行现场互不干扰。六、任务控制块 TCB任务控制块是 FreeRTOS 管理任务的核心结构。每创建一个任务系统都需要一个 TCB 来保存任务信息。当前 FreeRTOStasks.c中的 TCB 会根据配置包含不同成员但核心字段包括栈顶指针、任务状态列表项、事件列表项、任务优先级、栈起始地址和任务名称等。FreeRTOS tasks.c下面是一个简化版 TCB用于理解任务控制块的基本思想typedef struct TaskControlBlock { /* 栈顶指针。 任务切换时CPU 上下文会保存到任务栈中。 pxTopOfStack 用来记录当前任务栈顶位置。 */ volatile StackType_t *pxTopOfStack; /* 任务状态列表项。 任务处于就绪、阻塞、挂起等状态时 会通过该节点挂入不同的状态列表。 */ ListItem_t xStateListItem; /* 事件列表项。 当任务等待队列、信号量、事件组时 会通过该节点挂入对应事件等待列表。 */ ListItem_t xEventListItem; /* 任务优先级。 调度器会根据优先级选择要运行的任务。 */ UBaseType_t uxPriority; /* 任务栈起始地址。 静态创建任务时该指针指向应用程序提供的栈数组。 */ StackType_t *pxStack; /* 任务名称。 主要用于调试、任务跟踪和状态打印。 */ char pcTaskName[configMAX_TASK_NAME_LEN]; } TCB_t;TCB 成员说明成员说明pxTopOfStack当前任务栈顶位置任务切换时非常关键xStateListItem用于挂入就绪列表、延时列表、挂起列表xEventListItem用于挂入队列、信号量、事件组等待列表uxPriority保存任务优先级pxStack指向任务栈空间起始地址pcTaskName保存任务名称七、静态创建任务的内部流程从使用角度看xTaskCreateStatic()只是一行函数调用。但从内核角度看它大致完成以下工作检查任务栈和 TCB 内存是否有效。把用户提供的StaticTask_t转换成内部使用的TCB_t。把用户提供的栈数组记录到 TCB 中。初始化任务名称、任务优先级、任务列表项。初始化任务栈使任务第一次被调度时能从任务函数入口开始执行。把任务加入就绪列表。返回任务句柄。下面用简化代码模拟这个过程。TaskHandle_t MyTaskCreateStatic( TaskFunction_t pxTaskCode, /* 任务入口函数 */ const char *pcName, /* 任务名称 */ uint32_t stackDepth, /* 任务栈大小单位为 StackType_t */ void *pvParameters, /* 传入任务函数的参数 */ UBaseType_t uxPriority, /* 任务优先级 */ StackType_t *puxStackBuffer, /* 任务栈数组 */ TCB_t *pxTaskBuffer /* 任务控制块内存 */ ) { TCB_t *pxNewTCB; /* 指向新任务 TCB 的指针 */ TaskHandle_t xReturn; /* 返回给调用者的任务句柄 */ /* 静态创建任务时任务栈和任务控制块必须由外部提供。 只要其中一个为空就不能创建任务。 */ if ((puxStackBuffer NULL) || (pxTaskBuffer NULL)) { return NULL; } /* 静态创建时pxTaskBuffer 就是新任务的 TCB 存储空间。 */ pxNewTCB pxTaskBuffer; /* 把任务栈起始地址保存到 TCB 中。 */ pxNewTCB-pxStack puxStackBuffer; /* 初始化新任务的 TCB、任务栈、任务名称、列表项等。 */ MyInitialiseNewTask( pxTaskCode, pcName, stackDepth, pvParameters, uxPriority, xReturn, pxNewTCB ); /* 把新任务加入就绪列表。 任务进入就绪列表后就可以被调度器选择运行。 */ MyAddTaskToReadyList(pxNewTCB); /* 返回任务句柄。 */ return xReturn; }这段代码不是 FreeRTOS 源码只是为了说明静态创建任务的核心流程。真实 FreeRTOS 中还会处理 MPU、多核、任务编号、栈溢出检查、trace 宏等配置。八、初始化新任务任务创建过程中最关键的一步是初始化 TCB 和任务栈。下面给出一个简化版MyInitialiseNewTask()。static void MyInitialiseNewTask( TaskFunction_t pxTaskCode, /* 任务入口函数 */ const char *pcName, /* 任务名称 */ uint32_t stackDepth, /* 任务栈大小 */ void *pvParameters, /* 任务入口参数 */ UBaseType_t uxPriority, /* 任务优先级 */ TaskHandle_t *pxCreatedTask, /* 用于保存任务句柄的指针 */ TCB_t *pxNewTCB /* 新任务的 TCB */ ) { StackType_t *pxTopOfStack; /* 任务栈顶指针 */ uint32_t i; /* 循环变量用于复制任务名称 */ /* Cortex-M 常见端口中任务栈通常从高地址向低地址增长。 因此栈顶初始位置通常位于栈数组的最后一个元素。 */ pxTopOfStack pxNewTCB-pxStack (stackDepth - 1U); /* 对栈顶地址进行 8 字节对齐。 Cortex-M 上保持栈对齐有利于异常返回、函数调用和浮点上下文保存。 */ pxTopOfStack (StackType_t *) ( ((uintptr_t)pxTopOfStack) (~((uintptr_t)0x07U)) ); /* 保存任务优先级。 */ pxNewTCB-uxPriority uxPriority; /* 将任务名称复制到 TCB 中。 */ for (i 0U; i (configMAX_TASK_NAME_LEN - 1U); i) { pxNewTCB-pcTaskName[i] pcName[i]; /* 如果遇到字符串结束符则提前结束复制。 */ if (pcName[i] \0) { break; } } /* 确保任务名称以 \0 结尾防止字符串越界。 */ pxNewTCB-pcTaskName[configMAX_TASK_NAME_LEN - 1U] \0; /* 初始化任务状态列表项。 该列表项后续可挂入就绪列表、延时列表或挂起列表。 */ vListInitialiseItem((pxNewTCB-xStateListItem)); /* 设置状态列表项的拥有者为当前 TCB。 后续通过列表项可以反向找到所属任务。 */ listSET_LIST_ITEM_OWNER((pxNewTCB-xStateListItem), pxNewTCB); /* 初始化任务事件列表项。 当任务等待队列、信号量或事件组时会使用该列表项。 */ vListInitialiseItem((pxNewTCB-xEventListItem)); /* 事件列表项的拥有者同样设置为当前 TCB。 */ listSET_LIST_ITEM_OWNER((pxNewTCB-xEventListItem), pxNewTCB); /* 初始化任务栈。 pxPortInitialiseStack 与具体 CPU 架构有关 它会伪造一个任务首次运行所需的栈帧。 */ pxNewTCB-pxTopOfStack pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters ); /* 如果调用者需要任务句柄则把 TCB 地址作为句柄返回。 */ if (pxCreatedTask ! NULL) { *pxCreatedTask (TaskHandle_t)pxNewTCB; } }关键变量说明变量说明pxTaskCode任务入口函数地址pcName任务名称字符串stackDepth任务栈深度单位为StackType_tpvParameters传入任务函数的参数uxPriority任务优先级pxCreatedTask用于返回任务句柄pxNewTCB指向新任务控制块pxTopOfStack计算得到的初始栈顶地址i复制任务名称时使用的循环变量九、pxPortInitialiseStack() 的作用pxPortInitialiseStack()是任务创建中非常关键的底层函数。它和具体 CPU 架构有关通常位于 FreeRTOS 的 portable 目录中。它的作用可以概括为在任务栈中预先构造一个“假现场”使任务第一次被调度时看起来像是从中断返回后自然进入任务函数。在 Cortex-M 内核中任务切换和异常返回机制关系密切。任务第一次运行前栈中需要准备好寄存器初始值例如 PC、LR、xPSR、R0 等。这样调度器恢复任务上下文时CPU 才能正确跳转到任务入口函数执行。简化理解如下pxTopOfStack pxPortInitialiseStack( pxTopOfStack, /* 当前计算出的栈顶地址 */ pxTaskCode, /* 任务入口函数 */ pvParameters /* 任务入口参数 */ );其中pvParameters最终会作为任务函数的参数传入相当于pxTaskCode(pvParameters);只是这个调用不是普通 C 函数直接调用而是通过任务栈初始化和调度器上下文恢复间接实现。十、任务创建后的状态任务创建成功后并不是立刻运行而是先进入就绪列表。调度器启动后会从就绪列表中选择最高优先级的任务运行。简化流程如下xTaskCreateStatic() - 分配或绑定 TCB - 绑定任务栈 - 初始化任务名称 - 初始化任务列表项 - 初始化任务栈 - 加入就绪列表 - 返回任务句柄当调用vTaskStartScheduler()后调度器开始工作任务才真正参与调度。十一、任务创建相关函数总结函数作用xTaskCreateStatic()静态创建任务不使用动态内存分配vListInitialiseItem()初始化任务 TCB 中的列表项listSET_LIST_ITEM_OWNER()设置列表项所属对象通常是 TCBpxPortInitialiseStack()初始化任务栈构造任务首次运行现场vTaskStartScheduler()启动调度器vTaskDelay()让当前任务进入阻塞态延时指定 tick 数十二、动态创建与静态创建的区别创建方式TCB 内存栈内存特点xTaskCreate()FreeRTOS 动态分配FreeRTOS 动态分配使用方便但依赖堆xTaskCreateStatic()应用程序提前提供应用程序提前提供内存确定性更好静态创建更适合对内存安全和可控性要求较高的嵌入式项目。动态创建使用更方便但需要正确配置 FreeRTOS heap并关注内存分配失败问题。十三、学习小结FreeRTOS 中的任务不是简单的函数调用而是由任务函数、任务栈、任务控制块和任务列表项共同组成的运行单元。任务函数定义了任务要执行的代码任务栈保存任务运行现场TCB 保存任务状态、优先级、栈指针和任务名称列表项用于把任务挂入就绪列表、延时列表或事件等待列表。静态任务创建的核心思想是应用程序提前准备好任务栈和任务控制块FreeRTOS 将这些内存与任务函数绑定起来初始化任务运行环境然后把任务加入就绪列表。调度器启动后任务才开始按照优先级和状态进行切换运行。