目录
[一:动态任务创建与删除 API](#一:动态任务创建与删除 API)
[1:三个 API](#1:三个 API)
[2:xTaskCreate 参数](#2:xTaskCreate 参数)
[4:时间轴(假设 KEY1 未按下)](#4:时间轴(假设 KEY1 未按下))
[5:如果 KEY1 被按下](#5:如果 KEY1 被按下)
[6:动态为什么没有 TCB 和栈](#6:动态为什么没有 TCB 和栈)
[5:正点 start_task 的自杀](#5:正点 start_task 的自杀)
[六:Task Control Block,任务控制块](#六:Task Control Block,任务控制块)
一:动态任务创建与删除 API
1:三个 API
| API | 方式 | 栈内存 | TCB 内存 |
|---|---|---|---|
xTaskCreate() |
动态 | FreeRTOS 堆里分配 | FreeRTOS 堆里分配 |
xTaskCreateStatic() |
静态 | 你自己定义数组 | 你自己定义结构体 |
vTaskDelete() |
--- | 释放回堆 | 释放回堆 |
2:xTaskCreate 参数
objectivec
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // ① 任务函数名
const char * const pcName, // ② 任务名(调试用)
const uint16_t usStackDepth, // ③ 栈大小,单位 word
void * const pvParameters, // ④ 传给任务的参数
UBaseType_t uxPriority, // ⑤ 优先级,0 ~ configMAX_PRIORITIES-1
TaskHandle_t * const pxCreatedTask // ⑥ 任务句柄(输出)
);
| 参数 | 我们的写法 | 含义 |
|---|---|---|
pxTaskCode |
task1 |
函数名就是指针,不用 & |
pcName |
"task1" |
最长 configMAX_TASK_NAME_LEN-1 字符 |
usStackDepth |
128 |
128 words = 512 字节,局部变量多就加大 |
pvParameters |
NULL |
没参数传 NULL,有参数传结构体指针 |
uxPriority |
2 |
数字越大越优先 |
pxCreatedTask |
&Task1_Handler |
存起来,以后删任务/查状态用这个句柄 |
返回值: pdPASS = 成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY = 堆不够。
3:动态创建的前提
// FreeRTOSConfig.h
#define configSUPPORT_DYNAMIC_ALLOCATION 1 // 必须开,xTaskCreate 依赖它
#define configTOTAL_HEAP_SIZE 36*1024 // 堆总大小,所有任务栈+TCB 都从这扣
4:函数内部干了什么
xTaskCreate(task1, "task1", 128, NULL, 2, &handle)
│
├─ ① 从 FreeRTOS 堆里申请 TCB + 栈空间
│ TCB: 任务控制块(存状态、优先级、栈指针)
│ 栈: 128 × 4 = 512 字节
│
├─ ② 填 TCB 字段:函数入口、优先级、栈顶指针...
│ 栈顶部预填充寄存器初值(xPSR、PC、LR...)
│
└─ ③ 把 TCB 挂到就绪列表 pxReadyTasksLists[2]
任务立刻处于就绪态,调度器随时可以选它跑
创建完 = 立刻可跑。不是"先创建,后面再手动启动"------调度器一跑,优先级最高的就绪任务直接上场。
5:vTaskDelete(handle)
vTaskDelete(Task1_Handler); // 删别人,释放栈和 TCB
vTaskDelete(NULL); // 删自己
删完句柄立马无效------正点原子的做法是同时把句柄置 NULL 防止误用:
vTaskDelete(Task1_Handler);
Task1_Handler = NULL;
6:临界区
在实时操作系统(如 FreeRTOS、RT-Thread 等)中,临界区(Critical Section)是一段必须"一气呵成"执行的代码区域。通过进入临界区(taskENTER_CRITICAL() 等接口)和退出临界区(taskEXIT_CRITICAL()),内核会暂时屏蔽任务调度乃至可屏蔽硬件中断,保证当前代码在执行期间不会被其他任务或调度器打断。,这里乃至可屏蔽硬件优先级5~15的中断。
7:中断屏蔽
- Thread 模式 vs Handler 模式
Cortex-M 有两个运行模式:
| 模式 | 运行什么 | 进入方式 |
|---|---|---|
| Thread 模式 | 普通任务代码(FreeRTOS 的 Task 函数体) | 上电 / 从异常返回(EXC_RETURN) |
| Handler 模式 | 异常/中断服务函数(ISR、PendSV、SysTick) | 硬件自动响应异常 |
你的任务代码(比如 vTaskA、vTaskB)始终在 Thread 模式下运行。
- 优先级谁高谁低?
ARM 的优先级逻辑是"数值越小、优先级越高":
硬件优先级数值: 0 ─── 16 ─── 255 ─── 256
▲ ▲ ▲
最高(Reset/NMI) PendSV Thread 模式
(0xFF) (无异常激活)
| 执行体 | 优先级数值 | 实际含义 |
|---|---|---|
| SysTick / IRQ 0~4 | 较小 (如 16、32) | 高优先级,可打断 PendSV |
| PendSV | 0xFF (255) | 最低异常优先级,但仍高于 Thread 模式 |
| Thread 模式 | 256(等效) | 比任何异常都低,任何时候都能被异常打断 |
结论:PendSV (255) 的硬件优先级 > Thread 模式 (256)。 只要有 PendSV 挂起,CPU 在不被屏蔽、且没有更高优先级异常活动的情况下,必然会响应它。
3.我们定义的宏
ARM 优先级寄存器是 8 位,但 F429 只实现了高 4 位------所以有效值只有 16 个(0~15)。存到寄存器时要左移 4 位:
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 0x50 设 BASEPRI = 0x50,优先级 5~15 的 ISR 全被走廊门挡住,不采用0~255的中断优先级
//taskEXIT_CRITICAL() 把 BASEPRI 归零,走廊门打开,憋着的 PendSV 立刻触发切任务。
8:PendSV中断与SVC中断
PendSV 是整个系统里最低优先级的可屏蔽异常。 任何硬件中断(包括 SysTick)优先级都比它高。所以 PendSV 不会打断任何 ISR------等所有硬件中断都处理完了,CPU 空闲下来才切任务。这就是 FreeRTOS 任务切换不破坏 ISR 上下文的关键。
不需要设优先级为多少------FreeRTOS 启动时自己设的。vTaskStartScheduler() → vPortSetupTimerInterrupt() → 把 PendSV 和 SysTick 设到最低优先级15
#define configKERNEL_INTERRUPT_PRIORITY (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
PendSV 优先级 = 15 = 最低。 能打断它的中断只有硬件故障(NMI、HardFault),所有外设中断、SysTick 都在它上面。所以无论如何切任务,一定得等所有中断跑完才动手。
SVC = 从门外(裸机)进 RTOS 的门,一生只推一次。
SVC 的优先级 FreeRTOS 没动------用芯片上电的默认值。它跟 PendSV 不一样:SVC 一生只触发一次(启动第一个任务),不需要显式设优先级,能跑完就行。
二:动态创建工程代码逻辑:
1:工程概览
硬件平台: STM32F429, 180MHz 主频 RTOS: FreeRTOS,抢占式调度 + 同优先级时间片轮转 Tick 频率: 1000Hz(1 tick = 1ms)
2:任务定义
| 任务 | 优先级 | 栈大小 | 功能 |
|---|---|---|---|
| start_task | 1 | 128 word | 一次性启动任务:创建 task1/2/3 后自删除 |
| task1 | 2 | 128 word | 蓝灯 500ms 翻转 + printf |
| task2 | 3 | 128 word | 每 1000ms printf 一次 |
| task3 | 4 (最高) | 128 word | 每 5ms 扫描按键,按下 KEY1 则删除 task1 |
| idle | 0 (最低) | 128 word | FreeRTOS 自带空闲任务 |
3:后台硬件中断(独立于任务运行)
| 中断源 | 周期 | NVIC 优先级 | 作用 |
|---|---|---|---|
| SysTick | 1ms | 最低 | FreeRTOS 调度心跳 + TimingDelay 延时计数器 |
| TIM7 | 1ms | 15 (最低) | HAL_IncTick() --- HAL 库时基 |
| TIM6 | 500ms | 1 | 回调函数体目前为空,占位 |
| EXTI0 (KEY1) | 异步 | --- | 软消抖后翻转红灯 |
| EXTI15_10 (KEY2) | 异步 | --- | 软消抖后翻转绿灯 |
| USART1 + DMA | 异步 | --- | DMA + IDLE 中断接收变长帧 |
4:时间轴(假设 KEY1 未按下)
objectivec
T=0 调度器启动
├─ start_task (prio=1) 运行
│ ├─ 进入临界区
│ ├─ xTaskCreate(task1, prio=2) ← 就绪
│ ├─ xTaskCreate(task2, prio=3) ← 就绪
│ ├─ xTaskCreate(task3, prio=4) ← 就绪
│ ├─ 退出临界区
│ └─ vTaskDelete(NULL) ← 自杀
│
├─ task3 (prio=4, 最高) 立即抢占运行
│ └─ Key_Scan → 未按下 → vTaskDelay(5) 阻塞
│
├─ task2 (prio=3) 运行
│ └─ printf("task2 正在运行!") → vTaskDelay(1000) 阻塞
│
└─ task1 (prio=2) 运行
└─ printf("task1 正在运行!") → LED_B ON → vTaskDelay(500) 阻塞
T=0~5ms 全部用户任务阻塞 → 空闲任务运行
T=5ms task3 到期唤醒
├─ Key_Scan → 未按下
└─ vTaskDelay(5) 阻塞 ← 此时 task1/2 仍在阻塞,空闲任务运行
T=10ms task3 唤醒 → 同上 → vTaskDelay(5)
T=15ms task3 唤醒 → 同上 → vTaskDelay(5)
... 每 5ms 重复一次 ...
T=500ms task1 到期!但 task3 也恰好在 T=500ms 到期(500 是 5 的倍数)
├─ task3 (prio=4) 先运行 → Key_Scan → vTaskDelay(5)
└─ task1 (prio=2) 后运行
└─ printf("task1 正在运行!") → LED_B OFF → vTaskDelay(500)
T=1000ms task2 + task1 同时到期!
├─ task2 (prio=3) 先运行
│ └─ printf("task2 正在运行!") → vTaskDelay(1000) ← 下次 T=2000ms
└─ task1 (prio=2) 后运行
└─ printf("task1 正在运行!") → LED_B ON → vTaskDelay(500) ← 下次 T=1500ms
T=1500ms task1 到期
└─ printf("task1 正在运行!") → LED_B OFF → vTaskDelay(500)
T=2000ms task2 + task1 同时到期 → 同上模式循环...
5:如果 KEY1 被按下
T=X task3 在某次 5ms 扫描中检测到 KEY1=按下
├─ printf("删除 task1!")
├─ vTaskDelete(Task1_Handler) ← task1 被杀死,栈和TCB释放
└─ vTaskDelay(5)
此后 只剩 task2 (prio=3) 和 task3 (prio=4)
├─ task3: 仍然每 5ms 扫键
└─ task2: 仍然每 1000ms 打印
task1 不复存在,蓝灯不再闪烁
6:如果写为退出缓存区前删除任务
objectivec
补充一下自己删除自己的函数流程:
vTaskDelete(NULL);如果这句代码在taskEXIT_CRITICAL();之前
那么正确流程:
start_task 优先级是 START_TASK_PRIO = 1,你代码里写的。
taskEXIT_CRITICAL() ← BASEPRI 归零,PendSV 立刻触发
→ 调度器找最高优先级就绪任务 = task3(优先级 4)
→ 切 task3
→ task3 干完 Delay(10) → task2 → task1 → 都睡
→ 空闲任务跑 → 扫到待删除列表里的 start_task
→ 释放 start_task 的 TCB + 栈 → 彻底消失
vTaskDelete(NULL) 不是当场执行完的------分两步:设 PendSV 挂起(当场)→
→真正回收内存(空闲任务异步完成)。
start_task 在 taskEXIT_CRITICAL() 那一刻已经被摘出就绪列表了,只是尸体还在等空闲任务来收。
关键时序总结
-
task3 是"老板"------优先级最高 (4),每 5ms 必定准时醒来扫键。它占用 CPU 极短(几 μs),几乎不影响其他任务
-
task2 居中------优先级 3,每秒打印一次,阻塞 1000ms 期间不占 CPU
-
task1 最低------优先级 2,500ms 翻转一次蓝灯,总是在 task3 和 task2 都没事干时才运行
-
利用阻塞实现协作 :三个任务全部通过
vTaskDelay()把自己挂起,谁都不占着 CPU 空转,空闲时间全部给 idle 任务(可以进低功耗) -
**TIM6 中断(500ms)**目前回调是空的,虽然每 500ms 产生一次中断,但不做任何事------是个预留的钩子位
三:空闲任务介绍:
1:空闲任务的本质
空闲任务是 FreeRTOS 内核的一部分,由 vTaskStartScheduler() 自动创建。它不是可选的------调度器必须保证"任何时候至少有一个任务能跑"。当所有用户任务都阻塞(vTaskDelay、等信号量等)时,空闲任务就顶上。
2:你这个工程的情况
你的 FreeRTOSConfig.h 同时开了两个宏:
#define configSUPPORT_DYNAMIC_ALLOCATION 1 // xTaskCreate 可用(动态创建任务)
#define configSUPPORT_STATIC_ALLOCATION 1 // xTaskCreateStatic 可用(静态创建任务)
当 configSUPPORT_STATIC_ALLOCATION = 1 时,内核会优先调用你提供的回调来获取空闲任务的内存。你的 freertos_demo.c:28-48 正好提供了这个回调:
objectivec
static StaticTask_t xIdleTaskTCB;
static StackType_t uxIdleTaskStack[configMINIMAL_STACK_SIZE]; // 128 × 4 = 512 字节
void vApplicationGetIdleTaskMemory(...)
{
*ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
*ppxIdleTaskStackBuffer = uxIdleTaskStack;
*pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}
3:所以当前的内存分配格局是:
| 任务 | 创建方式 | 内存来源 |
|---|---|---|
| start_task / task1 / task2 / task3 | xTaskCreate() |
FreeRTOS 堆(36KB) |
| 空闲任务 | 内核自动创建 | 静态 (vApplicationGetIdleTaskMemory 提供的全局数组) |
目前我们的工程:任务走动态,空闲任务走静态------混着用完全没问题。
4:如果要全部静态创建
你把 xTaskCreate 换成 xTaskCreateStatic,自己传 TCB 和栈的地址,那些用户任务就也走静态了。但空闲任务依然是内核自动创建的,你还得保留 vApplicationGetIdleTaskMemory ------这是 configSUPPORT_STATIC_ALLOCATION=1 的强制要求,否则编译报错。
顺带一提: 如果你以后开了
configUSE_TIMERS=1(软件定时器),还得多提供一对vApplicationGetTimerTaskMemory(),套路跟空闲任务一模一样。
5:任务内存的处理
1、当传入参数为NULL,则代表删除任务自身(当前正在运行的任务)
2、空闲任务会负责释放被删除的(动态任务)中由系统分配的内存,但是由用户在(静态任务)删除前申请的内存,则需要由用户在(静态任务)被删除前提前释放,否则将导致内存泄露
动态任务(xTaskCreate): 栈和 TCB 是 FreeRTOS 堆里分配的。删自己 → 挂到待删除列表 → 空闲任务来收尸(释放 TCB + 栈回堆)。你什么都不用管。
静态任务(xTaskCreateStatic): 栈和 TCB 是你自己定义的全局数组和结构体。内存从来不在 FreeRTOS 堆里,空闲任务不会、也没权限释放你定义的全局变量。
// 你定义的
static StaticTask_t xIdleTaskTCB; // ← 全局变量,永远在那
static StackType_t uxIdleTaskStack[128]; // ← 全局变量,永远在那
删了静态任务后,这块内存还是你的------可以拿来创新任务,也可以不管它(全局变量反正不占堆)。不存在"释放",只存在"标记为可用"。
官方说法就是你理解的那样,两种情况之间毫无关系。
空闲任务只管FreeRTOS 自己分配的 (任务栈、TCB,全是 pvPortMalloc 挖的),动态任务创建时自动分配,删除后空闲任务自动回收。而静态任务的内存(你自己定义的全局数组)本来就从未进过 FreeRTOS 堆、空闲任务无从管起,也不该管------这块内存根本不需要"释放"。
官方那段话后一句是提醒:如果你在任务函数里 malloc 了一段缓存、又把任务删了,没人替你 free,缓存就会变成孤儿。跟栈和 TCB 无关。
官网说的是任务函数内部 malloc 的:
// task 函数里申请了一块临时内存
static void task1(void *pvParameters)
{
uint8_t *buf = (uint8_t *)malloc(1024); // ← 这是从系统堆里挖的
// ...用 buf...
vTaskDelete(NULL); // ← 自杀
// buf 泄漏!FreeRTOS 只回收 TCB+栈,不会替你 free(buf)
}
跟前面静态任务时解释的一样,vTaskDelete 只回收 FreeRTOS 自己分配的东西------TCB 和任务栈。你在任务内部自己 malloc 的是从 C 库的堆里拿的,FreeRTOS 根本不认识它,更不可能替你 free。
正确做法是任务自杀前先 free(buf),或者干脆用全局变量和动态分配共存的方式------不过这种情况极少,大部分直接建个数组就完了。
四:静态创建任务
1:区别
动态创建 (xTaskCreate) |
静态创建 (xTaskCreateStatic) |
|
|---|---|---|
| 栈内存 | FreeRTOS 堆里自动分配 | 你手动定义数组 |
| TCB 内存 | FreeRTOS 堆里自动分配 | 你手动定义结构体 |
| 用完释放 | vTaskDelete 自动归还堆 |
你自己管(或不管,反正全局变量永不释放) |
| 前提宏 | configSUPPORT_DYNAMIC_ALLOCATION = 1 |
configSUPPORT_STATIC_ALLOCATION = 1 |
| 额外要求 | 无 | 必须实现两个接口函数 |
2:静态创建五步
objectivec
① 开宏:configSUPPORT_STATIC_ALLOCATION = 1
② 定义内存:
static StaticTask_t xIdleTaskTCB; // 空闲任务 TCB
static StackType_t uxIdleTaskStack[128]; // 空闲任务栈
③ 实现接口函数(FreeRTOS 内部会调):
vApplicationGetIdleTaskMemory() → 返回空闲任务的栈和 TCB
vApplicationGetTimerTaskMemory() → 返回定时器任务的栈和 TCB(开 configUSE_TIMERS 才需要),我们是关了软件定时器的宏
④ 定义任务函数:
static void task1(void *pvParameters) { for(;;) { ... } }
⑤ 创建任务:
xTaskCreateStatic(task1, "task1", 128, NULL, 1, task1_stack, &task1_TCB);
函数原型:
TaskHandle_t xTaskCreateStatic(
TaskFunction_t pxTaskCode, // ① 任务入口函数:必须符合 void func(void*) 原型
const char * const pcName, // ② 任务名(纯调试用):长度不超configMAX_TASK_NAME_LEN(你设的16)
const uint32_t usStackDepth, // ③ 栈大小:
void * const pvParameters, // ④ 传给任务的参数:
UBaseType_t uxPriority, // ⑤ 任务优先级:数值越大优先级越高,范围0~31
StackType_t * const puxStackBuffer, // ⑥ 【静态专属】栈起始地址
StaticTask_t * const pxTaskBuffer // ⑦ 【静态专属】TCB(任务控制块)
);
返回值:TaskHandle_t(任务句柄)
3:两个接口函数的关系
FreeRTOS 启动时内部流程:
│
├─ 创建空闲任务
│ └─ 调 vApplicationGetIdleTaskMemory(&tcb, &stack, &size)
│ 拿你提供的 TCB 和栈 → 创建空闲任务
│
└─ 创建定时器任务(如果 configUSE_TIMERS=1)
└─ 调 vApplicationGetTimerTaskMemory(&tcb, &stack, &size)
拿你提供的 TCB 和栈 → 创建定时器任务
我们 configUSE_TIMERS = 0,所以只写空闲任务那一个就够了。
4:删除函数句柄区分
vTaskDelete(StartTask_Handler) 改 vTaskDelete(NULL)------没区别
做的事完全一样 ------把任务从所有列表里摘掉,设 PendSV 切走。区别只是谁被删:
vTaskDelete(StartTask_Handler); // 删别人(通过句柄找到 TCB)
vTaskDelete(NULL); // 删自己(拿当前任务的 TCB)
底层都是找到同一个 TCB(start_task 的),结果一模一样。正点动态版用 NULL、静态版用句柄------就是因为静态版句柄在 xTaskCreateStatic 返回时已经拿到了,直接用名字删更可读,用vTaskDelete(NULL)代码逻辑也是一样的。
5:静态任务我们创建的TCB是什么
TCB =任务控制块 = 任务的档案。FreeRTOS 调度器不看人、不看函数名------它只看 TCB。
StaticTask_t(TCB)核心字段:
├─ pxTopOfStack → 栈顶指针——切任务时从这弹寄存器
├─ uxPriority → 任务优先级(2/3/4)
├─ xStateListItem → 挂在哪个状态链表里(就绪/阻塞/挂起)
├─ xEventListItem → 挂在哪个事件等待链表里(等队列/等信号量)
├─ pcTaskName → 任务名("task1",调试用)
├─ uxTCBNumber → 任务编号(比名字好查)
└─ ulRunTimeCounter → 跑了多久(统计用)
你创建任务时传的栈大小、优先级、任务名,全部写进这个结构体。 调度器切换时:读这个 TCB → 知道它优先级是 4 → 知道它的栈在哪 → 弹栈 → 跳转运行。切走时:保存寄存器到它的栈 → 更新 TCB → 选下一个 TCB。
6:动态为什么没有 TCB 和栈
有,只是你看不见。 xTaskCreate 内部第一件事就是 pvPortMalloc:
// xTaskCreate 内部(简化):
BaseType_t xTaskCreate(...)
{
// ① 从 FreeRTOS 堆里挖一块内存当 TCB
TCB_t *pxNewTCB = (TCB_t *)pvPortMalloc(sizeof(TCB_t));
// ② 从 FreeRTOS 堆里挖一块内存当任务栈
StackType_t *pxStack = (StackType_t *)pvPortMalloc(usStackDepth * 4);
// ③ 填 TCB 字段、把栈和 TCB 挂起来
prvInitialiseNewTask(pxTaskCode, pcName, pxStack, pxNewTCB, ...);
}
动态:堆里挖 → 用完空闲任务 free 回收。静态:你自己定义全局数组 → 永远在那,不挖也不收。
五:vTaskDelete删除任务:
1:函数原型
void vTaskDelete(TaskHandle_t xTaskToDelete);
| 参数 | 含义 |
|---|---|
xTaskToDelete |
要删的任务句柄。NULL = 删自己 |
2:怎么用
两步:开宏 + 调函数。
// FreeRTOSConfig.h
#define INCLUDE_vTaskDelete 1
// 代码里
vTaskDelete(Task1_Handler); // 删别人
vTaskDelete(NULL); // 删自己
3:内部干了什么
vTaskDelete(handle)
│
├─ ① 通过句柄找到对应的 TCB(任务控制块)
│
├─ ② 把任务从所有列表中踢出去
│ 就绪列表、阻塞列表、挂起列表、事件等待列表
│ → 这个任务从此消失,调度器再也不会选中它
│
├─ ③ 删的是自己(handle == NULL)?
│ ✅ 自己 → 把自己丢进"待删除列表",等空闲任务来收尸
│ 空闲任务会释放 TCB + 栈内存
│ ❌ 别人 → 立刻释放别人的 TCB + 栈内存,任务总数-1
│
└─ ④ 更新阻塞超时时间
防止刚才被删的任务恰好是下一个要超时唤醒的
4:关键细节
内存谁管?
| 内存类型 | 谁释放 |
|---|---|
| TCB + 任务栈(FreeRTOS 分配的) | 空闲任务自动释放,你不用管 |
任务里自己 malloc 的 |
你自己在删之前 free,不然内存泄漏 |
// 正确做法:删别人前,先让人家把资源清了
// 错误做法:任务里 malloc 了一坨,外面直接 vTaskDelete → 泄漏
删完句柄置 NULL:
vTaskDelete(Task1_Handler);
Task1_Handler = NULL; // 防止后面误调 vTaskSuspend(NULL_Task)
5:正点 start_task 的自杀
static void start_task(void *pvParameters)
{
xTaskCreate(task1, ...);
xTaskCreate(task2, ...);
xTaskCreate(task3, ...);
vTaskDelete(NULL); // 三个工作任务已就位,自己没用了,自杀
}
三个工作任务的句柄是全局变量 → TCB 不会被释放。自杀的 start_task 自己的 TCB + 栈被空闲任务回收。
六:Task Control Block,任务控制块
TCB = Task Control Block,任务控制块就是一个 C 结构体,里面存了调度器需要的所有信息。 调度器不看函数名、不看代码------它只看 TCB:
objectivec
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 栈顶指针------切换时弹/压寄存器 */
StackType_t *pxStack; /* 栈起始地址------删除时释放用 */
UBaseType_t uxPriority; /* 优先级 ------谁先跑靠这个 */
ListItem_t xStateListItem; /* 链表节点 ------挂在哪个状态链表 */
ListItem_t xEventListItem; /* 事件节点 ------等队列/信号量用 */
uint32_t ulRunTimeCounter;/* 运行时间------统计用 */
char pcTaskName[16]; /* 任务名 ------调试时一眼看出 */
// ... 还有任务通知值、TCB编号、临界嵌套计数等
} TCB_t;
xTaskCreate 参数
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // ① 任务函数名
const char * const pcName, // ② 任务名(调试用)
const uint16_t usStackDepth, // ③ 栈大小,单位 word
void * const pvParameters, // ④ 传给任务的参数
UBaseType_t uxPriority, // ⑤ 优先级,0 ~ configMAX_PRIORITIES-1
TaskHandle_t * const pxCreatedTask // ⑥ 任务句柄(输出)
);
两者的联系:
TCB是控制任务的一个大结构体,我们创建任务xTaskCreate 是一个函数,把要初始化的内容填进去,后续控制任务,就不直接操控TCB怕乱改东西,就控制句柄来控制任务
我们填入6个参数:任务函数名 ,任务名,栈大小,参数这个一会有例子解释,优先级,句柄
1:任务函数名和任务名
函数名 = CPU 跳去哪。任务名 = 调试时怎么称呼它。
xTaskCreate(task1, // ① 函数指针: "CPU,来跑这个函数"
"task1", // ② 字符串: "调试器,这人叫 task1"
128, NULL, 2, &Task1_Handler);
第一个是函数入口地址------PendSV 弹栈时 PC 寄存器 = 这个地址,CPU 就跳过去跑了。不能乱写。
第二个是调试用的标签 ------存在 TCB.pcTaskName[16] 里。你挂调试器、调 vTaskList()、IDE 的 FreeRTOS 任务视图里看到的都是这个字符串。可以不跟函数同名,但同名方便定位。
2:参数NULL
你写了 NULL 是因为 task1、task2、task3 各写了一个函数。如果三个灯,逻辑一模一样:
// 不用写三个函数,一个函数用参数区分:
static void Led_Task(void *pvParameters)
{
uint32_t pin = (uint32_t)pvParameters; // 把参数转回来
for (;;)
{
HAL_GPIO_TogglePin(GPIOH, pin); // 传哪个引脚就翻哪个
vTaskDelay(500);
}
}
// 创建三个任务,同一份代码:
xTaskCreate(Led_Task, "LedR", 128, (void *)GPIO_PIN_10, 1, &h1); // 传红灯
xTaskCreate(Led_Task, "LedG", 128, (void *)GPIO_PIN_11, 1, &h2); // 传绿灯
xTaskCreate(Led_Task, "LedB", 128, (void *)GPIO_PIN_12, 1, &h3); // 传蓝灯
一个函数,三个任务,参数决定谁亮谁灭。 我们目前每个任务逻辑不一样、分三个函数写,没共同参数,就全填 NULL。
3:句柄
// ① 你定义空指针变量
TaskHandle_t Task1_Handler; // 现在值是垃圾,不知道指向哪
// ② 传句柄变量的地址进去
xTaskCreate(task1, ..., &Task1_Handler); // 告诉函数:往这个地址写东西
// ↑
// Task1_Handler 变量的地址
// ③ FreeRTOS 内部,这一行帮你完成:
*pxCreatedTask = pxNewTCB;
// ↑ ↑
// 解引用,写到 新分配的 TCB 地址
// Task1_Handler 里
// 调用完回到你手里:
// Task1_Handler = TCB 的地址 ✅ 有值了
你提供空信封(句柄变量),传真地址给 FreeRTOS(&句柄),FreeRTOS 往信封里塞 TCB 的地址------全程不需要你碰 TCB
4:静态任务TCB
objectivec
StaticTask_t 就是 TCB_t 换了个马甲,方便你在外面声明静态 TCB:
// FreeRTOS 内部 task.h:
typedef struct tskTaskControlBlock StaticTask_t;
// ↑ 同一个结构体
// 所以这两行完全等价:
StaticTask_t Task3_TCB; // 用这个(FreeRTOS 给你的名字)
struct tskTaskControlBlock Task3_TCB; // 等于这个(原名)
TCB 的真实结构体定义在 tasks.c 里,不对外暴露。 FreeRTOS 可以改 TCB 内部字段而不影响你的代码。StaticTask_t 这个空壳只有一个用途------让你声明变量,FreeRTOS 知道它有多大就行。你永远不需要访问 StaticTask_t 的字段,操作任务照旧通过句柄。
5:静态任务和动态任务创建的区别
objectivec
TaskHandle_t xTaskCreateStatic(
TaskFunction_t pxTaskCode, // ① 任务入口函数:必须符合 void func(void*) 原型
const char * const pcName, // ② 任务名(纯调试用):长度不超configMAX_TASK_NAME_LEN(你设的16)
const uint32_t usStackDepth, // ③ 栈大小:
void * const pvParameters, // ④ 传给任务的参数:
UBaseType_t uxPriority, // ⑤ 任务优先级:数值越大优先级越高,范围0~31
StackType_t * const puxStackBuffer, // ⑥ 【静态专属】栈起始地址
StaticTask_t * const pxTaskBuffer // ⑦ 【静态专属】TCB(任务控制块)
);
返回值:TaskHandle_t(任务句柄)
动态任务创建函数
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // ① 任务入口函数:必须符合 void func(void*) 原型
const char * const pcName, // ② 任务名(纯调试用):长度不超configMAX_TASK_NAME_LEN(你设的16)
const uint16_t usStackDepth, // ③ 栈大小:单位「字(word)=4字节」,比如128就是512字节;
void * const pvParameters, // ④ 传给任务的参数:
UBaseType_t uxPriority, // ⑤ 任务优先级:数值越大优先级越高,范围0~31(你设的configMAX_PRIORITIES=32)
TaskHandle_t * const pxCreatedTask // ⑥ 任务句柄(输出参数):二级指针!内核把TCB地址(就是句柄)存到你传的指针里,不用就传NULL
);
返回值:pdPASS 就是创建成功;失败只会返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY,说明堆不够,调大你之前设的configTOTAL_HEAP_SIZE=36 * 1024就行。
objectivec
Task3_Handler = xTaskCreateStatic((TaskFunction_t) start_task,
(char *) "start_task",
(uint32_t) START_TASK_STACK_SIZE,
(void *) NULL,
(UBaseType_t) START_TASK_PRIO,
(StackType_t *) StartTask_Stack, /* 传栈地址 */
(StaticTask_t *) &StartTask_TCB); /* 传 TCB 地址 */
TaskHandle_t Task3_Handler; /* 任务3句柄(按键) */
StackType_t Task3_Stack[TASK3_STACK_SIZE]; /* 任务3栈(512B) */
StaticTask_t Task3_TCB; /* 任务3控制块 */
StartTask_Stack 栈 是我们自己创建的全局数组,Task3_TCB 是我们直接创建的结构体,两个传进去
然后 Task3_Handler 用做返回值接收 TCB 地址的地址