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不会帮你收尾。 - 用全局变量保护共享资源不可靠,这一段只是演示问题,不是解决方案。
请输入编辑凭据,只有站点所有者可以修改文章。