diff --git a/firmware/HANDOFF.md b/firmware/HANDOFF.md index c87c006..0048022 100644 --- a/firmware/HANDOFF.md +++ b/firmware/HANDOFF.md @@ -1,5 +1,38 @@ # 交接 -更新:2026-09-28 23:20 固件 **0.1.13-c5**(断电自动退出刷机模式);板 B 经 OTA 推上;rack 已部署 +更新:2026-09-30 固件 **0.1.16-c5**(开机键统一 5 s);板 B 经 OTA 推上;rack 已部署 + +## 0.1.15 / 0.1.16:开机长按 3 s 试验失败,所有开机操作统一 5 s(2026-09-30) +- 0.1.15 把 `ACT_KEY_MS_ON` 5000 → 3000:板 B 第 2 路 `power_cycle` 三轮全是「按住 330~390 mA、松手掉回 0」⇒ `no-boot`(事件 19); + 同一台手工 `key pwr 5000` 一次开起来(松手后稳在 ~250 mA)。⇒ 小米8 开机键 2 s、3 s 都不够,**别再往下调**。 +- 0.1.16 改回 5000;rack 单路「更多 → 开机」原来发的是 `ms:2000`(多半从来就开不了机),改成 5000、文案「长按电源 5 秒」。 +- 上板:第 3 路 `power_cycle` → hr-boot 5 s → 松键后 ~210 mA 平台 → `res ok`。 + +## 0.1.14:Hub 按需启停(2026-09-29,用户定「方案一:GPIO8 按住复位」) +- 起因:五路 VBUS 全关时,上行连着电脑 Hub 仍是活动态(第 6 口的 ESP32 USB 让电脑挂不起它),约多耗 1 W。 +- **推翻 R2 D13 代价 2 / R-11「运行期不许动 `HUB_RST#`」**,理由与退路见 `刀片式方案规划-R2.md` §8.2a、`main/hubctl.h` 文件头。 +- 新模块 `hubctl.{h,c}`:执行器任务每拍 `hubctl_tick()`(空闲时 ≤100 ms 一拍)。开的理由(按优先级) + `mode` 常开 > `vbus` 任一路 VBUS > `flash` 刷机模式 > `act` 执行器忙 > `hold` 保持窗口 > `link` MQTT 断满 60 s; + 都没有时再给 `idle` 10 分钟(`HUB_IDLE_MS`),从最后一次「需要」算起 —— 启动、`UP_VBUS_DET` 上升沿、USB 上来一条命令都算。 + 关 = GPIO8 配输出拉低;开 = 配成输入靠 R716 拉高,**从不驱高**。 +- **命令 15 `hub`**(两条链路都收,会动东西 ⇒ 必须带 seq): + `{"cmd":"hub","mode":"auto"|"on"}` 改模式(NVS 键 `hub_mode`,默认 auto); + `{"cmd":"hub","min":30}` 保持 1~240 分钟;`{"cmd":"hub","min":0}` 取消保持并结束 idle 窗口(没别的理由下一拍就关,量功耗用)。 + 两个键可同时给。回应带 `"hub":{...}`。 +- status 新增 `"hub":{"on":true,"mode":"auto","why":"vbus","hold":秒,"idle":秒}`(满配 1140 / 1536 字节)。 +- **事件 22 `LOG_EV_HUB`**(rail 0):arg 0 = 关、1 = 开、0x10|模式 = 改模式;bus_mv = 开的理由(`hub_why_t`,冻结: + 0 off / 1 mode / 2 vbus / 3 flash / 4 act / 5 hold / 6 link / 7 idle)。 +- `bringup.c` 的上电脉冲不变(仍只在 POWERON);脉冲后 `hubctl_init()` 把脚放成输入。 +- 测试:新增 `test_hubctl`(9 例:启动窗口、理由优先级、断网 60 s、电脑插上 / USB 命令刷新、保持与 min=0、 + 时钟回绕不复活、引脚从不写 1 且每次翻转只记一条、模式存盘、NVS 垃圾值回 auto),`test_cmd` +1(hub 命令)、 + `test_evlog` / 冻结编号同步;**27 ctest 全绿**;IDF 构建通过(0xf5960,余 68%)。 +- ✅ **板 B 实测(2026-09-29,0.1.14-c5,三路手机开着、VBUS 全关、上行插着电脑)**:Hub 开 / 关交替各 60 s, + 看「底板损耗 = 输入 − 三路主板合计」(手机电流 ±100 mA 的抖动会淹没输入电流):开 **1.77 W**,关 0.69 / 0.72 / 0.80 W ⇒ + **Hub 空转 ≈ 1.03 W**(12 V 输入电流 377 → 288 mA)。原始数据 `10.5.5.13:/root/phonerail-src/bench-hub-20260929.jsonl`, + 脚本 `bench-hub.sh` 同目录。`hub min:0` 下一拍就关、`hub min:3` 立刻开(why = hold),事件 22 各记一条。 +- ✅ **rack**:白名单放行 `hub`(mode auto/on、min 0~240);事件 22 命名(唤醒理由 / 休眠 / 模式改为…,mV 列置空); + 单板页「Hub」一行:状态 + 按需 / 常开 + 「唤醒 30 分」+「马上休眠」;模拟板同步。pytest 50 例,已部署。 +- ⏳ **没验证**:上行电脑不是本机也不是服务器,电脑那一侧 Hub 消失 / 重现、手机 VBUS 开后 adb 1~2 s 回来,都还没亲眼看过。 +- ⚠️ 现在起板 B 没人用 USB 满 10 分钟 COM 口就消失 —— 要 USB 烧录 / 看日志,先在 rack 点「唤醒 30 分」(或 MQTT 发 `hub min`)。 ## 0.1.13:这一路断了电 ⇒ 刷机模式一并退出(2026-09-28) - 起因:第 2 槽刀片带电拔走后,刷机模式还挂了约 48 分钟、占着「最多 2 路」的名额。`check_present` / SR-5 / 刀片不应答 / diff --git a/firmware/host/CMakeLists.txt b/firmware/host/CMakeLists.txt index 6054d29..1257267 100644 --- a/firmware/host/CMakeLists.txt +++ b/firmware/host/CMakeLists.txt @@ -29,6 +29,7 @@ set(PHONERAIL_CORE_SRCS ${FW_ROOT}/main/sched.c ${FW_ROOT}/main/tasks.c ${FW_ROOT}/main/chgmode.c + ${FW_ROOT}/main/hubctl.c ${FW_ROOT}/main/t_safety.c ${FW_ROOT}/main/t_actuator.c ${FW_ROOT}/main/t_thermal.c diff --git a/firmware/main/CMakeLists.txt b/firmware/main/CMakeLists.txt index a764136..59ce29b 100644 --- a/firmware/main/CMakeLists.txt +++ b/firmware/main/CMakeLists.txt @@ -13,6 +13,7 @@ idf_component_register( "sched.c" "tasks.c" "chgmode.c" + "hubctl.c" "t_safety.c" "t_actuator.c" "t_thermal.c" diff --git a/firmware/main/app_main.c b/firmware/main/app_main.c index bee2bda..f19f3fa 100644 --- a/firmware/main/app_main.c +++ b/firmware/main/app_main.c @@ -16,6 +16,7 @@ #include "hal_i2c.h" #include "hal_lock.h" #include "hal_time.h" +#include "hubctl.h" #include "i2cbus.h" #include "ina226.h" #include "pca9555.h" @@ -231,6 +232,7 @@ static void task_actuator(void *arg) cmd_msg_t m; for (;;) { cmd_chgmode_tick(hal_time_now_ms()); /* 刷机模式到期清理 + 记事件(chgmode.h:只在本任务写) */ + hubctl_tick(hal_time_now_ms()); /* Hub 按需启停(hubctl.h:只在本任务写)。空闲时 ≤100 ms 一拍 */ const TickType_t t = pdMS_TO_TICKS(t_actuator_step()); /* ⚠️ `CONFIG_FREERTOS_HZ=100` ⇒ 一个 tick 是 10 ms,`pdMS_TO_TICKS(5)` 是 **0**,而「等 0 tick」只是查一眼就回来 —— 那会让这个中优先级任务 @@ -342,6 +344,43 @@ static void want_save(void *ctx, int rail, bool on) } } +/* ---- Hub 模式的持久化(0.1.14,hubctl.h)------------------------------------- + 同一个命名空间,一个字节。新板 / 读不到 = AUTO(用户 2026-09-29 定:默认按需)。 */ +#define HUB_KEY "hub_mode" + +static uint8_t hub_mode_load(void) +{ + nvs_handle_t h; + uint8_t v = HUB_MODE_AUTO; + if (nvs_open(WANT_NS, NVS_READONLY, &h) == ESP_OK) { + if (nvs_get_u8(h, HUB_KEY, &v) != ESP_OK || v >= HUB_MODE_COUNT) { + v = HUB_MODE_AUTO; + } + nvs_close(h); + } + return v; +} + +static void hub_mode_save(void *ctx, hub_mode_t mode) +{ + (void)ctx; + if (hub_mode_load() == (uint8_t)mode) { + return; /* 没变就不写 flash */ + } + nvs_handle_t h; + esp_err_t e = nvs_open(WANT_NS, NVS_READWRITE, &h); + if (e == ESP_OK) { + e = nvs_set_u8(h, HUB_KEY, (uint8_t)mode); + if (e == ESP_OK) { + e = nvs_commit(h); + } + nvs_close(h); + } + if (e != ESP_OK) { + ESP_LOGW(TAG, "Hub 模式没存进 NVS:%s(这次照样生效)", esp_err_to_name(e)); + } +} + static void log_report(const bringup_report_t *r) { ESP_LOGI(TAG, "启动:降级位 0x%02x,空槽 0x%02x,盲跑 0x%02x,PCA 失联 0x%02x,PD %u mA → 上限 %u mW", @@ -441,6 +480,12 @@ void app_main(void) ESP_LOGE(TAG, "风扇起不来:%s —— 没有主动散热", hal_err_str(err)); } + /* Hub 按需启停(0.1.14)。放开 `HUB_RST#`、启动算一次「需要」⇒ 至少 HUB_IDLE_MS 之后才可能关。 + 必须在执行器任务起来之前:它的第一拍就会调 `hubctl_tick()`。 */ + hubctl_init((hub_mode_t)hub_mode_load(), hub_mode_save, NULL, hal_time_now_ms()); + ESP_LOGI(TAG, "Hub 模式 %s(无人用 USB %u 分钟后按住复位)", hub_mode_str(hubctl_mode()), + (unsigned)(HUB_IDLE_MS / 60000u)); + /* 执行器:PG 从巡检的快照里取,本任务自己不碰 I²C(t_actuator.h 开头那一段)。 ⚠️ 必须在 `spawn(TASK_SAFETY, ...)` 之前:判据要关路时会先 `t_actuator_abort()`, 而没 init 的执行器只会直接返回,那时序列还在跑,电关掉几秒又会被抬回来。 */ diff --git a/firmware/main/board_pins.h b/firmware/main/board_pins.h index 864ed9e..7b8dc53 100644 --- a/firmware/main/board_pins.h +++ b/firmware/main/board_pins.h @@ -48,9 +48,9 @@ extern "C" { #define BOARD_PIN_INA_ALERT_N 38 /* 模组脚 31 → U13.3 */ /* USB2517I 的复位,上拉到 +3V3_HUB ⇒ 低有效。 - ⛔⛔ **运行期一个字都不许写它**(R2 D13 代价 2):ESP32 自己的控制台与烧录口 - 就挂在这个 Hub 的第 6 个下行口上,误复位 = 自断控制台,而且下一次烧录也进不来。 - 只在**上电复位**时脉冲一次(其余复位原因不碰,见 bringup.c 的 `hub_pulse()`);真出事的退路是 J33 那个 5P 应急串口。 */ + ESP32 自己的控制台与烧录口就挂在这个 Hub 的第 6 个下行口上,复位 Hub = 自断控制台。 + 上电复位时脉冲一次(bringup.c 的 `hub_pulse()`);0.1.14 起运行期**只有 hubctl.c 许碰它** + (没人用 USB 时按住复位省约 1 W,退路见 hubctl.h);最后的退路是 J33 那个 5P 应急串口。 */ #define BOARD_PIN_HUB_RST_N 8 /* 模组脚 12 → U32.43 */ #define BOARD_PIN_UP_VBUS_DET 6 /* 模组脚 6 → R721/R722 分压,上游 USB-C 插没插 */ diff --git a/firmware/main/bringup.c b/firmware/main/bringup.c index 21e5ac9..b15a447 100644 --- a/firmware/main/bringup.c +++ b/firmware/main/bringup.c @@ -73,16 +73,14 @@ static hal_err_t pd_fn(void *arg) /* ---- 第一段 -------------------------------------------------------------- */ -/* USB2517I 的复位脉冲 —— **整个固件里唯一碰 `HUB_RST#` 的地方,而且只在启动调一次** - (A15 从 app_main.c 搬过来的,理由原样保留)。 +/* USB2517I 的复位脉冲 —— 启动时的那一次(A15 从 app_main.c 搬过来的)。 低有效(板上 R716 上拉到 +3V3_HUB),给 1 ms 足够:手册要的是几十微秒量级。 - ⛔⛔ **运行期一个字都不许再写它**(R2 D13 代价 2)。ESP32 自己的 - USB-Serial-JTAG 就挂在这个 Hub 的第 6 个下行口上,所以复位 Hub = - **自己把自己的控制台拔了**,而且下一次 `idf.py flash` 也进不来 - —— 到那时候唯一的退路是 J33 那个 5P 应急串口(UART0,IO43/IO44), - 得拆机接线。写一行 "Hub 卡了就复位一下" 的自愈代码,代价是整机变砖。 + ⚠️ 运行期**只有 hubctl.c 许碰它**(0.1.14 起按需启停,推翻了 R2 D13 代价 2 的 + 「运行期一个字都不许写」,退路见 hubctl.h)。ESP32 自己的 USB-Serial-JTAG 就挂在 + 这个 Hub 的第 6 个下行口上,所以复位 Hub = 自己把自己的控制台拔了 —— + 别处写一行 "Hub 卡了就复位一下" 的自愈代码,就绕开了 hubctl 的那几条退路。 ⛔ **只在上电复位(`ESP_RST_POWERON`)时脉冲**(2026-09-27 上板改)。 ESP32 的每一次重启都算启动:esptool 经 USB 复位芯片、崩溃 / 看门狗重启、 @@ -100,7 +98,7 @@ static void hub_pulse(void) } (void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 0); /* 低 = 复位 */ hal_time_delay_ms(1); - (void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 1); /* 放开,之后再也不动 */ + (void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 1); /* 放开;随后 hubctl_init() 把它放成输入 */ } /* mux:`i2cbus_init()` 认不到 ⇒ 按 §5.8 先脉冲一次 `MUX_RST#` 再判。 */ @@ -212,6 +210,7 @@ hal_err_t bringup_devices(const bringup_in_t *in, bringup_report_t *r) /* `UP_VBUS_DET`:上游 USB-C 的 VBUS 经 R721/R722 分压进来,高 = 电脑插着。 板上 R722 下拉到地,所以不开内部上下拉。只进遥测(status 的 `host`),不参与任何判据: Hub 连着电脑时比挂起多吃约 1 W,没有它面板上的「损耗」会无缘无故跳一档。 + 0.1.14 起 hubctl 也读它:上升沿(电脑刚插上)算一次「需要」。 失败不 abort —— 读不到时 status 报 null。每次启动都配:配成输入没有副作用。 */ (void)hal_gpio_config(BOARD_PIN_UP_VBUS_DET, HAL_GPIO_INPUT); diff --git a/firmware/main/cmd.c b/firmware/main/cmd.c index 103326c..d05ace8 100644 --- a/firmware/main/cmd.c +++ b/firmware/main/cmd.c @@ -5,6 +5,8 @@ #include "cmd_priv.h" #include "evlog.h" +#include "hal_time.h" +#include "hubctl.h" /* ---- 名字表(线上用的,全 ASCII) ---------------------------------------- */ @@ -14,6 +16,7 @@ static const char *const k_names[CMD_COUNT] = { [CMD_VBUS] = "vbus", [CMD_FAN] = "fan", [CMD_EVENTS] = "events", [CMD_POWER] = "power", [CMD_WIFI] = "wifi", [CMD_MQTT] = "mqtt", [CMD_NET] = "net", [CMD_OTA] = "ota", [CMD_FLASH_MODE] = "flash_mode", + [CMD_HUB] = "hub", }; static const char *const k_errs[CMD_ERR_COUNT] = { @@ -106,6 +109,7 @@ static const handler_t k_handlers[CMD_COUNT] = { [CMD_VBUS] = do_vbus, [CMD_FAN] = do_fan, [CMD_EVENTS] = do_events, [CMD_POWER] = do_power, [CMD_WIFI] = do_wifi, [CMD_MQTT] = do_mqtt, [CMD_NET] = do_net, [CMD_OTA] = do_ota, [CMD_FLASH_MODE] = do_flash_mode, + [CMD_HUB] = do_hub, }; static cmd_id_t lookup(const char *name) @@ -154,6 +158,9 @@ static size_t reply(char *out, size_t len, bool has_seq, uint32_t seq, cmd_err_t size_t cmd_handle_line(cmd_link_t link, const char *line, char *out, size_t out_len) { s_stats.lines++; + if (link == CMD_LINK_USB) { + hubctl_touch(hal_time_now_ms()); /* 解析得过得不过都算:有人正对着 USB 敲 */ + } /* 两块大缓冲放静态区:处理器只在执行器任务里跑(cmd.h),不可重入, 而 1.5 KB 放在那个任务的栈上就是把栈顶到头。 */ diff --git a/firmware/main/cmd.h b/firmware/main/cmd.h index faa7d94..5778d6b 100644 --- a/firmware/main/cmd.h +++ b/firmware/main/cmd.h @@ -60,6 +60,7 @@ typedef enum { CMD_NET = 12, /* A14:网络状态(只读,不回显密码) */ CMD_OTA = 13, /* C2:从 URL 下载固件、校验 sha256、切启动分区(ota.h) */ CMD_FLASH_MODE = 14, /* 0.1.5:刷机模式——VBUS 通着时允许手机充电、不断 USB(chgmode.h) */ + CMD_HUB = 15, /* 0.1.14:Hub 按需启停的模式 / 保持窗口(hubctl.h) */ CMD_COUNT, } cmd_id_t; @@ -143,6 +144,8 @@ void cmd_stats(cmd_stats_t *st); (chgmode.h:窗口表只有这个任务写),app_main 每转一圈调一次。 */ void cmd_chgmode_tick(uint32_t now); +/* ⚠️ USB 链路上来的每一行都会调 `hubctl_touch()`(算一次「有人在用 USB」,hubctl.h)。 */ + #ifdef __cplusplus } #endif diff --git a/firmware/main/cmd_exec.c b/firmware/main/cmd_exec.c index 5cf3e7f..c383667 100644 --- a/firmware/main/cmd_exec.c +++ b/firmware/main/cmd_exec.c @@ -9,6 +9,7 @@ #include "cmd_priv.h" #include "evlog.h" #include "hal_time.h" +#include "hubctl.h" #include "i2cbus.h" #include "rail.h" #include "rail_cfg.h" @@ -359,6 +360,38 @@ void cmd_chgmode_tick(uint32_t now) } } +/* 0.1.14:Hub 按需启停(hubctl.h)。整板命令,两条链路都收。 + `{"cmd":"hub","mode":"auto"|"on"}` —— 改模式,存 NVS; + `{"cmd":"hub","min":30}` —— 保持 30 分钟(1~240),要用 ESP32 的 USB 控制台 / 烧录时用; + `{"cmd":"hub","min":0}` —— 取消保持并结束 idle 窗口:没别的理由就下一拍关。 + 两个键可以同时给,至少给一个。回应带 `"hub":{...}`(同 status)。 + ⚠️ 下一拍(执行器循环,≤ 几十 ms)才真的动引脚,回应里的 on 是命令执行前那一拍的。 */ +cmd_err_t do_hub(ctx_t *c) +{ + const char *ms = NULL; + int64_t min = 0; + const int gm = cj_get_str(c->o, "mode", &ms); + const int gn = cj_get_int(c->o, "min", &min); + if (gm < 0 || gn < 0 || (gm == 0 && gn == 0)) { + return CMD_ERR_ARG; + } + hub_mode_t mode = HUB_MODE_AUTO; + if (gm == 1 && !hub_mode_parse(ms, &mode)) { + return CMD_ERR_ARG; + } + if (gn == 1 && (min < 0 || min > HUB_HOLD_MAX_MIN)) { + return reject(c, CMD_ERR_RANGE, min); + } + if (gm == 1) { + (void)hubctl_set_mode(mode); + } + if (gn == 1) { + (void)hubctl_hold((uint32_t)min, hal_time_now_ms()); + } + cmd_report_hub(c->w); + return CMD_OK; +} + cmd_err_t do_fan(ctx_t *c) { int64_t duty = 0; diff --git a/firmware/main/cmd_priv.h b/firmware/main/cmd_priv.h index eb860c4..ed6e9d5 100644 --- a/firmware/main/cmd_priv.h +++ b/firmware/main/cmd_priv.h @@ -27,6 +27,10 @@ cmd_err_t do_vbus(ctx_t *c); cmd_err_t do_power(ctx_t *c); cmd_err_t do_fan(ctx_t *c); cmd_err_t do_flash_mode(ctx_t *c); +cmd_err_t do_hub(ctx_t *c); + +/* `"hub":{...}` 对象(status 与 hub 命令的回应共用,以逗号开头)。 */ +void cmd_report_hub(cj_writer_t *w); /* cmd_net.c:网络配置(只收 USB)与网络状态。 */ cmd_err_t do_wifi(ctx_t *c); diff --git a/firmware/main/cmd_report.c b/firmware/main/cmd_report.c index af18859..d46645d 100644 --- a/firmware/main/cmd_report.c +++ b/firmware/main/cmd_report.c @@ -12,6 +12,7 @@ #include "evlog.h" #include "hal_gpio.h" #include "hal_time.h" +#include "hubctl.h" #include "ota.h" #include "pca9555.h" #include "phonerail.h" @@ -102,6 +103,17 @@ static void thermal_obj(cj_writer_t *w, const thermal_snap_t *t) cj_w(w, "]},\"derate\":%s", b(t->derate)); } +/* 0.1.14:`"hub":{"on":true,"mode":"auto","why":"vbus","hold":0,"idle":0}`(hubctl.h)。 + `why` 是上一拍开着的理由(关着是 "off");`hold` = 保持窗口剩余秒数; + `idle` = 只剩 idle 这一条理由时,离自动关还有几秒(否则 0)。 */ +void cmd_report_hub(cj_writer_t *w) +{ + const uint32_t now = hal_time_now_ms(); + cj_w(w, ",\"hub\":{\"on\":%s,\"mode\":\"%s\",\"why\":\"%s\",\"hold\":%lu,\"idle\":%lu}", + b(hubctl_is_on()), hub_mode_str(hubctl_mode()), hub_why_str(hubctl_why()), + (unsigned long)hubctl_hold_left_s(now), (unsigned long)hubctl_idle_left_s(now)); +} + void cmd_report_status(cj_writer_t *w, const cmd_env_t *env) { const safety_snap_t *s = env->safety; @@ -114,6 +126,7 @@ void cmd_report_status(cj_writer_t *w, const cmd_env_t *env) } else { cj_w(w, ",\"host\":null"); } + cmd_report_hub(w); if (s != NULL) { if (s->input_ok) { cj_w(w, ",\"in\":{\"mv\":%ld,\"ua\":%ld}", (long)s->input.bus_mv, diff --git a/firmware/main/evlog.c b/firmware/main/evlog.c index 1953be5..4889483 100644 --- a/firmware/main/evlog.c +++ b/firmware/main/evlog.c @@ -576,6 +576,7 @@ const char *evlog_ev_str(log_ev_t ev) case LOG_EV_OTA: return "固件升级"; case LOG_EV_BOOT_CHECK: return "开机重按"; case LOG_EV_CHG_MODE: return "刷机模式"; + case LOG_EV_HUB: return "Hub启停"; case LOG_EV_BLADE_MUTE: return "刀片不应答关路"; case LOG_EV_COUNT: break; } diff --git a/firmware/main/evlog.h b/firmware/main/evlog.h index 8130967..a24160b 100644 --- a/firmware/main/evlog.h +++ b/firmware/main/evlog.h @@ -97,6 +97,11 @@ typedef enum { 0 = 命令关(CHG_EV_OFF),255 = 到期(CHG_EV_EXPIRED)。bus_mv / current_ua 是当时这一路的读数。 */ LOG_EV_CHG_MODE = 21, + /* 0.1.14 新增:Hub 按需启停(hubctl.h)。整板级(rail = 0)。 + arg:0 = 关(`HUB_RST#` 拉低)、1 = 开(放开),0x10|模式 = 改模式(0 auto / 1 on)。 + bus_mv = `hub_why_t`(开的理由;关那一条是 0)。 */ + LOG_EV_HUB = 22, + LOG_EV_COUNT, } log_ev_t; diff --git a/firmware/main/hubctl.c b/firmware/main/hubctl.c new file mode 100644 index 0000000..e67def0 --- /dev/null +++ b/firmware/main/hubctl.c @@ -0,0 +1,205 @@ +/* Hub 按需启停。契约、判据表与退路在 hubctl.h。 */ +#include "hubctl.h" + +#include + +#include "board_pins.h" +#include "chgmode.h" +#include "evlog.h" +#include "hal_gpio.h" +#include "netcfg.h" +#include "phonerail.h" +#include "slot.h" +#include "t_actuator.h" + +static hub_mode_t s_mode; +static hub_save_fn s_save; +static void *s_save_ctx; + +static uint32_t s_last_need; /* 最后一次「需要」的时刻 */ +static bool s_idle; /* idle 窗口还开着(过期即清:32 位时钟 49.7 天回绕,放着不管会「复活」) */ +static uint32_t s_hold_until; +static bool s_hold; +static uint32_t s_down_since; /* MQTT 从什么时候开始不通 */ +static bool s_down; +static bool s_host_prev; + +static bool s_on; +static hub_why_t s_why; + +/* `a` 在 `b` 之前(回绕安全)。 */ +static bool before(uint32_t a, uint32_t b) { return (int32_t)(a - b) < 0; } + +static void need(uint32_t now) +{ + s_last_need = now; + s_idle = true; +} + +/* 开 = 放成输入,R716 拉高;关 = 配成输出、拉低。**不主动驱高**(hubctl.h)。 */ +static hal_err_t apply(bool on) +{ + if (on) { + return hal_gpio_config(BOARD_PIN_HUB_RST_N, HAL_GPIO_INPUT); + } + hal_err_t e = hal_gpio_config(BOARD_PIN_HUB_RST_N, HAL_GPIO_OUTPUT); + if (e == HAL_OK) { + e = hal_gpio_write(BOARD_PIN_HUB_RST_N, 0); + } + return e; +} + +void hubctl_init(hub_mode_t mode, hub_save_fn save, void *ctx, uint32_t now) +{ + s_mode = (mode < HUB_MODE_COUNT) ? mode : HUB_MODE_AUTO; + s_save = save; + s_save_ctx = ctx; + s_hold = false; + s_hold_until = 0; + s_down = false; + s_down_since = now; + s_host_prev = false; + need(now); /* 启动算一次「需要」:刚复位完,最可能就是有人要用 USB */ + s_on = (apply(true) == HAL_OK); + s_why = HUB_WHY_IDLE; +} + +hub_why_t hub_decide(const hub_in_t *in, uint32_t now) +{ + if (in->host && !s_host_prev) { + need(now); /* 电脑刚插上 */ + } + s_host_prev = in->host; + + if (in->link_up) { + s_down = false; + } else if (!s_down) { + s_down = true; + s_down_since = now; + } + if (s_hold && !before(now, s_hold_until)) { + s_hold = false; + } + + hub_why_t why = HUB_WHY_OFF; + if (s_mode == HUB_MODE_ON) { + why = HUB_WHY_MODE; + } else if (in->vbus_any) { + why = HUB_WHY_VBUS; + } else if (in->flash_any) { + why = HUB_WHY_FLASH; + } else if (in->act_any) { + why = HUB_WHY_ACT; + } else if (s_hold) { + why = HUB_WHY_HOLD; + } else if (s_down && !before(now, s_down_since + HUB_LINK_LOST_MS)) { + why = HUB_WHY_LINK; + } + if (why != HUB_WHY_OFF) { + need(now); + return why; + } + if (s_idle && before(now, s_last_need + HUB_IDLE_MS)) { + return HUB_WHY_IDLE; + } + s_idle = false; + return HUB_WHY_OFF; +} + +void hubctl_tick(uint32_t now) +{ + hub_in_t in; + memset(&in, 0, sizeof(in)); + for (int r = 1; r <= PHONERAIL_RAIL_COUNT; r++) { + in.vbus_any = in.vbus_any || slot_vbus_is_on(r); + in.act_any = in.act_any || t_actuator_busy(r); + } + in.flash_any = chgmode_count(now) > 0; + net_status_t st; + net_status_get(&st); + in.link_up = (st.mqtt == NET_UP); + int host = 0; + in.host = (hal_gpio_read(BOARD_PIN_UP_VBUS_DET, &host) == HAL_OK) && host != 0; + + s_why = hub_decide(&in, now); + const bool want = (s_why != HUB_WHY_OFF); + if (want == s_on) { + return; + } + if (apply(want) != HAL_OK) { + return; /* 引脚号写错才会走到这 —— 下一拍再试,状态不假装变了 */ + } + s_on = want; + evlog_put(0, LOG_EV_HUB, want ? HUB_EV_ON : HUB_EV_OFF, (int32_t)s_why, 0); +} + +void hubctl_touch(uint32_t now) { need(now); } + +hal_err_t hubctl_set_mode(hub_mode_t mode) +{ + if (mode >= HUB_MODE_COUNT) { + return HAL_ERR_ARG; + } + if (mode != s_mode) { + s_mode = mode; + evlog_put(0, LOG_EV_HUB, (uint8_t)(HUB_EV_MODE | (uint32_t)mode), (int32_t)s_why, 0); + } + if (s_save != NULL) { + s_save(s_save_ctx, mode); /* 没变也交给它:存盘函数自己判「没变不写」 */ + } + return HAL_OK; +} + +hal_err_t hubctl_hold(uint32_t minutes, uint32_t now) +{ + if (minutes > HUB_HOLD_MAX_MIN) { + return HAL_ERR_ARG; + } + if (minutes == 0) { + s_hold = false; + s_idle = false; + return HAL_OK; + } + s_hold_until = now + minutes * 60000u; + s_hold = true; + return HAL_OK; +} + +hub_mode_t hubctl_mode(void) { return s_mode; } +bool hubctl_is_on(void) { return s_on; } +hub_why_t hubctl_why(void) { return s_why; } + +uint32_t hubctl_hold_left_s(uint32_t now) +{ + if (!s_hold || !before(now, s_hold_until)) { + return 0; + } + return (s_hold_until - now + 999u) / 1000u; +} + +uint32_t hubctl_idle_left_s(uint32_t now) +{ + if (s_why != HUB_WHY_IDLE || !s_idle || !before(now, s_last_need + HUB_IDLE_MS)) { + return 0; + } + return (s_last_need + HUB_IDLE_MS - now + 999u) / 1000u; +} + +static const char *const k_modes[HUB_MODE_COUNT] = { "auto", "on" }; +static const char *const k_whys[HUB_WHY_COUNT] = { + "off", "mode", "vbus", "flash", "act", "hold", "link", "idle", +}; + +const char *hub_mode_str(hub_mode_t m) { return (m < HUB_MODE_COUNT) ? k_modes[m] : "?"; } +const char *hub_why_str(hub_why_t w) { return (w < HUB_WHY_COUNT) ? k_whys[w] : "?"; } + +bool hub_mode_parse(const char *s, hub_mode_t *out) +{ + for (int i = 0; i < HUB_MODE_COUNT; i++) { + if (strcmp(s, k_modes[i]) == 0) { + *out = (hub_mode_t)i; + return true; + } + } + return false; +} diff --git a/firmware/main/hubctl.h b/firmware/main/hubctl.h new file mode 100644 index 0000000..48bae37 --- /dev/null +++ b/firmware/main/hubctl.h @@ -0,0 +1,121 @@ +/* Hub(U32 USB2517I)按需启停(0.1.14,2026-09-29 用户定,推翻 R2 D13 代价 2 / R-11 的 + 「运行期不许动 `HUB_RST#`」)。 + + 为什么要有它:五台手机的 VBUS 平时全关,但 Hub 只要上行插着电脑就一直是活动态 —— + ESP32 自己的 USB-Serial-JTAG 挂在第 6 口上,电脑永远不会把它挂起。 + 2026-09-29 板 B 实测 Hub 工作 vs 按住复位差 1.03 W(12 V 输入差约 88 mA),而这 1 W 大部分时间什么都没在干。 + 把 `RESET_N`(43 脚)拉低 ⇒ Hub 从电脑那边消失,PHY 与 24 MHz 晶振都停,只剩 U31 的静态电流。 + + ---- 什么时候开(任一条成立就开,并刷新「最后一次需要」的时刻)------------- + | why | 条件 | + |---------|------| + | mode | 模式 = 常开(`hub` 命令,存 NVS) | + | vbus | 任一路 VBUS 通着 —— 手机的 USB 只有这时才在 Hub 上 | + | flash | 任一路刷机模式窗口内(chgmode.h) | + | act | 任一路执行器在跑序列(EDL 要枚举 9008;硬重启中途 VBUS 暂时是关的) | + | hold | `hub` 命令给的保持窗口内(要用 ESP32 的 USB 控制台 / 烧录) | + | link | MQTT 连续 HUB_LINK_LOST_MS 没连上(含从没配过网)⇒ **USB 是唯一的路,必须开** | + | idle | 以上都不成立,但离「最后一次需要」还不到 HUB_IDLE_MS | + 「最后一次需要」还会被三件事刷新:启动、上行电脑插上(`UP_VBUS_DET` 上升沿)、 + USB 链路上来了一条命令 —— 都是「有人正要用 USB」的信号。 + 全不成立 ⇒ 关(`HUB_RST#` 拉低)。 + + ---- 退路(这是推翻 R-11 的前提,一条都不许删)----------------------------- + 1. `link`:WiFi/MQTT 断了 ⇒ 1 分钟内 Hub 自己回来,USB 控制台可用; + 2. ESP32 任何一次复位(上电 / 看门狗 / esp_restart / esptool / J33 EN)引脚都回到高阻, + R716 把 Hub 拉出复位,而且启动算一次「需要」⇒ 至少 HUB_IDLE_MS 的窗口; + 3. 板子断电重上 = 第 2 条; + 4. J33 UART 应急口不经过 Hub,永远在。 + ⚠️ 只许**拉低**或**放成输入**(靠 R716 拉高),不许主动驱高 —— 和 bringup 的脉冲同一口径: + 放开 = 高阻,ESP32 这一侧出任何事 Hub 都停在「工作」那一边。 + + ---- 线程 ---------------------------------------------------------------- + **只有执行器任务写**(`hubctl_tick()` 在执行器循环里、命令处理器也在那个任务里), + status 跨任务读几个单字,遥测无害(cmd.h 同一条)。 */ +#ifndef HUBCTL_H +#define HUBCTL_H + +#include +#include + +#include "hal_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define HUB_IDLE_MS (10u * 60000u) /* 最后一次需要之后再开多久 */ +#define HUB_LINK_LOST_MS 60000u /* MQTT 断多久算「只剩 USB 这条路」 */ +#define HUB_HOLD_MAX_MIN 240 + +/* 模式。⚠️ 进 NVS,冻结。 */ +typedef enum { + HUB_MODE_AUTO = 0, + HUB_MODE_ON = 1, + HUB_MODE_COUNT, +} hub_mode_t; + +/* 为什么开着 / 关着。⚠️ 进事件(LOG_EV_HUB 的 bus_mv),冻结,只许在末尾追加。 */ +typedef enum { + HUB_WHY_OFF = 0, + HUB_WHY_MODE = 1, + HUB_WHY_VBUS = 2, + HUB_WHY_FLASH = 3, + HUB_WHY_ACT = 4, + HUB_WHY_HOLD = 5, + HUB_WHY_LINK = 6, + HUB_WHY_IDLE = 7, + HUB_WHY_COUNT, +} hub_why_t; + +/* LOG_EV_HUB 的 arg。⚠️ 进 flash,冻结。 */ +#define HUB_EV_OFF 0u +#define HUB_EV_ON 1u +#define HUB_EV_MODE 0x10u /* | 新模式 */ + +/* 一拍的输入(`hubctl_tick()` 自己采;宿主机测试直接喂 `hub_decide()`)。 */ +typedef struct { + bool vbus_any; + bool flash_any; + bool act_any; + bool link_up; + bool host; /* UP_VBUS_DET */ +} hub_in_t; + +/* 模式存盘回调(app_main 写 NVS)。NULL = 不存(宿主机测试)。 */ +typedef void (*hub_save_fn)(void *ctx, hub_mode_t mode); + +/* 起步:记下模式、把 `HUB_RST#` 放成输入(Hub 工作),启动算一次「需要」。 + 模式越界按 AUTO。 */ +void hubctl_init(hub_mode_t mode, hub_save_fn save, void *ctx, uint32_t now); + +/* 纯判定:这一拍 Hub 该不该开、为什么。会推进内部的时刻(最后一次需要 / 断线起点 / 上行边沿), + **不碰引脚**。宿主机测试用它。 */ +hub_why_t hub_decide(const hub_in_t *in, uint32_t now); + +/* 执行器循环每拍调:采输入 → `hub_decide()` → 状态变了才动引脚、记事件。 */ +void hubctl_tick(uint32_t now); + +/* USB 链路上来了一条命令(cmd.c 调):算一次「需要」。 */ +void hubctl_touch(uint32_t now); + +/* 命令:改模式(存盘、记事件)。越界 HAL_ERR_ARG。 */ +hal_err_t hubctl_set_mode(hub_mode_t mode); +/* 命令:保持 minutes 分钟(1~HUB_HOLD_MAX_MIN,覆盖原窗口);0 = 取消保持**并结束 idle 窗口** + (没有别的理由就下一拍关 —— 实测省电用)。越界 HAL_ERR_ARG。 */ +hal_err_t hubctl_hold(uint32_t minutes, uint32_t now); + +hub_mode_t hubctl_mode(void); +bool hubctl_is_on(void); /* 引脚此刻放着(Hub 在工作) */ +hub_why_t hubctl_why(void); /* 上一拍的结论 */ +uint32_t hubctl_hold_left_s(uint32_t now); +uint32_t hubctl_idle_left_s(uint32_t now); /* why == idle 时离自动关还有几秒,否则 0 */ +const char *hub_mode_str(hub_mode_t m); +const char *hub_why_str(hub_why_t w); +bool hub_mode_parse(const char *s, hub_mode_t *out); + +#ifdef __cplusplus +} +#endif + +#endif /* HUBCTL_H */ diff --git a/firmware/main/phonerail.h b/firmware/main/phonerail.h index e147b60..a50ed02 100644 --- a/firmware/main/phonerail.h +++ b/firmware/main/phonerail.h @@ -3,7 +3,7 @@ #define PHONERAIL_H /* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */ -#define PHONERAIL_FW_VERSION "0.1.13-c5" +#define PHONERAIL_FW_VERSION "0.1.16-c5" /* 通道数。五路直供,见设计文档 §2。 */ #define PHONERAIL_RAIL_COUNT 5 diff --git a/firmware/main/t_actuator.h b/firmware/main/t_actuator.h index 25dbf44..571d76e 100644 --- a/firmware/main/t_actuator.h +++ b/firmware/main/t_actuator.h @@ -110,6 +110,8 @@ extern "C" { /* SW-5 的三档按键时长。 ⚠️ 开机那一档 2026-09-28 小米8 实测:按 2 s 时 PMIC 起来了(415 mA),一松手 3 s 内 掉回 ~1 mA —— SoC 还没接住 PS_HOLD。按 5 s 松手后稳在 ~210 mA、约 45 s 进系统。 + 2026-09-30 试过 3 s(0.1.15):板 B 第 2 路 power_cycle 三轮全是「按住 330~390 mA、松手掉回 0」 + ⇒ no-boot;同一台手工按 5 s 一次开起来。⛔ 别再往下调。 上限是 ACT_KEY_MS_FORCE:按到 10 s 就成了强制重启。 */ #define ACT_KEY_MS_TAP 100 /* 短按:唤醒 / 息屏 */ #define ACT_KEY_MS_ON 5000 /* 长按:开机 */ diff --git a/firmware/test/test_cmd.c b/firmware/test/test_cmd.c index 7389ea3..4d7f0ae 100644 --- a/firmware/test/test_cmd.c +++ b/firmware/test/test_cmd.c @@ -16,6 +16,7 @@ #include "dac63204.h" #include "evlog.h" #include "hal_i2c.h" +#include "hubctl.h" #include "hal_lock.h" #include "hal_time.h" #include "i2cbus.h" @@ -189,7 +190,8 @@ static void test_cmd_numbers_are_frozen(void) TEST_ASSERT_EQUAL_INT(12, CMD_NET); TEST_ASSERT_EQUAL_INT(13, CMD_OTA); /* C2 */ TEST_ASSERT_EQUAL_INT(14, CMD_FLASH_MODE); /* 0.1.5 刷机模式 */ - TEST_ASSERT_EQUAL_INT(15, CMD_COUNT); + TEST_ASSERT_EQUAL_INT(15, CMD_HUB); /* 0.1.14 Hub 按需启停 */ + TEST_ASSERT_EQUAL_INT(16, CMD_COUNT); TEST_ASSERT_EQUAL_INT(0, CMD_OK); TEST_ASSERT_EQUAL_INT(1, CMD_ERR_PARSE); @@ -590,6 +592,38 @@ static void test_fan_floor(void) TEST_ASSERT_EQUAL_UINT16(0, t_thermal_floor()); } +/* 0.1.14 hub:改模式 / 保持窗口,两条链路都收;越界记事件、缺参数不记; + USB 链路上的任何一行都算一次「有人在用 USB」。 */ +static void test_hub_command(void) +{ + hubctl_init(HUB_MODE_AUTO, NULL, NULL, hal_time_now_ms()); + + const char *r = send_on(CMD_LINK_WIFI, "{\"seq\":1,\"cmd\":\"hub\",\"mode\":\"on\"}"); + ASSERT_HAS(r, "\"ok\":true"); + ASSERT_HAS(r, "\"hub\":{\"on\":true,\"mode\":\"on\""); + TEST_ASSERT_EQUAL_INT(HUB_MODE_ON, hubctl_mode()); + + r = send("{\"seq\":2,\"cmd\":\"hub\",\"mode\":\"auto\",\"min\":30}"); + ASSERT_HAS(r, "\"mode\":\"auto\""); + ASSERT_HAS(r, "\"hold\":1800"); + TEST_ASSERT_EQUAL_UINT32(1800, hubctl_hold_left_s(hal_time_now_ms())); + + ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"hub\",\"min\":241}"), "\"err\":\"range\""); + TEST_ASSERT_EQUAL_INT(1, count_events(LOG_EV_CMD_REJECT)); + ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"hub\",\"mode\":\"off\"}"), "\"err\":\"arg\""); + ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"hub\"}"), "\"err\":\"arg\""); + TEST_ASSERT_EQUAL_INT(1, count_events(LOG_EV_CMD_REJECT)); + ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"hub\"}"), "\"dup\":true"); /* 会动东西 ⇒ 进判重窗口 */ + + ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"hub\",\"min\":0}"), "\"hold\":0"); + const hub_in_t idle = { .link_up = true }; + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&idle, hal_time_now_ms() + 1)); + + /* USB 上来一行(哪怕是 status)⇒ 重新给一个 idle 窗口。 */ + (void)send_on(CMD_LINK_USB, "{\"cmd\":\"status\"}"); + TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&idle, hal_time_now_ms() + 1)); +} + /* ---- 只读命令 ------------------------------------------------------------ */ /* events 分页:一次最多 CMD_EVENTS_MAX 条,逐条带序号,游标接得上;RAM 里的先落盘。 */ @@ -858,6 +892,7 @@ int main(void) RUN_TEST(test_power_off_drops_vbus_and_always_drops_the_efuse); RUN_TEST(test_power_records_intent_only_when_it_took_effect); RUN_TEST(test_fan_floor); + RUN_TEST(test_hub_command); RUN_TEST(test_events_pages_with_contiguous_seqs); RUN_TEST(test_status_fits_and_reports_unknowns_as_null); RUN_TEST(test_small_output_buffer); diff --git a/firmware/test/test_evlog.c b/firmware/test/test_evlog.c index 13d2a2f..ca42b63 100644 --- a/firmware/test/test_evlog.c +++ b/firmware/test/test_evlog.c @@ -89,7 +89,8 @@ static void test_flash_format_is_frozen(void) TEST_ASSERT_EQUAL_INT(19, LOG_EV_BOOT_CHECK); /* 0.1.2 开机校验 */ TEST_ASSERT_EQUAL_INT(20, LOG_EV_BLADE_MUTE); /* 0.1.4 刀片不应答 */ TEST_ASSERT_EQUAL_INT(21, LOG_EV_CHG_MODE); /* 0.1.5 刷机模式 */ - TEST_ASSERT_EQUAL_INT(22, LOG_EV_COUNT); + TEST_ASSERT_EQUAL_INT(22, LOG_EV_HUB); /* 0.1.14 Hub 按需启停 */ + TEST_ASSERT_EQUAL_INT(23, LOG_EV_COUNT); } /* §10.7 的「必记事件」有八类,A10 只产生其中两类。剩下的号先占住, diff --git a/firmware/test/test_hubctl.c b/firmware/test/test_hubctl.c new file mode 100644 index 0000000..6c0c3dc --- /dev/null +++ b/firmware/test/test_hubctl.c @@ -0,0 +1,237 @@ +/* 0.1.14:Hub 按需启停(hubctl.h)。 + + 口径同其它测试:判据用 `hub_decide()` 直接喂输入;引脚与事件用 `hubctl_tick()` + 打在假 GPIO / 事件缓冲上。R716 的上拉用 `sim_gpio_drive(HUB_RST#, 1)` 模拟 —— + 放成输入时读回 1 = Hub 在工作,配成输出拉低时读回 0 = 按住复位。 */ +#include + +#include "board_pins.h" +#include "evlog.h" +#include "hal_time.h" +#include "hubctl.h" +#include "netcfg.h" +#include "sim_hal.h" +#include "unity.h" + +#define PIN BOARD_PIN_HUB_RST_N + +static hub_mode_t s_saved; +static int s_saves; + +static void save(void *ctx, hub_mode_t m) +{ + (void)ctx; + s_saved = m; + s_saves++; +} + +static void link(bool up) +{ + net_status_t st; + memset(&st, 0, sizeof(st)); + st.mqtt = up ? NET_UP : NET_CONNECTING; + net_status_set(&st); +} + +static uint32_t now(void) { return hal_time_now_ms(); } + +/* 每 100 ms 一拍(执行器空闲时的节奏),推 ms 毫秒。 */ +static void run_ms(uint32_t ms) +{ + for (uint32_t t = 0; t < ms; t += 100) { + sim_time_advance_ms(100); + hubctl_tick(now()); + } +} + +static int hub_events(uint8_t arg) +{ + (void)evlog_flush(0); + evlog_rec_t r[16]; + const uint32_t n = evlog_read(0, r, 16); + int k = 0; + for (uint32_t i = 0; i < n; i++) { + k += (r[i].type == LOG_EV_HUB && r[i].arg == arg) ? 1 : 0; + } + return k; +} + +void setUp(void) +{ + sim_hal_reset(); + TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init()); + sim_gpio_drive(PIN, 1); /* R716 */ + s_saves = 0; + link(true); + hubctl_init(HUB_MODE_AUTO, save, NULL, now()); +} + +void tearDown(void) {} + +static const hub_in_t k_idle = { .link_up = true }; + +/* ---- 判据 ---------------------------------------------------------------- */ + +/* 启动算一次需要:HUB_IDLE_MS 之内开着(idle),到点就关。 */ +static void test_boot_window_then_off(void) +{ + TEST_ASSERT_TRUE(hubctl_is_on()); + TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(PIN)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&k_idle, now() + HUB_IDLE_MS - 1)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&k_idle, now() + HUB_IDLE_MS)); +} + +/* 优先级:mode > vbus > flash > act > hold > link。任一条都刷新「最后一次需要」。 */ +static void test_reasons_and_their_order(void) +{ + hub_in_t in = k_idle; + in.vbus_any = in.flash_any = in.act_any = true; + TEST_ASSERT_EQUAL_INT(HUB_WHY_VBUS, hub_decide(&in, now())); + in.vbus_any = false; + TEST_ASSERT_EQUAL_INT(HUB_WHY_FLASH, hub_decide(&in, now())); + in.flash_any = false; + TEST_ASSERT_EQUAL_INT(HUB_WHY_ACT, hub_decide(&in, now())); + in.act_any = false; + TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&in, now())); + + /* VBUS 一直开着 30 分钟,关掉之后还要再开 HUB_IDLE_MS。 */ + const uint32_t t0 = now() + 30u * 60000u; + in.vbus_any = true; + TEST_ASSERT_EQUAL_INT(HUB_WHY_VBUS, hub_decide(&in, t0)); + in.vbus_any = false; + TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&in, t0 + HUB_IDLE_MS - 1)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t0 + HUB_IDLE_MS)); + + TEST_ASSERT_EQUAL_INT(HAL_OK, hubctl_set_mode(HUB_MODE_ON)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_MODE, hub_decide(&in, t0 + 10u * HUB_IDLE_MS)); +} + +/* MQTT 断满 HUB_LINK_LOST_MS 才算(WiFi 抖一下不开);恢复后再给一个 idle 窗口。 */ +static void test_link_lost_forces_on(void) +{ + const uint32_t t0 = now() + HUB_IDLE_MS; /* 启动窗口已过 */ + hub_in_t in = k_idle; + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t0)); + in.link_up = false; + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t0)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t0 + HUB_LINK_LOST_MS - 1)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_LINK, hub_decide(&in, t0 + HUB_LINK_LOST_MS)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_LINK, hub_decide(&in, t0 + 5u * HUB_IDLE_MS)); + in.link_up = true; + const uint32_t t1 = t0 + 5u * HUB_IDLE_MS + 1; + TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&in, t1)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t1 + HUB_IDLE_MS)); + + /* 断了不到一分钟又连上:计时重来,不累计。 */ + in.link_up = false; + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t1 + HUB_IDLE_MS + 1)); + in.link_up = true; + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t1 + HUB_IDLE_MS + 30000u)); + in.link_up = false; + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t1 + HUB_IDLE_MS + 30001u)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, + hub_decide(&in, t1 + HUB_IDLE_MS + 30001u + HUB_LINK_LOST_MS - 1)); +} + +/* 电脑插上(UP_VBUS_DET 上升沿)与 USB 命令都算一次需要;电平一直高不算。 */ +static void test_host_edge_and_usb_touch(void) +{ + const uint32_t t0 = now() + HUB_IDLE_MS; + hub_in_t in = k_idle; + in.host = true; + TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&in, t0)); /* 上升沿 */ + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t0 + HUB_IDLE_MS)); /* 电平不算 */ + + hubctl_touch(t0 + 2u * HUB_IDLE_MS); + TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&in, t0 + 3u * HUB_IDLE_MS - 1)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t0 + 3u * HUB_IDLE_MS)); +} + +/* 保持窗口;min=0 取消保持并结束 idle 窗口(下一拍就关)。 */ +static void test_hold(void) +{ + const uint32_t t0 = now(); + TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hubctl_hold(HUB_HOLD_MAX_MIN + 1, t0)); + TEST_ASSERT_EQUAL_INT(HAL_OK, hubctl_hold(30, t0)); + TEST_ASSERT_EQUAL_UINT32(1800, hubctl_hold_left_s(t0)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_HOLD, hub_decide(&k_idle, t0 + 30u * 60000u - 1)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&k_idle, t0 + 30u * 60000u)); + TEST_ASSERT_EQUAL_UINT32(0, hubctl_hold_left_s(t0 + 30u * 60000u)); + + TEST_ASSERT_EQUAL_INT(HAL_OK, hubctl_hold(0, t0 + 30u * 60000u)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&k_idle, t0 + 30u * 60000u + 1)); +} + +/* 关着 49.7 天,32 位时钟回绕之后 idle 窗口不许「复活」。 */ +static void test_idle_window_does_not_revive_after_wrap(void) +{ + const uint32_t t0 = now() + HUB_IDLE_MS; + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&k_idle, t0)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&k_idle, t0 + 0x80000000u)); + TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&k_idle, t0 + 0xFFFFFFF0u)); +} + +/* ---- 引脚与事件 ---------------------------------------------------------- */ + +/* 关 = 配成输出拉低;开 = 放成输入(R716 拉高),**从不主动写 1**。每次翻转一条事件。 */ +static void test_tick_drives_the_pin_and_logs(void) +{ + run_ms(HUB_IDLE_MS + 200); + TEST_ASSERT_FALSE(hubctl_is_on()); + TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(PIN)); + TEST_ASSERT_EQUAL_INT(1, hub_events(HUB_EV_OFF)); + TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(PIN)); /* 固件没写过 1 */ + + /* 断网一分钟 ⇒ 自己回来。 */ + link(false); + run_ms(HUB_LINK_LOST_MS + 200); + TEST_ASSERT_TRUE(hubctl_is_on()); + TEST_ASSERT_EQUAL_INT(HUB_WHY_LINK, hubctl_why()); + TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(PIN)); + TEST_ASSERT_EQUAL_INT(1, hub_events(HUB_EV_ON)); + TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(PIN)); + + /* 一直开着不重复记。 */ + run_ms(5000); + TEST_ASSERT_EQUAL_INT(1, hub_events(HUB_EV_ON)); +} + +static void test_mode_is_saved_and_logged(void) +{ + TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hubctl_set_mode(HUB_MODE_COUNT)); + TEST_ASSERT_EQUAL_INT(HAL_OK, hubctl_set_mode(HUB_MODE_ON)); + TEST_ASSERT_EQUAL_INT(1, s_saves); + TEST_ASSERT_EQUAL_INT(HUB_MODE_ON, s_saved); + TEST_ASSERT_EQUAL_INT(1, hub_events(HUB_EV_MODE | HUB_MODE_ON)); + run_ms(HUB_IDLE_MS + 200); + TEST_ASSERT_TRUE(hubctl_is_on()); /* 常开 */ + TEST_ASSERT_EQUAL_INT(HAL_OK, hubctl_set_mode(HUB_MODE_ON)); + TEST_ASSERT_EQUAL_INT(1, hub_events(HUB_EV_MODE | HUB_MODE_ON)); /* 没变不记 */ + + hub_mode_t m = HUB_MODE_ON; + TEST_ASSERT_TRUE(hub_mode_parse("auto", &m)); + TEST_ASSERT_EQUAL_INT(HUB_MODE_AUTO, m); + TEST_ASSERT_FALSE(hub_mode_parse("off", &m)); +} + +/* 从 NVS 读到的是垃圾 ⇒ 按 AUTO。 */ +static void test_bad_mode_falls_back_to_auto(void) +{ + hubctl_init((hub_mode_t)7, NULL, NULL, now()); + TEST_ASSERT_EQUAL_INT(HUB_MODE_AUTO, hubctl_mode()); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_boot_window_then_off); + RUN_TEST(test_reasons_and_their_order); + RUN_TEST(test_link_lost_forces_on); + RUN_TEST(test_host_edge_and_usb_touch); + RUN_TEST(test_hold); + RUN_TEST(test_idle_window_does_not_revive_after_wrap); + RUN_TEST(test_tick_drives_the_pin_and_logs); + RUN_TEST(test_mode_is_saved_and_logged); + RUN_TEST(test_bad_mode_falls_back_to_auto); + return UNITY_END(); +} diff --git a/firmware/test/test_t_actuator.c b/firmware/test/test_t_actuator.c index 0ac095b..9f556fd 100644 --- a/firmware/test/test_t_actuator.c +++ b/firmware/test/test_t_actuator.c @@ -1020,7 +1020,7 @@ static void test_timings_match_the_documents(void) TEST_ASSERT_TRUE(ACT_HR_PG_POLL_MS > 0 && ACT_HR_PG_POLL_MS * 10 <= ACT_HR_PG_TIMEOUT_MS); TEST_ASSERT_EQUAL_UINT32(100, ACT_KEY_MS_TAP); /* SW-5 三档 */ - TEST_ASSERT_EQUAL_UINT32(5000, ACT_KEY_MS_ON); /* 小米8 实测 2 s 不够(t_actuator.h) */ + TEST_ASSERT_EQUAL_UINT32(5000, ACT_KEY_MS_ON); /* 小米8 实测 2 s、3 s 都不够(t_actuator.h) */ TEST_ASSERT_TRUE(ACT_KEY_MS_ON < ACT_KEY_MS_FORCE); /* 开机那一下不能按成强制重启 */ TEST_ASSERT_EQUAL_UINT32(ACT_KEY_MS_ON, ACT_HR_BOOT_KEY_MS); TEST_ASSERT_EQUAL_UINT32(10000, ACT_KEY_MS_FORCE); @@ -1099,7 +1099,7 @@ static void test_format_prints_every_rail(void) /* ==== 0.1.2:硬重启的开机校验与重按 ======================================= 2026-09-28 板 B 第 1 路:电源键弹片压刀片铜盘接触不良,7 次硬重启只开起来 1 次, 失败时电流一直 1~2 mA(屏都不亮)。假手机照实测建模: - · 第 `boots_on` 次按满 5 s 松手后开机 ⇒ 电流 ~210 mA(logo 段); + · 第 `boots_on` 次按满 ACT_KEY_MS_ON 松手后开机 ⇒ 电流 ~210 mA(logo 段); · 其余情况 1.5 mA(关机态); · `half_on`:第一次按下去 PMIC 醒了、SoC 没接住 —— 松手后 415 mA 撑 2.5 s 再掉回去 (照「按 2 s」那次的实测形态),校验不能被它骗过去。 */ @@ -1202,7 +1202,7 @@ static void run_phone_ms(int rail, uint32_t ms) } } -/* 一轮按键 + 校验最长多久:按 5 s + 松键后判到窗口末尾。 */ +/* 一轮按键 + 校验最长多久:按 ACT_KEY_MS_ON + 松键后判到窗口末尾。 */ #define ONE_TRY_MS (ACT_HR_BOOT_KEY_MS + ACT_HR_VERIFY_DELAY_MS + ACT_HR_VERIFY_WINDOW_MS + 50) #define HR_TO_PRESS_MS (ACT_HR_OFF_MS + ACT_HR_RAMP_MS + ACT_HR_PG_POLL_MS)