diff --git a/firmware/HANDOFF.md b/firmware/HANDOFF.md index 23ab4d3..2c72495 100644 --- a/firmware/HANDOFF.md +++ b/firmware/HANDOFF.md @@ -7,6 +7,16 @@ `VBUS_EN` 在刀片 PCA9555 的 P0.4 上)。**19 个 ctest、351 个用例全绿**,`idf.py build` 通过。 器件层与拓扑层全部换掉,**判据阈值与时间常量一个没动**(它们来自电路计算,R2 没改电路)。 +## 2026-09-27 首次上板(底板,未插刀片)修的三处 +- `app_main.c` **HUB_RST 只在上电复位(`ESP_RST_POWERON`)时脉冲**。原先每次重启都脉冲 ⇒ + esptool 经 USB 复位即断口,只能走 J33 手动下载。现在 USB 复位(原因 11)不碰 Hub,可直接经 USB 反复烧。 +- `t_safety.c` **空槽不走总线**:原先五路无条件读,空槽 NAK 每次十几 ms,槽 1 就吃光 10 ms 预算, + 槽 2~5 与主干 0x43 一圈都没读到(输入侧监控全瞎)。新用例 + `test_empty_slots_are_never_touched_and_the_input_still_reads`(去掉修复即红)。 +- `app_main.c` 关掉 `i2c.master` 驱动日志:它的 NAK 日志在持锁期间同步打印,拖垮节拍。 +- 上板实测:空机 `skipped=0 i2c_err=0 过载 0 最迟 0 ms`;I²C 扫描主干 0x22/0x23/0x43/0x4A/0x4B/0x70 全在, + 另有 0x47 应答(推测 DAC63204 广播地址)。扫描固件在 `tools/i2c_scan/`(独立工程,只读、不写任何输出)。 + ## 下一步 **A12**:命令处理器(语义 + 序号判重 + 限幅)。R2 多一条验收: **空槽(`PRSNT#` 为高)的命令一律拒**(`slot_present()` 已备好)。 diff --git a/firmware/main/app_main.c b/firmware/main/app_main.c index 3227120..b62f407 100644 --- a/firmware/main/app_main.c +++ b/firmware/main/app_main.c @@ -3,6 +3,7 @@ #include "board_pins.h" #include "esp_log.h" +#include "esp_system.h" #include "evlog.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -204,9 +205,22 @@ static void task_log(void *arg) ⚠️ 这一下放在 `rail_en_init()` 之后、其它一切之前:Hub 复位会让 USB 重新枚举, 期间串口日志是丢的。越早做,后面越多的启动日志能被电脑收到。 失败**不 abort**:Hub 没起来只影响调试与手机的数据口,五路供电照常 - (§10.2:失联 ≠ 断电)。 */ + (§10.2:失联 ≠ 断电)。 + + ⛔ **只在上电复位(`ESP_RST_POWERON`)时脉冲**(2026-09-27 上板改)。 + 原来是「每次启动都脉冲」,而 ESP32 的每一次重启都算启动:esptool 经 USB 复位芯片、 + 崩溃 / 看门狗重启、`esp_restart()` —— 每一次都把 Hub 复位、把自己的 COM 口拔掉。 + 实测后果:板上一旦有这份固件,`esptool --before usb_reset` 连三次全断 + (`ClearCommError PermissionError 13`),只能走 J33 手动进下载模式。 + 上电复位时 Hub 自己也刚上电,脉冲没有代价;J33 的 EN 脚复位在 S3 上也报 POWERON, + 应急路径不受影响。其余原因一律不碰 `HUB_RST#`(此时它仍是复位态的高阻,靠 R716 拉高)。 */ static void hub_reset_pulse_once(void) { + const esp_reset_reason_t why = esp_reset_reason(); + if (why != ESP_RST_POWERON) { + ESP_LOGI(TAG, "复位原因 %d 不是上电复位 —— 不动 HUB_RST#(保住 USB 控制台)", (int)why); + return; + } hal_err_t err = hal_gpio_config(BOARD_PIN_HUB_RST_N, HAL_GPIO_OUTPUT); if (err != HAL_OK) { ESP_LOGE(TAG, "HUB_RST# 配置失败:%s —— 跳过复位脉冲", hal_err_str(err)); @@ -233,6 +247,12 @@ void app_main(void) ESP_LOGI(TAG, "PhoneRail %s boot, %d rails, t=%u ms", phonerail_version(), PHONERAIL_RAIL_COUNT, (unsigned)hal_time_now_ms()); + /* ⛔ 关掉 IDF I²C 驱动自己的错误日志(2026-09-27 上板实测)。它每次 NAK 打三行 E 级日志, + 打印是同步的、发生在**我们持着总线锁的时候**,一次 NAK 就吃掉十几 ms —— + 20 ms 的巡检节拍直接被它的日志拖垮,而且刷屏把真正有用的启动日志全冲掉。 + 失败并没有丢:每一次都进 `t_safety` 的 i2c_err 计数,每 10 s 汇总打印一次。 */ + esp_log_level_set("i2c.master", ESP_LOG_NONE); + selftest_or_die(); /* ⭐ R2:**五根** `EFUSE_EN` 配成输出并全部拉低(R1 是十根:另外五根 `VBUS_EN` @@ -249,7 +269,7 @@ void app_main(void) 真正"固件说了算"要等到下面的 `slot_init()`。 */ ESP_LOGI(TAG, "五路 eFuse 使能已全部拉低(VBUS 此刻靠下拉电阻关着)"); - /* USB Hub 复位脉冲 —— 只此一次,见函数头上那一段。 */ + /* USB Hub 复位脉冲 —— 只在上电复位时、只此一次,见函数头上那一段。 */ hub_reset_pulse_once(); /* 锁必须早于任何一个用总线的任务。 diff --git a/firmware/main/board_pins.h b/firmware/main/board_pins.h index 7618127..6cc21aa 100644 --- a/firmware/main/board_pins.h +++ b/firmware/main/board_pins.h @@ -50,7 +50,7 @@ extern "C" { /* USB2517I 的复位,上拉到 +3V3_HUB ⇒ 低有效。 ⛔⛔ **运行期一个字都不许写它**(R2 D13 代价 2):ESP32 自己的控制台与烧录口 就挂在这个 Hub 的第 6 个下行口上,误复位 = 自断控制台,而且下一次烧录也进不来。 - 只在启动自检时脉冲一次;真出事的退路是 J33 那个 5P 应急串口。 */ + 只在**上电复位**时脉冲一次(其余复位原因不碰,见 app_main.c);真出事的退路是 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/t_safety.c b/firmware/main/t_safety.c index 0231608..d2b5ff6 100644 --- a/firmware/main/t_safety.c +++ b/firmware/main/t_safety.c @@ -330,8 +330,20 @@ hal_err_t t_safety_step(safety_snap_t *s) 返回 0 表示预算用完了 —— 剩下的路这一圈不读了。 */ const uint32_t t0 = now; + /* ⛔ 空槽**不走总线**(2026-09-27 上板实测)。原来五路无条件各读一次:空槽的 INA226 / + PCA9555 必然 NAK,而 IDF 驱动每次 NAK 要十几 ms(含它自己的错误日志), + 槽 1 那两次就把 10 ms 整圈预算吃光 ⇒ 槽 2~5 **和主干上的输入侧 0x43** 一圈都没读到, + 现象是 `txns=2`、每圈 skipped、「最迟 57 ms」—— 空机时输入侧的监控整个是瞎的。 + 在位脚是纯 GPIO,先 poll 再按它挑。`check_present()` 在后面照旧再 poll 一次、照旧负责断电。 */ + slot_prsnt_poll(); + /* 五路,一路一次 `xact`(通道查表,**不许算** —— rail_cfg.h 那条禁令)。 */ for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) { + if (!slot_present(rail)) { + s->rail_ok[rail - 1] = false; /* 没读 ≠ 读到了:别让上一圈的旧值冒充新鲜 */ + s->io_ok[rail - 1] = false; + continue; + } const uint32_t budget = lock_budget_left(t0); if (budget == 0) { skipped = true; /* 整圈预算用完:剩下的路本圈作废,下一拍重来 */ diff --git a/firmware/test/test_t_safety.c b/firmware/test/test_t_safety.c index 42fb2c2..af1a07d 100644 --- a/firmware/test/test_t_safety.c +++ b/firmware/test/test_t_safety.c @@ -507,6 +507,42 @@ static void test_one_dead_ina226_does_not_stop_the_other_four(void) g_snap.bus_us); } +/* 空槽**一个字节也不许发**(2026-09-27 上板实测)。空机时五路无条件各读一次, + 真板上每次 NAK 要十几 ms,槽 1 两次就把整圈预算吃光 —— 槽 2~5 和主干的 0x43 + 一圈都没读到,输入侧监控整个是瞎的。这里把空槽的器件也拔掉:谁碰了它就会 NAK、 + `i2c_errors` 立刻非零;全空那一圈还要求主干那片照读不误。 */ +static void test_empty_slots_are_never_touched_and_the_input_still_reads(void) +{ + const int empty[] = { 2, 4 }; + for (unsigned i = 0; i < sizeof(empty) / sizeof(empty[0]); i++) { + const int rail = empty[i]; + sim_gpio_drive(rail_cfg(rail)->prsnt_n, 1); /* 高 = 不在位 */ + sim_i2c_set_present_ch(ch_of(rail), rail_cfg(rail)->ina_addr, false); + sim_i2c_set_present_ch(ch_of(rail), PCA9555_ADDR, false); + } + TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); + TEST_ASSERT_EQUAL_UINT32(0, g_snap.i2c_errors); + TEST_ASSERT_EQUAL_UINT32(0, g_snap.skipped); + for (int rail = 1; rail <= N; rail++) { + const bool here = (rail != 2 && rail != 4); + TEST_ASSERT_EQUAL(here, g_snap.rail_ok[rail - 1]); + TEST_ASSERT_EQUAL(here, g_snap.io_ok[rail - 1]); + TEST_ASSERT_EQUAL(here, g_snap.present[rail - 1]); + } + TEST_ASSERT_TRUE(g_snap.input_ok); + + /* 五槽全空(上板时的真实状态):总线上只剩主干那一片。 */ + for (int rail = 1; rail <= N; rail++) { + sim_gpio_drive(rail_cfg(rail)->prsnt_n, 1); + sim_i2c_set_present_ch(ch_of(rail), rail_cfg(rail)->ina_addr, false); + sim_i2c_set_present_ch(ch_of(rail), PCA9555_ADDR, false); + } + TEST_ASSERT_EQUAL_INT(HAL_OK, t_safety_step(&g_snap)); + TEST_ASSERT_EQUAL_UINT32(0, g_snap.i2c_errors); + TEST_ASSERT_TRUE(g_snap.input_ok); + TEST_ASSERT_EQUAL_INT32(12000, g_snap.input.bus_mv); +} + /* 读失败时保留**上一圈的值**并把 ok 标成 false —— 不清零。 清零的话,判据会看到一个 0 V 的「正常数」;而 `ok` 正是让它先看一眼 「这数是不是新的」。拿一个没更新的电压去判过压是最难查的那种错。 */ @@ -615,6 +651,7 @@ int main(void) RUN_TEST(test_a_busy_bus_skips_the_cycle_instead_of_waiting_forever); RUN_TEST(test_one_dead_ina226_does_not_stop_the_other_four); + RUN_TEST(test_empty_slots_are_never_touched_and_the_input_still_reads); RUN_TEST(test_a_failed_read_keeps_the_stale_value_and_flags_it); RUN_TEST(test_a_dead_pca9555_does_not_spoil_the_same_rails_ina226); RUN_TEST(test_the_beat_survives_a_dead_blade); diff --git a/firmware/tools/i2c_scan/CMakeLists.txt b/firmware/tools/i2c_scan/CMakeLists.txt new file mode 100644 index 0000000..f28d8dd --- /dev/null +++ b/firmware/tools/i2c_scan/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(i2c_scan) diff --git a/firmware/tools/i2c_scan/main/CMakeLists.txt b/firmware/tools/i2c_scan/main/CMakeLists.txt new file mode 100644 index 0000000..f3e0b6c --- /dev/null +++ b/firmware/tools/i2c_scan/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "i2c_scan.c" + PRIV_REQUIRES esp_driver_i2c esp_driver_gpio) diff --git a/firmware/tools/i2c_scan/main/i2c_scan.c b/firmware/tools/i2c_scan/main/i2c_scan.c new file mode 100644 index 0000000..97a16ce --- /dev/null +++ b/firmware/tools/i2c_scan/main/i2c_scan.c @@ -0,0 +1,242 @@ +/* R2 底板 I²C 扫描(临时诊断固件,不是产品固件)。 + + 做的事: + 1. 先以 GPIO 输入读 SDA / SCL / MUX_RST# / HUB_RST# 的空闲电平 —— 总线被拉死时 I²C 驱动只会报超时, + 这一步能直接看出是「没上拉」还是「有器件把线拉低」;SDA 被拉低时先打 9 个 SCL 脉冲做总线恢复。 + 2. 扫主干 0x08~0x77,认得的地址打出器件名,INA226 / CH224A / TCA9548A 再读一次身份寄存器。 + 3. TCA9548A 在的话逐个打开通道 0~7,扫分支(槽 1~5 = 通道 0~4),刀片上的 INA226(0x40) 读身份、 + PCA9555(0x20) 只读输入口 —— **一个输出寄存器都不写**。 + 4. 关掉所有通道,每 5 s 重扫一遍(插拔刀片可以直接看)。 + + 不碰的脚:HUB_RST#(IO8,只读)—— 控制台就挂在那颗 Hub 上。MUX_RST#(IO40)读完空闲电平后推高, + 保证 mux 不在复位里。其余 GPIO 一律保持复位态。 */ +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" +#include "driver/i2c_master.h" +#include "esp_rom_sys.h" + +#define PIN_SDA 41 +#define PIN_SCL 42 +#define PIN_MUX_RST_N 40 +#define PIN_HUB_RST_N 8 +#define MUX_ADDR 0x70 +#define I2C_HZ 100000 +#define TMO_MS 20 + +static i2c_master_bus_handle_t bus; + +static const char *main_name(uint8_t a) +{ + switch (a) { + case 0x70: return "TCA9548A U35 多路器"; + case 0x43: return "INA226 U13 输入检流"; + case 0x4A: return "DAC63204 U6"; + case 0x4B: return "DAC63204 U7"; + case 0x22: + case 0x23: return "CH224A U36 PD 取电"; + default: return "?? 未预期"; + } +} + +static const char *branch_name(uint8_t a) +{ + switch (a) { + case 0x40: return "INA226 刀片检流"; + case 0x20: return "PCA9555 刀片 IO 扩展"; + default: return "?? 未预期"; + } +} + +static esp_err_t rd(uint8_t addr, uint8_t reg, uint8_t *buf, size_t n) +{ + i2c_device_config_t cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = addr, + .scl_speed_hz = I2C_HZ, + }; + i2c_master_dev_handle_t dev; + esp_err_t e = i2c_master_bus_add_device(bus, &cfg, &dev); + if (e != ESP_OK) { + return e; + } + e = i2c_master_transmit_receive(dev, ®, 1, buf, n, TMO_MS); + i2c_master_bus_rm_device(dev); + return e; +} + +static esp_err_t mux_select(uint8_t mask) +{ + i2c_device_config_t cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = MUX_ADDR, + .scl_speed_hz = I2C_HZ, + }; + i2c_master_dev_handle_t dev; + esp_err_t e = i2c_master_bus_add_device(bus, &cfg, &dev); + if (e != ESP_OK) { + return e; + } + e = i2c_master_transmit(dev, &mask, 1, TMO_MS); + uint8_t back = 0xFF; + if (e == ESP_OK) { + e = i2c_master_receive(dev, &back, 1, TMO_MS); + if (e == ESP_OK && back != mask) { + printf(" ⚠ mux 回读 0x%02X ≠ 写入 0x%02X\n", back, mask); + } + } + i2c_master_bus_rm_device(dev); + return e; +} + +static void pin_levels(const char *when) +{ + gpio_config_t in = { + .pin_bit_mask = (1ULL << PIN_SDA) | (1ULL << PIN_SCL) | (1ULL << PIN_HUB_RST_N) | (1ULL << PIN_MUX_RST_N), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + }; + gpio_config(&in); + printf("[%s] 空闲电平(无内部上拉):SDA=%d SCL=%d HUB_RST#=%d MUX_RST#=%d\n", when, + gpio_get_level(PIN_SDA), gpio_get_level(PIN_SCL), gpio_get_level(PIN_HUB_RST_N), + gpio_get_level(PIN_MUX_RST_N)); +} + +static void bus_recover(void) +{ + gpio_set_direction(PIN_SCL, GPIO_MODE_INPUT_OUTPUT_OD); + for (int i = 0; i < 9 && !gpio_get_level(PIN_SDA); i++) { + gpio_set_level(PIN_SCL, 0); + esp_rom_delay_us(10); + gpio_set_level(PIN_SCL, 1); + esp_rom_delay_us(10); + } + gpio_set_direction(PIN_SCL, GPIO_MODE_INPUT); + printf(" 总线恢复后:SDA=%d SCL=%d\n", gpio_get_level(PIN_SDA), gpio_get_level(PIN_SCL)); +} + +/* 扫一遍,found[] 里放主干上应答的地址(分支扫描时排除它们)。返回应答个数,-1 = 总线超时。 */ +static int scan(uint8_t *found, int maxn, const uint8_t *skip, int nskip, bool branch) +{ + int n = 0; + for (uint8_t a = 0x08; a <= 0x77; a++) { + bool sk = false; + for (int i = 0; i < nskip; i++) { + sk |= (skip[i] == a); + } + if (sk) { + continue; + } + esp_err_t e = i2c_master_probe(bus, a, TMO_MS); + if (e == ESP_OK) { + if (n < maxn) { + found[n] = a; + } + n++; + printf(" 0x%02X %s\n", a, branch ? branch_name(a) : main_name(a)); + } else if (e == ESP_ERR_TIMEOUT) { + printf(" 0x%02X 总线超时(SCL 被拉住或无上拉)——停止本轮扫描\n", a); + return -1; + } + } + return n; +} + +static void identify(uint8_t a, bool branch) +{ + uint8_t b[2]; + if (a == 0x43 || (branch && a == 0x40)) { + uint16_t mid = 0, did = 0; + if (rd(a, 0xFE, b, 2) == ESP_OK) { + mid = (b[0] << 8) | b[1]; + } + if (rd(a, 0xFF, b, 2) == ESP_OK) { + did = (b[0] << 8) | b[1]; + } + printf(" INA226@0x%02X 厂商 0x%04X(应 0x5449) 器件 0x%04X(应 0x2260)%s\n", a, mid, did, + (mid == 0x5449 && did == 0x2260) ? " ✓" : " ✗"); + if (rd(a, 0x02, b, 2) == ESP_OK) { + printf(" 母线电压 %.3f V\n", ((b[0] << 8) | b[1]) * 1.25e-3); + } + } else if (!branch && (a == 0x22 || a == 0x23)) { + if (rd(a, 0x09, b, 1) == ESP_OK) { + printf(" CH224A 状态 0x09=0x%02X(bit3 PD 握手%s)\n", b[0], (b[0] & 0x08) ? "成功" : "未成功"); + } + if (rd(a, 0x50, b, 1) == ESP_OK) { + printf(" 当前档最大电流 0x50=%u ×50mA = %.2f A\n", b[0], b[0] * 0.05); + } + } else if (branch && a == 0x20) { + if (rd(a, 0x00, b, 2) == ESP_OK) { + printf(" PCA9555 输入口 P0=0x%02X P1=0x%02X(只读,未写任何输出)\n", b[0], b[1]); + } + } +} + +void app_main(void) +{ + vTaskDelay(pdMS_TO_TICKS(1500)); /* 等 USB 控制台枚举 */ + printf("\n==== PhoneRail R2 底板 I2C 扫描 ====\n"); + + pin_levels("上电"); + gpio_set_level(PIN_MUX_RST_N, 1); /* 先定电平再开输出,避免一上来输出 0 把 mux 复位 */ + gpio_set_direction(PIN_MUX_RST_N, GPIO_MODE_INPUT_OUTPUT); + if (!gpio_get_level(PIN_SDA)) { + printf(" ⚠ SDA 被拉低,尝试 9 个 SCL 脉冲恢复\n"); + bus_recover(); + } + + i2c_master_bus_config_t bc = { + .i2c_port = 0, + .sda_io_num = PIN_SDA, + .scl_io_num = PIN_SCL, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = false, /* 板上 R602/R603 4.7k 上拉 */ + }; + ESP_ERROR_CHECK(i2c_new_master_bus(&bc, &bus)); + + for (int round = 1;; round++) { + printf("\n---- 第 %d 轮 ----\n主干(期望 0x22/23 CH224A、0x43 INA226、0x4A/4B DAC、0x70 TCA9548A):\n", round); + uint8_t main_found[16]; + int nm = scan(main_found, 16, NULL, 0, false); + if (nm < 0) { + i2c_master_bus_reset(bus); + } else { + printf(" 主干共 %d 个应答\n", nm); + for (int i = 0; i < nm && i < 16; i++) { + identify(main_found[i], false); + } + bool mux = false; + for (int i = 0; i < nm && i < 16; i++) { + mux |= (main_found[i] == MUX_ADDR); + } + if (!mux) { + printf(" ✗ 0x70 TCA9548A 不应答 —— 刀片分支扫不了\n"); + } else { + for (int ch = 0; ch < 8; ch++) { + if (mux_select(1u << ch) != ESP_OK) { + printf(" 通道 %d:选通失败\n", ch); + continue; + } + printf(" 通道 %d%s:\n", ch, ch < 5 ? (const char *[]){"(槽 1)", "(槽 2)", "(槽 3)", "(槽 4)", "(槽 5)"}[ch] : ""); + uint8_t bf[16]; + int nb = scan(bf, 16, main_found, nm < 16 ? nm : 16, true); + if (nb == 0) { + printf(" (空)\n"); + } + for (int i = 0; i < nb && i < 16; i++) { + identify(bf[i], true); + } + if (nb < 0) { + i2c_master_bus_reset(bus); + } + } + mux_select(0); + } + } + vTaskDelay(pdMS_TO_TICKS(5000)); + } +} diff --git a/firmware/tools/i2c_scan/sdkconfig.defaults b/firmware/tools/i2c_scan/sdkconfig.defaults new file mode 100644 index 0000000..673612f --- /dev/null +++ b/firmware/tools/i2c_scan/sdkconfig.defaults @@ -0,0 +1,4 @@ +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y +CONFIG_LOG_DEFAULT_LEVEL_INFO=y +CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y diff --git a/固件开发计划.md b/固件开发计划.md index 99ac3de..1c4d452 100644 --- a/固件开发计划.md +++ b/固件开发计划.md @@ -233,7 +233,7 @@ R2 的事实来源是 `.claude/_esp32_pinmap.json` 的 `r2_base`(脚号 → | `MUX_RST#` | 33 | **IO40** | 无 | R2 新增,3V3 上拉(R611)⇒ 低 = 复位 TCA9548A | | `INT#` | 32 | **IO39** | IO38(`IOEXP_INT#`) | 五片刀片 PCA9555 的 INT# **wire-OR**(R604 上拉)⇒ 低 = 有某片报中断,**看不出是哪片** | | `INA_ALERT` | 31 | **IO38** | 无(R1 走 PCA9555 P0.6) | 只有**底板输入侧 U13** 一片的 ALERT(R605 上拉) | -| `HUB_RST` | 12 | **IO8** | 无(R1 走 595) | 上拉到 `+3V3_HUB`(R716)⇒ 低有效。**只在启动自检脉冲一次,运行期不许动**(D13 代价 2) | +| `HUB_RST` | 12 | **IO8** | 无(R1 走 595) | 上拉到 `+3V3_HUB`(R716)⇒ 低有效。**只在上电复位(`ESP_RST_POWERON`)时脉冲一次,其余复位原因与运行期都不许动**(D13 代价 2;2026-09-27 上板改:原先每次重启都脉冲,esptool 经 USB 复位即断口) | | `UP_VBUS_DET` | 6 | **IO6** | IO47 | R721/R722 分压 | | `NTC1` / `NTC2` | 4 / 5 | **IO4 / IO5** | IO1 / IO2 | 仍是 ADC1(R660/R661 上拉分压) | | `FAN_PWM` | 24 | **IO47** | IO18 | 两个风扇座共用 |