最近开始啃 FreeRTOS,上一篇用的都是动态方式创建任务,这次专门上手试了静态任务创建,顺便结合之前做多任务删除的例子做了一遍实验。静态分配内存也是实际项目里常用的写法,稳定性更好,还能避免内存碎片,这里把完整代码、运行效果和实操步骤记录下来,方便自己后续回顾,也给刚入门的小伙伴做个参考。
一、实验整体说明
本次实验基于 STM32 搭配 FreeRTOS,整体功能和之前的例子差不多,核心改动是把动态创建任务换成了静态创建。 实验逻辑:
- 一共设计三个任务:开始任务、任务 1、任务 2;
- 开始任务负责创建另外两个业务任务,完成工作后自行删除;
- 任务 1 优先级更高,控制 LED0 每秒翻转一次,同时做计数,当计数到 5 时,调用接口删除任务 2;
- 任务 2 控制 LED1 每秒闪烁,正常运行直到被任务 2 删除;
- 任务被删除后,对应的 LED 停止闪烁,串口也不再输出该任务的日志。
额外提一点,选择静态创建任务,就需要手动给 FreeRTOS 自带的空闲任务、软件定时器任务分配内存,这也是静态模式和动态模式最大的区别之一。
二、完整实验代码
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "FreeRTOS.h"
#include "task.h"
// 开始任务相关配置
#define START_TASK_SIZE 120
#define START_TASK_PRIO 1
StackType_t StartTaskStack[START_TASK_SIZE];
StaticTask_t StartTaskTCB;
TaskHandle_t StartTask_Handle;
void start_task( void * pvParameters );
// 任务1相关配置
#define TASK1_TASK_SIZE 120
#define TASK1_TASK_PRIO 3
StackType_t Task1TaskStack[START_TASK_SIZE];
StaticTask_t Task1TaskTCB;
TaskHandle_t Task1Task_Handle;
void task1_task( void * pvParameters );
// 任务2相关配置
#define TASK2_TASK_SIZE 120
#define TASK2_TASK_PRIO 2
StackType_t Task2TaskStack[START_TASK_SIZE];
StaticTask_t Task2TaskTCB;
TaskHandle_t Task2Task_Handle;
void task2_task( void * pvParameters );
// 静态方式:为空闲任务分配内存
static StaticTask_t IdleTaskTCB;
static StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer,
StackType_t ** ppxIdleTaskStackBuffer,
uint32_t * pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer = &IdleTaskTCB;
*ppxIdleTaskStackBuffer = IdleTaskStack;
*pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}
// 静态方式:为定时器任务分配内存
static StaticTask_t TimerTaskTCB;
static StackType_t TimerTaskStack[configTIMER_TASK_STACK_DEPTH];
void vApplicationGetTimerTaskMemory( StaticTask_t ** ppxTimerTaskTCBBuffer,
StackType_t ** ppxTimerTaskStackBuffer,
uint32_t * pulTimerTaskStackSize )
{
*ppxTimerTaskTCBBuffer = &TimerTaskTCB;
*ppxTimerTaskStackBuffer = TimerTaskStack;
*pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH;
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); // 配置中断分组
delay_init(); // 初始化延时函数
uart_init(115200); // 串口初始化,波特率115200
LED_Init(); // LED硬件初始化
// 静态创建开始任务
StartTask_Handle = xTaskCreateStatic( start_task,
"start_task",
START_TASK_SIZE,
NULL,
START_TASK_PRIO,
StartTaskStack,
&StartTaskTCB );
vTaskStartScheduler(); // 启动任务调度器
}
// 开始任务:统一创建其他业务任务
void start_task( void * pvParameters )
{
// 静态创建任务1
Task1Task_Handle = xTaskCreateStatic( task1_task,
"task1_task",
TASK1_TASK_SIZE,
NULL,
TASK1_TASK_PRIO,
Task1TaskStack,
&Task1TaskTCB );
// 静态创建任务2
Task2Task_Handle = xTaskCreateStatic( task2_task,
"task2_task",
TASK2_TASK_SIZE,
NULL,
TASK2_TASK_PRIO,
Task2TaskStack,
&Task2TaskTCB );
vTaskDelete(StartTask_Handle); // 任务创建完成,删除自身
}
// 任务1:闪烁LED0,计数达标后删除任务2
void task1_task( void * pvParameters )
{
char Task1_num = 0;
while(1)
{
Task1_num++;
if(Task1_num == 5)
{
printf("Task2 is delete %d \r\n",Task1_num);
vTaskDelete(Task2Task_Handle);
}
LED0 = -LED0;
vTaskDelay(1000);
printf("Task1 is running %d \r\n",Task1_num);
}
}
// 任务2:闪烁LED1,正常运行直至被删除
void task2_task( void * pvParameters )
{
char Task2_num = 0;
while(1)
{
Task2_num++;
LED1 = -LED1;
vTaskDelay(1000);
printf("Task2 is running %d \r\n",Task2_num);
}
}
三、代码逐段解读
1. 任务基础定义部分
和动态创建任务一样,每个任务都要先确定堆栈大小、运行优先级。但静态模式下,我们需要手动定义两个关键数组和变量: StackType_t 类型数组用来充当任务栈,相当于提前给任务划分好运行时的栈空间;StaticTask_t 是任务控制块 TCB,系统会在这里记录任务状态、栈指针等核心信息。最后再定义任务句柄和任务函数声明,句柄主要用来后续执行删除、挂起这类操作。
这里我设置的优先级:任务 1 (3) > 任务 2 (2) > 开始任务 (1),优先级数值越大,CPU 调度时越优先执行。
2. 空闲任务与定时器任务内存分配
这是静态创建任务必须补充的代码。FreeRTOS 系统内部自带空闲任务和定时器任务,动态模式下系统会自动为它们申请内存,切换到静态模式后,就需要我们开发者手动分配栈空间和 TCB,并且实现对应的回调函数,把分配好的内存地址交给系统。如果省略这部分代码,程序编译会直接报错,这也是新手很容易踩的坑。
3. main 函数
主函数的逻辑和常规写法保持一致,先完成中断、延时、串口、LED 这些底层硬件初始化。之后调用 xTaskCreateStatic 静态函数创建开始任务,最后调用 vTaskStartScheduler 开启调度器。调度器启动之后,程序就完全交由 FreeRTOS 管理,不再按照裸机的顺序执行。
4. 开始任务
这个任务相当于一个 "任务分发器",系统启动后最先运行它。它的工作很简单,依次静态创建任务 1 和任务 2,等两个业务任务都创建完毕,就调用删除接口把自己删掉,释放资源,不会一直占用系统开销。这也是工程里比较规范的写法。
5. 两个业务任务
任务 1 是整个实验的核心,内部做了计数,每运行一次数值加一,延时 1 秒实现 LED0 周期性闪烁。当计数到 5 的时候,就通过任务句柄删除任务 2。 任务 2 逻辑比较简单,循环翻转 LED1,同时通过串口打印运行信息。一旦被删除,内部的死循环就会彻底停止,LED1 定格在当前状态,串口也不再有对应输出。
另外提一句,代码里两个任务都直接调用了printf,运行时会出现打印内容错乱的情况,这是因为printf不支持多任务并发调用,属于线程不安全函数。日常学习可以忽略,要是做正式项目,建议加上互斥信号量做保护。当然了设置好优先级问题也不大。
四、实际运行现象
- 程序刚运行的前 5 秒,LED0、LED1 都会每隔 1 秒翻转一次,串口终端同时打印两个任务的运行日志,内容会有穿插错乱的情况;
- 当任务 1 计数到 5 时,串口打印 "Task2 is delete",同时任务 2 被删除;
- 5 秒之后,LED1 保持固定状态不再闪烁,串口也只会输出任务 1 的日志,LED0 继续正常闪烁。

五、总结:FreeRTOS 静态任务创建完整步骤
对比动态创建任务,静态方式全程由开发者手动分配内存,可控性更强,没有内存碎片问题,更适合正式产品开发。结合本次实验,整理出通用的静态任务创建步骤,后续写代码可以直接套用:
-
任务参数宏定义 针对每一个任务,用宏定义指定任务堆栈大小、任务运行优先级,数值根据实际需求调整。
-
定义静态内存载体 为每个任务分别定义
StackType_t类型数组(任务栈)和StaticTask_t类型变量(任务控制块 TCB),同时定义TaskHandle_t类型的任务句柄,用于后续操作任务。 -
声明任务函数 提前声明任务对应的执行函数,任务函数格式固定,参数为
void * pvParameters。 -
补充系统任务内存(必做) 手动为 FreeRTOS 内置的空闲任务、定时器任务分配栈空间与 TCB,并且实现
vApplicationGetIdleTaskMemory和vApplicationGetTimerTaskMemory两个回调函数,将内存地址传递给系统。 -
调用静态创建函数 在合适的位置(一般放在 main 函数或者专门的开始任务中),调用
xTaskCreateStatic函数创建任务,依次传入任务函数、任务名、栈大小、入口参数、优先级、栈数组地址、TCB 地址,函数返回值赋值给任务句柄。 -
编写任务业务逻辑 每个任务函数内部必须嵌套
while(1)死循环,保证任务可以持续运行,在循环里编写闪烁、打印、数据处理等具体功能,配合vTaskDelay实现延时,主动让出 CPU。 -
启动任务调度器 所有任务创建完成后,在 main 函数末尾调用
vTaskStartScheduler,正式开启 FreeRTOS 多任务调度。