/* 周期任务的节拍器(需求 §10.3)。 两个周期任务 —— `t_safety` 20 ms、`t_thermal` 1 s —— 都不能用 「干完活睡 20 ms」这种写法:那样周期会变成 `20 ms + 干活时间`, 而干活时间本身是变的(I²C 重试、NAK 超时),于是巡检的实际间隔会**漂**, 还会跟着总线忙闲一起抖。§10.3 的 20 ms 是**间隔**,不是延时。 所以这里记的是**下一次该醒的绝对时刻**,每次把它 += 周期, 睡「到那个时刻还差多久」。FreeRTOS 的 `vTaskDelayUntil()` 是同一个东西, 这里自己写一份的唯一理由是:它得在宿主机上用虚拟时钟跑得起来。 ---- 过载了怎么办 -------------------------------------------------------- 一个周期没干完就到点了(`sched_periodic_wait()` 算出来该睡 0 ms), **跳过错过的整周期、保住相位**,并计一次 `overruns`。 不补跑 —— 补跑的现象是「卡了一下之后连着跑五圈巡检」,把总线瞬间打满、 把已经落后的任务推得更后面。巡检读的是**当前**电压电流,补读三个周期前的没有意义。 */ #ifndef SCHED_H #define SCHED_H #include #ifdef __cplusplus extern "C" { #endif typedef struct { uint32_t period_ms; uint32_t next_ms; /* 下一次该醒的绝对时刻(32 位 ms,回绕安全) */ uint32_t cycles; /* 已经等过几个周期 */ uint32_t overruns; /* 其中「到点时活还没干完」的有几个 */ uint32_t worst_late_ms; /* 最严重的一次迟到了多久 —— 遥测里报出去 */ } sched_periodic_t; /* 以「现在」为相位起点。period_ms 为 0 会 abort(那是事件驱动任务,不该用这个)。 */ void sched_periodic_init(sched_periodic_t *p, uint32_t period_ms); /* 纯计算:now_ms 这一刻还该睡多久,并把下一次的时刻推进。 与 `sched_periodic_wait()` 分开是为了能不依赖时钟单独测它 —— 回绕、过载、连续过载这三种情况用真时钟很难摆出来。 */ uint32_t sched_periodic_delay_from(sched_periodic_t *p, uint32_t now_ms); /* 睡到下一个节拍。返回实际睡了多久(0 = 这一圈超时了)。 */ uint32_t sched_periodic_wait(sched_periodic_t *p); #ifdef __cplusplus } #endif #endif /* SCHED_H */