返回上一级
1834 字
9 分钟
任务的创建、参数与删除

p18 ~ p21(约 79 分钟)

声光色影#

配套源码第 5 个程序。四个任务,名字就说明了各自干什么:

声 无源蜂鸣器放《孤勇者》
光 LED 闪烁
色 全彩 LED 渐变调色
影 LCD 显示遥控器键值

另外还有一个默认任务负责红外遥控接收。烧写后能听到音乐、看到灯闪和变色、按遥控器屏幕有反应。

任务要三样东西:函数(做什么)、栈(每个任务独享)、优先级(不是必需的,但决定谁先跑)。优先级可以这么理解:妈妈在喂饭,厨房着火了,她会放下碗先去灭火——火比喂饭急。任务在内核里用 TCB(Task Control Block)结构体串成链表来管理。

动态创建和静态创建#

两套创建函数:

xTaskCreate 栈和 TCB 都由内核动态分配
xTaskCreateStatic 栈 buffer 和 TCB 都由用户提供

参数是同一套:函数指针、任务名、栈深度、参数、优先级、句柄输出。

usStackDepth 的单位是 word,不是字节。传 128 表示 128×4 = 512 字节。静态版也要传栈大小,但那个值只是告诉函数「你给我的这块 buffer 有多大」。

有个坑:静态版的返回值是句柄,不是 BaseType_t,所以不能写 if (xTaskCreateStatic(...) == pdPASS)。第一版就是这么写然后编译报错的,正确做法是把返回的句柄存起来。

实验里的分工:声音任务用 xTaskCreate,名字 sound_task,栈 128,参数 NULL,优先级 tskIDLE_PRIORITY + normal。光和色两个任务用 xTaskCreateStatic,各自定义:

static StackType_t light_task_stack[128];
static StaticTask_t light_task_tcb;

用 StackType_t 这种 32 位类型声明数组,就不用手动乘 4 了。

播放函数单独放在新建的 music.c 里,从开源仓库「孤勇者 无源蜂鸣器 STM32」搬过来,用 HAL 定时器设频率、占空比 50%,延时用 vTaskDelay。蜂鸣器要接无源那只——贴白纸的是有源的。

跑起来有个现象:四个任务一起跑,音乐明显卡顿迟钝;把其他任务注释掉只留音乐任务,声音立刻流畅。这个问题留到后面解决。

栈要开多大#

栈里放三样东西:

① 返回地址和其他寄存器
② 局部变量
③ 任务被切换时保存的现场(16 个寄存器 × 4 = 64 字节)

局部变量多少看代码,大数组、大 buffer 都很吃栈;返回地址和寄存器看函数调用深度。

估算的办法是找最复杂的那条调用链。每级函数最多保存 9 个「被调用者寄存器」——R4 ~ R11 共 8 个加 LR,9 × 4 = 每级 36 字节。反汇编里实际看到的是每级保存 5~6 个,36 是理论上限。所以 5 级调用大约 5 × 36。

这里有个容易搞错的地方:用栈最多的不一定是调用链最深的那个。如果某个比较浅的函数里定义了 1000 字节的局部 buffer,这条短链反而最耗栈。要找的是「局部变量最多的函数」。

现场保存那部分基本恒定 64 字节。

以音乐播放为例:假设 4 层调用,4 × 36 = 144 字节;局部变量约 32 字节(含一个 18 字节的结构体);现场 64 字节;加起来约 250 字节。给的栈是 128 word = 512 字节,比 250 大,粗估够用。这只是粗算,精确办法(栈水位)后面再说。

用参数区分任务#

第 6 个程序。同一个函数创建多个任务,靠参数区分行为。原理是:即使执行的是同一个函数,每个任务有自己的栈,函数里的局部变量各有各的副本。只有访问全局变量时才需要互斥。

实验是同一个 lcd_print_task(void *param) 创建三个任务,在屏幕不同位置打印:

typedef struct {
int8_t x;
int8_t y;
char name[16]; /* 屏幕每行最多 16 个字符 */
} task_print_info;

三个 static 全局结构体:

task1_info = {0, 0, "Task1"};
task2_info = {0, 3, "Task2"};
task3_info = {0, 6, "Task3"};

创建时分别作为 pvParameters 传进去,函数里再转回来:

task_print_info *p = (task_print_info *)param;

打印逻辑有点绕:lcd_print_string(x, y, p->name) 会返回打印了多少个字符,拿到长度 len 之后再打印「:」,位置要加上 len,然后在同一行打印局部变量 count(从 0 开始,各任务各自累加)。count 是局部变量,在各任务的栈里,所以三个任务的数字不同步。

LCD 的临时保护用了最土的办法:

static int lcd_can_use = 1;

用之前判断,用时清 0,用完置 1。

现象很有意思:最初只有 task3 在打印,task1 和 task2 完全没有输出。原因是 task3 在耗时的打印段中间被切换的概率最大,切换时它已经把 lcd_can_use 清成 0,其他任务进来看到 0 就不打印了;而 task3 恢复之后没被立刻切走,马上又进循环把 0 又清一遍,于是 task1、task2 永远拿不到 LCD。

解决办法是打印之后加 mdelay(500),让切换点大概率落在延时里——这时候它没占着 LCD,别人就有机会用。

另外,因为默认任务被屏蔽了,要在 main 里创建任务之前自己初始化 LCD 并清屏。

这一集留了两个问题:为什么用全局变量保护 LCD 不可靠?为什么最后创建的 task3 先运行?

删除任务#

第 7 个程序 07deltask。用遥控器控制音乐:三角键(play,键值 A8)创建音乐任务,红色电源键(power,键值 A2)删除音乐任务。红外读取函数返回 0 表示读到了键值,键值放在 data 里。

vTaskDelete(TaskHandle_t xTaskToDelete);

删别的任务要传句柄,自杀传 NULL。

逻辑上要注意句柄的初值是 NULL:

收到 play 且句柄为 NULL → 创建任务
收到 power 且句柄非 NULL → 删除任务,删完把句柄置回 NULL

两个条件都不加,就会重复创建或者删一个不存在的任务。

现象:删除任务之后蜂鸣器保持一个音调,变成噪音。因为频率已经设好了,任务没了,没人去停它。改两处:打印前用 lcd_clear_line 清行,避免旧字符残留;删除之前先调驱动里的停止蜂鸣器函数。

这一集的重点是两条讲评:

频繁创建、删除任务会反复动态分配和释放内存,容易产生内存碎片,
多次执行之后可能就分配不到内存了。
vTaskDelete 是直接终止任务,任务来不及做清理工作。
蜂鸣器停不下来就是例子。

所以一般不推荐随手删任务,更常见的做法是让任务自己读遥控器、自己停止并做清理。

坑#

  • usStackDepth 是 word 数不是字节数,STM32 上 128 表示 512 字节。
  • xTaskCreateStatic 返回的是句柄,拿它跟 pdPASS 比较会编译报错。
  • 估算栈别只找最深的调用链,局部变量多的浅函数可能更吃栈。
  • 任务被删除前该停的外设要先停(蜂鸣器、电机),vTaskDelete 不会帮你收尾。
  • 用全局变量保护共享资源不可靠,这一段只是演示问题,不是解决方案。
任务的创建、参数与删除
https://me.622168.xyz/posts/freertos/04-task-create-delete/
作者
Shaw 的小屋
发布于
2026-09-29
许可协议
CC BY-NC-SA 4.0