RTOS-F429-HAL-绝对延时和相对延时(2026/7/31)
目录
一:vTaskDelayUntil() — 绝对延时
1:函数原型
2:怎么用
3:跟 vTaskDelay 的语法对比
二:rtos\9\ 改动清单
1:freertos_demo.c
2:main.c
3:时间轴推演
task2(prio=5, 绝对延时) task1(prio=3, 相对延时)
task2(prio=3, 绝对延时) task1(prio=5, 相对延时)
三:生活化解释
1:vTaskDelay — "从现在开始算,睡多久"
2:vTaskDelayUntil — "定一个固定的闹钟时间"
3:一张图对比
4:什么时候用哪个
一: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= "定好下一次醒来的绝对时间",不管中间折腾了多久,节奏永远不乱。
