返回上一级
2057 字
10 分钟
优先级、任务状态与调度

p22 ~ p26(约 86 分钟)

只提高优先级会更糟#

第 8 个程序,在第 7 个基础上只改两处:把音乐播放任务的优先级提高(tskIDLE_PRIORITY + normal 再加 1,也就是 24 → 25),把音乐任务里的 mdelay 换成 vTaskDelay。

只改优先级、不改延时的现象出乎意料:音乐很流畅,但 LED 不闪了(摄像头能拍到,人眼看不出),全彩 LED 停止变化,按遥控器想停音乐也没反应。

原因在这个最高优先级任务一直在死循环里跑 mdelay,而 mdelay 本身就是个死循环(不断读定时器时间),任务从不主动放弃 CPU,于是独占了处理器,其他任务永远得不到运行。

把 mdelay 换成 vTaskDelay 之后一切正常:音乐不卡顿,LED 继续闪、颜色继续变、遥控器也能控制音乐播放和停止。因为 vTaskDelay 让任务进入阻塞,延时期间不参与调度,主动把 CPU 让了出来。

所以改善播放效果靠两件事:提高优先级,加上运行中主动放弃 CPU。少一样都不行。

四种任务状态#

第 9 个程序,在第 8 个基础上加了暂停和恢复:第一次按 play 创建任务,之后每次按 play 在暂停与恢复之间切换,按 power 删除任务。

四种状态还是用喂饭那套类比:

Running 正在执行
Ready 随时可以执行
Blocked 在等某个事件或时刻(等同事回消息)
Suspended 单纯不想干(累了)

任务创建出来一定是 Ready;被切出去之后回到 Ready;调 vTaskDelay 是等某个时刻,进入 Blocked。

进入挂起只有两条路:

自己调用 vTaskSuspend(NULL) 能自己调用,说明它此刻正在 Running
别人调用 vTaskSuspend(handle) 此时它可能在 Ready,也可能在 Blocked

恢复用 vTaskResume(handle)。

代码上要加一个标志记录音乐任务是不是在跑。play 键的处理:

没有任务 → 创建,标志置 1
已有任务 → 标志为 1 就 vTaskSuspend(句柄) 并清 0
标志为 0 就 vTaskResume(句柄) 并置 1

power 键仍然是 vTaskDelete。

现象跟删除那次一样:暂停时蜂鸣器保持一个音调,因为没人停它。暂停和恢复时都要先停止蜂鸣器,同时在屏幕上打印 suspend / resume(打印前先清行)。改完之后,暂停后再按 play 能从上次暂停的位置继续播放。

完整的链路是这样一圈:

创建 → Ready → Running → vTaskDelay → Blocked
→ 被默认任务 vTaskSuspend → Suspended
→ vTaskResume → Ready → Running → ……

调度就是链表#

第 10 个程序之前先把原理讲清楚:任务管理与调度的本质是链表。

调度的原则可以这样推出来:

① 同优先级任务轮流运行(时间片)
② 最高优先级的任务先运行
③ 高优先级任务一旦就绪就马上运行,抢占当前任务
④ 高优先级任务不让出 CPU,低优先级就永远跑不了
⑤ 多个最高优先级任务之间交叉执行

配置上,configMAX_PRIORITIES = 56,也就是优先级 0 ~ 55。就绪链表是个数组:

pxReadyTasksLists[] 56 项,第 N 项存放优先级为 N 且处于 Ready/Running 的任务
tskIDLE_PRIORITY 0
priority normal 24
pxCurrentTCB 指向当前要运行的任务

创建任务时,按优先级把新任务挂到对应的就绪链表;如果新任务的优先级更高,就更新 pxCurrentTCB。所以后创建的 color_led 会取代先创建的那个成为「最高优先级就绪任务」——这正好回答了前面「为什么最后创建的 task3 先运行」。

启动调度器时会创建空闲任务,优先级 0,比其他任务都低,pxCurrentTCB 仍然指向 color_led,所以最后创建的 color_led 先运行。

tick 中断的频率由 configTICK_RATE_HZ = 1000 决定,也就是每 1ms 一次。中断里做三件事:

① 计数累加
② 检查 delay 链表里的任务有没有到期,到期就移回就绪链表
③ 发起一次调度

调度就是从高到低遍历就绪链表,找到第一个非空的,按记录的下标(pxIndex)取出「下一个」任务,这样同优先级的任务才能公平轮转。

音乐任务的优先级是 25,一旦创建立刻抢占——图例里任务 1 才跑了 0.1ms 也被换下去。vTaskDelay(2) 的参数单位是 tick,任务会被从就绪链表移到 xDelayedTaskList(有 1、2 两条,用来处理 tick 溢出);vTaskSuspend 把它移到挂起链表;vTaskResume 再移回就绪链表。所有状态切换,落到代码里都是链表的增删。

空闲任务#

任务函数不能直接返回。创建任务时初始化栈会伪造一个 LR,让函数返回时跳到一个错误处理函数 prvTaskExitError,里面关中断然后死循环。任务切换依赖 tick 中断,中断一关,所有任务都跑不了,系统直接死机。

实验很直观:让任务 for 循环 10 次之后返回(不调 vTaskDelete),结果是闪 10 次后死机,按遥控器屏幕毫无反应。在循环后面加上 vTaskDelete(NULL),一切正常。

任务退出只有两条路:

自杀 vTaskDelete(NULL)
他杀 别的任务传句柄调用 vTaskDelete

收尸的规则是:A 杀 B 由 A 收拾;自杀的没人收尸,由空闲任务收。收尸就是释放动态分配的 TCB 和栈。

空闲任务优先级是 0,启动调度器时创建。风险在于:如果其他任务都不主动放弃 CPU,空闲任务永远轮不到,就没法帮自杀的任务收尸;一直创建、自杀下去会把内存耗光。所以习惯上要做到事件驱动;就算写死循环,延时也要用 vTaskDelay 这类阻塞函数,而不是 mdelay。

空闲任务有两个作用:

① 释放被删除任务的内存
② 保证系统永远有一个可运行任务(其他任务全阻塞时至少它还能跑)

它永远处于就绪态,要么在运行要么在等待,永不阻塞。空闲任务的函数里会检查等待终止的任务并做清理,还会调用空闲钩子 vApplicationIdleHook,可以在里面打印信息、统计运行状态。

两个 Delay#

第 11 个程序,基于第 6 个程序。两个 delay 的单位都是 tick;tick 中断每隔固定时间来一次,这个间隔就是 tick。

vTaskDelay(5) 表示从调用那一刻起等 5 次 tick 中断才变就绪,之后能不能立刻运行,要看有没有同优先级或更高优先级的就绪任务。

它只保证「函数从进入到出来 ≥ 5 个 tick」,不保证周期。时间轴上看:do_something 第一次跑了 1ms、第二次 10ms、第三次 5ms,启动间隔就变成大约 6ms、15ms 这样参差不齐。

vTaskDelayUntil(&last_time, 15) 才能保证固定周期:

循环开始前 last_time = xTaskGetTickCount() 设为 T1
执行 do_something()
调用 vTaskDelayUntil(&last_time, 15) 阻塞到 T1+15,即 T2
唤醒时 函数内部把 last_time 更新为 T2,任务进入就绪态
下一次 再阻塞到 T2+15

不管 do_something 耗时多久,T1 → T2 → T3 的间隔恒为 15 个 tick。关键是起始时间要提前取好并传进去,之后函数会自己更新它。

实验里在 do_something 中用 count % 30 * 3 之类造一个长度随机的死循环,测量进出消耗的时间。vTaskDelay 版打印约 499ms(设的是 500ms,很精确);vTaskDelayUntil 版因为前面随机延时不同,打印出 378、380 这样不等的值。

[待确认] 字幕把 API 读成「s task delay until / x task delay until」,正确名字是 vTaskDelayUntil。

坑#

  • 只提高优先级不加阻塞,高优先级任务会独占 CPU,其他任务全部饿死。
  • mdelay 是死循环读定时器,不会让出 CPU;任务里要用 vTaskDelay。
  • 任务函数不能返回,返回就是关中断死机。要退出就调 vTaskDelete(NULL)。
  • 挂起的任务不会因为超时自动恢复,只能靠 vTaskResume。
  • vTaskDelay 不保证周期,需要固定周期就用 vTaskDelayUntil。
优先级、任务状态与调度
https://me.622168.xyz/posts/freertos/05-priority-states-scheduling/
作者
Shaw 的小屋
发布于
2026-09-29
许可协议
CC BY-NC-SA 4.0