文件
phonerail/firmware/components/drv/include/pca9555.h
T
zhou yang和Claude Opus 5.5 7bf30e3c38 固件 0.1.0-c3:计划内重启不断电(OTA / 回滚锁住 EFUSE_EN,新固件接管;SR-6 改写)
- warm_esp.{h,c}:warm_restart() 记下开着的路(RTC_NOINIT,魔数 + 掩码 + 反码)→ gpio_hold 锁五根 EN → 重启;
  目标镜像没有能力标记(第 288 字节 PRCAPS1,.rodata_custom_desc)就不锁 —— 老固件写不动锁住的 EN。
  warm_take():软件复位 + 标记对得上才接管,一次性;再读 EN 真电平核对
- bringup:adopt 掩码;rail_en_init_keep() 接管的路写高、五根一律放开 hold(崩溃时旧锁可能还在);
  刀片不在 / mux 失联的接管路立刻关(SLOT_EMPTY arg 1);第二段已开着的路不重开、不等 PG
- pca9555_init_keep():方向寄存器是本固件写的才留 VBUS(掉过电的片子锁存器是 0xFF),键 / EDL 清零
- HAL:hal_gpio_hold();ESP 输出脚配 INPUT_OUTPUT(读得到真电平);sim 给 hold 建模
- 事件 LOG_EV_OTA arg 7 = 接管;兜底定时器回滚故意不走接管
- 顶层 CMake:phonerail.h 进 CMAKE_CONFIGURE_DEPENDS(否则镜像头版本停在旧值);-u g_phonerail_caps 防 gc
- test_bringup 加 6 例(接管 / 非计划复位全关 / 空槽 / mux 失联 / PCA 掉过电 / hold 冻结),全工程 448 例全绿
- rack:固件库识别能力标记,推送对话框按「支持 / 不支持重启不断电」给不同提示(已部署)
- 需求文档:SR-6 改为「非计划复位全关」+ 计划内重启例外;§10.9 新增「重启不断电」
- 板 B:台架镜像(空槽强制 CH1 在位,未提交)开 CH1 → OTA 带重启 → 事件 18/7 mv=1,CH1 一直开着;
  现跑 0.1.0-c3。未测:示波器看重启期间 EN / 输出毛刺(要插刀片)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 14:35:52 +08:00

193 行
9.2 KiB
C
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
/* PCA9555 —— **每片刀片上一颗**(`U4`,五片全是 0x20),R2 D9 取代 R1 的
74HC595 链。五片靠底板 TCA9548A 分通道区分(D8),所以从驱动看它们**完全一样**。
⚠️⚠️ **R1 那版驱动「16 个脚全是输入」的前提在 R2 不成立了。**
R2 的 P0.0~P0.4 是**输出**:三个侧键 + EDL + VBUS 开关。
拿 R1 的记忆改这个文件会在板子上冒烟,原因见下面那个框。
位分配从 `.claude/r2/_blade_pcb_nets.json` 的 `U4` 逐脚读回(2026-09-16):
Port0 bit0 KEY_PWR bit1 KEY_VUP bit2 KEY_VDN bit3 EDL_EN bit4 VBUS_EN
bit5~7 **直接接地**
Port1 bit0 EFUSE_FLT# bit1 BUCK_PG bit2 INA_ALERT bit3 EFUSE_PG
bit4~7 **直接接地**
┌─ ⛔⛔ 初始化顺序是这块板子上最危险的一处 ──────────────────────────────┐
│ PCA9555 的 **Output Port 复位值是 0xFF**,Config 复位值也是 0xFF(全输入)。│
│ 所以**必须先把输出锁存器写成 0,最后才改方向**: │
│ OUTPUT0=0 → OUTPUT1=0 → POLINV=0 → CONFIG1=0xFF → CONFIG0=0xE0 │
│ 顺序反过来的那一刻,P0.0~P0.4 一起输出 1 │
│ = 三个键全按下 + EDL 短接 + VBUS 上电,五台手机一起被按住电源键。 │
│ 而 P0.5~7 / P1.4~7 七个脚在刀片上**直接接地、不串限流电阻** —— │
│ 方向写错一位就是对地短路:不报错、不 NAK,只会发烫。 │
│ ⇒ `pca9555_dir_safe()` 是硬闸,走到 false 直接 abort。 │
└──────────────────────────────────────────────────────────────────────────┘
§10.10 的降级行为:某片不应答 = 失去那一路的 FAULT / PG 可见性,**不关输出**,
改用 INA226 的电压判据兜底并持续告警。所以本驱动的读失败一律只返回错误码。
⚠️ R2 新增的代价:这片不应答时**那一路的 VBUS 也控制不了了**(计划 §5.4)。
本驱动只经 hal_i2c_*,**不碰 mux**:通道由调用方(`slot.c`)先选好。 */
#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(`U4` 的 pin21 / pin2 / pin3,已按 PCB 焊盘网络核过)。
**五片同址** —— 这是 D8 的全部意义,不是笔误。 */
#define PCA9555_ADDR 0x20
#define PCA9555_SLOT_COUNT 5
/* ---- 寄存器(全部 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
/* 本板的两个方向字。Port0 只有 bit0~4 是输出。 */
#define PCA9555_CONFIG0_INIT 0xE0u
#define PCA9555_CONFIG1_INIT 0xFFu
/* 极性反转必须是 0。写成非 0 的现象是「故障全都看不见」,而 I²C 一切正常。 */
#define PCA9555_POLINV_NONE 0x00u
/* ---- Port0:五个输出位 --------------------------------------------------- */
/* 三个侧键的逻辑键位。**上层一律用这个枚举,不许写 `1 << 0`。**
(R1 时这个枚举在 `sr595.h` 里,链退役之后搬到这里。) */
typedef enum {
PCA9555_KEY_PWR = 0,
PCA9555_KEY_VUP,
PCA9555_KEY_VDN,
PCA9555_KEY_COUNT,
} pca9555_key_t;
#define PCA9555_BIT_KEY_PWR 0
#define PCA9555_BIT_KEY_VUP 1
#define PCA9555_BIT_KEY_VDN 2
#define PCA9555_BIT_EDL_EN 3
#define PCA9555_BIT_VBUS_EN 4
#define PCA9555_P0_OUT_MASK 0x1Fu /* bit0~4:唯一允许配成输出的五位 */
#define PCA9555_P0_GND_MASK 0xE0u /* bit5~7 焊死在地上 */
/* ---- Port1:四个输入位(全是开漏 + 上拉) -------------------------------- */
#define PCA9555_BIT_EFUSE_FLT 0
#define PCA9555_BIT_BUCK_PG 1
#define PCA9555_BIT_INA_ALERT 2
#define PCA9555_BIT_EFUSE_PG 3
#define PCA9555_P1_IN_MASK 0x0Fu
#define PCA9555_P1_GND_MASK 0xF0u /* bit4~7 焊死在地上 */
/* 键 → Port0 位号。**不带路号**:五片刀片同构,位号与路号无关(计划 §5.7)。
越界 abort。 */
int pca9555_key_bit(pca9555_key_t key);
/* 一路上三个键的掩码(Port0)。`t_actuator` 的「防御性松键」用它。 */
uint8_t pca9555_key_mask(void);
/* 位分配自检:五个输出位互不重复、四个输入位互不重复、
**一个信号都没落到接地脚上**、输出位与输入位分属两个端口。
启动自检调一次(和 `ina226_addr_table_ok()` 一起)。 */
bool pca9555_map_ok(void);
/* 0x20 不与主干上的 TCA9548A(0x70) / INA226(0x43) / DAC63204(0x4A/0x4B) 撞车。
⚠️ 刀片上的 INA226 也是 0x40,与 0x20 本来就不同,两者同在一条支路上没问题。 */
bool pca9555_addr_ok(void);
/* `dir` 里 1 = 输入。返回 false = 这个方向字会把某个**接地脚**配成输出,
或者把 Port1 上那四个开漏信号源配成输出(与 eFuse 的 FLT# 打架)。
驱动内部走到 false 直接 abort:悄悄放过去的后果是板子发烫,不是功能不好用。 */
bool pca9555_dir_safe(int port, uint8_t dir);
/* ---- 器件操作(当前 mux 通道上那一片) ----------------------------------- */
/* 认件:读回两个方向寄存器,全 0xFF(复位值)才算是它。
不应答 → HAL_ERR_NAK;应答了但方向寄存器不是复位值 → HAL_ERR_BUS。
⚠️ 已经被 `pca9555_init()` 配过的片子会返回 HAL_ERR_BUS —— 这是对的:
probe 的语义是「这是一片刚上电的 PCA9555 吗」。 */
hal_err_t pca9555_probe(void);
/* 按文件头那个框里的顺序初始化,并**逐个回读校验**。
`slot` 是 0~`PCA9555_SLOT_COUNT-1`,**只用来记影子**,不参与总线操作 ——
通道必须由调用方先选好(本驱动不碰 mux)。 */
hal_err_t pca9555_init(int slot);
/* 同上,但输出锁存器里 `keep_mask` 那几位**保留现值**(「重启不断电」时留 VBUS,
按键 / EDL 照样清零 —— 重启前正按着的键,起来就松开)。
**只在这片的方向寄存器已经是 PCA9555_CONFIG0_INIT 时才留**:否则它掉过电,
锁存器是复位值 0xFF,留下来 = 凭空打开 VBUS。`keep_mask` 只许是 Port0 的输出位。 */
hal_err_t pca9555_init_keep(int slot, uint8_t keep_mask);
/* 巡检主路径:**一次事务**读两个输入端口(PCA9555 的读指针在 0x00 / 0x01 之间
toggle,这两个字节是 datasheet 保证的)。 */
typedef struct {
uint8_t port0;
uint8_t port1;
} pca9555_in_t;
hal_err_t pca9555_read_inputs(pca9555_in_t *out);
/* ---- Port0 输出:影子 + 唯一写入点 --------------------------------------- */
/* 改 `mask` 指定的那几位为 `value` 里的值,其余位保持影子里的值,整字节写下去。
组合键的两位落在**同一次写**里(§10.6:分两次写会先单按一个键)。
⚠️ **`slot` 必须与当前 mux 通道对应的那一路一致。** 驱动没法自己验证这件事,
传错的后果是影子记在了另一片上 —— 下一次写会把这一片的键"顺手"改掉。
这就是 `slot.c` 存在的理由:别在 `slot.c` 之外调这个函数。
`mask` 里出现输出位以外的位 → HAL_ERR_ARG(不是 abort:那可能是上层算错了
掩码,返回错误码能让它记一条事件)。写失败时**影子不更新**。 */
hal_err_t pca9555_write_out0(int slot, uint8_t mask, uint8_t value);
uint8_t pca9555_shadow(int slot); /* 该片 Port0 输出锁存器的影子(init 后是 0) */
/* 把影子全部清成 0,**不碰总线**。给测试的 setUp() 与 A15 的降级路径用
(「现在生效的是上电默认值」)。 */
void pca9555_shadow_reset(void);
/* ---- 语义判读(Port1) ---------------------------------------------------
四个信号**两种极性**,这是本文件第二个容易写反的地方。极性只在下面四个函数里
各出现一次。上层一律问「有没有故障」,不许自己去 & 位。 */
/* TPS25940A 的 `FLT#`:开漏、上拉 ⇒ **低 = 有故障**。 */
bool pca9555_efuse_fault(const pca9555_in_t *in);
/* TPS25940A 的 `PGOOD`:开漏、上拉,输出过 2.97 V 才释放 ⇒ **高 = 电源良好**。 */
bool pca9555_efuse_pg(const pca9555_in_t *in);
/* 本路 Buck 的 `PWRGD` ⇒ 高 = 正常。
⚠️ R1 里这一位是**五路线或**,R2 里它只代表**本路**(一片刀片一个 Buck)。 */
bool pca9555_buck_pg(const pca9555_in_t *in);
/* 本片 INA226 的 `ALERT`:开漏、低有效 ⇒ **低 = 在告警**。
⚠️ R1 里是六片线或,R2 里刀片上的 ALERT 不跨板,只进本片的 P1.2。 */
bool pca9555_ina_alert(const pca9555_in_t *in);
#ifdef __cplusplus
}
#endif
#endif /* PCA9555_H */