固件 0.1.4-c5:整片刀片不应答(INA226 + PCA9555 同时 NAK 200 ms)⇒ 关这一路

- t_safety:eFuse 开着、本路两颗都 NAK 连续 10 圈 ⇒ abort + 关 eFuse,新事件 20 BLADE_MUTE;
  只关 eFuse,不再写刚 NAK 过的 PCA9555。只有一颗不答应仍按 §10.10 盲跑、保持供电
- 起因:板 B 第 2 槽 PRSNT2# 卡低,刀片挪走后空槽 eFuse 开了约 4 h,原判据一条没响
- 需求 §10.10 加一行 + 表后第 3 条;rack 事件 20 命名并标红
- 宿主机 26 ctest 全绿(test_t_safety_rules +4 例)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-28 09:07:39 +08:00
共同撰写人 Claude Opus 5.5
父节点 ef91daf07d
当前提交 385d2f9c47
共修改 11 个文件,包含 1275 行新增和 1105 行删除
+8 -1
查看文件
@@ -1,5 +1,12 @@
# 交接
更新:2026-09-28 固件 **0.1.3-c5**(status 加 `host`);板 B 经 OTA 推上(热重启,第 1 路没断电)
更新:2026-09-28 固件 **0.1.4-c5**(整片刀片不应答 ⇒ 关路);板 B 经 OTA 推上
## 0.1.4:整片刀片不应答 ⇒ 关这一路(2026-09-28,用户拍板)
- `t_safety` 每路锁内:eFuse 开着、本路 INA226 **和** PCA9555 同时 NAK,连续 `T_SAFETY_BLADE_MUTE_N` = 10 圈(200 ms)
⇒ abort 序列 + 关 eFuse,记新事件 **20 `LOG_EV_BLADE_MUTE`**。只关 eFuse、不去写那片 PCA9555(刚 NAK 过,再写只白等 IDF 超时)。
- §10.10 前两行(只有一颗不答应 ⇒ 盲跑 / 不关输出)**不变**,`test_a_blind_rail_keeps_its_power` 原样;需求 §10.10 新增一行 + 表后第 3 条。
- 起因:板 B 第 2 槽 `PRSNT2#` 卡低(见 0.1.3),刀片挪走后空槽 eFuse 开了约 4 h,原有判据一条都没响。
- 宿主机 26 ctest 全绿(`test_t_safety_rules` 39 → 43 例,`test_evlog` 事件号 20)。rack 已同步事件 20(标红)。
## 0.1.3:上游电脑在不在进遥测(2026-09-28)
- `UP_VBUS_DET`(IO6,R721/R722 分压,R722 下拉)启动时配成输入;status 新增 `"host":true|false|null`(读不到 null)。
+2
查看文件
@@ -217,6 +217,7 @@ log_ev_t evlog_from_safety(uint8_t safety_ev)
case SAFETY_EV_PUSHED_UP: return LOG_EV_PUSHED_UP;
case SAFETY_EV_MUX_LOST: return LOG_EV_MUX_LOST;
case SAFETY_EV_SLOT_EMPTY: return LOG_EV_SLOT_EMPTY;
case SAFETY_EV_BLADE_MUTE: return LOG_EV_BLADE_MUTE;
case SAFETY_EV_NONE: break;
}
return LOG_EV_NONE;
@@ -574,6 +575,7 @@ const char *evlog_ev_str(log_ev_t ev)
case LOG_EV_PG_TIMEOUT: return "上电等不到PG";
case LOG_EV_OTA: return "固件升级";
case LOG_EV_BOOT_CHECK: return "开机重按";
case LOG_EV_BLADE_MUTE: return "刀片不应答关路";
case LOG_EV_COUNT: break;
}
return "?";
+5
查看文件
@@ -88,6 +88,11 @@ typedef enum {
频繁出现 = 这一台的电源键弹片和刀片铜盘接触不良(2026-09-28 板 B 第 1 路)。 */
LOG_EV_BOOT_CHECK = 19,
/* 0.1.4 新增:整片刀片不应答(本路 INA226 + PCA9555 连续 10 圈都 NAK,eFuse 开着)⇒ 关这一路。
`t_safety.h` 的 `T_SAFETY_BLADE_MUTE_N`。bus_mv / current_ua 是上一次读到的旧值,仅供参考。
出现 = 这一槽 `PRSNT#` 假在位,或刀片的 I²C 整个断了。 */
LOG_EV_BLADE_MUTE = 20,
LOG_EV_COUNT,
} log_ev_t;
+1 -1
查看文件
@@ -3,7 +3,7 @@
#define PHONERAIL_H
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
#define PHONERAIL_FW_VERSION "0.1.3-c5"
#define PHONERAIL_FW_VERSION "0.1.4-c5"
/* 通道数。五路直供,见设计文档 §2。 */
#define PHONERAIL_RAIL_COUNT 5
+23 -1
查看文件
@@ -43,6 +43,8 @@ const char *t_safety_ev_str(safety_ev_t ev)
return "mux失联全断";
case SAFETY_EV_SLOT_EMPTY:
return "刀片不在位";
case SAFETY_EV_BLADE_MUTE:
return "刀片不应答关路";
default:
return "?";
}
@@ -183,7 +185,27 @@ static hal_err_t cycle_rail(void *arg)
}
}
/* 判据就在这里跑:断 VBUS 要在本路通道选好、锁还持着的此刻(规矩 3)。 */
/* 整片刀片不应答(T_SAFETY_BLADE_MUTE_N 的理由)。**不走 `trip_rail_locked()`**:
它要先写这片 PCA9555 断 VBUS,而这片刚刚 NAK 过 —— 再发一次只是白等十几 ms 的
IDF NAK 超时、把本圈预算吃光(规矩 4)。只关 eFuse(锁外,GPIO 直连,永远关得掉)。
判据本身不发任何事务:两个 ok 位是上面刚读出来的。 */
safety_rail_t *st = &s->st[rail - 1];
if (rail_efuse_is_on(rail) && !s->rail_ok[rail - 1] && !s->io_ok[rail - 1]) {
if (st->mute_n < 0xFF) {
st->mute_n++;
}
if (st->mute_n >= T_SAFETY_BLADE_MUTE_N) {
st->pend_abort = true;
st->pend_efuse = true;
st->pend_ev = (uint8_t)SAFETY_EV_BLADE_MUTE;
/* 同 SR-5:不清零。关掉之后下一圈 en 为 false 自然清;写引脚失败的话下一圈立刻再关。 */
}
} else {
st->mute_n = 0;
}
/* 判据就在这里跑:断 VBUS 要在本路通道选好、锁还持着的此刻(规矩 3)。
rail_ok 为 false 时它第一道闸就返回,不会盖掉上面攒下的 BLADE_MUTE。 */
judge_locked(s, rail, a->now);
return first;
}
+13
查看文件
@@ -118,6 +118,16 @@ extern "C" {
在拿旧数据跑判据。判定成立之后**先脉冲一次 `MUX_RST#`**,再不行才全断。 */
#define T_SAFETY_MUX_FAIL_N 3
/* ⭐ 0.1.4:**整片刀片不应答**(本路 INA226 与 PCA9555 **同时** NAK)连续多少圈就关这一路。
与 §10.10 第一行「INA226 无应答 → 盲跑、保持供电」不冲突:那一行是**一颗**器件坏了,
刀片上的另一颗还答应;两颗都不答应而 mux 是好的(选不中的话本路读函数根本不会被调,
计不进来),只剩两种可能 ——
· 刀片根本不在,是 `PRSNT#` 假在位(2026-09-28 板 B 第 2 槽:卡低之后空槽 eFuse 开了约 4 h);
· 刀片在,但这一路完全失控:看不到 V/I、看不到 PG/FLT、**连 VBUS 都断不了**。
两种都是「继续供电但看不见」,和 mux 失联全断同一个道理,只是范围是一路。
10 圈 = 200 ms:本板实测单路 9 万圈 0 次 NAK,连续 10 次不是毛刺;空槽带 12 V 多 200 ms 无所谓。 */
#define T_SAFETY_BLADE_MUTE_N 10
/* ---- A10:事件(§10.7 的「单条」形状) -----------------------------------
A11 把这条记录塞进环形缓冲,这里只负责**产生**它。 */
@@ -130,6 +140,8 @@ typedef enum {
/* ⭐ R2 新增两条(计划 §5.8)。放在**末尾**:`evlog` 那边的数字是 flash 格式。 */
SAFETY_EV_MUX_LOST, /* TCA9548A 连续选不中 ⇒ 五路全部断电(单点) */
SAFETY_EV_SLOT_EMPTY, /* `PRSNT#` 为高(刀片不在位)⇒ 这一路不许带电 */
/* 0.1.4,同样只许加在末尾。 */
SAFETY_EV_BLADE_MUTE, /* 本路 INA226 + PCA9555 连续 T_SAFETY_BLADE_MUTE_N 圈都不应答 ⇒ 关这一路 */
} safety_ev_t;
typedef struct {
@@ -153,6 +165,7 @@ typedef struct {
bool en_on; /* 上一圈看到的 `EFUSE_EN` 影子,用来认它的上升沿 */
uint32_t en_ms; /* 上升沿的时刻,缓启动窗口从这里算(这个是真按毫秒的) */
uint8_t dev_n; /* 连着几圈越界了。回到范围内 / 这一路被关掉就清零 */
uint8_t mute_n; /* 开着电、整片刀片连着几圈不应答。任一颗答应了 / 这一路关着就清零 */
/* 本圈锁内判据留给锁外的两件事(规矩 3:VBUS 锁内断,eFuse 锁外关)。 */
uint8_t pend_ev; /* safety_ev_t,0 = 没有 */
+7 -3
查看文件
@@ -87,7 +87,8 @@ static void test_flash_format_is_frozen(void)
TEST_ASSERT_EQUAL_INT(17, LOG_EV_PG_TIMEOUT); /* A15 */
TEST_ASSERT_EQUAL_INT(18, LOG_EV_OTA); /* C2 */
TEST_ASSERT_EQUAL_INT(19, LOG_EV_BOOT_CHECK); /* 0.1.2 开机校验 */
TEST_ASSERT_EQUAL_INT(20, LOG_EV_COUNT);
TEST_ASSERT_EQUAL_INT(20, LOG_EV_BLADE_MUTE); /* 0.1.4 刀片不应答 */
TEST_ASSERT_EQUAL_INT(21, LOG_EV_COUNT);
}
/* §10.7 的「必记事件」有八类,A10 只产生其中两类。剩下的号先占住,
@@ -129,6 +130,7 @@ static void test_the_safety_mapping_is_explicit(void)
TEST_ASSERT_EQUAL_INT(LOG_EV_UNDER_VOLT, evlog_from_safety(SAFETY_EV_UNDER_VOLT));
TEST_ASSERT_EQUAL_INT(LOG_EV_REVERSE, evlog_from_safety(SAFETY_EV_REVERSE));
TEST_ASSERT_EQUAL_INT(LOG_EV_PUSHED_UP, evlog_from_safety(SAFETY_EV_PUSHED_UP));
TEST_ASSERT_EQUAL_INT(LOG_EV_BLADE_MUTE, evlog_from_safety(SAFETY_EV_BLADE_MUTE));
TEST_ASSERT_EQUAL_INT(LOG_EV_NONE, evlog_from_safety(SAFETY_EV_NONE));
TEST_ASSERT_EQUAL_INT(LOG_EV_NONE, evlog_from_safety(200)); /* 认不出的不许瞎猜 */
@@ -161,6 +163,7 @@ static void test_the_safety_mapping_is_a_table_not_an_offset(void)
TEST_ASSERT_EQUAL_INT(4, SAFETY_EV_PUSHED_UP);
TEST_ASSERT_EQUAL_INT(5, SAFETY_EV_MUX_LOST);
TEST_ASSERT_EQUAL_INT(6, SAFETY_EV_SLOT_EMPTY);
TEST_ASSERT_EQUAL_INT(7, SAFETY_EV_BLADE_MUTE);
/* 逐对:safety 号 → flash 号,两边都是裸数字。 */
TEST_ASSERT_EQUAL_INT(0, (int)evlog_from_safety(0)); /* NONE */
@@ -170,6 +173,7 @@ static void test_the_safety_mapping_is_a_table_not_an_offset(void)
TEST_ASSERT_EQUAL_INT(6, (int)evlog_from_safety(4)); /* PUSHED_UP */
TEST_ASSERT_EQUAL_INT(14, (int)evlog_from_safety(5)); /* MUX_LOST */
TEST_ASSERT_EQUAL_INT(15, (int)evlog_from_safety(6)); /* SLOT_EMPTY */
TEST_ASSERT_EQUAL_INT(20, (int)evlog_from_safety(7)); /* BLADE_MUTE(0.1.4) */
/* 而且这张表**在数学上不可能**是一个偏移:前四对差 2,后两对差 9。
写死这一条,是为了让「改成偏移」这件事在用例名上就说得清楚,
@@ -178,8 +182,8 @@ static void test_the_safety_mapping_is_a_table_not_an_offset(void)
TEST_ASSERT_EQUAL_INT(9, (int)evlog_from_safety(5) - (int)SAFETY_EV_MUX_LOST);
/* 越过表尾的号一律 NONE。偏移写法在这里会算出一个**越界但合法**的
`log_ev_t`(7 → 记成 eFuse 故障,200 → 记成 202 这种根本没有的号)。 */
TEST_ASSERT_EQUAL_INT(0, (int)evlog_from_safety(7));
`log_ev_t`(8 → 记成 PG 变化,200 → 记成 202 这种根本没有的号)。 */
TEST_ASSERT_EQUAL_INT(0, (int)evlog_from_safety(8));
TEST_ASSERT_EQUAL_INT(0, (int)evlog_from_safety(200));
TEST_ASSERT_EQUAL_INT(0, (int)evlog_from_safety(255));
}
+86
查看文件
@@ -133,6 +133,7 @@ static void catch_event(void *ctx, const safety_event_t *ev)
case SAFETY_EV_OVER_VOLT:
case SAFETY_EV_UNDER_VOLT:
case SAFETY_EV_SLOT_EMPTY:
case SAFETY_EV_BLADE_MUTE:
if (rail_efuse_is_on(ev->rail)) {
g_ev_before_action = true; /* 记这条时电早该断了 */
}
@@ -336,6 +337,11 @@ static void test_thresholds_match_the_documents(void)
已经落盘的旧记录改变含义(`t_safety.h` 那句「flash 格式」)。 */
TEST_ASSERT_EQUAL_INT(SAFETY_EV_PUSHED_UP + 1, SAFETY_EV_MUX_LOST);
TEST_ASSERT_EQUAL_INT(SAFETY_EV_MUX_LOST + 1, SAFETY_EV_SLOT_EMPTY);
TEST_ASSERT_EQUAL_INT(SAFETY_EV_SLOT_EMPTY + 1, SAFETY_EV_BLADE_MUTE);
/* 0.1.4 刀片不应答:连续 10 圈 = 200 ms。必须 > 1(一圈毛刺不许关一台手机)。 */
TEST_ASSERT_EQUAL_INT(10, T_SAFETY_BLADE_MUTE_N);
TEST_ASSERT_EQUAL_UINT32(200u, T_SAFETY_BLADE_MUTE_N * task_desc(TASK_SAFETY)->period_ms);
}
/* ---- 验收主项一:注入过压 → 20 ms 内关路并记事件(SR-5) ----------------- */
@@ -547,6 +553,82 @@ static void test_a_blind_rail_keeps_its_power(void)
TEST_ASSERT_EQUAL_INT(0, en_level(4));
}
/* ---- 0.1.4:整片刀片不应答 ⇒ 关路(§10.10 新增那一行) ------------------ */
static void mute_blade(int rail, bool mute)
{
sim_i2c_set_present_ch(ch_of(rail), rail_cfg(rail)->ina_addr, !mute);
sim_i2c_set_present_ch(ch_of(rail), PCA9555_ADDR, !mute);
}
/* 2026-09-28 板 B 第 2 槽的现场:`PRSNT#` 卡低(假在位)、刀片已经拔走、eFuse 还开着。
两颗都不答应满 10 圈 ⇒ 关这一路、记一条 BLADE_MUTE;别的路一根线都不动。 */
static void test_a_mute_blade_is_cut_after_ten_cycles(void)
{
power_up(2);
power_up(3);
const uint32_t other = en_writes(3);
const uint32_t out0 = g_w[1].n;
mute_blade(2, true); /* PRSNT2# 仍是低:slot_present(2) 照旧为真 */
beats(T_SAFETY_BLADE_MUTE_N - 1);
TEST_ASSERT_EQUAL_INT(1, en_level(2)); /* 9 圈:还没到 */
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
beat();
TEST_ASSERT_EQUAL_INT(0, en_level(2)); /* 第 10 圈:关 */
TEST_ASSERT_FALSE(rail_efuse_is_on(2));
TEST_ASSERT_EQUAL_INT(1, g_n_ev);
TEST_ASSERT_EQUAL_INT(SAFETY_EV_BLADE_MUTE, g_ev[0].type);
TEST_ASSERT_EQUAL_INT(2, g_ev[0].rail);
/* 没有去写那片刚 NAK 过的 PCA9555(断 VBUS 那一写只会白等超时) */
TEST_ASSERT_EQUAL_UINT32(out0, g_w[1].n);
/* 第 3 路一根线都没动 */
TEST_ASSERT_EQUAL_INT(1, en_level(3));
TEST_ASSERT_EQUAL_UINT32(other, en_writes(3));
beats(50); /* 关掉之后不再刷事件 */
TEST_ASSERT_EQUAL_INT(1, g_n_ev);
}
/* 只差一圈就恢复了:从头数,不关。 */
static void test_a_blade_that_answers_again_restarts_the_count(void)
{
power_up(2);
mute_blade(2, true);
beats(T_SAFETY_BLADE_MUTE_N - 1);
mute_blade(2, false);
beat();
mute_blade(2, true);
beats(T_SAFETY_BLADE_MUTE_N - 1);
TEST_ASSERT_EQUAL_INT(1, en_level(2));
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
beat();
TEST_ASSERT_EQUAL_INT(0, en_level(2));
}
/* §10.10 原有的两行不变:只有一颗不答应 ⇒ 盲跑、保持供电。 */
static void test_one_mute_chip_on_the_blade_still_keeps_power(void)
{
power_up(2);
sim_i2c_set_present_ch(ch_of(2), PCA9555_ADDR, false); /* 只有 PCA9555 不答应 */
beats(50);
TEST_ASSERT_EQUAL_INT(1, en_level(2));
sim_i2c_set_present_ch(ch_of(2), PCA9555_ADDR, true);
sim_i2c_set_present_ch(ch_of(2), rail_cfg(2)->ina_addr, false); /* 只有 INA226 不答应 */
beats(50);
TEST_ASSERT_EQUAL_INT(1, en_level(2));
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
}
/* 关着的路不数:空槽假在位、没人开它,不该每 200 ms 刷一条事件。 */
static void test_a_mute_blade_that_is_off_is_left_alone(void)
{
mute_blade(2, true);
beats(50);
TEST_ASSERT_EQUAL_INT(0, en_level(2));
TEST_ASSERT_EQUAL_INT(0, g_n_ev);
}
/* ---- ⭐ 关路这件事本身:R2 的顺序与「写给了哪一片」 ---------------------- */
/* ⭐⭐ 本包最要紧的一条。关路 = **先断 VBUS(锁内、本路通道上)再关 eFuse(锁外)**。
@@ -1203,6 +1285,10 @@ int main(void)
RUN_TEST(test_a_rail_that_is_off_is_never_judged);
RUN_TEST(test_the_soft_start_window_is_not_judged);
RUN_TEST(test_a_blind_rail_keeps_its_power);
RUN_TEST(test_a_mute_blade_is_cut_after_ten_cycles);
RUN_TEST(test_a_blade_that_answers_again_restarts_the_count);
RUN_TEST(test_one_mute_chip_on_the_blade_still_keeps_power);
RUN_TEST(test_a_mute_blade_that_is_off_is_left_alone);
RUN_TEST(test_the_cut_drops_vbus_inside_the_lock_on_this_rails_channel);
RUN_TEST(test_only_the_offending_rail_is_touched);
+3 -2
查看文件
@@ -36,7 +36,8 @@ EVENT_TYPES: Dict[int, Tuple[str, str]] = {
16: ("PD_LIMIT", "PD 限功率"),
17: ("PG_TIMEOUT", "上电等不到 PG"), # A15:arg bit0 = FLT# 在报,bit1 = 按电压判
18: ("OTA", "固件升级"), # C2:arg 1 开始 / 2 就绪 / 3 失败(mV 列=原因) / 4 自证健康 / 5 回滚 / 6 为换固件重启
19: ("BOOT_CHECK", "开机重按"), # 0.1.2:arg bit0~3 = 按了几次,bit7 = 最终没开起来(电源键弹片接触不良)
19: ("BOOT_CHECK", "开机重按"),
20: ("BLADE_MUTE", "刀片不应答关路"), # 0.1.4:eFuse 开着、本路 INA226 + PCA9555 连续 200 ms 都不应答 # 0.1.2:arg bit0~3 = 按了几次,bit7 = 最终没开起来(电源键弹片接触不良)
}
# LOG_EV_OTA 的 arg(firmware/main/ota.h 的 ota_ev_t)与失败原因(ota_err_t,失败那条的 mV 列)。⚠️ 冻结。
@@ -46,7 +47,7 @@ OTA_ERR_NAMES = {1: "下载失败", 2: "大小不对", 3: "哈希不对", 4: "
6: "切启动分区失败", 7: "没有备用槽 / 新固件未自证"}
# 严重程度:界面上标红的那些(会断电 / 断 VBUS / 硬件故障)。
EVENT_SEVERE = {3, 4, 5, 6, 7, 9, 11, 14, 17} # OTA 的失败 / 回滚、开机重按的「没开起来」在 api.events 里按 arg 单独标红
EVENT_SEVERE = {3, 4, 5, 6, 7, 9, 11, 14, 17, 20} # OTA 的失败 / 回滚、开机重按的「没开起来」在 api.events 里按 arg 单独标红
BOOT_CHECK_FAILED = 0x80 # LOG_EV_BOOT_CHECK 的 arg bit7
# cmd_id_t(firmware/main/cmd.h),CMD_REJECT 事件的 arg 就是它。
+2 -2
查看文件
@@ -90,8 +90,8 @@ import { mA, V, W, C, ago, fmtTime, railsMw, railHealth, LEVEL_COLOR, RECOVERY_Z
// 与 backend/prrack/protocol.py 的 EVENT_TYPES / EVENT_SEVERE 同一张表(数字来自固件 evlog.h)
const EV = ['', '板子上电', '事件丢失', '过压关路', '欠压关路', '反灌断 VBUS', '顶高断 VBUS', 'eFuse 故障',
'PG 变化', 'I²C 失联', '过温降载', '风扇停转', '重启序列', '命令被拒', 'mux 失联全断', '刀片不在位', 'PD 限功率',
'上电等不到 PG', '固件升级', '开机重按']
const SEVERE = new Set([3, 4, 5, 6, 7, 9, 11, 14, 17])
'上电等不到 PG', '固件升级', '开机重按', '刀片不应答关路']
const SEVERE = new Set([3, 4, 5, 6, 7, 9, 11, 14, 17, 20])
// 开机重按(19)只有 arg bit7「最终没开起来」才算严重
const isSevere = ev => SEVERE.has(ev[3]) || (ev[3] === 19 && (ev[4] & 0x80) !== 0)
const evName = t => EV[t] || `事件 ${t}`
+36 -6
查看文件
@@ -280,7 +280,7 @@ USB 通断」这些原本要人手做的动作全部电气化,交给板上 MCU
| **SW-2** | 20 ms 周期巡检六路 INA226,电压偏离 >+100 / −250 mV 持续 20 ms 即关路告警(SR-5) |
| **SW-3** | 过流 / 故障事件记录:路号、时刻、电流值 —— 排查排线接触不良的唯一线索 |
| **SW-4** | 单路电压微调接口(跑不稳的手机提到 4.0 V,或反向降压降温),**上限受 SR-13 约束** |
| **SW-5** | 侧键序列:短按 100 ms 唤醒 / 长按 5 s 开机(小米8 实测 2 s 不够,2026-09-28)/ 长按 10 s 强制重启 / VOL−+PWR 进 Fastboot / VOL++PWR 进 Recovery |
| **SW-5** | 侧键序列:短按 100 ms 唤醒 / 长按 5 s 开机(小米8 实测 2 s 不够,2026-09-28)/ 长按 10 s 强制重启 / VOL−+PWR 进 Fastboot / VOL++PWR 进 Recovery。硬重启末尾的开机键**按完看电流**:松键后 3~5 s 内 < 100 mA 就重按,最多 3 次,仍不开记事件 19「开机重按」并保持供电(固件 0.1.2,起因是电源键弹片接触不良) |
| **SW-6** | EDL 时序**固定为** `EDL_EN=1` → `VBUS_EN=1` → 等枚举 → `EDL_EN=0`(先短接再插 USB) |
| **SW-7** | 开机脚本执行「关充电」(写 `input_suspend` 类节点),路径需在首台机上确认后写死 |
| **SW-8** | USB 反灌保护:检测到该路电流转负或母线上升,**200 ms 内**断开该路 VBUS |
@@ -918,19 +918,23 @@ base = `phonerail/<id>`,id = 板子 STA MAC 的 12 位小写十六进制(板
### 10.10 降级矩阵
> ⚠️ **2026-09-16 按 R2 硬件更新**(底板 + 5 片刀片,见 `刀片式方案规划-R2.md`)。
> 新增了三行(mux / `PRSNT#` / PD),改了两行(PCA9555、LM75),理由写在表后。
| 失效 | 检测方式 | 行为 |
|---|---|---|
| 某片 INA226 无应答 | I²C NAK | 该路失去遥测 → **保持供电**,标记为「盲跑」,禁用该路的 SR-5 / SW-8 判据并持续告警 |
| I²C 总线挂死(SDA 被拉低) | 连续超时 | 发 9 个时钟脉冲做总线恢复;仍不通则复位 I²C 外设;再不通进降级模式 |
| DAC 无应答 | I²C NAK | **不关输出** —— DAC 不响应时输出停在 NVM 默认的 3.90 V,是安全值 |
| PCA9555 无应答 | I²C NAK | 失去 FAULT / PG 可见性;改用 INA226 的电压判据兜底 |
| 载板 LM75BD 无应答 | I²C NAK | 多半是排针接触不良(它在排针另一侧)→ **风扇转到 100 %** 并告警,不要按「读不到温度=不热」处理 |
| **某片刀片的 PCA9555 无应答**(R2 改) | I²C NAK | 失去该路的 FAULT / PG 可见性 → **不关输出**,改用 INA226 的电压判据兜底并持续告警。⚠️ **R2 新增的代价:这一路的 `VBUS_EN` 也控制不了了** —— 它在这颗芯片的 P0.4 上。见表后第 1 条 |
| **TCA9548A(mux)连续选不中**(R2 新增) | 连续 N 次选通失败(N 待实测,暂定 3) | **先脉冲一次 `MUX_RST#`**;复位后仍失败 ⇒ **五路全部断电**并记事件。理由见表后第 2 条 |
| **`PRSNT#` 为高(刀片不在位)**(R2 新增) | GPIO 直读,不占总线 | 该路**不许上电**,已上电的立刻关断并记事件。空槽上电 = 金手指裸露带 12 V |
| **整片刀片不应答**:本路 INA226 **与** PCA9555 同时 NAK,mux 正常(0.1.4 新增) | 连续 10 圈(200 ms),只在 eFuse 开着时数 | **关这一路**(只关 eFuse,不再去写那片 PCA9555)并记事件 20 `BLADE_MUTE`。理由见表后第 3 条 |
| **CH224A 读不到 / PD 档位低于预期**(R2 新增) | I²C NAK 或 `0x50` 读数偏小 | **不关输出**,按读到的档位(读不到就按最保守的 60 W)设整板功率上限,逼近上限时按优先级降载(§8.3) |
| ~~载板 LM75BD 无应答~~(R2 作废) | — | **R2 没有载板,这片不存在**。温度只剩两路板载 NTC;两路都读不到时仍按老规矩处理:**风扇转到 100 %** 并告警,不要按「读不到温度 = 不热」处理 |
| 上位机失联 | 心跳超时 | 见 10.2:**什么都不做** |
| 看门狗复位 | — | 复位即全关(SR-6),重启后按 10.9 恢复,并记一条复位事件 |
**降级模式**的定义:只保留「关断能力」与「事件记录」,不再自动上电、不接受调压命令,
持续告警。宁可五台手机不开机,也不要在盲态下带电跑。
**A15 落地时补的三条(2026-09-27,逐行用例在 `firmware/test/test_bringup.c`):**
- **mux 失联时主干也够不着**:`i2cbus_xact(主干)` 要先给 mux 写「全关」,这一步失败回调就不跑。
所以启动时 mux 复位后仍不通 ⇒ 不去碰 DAC / 输入侧 / CH224A(不刷三条误导性的 I2C_LOST),
@@ -939,6 +943,32 @@ base = `phonerail/<id>`,id = 板子 STA MAC 的 12 位小写十六进制(板
- **I²C 总线挂死**那一行(9 个时钟恢复 + 复位外设)**还没做**,在 HAL 层,待办。
- CH224A:设计上挂 mux 通道 5,**板 B 实测在主干应答**(0x22/0x23);启动时两处都找。
**表后的两条,都是 R2 的架构后果,不是待办:**
1. **`VBUS_EN` 从 GPIO 变成了 I²C 后面的一个位。** R1 里 `t_safety` 断 VBUS 是一次
`gpio_set_level()`,几百 ns、永远成功;R2 里它是「选 mux 通道 + 写刀片 PCA9555」,
约 0.16 ms,而且**I²C 失联时断不掉**。兜底是关 `EFUSE_EN`(GPIO 直连,永远关得掉),
但要说清楚:**关 eFuse 并不能阻止手机经 USB 往回灌**,它只是让这一路不再直供电。
真正的反灌保护是硬件那一层(设计文档 §15.2 的母线泄放 + 电容),固件这一层在
I²C 失联时**确实降级了**。这是 R2 用一根跨板针换来的代价,验收在 §12.1 的 B7。
⚠️ 相应地,`EFUSE_EN` **必须永远是 ESP32 GPIO 直连**(金手指 A11 独占一针)——
它是保护链第三层唯一的执行手段,任何时候都不许挪到 I²C 后面。
2. **mux 是全板唯一的单点。** 它一挂,五路的电压电流**全部看不见**,SR-5 的整条监控链
同时断掉 —— 这时「继续供电但看不见」比「停机」危险,所以判据是**全断**,
与上面「某片 INA226 无应答就保持供电」的处理方向相反。区别在于范围:
一片失联时还有四路看得见,mux 失联时一路都看不见。
之所以先脉冲一次 `MUX_RST#` 再判,是因为一次总线毛刺就停掉五台手机代价太大。
3. **整片刀片不应答 ≠ 一颗器件不应答**(2026-09-28 加,用户拍板)。第一行「INA226 无应答 → 盲跑」
和 PCA9555 那一行的前提是刀片上**另一颗还答应**。两颗同时不答应、而 mux 正常(选不中的话读不到这一步),
只剩两种可能:`PRSNT#` 假在位、刀片其实不在(板 B 第 2 槽实测:`PRSNT2#` 卡低,刀片挪走后空槽 eFuse
开了约 4 h);或刀片在但这一路 V/I、PG/FLT、`VBUS_EN` 全都够不着。两种都是「继续供电但看不见」,
按第 2 条同一个道理处理,范围是一路。关着的路不数(假在位的空槽不会每 200 ms 刷一条)。
**降级模式**的定义:只保留「关断能力」与「事件记录」,不再自动上电、不接受调压命令,
持续告警。宁可五台手机不开机,也不要在盲态下带电跑。
### 10.11 关于 FR-12「模拟电量曲线」的一个说明
这条需求的实现方式和它的字面意思不一样,值得在架构层写清楚:**手机的电量计是从