- 根因:IDF 5.4 的 i2c_master_transmit / receive / transmit_receive 在从机 NACK / SCL 超时时返回 ESP_ERR_INVALID_STATE,HAL 照翻成 HAL_ERR_STATE(= 没 init);命令层 from_hal 又把 STATE 当成 「执行器忙」⇒ PCA9555 不应答时 power off 回 busy(eFuse 其实已关、意图已清),I2C_LOST 错误码是 5 - hal_i2c_esp.c:传输函数用 map_xfer_err(INVALID_STATE → NAK);总线没建起来仍单独报 STATE - cmd_exec.c:新增 from_bus,set_voltage / vbus / power 的器件失败一律 io;from_hal 只留给执行器 - sim:sim_i2c_inject_err_ch() 可指定注入的错误码;test_cmd 加 STATE 注入 ⇒ 必须 io 不是 busy (改回 from_hal 时这段确实红)。26 个 ctest 全绿 - 板 B(台架镜像,CH1 强制在位,未提交):power off 回 io、CH1 关掉;I2C_LOST 错误码 5 → 2。现跑 0.1.1-c3 Co-Authored-By: Claude Opus 5.5 <[email protected]>
310 行
17 KiB
C
310 行
17 KiB
C
/* 假板子的控制面 —— 只有宿主机测试与 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_store.h"
|
||
#include "hal_time.h"
|
||
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
|
||
/* 各路 sim 全部恢复出厂:器件表清空、引脚未配置、PWM 未配置、时钟归零、
|
||
init 标志清掉,**掉电不丢区整块擦成 0xFF**。每个测试的 setUp() 里调它。
|
||
语义是「换了一块新板子」。 */
|
||
void sim_hal_reset(void);
|
||
|
||
/* **掉电重启**:除了掉电不丢区里的内容,其余与 `sim_hal_reset()` 完全一样。
|
||
§10.7 的验收「掉电重启后能读回」就是靠这两个的区别 ——
|
||
拿 reset 去测重启,测的是「刚擦干净的 flash 读不出东西」,永远绿。 */
|
||
void sim_hal_reboot(void);
|
||
|
||
/* ---------------- I²C ---------------- */
|
||
|
||
#define SIM_I2C_MAX_DEV 24 /* R2:主干 4 + 五片刀片各 2 = 14,留些余量 */
|
||
#define SIM_I2C_REGS 256
|
||
#define SIM_I2C_LOG_BYTES 8
|
||
#define SIM_I2C_MUX_CH 8 /* TCA9548A 的通道数 */
|
||
|
||
/* 假器件的窃听事件。通用寄存器表模拟不了「配成输出就对地短路」这种事,
|
||
所以按 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;
|
||
/* **R2**:-1 = 挂在主干上;0~7 = 挂在 mux 的那个通道后面。
|
||
通道关着的时候它看不见(事务一律 NAK),这正是「选错通道」能被测出来的原因。 */
|
||
int bus;
|
||
bool is_mux; /* 这颗就是 TCA9548A 自己:写它是**一个裸字节**,不带寄存器号 */
|
||
uint8_t reg_width; /* 1 或 2 字节。2 字节按**大端**——INA226 / DAC63204 都是 */
|
||
bool present; /* false = 器件失联(A15 降级矩阵用),事务一律 NAK */
|
||
int nak_left; /* >0:接下来 n 次事务 NAK 后自动恢复;<0:一直 NAK;0:正常 */
|
||
hal_err_t fail_err; /* 注入的失败返回什么;0 = HAL_ERR_NAK(sim_i2c_inject_err_ch) */
|
||
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;
|
||
|
||
/* 往假总线的**主干**上挂一个器件。同一条支路上地址重复会 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 */
|
||
|
||
/* 往 mux 的某个通道后面挂一个器件(**R2**:五片刀片可以用同一个地址)。 */
|
||
sim_i2c_dev_t *sim_i2c_add_ch(int ch, uint8_t addr, uint8_t reg_width);
|
||
sim_i2c_dev_t *sim_i2c_dev_ch(int ch, uint8_t addr);
|
||
|
||
/* 挂那颗假 TCA9548A。`rst_pin` 是 `MUX_RST#` 接的 GPIO(低有效)——
|
||
固件拉低它的那一刻,假器件的通道**全部关掉**,和真器件一样。 */
|
||
void sim_i2c_add_mux(uint8_t addr, int rst_pin);
|
||
|
||
int sim_i2c_mux_ch(void); /* 当前通道:-1 全关 / 0~7 / **-2 同时开了多路** */
|
||
uint8_t sim_i2c_mux_mask(void); /* 控制字原样 */
|
||
uint32_t sim_i2c_mux_resets(void);
|
||
|
||
/* 同一个地址在**当前可见范围**里出现了不止一次的次数。
|
||
真板上那是两片一起应答、读回来是线与(数值正常、还很稳),
|
||
所以这个计数必须恒为 0 —— 非 0 就是通道开重了。 */
|
||
uint32_t sim_i2c_ambiguous(void);
|
||
|
||
/* 预置 / 回读寄存器。地址没挂过会 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);
|
||
void sim_i2c_set_reg_ch(int ch, uint8_t addr, uint8_t reg, uint16_t val);
|
||
uint16_t sim_i2c_get_reg_ch(int ch, 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);
|
||
void sim_i2c_inject_nak_ch(int ch, uint8_t addr, int count);
|
||
void sim_i2c_set_present_ch(int ch, uint8_t addr, bool present);
|
||
/* 同 inject_nak_ch,但失败时返回 `err`。模拟 ESP 驱动把「不应答」报成别的码
|
||
(IDF 5.4 的 NACK 是 ESP_ERR_INVALID_STATE,HAL 修之前会变成 HAL_ERR_STATE)。 */
|
||
void sim_i2c_inject_err_ch(int ch, uint8_t addr, int count, hal_err_t err);
|
||
|
||
/* **假总线上的事务是要花时间的**(默认开):每次事务把虚拟时钟往前拨
|
||
`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);
|
||
|
||
/* 「上一条命锁住的脚」:模拟计划内重启之后、这一条命还没配之前,引脚被 hold 在 level。
|
||
(sim_hal_reset() 之后调,代表重启前后外部世界是连续的。) */
|
||
void sim_gpio_preheld(int pin, int level);
|
||
bool sim_gpio_held(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);
|
||
|
||
/* 该脚**最后一次 0→1** 时的虚拟时刻(µs),从没抬高过返回 0。
|
||
「五路上电错开 100 ms」(A15)要的是线上的时刻,不是被测代码自己报的时刻。 */
|
||
uint64_t sim_gpio_rise_us(int pin);
|
||
|
||
/* 每次 `hal_gpio_write()` 成功之后调一次(`sim_hal_reset()` 清掉)。
|
||
给测试扮演「外部世界对引脚的反应」用 —— 比如 eFuse 的 EN 一抬高,
|
||
那一路 PCA9555 上的 PGOOD 就跟着来(A15 的上电序列靠它才测得出「等 PG」)。 */
|
||
typedef void (*sim_gpio_hook_t)(int pin, int level);
|
||
void sim_gpio_set_hook(sim_gpio_hook_t fn);
|
||
|
||
/* ---------------- 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 ×5(按 datasheet 建模的假器件) ---------------- */
|
||
|
||
/* 通用寄存器表证明不了这一层的核心判据。这个模型只多做三件真器件会做的事:
|
||
① **Output Port 复位值 0xFF** —— 所以把一个脚配成输出,它下一刻就输出高;
|
||
② 输入寄存器按位合成:配成输入的位读**外部电平**,配成输出的位读**自己的输出**;
|
||
③ 备用脚 P0.5~7 / P1.4~7 **焊死在地上** —— 读永远是 0,被驱动成高就是短路。
|
||
③ 是这块板子上「配错方向」唯一会留下痕迹的地方:真板子上它既不报错也不 NAK。
|
||
|
||
**R2**:五片刀片各一颗,同地址、不同 mux 通道。`idx` 是 0~4,
|
||
与 `rail - 1` 对应 —— 但那是测试自己的约定,假器件只认 attach 的顺序。 */
|
||
#define SIM_PCA9555_MAX 5
|
||
|
||
void sim_pca9555_attach_ch(int idx, int ch, uint8_t addr);
|
||
|
||
/* 外部世界把某个端口的 8 根线驱动成 level(接地脚不受影响,恒 0)。
|
||
FLT#/PGOOD 都是开漏上拉,所以「什么都没发生」的静息值是 0xFF。 */
|
||
void sim_pca9555_drive(int idx, int port, uint8_t level);
|
||
|
||
uint8_t sim_pca9555_config(int idx, int port); /* 方向寄存器现值,1 = 输入 */
|
||
uint8_t sim_pca9555_output(int idx, int port); /* 输出锁存器现值(复位 0xFF) */
|
||
uint8_t sim_pca9555_polinv(int idx, int port);
|
||
|
||
/* 某个输出脚**在栅极上的实际电平**:配成输入时返回 -1(高阻),
|
||
配成输出时返回锁存器里那一位。断言「键真的被按下了」用这个,
|
||
不要去读驱动自己的影子(那是自己证明自己)。 */
|
||
int sim_pca9555_pin(int idx, int port, int bit);
|
||
|
||
/* 接地脚被配成输出过几次(不管输出高低,配了就算 —— 那已经是在对地灌电流)。
|
||
**五片合计**:这个数必须恒为 0,是哪一片犯的都一样严重。 */
|
||
uint32_t sim_pca9555_gnd_driven(void);
|
||
/* 其中真的输出了高电平、也就是**直接对地短路**过几次。 */
|
||
uint32_t sim_pca9555_shorted(void);
|
||
|
||
/* ⭐ 「方向先于输出」那条规矩的拦网:某一片在**输出锁存器还是 0xFF 的时候**
|
||
就把 Port0 配成了输出的次数。真板上那一刻五路的键全被按下 + VBUS 全部上电。
|
||
必须恒为 0。 */
|
||
uint32_t sim_pca9555_unsafe_enable(void);
|
||
|
||
/* INT# 现在是不是被拉低(低有效)。输入变化拉低,**读走输入端口才释放**。 */
|
||
bool sim_pca9555_int_asserted(int idx);
|
||
uint32_t sim_pca9555_int_asserts(int idx); /* 累计拉低次数 */
|
||
|
||
/* ---------------- PWM ---------------- */
|
||
|
||
int sim_pwm_pin(int ch); /* 未配置返回 -1 */
|
||
uint32_t sim_pwm_freq(int ch); /* 未配置返回 0 */
|
||
int sim_pwm_permille(int ch); /* 未配置返回 -1 */
|
||
|
||
/* ---------------- 假风扇 ×2(A16 / TR-6 的 TACH 采样窗口) ---------------- */
|
||
|
||
/* 按板子的接法建模的假风扇:**一路 PWM 喂两只**、一个 12V 总开关、两路独立测速。
|
||
它比「一个转速变量」多做四件真板子会做的事,每一件都是一条判据的靠山:
|
||
① `FAN_EN` 是低电平就**一圈都不转**(复位态就是这样,固件不主动开就没风);
|
||
② PWM 反相(板上 Q6 低边管):GPIO 占空比 d ⇒ 风扇侧 `1000 − d`
|
||
—— 固件把反相写反时,这里的转速会随温度**下降**;
|
||
③ **可选的「关断期无脉冲」**(`sim_fan_set_gated`,三线低边调速的行为):
|
||
占空比 40 % 就只有 40 % 的时间在吐脉冲。TR-6 那条误报全靠它才摆得出来;
|
||
④ 0 % PWM 仍转 `rpm_min`(设计文档 §22.6:四线风扇 0 % 下仍 3300 RPM)。
|
||
|
||
边沿数是按**虚拟时钟**积分出来的,积分在读 `sim_fan_edges()/sim_fan_rpm()`、
|
||
写 `hal_pwm_set_permille()`、以及 `sim_fan_set_*()` 时结算。
|
||
⚠️ 改 `FAN_EN` 引脚之前请先读一次 `sim_fan_edges()`:模型不监视 GPIO 的写,
|
||
那一段时间会整段按新的 EN 状态算。 */
|
||
#define SIM_FAN_MAX 2
|
||
|
||
void sim_fan_attach(int idx, int pwm_ch, int en_pin,
|
||
uint32_t rpm_full, uint32_t rpm_min, uint8_t ppr);
|
||
|
||
void sim_fan_set_gated(int idx, bool on); /* 默认 true = 关断期不吐脉冲 */
|
||
void sim_fan_set_stalled(int idx, bool on); /* 堵转 / 拔线:一个脉冲也没有 */
|
||
|
||
uint32_t sim_fan_edges(int idx); /* 累计 TACH 边沿数(没 attach 返回 0) */
|
||
uint32_t sim_fan_rpm(int idx); /* 此刻的真实转速,给测试自查用 */
|
||
|
||
/* ---------------- 两把互斥锁 ---------------- */
|
||
|
||
/* 扮演第几号任务(默认 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 各路自己调,测试一般用不到 */
|
||
|
||
/* ---------------- 掉电不丢区(NOR flash 那块) ---------------- */
|
||
|
||
/* 假 flash 的几何。与 partitions.csv 里 `evlog` 分区的大小是同一个数,
|
||
`test_evlog.c` 拿 `EVLOG_SECTORS` 把两边钉在一起。 */
|
||
#define SIM_STORE_SECTOR_BYTES 4096
|
||
#define SIM_STORE_SECTORS 17
|
||
|
||
/* 故障注入:第 nth 次写整条失败(一个字节都不落)。0 = 清除。 */
|
||
void sim_store_fail_next_write(int nth);
|
||
|
||
/* **掉电打断一次写**:下一次写只落前 bytes 个字节,然后返回 HAL_ERR_BUS。
|
||
这是每条记录都要带校验字节的理由 —— 半条记录在 flash 里长得和一条真记录一样。 */
|
||
void sim_store_tear_next_write(uint32_t bytes);
|
||
|
||
uint32_t sim_store_writes(void); /* 成功或半成功的写事务次数 */
|
||
uint32_t sim_store_erases(void); /* 擦过几个扇区 —— 寿命就花在这上面 */
|
||
uint32_t sim_store_bytes(void); /* 真正落进去的字节数 */
|
||
uint8_t sim_store_byte(uint32_t off); /* 越界返回 0xFF */
|
||
|
||
/* ---------------- 临界区(不是第三把锁,见 hal_lock.h) ---------------- */
|
||
|
||
int sim_crit_depth(void); /* 当前嵌套层数。测试结束时必须是 0 */
|
||
uint32_t sim_crit_enters(void); /* 进过几次 —— 断言「入队真的是在临界区里做的」*/
|
||
|
||
/* 在临界区里干了会阻塞的事(等锁 / 睡)的次数。**必须恒为 0**。 */
|
||
uint32_t sim_crit_blocking(void);
|
||
void sim_crit_blocking_note(void); /* 给 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 */
|