From 28d8426052118868ebecdb979feb7ffc90800e67 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Thu, 24 Sep 2026 03:18:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(ethercat):=20=E6=B7=BB=E5=8A=A0ESP32?= =?UTF-8?q?=E4=BD=BF=E8=83=BD=E6=8E=A7=E5=88=B6=E5=8A=9F=E8=83=BD=E9=80=9A?= =?UTF-8?q?=E8=BF=87EtherCAT=20SDO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加0x2503对象字典项用于通过EtherCAT SDO控制ESP32 WiFi使能信号, 解决WiFi关断后无法重新开启的问题。同时添加sys/esp32_en.h/.cpp文件 提供GPIO控制接口。 SPI诊断命令(0xA2)扩展支持ESP32 EN引脚控制,用于诊断备援。 ESP32掉电重启后自动清理SPI链路状态,防止通信异常。 --- .../GD32E508_DCU_APP.cproject.yml | 2 + GD32E508_DCU_APP/comm/ssc/port/dc_app.c | 49 ++++++++ .../device/spi/app_spi_cmd_exmc_diag.cpp | 10 +- .../device/spi/app_spi_command.cpp | 45 +++++++- GD32E508_DCU_APP/device/spi/app_spi_command.h | 9 ++ .../device/spi/app_spi_command_defs.h | 5 +- GD32E508_DCU_APP/sys/config.h | 2 + GD32E508_DCU_APP/sys/esp32_en.cpp | 109 ++++++++++++++++++ GD32E508_DCU_APP/sys/esp32_en.h | 42 +++++++ GD32E508_DCU_APP/sys/task_runner.cpp | 4 + 10 files changed, 272 insertions(+), 5 deletions(-) create mode 100644 GD32E508_DCU_APP/sys/esp32_en.cpp create mode 100644 GD32E508_DCU_APP/sys/esp32_en.h diff --git a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml index 706ddf7..a67fd01 100644 --- a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml +++ b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml @@ -27,6 +27,8 @@ project: - file: ./sys/hardfault.cpp - file: ./sys/cpp_stubs.cpp - file: ./sys/config.h + - file: ./sys/esp32_en.h + - file: ./sys/esp32_en.cpp - group: device files: - file: ./device/adc.h diff --git a/GD32E508_DCU_APP/comm/ssc/port/dc_app.c b/GD32E508_DCU_APP/comm/ssc/port/dc_app.c index 03d09f7..e82f14c 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/dc_app.c +++ b/GD32E508_DCU_APP/comm/ssc/port/dc_app.c @@ -167,6 +167,10 @@ extern void EcatPdo_WriteInputs(UINT8 *esc); * 只置 pending 标志,真正的配置页写入 + 复位在主循环下一圈,见 ObjBootReqWrite 注。 */ extern void EcatEnterBootRequestAsync(void); +/* 0x2503 用:ESP32 EN(PC9) 控制(sys/esp32_en.cpp 的 extern "C" shim)。 + * 经 EtherCAT SDO 下发,**不走 ESP32 WiFi**——WiFi 关断后主站仍能拉回使能。 */ +extern UINT8 EcatEsp32EnDrive(UINT8 action); + /*--------------------------------------------------------------------------- * PDO Assign 对象 (0x1C12 / 0x1C13) *---------------------------------------------------------------------------*/ @@ -346,6 +350,47 @@ static UINT8 ObjBootReqWrite(UINT16 index, UINT8 subindex, UINT32 dataSize, return 0U; } +/*--------------------------------------------------------------------------- + * 0x2503 ESP32 WiFi EN(PC9 / WLESS_EN)控制(主站 → APP) + * + * 为什么走 EtherCAT 而不是 SPI 0xA2 / WiFi HTTP:WiFi 使能被拉低后 ESP32 + * 当场掉线,经 /spi/send 的控制路径跟被控对象同生共死——关得掉、开不回来。 + * EtherCAT 主站在 PC 侧,与 ESP32 完全解耦,关断/开启都能下发。 + * + * 语义(UINT32 bit[7:0]=action,与 sys/esp32_en.h Esp32En::drive 对齐): + * 0=拉高(开) 1=拉低保持(关) 2=拉高 3=浮空 4=只读 + * 读回:bit[7:0] = 实际引脚电平 0/1(0xEE=无效 action)。 + *---------------------------------------------------------------------------*/ +OBJCONST TSDOINFOENTRYDESC OBJMEM sEntryDesc0x2503[] = { + {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, +}; +static OBJCONST UCHAR aName0x2503[] = "ESP32 WiFi EN"; + +static UINT32 s_esp32EnVal = 1U; /**< 读回缓存:默认 1=上电已释放 EN */ + +static UINT8 ObjEsp32EnWrite(UINT16 index, UINT8 subindex, UINT32 dataSize, + UINT16 MBXMEM *pData, UINT8 bCompleteAccess) +{ + const UINT8 *p8 = (const UINT8 *)pData; + UINT32 v; + UINT8 action, st; + + (void)index; + (void)bCompleteAccess; + + if (subindex != 0U) { + return ABORTIDX_SUBINDEX_NOT_EXISTING; + } + if (pData == NULL || dataSize < 4U) { + return ABORTIDX_PARAM_LENGTH_TOO_SHORT; + } + v = (UINT32)p8[0] | ((UINT32)p8[1] << 8) | ((UINT32)p8[2] << 16) | ((UINT32)p8[3] << 24); + action = (UINT8)(v & 0xFFU); + st = EcatEsp32EnDrive(action); + s_esp32EnVal = (UINT32)st; + return 0U; +} + /*--------------------------------------------------------------------------- * Object Names *---------------------------------------------------------------------------*/ @@ -395,6 +440,10 @@ TOBJECT OBJMEM ApplicationObjDicList[] = { {NULL, NULL, 0x2502, {DEFTYPE_UNSIGNED32, 0 | (OBJCODE_VAR << 8)}, sEntryDesc0x2502, aName0x2502, &s_bootReq, NULL, ObjBootReqWrite, 0x0000}, + /* 0x2503: ESP32 WiFi EN(PC9) —— 经 EtherCAT 控制,WiFi 关断后仍可拉回 */ + {NULL, NULL, 0x2503, {DEFTYPE_UNSIGNED32, 0 | (OBJCODE_VAR << 8)}, + sEntryDesc0x2503, aName0x2503, &s_esp32EnVal, NULL, ObjEsp32EnWrite, 0x0000}, + /* 终止条目 */ {NULL, NULL, 0xFFFF, {0, 0}, NULL, NULL, NULL, NULL, NULL, 0x000} }; diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_exmc_diag.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_exmc_diag.cpp index ee12505..3d66c40 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_cmd_exmc_diag.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_exmc_diag.cpp @@ -1,13 +1,14 @@ /** * @file app_spi_cmd_exmc_diag.cpp - * @brief SPI 0xA2 EXMC/HBI 诊断命令处理器:驱动 LAN9252 通断测试 pattern / NRST 手动诊断 / 电源管理。 + * @brief SPI 0xA2 EXMC/HBI 诊断命令处理器:驱动 LAN9252 通断测试 pattern / NRST 手动诊断 / 电源管理 / ESP32 EN。 * * 网页按钮 → ESP32 /spi/send → SPI 0xA2 → 本 handler → 分流: * action 0~10 → EscExmcBus::continuityDrive() (EXMC 数据/控制线 pattern) * action 20~24 → EscLan9252::nrstDrive() (ECAT_NRST/PB10 拉低/拉高/浮空/回读) * action 30~34 → EscLan9252::pmDrive() (电源管理:读状态/进D1/D2/D3/手动唤醒) + * action 40~44 → Esp32En::drive() (ESP32 EN/PC9 诊断备援;开关请走 EtherCAT 0x2503) * 立即返回 ACK,不阻塞 SPI 总线。电平保持到下次调用或 action=0(stop)。 - * NRST action 返回的 status 字节 = 实际引脚电平(0/1),非 OK 码。 + * NRST/ESP32_EN action 返回的 status 字节 = 实际引脚电平(0/1),非 OK 码。 * PM action 返回的 status 字节 = bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE; * 唤醒(34)另有 1=成功 0=超时;0xEE=无效 action,0xE1=HBI 未就绪。 */ @@ -16,6 +17,7 @@ #include "device/spi/app_spi_command_defs.h" #include "comm/esc_exmc_bus.h" #include "comm/esc_lan9252.h" +#include "sys/esp32_en.h" namespace { @@ -32,7 +34,9 @@ public: uint8_t status = 0xEE; if (ctx.payload_len >= 1) { const uint8_t action = ctx.payload[0]; - if (action >= 30U) { + if (action >= 40U) { + status = Esp32En::drive(action - 40U); + } else if (action >= 30U) { status = EscLan9252::pmDrive(action - 30U); } else if (action >= 20U) { status = EscLan9252::nrstDrive(action - 20U); diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp index a989aab..d971943 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp @@ -38,6 +38,7 @@ extern "C" { #include "sys/critical.h" #include "sys/systick.h" #include "sys/cpu_load.h" +#include "sys/esp32_en.h" #include #include @@ -558,6 +559,8 @@ void AppSpiCommand::processCommand() g_pendingAckSeq = hdr.tx_seq; if (frame_type == V2_TYPE_LINK_HELLO) { + /* 合法 HELLO 已到:停掉起机补清,避免 reset 打断建连 */ + Esp32En::cancelRelinkFromSpi(); g_sessionId = hdr.session_id; g_txSeq = 0; g_expectedRxSeq = 0; @@ -693,6 +696,41 @@ void AppSpiCommand::cancelRing() updateDataReady(); } +void AppSpiCommand::resetEsp32Link() +{ + /* ESP32 被 PC9 掉电可能死在半笔 CS 里,SPI0/DMA/EXTI 都可能卡死。 + * 做一次完整外设重初始化(对齐 init 的硬件序列),而不是只清软件状态。 + * 仅在 PC9 翻转、对端确定不在发帧时调用。 */ + nvic_irq_disable(EXTI10_15_IRQn); + exti_interrupt_disable(EXTI_15); + + g_v2Active = false; + g_sessionId = 0U; + g_txSeq = 0U; + g_expectedRxSeq = 0U; + g_pendingAckSeq = 0U; + g_localCredit = 1U; + s_lastReqSeq = 0xFFU; + s_lastRespLen = 0U; + s_hasCachedResp = false; + + m_csActive = false; + m_rxLen = 0U; + m_ringPending = false; + m_ringDeadline = 0U; + m_txLen = 0U; + memset(m_txBuf, 0xFF, sizeof(m_txBuf)); + memset(m_rxBuf, 0x00, sizeof(m_rxBuf)); + + /* 硬件重初始化:SPI0 + DMA + CS EXTI(与 init() 同序列) */ + configureDma(); + configureSpi0(); + armSpiDmasForNextCs(); + updateDataReady(); + configureExti(); + printf("[SPI-APP] resetEsp32Link: SPI0/DMA/EXTI re-init done\r\n"); +} + bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len) { /* 若上一次按铃尚未被 ESP32 读走,拒绝重复按铃 */ @@ -789,6 +827,9 @@ void AppSpiCommand::onCsRelease() } if (m_rxLen >= V2_OVERHEAD) { processCommand(); + } else { + /* 残包:SPI 可能已乱,重配外设 */ + configureSpi0(); } armSpiDmasForNextCs(); updateDataReady(); @@ -843,7 +884,9 @@ void AppSpiCommand::onCsRelease() if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) { m_rxLen = 0U; - /* v2 DATA_READY: 无待处理帧时 updateDataReady() 释放 PB6。 */ + /* 起机毛刺/相位漂移后仅 re-arm DMA 不够:SPI0 可能已 OVR 卡死。 + * 顺手重配 SPI0+DMA(不动 EXTI/session),下一笔 HELLO 才能收进来。 */ + configureSpi0(); armSpiDmasForNextCs(); updateDataReady(); return; diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.h b/GD32E508_DCU_APP/device/spi/app_spi_command.h index 3d29590..c35b71e 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.h +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.h @@ -89,6 +89,15 @@ public: * 0xB0 进 BOOT 等路径需公开调用,避免 0xC1 残留盖住 ACK。 */ void cancelRing(); + /** + * @brief ESP32 掉电/重上电后复位 SPI 链路(PC9 翻转时调用)。 + * + * ESP32 被 EN 拉低时可能死在半笔 CS 事务里:session/序号/按铃/DMA 都会脏。 + * 重上电 HELLO 前必须清干净,否则首帧对不齐 → 永远建不起 v2 会话。 + * 清 session(HELLO 会重建)、取消按铃、re-arm DMA、释放 DATA_READY。 + */ + void resetEsp32Link(); + static constexpr uint16_t kRxBufSize = CONFIG_SPI_RX_BUF_SIZE; static constexpr uint16_t kTxBufSize = CONFIG_SPI_TX_BUF_SIZE; static constexpr uint16_t kLogBufSize = CONFIG_SPI_LOG_BUF_SIZE; diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h b/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h index 9740fa7..d9bda2e 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h +++ b/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h @@ -21,7 +21,10 @@ static constexpr uint8_t kTypeOtaCancel = 0x03U; static constexpr uint8_t kTypeAppPing = 0xA1U; /** @brief EXMC/HBI 诊断命令(ESP32→GD32):payload[0]=action。 * 0=stop(恢复AF_PP), 1-4=数据pattern(0xFFFF/0x0000/0xAAAA/0x5555), - * 5=CS低, 6=RD低, 7=WR低, 8=A1高, 9=A2高。用于 LAN9252 通断测试。 */ + * 5=CS低, 6=RD低, 7=WR低, 8=A1高, 9=A2高。用于 LAN9252 通断测试。 + * 20~24=ECAT_NRST(PB10) 30~34=LAN9252 电源管理 + * 40~44=ESP32 EN(PC9):40/42=拉高 41=拉低保持 43=浮空 44=回读 + * (运行期开关请走 EtherCAT SDO 0x2503,本组仅 SPI 诊断备援) */ static constexpr uint8_t kTypeExmcDiag = 0xA2U; /** @brief APP 初始化状态查询(ESP32→GD32,1.5.34 新增):无 payload。 * 响应 ACK 25B(1.5.35 起;1.5.34 仅前 12B): diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index f9f8e65..225e878 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -214,6 +214,8 @@ constexpr uint32_t CONFIG_SPI_RING_TIMEOUT_MS = 50U; * registerHandler 表满时静默丢弃,motor_ota 曾因容量=8 被丢弃(0xD0 回 0xEE)。 * 取 14 留余量;新增 handler 时须确认本容量足够。 */ constexpr uint8_t CONFIG_SPI_MAX_COMMANDS = 14U; +/** @brief GD32 APP 版本(HELLO_ACK/PING 身份块;改功能后请递增便于现场核对)。 */ +/* 版本号在 app_spi_cmd_ping.cpp fillGd32AppIdentityPayload 维护 */ /** @brief 每通道不重复 ID 快照表最大条目数(与 ESP32 CANFD_ID_SNAPSHOT_MAX 对齐)。 */ constexpr uint8_t CONFIG_CANFD_ID_SNAPSHOT_MAX = 16U; /** @brief 快照条目最大数据字节数(CAN FD DLC 上限)。 */ diff --git a/GD32E508_DCU_APP/sys/esp32_en.cpp b/GD32E508_DCU_APP/sys/esp32_en.cpp new file mode 100644 index 0000000..8748744 --- /dev/null +++ b/GD32E508_DCU_APP/sys/esp32_en.cpp @@ -0,0 +1,109 @@ +/** + * @file esp32_en.cpp + * @brief ESP32 EN (PC9) 驱动。PC9 拉高后延时 1.2s 复位 SPI,避开起机毛刺。 + */ +#include "sys/esp32_en.h" +#include "sys/config.h" +#include "sys/task_manager.h" +#include "device/spi/app_spi_command.h" +#include + +namespace { + +/** @brief PC9 拉高后延时多久再清 SPI(等 ESP32 ROM/APP 引脚初始化毛刺结束)。 */ +constexpr uint32_t kPostBootRelinkDelayMs = 1200U; + +/** @brief 延时复位排期;0=无。 */ +volatile uint32_t s_relinkAtMs = 0U; + +void drivePinLow() +{ + gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, + CONFIG_ESP32_RESET_PIN); + gpio_bit_reset(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN); +} + +void drivePinHigh() +{ + gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, + CONFIG_ESP32_RESET_PIN); + gpio_bit_set(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN); +} + +void floatPin() +{ + gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_IN_FLOATING, 0, + CONFIG_ESP32_RESET_PIN); +} + +bool readPin() +{ + return gpio_input_bit_get(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN) == SET; +} + +} // namespace + +uint8_t Esp32En::drive(uint8_t action) +{ + switch (action) { + case 0: /* 拉高(开 WiFi) */ + case 2: + /* ESP32 仍死着:先清一次 SPI;起机毛刺会在 1.2s 后再清一次 */ + s_relinkAtMs = 0U; + AppSpiCommandBus.resetEsp32Link(); + drivePinHigh(); + s_relinkAtMs = TaskManager::state().tick_1ms + kPostBootRelinkDelayMs; + break; + case 1: /* 拉低保持(关 WiFi) */ + s_relinkAtMs = 0U; + drivePinLow(); + AppSpiCommandBus.resetEsp32Link(); + break; + case 3: /* 浮空 */ + s_relinkAtMs = 0U; + floatPin(); + AppSpiCommandBus.resetEsp32Link(); + break; + case 4: /* 只读 */ + break; + case 5: /* 仅复位 SPI(诊断;ESP32 空闲时调用,再让它 HELLO) */ + s_relinkAtMs = 0U; + AppSpiCommandBus.resetEsp32Link(); + break; + default: + return 0xEEU; + } + + const bool level = readPin(); + static const char *const kName[6] = { + "restore HIGH", "drive LOW (hold)", "drive HIGH", "float", "read-only", "spi-relink"}; + printf("[APP][ESP32_EN] %s -> pin reads %s\r\n", kName[action], + level ? "HIGH" : "LOW"); + return level ? 1U : 0U; +} + +void Esp32En::poll() +{ + if (s_relinkAtMs == 0U) { + return; + } + const uint32_t now = TaskManager::state().tick_1ms; + if ((int32_t)(now - s_relinkAtMs) < 0) { + return; + } + s_relinkAtMs = 0U; + AppSpiCommandBus.resetEsp32Link(); + printf("[APP][ESP32_EN] post-boot SPI relink (glitch window over)\r\n"); +} + +void Esp32En::cancelRelinkFromSpi() +{ + /* HELLO/业务帧已进来:不必再延时复位(会打断建连) */ + s_relinkAtMs = 0U; +} + +/* EtherCAT SDO 0x2503 回调入口(dc_app.c 为 C 侧)。 */ +extern "C" uint8_t EcatEsp32EnDrive(uint8_t action) +{ + return Esp32En::drive(action); +} diff --git a/GD32E508_DCU_APP/sys/esp32_en.h b/GD32E508_DCU_APP/sys/esp32_en.h new file mode 100644 index 0000000..d07c423 --- /dev/null +++ b/GD32E508_DCU_APP/sys/esp32_en.h @@ -0,0 +1,42 @@ +/** + * @file esp32_en.h + * @brief ESP32 EN/RESET (PC9 / WLESS_EN) 手动控制 —— 网页开关经 EtherCAT SDO 0x2503 下发。 + * + * 硬件语义(见 sys/config.h CONFIG_ESP32_RESET_*): + * - PC9 推挽高 = 释放 ESP_CHIP_EN,ESP32 运行、WiFi 可用 + * - PC9 推挽低 = 按死 EN,ESP32 硬复位/掉使能,WiFi 与 HTTP 全断 + * - PC9 浮空 = 外部 C41/U11 把 ESP_CHIP_EN 拉低,同样关断(与复位后一致) + * + * 控制必须走 EtherCAT(与 ESP32 解耦):WiFi 关断后 ESP32 已死, + * 经 /spi/send 的路径关得掉、开不回来。 + * 恢复:再次 SDO 写 action 0/2 拉高;或 GD32 复位/重新上电(initial.cpp 无条件拉高)。 + * + * SPI 重建:ESP32 冷启动的 GPIO 毛刺会再次弄脏 SPI 链路,故拉高 PC9 后 + * 在起机窗口内多次 resetEsp32Link(),赶在 HELLO 前把 session/DMA 清干净。 + */ +#ifndef SYS_ESP32_EN_H +#define SYS_ESP32_EN_H + +#include + +class Esp32En { +public: + /** + * @brief 驱动/回读 ESP32 EN(PC9)。EtherCAT SDO 0x2503 转发到此。 + * @param action 0=拉高(开) 1=拉低保持(关) 2=拉高 3=浮空 4=只读 5=仅复位 SPI + * @return 实际引脚电平 0/1(与 nrstDrive 同口径);0xEE=无效 action。 + */ + static uint8_t drive(uint8_t action); + + /** + * @brief 起机窗口内补清 SPI 链路(TaskRunner::runPending 每圈调用)。 + * @note 无排期时立即返回。PC9 拉高后早期少量补清;一旦收到 CS 事务即停, + * 避免打断正在建连的 HELLO。 + */ + static void poll(); + + /** @brief SPI 收到任意 CS 事务时取消剩余补清(由 processCommand 调用)。 */ + static void cancelRelinkFromSpi(); +}; + +#endif /* SYS_ESP32_EN_H */ diff --git a/GD32E508_DCU_APP/sys/task_runner.cpp b/GD32E508_DCU_APP/sys/task_runner.cpp index cbe02d4..6620403 100644 --- a/GD32E508_DCU_APP/sys/task_runner.cpp +++ b/GD32E508_DCU_APP/sys/task_runner.cpp @@ -16,6 +16,7 @@ #include "sys/motor_ota_relay.h" #include "sys/task_manager.h" #include "sys/cpu_load.h" +#include "sys/esp32_en.h" #include "sys/task_runner.h" #include "comm/ethercat_slave.h" #include "comm/ethercat_sync.h" @@ -575,6 +576,9 @@ void TaskRunner::runPending() * 主动推送已随 2026-08 请求驱动改造移除,此处仅需消费请求响应的按铃。 */ AppSpiCommandBus.poll(); + /* PC9 拉高后 ESP32 起机窗口内的 SPI 补清(GPIO 毛刺会弄脏链路)。 */ + Esp32En::poll(); + /* 0xB0 进 BOOT:必须每圈检查,不能只靠 1Hz TASK_HEARTBEAT。 * 否则 Flash 写/复位被拖到下一秒,ESP32 HELLO 一直 role=app 超时。 */ if (OtaBootRequest::isRebootPending()) {