Skip to content

FreeRTOS 时间体系与周期任务

用一句话说明这篇笔记最终要解决的问题。

1. 问题与背景

vTaskDelay() 和 vTaskDelayUntil() 到底差在哪? vTaskDelay() 的本质:相对延时 它表达的是:“从我现在调用这个函数开始,再等 N Tick。” 工作 Delay |-------------|----------------------| 0 2 12 ↑ 从这里开始算10ms

但是恶心的是:函数执行时间通常还不是固定的 1个任务的实际周期大部分都是 11ms 12ms 14ms 11ms 13ms ... 也就是周期抖动

vTaskDelayUntil() 就是为这个问题设计的 标准写法: TickType_t xLastWakeTime; xLastWakeTime = xTaskGetTickCount(); for (;😉 { Motor_Control(); vTaskDelayUntil(&xLastWakeTime,pdMS_TO_TICKS(10)); }

&xLastWakeTime 先理解成:保存这套周期计划的时间基准。 0 10 20 30 40 50 ----

vTaskDelay(10ms) 0 │ 工作2ms ├──2 │ Delay10 ├──────────12 │ 工作4 ├────16 │ Delay10 ├──────────26 │ 工作1 ├─27 │ Delay10 ├──────────37

vTaskDelayUntil(..., 10ms) 0 │ 工作2 ├──2 │ 等到10 ├────────10

10 │ 工作4 ├────14 │ 等到20 ├──────20

20 │ 工作1 ├─21 │ 等到30 ├─────────30

相对时间 vs 绝对周期基准

2. 机器人为什么特别在乎这个?

周期漂移:想100Hz 理论周期 = 10ms 如果实际:10ms + 2ms工作 = 12ms 频率变成大约:83.3Hz 更麻烦的是累计时间: 理想:1000次×10ms=10s 实际:1000次×12ms=12s 已经偏:2秒

假设 IMU: SensorTask 100Hz 姿态算法假定:dt = 0.01s 但你的实际采样: 11ms 13ms 12ms 14ms 而算法还一直以为:dt = 10ms 积分:角速度 × dt 就会产生误差。

再比如 PID: Derivative Integral 都依赖:dt

所以:周期稳定性直接影响控制算法。

这就是 FreeRTOS 时间体系开始真正连接机器人控制的地方。

不过注意: vTaskDelayUntil() 也不能保证10.000000ms绝对准时。 因为到20ms,只是说明:Task 从 Blocked 变成 Ready 不是:CPU 在20.000ms立刻执行它。

还要看: 有没有更高优先级Task? 有没有ISR正在执行? Critical Section多长? Scheduler什么时候运行?

所以真实情况可能: 计划唤醒:20.000ms 真正开始Running:20.120ms

这 0.120ms 就是 Scheduling Jitter 的来源之一。 FreeRTOS 的唤醒时间,不等于真正获得 CPU 的运行时间。

3. 底层原理

Period 周期:任务希望多久运行一次。 Execution Time 执行时间:一次任务真正需要多少 CPU 时间。 Deadline 截止时间:这次工作最晚什么时候必须完成。

最简单的实时系统条件

假设:周期 = 10ms 但任务一次执行:15ms vTaskDelayUntil(..., 10ms)也不能救回来。 因为 0 │ │ 工作 │ │ ├────────────15ms

但下一次计划 10ms 早已经过去了。 这不是调度函数能解决的问题。 而是:任务本身已经不具备满足这个实时周期的能力。

那这种情况下 vTaskDelayUntil() 会怎样?这是一个很好的源码级问题。

假设:周期 = 10ms 计划:第一次:0 → 10 但任务执行到15ms才调用DelayUntil FreeRTOS计算: 计划唤醒时间 = 10ms 当前时间 = 15ms

计划时间已经过去。

那么通常不会再傻等到“过去的10ms”。 它会发现已经迟到了,因此这次可能直接不 Block,继续按照原来的周期时间轴推进。

下一计划点:20ms 如果任务很快:15 → 17 那么还可以:等到20ms

重新追上时间轴。

但如果每次都执行 15ms 呢? 周期:10ms 执行:15ms 那么计划: 10 20 30 40... 实际: 15 30 45 60...

它会一直:Overrun 也就是任务执行时间超过允许周期。 这时候系统设计本身就有问题。

解决方案可能是:

优化算法 降低任务频率 拆任务 使用DMA 提高CPU性能 改变架构 重新分析优先级

绝不是:把vTaskDelay换个API

这就是实时系统工程。

什么时候优先考虑 vTaskDelayUntil() 例如: Motor Control IMU Sampling Sensor Fusion 周期数据采集 固定频率状态更新 某些周期通信

尤其当需求明确:100Hz 500Hz 1kHz 就应该开始考虑:vTaskDelayUntil() 或者更进一步:Hardware Timer ISR DMA

根据实时要求决定。

4. 数据流与调用流程

以后机器人 Task,我希望你看到代码就产生这种条件反射

看到: for (;😉 { IMU_Update();

vTaskDelay(10);

}

不要只想:每10ms执行一次。 要马上问: IMU_Update耗时多少? 实际Period是多少? 周期会不会漂? 有没有Jitter要求? 为什么不用DelayUntil? 是不是应该DMA + Notification? 它真的需要Task吗? Deadline是什么?

这才是从 API 使用者变成工程师。

未来很典型的机器人任务 void ChassisControlTask(void *argument) { TickType_t xLastWakeTime = xTaskGetTickCount(); const TickType_t xPeriod = pdMS_TO_TICKS(1);

for (;;)
{
    Read_Control_State();
    Chassis_Controller_Update();
    Motor_Command_Update();
    vTaskDelayUntil(&xLastWakeTime,xPeriod);
}

}

目标:1kHz控制循环

但真正工程验收还要测: Worst Case Execution Time < 1ms ? 实际Jitter多少? 有没有高优先级IRQ干扰? 是否Deadline Miss? PWM更新时间是否匹配? 传感器数据是不是新鲜的?

光看到:vTaskDelayUntil(...1ms) 不能宣布:“控制环就是精准1kHz。”

这个意识非常重要。

5. 最小验证实验

                周期任务
                   │
   ┌───────────────┴──────────────┐
   │                              │

vTaskDelay() vTaskDelayUntil() │ │ 当前时间 + Delay 上次计划时间 + Period │ │ Relative Delay Absolute Schedule │ │ 执行时间参与周期 尽量维持固定周期 │ │ 容易累计漂移 减少周期漂移

但二者都受到: Task Priority Higher Priority Task ISR Critical Section Execution Time Tick Resolution 影响。

所以:DelayUntil ≠ 硬实时保证

6. 调试观察

这个任务什么时候应该来、什么时候真正获得 CPU、执行多久、最晚什么时候必须结束。

先建立一条最重要的时间轴。

假设一个电机控制任务要求:Period = 10 ms 理论上: 0ms 10ms 20ms 30ms │ │ │ │ ↓ ↓ ↓ ↓ 一次任务 一次任务 一次任务 一次任务 实际上: 0ms │ Release │ ├──0.15ms ← 真正获得CPU │ Running ├────2.15ms ← 执行完成 │ │ 10ms │ Release ├─10.30ms ← 这次被高优先级任务挡了一会 │ Running ├──12.10ms

最坏情况执行时间WCET:可通过充分压力测试得到最大观测值 + 安全裕量。

Release Time:这一轮任务“有资格开始执行”的时间点。 Release ↓ Ready │ │ 等CPU │ ↓ Start Running ↓ 执行 ↓ Finish 然后Deadline在旁边盯着你。

Release Start Finish Deadline ↓ ↓ ↓ ↓ ---|-------------|=======================|-------------|--- 等待CPU 执行时间

一次任务的总响应时间:Response Time = Finish - Release 它不仅包含:自己的 Execution Time 还包含: 这些时间。 等待高优先级任务 被ISR打断 被Mutex阻塞

Jitter:周期任务实际开始执行时间相对理想时刻的波动 Release Jitter → 任务被释放的时间不稳定 Start-time Jitter → 真正开始Running的时间不稳定 Completion Jitter → 完成时间不稳定 Period Jitter → 两次实际执行之间间隔不稳定

vTaskDelayUntil() 没有消除 Jitter, 它主要解决:Drift —— 累积漂移 vTaskDelayUntil() 保持时间基准,但不能保证 CPU 恰好在那个时刻执行你。

7. 不能只按重要性分优先级

MotorTask Period = 1ms IMUTask Period = 5ms CommTask Period = 10ms LogTask Period = 100ms

如果这些任务: 都是周期性的 彼此独立 Deadline大致等于Period

有一个经典实时调度思想:Rate Monotonic 简单说:周期越短,优先级越高。

于是: Motor 1ms → 高 IMU 5ms → 次高 Comm 10ms → 中 Log 100ms → 低

这个和我们以前凭工程直觉结果很像。 电机重要,所以高 日志不重要,所以低

但理论依据完全升级了。

为什么周期短的通常更急?

Motor:每1ms来一次 意味着:它只有很短时间窗口处理 Log:每100ms一次 通常时间余量大得多。

所以固定优先级实时系统里:短周期 / 短Deadline 往往应该得到更高优先级。

但是不是定死的,比如: TaskA Period = 5ms Deadline = 5ms

TaskB Period = 20ms Deadline = 1ms

谁更急?虽然 TaskB 周期更长:20ms 但它释放以后:1ms就必须完成

所以只看 Period 就可能错。

更一般的思想是:Deadline Monotonic

固定优先级下,如果条件适合:Deadline 越短,优先级越高。

现在只需要知道:Rate Monotonic → 看Period Deadline Monotonic → 看Deadline

8. 真实工程中的用途

真实 FreeRTOS 工程怎么定优先级?

9. 我的结论

FreeRTOS 内核与工程实践