文件
phonerail/firmware/HANDOFF.md
T
zhou yang和Claude Opus 5 ec31891429 A8:任务骨架与调度(5 个任务、两把锁)
需求 §10.3 落地。四件事:

1. 五任务表(tasks.c):周期与优先级写在一张 const 表里,开机自检查的是
   相对顺序(safety 唯一最高、log 唯一最低、thermal 与 actuator 同级),
   外加「巡检周期必须长于 INA226 一帧转换 8.8 ms」。不过就 abort。

2. 节拍器(sched.c):记下一次该醒的绝对时刻,回绕安全(int32_t 差判先后)。
   过载时跳过错过的整周期、保住相位,不补跑。

3. 两把锁(hal_lock):I²C 与 74HC595,禁止嵌套持有 —— 这块板上两者没有交集,
   「拿着 A 去拿 B」永远是写错了。ESP 侧用 mutex 取优先级继承。

4. t_safety 的一个周期:拿锁 → 6 片 INA226 的 V+I + PCA9555 两端口 → 放锁。
   不死等锁(5 ms 超时就跳过这圈),一片读失败不中断整圈、保留上一圈的值。

I²C 占用由 HAL 按线上真实字节数记账(两个目标同一份代码),静态预算与实测
必须相等:一次寄存器读 48 位,一圈 13 次事务 = 1560 µs / 20 ms = 7.8 %,
与 §10.3 手算的「约 1.5 ms / 约 8 %」相符(本节的 46 位没数 STOP 和重复 START)。

sim 侧配套两样:假 I²C 事务真的把虚拟时钟花掉(否则时序测试是空话),
以及可强制的加锁纪律检查(配一条反向测试,防止检查本身失灵)。

11 条 ctest(161 例)全绿,idf.py build 通过;16 个注错实验全部变红。

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 07:35:39 +08:00

6.5 KiB
原始文件 Blame 文件历史

交接

更新:2026-09-07 完成任务包 A8(任务骨架与调度:5 个任务、两把锁)

一句话现状

§10.3 的五个任务成了一张 const 表(周期 + 优先级,开机自检不过就 abort), 周期任务走绝对时刻的节拍器(不漂、回绕安全、过载跳过不补跑), 两把互斥锁(I²C / 595)禁止嵌套持有,t_safety 的一圈已经能跑: 拿锁 → 6 片 INA226 的 V+I + PCA9555 两个输入端口 → 放锁 → 记账。 I²C 占用由 HAL 按线上真实字节数算出来:1560 µs / 20 ms = 7.8 %, 与 §10.3 手算的「约 1.5 ms / 约 8 %」相符。11 条 ctest(161 个用例)全绿,idf.py build 通过。

下一步

A9:三个执行器状态机 + 互斥(需求 §10.6 全节)。 验收是「硬重启走完 17 ms 上电时序;按键序列不重入;EDL 顺序固定」。 A8 给它备好的:HAL_LOCK_SR595(595 那把锁,sim 里能强制检查)、 虚拟时钟下的 sched_*、sim_gpio_write_seq()(A7 留的顺序断言)。 ⚠️ A9 的状态机同样要写成「一个函数一步」,不要写进 while(1) —— 死循环在宿主机上跑不到。

本次新增/修改的文件

  • main/tasks.h / tasks.c — 五任务表(名字/周期/优先级/栈)+ task_table_ok()
  • main/sched.h / sched.c — 绝对时刻节拍器(≈60 行),过载计数与最迟到时长
  • main/t_safety.h / t_safety.c — 巡检的一个周期 + 静态预算 + 一行人话报告
  • components/phonerail_hal/include/hal_lock.h + esp/hal_lock_esp.c + sim/hal_lock_sim.c — 两把锁(HAL 的第五样东西,理由写在 hal_lock.h 开头)
  • components/phonerail_hal/include/hal_i2c_acct.h + hal_i2c_acct.c — I²C 占用核算,两个目标编同一份,所以宿主机跑出来的数就是上板会打印的数
  • sim/hal_i2c_sim.c — 每次事务记账 + 把虚拟时钟真的花掉 + 查加锁纪律
  • sim/sim_sr595.c / sim/sim_hal.c / sim/sim_hal.h — 595 那四根线也查加锁纪律; 新增 sim_lock_*()(扮演第二个任务、强制加锁纪律、违规计数)与 sim_i2c_set_bus_time()
  • esp/hal_i2c_esp.c — 四个事务入口各加一行记账
  • components/drv/include/ina226.h — 新增 INA226_CONV_US(8.8 ms,给任务表自检对账)
  • main/app_main.c — 建五个任务;selftest_or_die() 多查一项任务表
  • test/test_sched.c(17 个用例)/ test/test_t_safety.c(13 个用例)
  • 三处 CMakeLists + main/phonerail.h(版本 0.1.0-a8)
  • ../需求文档.md §10.3 — 记 A8 落地 + 五条实现约定

验收命令与结果

$ bash host/test.sh
100% tests passed, 0 tests failed out of 11     # +test_sched(17) +test_t_safety(13),共 161 例

$ ./host/build/test_t_safety.exe
t_safety: cycles=1 skipped=0 i2c_err=0 bus=1560 us/20 ms load=7.8%(§10.3 手算:约 1.5 ms / 约 8 %)

$ .\build_idf.ps1          # PowerShell
phonerail.bin binary size 0x3cc40 bytes. ... 0xc33c0 bytes (76%) free.
Project build complete.

# 16 个「一定会被写出来的错」逐个注进去跑过(一次性脚本,没入库):
#   节拍:next 从「现在」起算(漂移)/ 用裸比较判先后(回绕)/ 过载后补跑
#   任务表:t_log 抬到最高 / 巡检周期缩到 5 ms / thermal 与 actuator 不同级
#   巡检:拿锁改成死等 / 一片读失败就中断整圈 / 读失败把值清零 / 忘了放锁 /
#         地址改成 0x40+ch / 多读一次 PCA9555 / 占用游标不对齐到「现在」
#   记账:漏掉 STOP 位
#   sim:两把锁允许嵌套 / 假总线上的事务不花时间
#   → 16 条全部变红。

踩到的坑(只写会再犯的)

  • ⚠️ sim 里「干活不花时间」会让时序测试变成一句空话。 巡检在 sim 中耗时为零时, 「周期 20 ms」测的只是「delay 参数传对了没」,任何实现都能踩准。 所以假 I²C 现在真的把虚拟时钟花掉(按位周期折算), 而且测试第一句就断言「这一圈确实花掉了 t_safety_budget_us()」—— 这条断言本身就是防着有人把 sim_i2c_set_bus_time 关掉。
  • ⚠️ 注错实验里 old in src 会命中你没想到的那一行。 这次 " s_bus_time = true;" 先匹配到了声明行(static bool s_bus_time = true; 里正好含这段),改的是初值、而它每次 reset 又被覆盖回来 —— 于是那条注错「绿了」, 看上去像「测试没覆盖到」。注错的锚点要连着上一行一起写, 跑完还要确认它真的改到了那一处。
  • 周期任务的相位要用绝对时刻,而且比较必须走 int32_t 差。 裸写 now >= next 在 49.7 天回绕那一刻会把「还早」判成「迟到了 49 天」, 然后一口气空转两百万个周期。农场是长稳跑的,这一定会发生。
  • 加锁纪律要能被强制检查,否则「都加了锁」是靠人读代码保证的。 sim 侧 sim_lock_require() 打开后,动 I²C / 595 却没持锁就记一次违规; 配套必须有一条反向测试(不拿锁直接读一片,违规必须 +1), 不然检查失灵时那些「违规为 0」的断言会永远绿。
  • 两把锁不定加锁顺序,直接禁止嵌套。 这块板上 595 是纯 GPIO、I²C 是纯总线, 没有任何一处需要同时持有;把「不可能」编码进 hal_lock_take(), 比定一套顺序然后指望后面每个人都记得可靠。

这一层的几条约定(写 A9/A10 之前看一眼)

  • 任务体里不许有判据和时序。 写进 while(1) 的东西宿主机一行也跑不到 —— app_main.c 里的五个任务体各自只有三行(拿节拍 / 调一个函数 / 睡到下一拍), 真正的活在 main/ 的其它 .c 里。A9 的状态机照这个形状写。
  • A10 的判据写在 t_safety_step() 里,而且要在放锁之后。 §10.3 原话:越界直接操作 EFUSE_EN / VBUS_EN,不经任何队列或抽象层。 发消息给 t_actuator 是错的;拿着总线锁去写引脚也是错的(那两根线和 I²C 无关)。
  • 判据先看 ok 再看值。 读失败时 snap.rail[] 里留的是上一圈的值。
  • 动了巡检里读的东西,就要同步改 t_safety_budget_us() 的那一行 (测试会拦:静态预算与实测必须相等)。
  • 器件初始化还没做(INA226 的 Config/Calibration、DAC、PCA9555 方向、595 安全值), 那是 A15。在那之前上板跑起来电流恒读 0,是预期的,不是故障。