固件 0.1.14~0.1.16-c5:Hub 按需启停(hubctl)、开机长按统一 5 s

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-04 19:24:31 +08:00
共同撰写人 Claude Opus 5.5
父节点 92f38c2328
当前提交 c41d1ec931
共修改 21 个文件,包含 764 行新增和 18 行删除
+1
查看文件
@@ -13,6 +13,7 @@ idf_component_register(
"sched.c"
"tasks.c"
"chgmode.c"
"hubctl.c"
"t_safety.c"
"t_actuator.c"
"t_thermal.c"
+45
查看文件
@@ -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 的执行器只会直接返回,那时序列还在跑,电关掉几秒又会被抬回来。 */
+3 -3
查看文件
@@ -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 插没插 */
+7 -8
查看文件
@@ -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);
+7
查看文件
@@ -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 放在那个任务的栈上就是把栈顶到头。 */
+3
查看文件
@@ -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
+33
查看文件
@@ -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;
+4
查看文件
@@ -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);
+13
查看文件
@@ -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,
+1
查看文件
@@ -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;
}
+5
查看文件
@@ -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;
+205
查看文件
@@ -0,0 +1,205 @@
/* Hub 按需启停。契约、判据表与退路在 hubctl.h。 */
#include "hubctl.h"
#include <string.h>
#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;
}
+121
查看文件
@@ -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 <stdbool.h>
#include <stdint.h>
#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 */
+1 -1
查看文件
@@ -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
+2
查看文件
@@ -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 /* 长按:开机 */