切换主题
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 工程怎么定优先级?