A6:PCA9555 驱动 + 「接地脚永远是输入」的硬约束
U23 @ 0x20,16 个脚全部是输入:5×EFUSE_FLT# + 5×EFUSE_PGOOD + BUCK_PG + INA_ALERT + 4 个焊死在地上的备用脚(P0_7 / P1_5~7)。位表与地址从 .claude/pcb1-parts.json 的真实焊盘网络逐脚核回来,与设计文档 §17.2 一致。 驱动根本不提供「把某个脚配成输出」的接口(和 sr595.h 不提供「抬 OE#」同一条 规矩),方向字一律过 pca9555_dir_safe(),不安全直接 abort。原因是 PCA9555 的 Output Port 寄存器复位值是 0xFF —— 把接地脚配成输出的那一刻它就输出高电平, 而真器件既不报错也不 NAK,只是发烫。 三种极性(FLT# 低有效 / PGOOD 高有效 / INA_ALERT 低有效)各自只在一个函数里 出现一次;上层问「有没有故障」,不许自己去 & 位。 sim 侧新增按 datasheet 建模的假 PCA9555:Output 复位 0xFF、输入按位合成、 接地脚被驱动时记短路、INT# 只被「读输入端口」清除。为此给假 I²C 总线加了一个 窃听钩子(POST_WRITE / PRE_READ)—— 通用寄存器表模拟不了「配成输出就对地 短路」这类事,以后再有这种器件照这个挂。 验收:8 条 ctest(81 用例)全绿;idf.py build 通过,pca9555.c.obj 在。 12 个「一定会被写出来的错」逐个注进去跑过,全部变红。 Co-Authored-By: Claude Opus 5 <[email protected]>
这个提交包含在:
+57
-57
@@ -1,76 +1,76 @@
|
||||
# 交接
|
||||
更新:2026-09-06 完成任务包 **A5**(74HC595 24 位链 + 影子寄存器单一写入点)
|
||||
更新:2026-09-07 完成任务包 **A6**(PCA9555 驱动)
|
||||
|
||||
## 一句话现状
|
||||
15 路侧键 + 5 路 `EDL_EN` + `HUB_RST` 全在一条 24 位链上,内存里只有一份影子,
|
||||
唯一写入点是 `sr595_flush()`。位表**从 PCB 的真实焊盘网络逐脚核回来**(不是照文档抄),
|
||||
上电顺序按「先锁存安全值、再使能 `OE#`」写死。7 条 ctest(61 个用例)全绿,
|
||||
`idf.py build` 通过。
|
||||
板上唯一的 I/O 扩展器(U23 @ 0x20)**16 个脚全部是输入**:5×FLT# + 5×PGOOD +
|
||||
BUCK_PG + INA_ALERT + 4 个焊死在地上的备用脚。驱动**根本不提供「配成输出」的接口**,
|
||||
方向字一律过 `pca9555_dir_safe()`。位表与地址从 PCB 焊盘网络逐脚核回来(与文档一致)。
|
||||
8 条 ctest(81 个用例)全绿,`idf.py build` 通过。
|
||||
|
||||
## 下一步
|
||||
**A6**:PCA9555 驱动(需求 §10.4 末段)。验收断言:**方向寄存器永远不会把
|
||||
Port0[7] / Port1[7:5] 配成输出**(配错直接 abort)—— 那 4 个备用脚在板上**直接接地**
|
||||
(设计文档 §17.2 的注),配成输出并输出高就是对地短路。
|
||||
A3/A4/A5/A6 四个驱动包彼此独立,做完 A6 就能进 A7(通道配置表)。
|
||||
**A7**:通道配置表 + 表驱动遍历(需求 §10.4 全节)。四个驱动包(A3/A4/A5/A6)已全部完成,
|
||||
A7 的活是把 `rail_cfg_t` 那张静态表建起来,并在启动自检里**一起**调上四个自检谓词:
|
||||
`ina226_addr_table_ok()` / `dac63204_map_ok()` / `sr595_map_ok()` / `pca9555_map_ok()`
|
||||
(前三个至今没有任何调用点,A7 是它们的归宿)。
|
||||
|
||||
## 本次新增/修改的文件
|
||||
- `components/drv/include/sr595.h` — 24 位链的位表、引脚、影子契约(唯一对外契约)
|
||||
- `components/drv/sr595.c` — 驱动本体(≈200 行,只经 `hal_gpio_*`)
|
||||
- `components/phonerail_hal/sim/sim_sr595.c` — **按 datasheet 建模的假移位链**
|
||||
(SRCLK 上升沿移一格、RCLK 上升沿锁存),加上 `sim_hal.h` 的一节声明
|
||||
- `components/phonerail_hal/sim/hal_gpio_sim.c` — 加了一个窃听钩子给上面那个模型;
|
||||
`hal_gpio_config(OUTPUT)` 也会触发(真芯片配成输出就开始驱动,`OE#` 靠这一条)
|
||||
- `test/test_sr595.c`(15 个用例)
|
||||
- 三处 CMakeLists + `main/phonerail.h`(版本 `0.1.0-a5`)
|
||||
- `../需求文档.md` §10.6、`../五路直供电源板设计.md` §17.2 — 记落地 + **修掉过期的位表**
|
||||
(§8.2 那行过期的 DAC 通道分配也顺手改了)
|
||||
- `components/drv/include/pca9555.h` — 位表、三种极性、接地脚掩码(唯一对外契约)
|
||||
- `components/drv/pca9555.c` — 驱动本体(≈195 行,只经 `hal_i2c_*`)
|
||||
- `components/phonerail_hal/sim/sim_pca9555.c` — **按 datasheet 建模的假器件**
|
||||
(Output 复位 0xFF、输入按位合成、接地脚短路检测、INT# 只被读输入端口清)
|
||||
- `components/phonerail_hal/sim/hal_i2c_sim.c` + `sim_hal.h` — 假 I²C 加了一个
|
||||
**窃听钩子**(`SIM_I2C_EV_POST_WRITE` / `PRE_READ`),通用寄存器表模拟不了「配成
|
||||
输出就对地短路」这种事;以后再有这类器件照这个挂
|
||||
- `test/test_pca9555.c`(20 个用例)
|
||||
- 三处 CMakeLists + `sim_hal.c` 的 reset 汇总 + `main/phonerail.h`(版本 `0.1.0-a6`)
|
||||
- `../需求文档.md` §10.4 — 记 A6 落地 + 四条实现约定
|
||||
|
||||
## 验收命令与结果
|
||||
```
|
||||
$ bash host/test.sh
|
||||
100% tests passed, 0 tests failed out of 7 # +test_sr595(15)
|
||||
100% tests passed, 0 tests failed out of 8 # +test_pca9555(20)
|
||||
|
||||
$ .\build_idf.ps1 # PowerShell
|
||||
phonerail.bin binary size 0x32550 bytes. ... 0xcdab0 bytes (80%) free.
|
||||
Project build complete. # sr595.c.obj 在;bin 与 A4 同样大是正常的(还没人调它)
|
||||
Project build complete. # pca9555.c.obj 在(已单独确认)
|
||||
|
||||
# 四个「一定会被写出来的错」逐个改进去跑过,确认测试拦得住:
|
||||
# 字节顺序发反(U21 先发) -> test_byte_order 红
|
||||
# 先使能 OE# 再锁存安全值 -> test_outputs_are_enabled_only_after_a_safe_latch 红
|
||||
# 安全影子写成 0(HUB_RST 摁死) -> 三条红,其中一条直接说「上电后 Hub 不能被摁在复位里」
|
||||
# 位表改成 3(rail−1)+key 的算式 -> sr595_modify 的 assert 当场 abort(碰到空脚 bit4)
|
||||
# 12 个「一定会被写出来的错」逐个注进去跑过,**全部变红**(一次性脚本,没入库;做法见下面第 4、5 条坑):
|
||||
# FLT#/PGOOD/INA_ALERT/BUCK_PG 四处极性各写反 -> 3~4 条红
|
||||
# FLT 去读 Port1(端口搞反) -> 只有 test_one_rail_at_a_time 红
|
||||
# 方向写成 0x7F(P0_7 配成输出) -> 4 条红,含 sim 的短路计数
|
||||
# 忘了清极性反转 / 写了 Output Port -> 各 1~2 条红
|
||||
# dir_safe 写成近似判据 dir != 0 -> 1 条红
|
||||
# 读输入拆成两次事务 -> 1 条红(事务计数)
|
||||
# 读失败时把调用方缓冲清零 -> 1 条红
|
||||
```
|
||||
|
||||
## 踩到的坑(只写会再犯的)
|
||||
- **位表不能照 §17.2 抄** —— 那张表是 09-04 的两片版,§26.18 重排之后 15 路里
|
||||
**12 路对不上**。这次是从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核出来的
|
||||
(顺手把 §17.2 和 §8.2 里过期的两张表都改了)。和 A4 的 DAC 通道分配同一类坑。
|
||||
- ⚠️ **CH4 的三个键碰巧就是 bit 9/10/11**,和「顺排公式 `3(rail−1)+key`」一模一样。
|
||||
所以「断言位表不是公式」只能**整体**断言(15 位里只有 3 位重合),
|
||||
逐路写 `NOT_EQUAL` 到 CH4 就自己红了 —— 第一版正是这么写的。
|
||||
- **`HUB_RST` 是低有效**(USB2517I 的 `RESET_N`,§21.7)。全 0 的上电影子会把 Hub
|
||||
一直摁在复位里,而五路手机的电压电流全都正常 —— 看起来跟固件一点关系都没有。
|
||||
- **上电顺序不能换**:三片 `MR#` 接死在 +3V3,没有复位线 ⇒ 上电时移位寄存器内容
|
||||
不确定。所以 `KEY_OE#` 在安全值锁存完之前**根本不配成输出**(ESP-IDF 一配成输出
|
||||
就开始驱动,输出寄存器复位值 0,等于当场使能)。板上 R609 100 kΩ 上拉扛着这一段。
|
||||
- **假移位链值得写**:A5 的两条验收断言如果只对着驱动自己的影子变量断言,那是自己
|
||||
证明自己 —— 字节顺序发反、每帧多打一个时钟、RCLK 打早了,影子变量一律看不出来。
|
||||
`sim_sr595_*` 按 datasheet 建模,attach 时把移位寄存器塞成 `0xA5A5A5`(真板子上电
|
||||
就是不确定值),四个注错进去的 bug 全被它照出来了。
|
||||
- **cygwin 的偶发 `Subprocess aborted` 又来了一次**:这次是三条测试同时在 0.03 s 里挂,
|
||||
其中两条根本不碰 595。重跑就正常。**别去查代码。**
|
||||
- ⚠️ **PCA9555 的 Output Port 寄存器复位值是 0xFF。** 所以把一个接地脚配成输出的
|
||||
那一刻它就输出高电平,中间没有「先写个 0 再说」的余地 —— 这比「配错方向」本身
|
||||
更急。和 A5 那条「ESP-IDF 一配成输出就开始驱动」是同一类坑,只是这里默认值是**高**。
|
||||
- **FLT 与 PG 这次确实是顺排的**(`bit = rail - 1`),和 INA226 地址、595 位号那两张表
|
||||
不一样。所以这一片最容易写出来的错不是算式,而是**把 Port0 / Port1 搞反**:
|
||||
两个端口的 bit0~4 长得一模一样,**静息态两个端口读回来都是全 1**,
|
||||
单看一帧完全看不出来。注错实验证实:搞反之后 20 条测试里只有
|
||||
`test_one_rail_at_a_time` 一条红 —— 那条是专门为它写的,别删。
|
||||
- **极性反转寄存器要无条件清零,不能赌复位值**:看门狗只复位 ESP32,PCA9555 不掉电。
|
||||
- **`-Werror` 会替测试「假装」拦住错**:注错实验里「读输入拆成两次事务」第一版是
|
||||
编译期红(`reg` 变量没人用),根本没跑到断言。补了一版能编过的才算数。
|
||||
以后做注错实验,**编译期红的那一条一定要重写成能编过的**。
|
||||
- **注错脚本必须 try/finally 恢复原文**:第一版崩在 `subprocess` 吃不下
|
||||
`/c/cygwin64/...` 这种 POSIX 路径(Windows Python 要 `C:\cygwin64\...`),
|
||||
异常直接把注错留在了源文件里。(同批小坑:长测试文件用 Bash heredoc 写会
|
||||
`unexpected EOF`,改用 Write 工具。)
|
||||
|
||||
## 这个驱动 / 这一层的几条约定(写 A9 / A12 之前看一眼)
|
||||
- **除 `sr595.c` 外,任何地方都不许自己去打 SER/SRCLK/RCLK。** 改按键或 EDL 一律是
|
||||
「`sr595_modify()` 改影子 → `sr595_flush()`」。多处直接操作移位链的症状是
|
||||
**另一台手机的按键被顺手清掉**,而且要两个动作正好撞在一起才复现。
|
||||
- **组合键必须 `sr595_write(mask, value)` 一次给两位**(VOL− + PWR 落在同一帧)。
|
||||
分两次写会先单按一个键。`RCLK` 之前输出不变,所以整帧是原子的。
|
||||
- **不许拿 `OE#` 当「临时全松开」用**(需求 §10.6):那是复位路径不是控制路径,
|
||||
所以 `sr595.h` 根本没提供抬 `OE#` 的接口。
|
||||
- **rail 是 1~5**,`sr595_key_bit()` / `sr595_edl_bit()` 越界 abort;
|
||||
`sr595_modify()` 的 mask 碰到三个空脚(bit 4/8/12)也 abort ——
|
||||
悄悄忽略的话那一位永远是 0,而「按键没反应」要到接上真手机才发现。
|
||||
- **`sr595_map_ok()` 还没人调**:A7 的启动自检要把它和 `dac63204_map_ok()` 一起调上。
|
||||
- **按键的保持时长(100 ms / 2 s / 10 s)与序列互斥还没做**,那是 A9 的活。
|
||||
A5 只保证「写下去的那一帧是对的」。
|
||||
## 这个驱动 / 这一层的几条约定(写 A10 之前看一眼)
|
||||
- **三种极性各只出现一次**(`pca9555_rail_fault` / `_rail_pg` / `_buck_pg` / `_ina_alert`)。
|
||||
上层一律问「有没有故障」,**不许自己去 & 位** —— 和 `sr595_set_hub_reset()` 里那个
|
||||
反相、`rail_mv_to_code()` 里那个减法同一个道理:多一处就多一处写反的机会。
|
||||
- **`IOEXP_INT#`(IO38)是加速路径不是主路径**:PCA9555 要**读走输入端口**才释放 INT,
|
||||
而且输入变回原值时它也会释放。只靠中断的话,漏掉一次读就再也等不到下一个沿。
|
||||
所以 `t_safety` 的 20 ms 轮询是无条件的。A6 只留了引脚宏,读它是 A8/A10 的活。
|
||||
- **这一片不应答不许关输出**(§10.10):只是失去 FAULT / PG 可见性,改用 INA226 的
|
||||
电压判据兜底并持续告警。驱动的读失败一律只返回错误码,不做任何动作。
|
||||
- ⚠️ **「全 0」不是安全默认值**:它和「五路真的全炸了」读起来一模一样
|
||||
(五路全故障 + 五路全不良 + Buck 不良 + 告警)。上拉没焊、总线被拉低都是这个样子,
|
||||
所以 A10 判故障之前要先确认这一帧是真读回来的,不是失败后的残留缓冲。
|
||||
|
||||
@@ -6,5 +6,6 @@ idf_component_register(
|
||||
SRCS "ina226.c"
|
||||
"dac63204.c"
|
||||
"sr595.c"
|
||||
"pca9555.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES phonerail_hal)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/* PCA9555(U23,地址 0x20)—— 板上唯一的 I/O 扩展器,**16 个脚全部是输入**。
|
||||
|
||||
挂的是五路 eFuse 的 FLT#/PGOOD、Buck 的线或 PWRGD、六片 INA226 的线或 ALERT,
|
||||
共 12 个信号 + 4 个直接接地的备用脚(设计文档 §17.2)。
|
||||
|
||||
⚠️ **本驱动不提供「把某个脚配成输出」的接口,一个都不提供。**
|
||||
四个备用脚 P0_7 / P1_5~7 在板上**直接接地、不串限流电阻**(§17.2 的注,
|
||||
图纸上已标)。而 PCA9555 的 Output Port 寄存器**复位值是 0xFF**——
|
||||
所以只要把其中一个配成输出,它下一刻就输出高电平,**直接对地短路**。
|
||||
配错既不会报错也不会 NAK,只会发烫。所以方向字一律走 pca9555_dir_safe(),
|
||||
不安全直接 abort。这跟 sr595.h 不提供「抬 OE#」是同一条规矩。
|
||||
|
||||
§10.10 的降级行为:这片不应答 = 失去 FAULT / PG 可见性,**不关输出**,
|
||||
改用 INA226 的电压判据兜底并持续告警。所以本驱动的读失败一律只是返回错误码,
|
||||
不做任何动作。
|
||||
|
||||
本驱动只经 hal_i2c_*,宿主机上由 sim 的假 PCA9555(`sim_pca9555_*`)全部测完。 */
|
||||
#ifndef PCA9555_H
|
||||
#define PCA9555_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hal_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- 地址与引脚 ---------------------------------------------------------- */
|
||||
|
||||
/* A0 = A1 = A2 = GND(U23 的 pin21 / pin2 / pin3,已按 PCB 焊盘网络核过)。 */
|
||||
#define PCA9555_ADDR 0x20
|
||||
|
||||
/* `IOEXP_INT#`:开漏、低有效、板上 R608 10 kΩ 上拉,直连 ESP32 IO38(§8.1)。
|
||||
⚠️ **中断只是加速路径,不是主路径**:§10.3 的 `t_safety` 每 20 ms 无条件轮询
|
||||
读一次两个输入端口。原因是 PCA9555 的 INT 要**读走输入端口才会释放**,而且
|
||||
输入自己变回原值时它也会释放 —— 只靠中断的话,漏掉一次读就再也等不到下一个沿。
|
||||
A6 只留这个宏,读它是 A8/A10 的活。 */
|
||||
#define PCA9555_PIN_INT_N 38
|
||||
|
||||
/* ---- 寄存器(全部 8 位) ------------------------------------------------- */
|
||||
|
||||
#define PCA9555_REG_INPUT0 0x00
|
||||
#define PCA9555_REG_INPUT1 0x01
|
||||
#define PCA9555_REG_OUTPUT0 0x02
|
||||
#define PCA9555_REG_OUTPUT1 0x03
|
||||
#define PCA9555_REG_POLINV0 0x04
|
||||
#define PCA9555_REG_POLINV1 0x05
|
||||
#define PCA9555_REG_CONFIG0 0x06
|
||||
#define PCA9555_REG_CONFIG1 0x07
|
||||
|
||||
/* 方向寄存器:**1 = 输入**(复位值 0xFF)。本板全输入,所以只会写这一个值。 */
|
||||
#define PCA9555_DIR_ALL_INPUT 0xFFu
|
||||
|
||||
/* 极性反转寄存器必须是 0。写成非 0 的现象是「五路同时报故障」或者更糟的
|
||||
「五路故障全都看不见」,而 I²C 一切正常 —— init 会写 0 并回读校验。 */
|
||||
#define PCA9555_POLINV_NONE 0x00u
|
||||
|
||||
/* ---- 位分配(从 `.claude/pcb1-parts.json` 的真实焊盘网络逐脚核对) -------
|
||||
|
||||
Port0 bit0~4 = EFUSE_FLT1~5 bit5 = BUCK_PG bit6 = INA_ALERT bit7 = 接地
|
||||
Port1 bit0~4 = EFUSE_PG1~5 bit5~7 = 接地
|
||||
|
||||
这一次两张表**确实是顺排的**(`bit = rail - 1`),和 INA226 地址、595 位号
|
||||
那两张表不一样。所以这里最容易写出来的错不是「算式」,而是
|
||||
**把 FLT 和 PG 的端口搞反** —— 两个端口的 bit0~4 长得一模一样,
|
||||
搞反之后「有故障」会被读成「电源良好」,方向正好相反。
|
||||
所以取位号一律走下面两个函数,不要在别处写 `1 << (rail - 1)`。 */
|
||||
|
||||
#define PCA9555_RAIL_COUNT 5
|
||||
|
||||
#define PCA9555_BIT_BUCK_PG 5 /* Port0,五路 Buck 的 PWRGD 线或(都是开漏) */
|
||||
#define PCA9555_BIT_INA_ALERT 6 /* Port0,六片 INA226 的 ALERT 线或 */
|
||||
|
||||
/* 接地的备用脚。**永远、永远是输入。** */
|
||||
#define PCA9555_P0_GND_MASK 0x80u /* P0_7 */
|
||||
#define PCA9555_P1_GND_MASK 0xE0u /* P1_5 P1_6 P1_7 */
|
||||
|
||||
/* rail 是 **1~5**,越界 abort。返回的是**位号**(0~4),不是掩码。 */
|
||||
int pca9555_flt_bit(int rail); /* Port0 */
|
||||
int pca9555_pg_bit(int rail); /* Port1 */
|
||||
|
||||
/* 12 个信号位互不重复、都没落到接地脚上、FLT 与 PG 分属两个端口。
|
||||
启动自检调一次(A7,和 ina226_addr_table_ok() / sr595_map_ok() 一起)。 */
|
||||
bool pca9555_map_ok(void);
|
||||
|
||||
/* 0x20 不与 INA226(0x40/41/44/45/42/43) / DAC63204(0x4A/0x4B) / LM75(0x48) 撞车。 */
|
||||
bool pca9555_addr_ok(void);
|
||||
|
||||
/* dir 里 1 = 输入。返回 false = 这个方向字会把某个**接地脚**配成输出。
|
||||
驱动内部走到 false 直接 abort:悄悄放过去的后果是板子发烫,不是功能不好用。 */
|
||||
bool pca9555_dir_safe(int port, uint8_t dir);
|
||||
|
||||
/* ---- 采样 ---------------------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
uint8_t port0;
|
||||
uint8_t port1;
|
||||
} pca9555_in_t;
|
||||
|
||||
/* 认件:读回两个方向寄存器,全 0xFF(复位值)才算是它。
|
||||
不应答 → HAL_ERR_NAK;应答了但方向寄存器不是复位值 → HAL_ERR_BUS
|
||||
(这个地址上有东西,但不是一片刚上电的 PCA9555)。 */
|
||||
hal_err_t pca9555_probe(void);
|
||||
|
||||
/* 写方向(全输入)与极性反转(全 0)并**逐个回读校验**,回读不一致 → HAL_ERR_BUS。
|
||||
⚠️ Output Port 一个字节都不写:写它没有意义(没有输出脚),而写错方向的
|
||||
后果见文件头。 */
|
||||
hal_err_t pca9555_init(void);
|
||||
|
||||
/* 巡检主路径:**一次事务**读两个输入端口(§10.3 的 0.12 ms 预算就是按一次算的;
|
||||
PCA9555 的读指针在 0x00 / 0x01 之间 toggle,这两个字节是 datasheet 保证的)。 */
|
||||
hal_err_t pca9555_read_inputs(pca9555_in_t *out);
|
||||
|
||||
/* ---- 语义判读 ------------------------------------------------------------
|
||||
四个信号**三种极性**,这是本文件第二个容易写反的地方。极性只在下面四个函数里
|
||||
各出现一次(和 sr595_set_hub_reset() 里那个反相同一个道理:多一处就多一处写反
|
||||
的机会)。上层一律问「有没有故障」,不许自己去 & 位。 */
|
||||
|
||||
/* TPS25940A 的 `FLT#`:开漏、板上 R113 47 kΩ 上拉 ⇒ **低 = 有故障**。 */
|
||||
bool pca9555_rail_fault(const pca9555_in_t *in, int rail);
|
||||
|
||||
/* TPS25940A 的 `PGOOD`:开漏、R115 47 kΩ 上拉,输出 2.97 V 以上才释放
|
||||
⇒ **高 = 电源良好**。 */
|
||||
bool pca9555_rail_pg(const pca9555_in_t *in, int rail);
|
||||
|
||||
/* 五路 Buck 的 `PWRGD` 线或(开漏)⇒ **高 = 五路都正常**;低只说明「有某一路
|
||||
不正常」,看不出是哪一路(§16.1)。 */
|
||||
bool pca9555_buck_pg(const pca9555_in_t *in);
|
||||
|
||||
/* 六片 INA226 的 `ALERT` 线或:开漏、低有效 ⇒ **低 = 有片子在告警**。 */
|
||||
bool pca9555_ina_alert(const pca9555_in_t *in);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PCA9555_H */
|
||||
@@ -0,0 +1,194 @@
|
||||
/* PCA9555 驱动。只经 hal_i2c_*,不碰任何平台头文件。 */
|
||||
#include "pca9555.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "hal_i2c.h"
|
||||
|
||||
/* 位表见 pca9555.h。这两张表**这次确实是顺排的**,但仍然写成表:
|
||||
哪天硬件改一根线,改这里一处就够,而不是满工程找 `1 << (rail - 1)`。 */
|
||||
static const uint8_t s_flt_bit[PCA9555_RAIL_COUNT] = { 0, 1, 2, 3, 4 }; /* Port0 */
|
||||
static const uint8_t s_pg_bit[PCA9555_RAIL_COUNT] = { 0, 1, 2, 3, 4 }; /* Port1 */
|
||||
|
||||
int pca9555_flt_bit(int rail)
|
||||
{
|
||||
assert(rail >= 1 && rail <= PCA9555_RAIL_COUNT); /* 越界是调用方算错了,不许悄悄返回 */
|
||||
return s_flt_bit[rail - 1];
|
||||
}
|
||||
|
||||
int pca9555_pg_bit(int rail)
|
||||
{
|
||||
assert(rail >= 1 && rail <= PCA9555_RAIL_COUNT);
|
||||
return s_pg_bit[rail - 1];
|
||||
}
|
||||
|
||||
bool pca9555_map_ok(void)
|
||||
{
|
||||
uint8_t p0 = 0, p1 = 0;
|
||||
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
const uint8_t f = (uint8_t)(1u << s_flt_bit[rail - 1]);
|
||||
const uint8_t g = (uint8_t)(1u << s_pg_bit[rail - 1]);
|
||||
if ((p0 & f) != 0 || (p1 & g) != 0) {
|
||||
return false; /* 两路占了同一位 */
|
||||
}
|
||||
p0 |= f;
|
||||
p1 |= g;
|
||||
}
|
||||
|
||||
const uint8_t buck = (uint8_t)(1u << PCA9555_BIT_BUCK_PG);
|
||||
const uint8_t alrt = (uint8_t)(1u << PCA9555_BIT_INA_ALERT);
|
||||
if ((p0 & buck) != 0 || (p0 & alrt) != 0 || buck == alrt) {
|
||||
return false; /* 线或的两根撞到某路 FLT 上,或者撞到一起 */
|
||||
}
|
||||
p0 |= (uint8_t)(buck | alrt);
|
||||
|
||||
/* 一个信号都不许落到接地脚上 —— 落上去就等于永远读到 0(FLT 永远报故障,
|
||||
PG 永远报「不良」),而 I²C 一切正常。 */
|
||||
if ((p0 & PCA9555_P0_GND_MASK) != 0 || (p1 & PCA9555_P1_GND_MASK) != 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pca9555_addr_ok(void)
|
||||
{
|
||||
/* 同一条 I²C 上的其它器件(§8.2)。 */
|
||||
static const uint8_t others[] = { 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x48, 0x4A, 0x4B };
|
||||
|
||||
if (PCA9555_ADDR > 0x7f) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < sizeof(others); i++) {
|
||||
if (others[i] == PCA9555_ADDR) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pca9555_dir_safe(int port, uint8_t dir)
|
||||
{
|
||||
if (port != 0 && port != 1) {
|
||||
return false;
|
||||
}
|
||||
/* 1 = 输入。接地脚对应的位必须是 1,一位都不能少。 */
|
||||
const uint8_t gnd = (port == 0) ? PCA9555_P0_GND_MASK : PCA9555_P1_GND_MASK;
|
||||
return (dir & gnd) == gnd;
|
||||
}
|
||||
|
||||
/* ---- 寄存器读写 ---------------------------------------------------------
|
||||
PCA9555 的寄存器是 8 位的,命令字节在**成对**的寄存器之间 toggle
|
||||
(0/1、2/3、4/5、6/7),不是无限自增。本驱动只在一对之内连读 2 字节,
|
||||
落在 datasheet 保证的范围里;跨对连读(比如从 0x06 读 4 字节)行为不同,
|
||||
别那么写。 */
|
||||
|
||||
static hal_err_t read_reg(uint8_t reg, uint8_t *val)
|
||||
{
|
||||
return hal_i2c_write_read(PCA9555_ADDR, ®, 1, val, 1);
|
||||
}
|
||||
|
||||
static hal_err_t write_reg(uint8_t reg, uint8_t val)
|
||||
{
|
||||
const uint8_t wr[2] = { reg, val };
|
||||
return hal_i2c_write(PCA9555_ADDR, wr, sizeof(wr));
|
||||
}
|
||||
|
||||
/* 写完立刻回读比对。回读不一致 = 总线上有东西但不是我们以为的那片。 */
|
||||
static hal_err_t write_verify(uint8_t reg, uint8_t val)
|
||||
{
|
||||
hal_err_t err = write_reg(reg, val);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
uint8_t rb = 0;
|
||||
err = read_reg(reg, &rb);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return (rb == val) ? HAL_OK : HAL_ERR_BUS;
|
||||
}
|
||||
|
||||
hal_err_t pca9555_probe(void)
|
||||
{
|
||||
uint8_t c0 = 0, c1 = 0;
|
||||
hal_err_t err = read_reg(PCA9555_REG_CONFIG0, &c0);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
err = read_reg(PCA9555_REG_CONFIG1, &c1);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
/* 刚上电的 PCA9555 两个方向寄存器都是 0xFF。不是的话,要么这地址上蹲着别的
|
||||
器件,要么它已经被谁配过了 —— 两种都要在启动时就叫出来。 */
|
||||
return (c0 == PCA9555_DIR_ALL_INPUT && c1 == PCA9555_DIR_ALL_INPUT) ? HAL_OK : HAL_ERR_BUS;
|
||||
}
|
||||
|
||||
hal_err_t pca9555_init(void)
|
||||
{
|
||||
/* 方向必须先于一切:这一步写错就是四个接地脚被驱动成高电平。
|
||||
全输入是唯一合法值,assert 拦的是「哪天有人顺手改了这个常量」。 */
|
||||
assert(pca9555_dir_safe(0, PCA9555_DIR_ALL_INPUT));
|
||||
assert(pca9555_dir_safe(1, PCA9555_DIR_ALL_INPUT));
|
||||
|
||||
hal_err_t err = write_verify(PCA9555_REG_CONFIG0, PCA9555_DIR_ALL_INPUT);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
err = write_verify(PCA9555_REG_CONFIG1, PCA9555_DIR_ALL_INPUT);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* 极性反转清零。复位值本来就是 0,但「本来就是」不值得赌 —— 看门狗复位
|
||||
只复位 ESP32,PCA9555 是不掉电的,上一条命的残留值会原样留着。 */
|
||||
err = write_verify(PCA9555_REG_POLINV0, PCA9555_POLINV_NONE);
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
return write_verify(PCA9555_REG_POLINV1, PCA9555_POLINV_NONE);
|
||||
}
|
||||
|
||||
hal_err_t pca9555_read_inputs(pca9555_in_t *out)
|
||||
{
|
||||
if (out == NULL) {
|
||||
return HAL_ERR_ARG;
|
||||
}
|
||||
const uint8_t reg = PCA9555_REG_INPUT0;
|
||||
uint8_t rd[2] = { 0, 0 };
|
||||
hal_err_t err = hal_i2c_write_read(PCA9555_ADDR, ®, 1, rd, sizeof(rd));
|
||||
if (err != HAL_OK) {
|
||||
return err;
|
||||
}
|
||||
out->port0 = rd[0];
|
||||
out->port1 = rd[1];
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* ---- 语义判读(极性各出现一次,见头文件) -------------------------------- */
|
||||
|
||||
bool pca9555_rail_fault(const pca9555_in_t *in, int rail)
|
||||
{
|
||||
assert(in != NULL);
|
||||
return (in->port0 & (uint8_t)(1u << pca9555_flt_bit(rail))) == 0; /* FLT# 低有效 */
|
||||
}
|
||||
|
||||
bool pca9555_rail_pg(const pca9555_in_t *in, int rail)
|
||||
{
|
||||
assert(in != NULL);
|
||||
return (in->port1 & (uint8_t)(1u << pca9555_pg_bit(rail))) != 0; /* PGOOD 高 = 良好 */
|
||||
}
|
||||
|
||||
bool pca9555_buck_pg(const pca9555_in_t *in)
|
||||
{
|
||||
assert(in != NULL);
|
||||
return (in->port0 & (uint8_t)(1u << PCA9555_BIT_BUCK_PG)) != 0; /* 高 = 五路都正常 */
|
||||
}
|
||||
|
||||
bool pca9555_ina_alert(const pca9555_in_t *in)
|
||||
{
|
||||
assert(in != NULL);
|
||||
return (in->port0 & (uint8_t)(1u << PCA9555_BIT_INA_ALERT)) == 0; /* ALERT 低有效 */
|
||||
}
|
||||
@@ -111,6 +111,9 @@ static hal_err_t do_write(sim_i2c_dev_t *d, const uint8_t *data, size_t len)
|
||||
}
|
||||
d->regs[d->ptr++] = v;
|
||||
}
|
||||
if (d->hook != NULL) {
|
||||
d->hook(d, SIM_I2C_EV_POST_WRITE);
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
@@ -123,6 +126,9 @@ static hal_err_t do_read(sim_i2c_dev_t *d, uint8_t *data, size_t len)
|
||||
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) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
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);
|
||||
|
||||
@@ -13,6 +14,7 @@ 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();
|
||||
}
|
||||
|
||||
@@ -28,7 +28,17 @@ void sim_hal_reset(void);
|
||||
#define SIM_I2C_REGS 256
|
||||
#define SIM_I2C_LOG_BYTES 8
|
||||
|
||||
typedef struct {
|
||||
/* 假器件的窃听事件。通用寄存器表模拟不了「配成输出就对地短路」这种事,
|
||||
所以按 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 */
|
||||
@@ -39,6 +49,7 @@ typedef struct {
|
||||
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(测试写错了,不该悄悄过)。 */
|
||||
@@ -91,6 +102,32 @@ 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 */
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/* 按 datasheet 建模的假 PCA9555。宿主机专用。
|
||||
|
||||
通用寄存器表(sim_i2c)能测「写下去的字节对不对」,但测不出 A6 真正要拦的那件事:
|
||||
**把一个焊死在地上的脚配成输出**。真板子上这既不报错也不 NAK,只是发烫。
|
||||
所以这里把三件真器件会做的事补上(见 sim_hal.h 那一节)。
|
||||
|
||||
⚠️ 这个模型是用来证明**代码逻辑**对的。别拿它推翻设计文档 §17.2 的任何一条。 */
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "pca9555.h"
|
||||
#include "sim_hal.h"
|
||||
|
||||
/* 焊死在地上的备用脚(设计文档 §17.2 的注)。 */
|
||||
static const uint8_t s_gnd_mask[2] = { PCA9555_P0_GND_MASK, PCA9555_P1_GND_MASK };
|
||||
|
||||
typedef struct {
|
||||
bool attached;
|
||||
uint8_t addr;
|
||||
uint8_t ext[2]; /* 外部世界驱动的电平;开漏上拉,静息 0xFF */
|
||||
uint8_t int_latch[2];/* 上一次「被读走」时的输入值,用来判 INT# */
|
||||
bool int_low;
|
||||
uint32_t int_asserts;
|
||||
uint32_t gnd_driven;
|
||||
uint32_t shorted;
|
||||
} sim_pca9555_t;
|
||||
|
||||
static sim_pca9555_t s;
|
||||
|
||||
void sim_pca9555_reset_internal(void); /* 只给 sim_hal_reset() 用 */
|
||||
|
||||
void sim_pca9555_reset_internal(void)
|
||||
{
|
||||
memset(&s, 0, sizeof(s));
|
||||
}
|
||||
|
||||
/* 输入寄存器 = 逐位选择:配成输入的位取外部电平,配成输出的位取自己的输出锁存器。
|
||||
接地脚无论配成什么,线上都是 0(它焊在地上,赢的永远是铜)。 */
|
||||
static uint8_t input_value(const sim_i2c_dev_t *d, int port)
|
||||
{
|
||||
const uint8_t cfg = (uint8_t)d->regs[PCA9555_REG_CONFIG0 + port];
|
||||
const uint8_t out = (uint8_t)d->regs[PCA9555_REG_OUTPUT0 + port];
|
||||
const uint8_t pol = (uint8_t)d->regs[PCA9555_REG_POLINV0 + port];
|
||||
|
||||
uint8_t v = (uint8_t)((s.ext[port] & cfg) | (out & (uint8_t)~cfg));
|
||||
v &= (uint8_t)~s_gnd_mask[port]; /* 接地脚读回来永远是 0 */
|
||||
return (uint8_t)(v ^ pol); /* 极性反转寄存器作用在**输入**上 */
|
||||
}
|
||||
|
||||
/* 方向寄存器一变就查一次:有没有把接地脚配成输出,输出的又是不是高电平。 */
|
||||
static void check_grounded_pins(const sim_i2c_dev_t *d)
|
||||
{
|
||||
for (int port = 0; port < 2; port++) {
|
||||
const uint8_t cfg = (uint8_t)d->regs[PCA9555_REG_CONFIG0 + port];
|
||||
const uint8_t out = (uint8_t)d->regs[PCA9555_REG_OUTPUT0 + port];
|
||||
const uint8_t driven = (uint8_t)(s_gnd_mask[port] & (uint8_t)~cfg); /* cfg 0 = 输出 */
|
||||
if (driven == 0) {
|
||||
continue;
|
||||
}
|
||||
s.gnd_driven++;
|
||||
if ((driven & out) != 0) {
|
||||
s.shorted++; /* 真板子上这一刻开始冒烟 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* INT#:任何输入位相对「上次被读走的那一帧」发生变化就拉低,读走输入端口才释放。 */
|
||||
static void update_int(const sim_i2c_dev_t *d)
|
||||
{
|
||||
for (int port = 0; port < 2; port++) {
|
||||
if (input_value(d, port) != s.int_latch[port]) {
|
||||
if (!s.int_low) {
|
||||
s.int_low = true;
|
||||
s.int_asserts++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void hook(sim_i2c_dev_t *d, sim_i2c_ev_t ev)
|
||||
{
|
||||
if (ev == SIM_I2C_EV_POST_WRITE) {
|
||||
check_grounded_pins(d);
|
||||
update_int(d);
|
||||
return;
|
||||
}
|
||||
|
||||
/* PRE_READ:把两个输入寄存器合成好再让总线取走。 */
|
||||
for (int port = 0; port < 2; port++) {
|
||||
d->regs[PCA9555_REG_INPUT0 + port] = input_value(d, port);
|
||||
}
|
||||
/* 读输入端口 = 确认中断。读别的寄存器不清 INT#(真器件就是这个行为,
|
||||
也是「只靠中断会永远收不到下一个沿」的根源)。 */
|
||||
if (d->ptr == PCA9555_REG_INPUT0 || d->ptr == PCA9555_REG_INPUT1) {
|
||||
s.int_latch[0] = (uint8_t)d->regs[PCA9555_REG_INPUT0];
|
||||
s.int_latch[1] = (uint8_t)d->regs[PCA9555_REG_INPUT1];
|
||||
s.int_low = false;
|
||||
}
|
||||
}
|
||||
|
||||
void sim_pca9555_attach(uint8_t addr)
|
||||
{
|
||||
assert(!s.attached); /* 板上只有一片 */
|
||||
sim_i2c_dev_t *d = sim_i2c_add(addr, 1);
|
||||
|
||||
s.attached = true;
|
||||
s.addr = addr;
|
||||
s.ext[0] = 0xFF; /* FLT# / PGOOD 都是开漏上拉,静息是高 */
|
||||
s.ext[1] = 0xFF;
|
||||
|
||||
/* 上电复位值(datasheet 表 6):方向全输入 0xFF,**输出锁存器 0xFF**,极性 0。
|
||||
输出的 0xFF 是本器件最阴的一条:配成输出的那一刻就输出高电平。 */
|
||||
d->regs[PCA9555_REG_CONFIG0] = 0xFF;
|
||||
d->regs[PCA9555_REG_CONFIG1] = 0xFF;
|
||||
d->regs[PCA9555_REG_OUTPUT0] = 0xFF;
|
||||
d->regs[PCA9555_REG_OUTPUT1] = 0xFF;
|
||||
d->regs[PCA9555_REG_POLINV0] = 0x00;
|
||||
d->regs[PCA9555_REG_POLINV1] = 0x00;
|
||||
|
||||
s.int_latch[0] = input_value(d, 0);
|
||||
s.int_latch[1] = input_value(d, 1);
|
||||
d->hook = hook;
|
||||
}
|
||||
|
||||
static sim_i2c_dev_t *must_dev(void)
|
||||
{
|
||||
assert(s.attached); /* 先 sim_pca9555_attach() */
|
||||
sim_i2c_dev_t *d = sim_i2c_dev(s.addr);
|
||||
assert(d != NULL);
|
||||
return d;
|
||||
}
|
||||
|
||||
void sim_pca9555_drive(int port, uint8_t level)
|
||||
{
|
||||
assert(port == 0 || port == 1);
|
||||
s.ext[port] = level;
|
||||
update_int(must_dev());
|
||||
}
|
||||
|
||||
uint8_t sim_pca9555_config(int port)
|
||||
{
|
||||
assert(port == 0 || port == 1);
|
||||
return (uint8_t)must_dev()->regs[PCA9555_REG_CONFIG0 + port];
|
||||
}
|
||||
|
||||
uint8_t sim_pca9555_output(int port)
|
||||
{
|
||||
assert(port == 0 || port == 1);
|
||||
return (uint8_t)must_dev()->regs[PCA9555_REG_OUTPUT0 + port];
|
||||
}
|
||||
|
||||
uint8_t sim_pca9555_polinv(int port)
|
||||
{
|
||||
assert(port == 0 || port == 1);
|
||||
return (uint8_t)must_dev()->regs[PCA9555_REG_POLINV0 + port];
|
||||
}
|
||||
|
||||
uint32_t sim_pca9555_gnd_driven(void) { return s.gnd_driven; }
|
||||
uint32_t sim_pca9555_shorted(void) { return s.shorted; }
|
||||
bool sim_pca9555_int_asserted(void) { return s.int_low; }
|
||||
uint32_t sim_pca9555_int_asserts(void) { return s.int_asserts; }
|
||||
@@ -33,6 +33,7 @@ set(PHONERAIL_HAL_SIM_SRCS
|
||||
${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
|
||||
)
|
||||
@@ -43,6 +44,7 @@ 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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#define PHONERAIL_H
|
||||
|
||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a5"
|
||||
#define PHONERAIL_FW_VERSION "0.1.0-a6"
|
||||
|
||||
/* 通道数。五路直供,见设计文档 §2。 */
|
||||
#define PHONERAIL_RAIL_COUNT 5
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
/* A6:PCA9555 驱动。
|
||||
验收主项:**方向寄存器永远不会把 Port0[7] / Port1[7:5] 配成输出** ——
|
||||
那四个脚在板上直接接地,配成输出等于对地短路,而真器件既不报错也不 NAK。
|
||||
第二主项:FLT / PG / BUCK_PG / INA_ALERT 三种极性一个都不许写反。 */
|
||||
#include "pca9555.h"
|
||||
#include "sim_hal.h"
|
||||
#include "unity.h"
|
||||
|
||||
void setUp(void) { sim_hal_reset(); }
|
||||
void tearDown(void) {}
|
||||
|
||||
static void mount(void)
|
||||
{
|
||||
sim_pca9555_attach(PCA9555_ADDR);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(NULL));
|
||||
}
|
||||
|
||||
/* 挂好并 init 完,外部电平停在静息值(全高 = 无故障、全部良好)。 */
|
||||
static void mount_ready(void)
|
||||
{
|
||||
mount();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_init());
|
||||
}
|
||||
|
||||
/* ---- 验收主项一:接地脚永远是输入 ---------------------------------------- */
|
||||
|
||||
/* init 之后两个方向寄存器都必须是 0xFF —— 不是「大致全输入」,是逐位。 */
|
||||
static void test_init_configures_every_pin_as_input(void)
|
||||
{
|
||||
mount_ready();
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_config(0));
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_config(1));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_gnd_driven());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_shorted());
|
||||
}
|
||||
|
||||
/* dir_safe 的判据穷举:**当且仅当**接地位全为 1(输入)才算安全。
|
||||
写成「dir != 0」「dir & 0x80」这类近似判据的,会在这 512 个组合里露馅。 */
|
||||
static void test_dir_safe_is_exactly_the_grounded_pins(void)
|
||||
{
|
||||
for (int dir = 0; dir <= 0xFF; dir++) {
|
||||
const bool p0_want = ((uint8_t)dir & PCA9555_P0_GND_MASK) == PCA9555_P0_GND_MASK;
|
||||
const bool p1_want = ((uint8_t)dir & PCA9555_P1_GND_MASK) == PCA9555_P1_GND_MASK;
|
||||
TEST_ASSERT_EQUAL_INT(p0_want, pca9555_dir_safe(0, (uint8_t)dir));
|
||||
TEST_ASSERT_EQUAL_INT(p1_want, pca9555_dir_safe(1, (uint8_t)dir));
|
||||
}
|
||||
/* 几个会被真写出来的错值 */
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(0, 0x7F)); /* 只把 P0_7 配成输出:最像笔误的一个 */
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(1, 0x1F)); /* 「低五位是 PG,高三位不用」 */
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(1, 0x00)); /* 全输出 */
|
||||
TEST_ASSERT_TRUE(pca9555_dir_safe(0, PCA9555_DIR_ALL_INPUT));
|
||||
TEST_ASSERT_TRUE(pca9555_dir_safe(1, PCA9555_DIR_ALL_INPUT));
|
||||
TEST_ASSERT_FALSE(pca9555_dir_safe(2, PCA9555_DIR_ALL_INPUT)); /* 端口号越界 */
|
||||
}
|
||||
|
||||
/* 这条证明的是**假器件真的会照出短路**,否则上面那条「shorted == 0」是句空话。
|
||||
手工绕过驱动,按「有人把 P0_7 配成输出」写一次方向寄存器:
|
||||
PCA9555 的 Output Port 复位值是 0xFF,所以这一刻它就在对地灌电流。 */
|
||||
static void test_sim_catches_a_grounded_pin_driven_high(void)
|
||||
{
|
||||
mount_ready();
|
||||
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_output(0)); /* 复位值就是全高,这才是要命的地方 */
|
||||
|
||||
const uint8_t wr[2] = { PCA9555_REG_CONFIG0, 0x7F };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(PCA9555_ADDR, wr, sizeof(wr)));
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_gnd_driven());
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_shorted());
|
||||
}
|
||||
|
||||
/* 同上,但先把输出锁存器写成 0 再配成输出:不冒烟,可仍然是在驱动一个接地脚。
|
||||
两个计数分开就是为了让这一格不被漏掉。 */
|
||||
static void test_sim_counts_driving_a_grounded_pin_even_when_low(void)
|
||||
{
|
||||
mount_ready();
|
||||
|
||||
const uint8_t out0[2] = { PCA9555_REG_OUTPUT0, 0x00 };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(PCA9555_ADDR, out0, sizeof(out0)));
|
||||
const uint8_t cfg0[2] = { PCA9555_REG_CONFIG0, 0x7F };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_write(PCA9555_ADDR, cfg0, sizeof(cfg0)));
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_gnd_driven());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, sim_pca9555_shorted());
|
||||
}
|
||||
|
||||
/* 驱动全程一个字节都不许写 Output Port:没有输出脚,写它只可能是写错寄存器号。 */
|
||||
static void test_init_never_touches_the_output_registers(void)
|
||||
{
|
||||
mount_ready();
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_output(0));
|
||||
TEST_ASSERT_EQUAL_HEX8(0xFF, sim_pca9555_output(1));
|
||||
}
|
||||
|
||||
/* ---- 极性反转寄存器 ------------------------------------------------------ */
|
||||
|
||||
/* 极性反转是「看门狗复位后残留」的重灾区:ESP32 重启了,PCA9555 没掉电。
|
||||
init 必须无条件清零,而不是「复位值本来就是 0」。 */
|
||||
static void test_init_clears_a_stale_polarity_inversion(void)
|
||||
{
|
||||
mount();
|
||||
sim_i2c_set_reg(PCA9555_ADDR, PCA9555_REG_POLINV0, 0xFF); /* 上一条命的残留 */
|
||||
sim_i2c_set_reg(PCA9555_ADDR, PCA9555_REG_POLINV1, 0x1F);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_init());
|
||||
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(0));
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(1));
|
||||
}
|
||||
|
||||
/* ---- 验收主项二:三种极性 ------------------------------------------------ */
|
||||
|
||||
/* 静息态(两个端口都被上拉到 0xFF)= 五路无故障、五路电源良好、Buck 正常、无告警。
|
||||
任何一处极性写反,这条立刻红。 */
|
||||
static void test_idle_bus_means_everything_is_fine(void)
|
||||
{
|
||||
mount_ready();
|
||||
pca9555_in_t in;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
|
||||
TEST_ASSERT_EQUAL_HEX8(0x7F, in.port0); /* P0_7 接地,读回来必是 0 */
|
||||
TEST_ASSERT_EQUAL_HEX8(0x1F, in.port1); /* P1_5~7 接地 */
|
||||
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_FALSE(pca9555_rail_fault(&in, rail));
|
||||
TEST_ASSERT_TRUE(pca9555_rail_pg(&in, rail));
|
||||
}
|
||||
TEST_ASSERT_TRUE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_ina_alert(&in));
|
||||
}
|
||||
|
||||
/* 全 0(比如整条总线被拉低、或者上拉没焊)= 五路全故障、五路全不良、Buck 不良、告警。
|
||||
注意这**不是**「安全默认值」:它和「五路真的全炸了」长得一模一样,
|
||||
§10.10 因此规定这片不应答时改用 INA226 的电压判据兜底,而不是信这一帧。 */
|
||||
static void test_all_low_means_everything_is_bad(void)
|
||||
{
|
||||
mount_ready();
|
||||
sim_pca9555_drive(0, 0x00);
|
||||
sim_pca9555_drive(1, 0x00);
|
||||
|
||||
pca9555_in_t in;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_TRUE(pca9555_rail_fault(&in, rail));
|
||||
TEST_ASSERT_FALSE(pca9555_rail_pg(&in, rail));
|
||||
}
|
||||
TEST_ASSERT_FALSE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_ina_alert(&in));
|
||||
}
|
||||
|
||||
/* 逐路点名:只让一路报故障,其余四路必须纹丝不动。
|
||||
FLT 和 PG 两张表在两个端口上长得一模一样(都是 bit0~4),把端口搞反之后
|
||||
「有故障」会被读成「电源良好」,所以这里一路一路地把两个端口错开来验。 */
|
||||
static void test_one_rail_at_a_time(void)
|
||||
{
|
||||
mount_ready();
|
||||
|
||||
for (int hurt = 1; hurt <= PCA9555_RAIL_COUNT; hurt++) {
|
||||
/* FLT 只拉低 hurt 那一路;PG 只拉低**另一路**(hurt 的下一路,绕回来)。 */
|
||||
const int other = (hurt % PCA9555_RAIL_COUNT) + 1;
|
||||
sim_pca9555_drive(0, (uint8_t)(0xFFu & ~(1u << pca9555_flt_bit(hurt))));
|
||||
sim_pca9555_drive(1, (uint8_t)(0xFFu & ~(1u << pca9555_pg_bit(other))));
|
||||
|
||||
pca9555_in_t in;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_EQUAL_INT(rail == hurt, pca9555_rail_fault(&in, rail));
|
||||
TEST_ASSERT_EQUAL_INT(rail != other, pca9555_rail_pg(&in, rail));
|
||||
}
|
||||
/* 两根线或信号不受任何一路 FLT 影响(它们在 P0_5 / P0_6 上)。 */
|
||||
TEST_ASSERT_TRUE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_ina_alert(&in));
|
||||
}
|
||||
}
|
||||
|
||||
/* 两根线或信号自己点名:拉低 BUCK_PG 不许把任何一路读成故障,反之亦然。 */
|
||||
static void test_wire_or_signals_do_not_alias_a_rail(void)
|
||||
{
|
||||
mount_ready();
|
||||
pca9555_in_t in;
|
||||
|
||||
sim_pca9555_drive(0, (uint8_t)(0xFFu & ~(1u << PCA9555_BIT_BUCK_PG)));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_FALSE(pca9555_ina_alert(&in));
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_FALSE(pca9555_rail_fault(&in, rail));
|
||||
}
|
||||
|
||||
sim_pca9555_drive(0, (uint8_t)(0xFFu & ~(1u << PCA9555_BIT_INA_ALERT)));
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_buck_pg(&in));
|
||||
TEST_ASSERT_TRUE(pca9555_ina_alert(&in));
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_FALSE(pca9555_rail_fault(&in, rail));
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 位表与地址 ---------------------------------------------------------- */
|
||||
|
||||
static void test_bit_map_is_consistent(void)
|
||||
{
|
||||
TEST_ASSERT_TRUE(pca9555_map_ok());
|
||||
TEST_ASSERT_TRUE(pca9555_addr_ok());
|
||||
TEST_ASSERT_EQUAL_HEX8(0x20, PCA9555_ADDR); /* A0=A1=A2=GND,U23 的 pin21/2/3 */
|
||||
|
||||
for (int rail = 1; rail <= PCA9555_RAIL_COUNT; rail++) {
|
||||
TEST_ASSERT_EQUAL_INT(rail - 1, pca9555_flt_bit(rail));
|
||||
TEST_ASSERT_EQUAL_INT(rail - 1, pca9555_pg_bit(rail));
|
||||
/* 一路都不许落到接地脚或两根线或信号上 */
|
||||
TEST_ASSERT_TRUE(pca9555_flt_bit(rail) < PCA9555_BIT_BUCK_PG);
|
||||
TEST_ASSERT_EQUAL_HEX8(0, (uint8_t)((1u << pca9555_flt_bit(rail)) & PCA9555_P0_GND_MASK));
|
||||
TEST_ASSERT_EQUAL_HEX8(0, (uint8_t)((1u << pca9555_pg_bit(rail)) & PCA9555_P1_GND_MASK));
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(5, PCA9555_BIT_BUCK_PG);
|
||||
TEST_ASSERT_EQUAL_INT(6, PCA9555_BIT_INA_ALERT);
|
||||
}
|
||||
|
||||
/* ---- 总线行为 ------------------------------------------------------------ */
|
||||
|
||||
/* §10.3 的 0.12 ms 预算是按**一次事务**算的(写命令字节 + 重复起始读 2 字节)。
|
||||
拆成两次事务照样能跑,但 t_safety 的 I²C 占用会对不上 A8 要断言的那个数。 */
|
||||
static void test_read_inputs_uses_one_transaction(void)
|
||||
{
|
||||
mount_ready();
|
||||
const sim_i2c_dev_t *d = sim_i2c_dev(PCA9555_ADDR);
|
||||
const uint32_t w0 = d->n_write, r0 = d->n_read;
|
||||
|
||||
pca9555_in_t in;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT32(1, d->n_write - w0);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, d->n_read - r0);
|
||||
}
|
||||
|
||||
static void test_probe_accepts_a_fresh_device(void)
|
||||
{
|
||||
mount();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_probe());
|
||||
}
|
||||
|
||||
/* 这个地址上蹲着别的东西(应答了,但方向寄存器不是复位值)。 */
|
||||
static void test_probe_rejects_a_stranger(void)
|
||||
{
|
||||
mount();
|
||||
sim_i2c_set_reg(PCA9555_ADDR, PCA9555_REG_CONFIG1, 0x00);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_BUS, pca9555_probe());
|
||||
}
|
||||
|
||||
static void test_probe_absent_device_naks(void)
|
||||
{
|
||||
mount();
|
||||
sim_i2c_set_present(PCA9555_ADDR, false);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_probe());
|
||||
}
|
||||
|
||||
/* §10.10:这片不应答只是失去可见性,驱动如实返回 NAK,什么动作都不做。 */
|
||||
static void test_read_propagates_nak(void)
|
||||
{
|
||||
mount_ready();
|
||||
sim_i2c_inject_nak(PCA9555_ADDR, -1);
|
||||
|
||||
pca9555_in_t in = { 0xAA, 0x55 };
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_EQUAL_HEX8(0xAA, in.port0); /* 失败时不许改写调用方的缓冲 */
|
||||
TEST_ASSERT_EQUAL_HEX8(0x55, in.port1);
|
||||
}
|
||||
|
||||
static void test_init_propagates_nak(void)
|
||||
{
|
||||
mount();
|
||||
sim_i2c_inject_nak(PCA9555_ADDR, -1);
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_init());
|
||||
}
|
||||
|
||||
/* 回读对不上 = 总线上有东西但不是我们以为的那片,init 必须失败而不是接着往下跑。
|
||||
这里用「写第二个方向寄存器时 NAK」来造回读失败之外的另一条错路。 */
|
||||
static void test_init_stops_at_the_first_failure(void)
|
||||
{
|
||||
mount();
|
||||
sim_i2c_inject_nak(PCA9555_ADDR, 3); /* 头三次事务 NAK,之后恢复 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_NAK, pca9555_init());
|
||||
/* 失败了就别把剩下的寄存器也写一遍 —— 极性反转还停在没被碰过的状态。 */
|
||||
TEST_ASSERT_EQUAL_HEX8(0x00, sim_pca9555_polinv(1));
|
||||
}
|
||||
|
||||
static void test_null_output_rejected(void)
|
||||
{
|
||||
mount_ready();
|
||||
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, pca9555_read_inputs(NULL));
|
||||
}
|
||||
|
||||
/* ---- INT#(A8/A10 会用,这里只钉住它的行为) ----------------------------- */
|
||||
|
||||
/* 中断是加速路径不是主路径:**读走输入端口才释放**。
|
||||
漏读一次就再也等不到下一个沿,所以 t_safety 必须无条件轮询。 */
|
||||
static void test_int_is_only_cleared_by_reading_the_input_ports(void)
|
||||
{
|
||||
mount_ready();
|
||||
TEST_ASSERT_FALSE(sim_pca9555_int_asserted());
|
||||
|
||||
sim_pca9555_drive(0, 0xFE); /* CH1 报故障 */
|
||||
TEST_ASSERT_TRUE(sim_pca9555_int_asserted());
|
||||
TEST_ASSERT_EQUAL_UINT32(1, sim_pca9555_int_asserts());
|
||||
|
||||
/* 读别的寄存器不清中断 */
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_probe());
|
||||
TEST_ASSERT_TRUE(sim_pca9555_int_asserted());
|
||||
|
||||
pca9555_in_t in;
|
||||
TEST_ASSERT_EQUAL_INT(HAL_OK, pca9555_read_inputs(&in));
|
||||
TEST_ASSERT_FALSE(sim_pca9555_int_asserted());
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_init_configures_every_pin_as_input);
|
||||
RUN_TEST(test_dir_safe_is_exactly_the_grounded_pins);
|
||||
RUN_TEST(test_sim_catches_a_grounded_pin_driven_high);
|
||||
RUN_TEST(test_sim_counts_driving_a_grounded_pin_even_when_low);
|
||||
RUN_TEST(test_init_never_touches_the_output_registers);
|
||||
RUN_TEST(test_init_clears_a_stale_polarity_inversion);
|
||||
RUN_TEST(test_idle_bus_means_everything_is_fine);
|
||||
RUN_TEST(test_all_low_means_everything_is_bad);
|
||||
RUN_TEST(test_one_rail_at_a_time);
|
||||
RUN_TEST(test_wire_or_signals_do_not_alias_a_rail);
|
||||
RUN_TEST(test_bit_map_is_consistent);
|
||||
RUN_TEST(test_read_inputs_uses_one_transaction);
|
||||
RUN_TEST(test_probe_accepts_a_fresh_device);
|
||||
RUN_TEST(test_probe_rejects_a_stranger);
|
||||
RUN_TEST(test_probe_absent_device_naks);
|
||||
RUN_TEST(test_read_propagates_nak);
|
||||
RUN_TEST(test_init_propagates_nak);
|
||||
RUN_TEST(test_init_stops_at_the_first_failure);
|
||||
RUN_TEST(test_null_output_rejected);
|
||||
RUN_TEST(test_int_is_only_cleared_by_reading_the_input_ports);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -413,6 +413,26 @@ typedef struct {
|
||||
同理,**PCA9555 的方向寄存器只能配成全输入**:备用脚 Port0[7] / Port1[7:5] 在硬件上
|
||||
直接接地,配成输出并写 1 就是对地短路(设计文档 §17.2 已在图纸上注明)。
|
||||
|
||||
> **A6 已落地**:驱动在 `firmware/components/drv/include/pca9555.h`,
|
||||
> **根本不提供「把某个脚配成输出」的接口**(和 sr595.h 不提供「抬 OE#」同一条规矩)。
|
||||
> 方向字一律过 `pca9555_dir_safe()`,不安全直接 abort。
|
||||
> 同处还记了四条这一片的实现约定:
|
||||
> ① ⚠️ **Output Port 寄存器的复位值是 0xFF** —— 所以把接地脚配成输出的那一刻它就
|
||||
> 输出高电平,中间没有「先写个 0 再说」的余地,这比「配错方向」本身更急;
|
||||
> ② **极性反转寄存器必须无条件清零**,不能赌复位值:看门狗只复位 ESP32,
|
||||
> PCA9555 不掉电,上一条命的残留值原样留着,而写错的现象是「五路同时报故障」
|
||||
> 或者更糟的「五路故障全都看不见」,I²C 一切正常;
|
||||
> ③ **三种极性**(FLT# 低有效 / PGOOD 高有效 / INA_ALERT 低有效)各自只在
|
||||
> `pca9555_rail_fault()` 等四个函数里出现一次,上层不许自己去 & 位;
|
||||
> ④ **INT# 是加速路径不是主路径**:它要读走输入端口才释放,漏读一次就再也等不到
|
||||
> 下一个沿,所以 `t_safety` 必须无条件轮询(§10.3 的 20 ms 就是按这个排的)。
|
||||
>
|
||||
> ⚠️ **FLT 与 PG 两张位表这次确实是顺排的**(都是 `bit = rail - 1`),
|
||||
> 和 INA226 地址、595 位号那两张表不一样。所以这里最容易写出来的错不是「算式」,
|
||||
> 而是**把 Port0 和 Port1 搞反** —— 两个端口的 bit0~4 长得一模一样,搞反之后
|
||||
> 「有故障」会被读成「电源良好」,方向正好相反,而且**静息态两个端口读回来都是
|
||||
> 全 1,单看一帧完全看不出来**。`test_one_rail_at_a_time` 专门拦这一手。
|
||||
|
||||
### 10.5 电压通道:DAC 只做微调,而且是反向的
|
||||
|
||||
| 事实 | 含义 |
|
||||
|
||||
在新工单中引用
屏蔽一个用户