RTOS-F429-HAL-(动/静态)任务的创建(2026/7/27)

目录

[一:动态任务创建与删除 API](#一:动态任务创建与删除 API)

[1:三个 API](#1:三个 API)

[2:xTaskCreate 参数](#2:xTaskCreate 参数)

3:动态创建的前提

4:函数内部干了什么

5:vTaskDelete(handle)

6:临界区

7:中断屏蔽

8:PendSV中断与SVC中断

二:动态创建工程代码逻辑:

1:工程概览

2:任务定义

3:后台硬件中断(独立于任务运行)

[4:时间轴(假设 KEY1 未按下)](#4:时间轴(假设 KEY1 未按下))

[5:如果 KEY1 被按下](#5:如果 KEY1 被按下)

6:如果写为退出缓存区前删除任务

关键时序总结

三:空闲任务介绍:

1:空闲任务的本质

2:你这个工程的情况

3:所以当前的内存分配格局是:

4:如果要全部静态创建

5:任务内存的处理

四:静态创建任务

1:区别

2:静态创建五步

3:两个接口函数的关系

4:删除函数句柄区分

5:静态任务我们创建的TCB是什么

[6:动态为什么没有 TCB 和栈](#6:动态为什么没有 TCB 和栈)

五:vTaskDelete删除任务:

1:函数原型

2:怎么用

3:内部干了什么

4:关键细节

[5:正点 start_task 的自杀](#5:正点 start_task 的自杀)

[六:Task Control Block,任务控制块](#六:Task Control Block,任务控制块)

1:任务函数名和任务名

2:参数NULL

3:句柄

4:静态任务TCB

5:静态任务和动态任务创建的区别


一:动态任务创建与删除 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:中断屏蔽

  1. Thread 模式 vs Handler 模式

Cortex-M 有两个运行模式:

模式 运行什么 进入方式
Thread 模式 普通任务代码(FreeRTOS 的 Task 函数体) 上电 / 从异常返回(EXC_RETURN)
Handler 模式 异常/中断服务函数(ISR、PendSV、SysTick) 硬件自动响应异常

你的任务代码(比如 vTaskAvTaskB始终在 Thread 模式下运行


  1. 优先级谁高谁低?

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() 那一刻已经被摘出就绪列表了,只是尸体还在等空闲任务来收。

关键时序总结

  1. task3 是"老板"------优先级最高 (4),每 5ms 必定准时醒来扫键。它占用 CPU 极短(几 μs),几乎不影响其他任务

  2. task2 居中------优先级 3,每秒打印一次,阻塞 1000ms 期间不占 CPU

  3. task1 最低------优先级 2,500ms 翻转一次蓝灯,总是在 task3 和 task2 都没事干时才运行

  4. 利用阻塞实现协作 :三个任务全部通过 vTaskDelay() 把自己挂起,谁都不占着 CPU 空转,空闲时间全部给 idle 任务(可以进低功耗)

  5. **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 地址的地址
相关推荐
无忧.芙桃2 小时前
数据结构之堆
c语言·数据结构·c++·算法·
小蒋学算法2 小时前
算法-删除元素后最大固定点数目-典型最长增长序列算法
数据结构·算法
LONGZETECH2 小时前
新能源汽车动力系统仿真硬核拆解:五系统分层架构的设计逻辑与技术权衡
c语言·开发语言·架构·汽车·汽车仿真教学软件·汽车教学软件
RD_daoyi2 小时前
Google偷偷给AI引用加了五个新功能:内联引用、悬停预览、品牌展示……但点击率真的回来了吗?
大数据·网络·人工智能·算法·安全·搜索引擎
船漏了就会沉2 小时前
后缀自动机(SAM):字符串处理的“万能瑞士军刀”
算法
醉城夜风~2 小时前
C++模板详解:从基础到高级全面解析
java·开发语言·c++
地球驾驶员2 小时前
NX二次开发C#-获取体的外表面
开发语言·c#
誰能久伴不乏3 小时前
深入理解 C++ 多态:对象模型与底层机制解析
开发语言·c++·架构
Wang's Blog3 小时前
Go-Zero项目开发30: 微服务配置中心的设计与实现
开发语言·微服务·golang·go-zero