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]>
这个提交包含在:
+73
-74
@@ -1,93 +1,92 @@
|
||||
# 交接
|
||||
更新:2026-09-07 完成任务包 **A7**(通道配置表 + 表驱动遍历 + 启动自检)
|
||||
更新:2026-09-07 完成任务包 **A8**(任务骨架与调度:5 个任务、两把锁)
|
||||
|
||||
## 一句话现状
|
||||
五路的六组编号(INA 地址 / DAC 片通道 / 三个按键位 / EDL 位 / 两根 EN 的 GPIO)
|
||||
拼成了**一张 const 表** `rail_cfg(1..5)`,开机自检 `rail_selftest()` 把它和四个驱动
|
||||
那四张表**逐格对账**,再查一遍全板引脚有没有重复 / 越界,不过就 `abort()`。
|
||||
A3~A6 留下的四个自检谓词至此全部有了调用点。9 条 ctest(131 个用例)全绿,
|
||||
`idf.py build` 通过。
|
||||
§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` 通过。
|
||||
|
||||
## 下一步
|
||||
**A8**:任务骨架与调度(5 个任务、两把锁,需求 §10.3 全节)。
|
||||
验收是「sim 时钟下 `t_safety` 周期 20 ms ±1 ms;I²C 占用打印出来与 §10.3 的 8 % 对得上」。
|
||||
A7 给它备好的东西:`rail_cfg()` 表驱动遍历、`board_pins.h` 的 SDA/SCL 引脚、
|
||||
`sim_gpio_write_seq()`(断言顺序用,A9 的 17 ms 上电时序会更需要它)。
|
||||
**A9**:三个执行器状态机 + 互斥(需求 §10.6 全节)。
|
||||
验收是「硬重启走完 17 ms 上电时序;按键序列不重入;EDL 顺序固定」。
|
||||
A8 给它备好的:`HAL_LOCK_SR595`(595 那把锁,sim 里能强制检查)、
|
||||
虚拟时钟下的 `sched_*`、`sim_gpio_write_seq()`(A7 留的顺序断言)。
|
||||
⚠️ A9 的状态机同样要写成「一个函数一步」,不要写进 `while(1)` —— 死循环在宿主机上跑不到。
|
||||
|
||||
## 本次新增/修改的文件
|
||||
- `main/rail_cfg.h` / `main/rail_cfg.c` — 那张表、启动自检、两根 EN 线(≈250 行)
|
||||
- `main/board_pins.h` — **全板引脚表**(§8.1 里「全板独一份」的那些脚:I²C、风扇、
|
||||
NTC、USB、UP_VBUS_DET,加上 strapping / 不存在 / R8 版 PSRAM 三张禁用清单)。
|
||||
五路自己的 `EFUSE_ENn` / `VBUS_ENn` **不在这里**,它们是「每路一个」,在五路表里
|
||||
- `main/app_main.c` — 开机跑 `rail_selftest()`,不过就 abort;然后 `rail_en_init()`
|
||||
- `test/test_rail_cfg.c`(28 个用例)
|
||||
- `components/phonerail_hal/sim/sim_hal.h` + `sim/hal_gpio_sim.c` — 新增
|
||||
`sim_gpio_write_seq()`(全局写序号,断言「先断 VBUS 再关 eFuse」这类顺序用);
|
||||
**`SIM_GPIO_PINS` 48 → 49**
|
||||
- `components/phonerail_hal/esp/hal_gpio_esp.c` — **`PIN_MAX` 48 → 49**
|
||||
- 两处 CMakeLists + `main/phonerail.h`(版本 `0.1.0-a7`)
|
||||
- `../需求文档.md` §10.4 — 记 A7 落地 + 五条实现约定 + 那张六列表
|
||||
- `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 9 # +test_rail_cfg(28),总计 131 个用例
|
||||
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 0x34250 bytes. ... 0xcbdb0 bytes (80%) free.
|
||||
Project build complete. # rail_cfg.c.obj 在(已单独确认)
|
||||
phonerail.bin binary size 0x3cc40 bytes. ... 0xc33c0 bytes (76%) free.
|
||||
Project build complete.
|
||||
|
||||
# 16 个「一定会被写出来的错」逐个注进去跑过(一次性脚本,没入库):
|
||||
# INA 地址改回 0x40+ch / CH3 的 DAC 挪回 U6 / CH4↔CH5 按键位对调
|
||||
# EDL 位改回 16+ch / PG 位跟着 FLT 加偏移
|
||||
# EFUSE_EN2 撞上 UP_VBUS_DET / EFUSE_EN1 落到 strapping / CH1 两根 EN 写反
|
||||
# EN 极性写反 / all_off 先关 eFuse / all_off 第一个错就 return
|
||||
# en_init 不写那句明文的 0 / 表与驱动对账被删 / 引脚查重被删
|
||||
# GPIO 上限退回 48
|
||||
# → 以上 15 条全部变红。
|
||||
#
|
||||
# ⚠️ 唯一没能变红的一条:**把 `rail_selftest()` 整个改成 `return 0;`**。
|
||||
# 判据本身(`rail_selftest_table()`)已经被测得很死,但「rail_selftest 有没有
|
||||
# 真的去调它」这一行接线,在一切正常的前提下测试**原理上分不出来**
|
||||
# —— 真表是 const 的,没有任何输入能让它该报错。这是已知残留,不是漏做。
|
||||
# 节拍:next 从「现在」起算(漂移)/ 用裸比较判先后(回绕)/ 过载后补跑
|
||||
# 任务表:t_log 抬到最高 / 巡检周期缩到 5 ms / thermal 与 actuator 不同级
|
||||
# 巡检:拿锁改成死等 / 一片读失败就中断整圈 / 读失败把值清零 / 忘了放锁 /
|
||||
# 地址改成 0x40+ch / 多读一次 PCA9555 / 占用游标不对齐到「现在」
|
||||
# 记账:漏掉 STOP 位
|
||||
# sim:两把锁允许嵌套 / 假总线上的事务不花时间
|
||||
# → 16 条全部变红。
|
||||
```
|
||||
|
||||
## 踩到的坑(只写会再犯的)
|
||||
- ⚠️ **HAL 两侧的 GPIO 边界原来都是 `pin < 48`,而 `EFUSE_EN2` 在 IO48 上。**
|
||||
ESP32-S3 的编号是 **0~48(含 48)**,写成「个数 48」正好把它关在门外,
|
||||
现象是 CH2 的 eFuse 既开不了也**关不掉**(`hal_gpio_config()` 直接 HAL_ERR_ARG)。
|
||||
已改成 49,`test_io48_is_a_usable_pin` 是回归拦网。
|
||||
顺带记住:IO22~IO25 在 S3 上**根本不存在**、IO26~IO32 是模组内部 flash/PSRAM,
|
||||
写进配置表 `gpio_config()` 既不报错也不干活 —— 自检把这些号也当越界。
|
||||
- ⚠️ **「不是顺排」要看整列,逐格撞算式会撞空。** 第一版测试是「拿几个常见错算式
|
||||
逐路比,断言错了几路」。写完逐格核对才发现:CH4 的按键位正好就是 9/10/11、
|
||||
CH1 的 EDL 位正好是 16、CH3 的两根 EN 正好落在猜的算式上 —— 三处「错了 5 路」
|
||||
实际都只有 4 路。**猜错算式的测试是句空话**,而且这种空话看上去和真断言一样。
|
||||
改成断言整列**不是等差数列**(`is_arithmetic()`),任何 `起点+步长×ch` 一次拦掉。
|
||||
- **注错实验里把源文件 mtime 推到未来**(`os.utime(path, now+5)`):注错与恢复
|
||||
挤在同一秒里,make 有可能认为 `.o` 还是新的、跳过重编,于是跑的是**上一轮**的
|
||||
二进制 —— 那样「注了错还是绿」是假绿,看起来却像「这条测试没覆盖到」。
|
||||
⚠️ 本轮**没有观察到它真的发生**(三条绿是真绿,原因见上面那段),
|
||||
这条是预防性的;但假绿这种错一旦发生就会被当成结论写进文档,代价不对称。
|
||||
(A6 那两条纪律仍然有效:try/finally 恢复原文;编译期红的一条要重写成能编过的。)
|
||||
- **判据要能脱离真表被调用**。真表是 const 的,测试改不动,所以「查得出问题吗」
|
||||
这件事本来是测不了的 —— 把判据拎成 `rail_selftest_table(const rail_cfg_t *t)`
|
||||
之后,测试复制一份表出来逐样改坏即可。这条比多写十个断言管用。
|
||||
- ⚠️ **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()`,
|
||||
比定一套顺序然后指望后面每个人都记得可靠。
|
||||
|
||||
## 这一层的几条约定(写 A8/A9/A10 之前看一眼)
|
||||
- **`rail_cfg()` 是唯一入口。** 任何地方都不许再出现 `0x40 + rail`、`3 * (rail-1)`
|
||||
这类由通道号算出来的编号。六列里没有一列是顺排的,而写错了**板子照样正常工作**,
|
||||
只是「重启 CH1」重启了 CH4。
|
||||
- ⚠️ **两列 GPIO 没有第二张表能对账**(INA/DAC/595/PCA 那四列都有驱动那边兜着)。
|
||||
所以自检**查不出**「`EFUSE_EN1` 和 `VBUS_EN1` 写反」—— 两个都是合法空闲脚、
|
||||
不重复、不越界。唯一的拦网是 `test_en_pins_match_the_pcb_netlist` 里钉死的十个值
|
||||
(出处 `.claude/pcb1-parts.json` 的 U20 焊盘网络)和 B1 上板实测。
|
||||
- **`rail_all_off()` 是复位 / 收尾路径,不是故障快路径。** A10 过压要断的是出事
|
||||
那一路,直接调 `rail_efuse_enable(rail, false)`,别等着遍历五路。
|
||||
- **开机顺序**:`rail_selftest()` → `rail_en_init()`(十个脚配成输出并拉低)→
|
||||
才轮到 `rail_volt_init()` 等器件。DAC 一上电输出就有电压,EN 这时必须已经确定是低的。
|
||||
- **高有效只出现一次**(`en_write()`)。和 `sr595_set_hub_reset()` 的反相、
|
||||
`rail_mv_to_code()` 的减法同一条规矩。
|
||||
- **自检不过就 abort,不降级。** 它查的全是编译进固件的常量表,不过 = 固件写错了,
|
||||
§10.10 的降级矩阵管的是硬件坏了。带着错表跑起来的样子是「命令发给了另一路」,
|
||||
五台手机全在跑、一个错误码都不报。
|
||||
## 这一层的几条约定(写 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,是预期的,不是故障。
|
||||
|
||||
+108
-103
@@ -1,103 +1,108 @@
|
||||
/* INA226 —— 板上 6 片(CH1~CH5 输出各一片 + 输入侧一片),分流电阻全部 5 mΩ / 1%。
|
||||
地址表见设计文档 §8.2,**不是顺排的**,详见下面 ina226_rail_addr() 的警告。
|
||||
|
||||
本驱动只通过 hal_i2c_* 访问总线,宿主机上用 sim 的假器件即可全部测完。 */
|
||||
#ifndef INA226_H
|
||||
#define INA226_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- 寄存器(全部 16 位、大端) ---------------------------------------- */
|
||||
|
||||
#define INA226_REG_CONFIG 0x00
|
||||
#define INA226_REG_SHUNT_V 0x01
|
||||
#define INA226_REG_BUS_V 0x02
|
||||
#define INA226_REG_POWER 0x03
|
||||
#define INA226_REG_CURRENT 0x04
|
||||
#define INA226_REG_CALIB 0x05
|
||||
#define INA226_REG_MASK_EN 0x06
|
||||
#define INA226_REG_ALERT_L 0x07
|
||||
#define INA226_REG_MFG_ID 0xFE
|
||||
#define INA226_REG_DIE_ID 0xFF
|
||||
|
||||
#define INA226_MFG_ID 0x5449 /* 'TI' */
|
||||
#define INA226_DIE_ID 0x2260
|
||||
|
||||
/* ---- 量程与标定(五路与输入侧同一颗分流电阻,所以是常量不是参数) ------ */
|
||||
|
||||
#define INA226_SHUNT_UOHM 5000 /* 5 mΩ,设计文档 §8.2 */
|
||||
#define INA226_CURRENT_LSB_UA 500 /* 0.5 mA —— 分流 LSB 2.5 µV ÷ 5 mΩ */
|
||||
|
||||
/* CAL = 0.00512 / (Current_LSB × R_shunt) = 0.00512 / (500 µA × 5 mΩ) = 2048 */
|
||||
#define INA226_CALIB 2048
|
||||
|
||||
/* Config:保留位 100 + AVG=4 + VBUSCT=1.1 ms + VSHCT=1.1 ms + 连续测量分流与母线。
|
||||
转换周期 4 × (1.1 + 1.1) = 8.8 ms < t_safety 的 20 ms,所以每次巡检都拿到新的一帧。 */
|
||||
#define INA226_CONFIG 0x4327
|
||||
|
||||
/* ---- 地址表(设计文档 §8.2) ------------------------------------------- */
|
||||
|
||||
#define INA226_RAIL_COUNT 5
|
||||
#define INA226_COUNT 6
|
||||
#define INA226_ADDR_INPUT 0x43 /* U13,输入侧 V+I,整板功率预算用 */
|
||||
|
||||
/* CH1~CH5 → U8~U12 的地址。rail 是 1~5,越界直接 abort。
|
||||
⚠️ **`0x40 + ch` 是错的**,CH3/CH4/CH5 三路全错(0x44 / 0x45 / 0x42)。
|
||||
地址按布线难度重排过(§8.2 那张 A1/A0 表),永远查表,不要算。 */
|
||||
uint8_t ina226_rail_addr(int rail);
|
||||
|
||||
extern const uint8_t ina226_all_addr[INA226_COUNT]; /* CH1..CH5 + 输入侧,扫描用 */
|
||||
|
||||
/* 六个地址互不重复、且不与 DAC63204(0x4A/0x4B) / PCA9555(0x20) / LM75(0x48) 撞车。
|
||||
启动自检调一次;返回 false 说明表被改坏了。 */
|
||||
bool ina226_addr_table_ok(void);
|
||||
|
||||
/* ---- 采样 --------------------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
int32_t bus_mv; /* 母线电压,1.25 mV/LSB */
|
||||
int32_t current_ua; /* **有符号** —— 反灌时为负(SW-8 靠这个判据) */
|
||||
} ina226_vi_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t bus_mv;
|
||||
int32_t shunt_uv; /* 2.5 µV/LSB,四舍五入到 µV(±1.25 µV ≈ ±0.25 mA) */
|
||||
int32_t current_ua;
|
||||
int32_t power_mw; /* 12.5 mW/LSB,向零截断 */
|
||||
} ina226_sample_t;
|
||||
|
||||
/* 读 Manufacturer ID / Die ID 认件。
|
||||
器件不应答 → HAL_ERR_NAK;**应答了但 ID 不对 → HAL_ERR_BUS**
|
||||
(这个地址上有东西,但不是 INA226:多半地址跳线焊错或贴错件)。 */
|
||||
hal_err_t ina226_probe(uint8_t addr);
|
||||
|
||||
/* 写 Config 与 Calibration 并**逐个回读校验**。回读不一致 → HAL_ERR_BUS。 */
|
||||
hal_err_t ina226_init(uint8_t addr);
|
||||
|
||||
/* 只校验 Calibration 还在。掉了的现象是 **Current/Power 恒读 0**(不是报错),
|
||||
看上去就像「这台手机不耗电」,所以巡检里要定期查一次。 */
|
||||
hal_err_t ina226_check(uint8_t addr);
|
||||
|
||||
/* 巡检主路径:母线电压 + 电流,两次事务。 */
|
||||
hal_err_t ina226_read_vi(uint8_t addr, ina226_vi_t *out);
|
||||
|
||||
/* 诊断/事件记录用:四个量一次读齐,四次事务。 */
|
||||
hal_err_t ina226_read_all(uint8_t addr, ina226_sample_t *out);
|
||||
|
||||
hal_err_t ina226_read_bus_mv(uint8_t addr, int32_t *mv);
|
||||
hal_err_t ina226_read_shunt_uv(uint8_t addr, int32_t *uv);
|
||||
hal_err_t ina226_read_current_ua(uint8_t addr, int32_t *ua);
|
||||
hal_err_t ina226_read_power_mw(uint8_t addr, int32_t *mw);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* INA226_H */
|
||||
/* INA226 —— 板上 6 片(CH1~CH5 输出各一片 + 输入侧一片),分流电阻全部 5 mΩ / 1%。
|
||||
地址表见设计文档 §8.2,**不是顺排的**,详见下面 ina226_rail_addr() 的警告。
|
||||
|
||||
本驱动只通过 hal_i2c_* 访问总线,宿主机上用 sim 的假器件即可全部测完。 */
|
||||
#ifndef INA226_H
|
||||
#define INA226_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- 寄存器(全部 16 位、大端) ---------------------------------------- */
|
||||
|
||||
#define INA226_REG_CONFIG 0x00
|
||||
#define INA226_REG_SHUNT_V 0x01
|
||||
#define INA226_REG_BUS_V 0x02
|
||||
#define INA226_REG_POWER 0x03
|
||||
#define INA226_REG_CURRENT 0x04
|
||||
#define INA226_REG_CALIB 0x05
|
||||
#define INA226_REG_MASK_EN 0x06
|
||||
#define INA226_REG_ALERT_L 0x07
|
||||
#define INA226_REG_MFG_ID 0xFE
|
||||
#define INA226_REG_DIE_ID 0xFF
|
||||
|
||||
#define INA226_MFG_ID 0x5449 /* 'TI' */
|
||||
#define INA226_DIE_ID 0x2260
|
||||
|
||||
/* ---- 量程与标定(五路与输入侧同一颗分流电阻,所以是常量不是参数) ------ */
|
||||
|
||||
#define INA226_SHUNT_UOHM 5000 /* 5 mΩ,设计文档 §8.2 */
|
||||
#define INA226_CURRENT_LSB_UA 500 /* 0.5 mA —— 分流 LSB 2.5 µV ÷ 5 mΩ */
|
||||
|
||||
/* CAL = 0.00512 / (Current_LSB × R_shunt) = 0.00512 / (500 µA × 5 mΩ) = 2048 */
|
||||
#define INA226_CALIB 2048
|
||||
|
||||
/* Config:保留位 100 + AVG=4 + VBUSCT=1.1 ms + VSHCT=1.1 ms + 连续测量分流与母线。
|
||||
转换周期 4 × (1.1 + 1.1) = 8.8 ms < t_safety 的 20 ms,所以每次巡检都拿到新的一帧。 */
|
||||
#define INA226_CONFIG 0x4327
|
||||
|
||||
/* 上面那个 Config 折算出来的**一帧转换时间**:AVG=4 × (1.1 ms + 1.1 ms) = 8.8 ms。
|
||||
`t_safety` 的周期必须长于它,否则同一帧会被读两次 —— 现象是「电流特别稳」,
|
||||
不是任何一种错误。tasks.c 的表自检拿它和 20 ms 对一次。 */
|
||||
#define INA226_CONV_US 8800
|
||||
|
||||
/* ---- 地址表(设计文档 §8.2) ------------------------------------------- */
|
||||
|
||||
#define INA226_RAIL_COUNT 5
|
||||
#define INA226_COUNT 6
|
||||
#define INA226_ADDR_INPUT 0x43 /* U13,输入侧 V+I,整板功率预算用 */
|
||||
|
||||
/* CH1~CH5 → U8~U12 的地址。rail 是 1~5,越界直接 abort。
|
||||
⚠️ **`0x40 + ch` 是错的**,CH3/CH4/CH5 三路全错(0x44 / 0x45 / 0x42)。
|
||||
地址按布线难度重排过(§8.2 那张 A1/A0 表),永远查表,不要算。 */
|
||||
uint8_t ina226_rail_addr(int rail);
|
||||
|
||||
extern const uint8_t ina226_all_addr[INA226_COUNT]; /* CH1..CH5 + 输入侧,扫描用 */
|
||||
|
||||
/* 六个地址互不重复、且不与 DAC63204(0x4A/0x4B) / PCA9555(0x20) / LM75(0x48) 撞车。
|
||||
启动自检调一次;返回 false 说明表被改坏了。 */
|
||||
bool ina226_addr_table_ok(void);
|
||||
|
||||
/* ---- 采样 --------------------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
int32_t bus_mv; /* 母线电压,1.25 mV/LSB */
|
||||
int32_t current_ua; /* **有符号** —— 反灌时为负(SW-8 靠这个判据) */
|
||||
} ina226_vi_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t bus_mv;
|
||||
int32_t shunt_uv; /* 2.5 µV/LSB,四舍五入到 µV(±1.25 µV ≈ ±0.25 mA) */
|
||||
int32_t current_ua;
|
||||
int32_t power_mw; /* 12.5 mW/LSB,向零截断 */
|
||||
} ina226_sample_t;
|
||||
|
||||
/* 读 Manufacturer ID / Die ID 认件。
|
||||
器件不应答 → HAL_ERR_NAK;**应答了但 ID 不对 → HAL_ERR_BUS**
|
||||
(这个地址上有东西,但不是 INA226:多半地址跳线焊错或贴错件)。 */
|
||||
hal_err_t ina226_probe(uint8_t addr);
|
||||
|
||||
/* 写 Config 与 Calibration 并**逐个回读校验**。回读不一致 → HAL_ERR_BUS。 */
|
||||
hal_err_t ina226_init(uint8_t addr);
|
||||
|
||||
/* 只校验 Calibration 还在。掉了的现象是 **Current/Power 恒读 0**(不是报错),
|
||||
看上去就像「这台手机不耗电」,所以巡检里要定期查一次。 */
|
||||
hal_err_t ina226_check(uint8_t addr);
|
||||
|
||||
/* 巡检主路径:母线电压 + 电流,两次事务。 */
|
||||
hal_err_t ina226_read_vi(uint8_t addr, ina226_vi_t *out);
|
||||
|
||||
/* 诊断/事件记录用:四个量一次读齐,四次事务。 */
|
||||
hal_err_t ina226_read_all(uint8_t addr, ina226_sample_t *out);
|
||||
|
||||
hal_err_t ina226_read_bus_mv(uint8_t addr, int32_t *mv);
|
||||
hal_err_t ina226_read_shunt_uv(uint8_t addr, int32_t *uv);
|
||||
hal_err_t ina226_read_current_ua(uint8_t addr, int32_t *ua);
|
||||
hal_err_t ina226_read_power_mw(uint8_t addr, int32_t *mw);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* INA226_H */
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# PhoneRail 的硬件抽象层(ESP-IDF 侧)。宿主机侧编的是 sim/,由 host/CMakeLists.txt 收。
|
||||
#
|
||||
# ⚠️ 组件名不能叫 `hal` —— ESP-IDF 自己有一个 components/hal(driver 依赖它),
|
||||
# 重名会把 IDF 的那个盖掉。所以目录叫 phonerail_hal。
|
||||
idf_component_register(
|
||||
SRCS "hal_err.c"
|
||||
"esp/hal_i2c_esp.c"
|
||||
"esp/hal_gpio_esp.c"
|
||||
"esp/hal_pwm_esp.c"
|
||||
"esp/hal_time_esp.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_driver_i2c esp_driver_gpio esp_driver_ledc esp_timer freertos)
|
||||
# PhoneRail 的硬件抽象层(ESP-IDF 侧)。宿主机侧编的是 sim/,由 host/CMakeLists.txt 收。
|
||||
#
|
||||
# ⚠️ 组件名不能叫 `hal` —— ESP-IDF 自己有一个 components/hal(driver 依赖它),
|
||||
# 重名会把 IDF 的那个盖掉。所以目录叫 phonerail_hal。
|
||||
idf_component_register(
|
||||
SRCS "hal_err.c"
|
||||
"hal_i2c_acct.c"
|
||||
"esp/hal_lock_esp.c"
|
||||
"esp/hal_i2c_esp.c"
|
||||
"esp/hal_gpio_esp.c"
|
||||
"esp/hal_pwm_esp.c"
|
||||
"esp/hal_time_esp.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_driver_i2c esp_driver_gpio esp_driver_ledc esp_timer freertos)
|
||||
|
||||
@@ -1,114 +1,120 @@
|
||||
/* I²C 的 ESP-IDF v5.4 实现(新版 i2c_master 驱动,不是已废弃的 driver/i2c.h)。 */
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "hal_i2c.h"
|
||||
|
||||
#define I2C_XFER_TIMEOUT_MS 50
|
||||
#define I2C_DEFAULT_FREQ_HZ 400000
|
||||
|
||||
static i2c_master_bus_handle_t s_bus;
|
||||
static i2c_master_dev_handle_t s_devs[128]; /* 按 7 位地址索引,用到才建 */
|
||||
static uint32_t s_freq_hz;
|
||||
|
||||
static hal_err_t map_err(esp_err_t e)
|
||||
{
|
||||
switch (e) {
|
||||
case ESP_OK: return HAL_OK;
|
||||
case ESP_ERR_INVALID_ARG: return HAL_ERR_ARG;
|
||||
case ESP_ERR_INVALID_STATE: return HAL_ERR_STATE;
|
||||
case ESP_ERR_TIMEOUT: return HAL_ERR_TIMEOUT;
|
||||
case ESP_ERR_NOT_FOUND: return HAL_ERR_NAK;
|
||||
case ESP_FAIL: return HAL_ERR_NAK; /* 从机没 ACK 走这条 */
|
||||
default: return HAL_ERR_BUS;
|
||||
}
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_init(const hal_i2c_cfg_t *cfg)
|
||||
{
|
||||
if (cfg == NULL || cfg->sda_gpio < 0 || cfg->scl_gpio < 0) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if (s_bus != NULL) {
|
||||
return HAL_OK; /* 幂等 */
|
||||
}
|
||||
i2c_master_bus_config_t bc = {
|
||||
.i2c_port = -1, /* 自动选口 */
|
||||
.sda_io_num = (gpio_num_t)cfg->sda_gpio,
|
||||
.scl_io_num = (gpio_num_t)cfg->scl_gpio,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
/* 板上已有 4.7 kΩ 外部上拉(设计文档 §8.2),内部上拉 45 kΩ 太弱,
|
||||
开着只会掩盖「上拉电阻漏焊」这类问题。 */
|
||||
.flags.enable_internal_pullup = false,
|
||||
};
|
||||
esp_err_t e = i2c_new_master_bus(&bc, &s_bus);
|
||||
if (e != ESP_OK) {
|
||||
return map_err(e);
|
||||
}
|
||||
s_freq_hz = cfg->freq_hz != 0 ? cfg->freq_hz : I2C_DEFAULT_FREQ_HZ;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
static hal_err_t dev_of(uint8_t addr, i2c_master_dev_handle_t *out)
|
||||
{
|
||||
if (s_bus == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (addr > 0x7f) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if (s_devs[addr] == NULL) {
|
||||
i2c_device_config_t dc = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = addr,
|
||||
.scl_speed_hz = s_freq_hz,
|
||||
};
|
||||
esp_err_t e = i2c_master_bus_add_device(s_bus, &dc, &s_devs[addr]);
|
||||
if (e != ESP_OK) {
|
||||
return map_err(e);
|
||||
}
|
||||
}
|
||||
*out = s_devs[addr];
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write(uint8_t addr, const uint8_t *data, size_t len)
|
||||
{
|
||||
i2c_master_dev_handle_t dev;
|
||||
hal_err_t err = dev_of(addr, &dev);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return map_err(i2c_master_transmit(dev, data, len, I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_read(uint8_t addr, uint8_t *data, size_t len)
|
||||
{
|
||||
i2c_master_dev_handle_t dev;
|
||||
hal_err_t err = dev_of(addr, &dev);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return map_err(i2c_master_receive(dev, data, len, I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write_read(uint8_t addr, const uint8_t *wr, size_t wr_len,
|
||||
uint8_t *rd, size_t rd_len)
|
||||
{
|
||||
i2c_master_dev_handle_t dev;
|
||||
hal_err_t err = dev_of(addr, &dev);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return map_err(i2c_master_transmit_receive(dev, wr, wr_len, rd, rd_len,
|
||||
I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_probe(uint8_t addr)
|
||||
{
|
||||
if (s_bus == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
return map_err(i2c_master_probe(s_bus, addr, I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
/* I²C 的 ESP-IDF v5.4 实现(新版 i2c_master 驱动,不是已废弃的 driver/i2c.h)。 */
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "hal_i2c.h"
|
||||
#include "hal_i2c_acct.h"
|
||||
|
||||
#define I2C_XFER_TIMEOUT_MS 50
|
||||
#define I2C_DEFAULT_FREQ_HZ 400000
|
||||
|
||||
static i2c_master_bus_handle_t s_bus;
|
||||
static i2c_master_dev_handle_t s_devs[128]; /* 按 7 位地址索引,用到才建 */
|
||||
static uint32_t s_freq_hz;
|
||||
|
||||
static hal_err_t map_err(esp_err_t e)
|
||||
{
|
||||
switch (e) {
|
||||
case ESP_OK: return HAL_OK;
|
||||
case ESP_ERR_INVALID_ARG: return HAL_ERR_ARG;
|
||||
case ESP_ERR_INVALID_STATE: return HAL_ERR_STATE;
|
||||
case ESP_ERR_TIMEOUT: return HAL_ERR_TIMEOUT;
|
||||
case ESP_ERR_NOT_FOUND: return HAL_ERR_NAK;
|
||||
case ESP_FAIL: return HAL_ERR_NAK; /* 从机没 ACK 走这条 */
|
||||
default: return HAL_ERR_BUS;
|
||||
}
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_init(const hal_i2c_cfg_t *cfg)
|
||||
{
|
||||
if (cfg == NULL || cfg->sda_gpio < 0 || cfg->scl_gpio < 0) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if (s_bus != NULL) {
|
||||
return HAL_OK; /* 幂等 */
|
||||
}
|
||||
i2c_master_bus_config_t bc = {
|
||||
.i2c_port = -1, /* 自动选口 */
|
||||
.sda_io_num = (gpio_num_t)cfg->sda_gpio,
|
||||
.scl_io_num = (gpio_num_t)cfg->scl_gpio,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
/* 板上已有 4.7 kΩ 外部上拉(设计文档 §8.2),内部上拉 45 kΩ 太弱,
|
||||
开着只会掩盖「上拉电阻漏焊」这类问题。 */
|
||||
.flags.enable_internal_pullup = false,
|
||||
};
|
||||
esp_err_t e = i2c_new_master_bus(&bc, &s_bus);
|
||||
if (e != ESP_OK) {
|
||||
return map_err(e);
|
||||
}
|
||||
s_freq_hz = cfg->freq_hz != 0 ? cfg->freq_hz : I2C_DEFAULT_FREQ_HZ;
|
||||
hal_i2c_acct_set_freq(s_freq_hz); /* 占用核算按真实速率折算,见 hal_i2c_acct.h */
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
static hal_err_t dev_of(uint8_t addr, i2c_master_dev_handle_t *out)
|
||||
{
|
||||
if (s_bus == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (addr > 0x7f) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if (s_devs[addr] == NULL) {
|
||||
i2c_device_config_t dc = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = addr,
|
||||
.scl_speed_hz = s_freq_hz,
|
||||
};
|
||||
esp_err_t e = i2c_master_bus_add_device(s_bus, &dc, &s_devs[addr]);
|
||||
if (e != ESP_OK) {
|
||||
return map_err(e);
|
||||
}
|
||||
}
|
||||
*out = s_devs[addr];
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write(uint8_t addr, const uint8_t *data, size_t len)
|
||||
{
|
||||
i2c_master_dev_handle_t dev;
|
||||
hal_err_t err = dev_of(addr, &dev);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
hal_i2c_acct_add(len, 0);
|
||||
return map_err(i2c_master_transmit(dev, data, len, I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_read(uint8_t addr, uint8_t *data, size_t len)
|
||||
{
|
||||
i2c_master_dev_handle_t dev;
|
||||
hal_err_t err = dev_of(addr, &dev);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
hal_i2c_acct_add(0, len);
|
||||
return map_err(i2c_master_receive(dev, data, len, I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write_read(uint8_t addr, const uint8_t *wr, size_t wr_len,
|
||||
uint8_t *rd, size_t rd_len)
|
||||
{
|
||||
i2c_master_dev_handle_t dev;
|
||||
hal_err_t err = dev_of(addr, &dev);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
hal_i2c_acct_add(wr_len, rd_len);
|
||||
return map_err(i2c_master_transmit_receive(dev, wr, wr_len, rd, rd_len,
|
||||
I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_probe(uint8_t addr)
|
||||
{
|
||||
if (s_bus == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
hal_i2c_acct_add(0, 0);
|
||||
return map_err(i2c_master_probe(s_bus, addr, I2C_XFER_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/* 两把互斥锁的 FreeRTOS 实现。静态分配(`xSemaphoreCreateMutexStatic`)——
|
||||
两把锁的数量是编译期定死的,没有理由去堆上要内存,也就没有「创建失败」这条路径。
|
||||
|
||||
⚠️ 用的是 mutex 不是 binary semaphore:mutex 有**优先级继承**。
|
||||
t_safety 是最高优先级且每 20 ms 就要总线,低优先级的 t_link 拿着 I²C 锁去读遥测时
|
||||
如果没有继承,t_safety 会被一个中优先级任务无限期挡在门外(经典优先级反转)。 */
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
#include "hal_lock.h"
|
||||
|
||||
static StaticSemaphore_t s_buf[HAL_LOCK_COUNT];
|
||||
static SemaphoreHandle_t s_mtx[HAL_LOCK_COUNT];
|
||||
static uint32_t s_timeouts[HAL_LOCK_COUNT];
|
||||
|
||||
hal_err_t hal_lock_init(void)
|
||||
{
|
||||
for (int i = 0; i < HAL_LOCK_COUNT; i++) {
|
||||
if (s_mtx[i] == NULL) {
|
||||
s_mtx[i] = xSemaphoreCreateMutexStatic(&s_buf[i]);
|
||||
s_timeouts[i] = 0;
|
||||
}
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
bool hal_lock_held(hal_lock_id_t id)
|
||||
{
|
||||
if (id < 0 || id >= HAL_LOCK_COUNT || s_mtx[id] == NULL) {
|
||||
return false;
|
||||
}
|
||||
return xSemaphoreGetMutexHolder(s_mtx[id]) == xTaskGetCurrentTaskHandle();
|
||||
}
|
||||
|
||||
hal_err_t hal_lock_take(hal_lock_id_t id, uint32_t timeout_ms)
|
||||
{
|
||||
if (id < 0 || id >= HAL_LOCK_COUNT || s_mtx[id] == NULL) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
/* 已经持有 = 重复加锁;持有另一把 = 嵌套。两者都是写错了,见 hal_lock.h。 */
|
||||
for (int i = 0; i < HAL_LOCK_COUNT; i++) {
|
||||
if (hal_lock_held((hal_lock_id_t)i)) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
}
|
||||
const TickType_t ticks =
|
||||
(timeout_ms == HAL_LOCK_FOREVER) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
|
||||
if (xSemaphoreTake(s_mtx[id], ticks) != pdTRUE) {
|
||||
s_timeouts[id]++;
|
||||
return HAL_ERR_TIMEOUT;
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
void hal_lock_give(hal_lock_id_t id)
|
||||
{
|
||||
configASSERT(id >= 0 && id < HAL_LOCK_COUNT && s_mtx[id] != NULL);
|
||||
configASSERT(hal_lock_held(id)); /* 不是自己拿的就别放 */
|
||||
(void)xSemaphoreGive(s_mtx[id]);
|
||||
}
|
||||
|
||||
uint32_t hal_lock_timeouts(hal_lock_id_t id)
|
||||
{
|
||||
return (id >= 0 && id < HAL_LOCK_COUNT) ? s_timeouts[id] : 0;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/* I²C 占用核算的实现。**两个目标编的是同一份**(和 hal_err.c 一样放在组件根下),
|
||||
所以宿主机上算出来的占用率就是上板会打印的那个数。 */
|
||||
#include "hal_i2c_acct.h"
|
||||
|
||||
static uint64_t s_bits;
|
||||
static uint32_t s_txns;
|
||||
static uint32_t s_freq_hz = HAL_I2C_FREQ_HZ_DEFAULT;
|
||||
|
||||
uint32_t hal_i2c_txn_bits(size_t wr_len, size_t rd_len)
|
||||
{
|
||||
if (wr_len == 0 && rd_len == 0) {
|
||||
return 1u + 9u + 1u; /* 探测:START + 地址字节 + STOP */
|
||||
}
|
||||
|
||||
uint32_t bits = 1u; /* START */
|
||||
if (wr_len > 0) {
|
||||
bits += 9u * (uint32_t)(1u + wr_len); /* 地址 + 载荷 */
|
||||
}
|
||||
if (rd_len > 0) {
|
||||
if (wr_len > 0) {
|
||||
bits += 1u; /* 重复 START */
|
||||
}
|
||||
bits += 9u * (uint32_t)(1u + rd_len);
|
||||
}
|
||||
bits += 1u; /* STOP */
|
||||
return bits;
|
||||
}
|
||||
|
||||
void hal_i2c_acct_add(size_t wr_len, size_t rd_len)
|
||||
{
|
||||
s_bits += hal_i2c_txn_bits(wr_len, rd_len);
|
||||
s_txns++;
|
||||
}
|
||||
|
||||
uint64_t hal_i2c_acct_bits(void) { return s_bits; }
|
||||
uint32_t hal_i2c_acct_txns(void) { return s_txns; }
|
||||
|
||||
void hal_i2c_acct_set_freq(uint32_t hz)
|
||||
{
|
||||
s_freq_hz = (hz == 0) ? HAL_I2C_FREQ_HZ_DEFAULT : hz;
|
||||
}
|
||||
|
||||
uint32_t hal_i2c_acct_freq(void) { return s_freq_hz; }
|
||||
|
||||
uint64_t hal_i2c_bits_to_us(uint64_t bits)
|
||||
{
|
||||
/* 先乘后除,别先算「每位多少 µs」—— 400 kHz 下那是 2.5 µs,整数除法会抹平。 */
|
||||
return (bits * 1000000ull) / (uint64_t)s_freq_hz;
|
||||
}
|
||||
|
||||
uint32_t hal_i2c_acct_window_us(uint64_t *cursor_bits)
|
||||
{
|
||||
const uint64_t now = s_bits;
|
||||
const uint64_t was = (cursor_bits != NULL) ? *cursor_bits : 0;
|
||||
if (cursor_bits != NULL) {
|
||||
*cursor_bits = now;
|
||||
}
|
||||
return (uint32_t)hal_i2c_bits_to_us(now - was);
|
||||
}
|
||||
|
||||
void hal_i2c_acct_reset(void)
|
||||
{
|
||||
s_bits = 0;
|
||||
s_txns = 0;
|
||||
s_freq_hz = HAL_I2C_FREQ_HZ_DEFAULT;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/* I²C 总线占用核算 —— 需求 §10.3 那 8 % 的可执行版本。
|
||||
|
||||
§10.3 手算了一遍「t_safety 每 20 ms 要占总线多久」,结论是约 1.5 ms、约 8 %,
|
||||
并在末尾要求「首板上电时用逻辑分析仪实测复核一次」。这里做的是中间那一档:
|
||||
让固件**自己按线上真实发生的事务**把这个数算出来,两个目标(sim 与 ESP)
|
||||
用的是同一份计数代码,所以宿主机上跑出来的数就是上板会打印出来的数。
|
||||
|
||||
为什么放在 HAL 里而不是应用层:只有 HAL 知道每次事务真正过了多少字节。
|
||||
放在应用层就变成「按计划中的事务数手算」,那是把 §10.3 的算式抄第二遍
|
||||
—— 少读一次寄存器、多一次重试,手算的那份都不会变。
|
||||
|
||||
⚠️ 这是**理想时序**的下限:不含时钟延展、不含 NAK 重试、不含事务之间
|
||||
固件自己的开销。它能证明「预算没被写崩」,不能替代 §10.3 要求的实测。 */
|
||||
#ifndef HAL_I2C_ACCT_H
|
||||
#define HAL_I2C_ACCT_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define HAL_I2C_FREQ_HZ_DEFAULT 400000u
|
||||
|
||||
/* 一次事务在总线上占的**位周期数**。
|
||||
写: START + (1+wr_len) 个字节 × 9 位 + STOP
|
||||
写后读: START + (1+wr_len) 字节 + 重复 START + (1+rd_len) 字节 + STOP
|
||||
每字节 9 位 = 8 数据 + 1 应答;START / 重复 START / STOP 各按 1 个位周期计。
|
||||
rd_len == 0 就是纯写,wr_len == 0 就是纯读(探测是 wr_len = rd_len = 0)。
|
||||
|
||||
对一次 INA226 寄存器读(wr 1 字节、rd 2 字节)它给出 48 位 = 120 µs @400 kHz。
|
||||
§10.3 手算的是「≈46 个位周期 ≈115 µs」—— 差的两位是 STOP 和一个 START,
|
||||
§10.3 没数进去。整周期的结论不变(1.56 ms / 7.8 %,见 §10.3 的「约 1.5 ms / 约 8 %」)。 */
|
||||
uint32_t hal_i2c_txn_bits(size_t wr_len, size_t rd_len);
|
||||
|
||||
/* 累计位周期数。两个 HAL 实现都在事务成功**与失败**之后调它 ——
|
||||
NAK 掉的事务也占了总线时间(地址字节已经发出去了)。 */
|
||||
void hal_i2c_acct_add(size_t wr_len, size_t rd_len);
|
||||
uint64_t hal_i2c_acct_bits(void);
|
||||
uint32_t hal_i2c_acct_txns(void);
|
||||
|
||||
/* 位周期 → µs,按当前总线频率。hal_i2c_init() 会把频率填进来(0 视为默认 400 kHz)。 */
|
||||
void hal_i2c_acct_set_freq(uint32_t hz);
|
||||
uint32_t hal_i2c_acct_freq(void);
|
||||
uint64_t hal_i2c_bits_to_us(uint64_t bits);
|
||||
|
||||
/* 取「从上一次取样到现在」占了多少 µs,并把取样点推到现在。
|
||||
t_safety 每个周期调一次,拿到的就是这一周期的总线占用。 */
|
||||
uint32_t hal_i2c_acct_window_us(uint64_t *cursor_bits);
|
||||
|
||||
void hal_i2c_acct_reset(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HAL_I2C_ACCT_H */
|
||||
@@ -0,0 +1,61 @@
|
||||
/* 互斥锁 —— HAL 的第五样东西,而且**只有两把**(需求 §10.3)。
|
||||
|
||||
计划 §4.1 写着「HAL 只抽象四样:I²C / GPIO / PWM-定时 / 单调时钟,不要抽象得更多」。
|
||||
这里多出来的第五样是有理由的:§10.3 的五个任务共享的资源正好是两个
|
||||
—— I²C 总线(10 个器件在同一条上)和 74HC595 的 24 位链 —— 而「谁在用总线」
|
||||
这件事没法用前四样表达。
|
||||
|
||||
为了不让它长成一个通用的锁库,这里**没有创建接口**:两把锁是枚举出来的,
|
||||
编译期就这么多,也不许再加第三把。要加第三把之前先回头看一眼 §10.3
|
||||
——「共享资源只有两个」是架构结论,不是当时懒得数。
|
||||
|
||||
⚠️ **两把锁不许嵌套持有。** 595 那条链是纯 GPIO(SER/SRCLK/RCLK),
|
||||
和 I²C 没有任何交集,所以「拿着 A 去拿 B」这件事在这块板子上永远是写错了,
|
||||
而不是一个需要小心处理的情况。`hal_lock_take()` 直接把它判成 HAL_ERR_STATE
|
||||
—— 与其定一套加锁顺序然后指望后面每个人都记得,不如让它根本不可能发生。 */
|
||||
#ifndef HAL_LOCK_H
|
||||
#define HAL_LOCK_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
HAL_LOCK_I2C = 0, /* I²C 总线:6×INA226 + 2×DAC63204 + PCA9555(+ 载板 LM75) */
|
||||
HAL_LOCK_SR595, /* 74HC595 的 24 位链,全板共享(§10.6 的影子寄存器) */
|
||||
HAL_LOCK_COUNT,
|
||||
} hal_lock_id_t;
|
||||
|
||||
/* 幂等。任何任务起来之前调一次;没 init 就 take 会返回 HAL_ERR_STATE
|
||||
(FreeRTOS 上那是个空句柄,不拦就是随机崩)。 */
|
||||
hal_err_t hal_lock_init(void);
|
||||
|
||||
/* 拿锁。timeout_ms = HAL_LOCK_FOREVER 表示一直等。
|
||||
HAL_ERR_TIMEOUT —— 等超时了,**调用方必须自己处理**:巡检任务应当跳过这一周期
|
||||
并计一次数,不要死等(死等的现象是 t_safety 整个停摆)。
|
||||
HAL_ERR_STATE —— 没 init,或者违反了「不许嵌套持有两把锁」。
|
||||
同一个任务重复拿同一把锁也是 HAL_ERR_STATE:这两把都不是递归锁。 */
|
||||
#define HAL_LOCK_FOREVER 0xFFFFFFFFu
|
||||
hal_err_t hal_lock_take(hal_lock_id_t id, uint32_t timeout_ms);
|
||||
|
||||
/* 放锁。不是自己拿的、或者根本没人拿,都会 abort ——
|
||||
那是代码写错了,悄悄放掉会让下一个持有者莫名其妙地丢掉互斥。 */
|
||||
void hal_lock_give(hal_lock_id_t id);
|
||||
|
||||
/* **当前任务**是不是正持着这把锁。给断言和 sim 的强制检查用,
|
||||
不要拿它做流程控制(「没拿就拿一下」正是双重加锁的写法)。 */
|
||||
bool hal_lock_held(hal_lock_id_t id);
|
||||
|
||||
/* 累计等锁失败次数(超时)。遥测里报出去,用来看总线是不是排队了。 */
|
||||
uint32_t hal_lock_timeouts(hal_lock_id_t id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HAL_LOCK_H */
|
||||
@@ -1,174 +1,193 @@
|
||||
/* I²C 的假实现:一张寄存器表 + 可注入的 NAK。宿主机专用。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sim_hal.h"
|
||||
|
||||
static sim_i2c_dev_t s_dev[SIM_I2C_MAX_DEV];
|
||||
static int s_n;
|
||||
static bool s_inited;
|
||||
|
||||
void sim_i2c_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
void sim_i2c_reset_internal(void)
|
||||
{
|
||||
memset(s_dev, 0, sizeof(s_dev));
|
||||
s_n = 0;
|
||||
s_inited = false;
|
||||
}
|
||||
|
||||
sim_i2c_dev_t *sim_i2c_dev(uint8_t addr)
|
||||
{
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_dev[i].addr == addr) {
|
||||
return &s_dev[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sim_i2c_dev_t *sim_i2c_add(uint8_t addr, uint8_t reg_width)
|
||||
{
|
||||
assert(addr <= 0x7f);
|
||||
assert(reg_width == 1 || reg_width == 2);
|
||||
assert(sim_i2c_dev(addr) == NULL); /* 地址撞车:测试写错了 */
|
||||
assert(s_n < SIM_I2C_MAX_DEV);
|
||||
|
||||
sim_i2c_dev_t *d = &s_dev[s_n++];
|
||||
memset(d, 0, sizeof(*d));
|
||||
d->addr = addr;
|
||||
d->reg_width = reg_width;
|
||||
d->present = true;
|
||||
return d;
|
||||
}
|
||||
|
||||
static sim_i2c_dev_t *must(uint8_t addr)
|
||||
{
|
||||
sim_i2c_dev_t *d = sim_i2c_dev(addr);
|
||||
assert(d != NULL); /* 先 sim_i2c_add() 再预置寄存器 */
|
||||
return d;
|
||||
}
|
||||
|
||||
void sim_i2c_set_reg(uint8_t addr, uint8_t reg, uint16_t val) { must(addr)->regs[reg] = val; }
|
||||
uint16_t sim_i2c_get_reg(uint8_t addr, uint8_t reg) { return must(addr)->regs[reg]; }
|
||||
void sim_i2c_inject_nak(uint8_t addr, int count) { must(addr)->nak_left = count; }
|
||||
void sim_i2c_set_present(uint8_t addr, bool present) { must(addr)->present = present; }
|
||||
|
||||
/* ---- HAL 实现 ---------------------------------------------------------- */
|
||||
|
||||
hal_err_t hal_i2c_init(const hal_i2c_cfg_t *cfg)
|
||||
{
|
||||
(void)cfg; /* sim 不关心引脚与速率 */
|
||||
s_inited = true;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* 取器件并把「这次事务要不要 NAK」判掉。 */
|
||||
static hal_err_t take(uint8_t addr, sim_i2c_dev_t **out)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (addr > 0x7f) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
sim_i2c_dev_t *d = sim_i2c_dev(addr);
|
||||
if (d == NULL || !d->present) {
|
||||
return HAL_ERR_NAK;
|
||||
}
|
||||
if (d->nak_left != 0) {
|
||||
if (d->nak_left > 0) {
|
||||
d->nak_left--;
|
||||
}
|
||||
return HAL_ERR_NAK;
|
||||
}
|
||||
*out = d;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* data[0] 是寄存器指针,其后按 reg_width 一格一格写,指针自增(uint8_t 自然回绕)。 */
|
||||
static hal_err_t do_write(sim_i2c_dev_t *d, const uint8_t *data, size_t len)
|
||||
{
|
||||
if (len != 0 && data == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if (len > 1 && ((len - 1) % d->reg_width) != 0) {
|
||||
return HAL_ERR_ARG; /* 半个寄存器:真器件上是未定义行为,这里直接报错 */
|
||||
}
|
||||
d->n_write++;
|
||||
d->last_write_len = (uint8_t)(len < SIM_I2C_LOG_BYTES ? len : SIM_I2C_LOG_BYTES);
|
||||
if (d->last_write_len != 0) {
|
||||
memcpy(d->last_write, data, d->last_write_len);
|
||||
}
|
||||
if (len == 0) {
|
||||
return HAL_OK; /* 只发地址 = 探测 */
|
||||
}
|
||||
d->ptr = data[0];
|
||||
for (size_t i = 1; i < len; i += d->reg_width) {
|
||||
uint16_t v = data[i];
|
||||
if (d->reg_width == 2) {
|
||||
v = (uint16_t)((v << 8) | data[i + 1]);
|
||||
}
|
||||
d->regs[d->ptr++] = v;
|
||||
}
|
||||
if (d->hook != NULL) {
|
||||
d->hook(d, SIM_I2C_EV_POST_WRITE);
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
static hal_err_t do_read(sim_i2c_dev_t *d, uint8_t *data, size_t len)
|
||||
{
|
||||
if (len != 0 && data == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if ((len % d->reg_width) != 0) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
d->n_read++;
|
||||
if (d->hook != NULL) {
|
||||
d->hook(d, SIM_I2C_EV_PRE_READ); /* 按 datasheet 建模的器件在这里合成输入寄存器 */
|
||||
}
|
||||
for (size_t i = 0; i < len; i += d->reg_width) {
|
||||
uint16_t v = d->regs[d->ptr++];
|
||||
if (d->reg_width == 2) {
|
||||
data[i] = (uint8_t)(v >> 8);
|
||||
data[i + 1] = (uint8_t)(v & 0xff);
|
||||
} else {
|
||||
data[i] = (uint8_t)v;
|
||||
}
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write(uint8_t addr, const uint8_t *data, size_t len)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
hal_err_t err = take(addr, &d);
|
||||
return err != HAL_OK ? err : do_write(d, data, len);
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_read(uint8_t addr, uint8_t *data, size_t len)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
hal_err_t err = take(addr, &d);
|
||||
return err != HAL_OK ? err : do_read(d, data, len);
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write_read(uint8_t addr, const uint8_t *wr, size_t wr_len,
|
||||
uint8_t *rd, size_t rd_len)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
hal_err_t err = take(addr, &d);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
err = do_write(d, wr, wr_len);
|
||||
return err != HAL_OK ? err : do_read(d, rd, rd_len);
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_probe(uint8_t addr)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
return take(addr, &d);
|
||||
}
|
||||
/* I²C 的假实现:一张寄存器表 + 可注入的 NAK。宿主机专用。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sim_hal.h"
|
||||
|
||||
static sim_i2c_dev_t s_dev[SIM_I2C_MAX_DEV];
|
||||
static int s_n;
|
||||
static bool s_inited;
|
||||
static bool s_bus_time = true;
|
||||
|
||||
void sim_i2c_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
void sim_i2c_reset_internal(void)
|
||||
{
|
||||
memset(s_dev, 0, sizeof(s_dev));
|
||||
s_n = 0;
|
||||
s_inited = false;
|
||||
s_bus_time = true;
|
||||
}
|
||||
|
||||
sim_i2c_dev_t *sim_i2c_dev(uint8_t addr)
|
||||
{
|
||||
for (int i = 0; i < s_n; i++) {
|
||||
if (s_dev[i].addr == addr) {
|
||||
return &s_dev[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sim_i2c_dev_t *sim_i2c_add(uint8_t addr, uint8_t reg_width)
|
||||
{
|
||||
assert(addr <= 0x7f);
|
||||
assert(reg_width == 1 || reg_width == 2);
|
||||
assert(sim_i2c_dev(addr) == NULL); /* 地址撞车:测试写错了 */
|
||||
assert(s_n < SIM_I2C_MAX_DEV);
|
||||
|
||||
sim_i2c_dev_t *d = &s_dev[s_n++];
|
||||
memset(d, 0, sizeof(*d));
|
||||
d->addr = addr;
|
||||
d->reg_width = reg_width;
|
||||
d->present = true;
|
||||
return d;
|
||||
}
|
||||
|
||||
static sim_i2c_dev_t *must(uint8_t addr)
|
||||
{
|
||||
sim_i2c_dev_t *d = sim_i2c_dev(addr);
|
||||
assert(d != NULL); /* 先 sim_i2c_add() 再预置寄存器 */
|
||||
return d;
|
||||
}
|
||||
|
||||
void sim_i2c_set_reg(uint8_t addr, uint8_t reg, uint16_t val) { must(addr)->regs[reg] = val; }
|
||||
uint16_t sim_i2c_get_reg(uint8_t addr, uint8_t reg) { return must(addr)->regs[reg]; }
|
||||
void sim_i2c_inject_nak(uint8_t addr, int count) { must(addr)->nak_left = count; }
|
||||
void sim_i2c_set_present(uint8_t addr, bool present) { must(addr)->present = present; }
|
||||
void sim_i2c_set_bus_time(bool on) { s_bus_time = on; }
|
||||
|
||||
/* 每次事务(成功或 NAK)都记账并把虚拟时钟花掉。NAK 也占总线:地址字节已经发出去了。
|
||||
同时查一次加锁纪律 —— I²C 是 §10.3 两个共享资源之一。 */
|
||||
static void spend(size_t wr_len, size_t rd_len)
|
||||
{
|
||||
sim_lock_check(HAL_LOCK_I2C);
|
||||
hal_i2c_acct_add(wr_len, rd_len);
|
||||
if (s_bus_time) {
|
||||
sim_time_advance_us(hal_i2c_bits_to_us(hal_i2c_txn_bits(wr_len, rd_len)));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- HAL 实现 ---------------------------------------------------------- */
|
||||
|
||||
hal_err_t hal_i2c_init(const hal_i2c_cfg_t *cfg)
|
||||
{
|
||||
(void)cfg; /* sim 不关心引脚 —— 但速率要告诉占用核算,它按频率折算 µs */
|
||||
hal_i2c_acct_set_freq(cfg != NULL ? cfg->freq_hz : 0);
|
||||
s_inited = true;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* 取器件并把「这次事务要不要 NAK」判掉。 */
|
||||
static hal_err_t take(uint8_t addr, sim_i2c_dev_t **out)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
if (addr > 0x7f) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
sim_i2c_dev_t *d = sim_i2c_dev(addr);
|
||||
if (d == NULL || !d->present) {
|
||||
return HAL_ERR_NAK;
|
||||
}
|
||||
if (d->nak_left != 0) {
|
||||
if (d->nak_left > 0) {
|
||||
d->nak_left--;
|
||||
}
|
||||
return HAL_ERR_NAK;
|
||||
}
|
||||
*out = d;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* data[0] 是寄存器指针,其后按 reg_width 一格一格写,指针自增(uint8_t 自然回绕)。 */
|
||||
static hal_err_t do_write(sim_i2c_dev_t *d, const uint8_t *data, size_t len)
|
||||
{
|
||||
if (len != 0 && data == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if (len > 1 && ((len - 1) % d->reg_width) != 0) {
|
||||
return HAL_ERR_ARG; /* 半个寄存器:真器件上是未定义行为,这里直接报错 */
|
||||
}
|
||||
d->n_write++;
|
||||
d->last_write_len = (uint8_t)(len < SIM_I2C_LOG_BYTES ? len : SIM_I2C_LOG_BYTES);
|
||||
if (d->last_write_len != 0) {
|
||||
memcpy(d->last_write, data, d->last_write_len);
|
||||
}
|
||||
if (len == 0) {
|
||||
return HAL_OK; /* 只发地址 = 探测 */
|
||||
}
|
||||
d->ptr = data[0];
|
||||
for (size_t i = 1; i < len; i += d->reg_width) {
|
||||
uint16_t v = data[i];
|
||||
if (d->reg_width == 2) {
|
||||
v = (uint16_t)((v << 8) | data[i + 1]);
|
||||
}
|
||||
d->regs[d->ptr++] = v;
|
||||
}
|
||||
if (d->hook != NULL) {
|
||||
d->hook(d, SIM_I2C_EV_POST_WRITE);
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
static hal_err_t do_read(sim_i2c_dev_t *d, uint8_t *data, size_t len)
|
||||
{
|
||||
if (len != 0 && data == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
if ((len % d->reg_width) != 0) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
d->n_read++;
|
||||
if (d->hook != NULL) {
|
||||
d->hook(d, SIM_I2C_EV_PRE_READ); /* 按 datasheet 建模的器件在这里合成输入寄存器 */
|
||||
}
|
||||
for (size_t i = 0; i < len; i += d->reg_width) {
|
||||
uint16_t v = d->regs[d->ptr++];
|
||||
if (d->reg_width == 2) {
|
||||
data[i] = (uint8_t)(v >> 8);
|
||||
data[i + 1] = (uint8_t)(v & 0xff);
|
||||
} else {
|
||||
data[i] = (uint8_t)v;
|
||||
}
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write(uint8_t addr, const uint8_t *data, size_t len)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
spend(len, 0);
|
||||
hal_err_t err = take(addr, &d);
|
||||
return err != HAL_OK ? err : do_write(d, data, len);
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_read(uint8_t addr, uint8_t *data, size_t len)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
spend(0, len);
|
||||
hal_err_t err = take(addr, &d);
|
||||
return err != HAL_OK ? err : do_read(d, data, len);
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_write_read(uint8_t addr, const uint8_t *wr, size_t wr_len,
|
||||
uint8_t *rd, size_t rd_len)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
spend(wr_len, rd_len);
|
||||
hal_err_t err = take(addr, &d);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
err = do_write(d, wr, wr_len);
|
||||
return err != HAL_OK ? err : do_read(d, rd, rd_len);
|
||||
}
|
||||
|
||||
hal_err_t hal_i2c_probe(uint8_t addr)
|
||||
{
|
||||
sim_i2c_dev_t *d = NULL;
|
||||
spend(0, 0);
|
||||
return take(addr, &d);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/* 两把互斥锁的假实现。宿主机测试是**单线程**的,所以这里模拟的不是并发本身,
|
||||
而是「加锁纪律」—— 谁拿了、放没放、有没有嵌套、该拿的时候拿了没有。
|
||||
|
||||
⚠️ 单线程 sim 里「等一把别人持有的锁」永远等不到,所以:
|
||||
· 有限超时 → 把虚拟时钟往前拨满 timeout_ms,计一次超时,返回 HAL_ERR_TIMEOUT
|
||||
(这正是真板上等不到锁时该发生的事,调用方的处理路径因此测得到);
|
||||
· HAL_LOCK_FOREVER 且被别人持有 → 直接 abort。那在真板上是死锁,
|
||||
在这里悄悄返回一个错误码只会把它藏起来。
|
||||
|
||||
「谁」是一个假任务号(`sim_lock_set_task()`),默认 0。测试用它扮演第二个任务。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "hal_lock.h"
|
||||
#include "sim_hal.h"
|
||||
|
||||
#define NO_OWNER (-1)
|
||||
|
||||
static int s_owner[HAL_LOCK_COUNT];
|
||||
static uint32_t s_takes[HAL_LOCK_COUNT];
|
||||
static uint32_t s_timeouts[HAL_LOCK_COUNT];
|
||||
static uint32_t s_violations[HAL_LOCK_COUNT];
|
||||
static bool s_require[HAL_LOCK_COUNT];
|
||||
static int s_task;
|
||||
static bool s_inited;
|
||||
|
||||
void sim_lock_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
void sim_lock_reset_internal(void)
|
||||
{
|
||||
for (int i = 0; i < HAL_LOCK_COUNT; i++) {
|
||||
s_owner[i] = NO_OWNER;
|
||||
}
|
||||
memset(s_takes, 0, sizeof(s_takes));
|
||||
memset(s_timeouts, 0, sizeof(s_timeouts));
|
||||
memset(s_violations, 0, sizeof(s_violations));
|
||||
memset(s_require, 0, sizeof(s_require));
|
||||
s_task = 0;
|
||||
s_inited = false;
|
||||
}
|
||||
|
||||
static bool valid(hal_lock_id_t id) { return id >= 0 && id < HAL_LOCK_COUNT; }
|
||||
|
||||
hal_err_t hal_lock_init(void)
|
||||
{
|
||||
s_inited = true;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
bool hal_lock_held(hal_lock_id_t id)
|
||||
{
|
||||
return valid(id) && s_owner[id] == s_task;
|
||||
}
|
||||
|
||||
hal_err_t hal_lock_take(hal_lock_id_t id, uint32_t timeout_ms)
|
||||
{
|
||||
if (!valid(id) || !s_inited) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
/* 已经持有 = 重复加锁(这两把都不是递归锁);持有另一把 = 嵌套,见 hal_lock.h。 */
|
||||
for (int i = 0; i < HAL_LOCK_COUNT; i++) {
|
||||
if (s_owner[i] == s_task) {
|
||||
return HAL_ERR_STATE;
|
||||
}
|
||||
}
|
||||
if (s_owner[id] != NO_OWNER) {
|
||||
assert(timeout_ms != HAL_LOCK_FOREVER); /* 单线程里死等 = 死锁 */
|
||||
s_timeouts[id]++;
|
||||
hal_time_delay_ms(timeout_ms); /* 真板上这段时间是等掉的,虚拟时钟也要花掉 */
|
||||
return HAL_ERR_TIMEOUT;
|
||||
}
|
||||
s_owner[id] = s_task;
|
||||
s_takes[id]++;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
void hal_lock_give(hal_lock_id_t id)
|
||||
{
|
||||
assert(valid(id));
|
||||
assert(s_owner[id] == s_task); /* 不是自己拿的 / 根本没人拿 */
|
||||
s_owner[id] = NO_OWNER;
|
||||
}
|
||||
|
||||
uint32_t hal_lock_timeouts(hal_lock_id_t id) { return valid(id) ? s_timeouts[id] : 0; }
|
||||
|
||||
/* ---- 假板子这一侧的控制面 ------------------------------------------------ */
|
||||
|
||||
void sim_lock_set_task(int task) { s_task = task; }
|
||||
int sim_lock_task(void) { return s_task; }
|
||||
int sim_lock_owner(hal_lock_id_t id) { return valid(id) ? s_owner[id] : NO_OWNER; }
|
||||
uint32_t sim_lock_takes(hal_lock_id_t id) { return valid(id) ? s_takes[id] : 0; }
|
||||
|
||||
void sim_lock_require(hal_lock_id_t id, bool on)
|
||||
{
|
||||
if (valid(id)) {
|
||||
s_require[id] = on;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t sim_lock_violations(hal_lock_id_t id) { return valid(id) ? s_violations[id] : 0; }
|
||||
|
||||
void sim_lock_check(hal_lock_id_t id)
|
||||
{
|
||||
if (valid(id) && s_require[id] && !hal_lock_held(id)) {
|
||||
s_violations[id]++;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
/* sim 的总闸:一次把四路都恢复出厂。每个测试的 setUp() 调它。
|
||||
各路的清理函数是内部符号,不进 sim_hal.h —— 测试没有理由单独重置某一路。 */
|
||||
#include "sim_hal.h"
|
||||
|
||||
void sim_i2c_reset_internal(void);
|
||||
void sim_gpio_reset_internal(void);
|
||||
void sim_sr595_reset_internal(void);
|
||||
void sim_pca9555_reset_internal(void);
|
||||
void sim_pwm_reset_internal(void);
|
||||
void sim_time_reset_internal(void);
|
||||
|
||||
void sim_hal_reset(void)
|
||||
{
|
||||
sim_i2c_reset_internal();
|
||||
sim_gpio_reset_internal();
|
||||
sim_sr595_reset_internal();
|
||||
sim_pca9555_reset_internal();
|
||||
sim_pwm_reset_internal();
|
||||
sim_time_reset_internal();
|
||||
}
|
||||
/* sim 的总闸:一次把四路都恢复出厂。每个测试的 setUp() 调它。
|
||||
各路的清理函数是内部符号,不进 sim_hal.h —— 测试没有理由单独重置某一路。 */
|
||||
#include "hal_i2c_acct.h"
|
||||
#include "sim_hal.h"
|
||||
|
||||
void sim_i2c_reset_internal(void);
|
||||
void sim_gpio_reset_internal(void);
|
||||
void sim_sr595_reset_internal(void);
|
||||
void sim_pca9555_reset_internal(void);
|
||||
void sim_pwm_reset_internal(void);
|
||||
void sim_time_reset_internal(void);
|
||||
void sim_lock_reset_internal(void);
|
||||
|
||||
void sim_hal_reset(void)
|
||||
{
|
||||
sim_i2c_reset_internal();
|
||||
sim_gpio_reset_internal();
|
||||
sim_sr595_reset_internal();
|
||||
sim_pca9555_reset_internal();
|
||||
sim_pwm_reset_internal();
|
||||
sim_time_reset_internal();
|
||||
sim_lock_reset_internal();
|
||||
hal_i2c_acct_reset();
|
||||
}
|
||||
|
||||
@@ -1,156 +1,186 @@
|
||||
/* 假板子的控制面 —— 只有宿主机测试与 sim/ 自己会包含它,应用代码一律不许包含。
|
||||
(包含了就编不过:ESP 目标根本不编译 sim/。)
|
||||
|
||||
⚠️ 模拟器是用来证明**代码逻辑**对的,不是用来证明**设计**对的。
|
||||
电压、余量、时序裕量的结论全部来自设计文档的计算与首板实测。见计划 §4.2。 */
|
||||
#ifndef SIM_HAL_H
|
||||
#define SIM_HAL_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_gpio.h"
|
||||
#include "hal_i2c.h"
|
||||
#include "hal_pwm.h"
|
||||
#include "hal_time.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 四路 sim 全部恢复出厂:器件表清空、引脚未配置、PWM 未配置、时钟归零、
|
||||
init 标志清掉。每个测试的 setUp() 里调它。 */
|
||||
void sim_hal_reset(void);
|
||||
|
||||
/* ---------------- I²C ---------------- */
|
||||
|
||||
#define SIM_I2C_MAX_DEV 16
|
||||
#define SIM_I2C_REGS 256
|
||||
#define SIM_I2C_LOG_BYTES 8
|
||||
|
||||
/* 假器件的窃听事件。通用寄存器表模拟不了「配成输出就对地短路」这种事,
|
||||
所以按 datasheet 建模的假器件(sim_pca9555)挂一个钩子进来。 */
|
||||
typedef enum {
|
||||
SIM_I2C_EV_POST_WRITE = 0, /* 一次写事务刚落进寄存器表 */
|
||||
SIM_I2C_EV_PRE_READ, /* 一次读事务即将取寄存器表 —— 在这里合成输入 */
|
||||
} sim_i2c_ev_t;
|
||||
|
||||
struct sim_i2c_dev;
|
||||
typedef void (*sim_i2c_hook_t)(struct sim_i2c_dev *d, sim_i2c_ev_t ev);
|
||||
|
||||
typedef struct sim_i2c_dev {
|
||||
uint8_t addr;
|
||||
uint8_t reg_width; /* 1 或 2 字节。2 字节按**大端**——INA226 / DAC63204 / LM75 都是 */
|
||||
bool present; /* false = 器件失联(A15 降级矩阵用),事务一律 NAK */
|
||||
int nak_left; /* >0:接下来 n 次事务 NAK 后自动恢复;<0:一直 NAK;0:正常 */
|
||||
uint16_t regs[SIM_I2C_REGS];
|
||||
uint8_t ptr; /* 器件内部的寄存器指针,跨事务保持(真器件就是这个行为) */
|
||||
uint32_t n_write; /* 事务计数,可用来断言「驱动真的访问过它 / 没多访问」 */
|
||||
uint32_t n_read;
|
||||
uint8_t last_write[SIM_I2C_LOG_BYTES]; /* 最后一次写进去的原始字节 */
|
||||
uint8_t last_write_len;
|
||||
sim_i2c_hook_t hook; /* NULL = 就是一张寄存器表 */
|
||||
} sim_i2c_dev_t;
|
||||
|
||||
/* 往假总线上挂一个器件。地址重复或超出 16 个会 abort(测试写错了,不该悄悄过)。 */
|
||||
sim_i2c_dev_t *sim_i2c_add(uint8_t addr, uint8_t reg_width);
|
||||
sim_i2c_dev_t *sim_i2c_dev(uint8_t addr); /* 没挂就返回 NULL */
|
||||
|
||||
/* 预置 / 回读寄存器。地址没挂过会 abort。 */
|
||||
void sim_i2c_set_reg(uint8_t addr, uint8_t reg, uint16_t val);
|
||||
uint16_t sim_i2c_get_reg(uint8_t addr, uint8_t reg);
|
||||
|
||||
/* 故障注入。count>0:接下来 count 次事务 NAK;count<0:一直 NAK;0:清除。 */
|
||||
void sim_i2c_inject_nak(uint8_t addr, int count);
|
||||
void sim_i2c_set_present(uint8_t addr, bool present);
|
||||
|
||||
/* ---------------- GPIO ---------------- */
|
||||
|
||||
/* ESP32-S3 的 GPIO 编号 **0~48**,所以数组要 49 个 —— `EFUSE_EN2` 就在 IO48 上。
|
||||
A7 之前这里是 48,于是 CH2 的 eFuse 既开不了也**关不掉**(`hal_gpio_config()`
|
||||
直接 HAL_ERR_ARG)。边界写成「个数」而不是「上限」,就是这么错的。 */
|
||||
#define SIM_GPIO_PINS 49
|
||||
|
||||
/* 外部世界把某个**输入**脚拉成 level。对输出脚调用会 abort(那是总线打架)。 */
|
||||
void sim_gpio_drive(int pin, int level);
|
||||
|
||||
/* 该脚当前电平:输出脚 = 固件驱动的值,输入脚 = 外部驱动的值。没配过返回 -1。 */
|
||||
int sim_gpio_level(int pin);
|
||||
|
||||
/* 被 hal_gpio_write() 写成功过几次。断言「改 CH1 没动 CH2~5」用得上(A5)。 */
|
||||
uint32_t sim_gpio_writes(int pin);
|
||||
|
||||
/* 全局递增的写序号:每成功写一个脚 +1,本函数返回**该脚最后一次**被写时的序号
|
||||
(从没被写过返回 0)。用来断言顺序 —— 「先断 VBUS 再关 eFuse」(A7)、
|
||||
「17 ms 上电时序」(A9)这类事,光看每个脚的终值和次数是看不出来的。 */
|
||||
uint32_t sim_gpio_write_seq(int pin);
|
||||
|
||||
/* ---------------- 74HC595 ×3 的 24 位链 ---------------- */
|
||||
|
||||
/* 按 datasheet 建模的假移位链,**只看线上真实发生的边**(SRCLK / RCLK 上升沿)。
|
||||
对着驱动自己的影子变量断言等于自己证明自己 —— 字节顺序发反、每帧多打一个时钟、
|
||||
RCLK 打早了,影子变量一律看不出来。位号与 sr595.h 一致:0 = U21.QA,23 = U33.QH。
|
||||
|
||||
attach 之后移位寄存器与输出锁存器被塞成 0xA5A5A5:真板子上电时它们的内容
|
||||
**不确定**(`MR#` 接死在 +3V3,没有复位线),驱动必须先锁存安全值再使能 OE#。 */
|
||||
void sim_sr595_attach(int ser_pin, int srclk_pin, int rclk_pin, int oe_pin);
|
||||
|
||||
uint32_t sim_sr595_outputs(void); /* 输出锁存器(不管 OE# 是高是低) */
|
||||
uint32_t sim_sr595_shift(void); /* 移位寄存器当前内容(还没锁存的那一份) */
|
||||
uint32_t sim_sr595_clocks(void); /* SRCLK 上升沿总数:一帧应当正好 +24 */
|
||||
uint32_t sim_sr595_latches(void); /* RCLK 上升沿总数:一帧应当正好 +1 */
|
||||
int sim_sr595_oe(void); /* OE# 当前电平。初值 1 —— 板上 R609 拉着 */
|
||||
|
||||
/* OE# 第一次被拉低那一刻的现场。断言「输出使能之前已经锁存过安全值」用:
|
||||
驱动要是把顺序换了,enable_latches 会是 0、enable_outputs 会是那坨 0xA5A5A5。 */
|
||||
bool sim_sr595_enabled(void);
|
||||
uint32_t sim_sr595_enable_latches(void);
|
||||
uint32_t sim_sr595_enable_outputs(void);
|
||||
|
||||
/* 某一位在栅极上的实际电平;OE# 为高时返回 -1(高阻,不是 0)。 */
|
||||
int sim_sr595_pin(int bit);
|
||||
|
||||
/* ---------------- PCA9555(按 datasheet 建模的假器件) ---------------- */
|
||||
|
||||
/* 通用寄存器表证明不了 A6 的核心判据。这个模型只多做三件真器件会做的事:
|
||||
① **Output Port 复位值 0xFF** —— 所以把一个脚配成输出,它下一刻就输出高;
|
||||
② 输入寄存器按位合成:配成输入的位读**外部电平**,配成输出的位读**自己的输出**;
|
||||
③ 四个备用脚 P0_7 / P1_5~7 **焊死在地上** —— 读永远是 0,被驱动成高就是短路。
|
||||
③ 是这块板子上「配错方向」唯一会留下痕迹的地方:真板子上它既不报错也不 NAK。 */
|
||||
void sim_pca9555_attach(uint8_t addr);
|
||||
|
||||
/* 外部世界把某个端口的 8 根线驱动成 level(接地脚不受影响,恒 0)。
|
||||
FLT#/PGOOD 都是开漏上拉,所以「什么都没发生」的静息值是 0xFF。 */
|
||||
void sim_pca9555_drive(int port, uint8_t level);
|
||||
|
||||
uint8_t sim_pca9555_config(int port); /* 方向寄存器现值,1 = 输入 */
|
||||
uint8_t sim_pca9555_output(int port); /* 输出锁存器现值(复位 0xFF) */
|
||||
uint8_t sim_pca9555_polinv(int port);
|
||||
|
||||
/* 接地脚被配成输出过几次(不管输出高低,配了就算 —— 那已经是在对地灌电流)。 */
|
||||
uint32_t sim_pca9555_gnd_driven(void);
|
||||
/* 其中真的输出了高电平、也就是**直接对地短路**过几次。 */
|
||||
uint32_t sim_pca9555_shorted(void);
|
||||
|
||||
/* INT# 现在是不是被拉低(低有效)。输入变化拉低,**读走输入端口才释放**。 */
|
||||
bool sim_pca9555_int_asserted(void);
|
||||
uint32_t sim_pca9555_int_asserts(void); /* 累计拉低次数 */
|
||||
|
||||
/* ---------------- PWM ---------------- */
|
||||
|
||||
int sim_pwm_pin(int ch); /* 未配置返回 -1 */
|
||||
uint32_t sim_pwm_freq(int ch); /* 未配置返回 0 */
|
||||
int sim_pwm_permille(int ch); /* 未配置返回 -1 */
|
||||
|
||||
/* ---------------- 时钟 ---------------- */
|
||||
|
||||
void sim_time_set_us(uint64_t us);
|
||||
void sim_time_advance_us(uint64_t us);
|
||||
void sim_time_advance_ms(uint32_t ms);
|
||||
uint32_t sim_time_delay_calls(void); /* hal_time_delay_ms() 被调了几次 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SIM_HAL_H */
|
||||
/* 假板子的控制面 —— 只有宿主机测试与 sim/ 自己会包含它,应用代码一律不许包含。
|
||||
(包含了就编不过:ESP 目标根本不编译 sim/。)
|
||||
|
||||
⚠️ 模拟器是用来证明**代码逻辑**对的,不是用来证明**设计**对的。
|
||||
电压、余量、时序裕量的结论全部来自设计文档的计算与首板实测。见计划 §4.2。 */
|
||||
#ifndef SIM_HAL_H
|
||||
#define SIM_HAL_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_gpio.h"
|
||||
#include "hal_i2c.h"
|
||||
#include "hal_i2c_acct.h"
|
||||
#include "hal_lock.h"
|
||||
#include "hal_pwm.h"
|
||||
#include "hal_time.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 四路 sim 全部恢复出厂:器件表清空、引脚未配置、PWM 未配置、时钟归零、
|
||||
init 标志清掉。每个测试的 setUp() 里调它。 */
|
||||
void sim_hal_reset(void);
|
||||
|
||||
/* ---------------- I²C ---------------- */
|
||||
|
||||
#define SIM_I2C_MAX_DEV 16
|
||||
#define SIM_I2C_REGS 256
|
||||
#define SIM_I2C_LOG_BYTES 8
|
||||
|
||||
/* 假器件的窃听事件。通用寄存器表模拟不了「配成输出就对地短路」这种事,
|
||||
所以按 datasheet 建模的假器件(sim_pca9555)挂一个钩子进来。 */
|
||||
typedef enum {
|
||||
SIM_I2C_EV_POST_WRITE = 0, /* 一次写事务刚落进寄存器表 */
|
||||
SIM_I2C_EV_PRE_READ, /* 一次读事务即将取寄存器表 —— 在这里合成输入 */
|
||||
} sim_i2c_ev_t;
|
||||
|
||||
struct sim_i2c_dev;
|
||||
typedef void (*sim_i2c_hook_t)(struct sim_i2c_dev *d, sim_i2c_ev_t ev);
|
||||
|
||||
typedef struct sim_i2c_dev {
|
||||
uint8_t addr;
|
||||
uint8_t reg_width; /* 1 或 2 字节。2 字节按**大端**——INA226 / DAC63204 / LM75 都是 */
|
||||
bool present; /* false = 器件失联(A15 降级矩阵用),事务一律 NAK */
|
||||
int nak_left; /* >0:接下来 n 次事务 NAK 后自动恢复;<0:一直 NAK;0:正常 */
|
||||
uint16_t regs[SIM_I2C_REGS];
|
||||
uint8_t ptr; /* 器件内部的寄存器指针,跨事务保持(真器件就是这个行为) */
|
||||
uint32_t n_write; /* 事务计数,可用来断言「驱动真的访问过它 / 没多访问」 */
|
||||
uint32_t n_read;
|
||||
uint8_t last_write[SIM_I2C_LOG_BYTES]; /* 最后一次写进去的原始字节 */
|
||||
uint8_t last_write_len;
|
||||
sim_i2c_hook_t hook; /* NULL = 就是一张寄存器表 */
|
||||
} sim_i2c_dev_t;
|
||||
|
||||
/* 往假总线上挂一个器件。地址重复或超出 16 个会 abort(测试写错了,不该悄悄过)。 */
|
||||
sim_i2c_dev_t *sim_i2c_add(uint8_t addr, uint8_t reg_width);
|
||||
sim_i2c_dev_t *sim_i2c_dev(uint8_t addr); /* 没挂就返回 NULL */
|
||||
|
||||
/* 预置 / 回读寄存器。地址没挂过会 abort。 */
|
||||
void sim_i2c_set_reg(uint8_t addr, uint8_t reg, uint16_t val);
|
||||
uint16_t sim_i2c_get_reg(uint8_t addr, uint8_t reg);
|
||||
|
||||
/* 故障注入。count>0:接下来 count 次事务 NAK;count<0:一直 NAK;0:清除。 */
|
||||
void sim_i2c_inject_nak(uint8_t addr, int count);
|
||||
void sim_i2c_set_present(uint8_t addr, bool present);
|
||||
|
||||
/* **假总线上的事务是要花时间的**(默认开):每次事务把虚拟时钟往前拨
|
||||
`hal_i2c_txn_bits() / 频率` 那么多微秒。没有这一条,t_safety 的一个周期在 sim 里
|
||||
耗时为零,「20 ms 周期」的测试就只是在测「delay 传了 20」——
|
||||
而真正会出事的是「巡检自己就占掉了 1.5 ms、周期还准不准」。
|
||||
关掉它只有一个正当理由:某个测试要拿虚拟时钟当纯计数器用。 */
|
||||
void sim_i2c_set_bus_time(bool on);
|
||||
|
||||
/* ---------------- GPIO ---------------- */
|
||||
|
||||
/* ESP32-S3 的 GPIO 编号 **0~48**,所以数组要 49 个 —— `EFUSE_EN2` 就在 IO48 上。
|
||||
A7 之前这里是 48,于是 CH2 的 eFuse 既开不了也**关不掉**(`hal_gpio_config()`
|
||||
直接 HAL_ERR_ARG)。边界写成「个数」而不是「上限」,就是这么错的。 */
|
||||
#define SIM_GPIO_PINS 49
|
||||
|
||||
/* 外部世界把某个**输入**脚拉成 level。对输出脚调用会 abort(那是总线打架)。 */
|
||||
void sim_gpio_drive(int pin, int level);
|
||||
|
||||
/* 该脚当前电平:输出脚 = 固件驱动的值,输入脚 = 外部驱动的值。没配过返回 -1。 */
|
||||
int sim_gpio_level(int pin);
|
||||
|
||||
/* 被 hal_gpio_write() 写成功过几次。断言「改 CH1 没动 CH2~5」用得上(A5)。 */
|
||||
uint32_t sim_gpio_writes(int pin);
|
||||
|
||||
/* 全局递增的写序号:每成功写一个脚 +1,本函数返回**该脚最后一次**被写时的序号
|
||||
(从没被写过返回 0)。用来断言顺序 —— 「先断 VBUS 再关 eFuse」(A7)、
|
||||
「17 ms 上电时序」(A9)这类事,光看每个脚的终值和次数是看不出来的。 */
|
||||
uint32_t sim_gpio_write_seq(int pin);
|
||||
|
||||
/* ---------------- 74HC595 ×3 的 24 位链 ---------------- */
|
||||
|
||||
/* 按 datasheet 建模的假移位链,**只看线上真实发生的边**(SRCLK / RCLK 上升沿)。
|
||||
对着驱动自己的影子变量断言等于自己证明自己 —— 字节顺序发反、每帧多打一个时钟、
|
||||
RCLK 打早了,影子变量一律看不出来。位号与 sr595.h 一致:0 = U21.QA,23 = U33.QH。
|
||||
|
||||
attach 之后移位寄存器与输出锁存器被塞成 0xA5A5A5:真板子上电时它们的内容
|
||||
**不确定**(`MR#` 接死在 +3V3,没有复位线),驱动必须先锁存安全值再使能 OE#。 */
|
||||
void sim_sr595_attach(int ser_pin, int srclk_pin, int rclk_pin, int oe_pin);
|
||||
|
||||
uint32_t sim_sr595_outputs(void); /* 输出锁存器(不管 OE# 是高是低) */
|
||||
uint32_t sim_sr595_shift(void); /* 移位寄存器当前内容(还没锁存的那一份) */
|
||||
uint32_t sim_sr595_clocks(void); /* SRCLK 上升沿总数:一帧应当正好 +24 */
|
||||
uint32_t sim_sr595_latches(void); /* RCLK 上升沿总数:一帧应当正好 +1 */
|
||||
int sim_sr595_oe(void); /* OE# 当前电平。初值 1 —— 板上 R609 拉着 */
|
||||
|
||||
/* OE# 第一次被拉低那一刻的现场。断言「输出使能之前已经锁存过安全值」用:
|
||||
驱动要是把顺序换了,enable_latches 会是 0、enable_outputs 会是那坨 0xA5A5A5。 */
|
||||
bool sim_sr595_enabled(void);
|
||||
uint32_t sim_sr595_enable_latches(void);
|
||||
uint32_t sim_sr595_enable_outputs(void);
|
||||
|
||||
/* 某一位在栅极上的实际电平;OE# 为高时返回 -1(高阻,不是 0)。 */
|
||||
int sim_sr595_pin(int bit);
|
||||
|
||||
/* ---------------- PCA9555(按 datasheet 建模的假器件) ---------------- */
|
||||
|
||||
/* 通用寄存器表证明不了 A6 的核心判据。这个模型只多做三件真器件会做的事:
|
||||
① **Output Port 复位值 0xFF** —— 所以把一个脚配成输出,它下一刻就输出高;
|
||||
② 输入寄存器按位合成:配成输入的位读**外部电平**,配成输出的位读**自己的输出**;
|
||||
③ 四个备用脚 P0_7 / P1_5~7 **焊死在地上** —— 读永远是 0,被驱动成高就是短路。
|
||||
③ 是这块板子上「配错方向」唯一会留下痕迹的地方:真板子上它既不报错也不 NAK。 */
|
||||
void sim_pca9555_attach(uint8_t addr);
|
||||
|
||||
/* 外部世界把某个端口的 8 根线驱动成 level(接地脚不受影响,恒 0)。
|
||||
FLT#/PGOOD 都是开漏上拉,所以「什么都没发生」的静息值是 0xFF。 */
|
||||
void sim_pca9555_drive(int port, uint8_t level);
|
||||
|
||||
uint8_t sim_pca9555_config(int port); /* 方向寄存器现值,1 = 输入 */
|
||||
uint8_t sim_pca9555_output(int port); /* 输出锁存器现值(复位 0xFF) */
|
||||
uint8_t sim_pca9555_polinv(int port);
|
||||
|
||||
/* 接地脚被配成输出过几次(不管输出高低,配了就算 —— 那已经是在对地灌电流)。 */
|
||||
uint32_t sim_pca9555_gnd_driven(void);
|
||||
/* 其中真的输出了高电平、也就是**直接对地短路**过几次。 */
|
||||
uint32_t sim_pca9555_shorted(void);
|
||||
|
||||
/* INT# 现在是不是被拉低(低有效)。输入变化拉低,**读走输入端口才释放**。 */
|
||||
bool sim_pca9555_int_asserted(void);
|
||||
uint32_t sim_pca9555_int_asserts(void); /* 累计拉低次数 */
|
||||
|
||||
/* ---------------- PWM ---------------- */
|
||||
|
||||
int sim_pwm_pin(int ch); /* 未配置返回 -1 */
|
||||
uint32_t sim_pwm_freq(int ch); /* 未配置返回 0 */
|
||||
int sim_pwm_permille(int ch); /* 未配置返回 -1 */
|
||||
|
||||
/* ---------------- 两把互斥锁 ---------------- */
|
||||
|
||||
/* 扮演第几号任务(默认 0)。单线程 sim 里「另一个任务持着锁」就靠它模拟。 */
|
||||
void sim_lock_set_task(int task);
|
||||
int sim_lock_task(void);
|
||||
|
||||
/* 现在被几号任务持着;没人持着返回 -1。 */
|
||||
int sim_lock_owner(hal_lock_id_t id);
|
||||
uint32_t sim_lock_takes(hal_lock_id_t id);
|
||||
|
||||
/* **强制加锁纪律**:打开之后,动这个共享资源(I²C 的任何事务 / 595 那四根线)
|
||||
却没持着对应的锁,就记一次违规。默认关 —— A3~A6 的驱动测试是直接调驱动的,
|
||||
它们不该被要求先拿锁。
|
||||
|
||||
⚠️ 违规只计数、不 abort:这条检查的用处是**证明上层的加锁路径是通的**
|
||||
(t_safety 跑一圈违规为 0),以及证明这条检查本身不是句空话
|
||||
(不拿锁直接读一片 INA226,违规必须 +1)。 */
|
||||
void sim_lock_require(hal_lock_id_t id, bool on);
|
||||
uint32_t sim_lock_violations(hal_lock_id_t id);
|
||||
void sim_lock_check(hal_lock_id_t id); /* 给 sim 各路自己调,测试一般用不到 */
|
||||
|
||||
/* ---------------- 时钟 ---------------- */
|
||||
|
||||
void sim_time_set_us(uint64_t us);
|
||||
void sim_time_advance_us(uint64_t us);
|
||||
void sim_time_advance_ms(uint32_t ms);
|
||||
uint32_t sim_time_delay_calls(void); /* hal_time_delay_ms() 被调了几次 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SIM_HAL_H */
|
||||
|
||||
@@ -1,98 +1,105 @@
|
||||
/* 假的 74HC595 ×3 移位链 —— 挂在 GPIO sim 上,只看**线上真实发生的边**。
|
||||
|
||||
为什么要这么一个东西:A5 的两条验收断言(改一路不动别路、字节顺序 U33→U22→U21)
|
||||
如果只对着驱动自己的影子变量断言,那是**自己证明自己** —— 字节顺序发反了、
|
||||
每帧多打一个时钟、RCLK 在数据发完之前就打了,影子变量一律看不出来。
|
||||
所以这里按 datasheet 建模:SRCLK 上升沿移一格,RCLK 上升沿把移位寄存器抄进输出锁存器。
|
||||
|
||||
24 级的编号与 sr595.h 一致:0 = U21.QA(DS 进来的第一级),23 = U33.QH(链尾)。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sim_hal.h"
|
||||
|
||||
typedef struct {
|
||||
bool attached;
|
||||
int ser_pin, srclk_pin, rclk_pin, oe_pin;
|
||||
int ser, srclk, rclk, oe; /* 当前电平;oe 初值 1 = 板上 R609 拉着 */
|
||||
uint32_t shift; /* 移位寄存器(未锁存) */
|
||||
uint32_t out; /* 输出锁存器 */
|
||||
uint32_t clocks; /* SRCLK 上升沿总数 */
|
||||
uint32_t latches; /* RCLK 上升沿总数 */
|
||||
bool enabled_once;
|
||||
uint32_t enable_latches; /* OE# 第一次被拉低时,已经锁存过几次 */
|
||||
uint32_t enable_out; /* OE# 第一次被拉低那一刻,输出锁存器里是什么 */
|
||||
} chain_t;
|
||||
|
||||
static chain_t s_ch;
|
||||
|
||||
void sim_sr595_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
void sim_sr595_gpio_hook(int pin, int level);
|
||||
|
||||
void sim_sr595_reset_internal(void) { memset(&s_ch, 0, sizeof(s_ch)); }
|
||||
|
||||
void sim_sr595_attach(int ser_pin, int srclk_pin, int rclk_pin, int oe_pin)
|
||||
{
|
||||
memset(&s_ch, 0, sizeof(s_ch));
|
||||
s_ch.attached = true;
|
||||
s_ch.ser_pin = ser_pin;
|
||||
s_ch.srclk_pin = srclk_pin;
|
||||
s_ch.rclk_pin = rclk_pin;
|
||||
s_ch.oe_pin = oe_pin;
|
||||
/* `OE#` 上电就被 R609 拉高 —— 高阻,不是 0。 */
|
||||
s_ch.oe = 1;
|
||||
/* 上电后移位寄存器与输出锁存器的内容**不确定**(`MR#` 接死在 +3V3,没有复位线)。
|
||||
这里塞一个显眼的非零值:驱动要是先使能 OE# 再锁存安全值,
|
||||
`sim_sr595_enable_outputs()` 就会把这坨脏东西原样报出来。 */
|
||||
s_ch.shift = 0x00A5A5A5u & 0x00FFFFFFu;
|
||||
s_ch.out = s_ch.shift;
|
||||
}
|
||||
|
||||
void sim_sr595_gpio_hook(int pin, int level)
|
||||
{
|
||||
if (!s_ch.attached) {
|
||||
return;
|
||||
}
|
||||
level = level ? 1 : 0;
|
||||
|
||||
if (pin == s_ch.ser_pin) {
|
||||
s_ch.ser = level;
|
||||
} else if (pin == s_ch.srclk_pin) {
|
||||
if (level == 1 && s_ch.srclk == 0) { /* 上升沿:整条链往编号大的方向挪一格 */
|
||||
s_ch.shift = ((s_ch.shift << 1) | (uint32_t)s_ch.ser) & 0x00FFFFFFu;
|
||||
s_ch.clocks++;
|
||||
}
|
||||
s_ch.srclk = level;
|
||||
} else if (pin == s_ch.rclk_pin) {
|
||||
if (level == 1 && s_ch.rclk == 0) {
|
||||
s_ch.out = s_ch.shift;
|
||||
s_ch.latches++;
|
||||
}
|
||||
s_ch.rclk = level;
|
||||
} else if (pin == s_ch.oe_pin) {
|
||||
if (level == 0 && s_ch.oe != 0 && !s_ch.enabled_once) {
|
||||
s_ch.enabled_once = true;
|
||||
s_ch.enable_latches = s_ch.latches;
|
||||
s_ch.enable_out = s_ch.out;
|
||||
}
|
||||
s_ch.oe = level;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t sim_sr595_outputs(void) { return s_ch.out; }
|
||||
uint32_t sim_sr595_shift(void) { return s_ch.shift; }
|
||||
uint32_t sim_sr595_clocks(void) { return s_ch.clocks; }
|
||||
uint32_t sim_sr595_latches(void) { return s_ch.latches; }
|
||||
int sim_sr595_oe(void) { return s_ch.oe; }
|
||||
bool sim_sr595_enabled(void) { return s_ch.enabled_once; }
|
||||
uint32_t sim_sr595_enable_latches(void) { return s_ch.enable_latches; }
|
||||
uint32_t sim_sr595_enable_outputs(void) { return s_ch.enable_out; }
|
||||
|
||||
int sim_sr595_pin(int bit)
|
||||
{
|
||||
assert(bit >= 0 && bit < 24);
|
||||
if (s_ch.oe) {
|
||||
return -1; /* 高阻。栅极侧靠自己的 100 kΩ 下拉,HUB_RST 侧靠 R716 上拉 */
|
||||
}
|
||||
return (int)((s_ch.out >> bit) & 1u);
|
||||
}
|
||||
/* 假的 74HC595 ×3 移位链 —— 挂在 GPIO sim 上,只看**线上真实发生的边**。
|
||||
|
||||
为什么要这么一个东西:A5 的两条验收断言(改一路不动别路、字节顺序 U33→U22→U21)
|
||||
如果只对着驱动自己的影子变量断言,那是**自己证明自己** —— 字节顺序发反了、
|
||||
每帧多打一个时钟、RCLK 在数据发完之前就打了,影子变量一律看不出来。
|
||||
所以这里按 datasheet 建模:SRCLK 上升沿移一格,RCLK 上升沿把移位寄存器抄进输出锁存器。
|
||||
|
||||
24 级的编号与 sr595.h 一致:0 = U21.QA(DS 进来的第一级),23 = U33.QH(链尾)。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sim_hal.h"
|
||||
|
||||
typedef struct {
|
||||
bool attached;
|
||||
int ser_pin, srclk_pin, rclk_pin, oe_pin;
|
||||
int ser, srclk, rclk, oe; /* 当前电平;oe 初值 1 = 板上 R609 拉着 */
|
||||
uint32_t shift; /* 移位寄存器(未锁存) */
|
||||
uint32_t out; /* 输出锁存器 */
|
||||
uint32_t clocks; /* SRCLK 上升沿总数 */
|
||||
uint32_t latches; /* RCLK 上升沿总数 */
|
||||
bool enabled_once;
|
||||
uint32_t enable_latches; /* OE# 第一次被拉低时,已经锁存过几次 */
|
||||
uint32_t enable_out; /* OE# 第一次被拉低那一刻,输出锁存器里是什么 */
|
||||
} chain_t;
|
||||
|
||||
static chain_t s_ch;
|
||||
|
||||
void sim_sr595_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
void sim_sr595_gpio_hook(int pin, int level);
|
||||
|
||||
void sim_sr595_reset_internal(void) { memset(&s_ch, 0, sizeof(s_ch)); }
|
||||
|
||||
void sim_sr595_attach(int ser_pin, int srclk_pin, int rclk_pin, int oe_pin)
|
||||
{
|
||||
memset(&s_ch, 0, sizeof(s_ch));
|
||||
s_ch.attached = true;
|
||||
s_ch.ser_pin = ser_pin;
|
||||
s_ch.srclk_pin = srclk_pin;
|
||||
s_ch.rclk_pin = rclk_pin;
|
||||
s_ch.oe_pin = oe_pin;
|
||||
/* `OE#` 上电就被 R609 拉高 —— 高阻,不是 0。 */
|
||||
s_ch.oe = 1;
|
||||
/* 上电后移位寄存器与输出锁存器的内容**不确定**(`MR#` 接死在 +3V3,没有复位线)。
|
||||
这里塞一个显眼的非零值:驱动要是先使能 OE# 再锁存安全值,
|
||||
`sim_sr595_enable_outputs()` 就会把这坨脏东西原样报出来。 */
|
||||
s_ch.shift = 0x00A5A5A5u & 0x00FFFFFFu;
|
||||
s_ch.out = s_ch.shift;
|
||||
}
|
||||
|
||||
void sim_sr595_gpio_hook(int pin, int level)
|
||||
{
|
||||
if (!s_ch.attached) {
|
||||
return;
|
||||
}
|
||||
level = level ? 1 : 0;
|
||||
|
||||
/* 动这条链的四根线之一 = 在用那个共享资源,此刻必须持着 595 那把锁
|
||||
(只在 sim_lock_require(HAL_LOCK_SR595, true) 之后才计数,见 hal_lock_sim.c)。 */
|
||||
if (pin == s_ch.ser_pin || pin == s_ch.srclk_pin || pin == s_ch.rclk_pin ||
|
||||
pin == s_ch.oe_pin) {
|
||||
sim_lock_check(HAL_LOCK_SR595);
|
||||
}
|
||||
|
||||
if (pin == s_ch.ser_pin) {
|
||||
s_ch.ser = level;
|
||||
} else if (pin == s_ch.srclk_pin) {
|
||||
if (level == 1 && s_ch.srclk == 0) { /* 上升沿:整条链往编号大的方向挪一格 */
|
||||
s_ch.shift = ((s_ch.shift << 1) | (uint32_t)s_ch.ser) & 0x00FFFFFFu;
|
||||
s_ch.clocks++;
|
||||
}
|
||||
s_ch.srclk = level;
|
||||
} else if (pin == s_ch.rclk_pin) {
|
||||
if (level == 1 && s_ch.rclk == 0) {
|
||||
s_ch.out = s_ch.shift;
|
||||
s_ch.latches++;
|
||||
}
|
||||
s_ch.rclk = level;
|
||||
} else if (pin == s_ch.oe_pin) {
|
||||
if (level == 0 && s_ch.oe != 0 && !s_ch.enabled_once) {
|
||||
s_ch.enabled_once = true;
|
||||
s_ch.enable_latches = s_ch.latches;
|
||||
s_ch.enable_out = s_ch.out;
|
||||
}
|
||||
s_ch.oe = level;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t sim_sr595_outputs(void) { return s_ch.out; }
|
||||
uint32_t sim_sr595_shift(void) { return s_ch.shift; }
|
||||
uint32_t sim_sr595_clocks(void) { return s_ch.clocks; }
|
||||
uint32_t sim_sr595_latches(void) { return s_ch.latches; }
|
||||
int sim_sr595_oe(void) { return s_ch.oe; }
|
||||
bool sim_sr595_enabled(void) { return s_ch.enabled_once; }
|
||||
uint32_t sim_sr595_enable_latches(void) { return s_ch.enable_latches; }
|
||||
uint32_t sim_sr595_enable_outputs(void) { return s_ch.enable_out; }
|
||||
|
||||
int sim_sr595_pin(int bit)
|
||||
{
|
||||
assert(bit >= 0 && bit < 24);
|
||||
if (s_ch.oe) {
|
||||
return -1; /* 高阻。栅极侧靠自己的 100 kΩ 下拉,HUB_RST 侧靠 R716 上拉 */
|
||||
}
|
||||
return (int)((s_ch.out >> bit) & 1u);
|
||||
}
|
||||
|
||||
+80
-75
@@ -1,75 +1,80 @@
|
||||
# PhoneRail 宿主机构建 —— 不依赖 ESP-IDF,用来跑单元测试(CTest)。
|
||||
#
|
||||
# bash host/test.sh 配置 + 编译 + ctest
|
||||
#
|
||||
# 与 IDF 目标共享的源码只有 ../main 里的与硬件无关部分。
|
||||
# 新增一个既要上板又要被测的 .c,必须同时加进:
|
||||
# ../main/CMakeLists.txt 的 SRCS 和 下面的 PHONERAIL_CORE_SRCS
|
||||
# HAL 里新增一路实现则要同时加进:
|
||||
# ../components/phonerail_hal/CMakeLists.txt 的 SRCS(esp 那份)
|
||||
# 和下面的 PHONERAIL_HAL_SIM_SRCS(sim 那份)
|
||||
# 新增一个器件驱动则是:
|
||||
# ../components/drv/CMakeLists.txt 的 SRCS 和 下面的 PHONERAIL_DRV_SRCS
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(phonerail_host C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
add_compile_options(-Wall -Wextra -Werror)
|
||||
|
||||
set(FW_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..)
|
||||
|
||||
# ---- 被测代码 -------------------------------------------------------------
|
||||
set(PHONERAIL_CORE_SRCS
|
||||
${FW_ROOT}/main/version.c
|
||||
${FW_ROOT}/main/rail.c
|
||||
${FW_ROOT}/main/rail_cfg.c
|
||||
)
|
||||
|
||||
# HAL:接口在 include/,宿主机链 sim/ 的实现(esp/ 那份只有 IDF 目标编)。
|
||||
set(PHONERAIL_HAL_DIR ${FW_ROOT}/components/phonerail_hal)
|
||||
set(PHONERAIL_HAL_SIM_SRCS
|
||||
${PHONERAIL_HAL_DIR}/hal_err.c
|
||||
${PHONERAIL_HAL_DIR}/sim/sim_hal.c
|
||||
${PHONERAIL_HAL_DIR}/sim/hal_i2c_sim.c
|
||||
${PHONERAIL_HAL_DIR}/sim/hal_gpio_sim.c
|
||||
${PHONERAIL_HAL_DIR}/sim/sim_sr595.c
|
||||
${PHONERAIL_HAL_DIR}/sim/sim_pca9555.c
|
||||
${PHONERAIL_HAL_DIR}/sim/hal_pwm_sim.c
|
||||
${PHONERAIL_HAL_DIR}/sim/hal_time_sim.c
|
||||
)
|
||||
|
||||
# 器件驱动:与平台无关(只经 hal_i2c_*),两个目标编的是同一份源码。
|
||||
set(PHONERAIL_DRV_DIR ${FW_ROOT}/components/drv)
|
||||
set(PHONERAIL_DRV_SRCS
|
||||
${PHONERAIL_DRV_DIR}/ina226.c
|
||||
${PHONERAIL_DRV_DIR}/dac63204.c
|
||||
${PHONERAIL_DRV_DIR}/sr595.c
|
||||
${PHONERAIL_DRV_DIR}/pca9555.c
|
||||
)
|
||||
|
||||
add_library(phonerail_core STATIC
|
||||
${PHONERAIL_CORE_SRCS} ${PHONERAIL_HAL_SIM_SRCS} ${PHONERAIL_DRV_SRCS})
|
||||
target_include_directories(phonerail_core PUBLIC
|
||||
${FW_ROOT}/main
|
||||
${PHONERAIL_HAL_DIR}/include
|
||||
${PHONERAIL_HAL_DIR}/sim
|
||||
${PHONERAIL_DRV_DIR}/include)
|
||||
|
||||
# ---- 测试框架 Unity(从 ESP-IDF v5.4 原样复制到 third_party/,与上板测试同一套)
|
||||
add_library(unity STATIC ${FW_ROOT}/third_party/unity/unity.c)
|
||||
target_include_directories(unity PUBLIC ${FW_ROOT}/third_party/unity)
|
||||
target_compile_definitions(unity PUBLIC UNITY_OUTPUT_COLOR)
|
||||
# 第三方源码不吃本工程的 -Werror
|
||||
target_compile_options(unity PRIVATE -w)
|
||||
|
||||
# ---- 测试用例:test/test_*.c 一个文件一个可执行 + 一条 ctest ---------------
|
||||
enable_testing()
|
||||
|
||||
file(GLOB PHONERAIL_TESTS ${FW_ROOT}/test/test_*.c)
|
||||
foreach(test_src ${PHONERAIL_TESTS})
|
||||
get_filename_component(test_name ${test_src} NAME_WE)
|
||||
add_executable(${test_name} ${test_src})
|
||||
target_link_libraries(${test_name} PRIVATE phonerail_core unity)
|
||||
add_test(NAME ${test_name} COMMAND ${test_name})
|
||||
endforeach()
|
||||
# PhoneRail 宿主机构建 —— 不依赖 ESP-IDF,用来跑单元测试(CTest)。
|
||||
#
|
||||
# bash host/test.sh 配置 + 编译 + ctest
|
||||
#
|
||||
# 与 IDF 目标共享的源码只有 ../main 里的与硬件无关部分。
|
||||
# 新增一个既要上板又要被测的 .c,必须同时加进:
|
||||
# ../main/CMakeLists.txt 的 SRCS 和 下面的 PHONERAIL_CORE_SRCS
|
||||
# HAL 里新增一路实现则要同时加进:
|
||||
# ../components/phonerail_hal/CMakeLists.txt 的 SRCS(esp 那份)
|
||||
# 和下面的 PHONERAIL_HAL_SIM_SRCS(sim 那份)
|
||||
# 新增一个器件驱动则是:
|
||||
# ../components/drv/CMakeLists.txt 的 SRCS 和 下面的 PHONERAIL_DRV_SRCS
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(phonerail_host C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
add_compile_options(-Wall -Wextra -Werror)
|
||||
|
||||
set(FW_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..)
|
||||
|
||||
# ---- 被测代码 -------------------------------------------------------------
|
||||
set(PHONERAIL_CORE_SRCS
|
||||
${FW_ROOT}/main/version.c
|
||||
${FW_ROOT}/main/rail.c
|
||||
${FW_ROOT}/main/rail_cfg.c
|
||||
${FW_ROOT}/main/sched.c
|
||||
${FW_ROOT}/main/tasks.c
|
||||
${FW_ROOT}/main/t_safety.c
|
||||
)
|
||||
|
||||
# HAL:接口在 include/,宿主机链 sim/ 的实现(esp/ 那份只有 IDF 目标编)。
|
||||
set(PHONERAIL_HAL_DIR ${FW_ROOT}/components/phonerail_hal)
|
||||
set(PHONERAIL_HAL_SIM_SRCS
|
||||
${PHONERAIL_HAL_DIR}/hal_err.c
|
||||
${PHONERAIL_HAL_DIR}/hal_i2c_acct.c
|
||||
${PHONERAIL_HAL_DIR}/sim/hal_lock_sim.c
|
||||
${PHONERAIL_HAL_DIR}/sim/sim_hal.c
|
||||
${PHONERAIL_HAL_DIR}/sim/hal_i2c_sim.c
|
||||
${PHONERAIL_HAL_DIR}/sim/hal_gpio_sim.c
|
||||
${PHONERAIL_HAL_DIR}/sim/sim_sr595.c
|
||||
${PHONERAIL_HAL_DIR}/sim/sim_pca9555.c
|
||||
${PHONERAIL_HAL_DIR}/sim/hal_pwm_sim.c
|
||||
${PHONERAIL_HAL_DIR}/sim/hal_time_sim.c
|
||||
)
|
||||
|
||||
# 器件驱动:与平台无关(只经 hal_i2c_*),两个目标编的是同一份源码。
|
||||
set(PHONERAIL_DRV_DIR ${FW_ROOT}/components/drv)
|
||||
set(PHONERAIL_DRV_SRCS
|
||||
${PHONERAIL_DRV_DIR}/ina226.c
|
||||
${PHONERAIL_DRV_DIR}/dac63204.c
|
||||
${PHONERAIL_DRV_DIR}/sr595.c
|
||||
${PHONERAIL_DRV_DIR}/pca9555.c
|
||||
)
|
||||
|
||||
add_library(phonerail_core STATIC
|
||||
${PHONERAIL_CORE_SRCS} ${PHONERAIL_HAL_SIM_SRCS} ${PHONERAIL_DRV_SRCS})
|
||||
target_include_directories(phonerail_core PUBLIC
|
||||
${FW_ROOT}/main
|
||||
${PHONERAIL_HAL_DIR}/include
|
||||
${PHONERAIL_HAL_DIR}/sim
|
||||
${PHONERAIL_DRV_DIR}/include)
|
||||
|
||||
# ---- 测试框架 Unity(从 ESP-IDF v5.4 原样复制到 third_party/,与上板测试同一套)
|
||||
add_library(unity STATIC ${FW_ROOT}/third_party/unity/unity.c)
|
||||
target_include_directories(unity PUBLIC ${FW_ROOT}/third_party/unity)
|
||||
target_compile_definitions(unity PUBLIC UNITY_OUTPUT_COLOR)
|
||||
# 第三方源码不吃本工程的 -Werror
|
||||
target_compile_options(unity PRIVATE -w)
|
||||
|
||||
# ---- 测试用例:test/test_*.c 一个文件一个可执行 + 一条 ctest ---------------
|
||||
enable_testing()
|
||||
|
||||
file(GLOB PHONERAIL_TESTS ${FW_ROOT}/test/test_*.c)
|
||||
foreach(test_src ${PHONERAIL_TESTS})
|
||||
get_filename_component(test_name ${test_src} NAME_WE)
|
||||
add_executable(${test_name} ${test_src})
|
||||
target_link_libraries(${test_name} PRIVATE phonerail_core unity)
|
||||
add_test(NAME ${test_name} COMMAND ${test_name})
|
||||
endforeach()
|
||||
|
||||
+13
-10
@@ -1,10 +1,13 @@
|
||||
# ESP-IDF 侧的 main 组件。
|
||||
# app_main.c 是 ESP 专用入口;其余 .c 必须同时在宿主机上编得过
|
||||
# (新增文件时两处都要加:这里 + host/CMakeLists.txt 的 PHONERAIL_CORE_SRCS)。
|
||||
idf_component_register(
|
||||
SRCS "app_main.c"
|
||||
"version.c"
|
||||
"rail.c"
|
||||
"rail_cfg.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES phonerail_hal drv)
|
||||
# ESP-IDF 侧的 main 组件。
|
||||
# app_main.c 是 ESP 专用入口;其余 .c 必须同时在宿主机上编得过
|
||||
# (新增文件时两处都要加:这里 + host/CMakeLists.txt 的 PHONERAIL_CORE_SRCS)。
|
||||
idf_component_register(
|
||||
SRCS "app_main.c"
|
||||
"version.c"
|
||||
"rail.c"
|
||||
"rail_cfg.c"
|
||||
"sched.c"
|
||||
"tasks.c"
|
||||
"t_safety.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES phonerail_hal drv)
|
||||
|
||||
+174
-50
@@ -1,50 +1,174 @@
|
||||
/* ESP32-S3 入口。只在 ESP-IDF 目标里编译,宿主机构建不包含本文件。 */
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "hal_time.h"
|
||||
#include "phonerail.h"
|
||||
#include "rail_cfg.h"
|
||||
|
||||
static const char *TAG = "phonerail";
|
||||
|
||||
/* 启动自检(需求 §10.4):五路的地址 / 位号 / 引脚无重复、无越界,
|
||||
而且那张五路表和四个驱动自己的表没有分家。
|
||||
|
||||
⚠️ **不过就 abort,不降级。** 这里查的全是编译进固件的常量表,
|
||||
查不过 = 固件本身写错了,不是硬件坏了 —— §10.10 的降级矩阵管的是后者。
|
||||
带着一张错表跑起来的样子是「命令发给了另一路」,五台手机全在跑、
|
||||
一个错误码都不报,谁也不会去怀疑它。 */
|
||||
static void selftest_or_die(void)
|
||||
{
|
||||
const uint32_t bad = rail_selftest();
|
||||
if (bad == 0) {
|
||||
ESP_LOGI(TAG, "五路配置表自检通过");
|
||||
return;
|
||||
}
|
||||
for (uint32_t bit = 1; bit != 0; bit <<= 1) {
|
||||
if ((bad & bit) != 0) {
|
||||
ESP_LOGE(TAG, "自检失败:%s (0x%02x)", rail_selftest_str(bit), (unsigned)bit);
|
||||
}
|
||||
}
|
||||
abort();
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "PhoneRail %s boot, %d rails, t=%u ms", phonerail_version(),
|
||||
PHONERAIL_RAIL_COUNT, (unsigned)hal_time_now_ms());
|
||||
|
||||
selftest_or_die();
|
||||
|
||||
/* 十根 EN 线配成输出并全部拉低。**先于一切外设**:DAC / eFuse 一上电就有
|
||||
输出,这两根线这时必须已经确定是低的。 */
|
||||
const hal_err_t err = rail_en_init();
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "EN 引脚初始化失败:%s", hal_err_str(err));
|
||||
abort();
|
||||
}
|
||||
ESP_LOGI(TAG, "五路输出与 VBUS 全部关闭,等待 A8 的任务骨架");
|
||||
|
||||
/* 器件初始化与五个任务留到 A8/A15。 */
|
||||
}
|
||||
/* ESP32-S3 入口。只在 ESP-IDF 目标里编译,宿主机构建不包含本文件。 */
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "board_pins.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "hal_i2c.h"
|
||||
#include "hal_lock.h"
|
||||
#include "hal_time.h"
|
||||
#include "phonerail.h"
|
||||
#include "rail_cfg.h"
|
||||
#include "sched.h"
|
||||
#include "t_safety.h"
|
||||
#include "tasks.h"
|
||||
|
||||
static const char *TAG = "phonerail";
|
||||
|
||||
/* 启动自检(需求 §10.4):五路的地址 / 位号 / 引脚无重复、无越界,
|
||||
而且那张五路表和四个驱动自己的表没有分家。
|
||||
|
||||
⚠️ **不过就 abort,不降级。** 这里查的全是编译进固件的常量表,
|
||||
查不过 = 固件本身写错了,不是硬件坏了 —— §10.10 的降级矩阵管的是后者。
|
||||
带着一张错表跑起来的样子是「命令发给了另一路」,五台手机全在跑、
|
||||
一个错误码都不报,谁也不会去怀疑它。 */
|
||||
static void selftest_or_die(void)
|
||||
{
|
||||
/* 五个任务那张表(§10.3):周期与优先级的相对顺序。同样是编译进去的常量,
|
||||
同样不过就 abort —— 优先级写反了板子照跑,只是巡检会在总线忙时被挤后。 */
|
||||
if (!task_table_ok()) {
|
||||
ESP_LOGE(TAG, "任务表自检失败:周期或优先级与 §10.3 对不上");
|
||||
abort();
|
||||
}
|
||||
|
||||
const uint32_t bad = rail_selftest();
|
||||
if (bad == 0) {
|
||||
ESP_LOGI(TAG, "五路配置表自检通过");
|
||||
return;
|
||||
}
|
||||
for (uint32_t bit = 1; bit != 0; bit <<= 1) {
|
||||
if ((bad & bit) != 0) {
|
||||
ESP_LOGE(TAG, "自检失败:%s (0x%02x)", rail_selftest_str(bit), (unsigned)bit);
|
||||
}
|
||||
}
|
||||
abort();
|
||||
}
|
||||
|
||||
/* ---- 五个任务(需求 §10.3) ---------------------------------------------
|
||||
任务体只在这个文件里,而且都短得能一眼看完 —— 真正的活在 main/ 的其它 .c 里,
|
||||
那些在宿主机上编得过、测得到。**任务体里不许出现判据与时序**:写进死循环里的
|
||||
东西,宿主机一行也跑不到。 */
|
||||
|
||||
static void task_safety(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
static safety_snap_t snap; /* static:不占任务栈,也方便挂调试器看 */
|
||||
t_safety_init(&snap);
|
||||
|
||||
sched_periodic_t tick;
|
||||
sched_periodic_init(&tick, task_desc(TASK_SAFETY)->period_ms);
|
||||
|
||||
char line[128];
|
||||
for (;;) {
|
||||
(void)t_safety_step(&snap); /* 器件读失败不在这里处理:§10.2 失联 ≠ 断电 */
|
||||
|
||||
/* 每 5 s 报一次总线占用。§10.3 手算是「约 1.5 ms / 约 8 %」,
|
||||
首板上电时要拿逻辑分析仪复核这个数(§10.3 末尾那句)。 */
|
||||
if ((snap.cycles % 250u) == 0u) {
|
||||
t_safety_format(&snap, line, sizeof(line));
|
||||
ESP_LOGI(TAG, "%s (预算 %u us, 过载 %u, 最迟 %u ms)", line,
|
||||
(unsigned)t_safety_budget_us(), (unsigned)tick.overruns,
|
||||
(unsigned)tick.worst_late_ms);
|
||||
}
|
||||
sched_periodic_wait(&tick);
|
||||
}
|
||||
}
|
||||
|
||||
/* 下面四个是骨架:周期与优先级已经按 §10.3 定好,任务体等各自的包。
|
||||
现在就把它们建起来,是为了让 A9~A16 只需要往里填,不用再动启动流程。 */
|
||||
|
||||
static void task_thermal(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
sched_periodic_t tick;
|
||||
sched_periodic_init(&tick, task_desc(TASK_THERMAL)->period_ms);
|
||||
for (;;) {
|
||||
/* A16:两路 NTC + 载板 LM75 → 风扇 PWM 闭环(TR-6 的 TACH 采样窗口)。 */
|
||||
sched_periodic_wait(&tick);
|
||||
}
|
||||
}
|
||||
|
||||
static void task_actuator(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
/* A9:三个状态机 + 互斥 + 超时兜底。队列在那一包里建。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
}
|
||||
|
||||
static void task_link(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
/* A12~A14:命令处理器 + 两条传输链路。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
}
|
||||
}
|
||||
|
||||
static void task_log(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
/* A11:事件环形缓冲落盘。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
static void spawn(task_id_t id, TaskFunction_t fn)
|
||||
{
|
||||
const task_desc_t *d = task_desc(id);
|
||||
/* ⚠️ ESP-IDF 的 xTaskCreate 收的 stack_depth 是**字节**(原版 FreeRTOS 是字)。 */
|
||||
if (xTaskCreate(fn, d->name, d->stack_bytes, NULL, d->prio, NULL) != pdPASS) {
|
||||
ESP_LOGE(TAG, "建任务失败:%s", d->name);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "PhoneRail %s boot, %d rails, t=%u ms", phonerail_version(),
|
||||
PHONERAIL_RAIL_COUNT, (unsigned)hal_time_now_ms());
|
||||
|
||||
selftest_or_die();
|
||||
|
||||
/* 十根 EN 线配成输出并全部拉低。**先于一切外设**:DAC / eFuse 一上电就有
|
||||
输出,这两根线这时必须已经确定是低的。 */
|
||||
hal_err_t err = rail_en_init();
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "EN 引脚初始化失败:%s", hal_err_str(err));
|
||||
abort();
|
||||
}
|
||||
ESP_LOGI(TAG, "五路输出与 VBUS 全部关闭");
|
||||
|
||||
/* 两把锁必须早于任何一个用总线的任务。 */
|
||||
(void)hal_lock_init();
|
||||
|
||||
const hal_i2c_cfg_t i2c = {
|
||||
.sda_gpio = BOARD_PIN_I2C_SDA,
|
||||
.scl_gpio = BOARD_PIN_I2C_SCL,
|
||||
.freq_hz = 400000,
|
||||
};
|
||||
err = hal_i2c_init(&i2c);
|
||||
if (err != HAL_OK) {
|
||||
ESP_LOGE(TAG, "I²C 初始化失败:%s", hal_err_str(err));
|
||||
abort();
|
||||
}
|
||||
|
||||
/* ⚠️ 器件初始化(INA226 的 Config/Calibration、DAC、PCA9555 方向、595 安全值)
|
||||
与**降级矩阵**是 A15 的启动顺序那一包。在那之前 t_safety 读回来的电流恒为 0
|
||||
(Calibration 还没写),这是预期的,不是故障。 */
|
||||
|
||||
spawn(TASK_SAFETY, task_safety);
|
||||
spawn(TASK_THERMAL, task_thermal);
|
||||
spawn(TASK_ACTUATOR, task_actuator);
|
||||
spawn(TASK_LINK, task_link);
|
||||
spawn(TASK_LOG, task_log);
|
||||
|
||||
ESP_LOGI(TAG, "五个任务已启动(t_safety %u ms / t_thermal %u ms),"
|
||||
"I²C 预算 %u us 每周期",
|
||||
(unsigned)task_desc(TASK_SAFETY)->period_ms,
|
||||
(unsigned)task_desc(TASK_THERMAL)->period_ms,
|
||||
(unsigned)t_safety_budget_us());
|
||||
}
|
||||
|
||||
+23
-23
@@ -1,23 +1,23 @@
|
||||
/* PhoneRail 固件公共头 —— 与硬件无关,宿主机与 ESP32-S3 上都编得过。 */
|
||||
#ifndef PHONERAIL_H
|
||||
#define PHONERAIL_H
|
||||
|
||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a7"
|
||||
|
||||
/* 通道数。五路直供,见设计文档 §2。 */
|
||||
#define PHONERAIL_RAIL_COUNT 5
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 返回 PHONERAIL_FW_VERSION。存在的意义是让宿主机测试有一个真的会被链接进去的
|
||||
符号 —— 骨架期用来证明「同一份 main/ 源码两个目标都编得过」。 */
|
||||
const char *phonerail_version(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PHONERAIL_H */
|
||||
/* PhoneRail 固件公共头 —— 与硬件无关,宿主机与 ESP32-S3 上都编得过。 */
|
||||
#ifndef PHONERAIL_H
|
||||
#define PHONERAIL_H
|
||||
|
||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a8"
|
||||
|
||||
/* 通道数。五路直供,见设计文档 §2。 */
|
||||
#define PHONERAIL_RAIL_COUNT 5
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 返回 PHONERAIL_FW_VERSION。存在的意义是让宿主机测试有一个真的会被链接进去的
|
||||
符号 —— 骨架期用来证明「同一份 main/ 源码两个目标都编得过」。 */
|
||||
const char *phonerail_version(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PHONERAIL_H */
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "sched.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "hal_time.h"
|
||||
|
||||
void sched_periodic_init(sched_periodic_t *p, uint32_t period_ms)
|
||||
{
|
||||
assert(p != NULL);
|
||||
assert(period_ms > 0); /* 事件驱动的任务不该走这里 */
|
||||
p->period_ms = period_ms;
|
||||
p->next_ms = hal_time_now_ms() + period_ms;
|
||||
p->cycles = 0;
|
||||
p->overruns = 0;
|
||||
p->worst_late_ms = 0;
|
||||
}
|
||||
|
||||
uint32_t sched_periodic_delay_from(sched_periodic_t *p, uint32_t now_ms)
|
||||
{
|
||||
assert(p != NULL && p->period_ms > 0);
|
||||
p->cycles++;
|
||||
|
||||
/* ⚠️ 一律用有符号差判先后,不要写 `now >= next`。
|
||||
32 位 ms 在 49.7 天回绕,农场是长稳跑的,回绕一定会发生 ——
|
||||
裸比较会在回绕那一刻把「还早着呢」判成「已经迟到了」,
|
||||
现象是巡检突然连着空转 49.7 天份的周期。 */
|
||||
const int32_t late = (int32_t)(now_ms - p->next_ms);
|
||||
|
||||
if (late < 0) {
|
||||
const uint32_t delay = (uint32_t)(-late);
|
||||
p->next_ms += p->period_ms;
|
||||
return delay;
|
||||
}
|
||||
|
||||
/* 到点了活还没干完。跳过错过的整周期、保住相位,见 sched.h。 */
|
||||
p->overruns++;
|
||||
if ((uint32_t)late > p->worst_late_ms) {
|
||||
p->worst_late_ms = (uint32_t)late;
|
||||
}
|
||||
const uint32_t missed = (uint32_t)late / p->period_ms + 1u;
|
||||
p->next_ms += missed * p->period_ms;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t sched_periodic_wait(sched_periodic_t *p)
|
||||
{
|
||||
const uint32_t delay = sched_periodic_delay_from(p, hal_time_now_ms());
|
||||
if (delay > 0) {
|
||||
hal_time_delay_ms(delay);
|
||||
}
|
||||
return delay;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* 周期任务的节拍器(需求 §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 <stdint.h>
|
||||
|
||||
#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 */
|
||||
@@ -0,0 +1,118 @@
|
||||
#include "t_safety.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "hal_i2c_acct.h"
|
||||
#include "hal_lock.h"
|
||||
#include "rail_cfg.h"
|
||||
#include "tasks.h"
|
||||
|
||||
void t_safety_init(safety_snap_t *s)
|
||||
{
|
||||
assert(s != NULL);
|
||||
memset(s, 0, sizeof(*s));
|
||||
/* 游标对齐到「现在」,否则第一圈会把启动期间(器件初始化)的事务算进来。 */
|
||||
s->bus_cursor = hal_i2c_acct_bits();
|
||||
}
|
||||
|
||||
hal_err_t t_safety_step(safety_snap_t *s)
|
||||
{
|
||||
assert(s != NULL);
|
||||
s->cycles++;
|
||||
|
||||
hal_err_t err = hal_lock_take(HAL_LOCK_I2C, T_SAFETY_LOCK_TIMEOUT_MS);
|
||||
if (err != HAL_OK) {
|
||||
/* 规矩 1:不死等。这一圈作废,20 ms 后重来。 */
|
||||
s->skipped++;
|
||||
s->bus_us = hal_i2c_acct_window_us(&s->bus_cursor);
|
||||
return err;
|
||||
}
|
||||
|
||||
hal_err_t first = HAL_OK;
|
||||
|
||||
/* 五路 —— 地址查表,**不许算**(rail_cfg.h 那条禁令)。 */
|
||||
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
||||
ina226_vi_t vi;
|
||||
const hal_err_t e = ina226_read_vi(rail_cfg(rail)->ina_addr, &vi);
|
||||
s->rail_ok[rail - 1] = (e == HAL_OK);
|
||||
if (e == HAL_OK) {
|
||||
s->rail[rail - 1] = vi; /* 规矩 2:失败就留着上一圈的值,别清零 */
|
||||
} else {
|
||||
s->i2c_errors++;
|
||||
if (first == HAL_OK) {
|
||||
first = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 第六片:输入侧。 */
|
||||
{
|
||||
ina226_vi_t vi;
|
||||
const hal_err_t e = ina226_read_vi(INA226_ADDR_INPUT, &vi);
|
||||
s->input_ok = (e == HAL_OK);
|
||||
if (e == HAL_OK) {
|
||||
s->input = vi;
|
||||
} else {
|
||||
s->i2c_errors++;
|
||||
if (first == HAL_OK) {
|
||||
first = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* PCA9555 的两个输入端口一次事务读完(自增指针)。 */
|
||||
{
|
||||
pca9555_in_t in;
|
||||
const hal_err_t e = pca9555_read_inputs(&in);
|
||||
s->io_ok = (e == HAL_OK);
|
||||
if (e == HAL_OK) {
|
||||
s->io = in;
|
||||
} else {
|
||||
s->i2c_errors++;
|
||||
if (first == HAL_OK) {
|
||||
first = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hal_lock_give(HAL_LOCK_I2C);
|
||||
|
||||
/* A10 的判据接在这里:越界**直接**写 EFUSE_EN / VBUS_EN(规矩 3),
|
||||
而且要在放锁之后 —— 那两根线是 GPIO 直连,和 I²C 没关系,
|
||||
拿着总线锁去操作引脚只会把别的任务多挡一会儿。 */
|
||||
|
||||
s->bus_us = hal_i2c_acct_window_us(&s->bus_cursor);
|
||||
return first;
|
||||
}
|
||||
|
||||
uint32_t t_safety_budget_us(void)
|
||||
{
|
||||
/* 上面那一圈真正发出去的事务,逐条数:
|
||||
6 片 INA226 × 2 次寄存器读(母线电压 + 电流)
|
||||
1 次 PCA9555 读(一次事务读回两个输入端口)
|
||||
每次都是「写 1 字节寄存器号 + 读 2 字节」的形状。 */
|
||||
const uint32_t txns = INA226_COUNT * 2u + 1u;
|
||||
return (uint32_t)hal_i2c_bits_to_us((uint64_t)txns * hal_i2c_txn_bits(1, 2));
|
||||
}
|
||||
|
||||
uint32_t t_safety_load_permille(uint32_t bus_us, uint32_t period_ms)
|
||||
{
|
||||
if (period_ms == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (uint32_t)(((uint64_t)bus_us * 1000ull) / ((uint64_t)period_ms * 1000ull));
|
||||
}
|
||||
|
||||
int t_safety_format(const safety_snap_t *s, char *buf, size_t len)
|
||||
{
|
||||
assert(s != NULL && buf != NULL && len > 0);
|
||||
const uint32_t period = task_desc(TASK_SAFETY)->period_ms;
|
||||
const uint32_t pm = t_safety_load_permille(s->bus_us, period);
|
||||
return snprintf(buf, len,
|
||||
"t_safety: cycles=%u skipped=%u i2c_err=%u bus=%u us/%u ms load=%u.%u%%",
|
||||
(unsigned)s->cycles, (unsigned)s->skipped, (unsigned)s->i2c_errors,
|
||||
(unsigned)s->bus_us, (unsigned)period, (unsigned)(pm / 10u),
|
||||
(unsigned)(pm % 10u));
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/* `t_safety` —— 20 ms 巡检任务的**一个周期**(需求 §10.3 第一行)。
|
||||
|
||||
本包(A8)只做骨架:拿 I²C 锁 → 读 6 片 INA226 的 V+I → 读 PCA9555 两个输入端口
|
||||
→ 放锁 → 记账。**判据(SW-2 / SW-8 / 过压 / 反灌)是 A10 的事**,这里一条都没有。
|
||||
|
||||
分成「一个周期是个函数」而不是直接写成 `while(1)`,是为了让它在宿主机上跑得起来:
|
||||
宿主机没有 FreeRTOS,任务体是死循环就一行也测不了。ESP 侧的 `while(1)`
|
||||
在 app_main.c 里,只有三行(拿节拍 / 调这个 / 睡到下一拍)。
|
||||
|
||||
---- 三条不能改的规矩 ----------------------------------------------------
|
||||
1. **不许死等锁。** 拿不到就跳过这一周期并计数(`skipped`)。死等的现象是
|
||||
巡检整个停摆,而它是优先级最高的那个任务 —— 停摆了没有任何人会发现。
|
||||
2. **一个器件读失败不能中断这一圈。** 六片里坏一片,剩下五路的判据照常要出。
|
||||
失联 ≠ 断电(§10.2),失联更不等于「别的路也不看了」。
|
||||
3. **越界要断电时直接写 `EFUSE_EN` / `VBUS_EN` 引脚**(§10.3 原话:不经任何队列
|
||||
或抽象层)。所以 A10 的判据要写在这个函数里,不是发个消息给 t_actuator。 */
|
||||
#ifndef T_SAFETY_H
|
||||
#define T_SAFETY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
#include "ina226.h"
|
||||
#include "pca9555.h"
|
||||
#include "phonerail.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 等 I²C 锁最多等这么久。取周期的 1/4:等过头这一周期就废了,
|
||||
而**废一个周期**(20 ms 后重来)比**赖着不走**(把最高优先级任务钉死)好得多。 */
|
||||
#define T_SAFETY_LOCK_TIMEOUT_MS 5
|
||||
|
||||
typedef struct {
|
||||
/* 这一圈读回来的原始量。`ok` 为 false 时对应的值是**上一圈的**,不是 0 ——
|
||||
判据(A10)必须先看 ok 再看值,拿一个没更新的电压去判过压是最难查的那种错。 */
|
||||
ina226_vi_t rail[PHONERAIL_RAIL_COUNT];
|
||||
bool rail_ok[PHONERAIL_RAIL_COUNT];
|
||||
ina226_vi_t input; /* 输入侧 U13(0x43),整板功率预算 */
|
||||
bool input_ok;
|
||||
pca9555_in_t io; /* 五路 FLT# / PGOOD + Buck PG 或 + INA ALERT 或 */
|
||||
bool io_ok;
|
||||
|
||||
uint32_t cycles; /* 跑过几圈(含被跳过的) */
|
||||
uint32_t skipped; /* 其中因为拿不到 I²C 锁而整圈跳过的 */
|
||||
uint32_t i2c_errors; /* 累计器件级失败次数(一圈里可能不止一次) */
|
||||
uint32_t bus_us; /* **上一圈**在 I²C 上真正花掉的时间 */
|
||||
uint64_t bus_cursor; /* 内部:位数游标,别在外面改 */
|
||||
} safety_snap_t;
|
||||
|
||||
void t_safety_init(safety_snap_t *s);
|
||||
hal_err_t t_safety_step(safety_snap_t *s);
|
||||
|
||||
/* 按 §10.3 的算式静态算出来的「一圈该占总线多久」,单位 µs。
|
||||
它和 `s->bus_us` 应当**一模一样** —— 不一样只有两种可能:巡检里多读/少读了东西,
|
||||
或者有别的任务在同一圈里插了事务进来。测试拿这两个数对账。 */
|
||||
uint32_t t_safety_budget_us(void);
|
||||
|
||||
/* 占用率(千分之)。20 ms 周期、1560 µs → 78‰。§10.3 手算的是「约 8 %」。 */
|
||||
uint32_t t_safety_load_permille(uint32_t bus_us, uint32_t period_ms);
|
||||
|
||||
/* 一行人话,给启动日志和遥测用。返回写进去的字符数(不含结尾 0)。 */
|
||||
int t_safety_format(const safety_snap_t *s, char *buf, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* T_SAFETY_H */
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "tasks.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ina226.h"
|
||||
|
||||
/* 优先级的绝对值本身不重要,**相对顺序**才是 §10.3 定的东西。
|
||||
留出空档(6/4/4/3/1)是为了以后插一个任务时不用整体重排 —— 重排的那次
|
||||
一定会有人只改一半。 */
|
||||
#define PRIO_SAFETY 6
|
||||
#define PRIO_THERMAL 4
|
||||
#define PRIO_ACTUATOR 4
|
||||
#define PRIO_LINK 3
|
||||
#define PRIO_LOG 1
|
||||
|
||||
static const task_desc_t s_tasks[TASK_COUNT] = {
|
||||
[TASK_SAFETY] = { "t_safety", 20, PRIO_SAFETY, 4096 },
|
||||
[TASK_THERMAL] = { "t_thermal", 1000, PRIO_THERMAL, 3072 },
|
||||
/* 执行器要跑三个状态机 + 超时兜底,栈给足;link 要收发帧与 JSON,最费。 */
|
||||
[TASK_ACTUATOR] = { "t_actuator", 0, PRIO_ACTUATOR, 4096 },
|
||||
[TASK_LINK] = { "t_link", 0, PRIO_LINK, 6144 },
|
||||
[TASK_LOG] = { "t_log", 0, PRIO_LOG, 3072 },
|
||||
};
|
||||
|
||||
const task_desc_t *task_desc(task_id_t id)
|
||||
{
|
||||
assert(id >= 0 && id < TASK_COUNT);
|
||||
return &s_tasks[id];
|
||||
}
|
||||
|
||||
bool task_table_ok(void)
|
||||
{
|
||||
for (int i = 0; i < TASK_COUNT; i++) {
|
||||
const task_desc_t *a = &s_tasks[i];
|
||||
if (a->name == NULL || a->name[0] == '\0' || a->stack_bytes < 2048) {
|
||||
return false;
|
||||
}
|
||||
for (int j = i + 1; j < TASK_COUNT; j++) {
|
||||
if (strcmp(a->name, s_tasks[j].name) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 周期:§10.3 那一列,逐格钉死。 */
|
||||
if (s_tasks[TASK_SAFETY].period_ms != 20 || s_tasks[TASK_THERMAL].period_ms != 1000) {
|
||||
return false;
|
||||
}
|
||||
if (s_tasks[TASK_ACTUATOR].period_ms != 0 || s_tasks[TASK_LINK].period_ms != 0 ||
|
||||
s_tasks[TASK_LOG].period_ms != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 优先级:t_safety 唯一最高、t_log 唯一最低。 */
|
||||
for (int i = 0; i < TASK_COUNT; i++) {
|
||||
if (i != TASK_SAFETY && s_tasks[i].prio >= s_tasks[TASK_SAFETY].prio) {
|
||||
return false;
|
||||
}
|
||||
if (i != TASK_LOG && s_tasks[i].prio <= s_tasks[TASK_LOG].prio) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (s_tasks[TASK_THERMAL].prio != s_tasks[TASK_ACTUATOR].prio) {
|
||||
return false; /* §10.3 里两个都是「中」 */
|
||||
}
|
||||
if (s_tasks[TASK_LINK].prio >= s_tasks[TASK_THERMAL].prio) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 巡检周期要长于 INA226 的一帧转换时间(见 tasks.h 的最后一条)。 */
|
||||
if (s_tasks[TASK_SAFETY].period_ms * 1000u <= (uint32_t)INA226_CONV_US) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/* 五个任务的那张表(需求 §10.3)。
|
||||
|
||||
§10.3 定了五个任务、各自的周期与优先级,还有一句最容易被写丢的话:
|
||||
**`t_safety` 越界时直接操作 `EFUSE_EN` / `VBUS_EN`,不经任何队列或抽象层。**
|
||||
这张表在这里的意义不是「方便建任务」,而是让那五行**有一个能被自检查的形状** ——
|
||||
优先级顺序写反了不会报错,只会让巡检在总线忙的时候被挤到几十毫秒之后。
|
||||
|
||||
---- 为什么周期与优先级要合在一张表里 ------------------------------------
|
||||
因为它们是同一个判断的两半:`t_safety` 之所以是最高优先级,正是因为它有一个
|
||||
20 ms 的硬节拍;`t_link` 之所以最低,正是因为它没有节拍(§10.2:失联不断电,
|
||||
上位机慢一点没有任何后果)。分成两处写,改了一半的那天没人看得出来。
|
||||
|
||||
⚠️ 事件驱动的三个任务(actuator / link / log)在本表里 `period_ms == 0`。
|
||||
它们的队列与消息定义在 A9(执行器)/ A12(命令)/ A11(日志)里,本包只搭骨架。 */
|
||||
#ifndef TASKS_H
|
||||
#define TASKS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
TASK_SAFETY = 0, /* 20 ms,最高:SW-2 / SW-8 判据 + 直接操作 EN 引脚 */
|
||||
TASK_THERMAL, /* 1 s,中:两路 NTC + 载板 LM75 → 风扇 PWM 闭环 */
|
||||
TASK_ACTUATOR, /* 事件驱动,中:断电重启 / 按键序列 / EDL 序列 */
|
||||
TASK_LINK, /* 事件驱动,低:上位机命令与遥测 */
|
||||
TASK_LOG, /* 空闲,最低:事件环形缓冲落盘 */
|
||||
TASK_COUNT,
|
||||
} task_id_t;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint32_t period_ms; /* 0 = 事件驱动 */
|
||||
int prio; /* FreeRTOS 优先级,越大越高 */
|
||||
uint32_t stack_bytes; /* ESP-IDF 的 xTaskCreate 收的是**字节**,不是字 */
|
||||
} task_desc_t;
|
||||
|
||||
/* id 越界直接 abort。 */
|
||||
const task_desc_t *task_desc(task_id_t id);
|
||||
|
||||
/* 启动自检的一部分(和 `rail_selftest()` 一样:不过就 abort,不降级)。
|
||||
查的是**表本身的形状**,不碰硬件:
|
||||
· 名字非空且不重复;
|
||||
· `t_safety` 是**唯一**的最高优先级,`t_log` 是**唯一**的最低;
|
||||
· thermal 与 actuator 同级(§10.3 都是「中」),link 严格低于它们、高于 log;
|
||||
· 周期与 §10.3 逐格相符(20 ms / 1 s / 三个 0);
|
||||
· `t_safety` 的周期必须长于 INA226 的一帧转换时间,否则每次读到的是同一帧 ——
|
||||
现象是「电流看起来非常稳」,而不是任何一种错误。 */
|
||||
bool task_table_ok(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* TASKS_H */
|
||||
@@ -0,0 +1,272 @@
|
||||
/* A8 之一:节拍器(sched)、五任务表(tasks)、两把锁(hal_lock)。
|
||||
|
||||
这三样都是「写错了也照跑」的东西:周期漂了、优先级反了、锁忘了放,
|
||||
板子上的现象都是「偶尔慢一点」。所以这里的断言全部对着**判据**,
|
||||
不对着「跑起来没崩」。 */
|
||||
#include <string.h>
|
||||
|
||||
#include "hal_lock.h"
|
||||
#include "hal_time.h"
|
||||
#include "ina226.h"
|
||||
#include "sched.h"
|
||||
#include "sim_hal.h"
|
||||
#include "tasks.h"
|
||||
#include "unity.h"
|
||||
|
||||
void setUp(void) { sim_hal_reset(); }
|
||||
void tearDown(void) {}
|
||||
|
||||
/* ---- 节拍器:周期是「间隔」,不是「延时」 -------------------------------- */
|
||||
|
||||
/* 干活花掉 3 ms,节拍仍然是整 20 ms —— 这是 §10.3 的 20 ms 的定义。
|
||||
写成「干完睡 20」的话这里会变成 23 ms。 */
|
||||
static void test_period_is_the_interval_not_the_delay(void)
|
||||
{
|
||||
sched_periodic_t p;
|
||||
sched_periodic_init(&p, 20);
|
||||
|
||||
uint32_t prev = hal_time_now_ms();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
sim_time_advance_ms(3); /* 干活 */
|
||||
sched_periodic_wait(&p);
|
||||
const uint32_t now = hal_time_now_ms();
|
||||
TEST_ASSERT_EQUAL_UINT32(20, now - prev);
|
||||
prev = now;
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(0, p.overruns);
|
||||
}
|
||||
|
||||
/* 干活时间**每圈都不一样**(1~19 ms),周期照样必须是整 20 ms。
|
||||
固定耗时测不出漂移:漂移是「上一圈的耗时被加进下一圈」,等长时看不出来。 */
|
||||
static void test_jittery_work_does_not_move_the_phase(void)
|
||||
{
|
||||
sched_periodic_t p;
|
||||
sched_periodic_init(&p, 20);
|
||||
|
||||
uint32_t work = 1;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
sim_time_advance_ms(work);
|
||||
work = (work * 7u) % 19u + 1u; /* 1~19 ms,绕着周期打转 */
|
||||
sched_periodic_wait(&p);
|
||||
/* 每一次醒来都落在 20 ms 的格子上 */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, hal_time_now_ms() % 20u);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(0, p.overruns);
|
||||
}
|
||||
|
||||
/* 一圈超时(干了 25 ms):计一次过载、不睡,而且**相位不丢** ——
|
||||
下一次仍然落在 20 ms 的格子上,不是从 25 ms 重新起算。 */
|
||||
static void test_overrun_keeps_the_phase_and_is_counted(void)
|
||||
{
|
||||
sched_periodic_t p;
|
||||
sched_periodic_init(&p, 20);
|
||||
|
||||
sim_time_advance_ms(25);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sched_periodic_wait(&p)); /* 不睡 */
|
||||
TEST_ASSERT_EQUAL_UINT32(1, p.overruns);
|
||||
TEST_ASSERT_EQUAL_UINT32(5, p.worst_late_ms);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(15, sched_periodic_wait(&p)); /* 睡到 t=40 */
|
||||
TEST_ASSERT_EQUAL_UINT32(40, hal_time_now_ms());
|
||||
}
|
||||
|
||||
/* 卡了 105 ms(错过 5 个周期)之后**不补跑** —— 只跳过去,计一次过载。
|
||||
补跑的现象是「卡一下之后连着打满总线」,见 sched.h。 */
|
||||
static void test_a_long_stall_skips_instead_of_catching_up(void)
|
||||
{
|
||||
sched_periodic_t p;
|
||||
sched_periodic_init(&p, 20);
|
||||
|
||||
sim_time_advance_ms(105);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sched_periodic_wait(&p));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, p.overruns); /* 一次,不是五次 */
|
||||
|
||||
/* 下一拍是 t=120,也就是 15 ms 之后 —— 仍在 20 ms 的格子上。 */
|
||||
TEST_ASSERT_EQUAL_UINT32(15, sched_periodic_wait(&p));
|
||||
TEST_ASSERT_EQUAL_UINT32(120, hal_time_now_ms());
|
||||
}
|
||||
|
||||
/* 32 位毫秒在 49.7 天回绕。裸写 `now >= next` 的话,回绕那一刻会被判成
|
||||
「迟到了 49 天」,然后一口气跳掉两百万个周期。农场是长稳跑的,这一定会发生。 */
|
||||
static void test_millisecond_wraparound_is_just_another_tick(void)
|
||||
{
|
||||
sim_time_set_us(0xFFFFFFF6ull * 1000ull); /* 距回绕 10 ms */
|
||||
sched_periodic_t p;
|
||||
sched_periodic_init(&p, 20);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
const uint32_t slept = sched_periodic_wait(&p);
|
||||
TEST_ASSERT_EQUAL_UINT32(20, slept); /* 跨过回绕点也还是 20 */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, p.overruns);
|
||||
}
|
||||
}
|
||||
|
||||
/* 纯计算入口要能不依赖时钟单独用(回绕、连续过载用真时钟很难摆出来)。 */
|
||||
static void test_delay_from_is_pure(void)
|
||||
{
|
||||
sched_periodic_t p;
|
||||
sched_periodic_init(&p, 20); /* now=0 → next=20 */
|
||||
TEST_ASSERT_EQUAL_UINT32(20, sched_periodic_delay_from(&p, 0));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sched_periodic_delay_from(&p, 39));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, hal_time_now_ms()); /* 一点时间也没花 */
|
||||
TEST_ASSERT_EQUAL_UINT32(2, p.cycles);
|
||||
}
|
||||
|
||||
/* ---- 五个任务那张表(§10.3) --------------------------------------------- */
|
||||
|
||||
static void test_task_table_passes_selftest(void)
|
||||
{
|
||||
TEST_ASSERT_TRUE(task_table_ok());
|
||||
}
|
||||
|
||||
static void test_periods_match_section_10_3(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT32(20, task_desc(TASK_SAFETY)->period_ms);
|
||||
TEST_ASSERT_EQUAL_UINT32(1000, task_desc(TASK_THERMAL)->period_ms);
|
||||
/* 三个事件驱动的没有周期 —— 给它们安一个周期就是把 §10.3 读错了。 */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, task_desc(TASK_ACTUATOR)->period_ms);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, task_desc(TASK_LINK)->period_ms);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, task_desc(TASK_LOG)->period_ms);
|
||||
}
|
||||
|
||||
/* §10.3 的优先级列:最高 / 中 / 中 / 低 / 最低。
|
||||
反了的现象是「总线一忙巡检就被挤后」,没有任何错误码。 */
|
||||
static void test_priority_order_is_safety_highest_log_lowest(void)
|
||||
{
|
||||
const int safety = task_desc(TASK_SAFETY)->prio;
|
||||
const int thermal = task_desc(TASK_THERMAL)->prio;
|
||||
const int actuator = task_desc(TASK_ACTUATOR)->prio;
|
||||
const int link = task_desc(TASK_LINK)->prio;
|
||||
const int log = task_desc(TASK_LOG)->prio;
|
||||
|
||||
TEST_ASSERT_GREATER_THAN_INT(thermal, safety);
|
||||
TEST_ASSERT_GREATER_THAN_INT(actuator, safety);
|
||||
TEST_ASSERT_EQUAL_INT(thermal, actuator); /* 两个都是「中」 */
|
||||
TEST_ASSERT_GREATER_THAN_INT(link, thermal);
|
||||
TEST_ASSERT_GREATER_THAN_INT(log, link);
|
||||
}
|
||||
|
||||
/* 巡检周期必须长于 INA226 的一帧转换时间(8.8 ms),否则同一帧会被读两次。
|
||||
现象是「电流特别稳」,不是任何一种错误。 */
|
||||
static void test_safety_period_outlasts_one_ina226_conversion(void)
|
||||
{
|
||||
TEST_ASSERT_GREATER_THAN_UINT32(INA226_CONV_US,
|
||||
task_desc(TASK_SAFETY)->period_ms * 1000u);
|
||||
}
|
||||
|
||||
static void test_every_task_has_a_name_and_a_real_stack(void)
|
||||
{
|
||||
for (int i = 0; i < TASK_COUNT; i++) {
|
||||
const task_desc_t *d = task_desc((task_id_t)i);
|
||||
TEST_ASSERT_NOT_NULL(d->name);
|
||||
TEST_ASSERT_TRUE(d->name[0] != 0);
|
||||
TEST_ASSERT_GREATER_OR_EQUAL_UINT32(2048, d->stack_bytes);
|
||||
for (int j = i + 1; j < TASK_COUNT; j++) {
|
||||
TEST_ASSERT_NOT_EQUAL(0, strcmp(d->name, task_desc((task_id_t)j)->name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 两把锁 --------------------------------------------------------------- */
|
||||
|
||||
static void test_take_and_give_round_trip(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_init());
|
||||
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
|
||||
TEST_ASSERT_TRUE(hal_lock_held(HAL_LOCK_I2C));
|
||||
hal_lock_give(HAL_LOCK_I2C);
|
||||
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
|
||||
}
|
||||
|
||||
/* 没 init 就拿 —— 真板上那是个空句柄,不拦就是随机崩。 */
|
||||
static void test_take_before_init_is_refused(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, hal_lock_take(HAL_LOCK_I2C, 10));
|
||||
}
|
||||
|
||||
/* 同一把锁拿两次:这两把都不是递归锁,第二次必须被拒。
|
||||
悄悄通过的话,第一次 give 就把互斥放掉了,而代码看上去还在临界区里。 */
|
||||
static void test_double_take_is_refused(void)
|
||||
{
|
||||
(void)hal_lock_init();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, hal_lock_take(HAL_LOCK_I2C, 10));
|
||||
hal_lock_give(HAL_LOCK_I2C);
|
||||
}
|
||||
|
||||
/* 拿着一把去拿另一把:这块板上永远是写错了(595 是纯 GPIO,和 I²C 没交集),
|
||||
所以不定加锁顺序,直接判非法。见 hal_lock.h。 */
|
||||
static void test_nesting_the_two_locks_is_refused_both_ways(void)
|
||||
{
|
||||
(void)hal_lock_init();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, hal_lock_take(HAL_LOCK_SR595, 10));
|
||||
hal_lock_give(HAL_LOCK_I2C);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_SR595, 10));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_STATE, hal_lock_take(HAL_LOCK_I2C, 10));
|
||||
hal_lock_give(HAL_LOCK_SR595);
|
||||
}
|
||||
|
||||
/* 别的任务持着的时候:等满超时、计一次数、返回 TIMEOUT,
|
||||
而且**把那段时间真的花掉** —— 调用方跳过这一周期的路径因此测得到。 */
|
||||
static void test_contention_times_out_and_costs_real_time(void)
|
||||
{
|
||||
(void)hal_lock_init();
|
||||
sim_lock_set_task(1);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
|
||||
|
||||
sim_lock_set_task(0);
|
||||
const uint32_t t0 = hal_time_now_ms();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, hal_lock_take(HAL_LOCK_I2C, 5));
|
||||
TEST_ASSERT_EQUAL_UINT32(5, hal_time_elapsed_ms(t0));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, hal_lock_timeouts(HAL_LOCK_I2C));
|
||||
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C));
|
||||
|
||||
/* 1 号放掉之后 0 号才拿得到。 */
|
||||
sim_lock_set_task(1);
|
||||
hal_lock_give(HAL_LOCK_I2C);
|
||||
sim_lock_set_task(0);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 5));
|
||||
hal_lock_give(HAL_LOCK_I2C);
|
||||
}
|
||||
|
||||
/* 两把锁互不相干:拿着 I²C 不影响另一个任务去拿 595。 */
|
||||
static void test_the_two_locks_are_independent(void)
|
||||
{
|
||||
(void)hal_lock_init();
|
||||
sim_lock_set_task(1);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
|
||||
sim_lock_set_task(2);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_SR595, 10));
|
||||
TEST_ASSERT_EQUAL_INT(1, sim_lock_owner(HAL_LOCK_I2C));
|
||||
TEST_ASSERT_EQUAL_INT(2, sim_lock_owner(HAL_LOCK_SR595));
|
||||
hal_lock_give(HAL_LOCK_SR595);
|
||||
sim_lock_set_task(1);
|
||||
hal_lock_give(HAL_LOCK_I2C);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_period_is_the_interval_not_the_delay);
|
||||
RUN_TEST(test_jittery_work_does_not_move_the_phase);
|
||||
RUN_TEST(test_overrun_keeps_the_phase_and_is_counted);
|
||||
RUN_TEST(test_a_long_stall_skips_instead_of_catching_up);
|
||||
RUN_TEST(test_millisecond_wraparound_is_just_another_tick);
|
||||
RUN_TEST(test_delay_from_is_pure);
|
||||
RUN_TEST(test_task_table_passes_selftest);
|
||||
RUN_TEST(test_periods_match_section_10_3);
|
||||
RUN_TEST(test_priority_order_is_safety_highest_log_lowest);
|
||||
RUN_TEST(test_safety_period_outlasts_one_ina226_conversion);
|
||||
RUN_TEST(test_every_task_has_a_name_and_a_real_stack);
|
||||
RUN_TEST(test_take_and_give_round_trip);
|
||||
RUN_TEST(test_take_before_init_is_refused);
|
||||
RUN_TEST(test_double_take_is_refused);
|
||||
RUN_TEST(test_nesting_the_two_locks_is_refused_both_ways);
|
||||
RUN_TEST(test_contention_times_out_and_costs_real_time);
|
||||
RUN_TEST(test_the_two_locks_are_independent);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/* A8 之二:`t_safety` 的一个周期,以及需求 §10.3 的两条验收判据 ——
|
||||
① sim 时钟下周期是 20 ms ±1 ms;
|
||||
② I²C 占用打印出来,与 §10.3 手算的「约 1.5 ms / 约 8 %」对得上。
|
||||
|
||||
⚠️ 这两条能成立,靠的是 sim 的假 I²C **真的把虚拟时钟花掉**
|
||||
(`sim_i2c_set_bus_time`,默认开)。没有那一条,巡检在 sim 里耗时为零,
|
||||
「20 ms 周期」就退化成「测 delay 参数传对了没」—— 那是句空话:
|
||||
真正会出事的是「巡检自己占掉了 1.5 ms,节拍还准不准」。 */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "hal_i2c.h"
|
||||
#include "hal_i2c_acct.h"
|
||||
#include "hal_lock.h"
|
||||
#include "hal_time.h"
|
||||
#include "ina226.h"
|
||||
#include "pca9555.h"
|
||||
#include "rail_cfg.h"
|
||||
#include "sched.h"
|
||||
#include "sim_hal.h"
|
||||
#include "t_safety.h"
|
||||
#include "tasks.h"
|
||||
#include "unity.h"
|
||||
|
||||
#define N PHONERAIL_RAIL_COUNT
|
||||
|
||||
/* 一圈发出去的事务数:6 片 INA226 各 2 次寄存器读 + PCA9555 一次。 */
|
||||
#define TXNS_PER_CYCLE (INA226_COUNT * 2 + 1)
|
||||
|
||||
/* 把假板子搭起来:6 片 INA226 + 1 片 PCA9555,地址全部**查表**取。 */
|
||||
static void board_up(void)
|
||||
{
|
||||
const hal_i2c_cfg_t cfg = { .sda_gpio = 14, .scl_gpio = 15, .freq_hz = 400000 };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(&cfg));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_init());
|
||||
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
sim_i2c_dev_t *d = sim_i2c_add(rail_cfg(rail)->ina_addr, 2);
|
||||
d->regs[INA226_REG_BUS_V] = 3120; /* 3.9 V ÷ 1.25 mV */
|
||||
d->regs[INA226_REG_CURRENT] = (uint16_t)(rail * 1000); /* 每路不同,好认 */
|
||||
}
|
||||
sim_i2c_add(INA226_ADDR_INPUT, 2);
|
||||
sim_pca9555_attach(PCA9555_ADDR);
|
||||
}
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
sim_hal_reset();
|
||||
board_up();
|
||||
}
|
||||
void tearDown(void) {}
|
||||
|
||||
/* ---- 验收主项一:20 ms 的节拍 -------------------------------------------- */
|
||||
|
||||
/* 500 圈。每一圈都要**正好**落在 20 ms 的格子上,而且真实唤醒时刻
|
||||
(µs 级)对理想时刻的偏差始终 < 1 ms —— 这就是「20 ms ±1 ms」。
|
||||
会漂的实现(干完睡 20)在这里第一圈就出局:那样周期是 21.56 ms,
|
||||
500 圈之后差出 780 ms。 */
|
||||
static void test_safety_holds_a_20ms_beat_for_500_cycles(void)
|
||||
{
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
|
||||
sched_periodic_t tick;
|
||||
sched_periodic_init(&tick, task_desc(TASK_SAFETY)->period_ms);
|
||||
|
||||
/* 先确认「干活」在 sim 里真的花掉了时间。不然下面 500 圈测的只是
|
||||
「delay 参数传对了没」—— 巡检耗时为零的话,什么实现都能踩准 20 ms。 */
|
||||
const uint64_t t0 = hal_time_now_us();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT64(t_safety_budget_us(), hal_time_now_us() - t0);
|
||||
sched_periodic_wait(&tick);
|
||||
|
||||
uint32_t prev_ms = hal_time_now_ms();
|
||||
for (int i = 1; i < 500; i++) {
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
sched_periodic_wait(&tick);
|
||||
|
||||
const uint32_t now_ms = hal_time_now_ms();
|
||||
TEST_ASSERT_EQUAL_UINT32(20, now_ms - prev_ms);
|
||||
prev_ms = now_ms;
|
||||
|
||||
/* 理想时刻是 (i+1)×20 ms;实际唤醒的 µs 与它相差不到一个毫秒格。 */
|
||||
const int64_t ideal_us = (int64_t)(i + 1) * 20000;
|
||||
const int64_t err_us = (int64_t)hal_time_now_us() - ideal_us;
|
||||
TEST_ASSERT_TRUE(err_us > -1000 && err_us < 1000);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(0, tick.overruns);
|
||||
TEST_ASSERT_EQUAL_UINT32(500, snap.cycles);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, snap.skipped);
|
||||
}
|
||||
|
||||
/* ---- 验收主项二:I²C 占用 ------------------------------------------------ */
|
||||
|
||||
/* §10.3 手算:一次寄存器读 ≈46 个位周期,12 次 INA + 1 次 PCA ≈1.5 ms / 20 ms ≈8 %。
|
||||
这里按线上真实发生的字节数算,得 48 位/次 × 13 = 624 位 = 1560 µs = 7.8 %。
|
||||
差的两位是 STOP 和一个重复 START,§10.3 没数进去(hal_i2c_acct.h 里记了这件事)。 */
|
||||
static void test_i2c_load_per_cycle_matches_section_10_3(void)
|
||||
{
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(48, hal_i2c_txn_bits(1, 2));
|
||||
TEST_ASSERT_EQUAL_UINT32(1560, snap.bus_us);
|
||||
|
||||
const uint32_t pm = t_safety_load_permille(snap.bus_us,
|
||||
task_desc(TASK_SAFETY)->period_ms);
|
||||
TEST_ASSERT_EQUAL_UINT32(78, pm);
|
||||
/* §10.3 的「约 8 %」:7.0 %~8.5 % 都算对得上,超出就是巡检里多读/少读了东西。 */
|
||||
TEST_ASSERT_TRUE(pm >= 70 && pm <= 85);
|
||||
|
||||
char line[128];
|
||||
const int n = t_safety_format(&snap, line, sizeof(line));
|
||||
TEST_ASSERT_TRUE(n > 0 && n < (int)sizeof(line));
|
||||
TEST_ASSERT_NOT_NULL(strstr(line, "bus=1560 us/20 ms"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(line, "load=7.8%"));
|
||||
printf(" %s(§10.3 手算:约 1.5 ms / 约 8 %%)\n", line);
|
||||
}
|
||||
|
||||
/* 静态预算与实测必须一模一样。不一样只有两种可能:巡检里多读/少读了东西,
|
||||
或者别的任务在同一圈里插了事务进来 —— 两种都该被这条拦住。
|
||||
(这也是为什么预算函数不是一个写死的常数:它按同一个算式从事务形状来。) */
|
||||
static void test_measured_bus_time_equals_the_static_budget(void)
|
||||
{
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT32(t_safety_budget_us(), snap.bus_us);
|
||||
}
|
||||
}
|
||||
|
||||
/* 一圈正好 13 次事务,而且每片器件被读的次数是钉死的 ——
|
||||
多读一次寄存器(比如顺手 check 一下 Calibration)会当场变红,
|
||||
然后写的人才会想起来去把预算那一行也改了。 */
|
||||
static void test_one_cycle_is_exactly_thirteen_transactions(void)
|
||||
{
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
const uint32_t before = hal_i2c_acct_txns();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT32(TXNS_PER_CYCLE, hal_i2c_acct_txns() - before);
|
||||
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
TEST_ASSERT_EQUAL_UINT32(2, sim_i2c_dev(rail_cfg(rail)->ina_addr)->n_read);
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(2, sim_i2c_dev(INA226_ADDR_INPUT)->n_read);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_i2c_dev(PCA9555_ADDR)->n_read);
|
||||
}
|
||||
|
||||
/* 五路读的是**表里的**地址。挂一片在「0x40+ch 算出来」的地址上,
|
||||
它一次也不该被碰到 —— 这是 A7 那条禁令在巡检路径上的回归拦网。 */
|
||||
static void test_rails_are_read_by_table_not_by_formula(void)
|
||||
{
|
||||
sim_i2c_dev_t *wrong = sim_i2c_add(0x46, 2); /* 谁都不是的地址 */
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, wrong->n_read);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, wrong->n_write);
|
||||
|
||||
/* 顺带确认值确实按路对上了(每路电流不同)。 */
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
TEST_ASSERT_TRUE(snap.rail_ok[rail - 1]);
|
||||
TEST_ASSERT_EQUAL_INT32(rail * 1000 * INA226_CURRENT_LSB_UA,
|
||||
snap.rail[rail - 1].current_ua);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 加锁纪律 ------------------------------------------------------------- */
|
||||
|
||||
/* 一整圈里的每一次事务都持着 I²C 锁;出了这个函数锁必须已经放掉。 */
|
||||
static void test_every_transaction_happens_under_the_i2c_lock(void)
|
||||
{
|
||||
sim_lock_require(HAL_LOCK_I2C, true);
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_lock_violations(HAL_LOCK_I2C));
|
||||
TEST_ASSERT_FALSE(hal_lock_held(HAL_LOCK_I2C)); /* 出门放锁 */
|
||||
TEST_ASSERT_EQUAL_INT(-1, sim_lock_owner(HAL_LOCK_I2C));
|
||||
}
|
||||
|
||||
/* 上一条不是句空话:不拿锁直接读一片,违规计数必须 +1。
|
||||
(检查本身失灵的话,上一条会永远绿。) */
|
||||
static void test_the_lock_check_itself_can_fail(void)
|
||||
{
|
||||
sim_lock_require(HAL_LOCK_I2C, true);
|
||||
ina226_vi_t vi;
|
||||
(void)ina226_read_vi(rail_cfg(1)->ina_addr, &vi);
|
||||
TEST_ASSERT_EQUAL_UINT32(2, sim_lock_violations(HAL_LOCK_I2C)); /* 两次事务 */
|
||||
}
|
||||
|
||||
/* 拿不到锁:**跳过这一圈**,不死等(t_safety.h 规矩 1)。
|
||||
等掉的是 5 ms,还在 20 ms 之内,所以节拍不丢。 */
|
||||
static void test_a_busy_bus_skips_the_cycle_instead_of_waiting_forever(void)
|
||||
{
|
||||
sim_lock_set_task(1); /* 假装 t_link 正在用总线 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_take(HAL_LOCK_I2C, 10));
|
||||
sim_lock_set_task(0);
|
||||
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
const uint32_t t0 = hal_time_now_ms();
|
||||
const uint32_t txns = hal_i2c_acct_txns();
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_TIMEOUT, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, snap.skipped);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, snap.cycles);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, hal_i2c_acct_txns() - txns); /* 一次事务也没发 */
|
||||
TEST_ASSERT_EQUAL_UINT32(T_SAFETY_LOCK_TIMEOUT_MS, hal_time_elapsed_ms(t0));
|
||||
TEST_ASSERT_TRUE(T_SAFETY_LOCK_TIMEOUT_MS < task_desc(TASK_SAFETY)->period_ms);
|
||||
}
|
||||
|
||||
/* ---- 器件出问题的时候(§10.2:失联 ≠ 断电) ------------------------------ */
|
||||
|
||||
/* 六片里坏一片,剩下五片照读、PCA 照读。中断这一圈的话,
|
||||
现象是「一片 INA226 掉线,五路判据全瞎」。 */
|
||||
static void test_one_dead_device_does_not_stop_the_round(void)
|
||||
{
|
||||
sim_i2c_set_present(rail_cfg(3)->ina_addr, false);
|
||||
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, t_safety_step(&snap));
|
||||
|
||||
TEST_ASSERT_FALSE(snap.rail_ok[2]);
|
||||
for (int rail = 1; rail <= N; rail++) {
|
||||
if (rail != 3) {
|
||||
TEST_ASSERT_TRUE(snap.rail_ok[rail - 1]);
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_TRUE(snap.input_ok);
|
||||
TEST_ASSERT_TRUE(snap.io_ok);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, snap.i2c_errors);
|
||||
}
|
||||
|
||||
/* 读失败时保留**上一圈的值**并把 ok 标成 false —— 不清零。
|
||||
清零的话,A10 的过压判据会看到一个 0 V 的「正常」电压,
|
||||
而 ok 标志正是让它先看一眼「这数是不是新的」。 */
|
||||
static void test_a_failed_read_keeps_the_stale_value_and_flags_it(void)
|
||||
{
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
const int32_t good_mv = snap.rail[0].bus_mv;
|
||||
TEST_ASSERT_EQUAL_INT32(3900, good_mv);
|
||||
|
||||
sim_i2c_set_present(rail_cfg(1)->ina_addr, false);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, t_safety_step(&snap));
|
||||
TEST_ASSERT_FALSE(snap.rail_ok[0]);
|
||||
TEST_ASSERT_EQUAL_INT32(good_mv, snap.rail[0].bus_mv); /* 还是上一圈那个 */
|
||||
}
|
||||
|
||||
/* 器件掉线之后节拍照样是 20 ms(NAK 的事务也占总线时间,但占得更少)。
|
||||
⚠️ 这条测的是「巡检不会因为总线出错就乱了节奏」。 */
|
||||
static void test_the_beat_survives_a_dead_device(void)
|
||||
{
|
||||
sim_i2c_set_present(rail_cfg(2)->ina_addr, false);
|
||||
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
sched_periodic_t tick;
|
||||
sched_periodic_init(&tick, 20);
|
||||
|
||||
uint32_t prev = hal_time_now_ms();
|
||||
for (int i = 0; i < 50; i++) {
|
||||
(void)t_safety_step(&snap);
|
||||
sched_periodic_wait(&tick);
|
||||
TEST_ASSERT_EQUAL_UINT32(20, hal_time_now_ms() - prev);
|
||||
prev = hal_time_now_ms();
|
||||
}
|
||||
TEST_ASSERT_EQUAL_UINT32(0, tick.overruns);
|
||||
TEST_ASSERT_EQUAL_UINT32(50, snap.i2c_errors);
|
||||
}
|
||||
|
||||
/* PCA9555 的输入被读进来了,而且语义判读能用(A10 要拿它做判据)。 */
|
||||
static void test_pca9555_inputs_land_in_the_snapshot(void)
|
||||
{
|
||||
sim_pca9555_drive(0, 0xFF & ~(1u << pca9555_flt_bit(4))); /* CH4 的 FLT# 拉低 */
|
||||
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
|
||||
TEST_ASSERT_TRUE(snap.io_ok);
|
||||
TEST_ASSERT_TRUE(pca9555_rail_fault(&snap.io, 4));
|
||||
TEST_ASSERT_FALSE(pca9555_rail_fault(&snap.io, 1));
|
||||
}
|
||||
|
||||
/* 第一圈的占用里不该混进启动期间的事务(游标要在 init 时对齐到「现在」)。 */
|
||||
static void test_startup_traffic_is_not_charged_to_the_first_cycle(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_probe(PCA9555_ADDR)); /* 假装启动扫了一下 */
|
||||
|
||||
safety_snap_t snap;
|
||||
t_safety_init(&snap);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&snap));
|
||||
TEST_ASSERT_EQUAL_UINT32(t_safety_budget_us(), snap.bus_us);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_safety_holds_a_20ms_beat_for_500_cycles);
|
||||
RUN_TEST(test_i2c_load_per_cycle_matches_section_10_3);
|
||||
RUN_TEST(test_measured_bus_time_equals_the_static_budget);
|
||||
RUN_TEST(test_one_cycle_is_exactly_thirteen_transactions);
|
||||
RUN_TEST(test_rails_are_read_by_table_not_by_formula);
|
||||
RUN_TEST(test_every_transaction_happens_under_the_i2c_lock);
|
||||
RUN_TEST(test_the_lock_check_itself_can_fail);
|
||||
RUN_TEST(test_a_busy_bus_skips_the_cycle_instead_of_waiting_forever);
|
||||
RUN_TEST(test_one_dead_device_does_not_stop_the_round);
|
||||
RUN_TEST(test_a_failed_read_keeps_the_stale_value_and_flags_it);
|
||||
RUN_TEST(test_the_beat_survives_a_dead_device);
|
||||
RUN_TEST(test_pca9555_inputs_land_in_the_snapshot);
|
||||
RUN_TEST(test_startup_traffic_is_not_charged_to_the_first_cycle);
|
||||
return UNITY_END();
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户