diff --git a/PROGRESS.md b/PROGRESS.md index ef7c309..b527565 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,5 +1,10 @@ # 进度与交接 +## ⭐ 2026-09-27 夜:固件 0.1.1-c3 —— I²C 不应答不再被报成「没初始化 / 忙」 + +- IDF 5.4 的 NACK 返回 `ESP_ERR_INVALID_STATE`,HAL 照翻成 STATE ⇒ 关电在 PCA9555 不应答时回 `busy`(其实已经关了)。 + HAL 改翻成 NAK;命令层 I²C 路径失败一律 `io`。板 B 实测 `power off` 回 io、I2C_LOST 错误码 5 → 2。详见 `firmware/HANDOFF.md`。 + ## ⭐ 2026-09-27 夜:固件 0.1.0-c3「重启不断电」,已上板 - OTA 换固件 / 超时回滚锁住 `EFUSE_EN`、新固件接管,手机不断电;崩溃 / 看门狗 / 上电仍全关。**SR-6 已改写**(需求 §10.9「重启不断电」)。 diff --git a/firmware/HANDOFF.md b/firmware/HANDOFF.md index 27887cc..7e950bb 100644 --- a/firmware/HANDOFF.md +++ b/firmware/HANDOFF.md @@ -1,11 +1,21 @@ # 交接 -更新:2026-09-27 **0.1.0-c3:重启不断电**(计划内重启锁住 EFUSE_EN、新固件接管;SR-6 已改写) +更新:2026-09-27 **0.1.1-c3**:修了 I²C 不应答被报成 STATE / 关电回 busy(下面「0.1.1」);**0.1.0-c3:重启不断电** ## 一句话现状 OTA 换固件 / 超时回滚现在走 `warm_restart()`(`main/warm_esp.c`):目标固件认接管 ⇒ 手机不断电; -崩溃 / 看门狗 / 上电 / 兜底回滚仍全关(SR-6)。**26 个 ctest、448 个用例全绿**。板 B 跑 **0.1.0-c3**(经 OTA 推上去的)。 +崩溃 / 看门狗 / 上电 / 兜底回滚仍全关(SR-6)。**26 个 ctest、448 个用例全绿**。板 B 跑 **0.1.1-c3**(经 OTA 推上去的)。 定稿写在需求 §10.9「重启不断电」。C2 OTA 的说明在下面。 +## 0.1.1:I²C 不应答被报成 STATE(真板独有,sim 测不出) +- 根因:IDF 5.4 的 `i2c_master_transmit/receive/transmit_receive` 在**从机 NACK / SCL 超时**时返回 `ESP_ERR_INVALID_STATE`, + HAL 照翻成 `HAL_ERR_STATE`(= 没 init)。于是:I2C_LOST 事件的错误码是 5;命令层 `from_hal` 又把 STATE 当成「执行器忙」⇒ + PCA9555 不应答时 `power off` 回 `busy`(eFuse 其实已经关了、意图也清了),上位机会以为没执行。 +- 修:`hal_i2c_esp.c` 传输函数用 `map_xfer_err`(INVALID_STATE → NAK);`cmd_exec.c` 新增 `from_bus`, + I²C / GPIO 路径(set_voltage / vbus / power)失败一律 `io`,`from_hal` 只留给执行器。 +- 测试:sim 加 `sim_i2c_inject_err_ch()`(指定注入的错误码);`test_cmd` 加一段 STATE 注入 ⇒ 必须是 io 不是 busy + (把关电那行改回 from_hal 时这段确实红,回应就是台架上看到的 `{"err":"busy"}`)。 +- 上板(台架镜像,CH1 强制在位):`power off` 回 `io`、CH1 关掉;I2C_LOST 的错误码 5 → **2**。 + ## 重启不断电(c3) - 三道闸:RTC 标记(软件复位 + 魔数 / 反码,一次性)· 目标镜像能力标记(第 288 字节 `PRCAPS1`,老固件不认就不锁)· 起来后读 EN 真电平核对。 - `rail_en_init_keep(adopt)` **五根一律放开 hold**:崩溃时上一条命的锁可能还在。 @@ -17,7 +27,6 @@ OTA 换固件 / 超时回滚现在走 `warm_restart()`(`main/warm_esp.c`): ## 下一步(没人定,候选) - 示波器验证:插一片刀片,CH 开着,OTA 带重启,抓 EN / 输出电压(`scope_*` 工具)。 -- 小毛病(以前就有):PCA9555 不应答时 `power off` 回 `busy`,其实 eFuse 已经关了、意图也清了(`from_hal` 把 VBUS 失败的 HAL_ERR_STATE 映射成了 busy)。 - 服务器 fw 目录不会自动清理旧镜像(每个 ~1 MB,问题不大)。 ## 踩到的坑(c3) diff --git a/firmware/components/phonerail_hal/esp/hal_i2c_esp.c b/firmware/components/phonerail_hal/esp/hal_i2c_esp.c index 1ff214c..ccc87d1 100644 --- a/firmware/components/phonerail_hal/esp/hal_i2c_esp.c +++ b/firmware/components/phonerail_hal/esp/hal_i2c_esp.c @@ -25,6 +25,17 @@ static hal_err_t map_err(esp_err_t e) } } +/* 传输函数专用。⚠️ IDF 5.4 的 i2c_master_transmit / receive / transmit_receive 在**从机 NACK** + (以及硬件 SCL 超时)时返回的是 ESP_ERR_INVALID_STATE,不是 ESP_FAIL(i2c_master.c + s_i2c_synchronous_transaction:「status != DONE ⇒ INVALID_STATE」)。照 map_err 翻成 + HAL_ERR_STATE(= 没 init)的话,真板上所有「器件不应答」都长得像「没初始化」—— + 2026-09-27 实测:I2C_LOST 事件的错误码是 5,`power` 关电回 `busy`。sim 回的是 NAK,宿主机测不出来。 + 总线没建起来的情况在调它之前已经单独返回 HAL_ERR_STATE 了(dev_of)。 */ +static hal_err_t map_xfer_err(esp_err_t e) +{ + return (e == ESP_ERR_INVALID_STATE) ? HAL_ERR_NAK : map_err(e); +} + hal_err_t hal_i2c_init(const hal_i2c_cfg_t *cfg) { if (cfg == NULL || cfg->sda_gpio < 0 || cfg->scl_gpio < 0) { @@ -83,7 +94,7 @@ hal_err_t hal_i2c_write(uint8_t addr, const uint8_t *data, size_t len) return err; } hal_i2c_acct_add(len, 0); - return map_err(i2c_master_transmit(dev, data, len, I2C_XFER_TIMEOUT_MS)); + return map_xfer_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) @@ -94,7 +105,7 @@ hal_err_t hal_i2c_read(uint8_t addr, uint8_t *data, size_t len) return err; } hal_i2c_acct_add(0, len); - return map_err(i2c_master_receive(dev, data, len, I2C_XFER_TIMEOUT_MS)); + return map_xfer_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, @@ -106,7 +117,7 @@ hal_err_t hal_i2c_write_read(uint8_t addr, const uint8_t *wr, size_t wr_len, return err; } hal_i2c_acct_add(wr_len, rd_len); - return map_err(i2c_master_transmit_receive(dev, wr, wr_len, rd, rd_len, + return map_xfer_err(i2c_master_transmit_receive(dev, wr, wr_len, rd, rd_len, I2C_XFER_TIMEOUT_MS)); } diff --git a/firmware/components/phonerail_hal/sim/hal_i2c_sim.c b/firmware/components/phonerail_hal/sim/hal_i2c_sim.c index 2d94b34..09ebe22 100644 --- a/firmware/components/phonerail_hal/sim/hal_i2c_sim.c +++ b/firmware/components/phonerail_hal/sim/hal_i2c_sim.c @@ -177,7 +177,11 @@ static sim_i2c_dev_t *must_ch(int ch, uint8_t addr) 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_inject_nak(uint8_t addr, int count) +{ + must(addr)->nak_left = count; + must(addr)->fail_err = HAL_OK; +} void sim_i2c_set_present(uint8_t addr, bool present) { must(addr)->present = present; } void sim_i2c_set_reg_ch(int ch, uint8_t addr, uint8_t reg, uint16_t val) @@ -191,6 +195,12 @@ uint16_t sim_i2c_get_reg_ch(int ch, uint8_t addr, uint8_t reg) void sim_i2c_inject_nak_ch(int ch, uint8_t addr, int count) { must_ch(ch, addr)->nak_left = count; + must_ch(ch, addr)->fail_err = HAL_OK; +} +void sim_i2c_inject_err_ch(int ch, uint8_t addr, int count, hal_err_t err) +{ + must_ch(ch, addr)->nak_left = count; + must_ch(ch, addr)->fail_err = err; } void sim_i2c_set_present_ch(int ch, uint8_t addr, bool present) { @@ -237,7 +247,7 @@ static hal_err_t take(uint8_t addr, sim_i2c_dev_t **out) if (d->nak_left > 0) { d->nak_left--; } - return HAL_ERR_NAK; + return d->fail_err != HAL_OK ? d->fail_err : HAL_ERR_NAK; } *out = d; return HAL_OK; diff --git a/firmware/components/phonerail_hal/sim/sim_hal.h b/firmware/components/phonerail_hal/sim/sim_hal.h index 0883449..ef7d673 100644 --- a/firmware/components/phonerail_hal/sim/sim_hal.h +++ b/firmware/components/phonerail_hal/sim/sim_hal.h @@ -57,6 +57,7 @@ typedef struct sim_i2c_dev { 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; /* 事务计数,可用来断言「驱动真的访问过它 / 没多访问」 */ @@ -98,6 +99,9 @@ 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 里 diff --git a/firmware/main/cmd_exec.c b/firmware/main/cmd_exec.c index 7456015..06e21f6 100644 --- a/firmware/main/cmd_exec.c +++ b/firmware/main/cmd_exec.c @@ -38,6 +38,7 @@ static cmd_err_t need_rail(ctx_t *c) return CMD_OK; } +/* 执行器(t_actuator_*)的返回值 → 回应。它的 HAL_ERR_STATE 就是「这一路正在跑别的序列」。 */ static cmd_err_t from_hal(hal_err_t e) { switch (e) { @@ -48,6 +49,18 @@ static cmd_err_t from_hal(hal_err_t e) } } +/* 直接动器件(I²C / GPIO)的返回值 → 回应。**失败一律 io**,不许借用 from_hal: + 总线上的 HAL_ERR_STATE 不是「忙」(2026-09-27:PCA9555 不应答时关电回了 busy, + 上位机会以为没执行、换个时机再关一次,其实 eFuse 早就关了)。 */ +static cmd_err_t from_bus(hal_err_t e) +{ + switch (e) { + case HAL_OK: return CMD_OK; + case HAL_ERR_ARG: return CMD_ERR_RANGE; + default: return CMD_ERR_IO; + } +} + /* ---- 各条命令 ------------------------------------------------------------ */ typedef struct { @@ -79,7 +92,7 @@ cmd_err_t do_set_voltage(ctx_t *c) /* ⚠️ DAC 在主干上,但 `rail_set_mv()` 自己**不拿锁**(它比 i2cbus 早)。 不包这一层就是在巡检的事务中间插一笔 —— 总线上两个主机同时说话。 */ set_mv_arg_t a = { .rail = c->rail, .mv = (int)mv }; - e = from_hal(i2cbus_xact(I2CBUS_CH_TRUNK, SLOT_LOCK_TIMEOUT_MS, set_mv_fn, &a)); + e = from_bus(i2cbus_xact(I2CBUS_CH_TRUNK, SLOT_LOCK_TIMEOUT_MS, set_mv_fn, &a)); if (e == CMD_OK) { cj_w(c->w, ",\"set\":%d", rail_setpoint_mv(c->rail)); } @@ -196,7 +209,7 @@ cmd_err_t do_vbus(ctx_t *c) if (on && !rail_efuse_is_on(c->rail)) { return reject(c, CMD_ERR_STATE, 1); } - return from_hal(slot_vbus_enable(c->rail, on)); + return from_bus(slot_vbus_enable(c->rail, on)); } /* A15:意图出口(cmd.h 的 `cmd_intent_fn`)。 */ @@ -226,7 +239,7 @@ cmd_err_t do_power(ctx_t *c) if (ee == HAL_OK) { note_intent(c->rail, true); } - return from_hal(ee); + return from_bus(ee); } /* 关:与 `rail_all_off()` 同序 —— **先断 VBUS 再关 eFuse**。 VBUS 走 I²C 可能断不掉,eFuse 是 GPIO 永远关得掉:前者失败也照样关后者, @@ -236,7 +249,7 @@ cmd_err_t do_power(ctx_t *c) if (ee == HAL_OK) { note_intent(c->rail, false); /* 直供确实断了:下次开机也别开它 */ } - return from_hal(ee != HAL_OK ? ee : ev); + return from_bus(ee != HAL_OK ? ee : ev); } cmd_err_t do_fan(ctx_t *c) diff --git a/firmware/main/phonerail.h b/firmware/main/phonerail.h index 504b0e9..0362238 100644 --- a/firmware/main/phonerail.h +++ b/firmware/main/phonerail.h @@ -3,7 +3,7 @@ #define PHONERAIL_H /* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */ -#define PHONERAIL_FW_VERSION "0.1.0-c3" +#define PHONERAIL_FW_VERSION "0.1.1-c3" /* 通道数。五路直供,见设计文档 §2。 */ #define PHONERAIL_RAIL_COUNT 5 diff --git a/firmware/test/test_cmd.c b/firmware/test/test_cmd.c index 5cb20c9..79af22b 100644 --- a/firmware/test/test_cmd.c +++ b/firmware/test/test_cmd.c @@ -523,6 +523,17 @@ static void test_power_off_drops_vbus_and_always_drops_the_efuse(void) ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"power\",\"rail\":2,\"on\":false}"), "\"err\":\"io\""); TEST_ASSERT_EQUAL_INT(0, en_pin(2)); /* 直供照样断了 */ TEST_ASSERT_TRUE(slot_vbus_is_on(2)); /* 影子如实留在 on(slot.h:倒向安全的一侧) */ + + /* ⭐ 2026-09-27 上板实测的 bug:器件不应答被报成 HAL_ERR_STATE(IDF 5.4 的 NACK 码), + 命令层又把 STATE 当成「执行器忙」回了 busy —— 上位机会以为没执行。总线上的失败一律 io。 */ + ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true"); + ASSERT_HAS(send("{\"seq\":8,\"cmd\":\"vbus\",\"rail\":3,\"on\":true}"), "\"ok\":true"); + sim_i2c_inject_err_ch(rail_cfg(3)->mux_ch, PCA9555_ADDR, 100, HAL_ERR_STATE); + const char *r = send("{\"seq\":9,\"cmd\":\"power\",\"rail\":3,\"on\":false}"); + ASSERT_HAS(r, "\"err\":\"io\""); + ASSERT_LACKS(r, "busy"); + TEST_ASSERT_EQUAL_INT(0, en_pin(3)); + ASSERT_HAS(send("{\"seq\":10,\"cmd\":\"vbus\",\"rail\":3,\"on\":false}"), "\"err\":\"io\""); } /* A15:只有「真的开成 / 关成了」才改意图(下次开机按它恢复);被拒的、判重命中的都不改。 */