目录
[一:vTaskDelayUntil() --- 绝对延时](#一:vTaskDelayUntil() — 绝对延时)
[3:跟 vTaskDelay 的语法对比](#3:跟 vTaskDelay 的语法对比)
[二:rtos\9\ 改动清单](#二:rtos\9\ 改动清单)
[task2(prio=5, 绝对延时) task1(prio=3, 相对延时)](#task2(prio=5, 绝对延时) task1(prio=3, 相对延时))
[task2(prio=3, 绝对延时) task1(prio=5, 相对延时)](#task2(prio=3, 绝对延时) task1(prio=5, 相对延时))
[1:vTaskDelay --- "从现在开始算,睡多久"](#1:vTaskDelay — "从现在开始算,睡多久")
[2:vTaskDelayUntil --- "定一个固定的闹钟时间"](#2:vTaskDelayUntil — "定一个固定的闹钟时间")
一:vTaskDelayUntil() --- 绝对延时
1:函数原型
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement );
| 形参 | 含义 |
|---|---|
pxPreviousWakeTime |
上一次醒来的绝对时间 。第一次调用前必须用 xTaskGetTickCount() 初始化。函数内部每次调用自动更新这个值。 |
xTimeIncrement |
周期时长(单位:tick)。每次醒来后间隔多久再醒。 |
返值:无。
需要宏 :INCLUDE_vTaskDelayUntil = 1 ✅ 我们已开
2:怎么用
TickType_t xLastWakeTime = xTaskGetTickCount(); // 记下"现在是几点"
while (1)
{
// 干活...
vTaskDelayUntil(&xLastWakeTime, 500); // 下次醒 = xLastWakeTime + 500
// ↑ ↑
// 它帮你 +500 更新 周期 500 个 tick
}
第一次:xLastWakeTime = 100(假设当前时间)→ 睡到 100+500=600 第二次:xLastWakeTime = 600 → 睡到 600+500=1100 第三次:xLastWakeTime = 1100 → 睡到 1100+500=1600
内部自动帮你做了 xLastWakeTime += 500,你不用手动加。
3:跟 vTaskDelay 的语法对比
// 相对延时:就一个参数,睡多久
vTaskDelay(500);
// 绝对延时:多一个"上次几点醒的"变量来锚定节奏
TickType_t wake = xTaskGetTickCount();
vTaskDelayUntil(&wake, 500); // wake 必须传地址,函数内部会更新它
二:rtos\9\ 改动清单
1:freertos_demo.c
| 行号 | 内容 |
|---|---|
| 37-38 | task1(prio=3)、task2(prio=5)------高优先级抢占低优先级 |
| 88-98 | task1:vTaskDelay(500) 相对延时,周期会漂 |
| 108-119 | task2:vTaskDelayUntil(&xLastWakeTime, 500) 绝对延时,不漂 |
2:main.c
| 行号 | 内容 |
|---|---|
| 5-7 | 去掉 Key.h/TIM6.h(本实验不需要按键和运行时间统计) |
| 18-20 | 去掉 Key_Init()/ConfigureTimeForRunTimeStats() |
| 27 | printf 标题 → vTaskDelay vs vTaskDelayUntil Test! |
代码很简洁。用示波器看 PH10(红灯, task1) 和 PH11(绿灯, task2):
-
task2 绿灯:精确 500ms 一个周期,不漂
-
task1 红灯:~500ms 但有抖动,因为会被 task2 抢或者起步晚了
没示波器的话看着灯闪也行,肉眼分不出,重点是用逻辑分析仪/示波器看差异。
3:时间轴推演
task2(prio=5, 绝对延时) task1(prio=3, 相对延时)
t=0: task2(prio=5) 跑
LED1翻 → delay_ms(20) → vTaskDelayUntil → 睡到 t=500
t=20: task1(prio=3) 拿到 CPU(task2 在睡)
LED0翻 → delay_ms(20) → vTaskDelay(500) → 睡到 t=20+500=540
t=500: task2 准时醒
LED1翻 → delay_ms(20) → t=520
vTaskDelayUntil → 睡到 t=1000
t=540: task1 醒(task2 在睡,没人抢)
LED0翻 → delay_ms(20) → t=560
vTaskDelay(500) → 睡到 t=1060
task1 周期:第一次翻灯 t=20,第二次 t=540 → 520ms
task2 周期:第一次翻灯 t=0,第二次 t=500 → 500ms
差的那 20ms 不是被抢了 CPU——task2 醒的时候 task1 本来就在睡。差的是开头 task2 先占了 20ms,task1 天生起步晚了 20ms。之后两家的唤醒时间永远错开,谁也不抢谁,各过各的。
那之前说的"抢占导致波形拉长"是上一节的场景——task1 优先级更高的时候才会发生(task1 在跑 delay_ms 期间,task2 的绝对时间到了,但 task2 优先级低,抢不进去,只能等 task1 腾出手,所以 task2 的 500ms 才变成 ~520ms)。
这一节的结论更简单:task2 优先级高就先跑,占了前面 20ms,task1 起步晚,周期自然长。
task2(prio=3, 绝对延时) task1(prio=5, 相对延时)
两个任务都做同一件事:翻转 LED → delay_ms(20) 忙等 → 延时。区别只在延时方式。
t=0 调度器启动
task1(prio=5,更高) 先跑
├── LED0 翻转(瞬间)
├── delay_ms(20) → 忙等 20ms,CPU 不释放
└── vTaskDelay(500) → 阻塞,记录"500ms 后叫醒我",将于t=520ms时醒来。
t≈20 task2(prio=3,更低) 终于等到 CPU
├── xLastWakeTime = xTaskGetTickCount() → 记下"现在是 20"
├── LED1 翻转
├── delay_ms(20) → 忙等 20ms,t≈40
└── vTaskDelayUntil(&wake, 500)
→ 计算:下次醒 = 20 + 500 = t=520
→ 现在才 t=40,睡 480ms
t=520 task1 的 vTaskDelay(500) 到期,醒了
→ task1 优先级 5 > task2 优先级 3, //task1 先跑!,把task2抢占了,两人同时从t=520ms醒来!!!!!!!!!!
├── LED0 翻转
└── delay_ms(20) → t≈540
t=540 task2 的 vTaskDelayUntil 也到期了(应该 t=520 醒的,被 task1 拖到 540)
→ 已经错过了 20ms!立刻跑
├── LED1 翻转
├── delay_ms(20) → t≈560
└── vTaskDelayUntil:xLastWakeTime += 500 → 下次醒 = 20+500+500 = t=1020
实际睡多久?1020 - 560 = 460ms ← ★ 比 500 短!
t≈1040 task1 又醒了(520+500=1020 + delay_ms 20 = t≈1040)
→ 又抢占!
为什么示波器上看到的是 ~496ms 而不是 500ms?
看上图——task2 每次都被 task1 拖后腿,但 vTaskDelayUntil 死守着绝对时间点。你迟到了它就缩短下一段睡眠来追回来,所以示波器上 task2 的 LED 周期会围绕 500ms 上下抖动,有时候是 496ms(在追)、有时候是 504ms(被拖)。
这就是绝对延时的意义:长期来看平均周期精确是 500ms,不漂移。反观 task1 的 vTaskDelay,每次都在漂,100 次以后可能漂出几十毫秒。
一句话
vTaskDelay = 倒计时闹钟,晚起了就整体漂移。vTaskDelayUntil = 固定时刻闹钟,晚起了就自己缩短下一次睡眠来追回去,长期节奏永远不变。你示波器看到的 496ms 就是因为 task2 在"追"被 task1 拖后腿的进度。
三:生活化解释
1:vTaskDelay --- "从现在开始算,睡多久"
vTaskDelay(500); // 从现在起,睡 500ms
生活例子:午休闹钟
你说:"从现开始睡 30 分钟" → 设了个倒计时 30 分钟
BUT:你在床上刷了 10 分钟手机才真的闭眼
→ 实际醒来是 40 分钟后(刷手机也算在了"睡之前"的时间里)
→ 你的午休节奏漂移了
代码里的后果:
一个任务每 100ms 打印一次:
第 1 次:t=0 → 打印 → vTaskDelay(100) → t=100ms 醒来
第 2 次:t=100 → 打印 → 但!打印花了 5ms → vTaskDelay(100) → t=205ms 醒来
第 3 次:t=205 → 打印(5ms) → vTaskDelay(100) → t=310ms 醒来
第 4 次:t=310 → ...
每次都在漂移,100 次以后可能差了半秒!
2:vTaskDelayUntil --- "定一个固定的闹钟时间"
TickType_t last_wake = xTaskGetTickCount(); // 记下"起床时间"
while (1)
{
// 干活...
vTaskDelayUntil(&last_wake, 100); // 不管刚才干了多久,下次必须是 last_wake+100ms
}
生活例子:学校上课铃
上课铃每隔 45 分钟响一次——不管你是准时回到座位还是磨蹭了 3 分钟:
第一节:9:00 响铃 → 上课 45 分钟
第二节:9:45 响铃 ← 固定!不是"从第一节下课开始 +45"
第三节:10:30 响铃 ← 固定!
即使你课间拖堂了 5 分钟,下一节的**响铃时间不变**。
代码里的效果:
第 1 次:t=0 → 打印(5ms) → vTaskDelayUntil(&wake, 100) → t=100 准时醒
第 2 次:t=100 → 打印(5ms) → vTaskDelayUntil(&wake, 100) → t=200 准时醒
第 3 次:t=200 → 打印(5ms) → vTaskDelayUntil(&wake, 100) → t=300 准时醒
不管每次打印花了 5ms 还是 50ms,下一次醒来的**绝对时间点不变**!
3:一张图对比
vTaskDelay(相对):
干活→睡100ms→干活→睡100ms→干活→睡100ms→...
↑ ↑ 漂了5ms ↑ 漂了10ms
节奏越来越偏
vTaskDelayUntil(绝对):
t=0 t=100 t=200 t=300 t=400 ← 固定的绝对时间点
│ │ │ │ │
干活 干活 干活 干活 干活
↑ ↑ ↑ ↑ ↑
每次都在同一个节拍上,不漂移
4:什么时候用哪个
| 场景 | 用哪个 |
|---|---|
| 无所谓精确周期,随便睡一下 | vTaskDelay(100) |
| 周期性采集数据(ADC 采样) | vTaskDelayUntil() |
| 周期性发送心跳包 | vTaskDelayUntil() |
| 按键扫描、LED 闪烁 | vTaskDelay(10) 够用了 |
一句话 :vTaskDelay = "从刚才睡醒开始倒计时",每次会漂移。vTaskDelayUntil = "定好下一次醒来的绝对时间",不管中间折腾了多久,节奏永远不乱。