diff --git a/CMakeLists.txt b/CMakeLists.txt index d9d0adc..d3747ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ -# ESP32-C3 OTA 中继项目 - 根构建文件 +# ESP32-C3 OTA 中继项目 - 根构建文件 # 要求 ESP-IDF v6.0+,使用 idf.py build 编译 +# Kconfig.projbuild 提供 DCU_SPI_CLOCK_HZ 等可配置项 cmake_minimum_required(VERSION 3.22) include($ENV{IDF_PATH}/tools/cmake/project.cmake) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..6c64c5a --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,42 @@ +menu "DCU OTA Configuration" + + config DCU_SPI_CLOCK_HZ + int "SPI clock frequency for GD32 (Hz)" + default 4000000 + range 1000000 20000000 + help + SPI clock speed between ESP32-C3 and GD32E50x. + Higher value speeds up GD32 OTA relay, but must be + validated against actual wiring length/quality and GD32 + SPI slave timing. + + config DCU_OTA_PACKET_INTERVAL_MS + int "Delay between GD32 OTA packets (ms)" + default 1 + range 0 50 + help + Idle delay inserted after each successfully acknowledged + firmware packet before sending the next one. + Lower value speeds up OTA, but if GD32 cannot finish + flash write / prepare next ACK in time, set it back. + + config DCU_OTA_POLL_BACKOFF_MS + int "Polling backoff when GD32 is not ready (ms)" + default 2 + range 0 50 + help + Delay between SPI response polls when GD32 returns WAIT + or does not drive MISO yet. Lower value reduces per-packet + idle time and can significantly speed up OTA, but if GD32 + cannot finish flash write before the next poll, increase it. + + config DCU_OTA_FINISH_TIMEOUT_MS + int "Timeout for FINISH_OTA command (ms)" + default 10000 + range 3000 60000 + help + How long ESP32 waits for GD32 to respond to the FINISH_OTA + command. GD32 may need extra time to drain flash buffers, + verify firmware, or prepare the APP jump. + +endmenu diff --git a/main/include/version.h b/main/include/version.h index c215374..259429b 100644 --- a/main/include/version.h +++ b/main/include/version.h @@ -4,11 +4,11 @@ // 固件版本 #define FW_VERSION_MAJOR 0 #define FW_VERSION_MINOR 3 -#define FW_VERSION_PATCH 37 -#define FW_VERSION_STRING "0.3.37" +#define FW_VERSION_PATCH 38 +#define FW_VERSION_STRING "0.3.38" // 编译时间 -#define BUILD_TIME "2026-07-06 19:07:54" +#define BUILD_TIME "2026-07-08 02:41:52" // Git 信息 -#define GIT_HASH "2fe5393" +#define GIT_HASH "069ce8e" diff --git a/main/monitor/can_feedback_task.cpp b/main/monitor/can_feedback_task.cpp index 757a4e7..44080fb 100644 --- a/main/monitor/can_feedback_task.cpp +++ b/main/monitor/can_feedback_task.cpp @@ -1,15 +1,23 @@ /* - * can_feedback_task.cpp - 周期性轮询 GD32 三路 CAN 1Hz 反馈快照 + * can_feedback_task.cpp - 被动监听 GD32 主动推送的 CAN 反馈快照 * - * 通过 SPI 协议帧 0xC1 拉取 GD32 端的通道统计与最近一帧信息, - * 用调试日志输出;其它消费者(Web/WebSocket)可直接读取 - * CanFeedbackTask::snapshot(channel) 拷贝最新一份数据。 + * 架构(GD32 主动推送,ESP32 被动接收): + * GD32 APP 在主循环中调用 canFeedbackPushStep(),循环遍历 3 通道 × 2 页, + * 通过 requestEsp32Read(0xC1) 下拉 SPIready 主动按铃,将每页反馈 payload + * 推送给 ESP32。ESP32 本任务调用 checkGd32Request() 阻塞等待按铃,读到 + * 一帧后用 canFdParseFeedbackPage() 解析并累积到 s_work[channel],再 + * publish() 到 s_latest[channel] 供 Web/WebSocket 读取。 + * + * 替代了原先 ESP32 1Hz 主动发 0xC1 轮询的逻辑,避免占用主线 SPI 带宽。 + * + * OTA 期间通过 pauseCanFeedbackTask(true) 暂停本任务,避免与 OTA 抢占 SPI。 */ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "transport/canfd_bridge.h" +#include "transport/spi_master.h" #include "monitor/debug_log.h" #include "monitor/can_feedback_task.h" @@ -17,17 +25,20 @@ namespace { -constexpr uint32_t kPollIntervalMs = 1000U; ///< 1Hz 拉取节拍 -constexpr uint32_t kPollTimeoutMs = 200U; ///< 单次 SPI 事务超时 +constexpr uint32_t kWaitRingMs = 500U; ///< 单次等待 GD32 按铃超时 +constexpr uint32_t kPauseDelayMs = 100U; ///< 暂停时的轮询间隔 +constexpr uint8_t kSpiCanFeedbackType = 0xC1U; +constexpr size_t kRingPayloadMax = 128U; ///< 反馈 payload 最大长度 constexpr const char *kTag = "CAN_FB"; static can_feedback_result_t s_latest[CAN_CHANNEL_MAX]; static bool s_valid [CAN_CHANNEL_MAX]; static SemaphoreHandle_t s_mutex = nullptr; static volatile bool s_paused = false; -static can_channel_feedback_t s_prev_stats[CAN_CHANNEL_MAX]; -static bool s_prev_valid[CAN_CHANNEL_MAX]; -static bool s_prev_poll_ok[CAN_CHANNEL_MAX]; + +/* 累积工作缓冲:跨 page 0/1 累积每通道的完整反馈 */ +static can_feedback_result_t s_work [CAN_CHANNEL_MAX]; +static uint8_t s_workIdx[CAN_CHANNEL_MAX]; static void publish(uint8_t ch, const can_feedback_result_t &fb) { @@ -41,55 +52,63 @@ static void publish(uint8_t ch, const can_feedback_result_t &fb) static void taskLoop(void *) { - DEBUG_INFO(kTag, "CAN 反馈轮询任务启动 (1Hz, 0xC1)"); + DEBUG_INFO(kTag, "CAN 反馈被动监听任务启动 (GD32 主动推送, 0xC1)"); + + uint8_t ring_type = 0U; + uint8_t ring_payload[kRingPayloadMax] = {}; + size_t ring_len = 0U; - can_feedback_result_t fb{}; for (;;) { - /* OTA 进行中时暂停 CAN 轮询,避免抢占 SPI 导致 OTA 握手失败 */ if (s_paused) { - vTaskDelay(pdMS_TO_TICKS(kPollIntervalMs)); + vTaskDelay(pdMS_TO_TICKS(kPauseDelayMs)); continue; } - for (uint8_t ch = 0U; ch < CAN_CHANNEL_MAX; ++ch) { - const int rc = canFdPollFeedback(static_cast(ch), - &fb, kPollTimeoutMs); - if (rc == 0) { - publish(ch, fb); + /* 阻塞等待 GD32 按铃并读取一帧(超时则重试) */ + esp_err_t ret = SpiMaster::instance().checkGd32Request(kWaitRingMs); + if (ret != ESP_OK) { + continue; + } - if (fb.stats.error_count != 0U) { - DEBUG_INFO(kTag, - "CH%u seq=%lu mode=%u rx=%lu tx=%lu drop=%lu err=%lu ids=%u", - static_cast(ch), - static_cast(fb.stats.snapshot_seq), - static_cast(fb.stats.mode), - static_cast(fb.stats.rx_count), - static_cast(fb.stats.tx_count), - static_cast(fb.stats.rx_drop_count), - static_cast(fb.stats.error_count), - static_cast(fb.valid)); - } + /* 取出 checkGd32Request 存入的 type/payload */ + if (!SpiMaster::instance().takeRingRx(&ring_type, ring_payload, + sizeof(ring_payload), &ring_len)) { + continue; + } - s_prev_stats[ch] = fb.stats; - s_prev_valid[ch] = true; - s_prev_poll_ok[ch] = true; - } else { - if (s_prev_poll_ok[ch]) { - DEBUG_WARN(kTag, "CH%u 轮询失败", static_cast(ch)); - } - s_prev_stats[ch] = {}; - s_prev_valid[ch] = false; - s_prev_poll_ok[ch] = false; + if (ring_type != kSpiCanFeedbackType || ring_len < 28U) { + continue; + } + + const uint8_t ch = ring_payload[0]; + if (ch >= CAN_CHANNEL_MAX) { + continue; + } + + /* 解析本页并累积到工作缓冲,page=0 会自动重置 */ + if (canFdParseFeedbackPage(ring_payload, ring_len, + &s_work[ch], &s_workIdx[ch]) == 0) { + publish(ch, s_work[ch]); + + if (s_work[ch].stats.error_count != 0U) { + DEBUG_INFO(kTag, + "CH%u seq=%lu mode=%u rx=%lu tx=%lu drop=%lu err=%lu", + static_cast(ch), + static_cast(s_work[ch].stats.snapshot_seq), + static_cast(s_work[ch].stats.mode), + static_cast(s_work[ch].stats.rx_count), + static_cast(s_work[ch].stats.tx_count), + static_cast(s_work[ch].stats.rx_drop_count), + static_cast(s_work[ch].stats.error_count)); } } - vTaskDelay(pdMS_TO_TICKS(kPollIntervalMs)); } } } // namespace /** - * @brief 启动 1Hz CAN 反馈轮询任务(幂等)。 + * @brief 启动 CAN 反馈被动监听任务(幂等)。 * @return true 表示任务已存在或创建成功。 */ bool startCanFeedbackTask() @@ -106,6 +125,8 @@ bool startCanFeedbackTask() } memset(s_latest, 0, sizeof(s_latest)); memset(s_valid, 0, sizeof(s_valid)); + memset(s_work, 0, sizeof(s_work)); + memset(s_workIdx, 0, sizeof(s_workIdx)); BaseType_t xRet = xTaskCreatePinnedToCore( taskLoop, @@ -127,9 +148,9 @@ void pauseCanFeedbackTask(bool pause) { s_paused = pause; if (pause) { - DEBUG_INFO(kTag, "CAN 轮询已暂停(OTA 进行中)"); + DEBUG_INFO(kTag, "CAN 监听已暂停(OTA 进行中)"); } else { - DEBUG_INFO(kTag, "CAN 轮询已恢复"); + DEBUG_INFO(kTag, "CAN 监听已恢复"); } } diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index ea40b43..4649396 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -278,6 +278,8 @@ ota_result_t OtaRelay::handleHandshaking(relay_event_t event, const uint8_t *pay DEBUG_INFO("OTA", "=== 阶段1: 握手与认证开始 ==="); // 步骤1: 请求 GD32 进入 Bootloader + // 0xB0 对 APP:触发写标志+复位 → BOOT 启动 + // 0xB0 对 BOOT:返回 0xB5 + resetToIdle()(IDLE 状态下无害) esp_err_t err = requestGd32AppEnterBootloader(); if (err != ESP_OK) { DEBUG_WARN("OTA", "GD32 进入 Bootloader 请求未确认,继续尝试握手"); @@ -778,31 +780,35 @@ void OtaRelay::calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, esp_err_t OtaRelay::requestGd32AppEnterBootloader() { DEBUG_INFO("OTA", "尝试通知 GD32 APP 进入 Bootloader OTA 窗口"); - esp_err_t err = SpiMaster::instance().appRequestBootloader(); - if (err == ESP_OK) { - DEBUG_INFO("OTA", "GD32 已确认进入 Bootloader,等待就绪"); - vTaskDelay(pdMS_TO_TICKS(1000)); - return ESP_OK; + SpiMaster::instance().appRequestBootloader(); + + /* 方案A: 通过 PB6 LOW→HIGH 序列等待 Bootloader 就绪 + * APP 模式: 复位前拉低 PB6 → BOOT 极早拉低 → init 完成释放 HIGH + * BOOT 模式: PB6 一直 HIGH,Phase1 超时后直接握手 + * 超时退化为固定延时(容错旧固件) */ + esp_err_t wait_err = SpiMaster::instance().waitForBootloaderReady(3000); + if (wait_err != ESP_OK) { + DEBUG_WARN("OTA", "硬件就绪信号超时,退化为固定延时"); + vTaskDelay(pdMS_TO_TICKS(1500)); } - DEBUG_WARN("OTA", "GD32 APP 未确认进入 Bootloader: %s", esp_err_to_name(err)); - return err; + return ESP_OK; } -/** @brief 协议子步骤:发送握手请求(0x00 0xAC)并等待 GD32 回显 ACK。 */ -ota_result_t OtaRelay::doHandshakeReq() +/** @brief 协议子步骤:发送握手请求(0x00 0xAC)并等待 GD32 回显 ACK。 + * @param attempts 最大尝试次数,默认 20。先用 3 次快速探测 GD32 是否已在 BOOT。 */ +ota_result_t OtaRelay::doHandshakeReq(uint32_t attempts) { - DEBUG_INFO("OTA", "--- 握手请求 ---"); + DEBUG_INFO("OTA", "--- 握手请求 (max=%" PRIu32 ") ---", attempts); uint8_t payload[] = {0x00, 0xAC}; uint8_t last_resp_type = 0U; size_t last_resp_len = 0U; esp_err_t last_err = ESP_OK; - constexpr uint32_t kHandshakeAttempts = 10U; constexpr uint32_t kHandshakeReqTimeoutMs = 200U; constexpr uint32_t kHandshakePollTimeoutMs = 200U; constexpr uint32_t kHandshakeRetryGapMs = 100U; - for (uint32_t attempt = 0U; attempt < kHandshakeAttempts; ++attempt) { + for (uint32_t attempt = 0U; attempt < attempts; ++attempt) { uint8_t resp_type = 0U; uint8_t resp_payload[64] = {0}; size_t resp_len = 0U; @@ -830,7 +836,7 @@ ota_result_t OtaRelay::doHandshakeReq() } DEBUG_WARN("OTA", "HANDSHAKE_REQ 未就绪,重试 %" PRIu32 "/%" PRIu32 ": err=%d resp_type=0x%02X resp_len=%u", - attempt + 1U, kHandshakeAttempts, last_err, last_resp_type, (unsigned)last_resp_len); + attempt + 1U, attempts, last_err, last_resp_type, (unsigned)last_resp_len); vTaskDelay(pdMS_TO_TICKS(kHandshakeRetryGapMs)); } @@ -961,7 +967,8 @@ ota_result_t OtaRelay::prepareRelayPacket(uint32_t seq, const ota_meta_t &meta, return OTA_RESULT_ERR_STORAGE; } - tx_buf[0] = 0x00; tx_buf[1] = 0x00; tx_buf[2] = 0x00; tx_buf[3] = 0x00; + /* session_num = nonce_server 前 4 字节,GD32 端做校验防重放/乱序 */ + memcpy(&tx_buf[0], m_nonceServer, 4); tx_buf[4] = (uint8_t)(seq & 0xFFU); tx_buf[5] = (uint8_t)((seq >> 8) & 0xFFU); tx_buf[6] = 0x00; @@ -995,8 +1002,12 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], size_t resp_len = 0; uint32_t retry_count = 0; ota_result_t result = OTA_RESULT_OK; + bool got_expected_credit_ok = false; const uint32_t expected_ack = (seq == m_transferTotalPackets - 1) ? kLastPacketAckSeq : (seq + 1U); + // ★ 单包耗时统计,用于定位 OTA 速度瓶颈。 + TickType_t pkt_start = xTaskGetTickCount(); + // 注意:goto resend_packet 保持原有语义,仅作用范围缩小到本 helper。 // 完全去除 goto 需配合 GD32 全链路 OTA 回归验证后再进行。 resend_packet: @@ -1034,7 +1045,30 @@ resend_packet: } } + /* CREDIT 感知:若 GD32 报 CREDIT=0,说明它在写 Flash 缓冲, + * 此时重发只会被丢弃。改为耐心 POLL 等待 CREDIT 恢复,最多等 500ms。 */ + if (SpiMaster::instance().remoteCredit() == 0U) { + DEBUG_DBG("OTA", "包 %" PRIu32 " GD32 CREDIT=0(写Flash中),POLL 等待...", seq); + for (uint32_t cw = 0; cw < 50U; ++cw) { + err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, + sizeof(resp_payload), &resp_len, 100); + if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK && + isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) { + got_expected_credit_ok = true; + break; + } + if (err == ESP_OK && SpiMaster::instance().remoteCredit() > 0U) { + break; /* CREDIT 恢复,可以继续轮询或重发 */ + } + vTaskDelay(pdMS_TO_TICKS(1)); + } + } + bool got_expected = false; + if (got_expected_credit_ok) { + m_transferFlashOffset += (uint32_t)chunk_size; + break; + } for (uint32_t retry = 0; retry < 10; ++retry) { DEBUG_DBG("OTA", "包 %" PRIu32 " 收到非目标ACK(seq不匹配),继续轮询(%" PRIu32 "/5)...", seq, retry + 1); err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000); @@ -1050,6 +1084,7 @@ resend_packet: } // ★ resend 前延迟 vTaskDelay(pdMS_TO_TICKS(1)); + got_expected_credit_ok = false; goto resend_packet; } if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) { @@ -1091,6 +1126,10 @@ resend_packet: break; } + TickType_t pkt_elapsed = xTaskGetTickCount() - pkt_start; + DEBUG_DBG("OTA", "包 %" PRIu32 " 完成: elapsed=%" PRIu32 " ms, retries=%" PRIu32, seq, + (uint32_t)(pkt_elapsed * portTICK_PERIOD_MS), retry_count); + if (result != OTA_RESULT_OK) { *out_result = result; return true; // 中止外层循环 @@ -1151,8 +1190,8 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick) // ★ 包间延迟:给 GD32 Bootloader 充足时间完成 flash 写入并准备下一包 ACK。 // 无此延迟时,ESP32 立即 poll 会读到上一包的 stale ACK,导致误判为丢包。 - if (seq < m_transferTotalPackets - 1) { - vTaskDelay(pdMS_TO_TICKS(1)); + if (seq < m_transferTotalPackets - 1 && CONFIG_DCU_OTA_PACKET_INTERVAL_MS > 0) { + vTaskDelay(pdMS_TO_TICKS(CONFIG_DCU_OTA_PACKET_INTERVAL_MS)); } } return result; @@ -1202,7 +1241,8 @@ ota_result_t OtaRelay::doSendFinishOta() memset(resp_payload, 0, sizeof(resp_payload)); esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, finish_payload, sizeof(finish_payload), - &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); + &resp_type, resp_payload, sizeof(resp_payload), &resp_len, + CONFIG_DCU_OTA_FINISH_TIMEOUT_MS); logRespPayload("FINISH_OTA", resp_type, resp_payload, resp_len); if (err != ESP_OK) { DEBUG_WARN("OTA", "FINISH_OTA 传输失败, err=%d, retry=%" PRIu32 "/10", err, retry + 1); diff --git a/main/relay/ota_relay.h b/main/relay/ota_relay.h index e7320f9..9a32efc 100644 --- a/main/relay/ota_relay.h +++ b/main/relay/ota_relay.h @@ -317,7 +317,7 @@ private: * @brief 执行握手请求(发送 HANDSHAKE_REQ). * @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_TIMEOUT 超时。 */ - ota_result_t doHandshakeReq(); + ota_result_t doHandshakeReq(uint32_t attempts = 20U); /** * @brief 执行挑战认证. * @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_AUTH 认证失败。 diff --git a/main/transport/canfd_bridge.cpp b/main/transport/canfd_bridge.cpp index 07126da..dd7a63f 100644 --- a/main/transport/canfd_bridge.cpp +++ b/main/transport/canfd_bridge.cpp @@ -106,90 +106,80 @@ int canFdSendNoReply(can_channel_t channel, uint32_t ext_id, /** - * @brief 拉取一路 CAN 通道的 1Hz 反馈 + ID 快照表(GD32 SPI 帧类型 0xC1,分页)。 - * 连续拉取 page=0 和 page=1,合并为完整结果。 + * @brief 解析 GD32 主动推送的一页 CAN 反馈 payload,累积到结果结构中。 + * + * GD32 APP 通过 requestEsp32Read(0xC1) 主动推送每页反馈,ESP32 收到后 + * 调用本函数解析: + * page=0 → 重置 out,填充 28 字节固定头 + 首批条目; + * page=1 → 在已有条目后追加。 + * 调用方按通道维护 entryIdx,page=0 时会被重置为 0。 */ -int canFdPollFeedback(can_channel_t channel, - can_feedback_result_t *out, - uint32_t timeout_ms) +int canFdParseFeedbackPage(const uint8_t *payload, size_t len, + can_feedback_result_t *out, uint8_t *entryIdx) { - if (channel >= CAN_CHANNEL_MAX || out == nullptr) { + if (payload == nullptr || out == nullptr || entryIdx == nullptr) { return -1; } - out->valid = 0; - uint8_t entryIdx = 0U; - - constexpr uint8_t kFixedHeader = 28U; /* ch+page+total+cnt+mode+seq+rx+tx+drop+err+tec+rec+bus_state */ - - for (uint8_t page = 0U; page < 2U; ++page) { - uint8_t req[2] = { static_cast(channel & CAN_CHANNEL_MASK), page }; - uint8_t resp_buf[kFeedbackPayloadLen] = {}; - uint8_t resp_type = 0U; - size_t resp_len = 0U; - - esp_err_t ret = SpiMaster::instance().otaTransfer( - kSpiCanFeedbackType, - req, 2U, - &resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms); - if (ret != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < kFixedHeader) { - return -1; - } - - /* 第一页时解析固定头 */ - if (page == 0U) { - out->stats.channel = resp_buf[0]; - out->stats.mode = resp_buf[4]; - out->stats.snapshot_seq = readU32Le(&resp_buf[5]); - out->stats.rx_count = readU32Le(&resp_buf[9]); - out->stats.tx_count = readU32Le(&resp_buf[13]); - out->stats.rx_drop_count = readU32Le(&resp_buf[17]); - out->stats.error_count = readU32Le(&resp_buf[21]); - out->stats.tec = resp_buf[25]; - out->stats.rec = resp_buf[26]; - out->stats.bus_state = resp_buf[27]; - out->total_count = resp_buf[2]; - } - - const uint8_t entryCount = resp_buf[3]; - if (entryCount == 0U) break; - - uint16_t offset = kFixedHeader; - for (uint8_t i = 0U; i < entryCount && entryIdx < CANFD_ID_SNAPSHOT_MAX; ++i) { - if (offset + 8U > resp_len) break; - - id_snapshot_entry_t &e = out->entries[entryIdx]; - e.id = readU32Le(&resp_buf[offset]); - e.dlc = resp_buf[offset + 4U]; - const uint8_t flags = resp_buf[offset + 5U]; - e.fd_flag = (flags & 0x01U) ? 1U : 0U; - e.brs = (flags & 0x02U) ? 1U : 0U; - e.esi = (flags & 0x04U) ? 1U : 0U; - - const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX) - ? e.dlc : static_cast(CANFD_ID_SNAPSHOT_DATA_MAX); - const uint16_t entrySize = static_cast(8U + dataLen); - if (offset + entrySize > resp_len) break; - - memcpy(e.data, &resp_buf[offset + 8U], dataLen); - for (uint8_t d = dataLen; d < CANFD_ID_SNAPSHOT_DATA_MAX; ++d) { - e.data[d] = 0U; - } - - offset += entrySize; - entryIdx++; - } - - if (entryIdx >= out->total_count) break; + constexpr uint8_t kFixedHeader = 28U; + if (len < kFixedHeader) { + return -1; } + + const uint8_t channel = payload[0]; + const uint8_t page = payload[1]; + if (channel >= CAN_CHANNEL_MAX) { + return -1; + } + + /* page=0:重置并解析固定头 */ + if (page == 0U) { + *entryIdx = 0U; + out->stats.channel = channel; + out->stats.mode = payload[4]; + out->stats.snapshot_seq = readU32Le(&payload[5]); + out->stats.rx_count = readU32Le(&payload[9]); + out->stats.tx_count = readU32Le(&payload[13]); + out->stats.rx_drop_count = readU32Le(&payload[17]); + out->stats.error_count = readU32Le(&payload[21]); + out->stats.tec = payload[25]; + out->stats.rec = payload[26]; + out->stats.bus_state = payload[27]; + out->total_count = payload[2]; + } + + /* 解析本页条目并追加 */ + const uint8_t entryCount = payload[3]; + uint16_t offset = kFixedHeader; + for (uint8_t i = 0U; i < entryCount && *entryIdx < CANFD_ID_SNAPSHOT_MAX; ++i) { + if (offset + 8U > len) break; + + id_snapshot_entry_t &e = out->entries[*entryIdx]; + e.id = readU32Le(&payload[offset]); + e.dlc = payload[offset + 4U]; + const uint8_t flags = payload[offset + 5U]; + e.fd_flag = (flags & 0x01U) ? 1U : 0U; + e.brs = (flags & 0x02U) ? 1U : 0U; + e.esi = (flags & 0x04U) ? 1U : 0U; + + const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX) + ? e.dlc : static_cast(CANFD_ID_SNAPSHOT_DATA_MAX); + const uint16_t entrySize = static_cast(8U + dataLen); + if (offset + entrySize > len) break; + + memcpy(e.data, &payload[offset + 8U], dataLen); + for (uint8_t d = dataLen; d < CANFD_ID_SNAPSHOT_DATA_MAX; ++d) { + e.data[d] = 0U; + } + + offset += entrySize; + (*entryIdx)++; + } + out->valid = 1; return 0; } -/** - * @brief 切换 GD32 三路 CAN 接收过滤模式(GD32 SPI 帧类型 0xC2)。 - */ - /** * @brief 拉取指定通道的不重复 ID 快照表(GD32 SPI 帧类型 0xC3)。 * 分页拉取,每页 8 条,最多拉 2 页(16 条)。 diff --git a/main/transport/canfd_bridge.h b/main/transport/canfd_bridge.h index 320a6a7..b8fd858 100644 --- a/main/transport/canfd_bridge.h +++ b/main/transport/canfd_bridge.h @@ -121,15 +121,20 @@ typedef struct { } can_feedback_result_t; /** - * @brief 拉取一路 CAN 通道的 1Hz 反馈快照。 - * @param channel 目标通道 0/1/2。 - * @param out 输出快照(不可为 nullptr)。 - * @param timeout_ms SPI 事务超时。 - * @return 0 成功,<0 失败(ERROR 应答、payload 长度不足或 SPI 错误)。 + * @brief 解析 GD32 主动推送的一页 CAN 反馈 payload,累积到结果结构中。 + * + * GD32 APP 通过 requestEsp32Read(0xC1) 主动推送每页反馈,ESP32 收到后 + * 调用本函数解析。page=0 时重置 out 并填充固定头 + 首批条目; + * page=1 时在已有条目后追加。每页解析后置 out->valid=1。 + * + * @param payload GD32 推送帧的 payload([channel, page, total, count, mode, seq, ...])。 + * @param len payload 长度。 + * @param out 累积结果(跨 page 0/1 累积)。 + * @param entryIdx 当前已填充条目数(page 0 时被重置为 0,调用方按通道维护)。 + * @return 0 成功,<0 失败(payload 过短或通道非法)。 */ -int canFdPollFeedback(can_channel_t channel, - can_feedback_result_t *out, - uint32_t timeout_ms); +int canFdParseFeedbackPage(const uint8_t *payload, size_t len, + can_feedback_result_t *out, uint8_t *entryIdx); /** * @brief 切换 GD32 三路 CAN 的接收过滤模式。 diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index 8de7fc2..b0b1f79 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -13,9 +13,12 @@ #include "freertos/semphr.h" #include "driver/spi_master.h" #include "driver/gpio.h" +#include "soc/gpio_reg.h" #include "soc/spi_struct.h" #include "esp_err.h" #include "esp_log.h" +#include "rom/ets_sys.h" // esp_rom_delay_us (ets_delay_us) +#include "esp_intr_alloc.h" // ESP_INTR_FLAG_IRAM #include "spi_master.h" #include "monitor/debug_log.h" @@ -23,6 +26,14 @@ static constexpr size_t kSpiTransferMaxBytes = 256U; +/** @brief ISR-safe GPIO 电平读取(不调用 Flash 中的 gpio_get_level)。 + * 用于 SPIready 边沿中断,避免在 cache 被禁用期间访问 Flash 代码。 + */ +static int IRAM_ATTR readReadyLevelRaw(void) +{ + return (REG_READ(GPIO_IN_REG) >> SPI_READY_GPIO) & 0x1; +} + // ============================================================ // 单例 // ============================================================ @@ -37,40 +48,109 @@ SpiMaster& SpiMaster::instance() } /** - * @brief 等待 GD32 PB6 SPI Ready 握手引脚拉高(就绪)。 - * GD32 从机在 CS 断言瞬间拉低 PB6(忙),在响应帧就绪后拉高(就绪)。 - * 主机每次发起 CS 前调用,确保从机已准备好接收下一笔事务。 - * @param timeout_ms 超时时间(毫秒),0 表示不等待直接返回。 - * @return ESP_OK 表示从机就绪;ESP_ERR_TIMEOUT 表示超时(退化为继续发起)。 + * @brief 下拉 SPIready(开漏驱动低)。 + * 线与架构下,ESP32 主动发起通信时调用,通知 GD32「我要来了」。 + */ +void SpiMaster::driveReadyLow() +{ + gpio_set_level((gpio_num_t)SPI_READY_GPIO, 0); +} + +/** + * @brief 释放 SPIready(开漏高阻,靠上拉拉高)。 + * 线与架构下,ESP32 作为响应方完成读事务后调用。 + */ +void SpiMaster::releaseReady() +{ + gpio_set_level((gpio_num_t)SPI_READY_GPIO, 1); +} + +/** + * @brief GPIO 边沿中断回调(SPIready 线跳变)。 + * + * - 下降沿:GD32 主动按铃(请求 ESP32 来读)。若当前空闲则置 + * m_gd32RequestPending 并唤醒 SPI 任务。 + * - 上升沿:Bootloader init 完成(释放 SPIready),唤醒等待 + * WAITING_BOOT_READY 的任务。 + * + * ISR 内仅做标志翻转 + 任务通知,不做 SPI 传输。 + */ +void IRAM_ATTR SpiMaster::onReadyEdgeISR(void *arg) +{ + SpiMaster *self = static_cast(arg); + if (!self) { return; } + int level = readReadyLevelRaw(); + if (level == 0) { + /* 下降沿 */ + if (self->m_hsState == HandshakeState::IDLE) { + /* GD32 按铃 */ + self->m_gd32RequestPending = true; + if (self->m_spiTaskHandle) { + BaseType_t hp = pdFALSE; + xTaskNotifyFromISR(self->m_spiTaskHandle, 0x01U, + eSetBits, &hp); + portYIELD_FROM_ISR(hp); + } + } else if (self->m_hsState == HandshakeState::WAITING_BOOT_READY) { + /* 方案A: Bootloader 已接管(APP 拉低或 BOOT 极早拉低) */ + if (self->m_spiTaskHandle) { + BaseType_t hp = pdFALSE; + xTaskNotifyFromISR(self->m_spiTaskHandle, 0x04U, + eSetBits, &hp); + portYIELD_FROM_ISR(hp); + } + } + } else { + /* 上升沿 */ + if (self->m_hsState == HandshakeState::WAITING_RESPONSE) { + /* GD32 释放 SPIready,可发起新 CS */ + if (self->m_spiTaskHandle) { + BaseType_t hp = pdFALSE; + xTaskNotifyFromISR(self->m_spiTaskHandle, 0x02U, + eSetBits, &hp); + portYIELD_FROM_ISR(hp); + } + } else if (self->m_hsState == HandshakeState::WAITING_BOOT_READY) { + /* 方案A: Bootloader init 完成,释放 SPIready */ + if (self->m_spiTaskHandle) { + BaseType_t hp = pdFALSE; + xTaskNotifyFromISR(self->m_spiTaskHandle, 0x08U, + eSetBits, &hp); + portYIELD_FROM_ISR(hp); + } + } + } +} + +/** + * @brief 等 GD32 释放 SPIready(上升沿),中断驱动非忙等。 + * + * 调用前须将 m_hsState 置为 WAITING_RESPONSE。ISR 上升沿检测到该状态后 + * 发 0x02 通知唤醒本任务。GD32 写 Flash 期间保持 PB6 低,本函数阻塞, + * 写完拉高 → 唤醒 → 继续,自然避免 overrun。 */ esp_err_t SpiMaster::waitSpiReady(uint32_t timeout_ms) { - if (timeout_ms == 0U) { - return ESP_OK; - } + if (timeout_ms == 0U) { return ESP_OK; } + + /* 已高,立即返回 */ + if (readReadyLevelRaw() == 1) { return ESP_OK; } + + m_spiTaskHandle = xTaskGetCurrentTaskHandle(); + /* 清遗留 0x02 通知位 */ + xTaskNotifyWait(0x02U, 0x02U, NULL, 0U); const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms); for (;;) { - int level = gpio_get_level((gpio_num_t)SPI_READY_GPIO); - if (level == 1) { - /* 从机就绪:复位超时计数器,下次掉线时首次超时又能立即告警 */ - m_spiReadyTimeoutCount = 0U; - return ESP_OK; - } - if (xTaskGetTickCount() >= deadline) { - /* 限频:首次超时立即告警,之后每 kSpiReadyWarnEvery 次打印一条, - * 避免 GD32 掉线/卡死时高频日志刷屏。计数器无锁:仅在持锁的 - * transferClocked 路径调用,单任务访问,无需保护。 */ - ++m_spiReadyTimeoutCount; - if (m_spiReadyTimeoutCount == 1U || - (m_spiReadyTimeoutCount % kSpiReadyWarnEvery) == 0U) { - DEBUG_WARN("SPI", "waitSpiReady 超时: GPIO%d 仍为 LOW(从机忙), timeout=%ums, 累计=%lu", - SPI_READY_GPIO, (unsigned)timeout_ms, - (unsigned long)m_spiReadyTimeoutCount); - } - return ESP_ERR_TIMEOUT; - } - vTaskDelay(pdMS_TO_TICKS(1)); + TickType_t now = xTaskGetTickCount(); + if (now >= deadline) { return ESP_ERR_TIMEOUT; } + TickType_t remain = deadline - now; + uint32_t notify_val = 0U; + BaseType_t ok = xTaskNotifyWait(0x02U, 0x02U, ¬ify_val, + pdMS_TO_TICKS(remain)); + if (ok == pdTRUE && (notify_val & 0x02U)) { return ESP_OK; } + /* 电平二次检查,防中断丢失 */ + if (readReadyLevelRaw() == 1) { return ESP_OK; } } } @@ -108,11 +188,6 @@ esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len, return ESP_ERR_INVALID_SIZE; } - /* CS 断言前等待 GD32 PB6 拉高(就绪):从机在上一笔 onCsRelease/ - * processCommand 末尾才会拉高 PB6,提前发起 CS 会因从机未装好 - * DMA/响应帧而丢失字节。超时退化为继续发起,保留原有容错路径。 */ - (void)waitSpiReady(SPI_READY_TIMEOUT_MS); - uint8_t tx_staging[kSpiTransferMaxBytes]; uint8_t rx_staging[kSpiTransferMaxBytes]; const uint8_t *tx_ptr = tx_buf; @@ -136,8 +211,8 @@ esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len, t.tx_buffer = tx_ptr; t.rx_buffer = (rx_len > 0U) ? rx_ptr : NULL; - TickType_t ticks = (timeout_ms == 0U) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms); - esp_err_t ret = spi_device_queue_trans(m_spiDevice, &t, ticks); + /* timeout handled via spi_device_get_trans_result below */ + esp_err_t ret = spi_device_queue_trans(m_spiDevice, &t, portMAX_DELAY) /* always wait for queue slot */; if (ret != ESP_OK) { DEBUG_WARN("SPI", "spi_device_queue_trans fail: %s len=%u", esp_err_to_name(ret), (unsigned)(total_len * 8U)); @@ -145,13 +220,22 @@ esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len, } spi_transaction_t *ret_trans = NULL; - ret = spi_device_get_trans_result(m_spiDevice, &ret_trans, ticks); - (void)ret_trans; + ret = spi_device_get_trans_result(m_spiDevice, &ret_trans, pdMS_TO_TICKS(500)) /* DMA completion timeout */; if (ret != ESP_OK) { - DEBUG_WARN("SPI", "spi_device_get_trans_result timeout/fail: %s len=%u tx=%p rx=%p", - esp_err_to_name(ret), (unsigned)(total_len * 8U), t.tx_buffer, t.rx_buffer); - return ret; + /* Queue timeout: transaction may still be in DMA. Must retrieve with + * infinite wait, otherwise ret_queue residual blocks future queue_trans. */ + DEBUG_WARN("SPI", "spi_device_get_trans_result timeout: %s len=%u, retrying", + esp_err_to_name(ret), (unsigned)(total_len * 8U)); + ret = spi_device_get_trans_result(m_spiDevice, &ret_trans, portMAX_DELAY); + if (ret != ESP_OK) { + DEBUG_ERR("SPI", "spi_device_get_trans_result fail with infinite wait: %s", + esp_err_to_name(ret)); + return ret; + } + (void)ret_trans; + return ESP_ERR_TIMEOUT; } + (void)ret_trans; if (rx_len > 0U && rx_ptr != rx_buf) { memcpy(rx_buf, rx_ptr, rx_len); @@ -159,162 +243,105 @@ esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len, return ESP_OK; } -/** - * @brief 执行 SPI 请求-响应组合传输。 - * @param cmd 命令字节(0 表示无命令)。 - * @param payload 负载数据缓冲区。 - * @param payload_len 负载数据长度。 - * @param rx_buf 接收数据缓冲区。 - * @param rx_len 期望接收的数据长度。 - * @param timeout_ms 超时时间(毫秒),0 表示无限等待。 - * @return ESP_OK 表示成功,否则返回错误码。 - */ -esp_err_t SpiMaster::transferRequestResponse(uint8_t cmd, - const uint8_t *payload, size_t payload_len, - uint8_t *rx_buf, size_t rx_len, - uint32_t timeout_ms) -{ - if ((payload_len > 0U && payload == NULL) || (rx_len > 0U && rx_buf == NULL)) { - return ESP_ERR_INVALID_ARG; - } - - const size_t cmd_len = (cmd != 0U) ? 1U : 0U; - const size_t total_len = cmd_len + payload_len + rx_len; - if (total_len == 0U) { - return ESP_OK; - } - if (total_len > kSpiTransferMaxBytes) { - return ESP_ERR_INVALID_SIZE; - } - - uint8_t tx_staging[kSpiTransferMaxBytes]; - uint8_t rx_staging[kSpiTransferMaxBytes]; - size_t tx_pos = 0U; - - if (cmd_len > 0U) { - tx_staging[tx_pos++] = cmd; - } - if (payload_len > 0U) { - memcpy(&tx_staging[tx_pos], payload, payload_len); - tx_pos += payload_len; - } - if (tx_pos < total_len) { - memset(&tx_staging[tx_pos], 0xFF, total_len - tx_pos); - } - - esp_err_t ret = transferClocked(tx_staging, total_len, - (rx_len > 0U) ? rx_staging : NULL, - (rx_len > 0U) ? total_len : 0U, - timeout_ms); - if (ret == ESP_OK && rx_len > 0U) { - memcpy(rx_buf, &rx_staging[cmd_len + payload_len], rx_len); - } - return ret; -} - // ============================================================ // OTA 帧工具 // ============================================================ /** - * @brief 计算 OTA 帧的 8 位 CRC 校验和。 - * @param data 数据缓冲区。 - * @param len 数据长度。 - * @return 计算得到的 CRC 值。 + * @brief 计算 CRC32(MPEG-2,与 GD32 三端一致)。 */ -uint8_t SpiMaster::otaCrc8(const uint8_t *data, size_t len) +uint32_t SpiMaster::otaCrc32(const uint8_t *data, size_t len) { - uint8_t crc = 0x00; + uint32_t crc = 0xFFFFFFFFU; for (size_t i = 0; i < len; ++i) { - crc ^= data[i]; + crc ^= (uint32_t)data[i] << 24; + for (int b = 0; b < 8; ++b) { + if (crc & 0x80000000U) { + crc = (crc << 1) ^ 0x04C11DB7U; + } else { + crc <<= 1; + } + } } return crc; } -/** - * @brief 构建 OTA 协议帧。 - * @param type 帧类型。 - * @param payload 负载数据缓冲区。 - * @param payload_len 负载数据长度。 - * @param out 输出帧缓冲区。 - * @param out_len 输出实际帧长度。 - * @return true 表示构建成功,false 表示失败。 - */ +/* 同步帧头(6 字节): + * SOF0 SOF1 CREDIT TYPE LEN_LO LEN_HI + * 随后 payload,末尾 4 字节 CRC32(覆盖头 6 + payload,不含自身)。 */ +static constexpr size_t kSyncHeaderLen = 6U; +static constexpr size_t kSyncCrcLen = 4U; +static constexpr size_t kSyncOverhead = kSyncHeaderLen + kSyncCrcLen; + +static void putU16Le(uint8_t *pp, uint16_t v) +{ + pp[0] = (uint8_t)(v & 0xFFU); + pp[1] = (uint8_t)((v >> 8) & 0xFFU); +} + +static uint16_t getU16Le(const uint8_t *pp) +{ + return (uint16_t)(pp[0] | ((uint16_t)pp[1] << 8)); +} + +static void putU32Le(uint8_t *pp, uint32_t v) +{ + pp[0] = (uint8_t)(v & 0xFFU); + pp[1] = (uint8_t)((v >> 8) & 0xFFU); + pp[2] = (uint8_t)((v >> 16) & 0xFFU); + pp[3] = (uint8_t)((v >> 24) & 0xFFU); +} + +static uint32_t getU32Le(const uint8_t *pp) +{ + return (uint32_t)pp[0] | ((uint32_t)pp[1] << 8) | + ((uint32_t)pp[2] << 16) | ((uint32_t)pp[3] << 24); +} + bool SpiMaster::otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len, uint8_t *out, size_t *out_len) { if (!out || !out_len) return false; - size_t total = 5 + payload_len; + if (payload_len > 0U && payload == NULL) return false; + size_t total = kSyncOverhead + payload_len; if (total > SPI_OTA_FRAME_MAX) return false; - out[0] = SPI_OTA_SOF; - out[1] = type; - out[2] = (uint8_t)(payload_len & 0xFF); - out[3] = (uint8_t)((payload_len >> 8) & 0xFF); - if (payload && payload_len > 0) { - memcpy(&out[4], payload, payload_len); + + SpiMaster &self = SpiMaster::instance(); + out[0] = SPI_SYNC_SOF0; + out[1] = SPI_SYNC_SOF1; + out[2] = self.m_localCredit; + out[3] = type; + putU16Le(&out[4], (uint16_t)payload_len); + if (payload && payload_len > 0U) { + memcpy(&out[kSyncHeaderLen], payload, payload_len); } - out[4 + payload_len] = otaCrc8(out, 4 + payload_len); + uint32_t crc = otaCrc32(out, kSyncHeaderLen + payload_len); + putU32Le(&out[kSyncHeaderLen + payload_len], crc); *out_len = total; return true; } -/** - * @brief 解析 OTA 协议帧。 - * @param frame 帧数据缓冲区。 - * @param frame_len 帧数据长度。 - * @param type 输出解析得到的帧类型。 - * @param payload 输出指向负载数据的指针。 - * @param payload_len 输出负载数据长度。 - * @return true 表示解析成功,false 表示失败。 - */ -bool SpiMaster::otaParseFrame(const uint8_t *frame, size_t frame_len, - uint8_t *type, const uint8_t **payload, size_t *payload_len) -{ - if (!frame || frame_len < 5) return false; - if (frame[0] != SPI_OTA_SOF) return false; - - size_t plen = (size_t)frame[2] | ((size_t)frame[3] << 8); - size_t total = 5 + plen; - if (frame_len < total) return false; - - uint8_t expected_crc = otaCrc8(frame, 4 + plen); - if (frame[4 + plen] != expected_crc) return false; - - if (type) *type = frame[1]; - if (payload) *payload = (plen > 0) ? &frame[4] : nullptr; - if (payload_len) *payload_len = plen; - return true; -} - -/** - * @brief 在数据缓冲区中搜索并解析 OTA 帧。 - * @param buf 数据缓冲区。 - * @param buf_len 数据缓冲区长度。 - * @param type 输出帧类型。 - * @param payload 输出指向负载数据的指针。 - * @param payload_len 输出负载数据长度。 - * @param frame_offset 输出帧在缓冲区中的偏移位置。 - * @return true 表示找到并成功解析,false 表示未找到。 - */ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len, uint8_t *type, const uint8_t **payload, size_t *payload_len, size_t *frame_offset) { - for (size_t i = 0; i + 5 <= buf_len; ++i) { - if (buf[i] != SPI_OTA_SOF) continue; + if (!buf) return false; - size_t plen = (size_t)buf[i + 2] | ((size_t)buf[i + 3] << 8); - size_t total = 5 + plen; + for (size_t i = 0; i + kSyncOverhead <= buf_len; ++i) { + if (buf[i] != SPI_SYNC_SOF0 || buf[i + 1] != SPI_SYNC_SOF1) continue; + size_t plen = getU16Le(&buf[i + 4]); + size_t total = kSyncOverhead + plen; if (i + total > buf_len) continue; - - uint8_t expected_crc = otaCrc8(&buf[i], 4 + plen); - if (buf[i + 4 + plen] != expected_crc) { - DEBUG_WARN("SPI", "CRC mismatch at offset %u: plen=%u expected=0x%02X actual=0x%02X", - (unsigned)i, (unsigned)plen, expected_crc, buf[i + 4 + plen]); + uint32_t expect = getU32Le(&buf[i + kSyncHeaderLen + plen]); + uint32_t calc = otaCrc32(&buf[i], kSyncHeaderLen + plen); + if (expect != calc) { + DEBUG_WARN("SPI", "SYNC CRC mismatch @%u: plen=%u", + (unsigned)i, (unsigned)plen); continue; } - - if (type) *type = buf[i + 1]; - if (payload) *payload = (plen > 0) ? &buf[i + 4] : nullptr; + /* 解析对端 CREDIT,让 ota_relay 的 CREDIT 感知逻辑生效 */ + SpiMaster::instance().m_remoteCredit = buf[i + 2]; + if (type) *type = buf[i + 3]; + if (payload) *payload = (plen > 0) ? &buf[i + kSyncHeaderLen] : nullptr; if (payload_len) *payload_len = plen; if (frame_offset) *frame_offset = i; return true; @@ -352,7 +379,7 @@ esp_err_t SpiMaster::init() bus_cfg.sclk_io_num = 6; bus_cfg.quadwp_io_num = -1; bus_cfg.quadhd_io_num = -1; - bus_cfg.max_transfer_sz = kSpiTransferMaxBytes; + bus_cfg.max_transfer_sz = kSpiTransferMaxBytes * SPI_QUEUE_SIZE; esp_err_t ret = spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO); if (ret != ESP_OK) { @@ -368,8 +395,8 @@ esp_err_t SpiMaster::init() dev_cfg.command_bits = 0; dev_cfg.address_bits = 0; dev_cfg.dummy_bits = 0; - dev_cfg.cs_ena_posttrans = 10; - dev_cfg.cs_ena_pretrans = 20; + dev_cfg.cs_ena_posttrans = 20; + dev_cfg.cs_ena_pretrans = 40; ret = spi_bus_add_device(SPI2_HOST, &dev_cfg, &m_spiDevice); if (ret != ESP_OK) { @@ -380,74 +407,54 @@ esp_err_t SpiMaster::init() DEBUG_INFO("SPI", "SPI2 总线初始化完成: MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6, CS=GPIO%d, CLK=%d Hz", SPI_MASTER_CS_GPIO, SPI_CLOCK_HZ); - /* 配置 SPI Ready 握手输入引脚(GPIO10),监测 GD32 PB6 电平。 - * GD32 在 CS 下降沿拉低 PB6(忙),在 onCsRelease/processCommand 末尾拉高(就绪)。 - * 主机每次发起 CS 前须等待 PB6=HIGH,避免从机未就绪时抢跑丢帧。 */ + /* 配置 SPI Ready 握手引脚(GPIO10),开漏线与(GPIO10<->GD32 PB6)。 + * 双方都是开漏输出+上拉,任一下拉 = 线低;双方释放 = 线高(空闲)。 + * 双边沿中断:下降沿=GD32 按铃,上升沿=GD32 响应就绪。 */ gpio_config_t ready_cfg = {}; ready_cfg.pin_bit_mask = (1ULL << SPI_READY_GPIO); - ready_cfg.mode = GPIO_MODE_INPUT; + ready_cfg.mode = GPIO_MODE_INPUT_OUTPUT_OD; /* 开漏:可输出驱动低,也可读回电平 */ ready_cfg.pull_up_en = GPIO_PULLUP_ENABLE; ready_cfg.pull_down_en = GPIO_PULLDOWN_DISABLE; - ready_cfg.intr_type = GPIO_INTR_DISABLE; + ready_cfg.intr_type = GPIO_INTR_ANYEDGE; /* 双边沿中断 */ esp_err_t gpio_ret = gpio_config(&ready_cfg); if (gpio_ret != ESP_OK) { DEBUG_WARN("SPI", "SPI Ready 握手引脚 GPIO%d 配置失败: %s", SPI_READY_GPIO, esp_err_to_name(gpio_ret)); - /* 非致命:握手失败时退化为不等待,保持原有行为 */ } else { - DEBUG_INFO("SPI", "SPI Ready 握手输入已配置: GPIO%d (监测 GD32 PB6)", SPI_READY_GPIO); + DEBUG_INFO("SPI", "SPI Ready 开漏握手已配置: GPIO%d (线与 GD32 PB6)", SPI_READY_GPIO); } + /* 释放 SPIready(开漏高阻),确保空闲态线 HIGH */ + releaseReady(); + + /* 安装 GPIO ISR 服务(全局仅安装一次)并注册边沿回调 */ + static bool s_isrInstalled = false; + if (!s_isrInstalled) { + gpio_ret = gpio_install_isr_service(ESP_INTR_FLAG_IRAM); + if (gpio_ret != ESP_OK && gpio_ret != ESP_ERR_INVALID_STATE) { + DEBUG_WARN("SPI", "gpio_install_isr_service 失败: %s", esp_err_to_name(gpio_ret)); + } else { + s_isrInstalled = true; + } + } + if (s_isrInstalled) { + gpio_ret = gpio_isr_handler_add((gpio_num_t)SPI_READY_GPIO, + onReadyEdgeISR, this); + if (gpio_ret != ESP_OK) { + DEBUG_WARN("SPI", "gpio_isr_handler_add GPIO%d 失败: %s", + SPI_READY_GPIO, esp_err_to_name(gpio_ret)); + } + } + + /* 记录当前任务句柄,供 GPIO ISR 唤醒 */ + m_spiTaskHandle = xTaskGetCurrentTaskHandle(); + m_hsState = HandshakeState::IDLE; + m_gd32RequestPending = false; + m_localCredit = 8; + m_remoteCredit = 8; + return ESP_OK; } -// ============================================================ -// 公开 SPI 传输接口 -// ============================================================ -/** - * @brief 执行一次同步 SPI 事务,先发送请求,再读取响应字节。 - * @param cmd 命令字节。 - * @param payload 负载数据缓冲区。 - * @param payload_len 负载数据长度。 - * @param recv_buf 接收数据缓冲区。 - * @param recv_len 接收数据长度。 - * @param timeout_ms 超时时间(毫秒)。 - * @return ESP_OK 表示成功,否则返回错误码。 - */ -esp_err_t SpiMaster::syncTransfer(uint8_t cmd, const uint8_t *payload, - size_t payload_len, uint8_t *recv_buf, - size_t recv_len, uint32_t timeout_ms) -{ - LockGuard lock(m_mutex); - return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len, timeout_ms); -} - -/** - * @brief 仅发送 SPI 请求数据,不关心返回内容。 - * @param cmd 命令字节。 - * @param payload 负载数据缓冲区。 - * @param len 负载数据长度。 - * @return ESP_OK 表示成功,否则返回错误码。 - */ -esp_err_t SpiMaster::send(uint8_t cmd, const uint8_t *payload, size_t len) -{ - LockGuard lock(m_mutex); - return transferRequestResponse(cmd, payload, len, NULL, 0, 0U); -} - -/** - * @brief 仅接收 SPI 数据,必要时先发送命令字节。 - * @param cmd 命令字节。 - * @param recv_buf 接收数据缓冲区。 - * @param len 接收数据长度。 - * @param timeout_ms 超时时间(毫秒)。 - * @return ESP_OK 表示成功,否则返回错误码。 - */ -esp_err_t SpiMaster::recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms) -{ - LockGuard lock(m_mutex); - return transferRequestResponse(cmd, NULL, 0, recv_buf, len, timeout_ms); -} - // ============================================================ // SPI OTA 协议 // ============================================================ @@ -474,17 +481,83 @@ esp_err_t SpiMaster::otaTransfer(uint8_t type, const uint8_t *payload, size_t pa return ESP_ERR_INVALID_ARG; } - DEBUG_DBG("SPI", "OTA TX: type=0x%02X len=%u first4=%02X%02X%02X%02X", - type, (unsigned)tx_len, tx_frame[0], tx_frame[1], tx_frame[2], tx_frame[3]); - esp_err_t ret = transferClocked(tx_frame, tx_len, NULL, 0, timeout_ms); + DEBUG_DBG("SPI", "OTA TX: type=0x%02X len=%u", type, (unsigned)tx_len); + + /* OTA 帧内容 hex dump(前 18 字节),用于与 GD32 端 [RX-FRM] 日志对比诊断 CRC 问题。 + * 仅对 OTA 帧类型(0x01~0x0F)打印,避免 CAN 反馈(0xC1)每秒刷屏。 */ + if (type <= 0x0FU) { + char hexbuf[64] = {0}; + size_t dump_len = (tx_len < 18U) ? tx_len : 18U; + for (size_t i = 0U; i < dump_len; ++i) { + snprintf(hexbuf + i * 3, sizeof(hexbuf) - i * 3, "%02X ", tx_frame[i]); + } + DEBUG_INFO("SPI", "OTA TX hex: %s", hexbuf); + } + + if (tx_len > SPI_OTA_FRAME_MAX) { + DEBUG_ERR("SPI", "OTA frame too large: %u", (unsigned)tx_len); + return ESP_ERR_INVALID_SIZE; + } + + /* 同步单 CS 传输:拉低 SPIready 后,在单次 SPI 事务中同时发送请求并读回响应。 + * 注意:GD32 BOOT 采用"延迟一拍"协议——响应在 onCsAssert() 装载 DMA, + * queueTxFrame() 只缓冲不启动 DMA。因此本笔 CS 内读到的响应属于上一笔请求, + * 首笔请求必读 0xFF。握手请求幂等可通过重试匹配;唯一性请求需配合 + * otaPollResponse() 读取延迟响应。详见 SpiOta.cpp queueTxFrame 注释。 */ + m_hsState = HandshakeState::WAITING_RESPONSE; + /* 等 GD32 释放 PB6(上一笔 onCsRelease 处理完)再发起新 CS。 + * 中断驱动:GD32 拉高 → ISR 0x02 → 唤醒。GD32 写 Flash 期间 + * PB6 保持低,本函数阻塞直到写完,自然避免 overrun。 + * 0xB0 不走 otaTransfer,不受此等待影响。 */ + (void)waitSpiReady(SPI_READY_TIMEOUT_MS); + m_hsState = HandshakeState::MASTER_SENDING; + + driveReadyLow(); + if (SPI_READY_DRIVE_DELAY_US > 0U) { + esp_rom_delay_us(SPI_READY_DRIVE_DELAY_US); + } + + uint8_t tx_padded[SPI_OTA_FRAME_MAX]; + if (tx_len > sizeof(tx_padded)) { + DEBUG_ERR("SPI", "OTA frame %u exceeds tx_padded %u", (unsigned)tx_len, (unsigned)sizeof(tx_padded)); + releaseReady(); + m_hsState = HandshakeState::IDLE; + return ESP_ERR_INVALID_SIZE; + } + memset(tx_padded, 0xFF, sizeof(tx_padded)); + memcpy(tx_padded, tx_frame, tx_len); + + constexpr size_t kRxBytes = SPI_OTA_FRAME_MAX; + uint8_t rx_raw[kRxBytes]; + memset(rx_raw, 0xFF, sizeof(rx_raw)); + + esp_err_t ret = transferClocked(tx_padded, sizeof(tx_padded), rx_raw, sizeof(rx_raw), timeout_ms); + releaseReady(); + m_hsState = HandshakeState::IDLE; if (ret != ESP_OK) { - DEBUG_WARN("SPI", "OTA 请求发送失败: %s", esp_err_to_name(ret)); + DEBUG_WARN("SPI", "OTA sync transfer failed: %s", esp_err_to_name(ret)); return ret; } - return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, timeout_ms); -} + uint8_t found_type = 0; + const uint8_t *found_payload = nullptr; + size_t found_len = 0; + size_t frame_offset = 0; + if (otaFindFrame(rx_raw, sizeof(rx_raw), &found_type, &found_payload, &found_len, &frame_offset)) { + if (resp_type) *resp_type = found_type; + if (resp_payload && resp_max > 0) { + size_t copy_len = (found_len < resp_max) ? found_len : resp_max; + if (found_payload && copy_len > 0) { + memcpy(resp_payload, found_payload, copy_len); + } + } + if (resp_len) *resp_len = found_len; + return ESP_OK; + } + DEBUG_WARN("SPI", "OTA sync transfer response not found"); + return ESP_ERR_INVALID_RESPONSE; +} /** * @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。 * @param resp_type 输出响应类型。 @@ -515,21 +588,40 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p { constexpr size_t kPollBytes = 160U; TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms); + uint32_t attempt = 0; while (xTaskGetTickCount() <= deadline) { + ++attempt; + + /* 等 GD32 释放 PB6(当前帧 onCsRelease 处理完)再发起 CS#2 poll。 + * 中断驱动:GD32 拉高 → ISR 0x02 → 唤醒。GD32 写 Flash 期间 + * PB6 保持低,本函数阻塞直到写完,自然避免 overrun。 */ + m_hsState = HandshakeState::WAITING_RESPONSE; + if (waitSpiReady(SPI_READY_TIMEOUT_MS) != ESP_OK) { + /* 超时则退化为继续 poll(容错) */ + } + uint8_t rx_raw[kPollBytes]; uint8_t dummy[kPollBytes]; memset(dummy, 0xFF, sizeof(dummy)); - /* 用剩余时间作为单次 SPI 事务超时,避免总等待远超 deadline */ TickType_t now = xTaskGetTickCount(); uint32_t remain_ms = (deadline > now) ? ((deadline - now) * portTICK_PERIOD_MS) : 0U; if (remain_ms == 0U) break; - - esp_err_t ret = transferClocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw), remain_ms); + uint32_t spi_timeout = (remain_ms > 500U) ? 500U : remain_ms; + esp_err_t ret = transferClocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw), spi_timeout); if (ret != ESP_OK) { - DEBUG_WARN("SPI", "OTA 响应读取失败: %s", esp_err_to_name(ret)); - vTaskDelay(pdMS_TO_TICKS(10)); + vTaskDelay(pdMS_TO_TICKS(2)); + continue; + } + + /* Check if all 0xFF (slave not driving MISO) */ + bool all_ff = true; + for (size_t k = 0; k < 8 && k < sizeof(rx_raw); ++k) { + if (rx_raw[k] != 0xFF) { all_ff = false; break; } + } + if (all_ff) { + vTaskDelay(pdMS_TO_TICKS(CONFIG_DCU_OTA_POLL_BACKOFF_MS)); continue; } @@ -539,12 +631,9 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p size_t frame_offset = 0; if (otaFindFrame(rx_raw, sizeof(rx_raw), &found_type, &found_payload, &found_len, &frame_offset)) { - /* 有效性检查:ACK/ERROR 帧必须有 payload,空 payload 的 ACK 是帧截断/错位, - * 继续轮询而非当作有效响应返回。 */ + /* Skip empty ACK/ERROR responses (stale from previous transaction) */ if ((found_type == SPI_OTA_RSP_ACK || found_type == SPI_OTA_RSP_ERROR) && found_len == 0) { - DEBUG_DBG("SPI", "OTA 响应帧截断: type=0x%02X len=0 offset=%u, 继续轮询", - found_type, (unsigned)frame_offset); - vTaskDelay(pdMS_TO_TICKS(5)); + vTaskDelay(pdMS_TO_TICKS(2)); continue; } @@ -557,23 +646,23 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p } if (resp_len) *resp_len = found_len; - DEBUG_DBG("SPI", "OTA 响应: type=0x%02X len=%u offset=%u", - found_type, (unsigned)found_len, (unsigned)frame_offset); - if (found_type == SPI_OTA_RSP_WAIT) { - vTaskDelay(pdMS_TO_TICKS(10)); + vTaskDelay(pdMS_TO_TICKS(CONFIG_DCU_OTA_POLL_BACKOFF_MS)); continue; } + m_hsState = HandshakeState::IDLE; return ESP_OK; } - vTaskDelay(pdMS_TO_TICKS(10)); + vTaskDelay(pdMS_TO_TICKS(2)); } - DEBUG_WARN("SPI", "OTA 响应超时: %lu ms", (unsigned long)timeout_ms); + m_hsState = HandshakeState::IDLE; + DEBUG_WARN("SPI", "OTA response timeout: %lu ms", (unsigned long)timeout_ms); return ESP_ERR_TIMEOUT; } + /** * @brief 通知 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。 * @return ESP_OK 表示成功,否则返回错误码。 @@ -583,8 +672,14 @@ esp_err_t SpiMaster::appRequestBootloader() LockGuard lock(m_mutex); DEBUG_INFO("SPI", "请求 GD32 APP 进入 Bootloader..."); + /* 清空残留的 GD32 按铃标志,避免 can_feedback_task 的 checkGd32Request() + * 在 OTA 期间因残留标志发起 SPI 读事务,与 0xB0 通信抢占总线。 + * can_feedback_task 此时已被 pauseCanFeedbackTask(true) 暂停,但可能 + * 有一次 checkGd32Request() 正在阻塞中,清空标志让它安全返回。 */ + m_gd32RequestPending = false; + constexpr uint32_t kMaxRetries = 3U; - constexpr uint32_t kPerRetryAckMs = 200U; + constexpr uint32_t kPerRetryAckMs = 100U; constexpr uint32_t kRetryGapMs = 50U; for (uint32_t retry = 0U; retry < kMaxRetries; ++retry) { @@ -594,7 +689,9 @@ esp_err_t SpiMaster::appRequestBootloader() } uint8_t cmd = SPI_APP_ENTER_BOOT_CMD; - esp_err_t ret = transferClocked(&cmd, 1, NULL, 0); + /* 0xB0 是单字节命令,GD32 APP 可能在主动按铃(PB6=LOW)。 + * GD32 onCsAssert() 会立即处理 0xB0,不等 SPIready 就绪信号。 */ + esp_err_t ret = transferClocked(&cmd, 1, NULL, 0, 0U); if (ret != ESP_OK) { DEBUG_WARN("SPI", "发送 Bootloader 请求失败: %s", esp_err_to_name(ret)); continue; @@ -604,11 +701,16 @@ esp_err_t SpiMaster::appRequestBootloader() while (xTaskGetTickCount() <= deadline) { uint8_t rx = 0; uint8_t dummy = 0xFF; - ret = transferClocked(&dummy, 1, &rx, 1); + /* 读 ACK 时 GD32 APP/BOOT 可能仍持有 PB6=LOW(正在准备 0xB5 响应), + * 延迟一拍协议下下一次 CS 的 onCsAssert() 会装载 0xB5 到 DMA。 */ + ret = transferClocked(&dummy, 1, &rx, 1, 0U); if (ret == ESP_OK && rx == SPI_APP_ENTER_BOOT_ACK_OK) { DEBUG_INFO("SPI", "GD32 APP 已确认进入 Bootloader (retry=%lu)", (unsigned long)retry); return ESP_OK; } + if (ret == ESP_OK && rx != 0xFFU) { + DEBUG_DBG("SPI", "Bootloader ACK 字节=0x%02X (非0xB5,APP可能使用帧ACK)", rx); + } vTaskDelay(pdMS_TO_TICKS(10)); } } @@ -617,6 +719,91 @@ esp_err_t SpiMaster::appRequestBootloader() return ESP_ERR_TIMEOUT; } +/** + * @brief 方案A: 通过 PB6 LOW→HIGH 序列等待 Bootloader 就绪(中断驱动)。 + * + * 时序: + * APP 拉低 PB6 → 20ms → 复位(μs级HIGH) → Bootloader极早推挽LOW → init... → 释放HIGH + * ESP32 过滤: + * Phase1: 置 WAITING_BOOT_READY,GPIO 下降沿中断唤醒(bit 0x04) + * Phase2: 同状态,GPIO 上升沿中断唤醒(bit 0x08)+ 10ms 稳定确认 + * + * @param timeout_ms 总超时(毫秒)。 + * @return ESP_OK 表示 Bootloader 就绪;ESP_ERR_TIMEOUT 表示超时。 + */ +esp_err_t SpiMaster::waitForBootloaderReady(uint32_t timeout_ms) +{ + m_spiTaskHandle = xTaskGetCurrentTaskHandle(); + + /* 清除遗留通知位 (0x04=下降沿, 0x08=上升沿) */ + xTaskNotifyWait(0x0CU, 0x0CU, NULL, 0U); + + /* Phase1: 等待下降沿 (APP 复位前拉低 / Bootloader 极早拉低) + * 先检查当前电平,若已为 LOW 则跳过等待 */ + m_hsState = HandshakeState::WAITING_BOOT_READY; + + if (gpio_get_level((gpio_num_t)SPI_READY_GPIO) == 1) { + /* 仍为 HIGH,等下降沿中断 */ + constexpr uint32_t kPhase1TimeoutMs = 500U; + const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(kPhase1TimeoutMs); + bool seen_low = false; + for (;;) { + TickType_t now = xTaskGetTickCount(); + if (now >= deadline) { break; } + TickType_t remain = deadline - now; + uint32_t notify_val = 0U; + BaseType_t ok = xTaskNotifyWait(0x0CU, 0x0CU, ¬ify_val, + pdMS_TO_TICKS(remain > portMAX_DELAY / 2 ? portMAX_DELAY / 2 : remain)); + if (ok == pdTRUE && (notify_val & 0x04U)) { seen_low = true; break; } + /* 防中断丢失:再次检查电平 */ + if (gpio_get_level((gpio_num_t)SPI_READY_GPIO) == 0) { seen_low = true; break; } + } + if (!seen_low) { + m_hsState = HandshakeState::IDLE; + DEBUG_WARN("SPI", "waitForBootloaderReady: Phase1 未检测到 LOW"); + return ESP_ERR_TIMEOUT; + } + } + + DEBUG_INFO("SPI", "waitForBootloaderReady: Phase1 检测到 LOW,等待 Bootloader 就绪"); + + /* Phase2: 等待上升沿 (Bootloader init 完成释放 HIGH) + * 10ms 稳定确认过滤复位期间 μs 级虚假 HIGH */ + const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms); + for (;;) { + TickType_t now = xTaskGetTickCount(); + if (now >= deadline) { break; } + TickType_t remain = deadline - now; + uint32_t notify_val = 0U; + BaseType_t ok = xTaskNotifyWait(0x0CU, 0x0CU, ¬ify_val, + pdMS_TO_TICKS(remain > portMAX_DELAY / 2 ? portMAX_DELAY / 2 : remain)); + if (ok == pdTRUE && (notify_val & 0x08U)) { + /* 收到上升沿,确认 HIGH 持续 10ms 过滤虚假脉冲 */ + vTaskDelay(pdMS_TO_TICKS(10)); + if (gpio_get_level((gpio_num_t)SPI_READY_GPIO) == 1) { + m_hsState = HandshakeState::IDLE; + DEBUG_INFO("SPI", "waitForBootloaderReady: Bootloader 就绪 (PB6 HIGH stable)"); + return ESP_OK; + } + /* 虚假脉冲,继续等待 */ + DEBUG_DBG("SPI", "waitForBootloaderReady: 虚假 HIGH,继续等待"); + } + /* 防中断丢失:再次检查电平 */ + if (gpio_get_level((gpio_num_t)SPI_READY_GPIO) == 1) { + vTaskDelay(pdMS_TO_TICKS(10)); + if (gpio_get_level((gpio_num_t)SPI_READY_GPIO) == 1) { + m_hsState = HandshakeState::IDLE; + DEBUG_INFO("SPI", "waitForBootloaderReady: Bootloader 就绪 (poll fallback)"); + return ESP_OK; + } + } + } + + m_hsState = HandshakeState::IDLE; + DEBUG_WARN("SPI", "waitForBootloaderReady: Phase2 等待 HIGH 超时 (%ums)", (unsigned)timeout_ms); + return ESP_ERR_TIMEOUT; +} + /** * @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 * @param timeout_ms 超时时间(毫秒)。 @@ -629,7 +816,7 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver LockGuard lock(m_mutex); constexpr uint8_t kAppPingType = 0xA1U; - uint8_t tx_frame[8] = {0}; + uint8_t tx_frame[16] = {0}; size_t tx_len = 0U; if (!otaBuildFrame(kAppPingType, NULL, 0U, tx_frame, &tx_len)) { return ESP_ERR_INVALID_ARG; @@ -648,25 +835,21 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver memset(tx_padded, 0xFF, sizeof(tx_padded)); memcpy(tx_padded, tx_frame, tx_len); /* [0..4]=PING帧, [5..15]=0xFF */ - esp_err_t err = transferClocked(tx_padded, sizeof(tx_padded), NULL, 0U); - if (err != ESP_OK) { - DEBUG_DBG("SPI", "APP PING 请求发送失败[%lu]: %s", - (unsigned long)attempt, esp_err_to_name(err)); - vTaskDelay(pdMS_TO_TICKS(5)); - continue; + /* 同步单 CS PING:下拉 SPIready 后,在单次 SPI 事务中发送并读回响应。 */ + m_hsState = HandshakeState::MASTER_SENDING; + driveReadyLow(); + if (SPI_READY_DRIVE_DELAY_US > 0U) { + esp_rom_delay_us(SPI_READY_DRIVE_DELAY_US); } - /* 给 GD32 充足时间处理 CS 释放中断并准备响应帧 */ - vTaskDelay(pdMS_TO_TICKS(10)); - uint8_t rx_raw[48]; /* 增大缓冲区,吸收 SPI 字节偏移 */ - uint8_t dummy[48]; - memset(dummy, 0xFF, sizeof(dummy)); memset(rx_raw, 0xFF, sizeof(rx_raw)); - err = transferClocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw)); + esp_err_t err = transferClocked(tx_padded, sizeof(tx_padded), rx_raw, sizeof(rx_raw), 0U); + releaseReady(); + m_hsState = HandshakeState::IDLE; if (err != ESP_OK) { - DEBUG_DBG("SPI", "APP PING 响应读取失败[%lu]: %s", + DEBUG_DBG("SPI", "APP PING sync transfer failed[%lu]: %s", (unsigned long)attempt, esp_err_to_name(err)); vTaskDelay(pdMS_TO_TICKS(5)); continue; @@ -748,43 +931,95 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver } /** - * @brief 执行带重试的 SPI 同步传输。 - * @param cmd 命令字节。 - * @param payload 负载数据缓冲区。 - * @param payload_len 负载数据长度。 - * @param recv_buf 接收数据缓冲区。 - * @param recv_len 接收数据长度。 - * @param max_retries 最大重试次数。 - * @return ESP_OK 表示成功,否则返回错误码。 + * @brief 检查 GD32 是否有主动按铃请求,若有则发起读事务读取请求帧。 + * + * GD32 下拉 SPIready 表示「我有请求帧要给你读」。此函数检测到该状态后 + * 发起一次 SPI 读事务(发 dummy 字节,读回 GD32 准备的请求帧)。 + * + * @param timeout_ms 等待 GD32 请求的超时(毫秒),0 表示不等待仅检查。 + * @return ESP_OK 成功读到请求帧;ESP_ERR_NOT_FOUND 当前无请求;其余为错误码。 */ -esp_err_t SpiMaster::transferWithRetry(uint8_t cmd, const uint8_t *payload, - size_t payload_len, uint8_t *recv_buf, - size_t recv_len, int max_retries) +esp_err_t SpiMaster::checkGd32Request(uint32_t timeout_ms) { - LockGuard lock(m_mutex); - esp_err_t ret; - int retry_count = 0; + /* 登记 ISR 通知目标任务,确保下降沿中断能唤醒本任务 */ + m_spiTaskHandle = xTaskGetCurrentTaskHandle(); - DEBUG_INFO("SPI", "SPI 传输 (带重试): cmd=0x%02X, 最大重试=%d", cmd, max_retries); - - while (retry_count < max_retries) { - ret = transferRequestResponse(cmd, payload, payload_len, - recv_buf, recv_len, 3000); - if (ret == ESP_OK) { - if (retry_count > 0) { - DEBUG_INFO("SPI", "SPI 传输成功 (重试 %d 次后成功)", retry_count); - } - return ESP_OK; + if (!m_gd32RequestPending) { + if (timeout_ms == 0U) { + return ESP_ERR_NOT_FOUND; + } + /* 等待 GD32 按铃通知(bit 0x01) */ + xTaskNotifyWait(0x01U, 0x01U, NULL, 0U); + TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms); + while (!m_gd32RequestPending && xTaskGetTickCount() < deadline) { + uint32_t val = 0U; + xTaskNotifyWait(0x01U, 0x01U, &val, pdMS_TO_TICKS(10)); + } + if (!m_gd32RequestPending) { + return ESP_ERR_NOT_FOUND; } - - retry_count++; - DEBUG_WARN("SPI", "SPI 重试 %d/%d, cmd=0x%02X, err=%s", - retry_count, max_retries, cmd, esp_err_to_name(ret)); - vTaskDelay(pdMS_TO_TICKS(100)); } - DEBUG_ERR("SPI", "SPI 传输 %d 次重试后失败, cmd=0x%02X", max_retries, cmd); - return ESP_ERR_TIMEOUT; + LockGuard lock(m_mutex); + m_gd32RequestPending = false; + m_hsState = HandshakeState::SERVING_GD32_RING; + + /* GD32 已下拉 SPIready 并准备好请求帧在 TX buffer。 + * 发起一次 SPI 读事务:发 dummy 字节,读回 GD32 的请求帧。 + * GD32 在 CS 释放后释放 SPIready。 */ + constexpr size_t kRingReadBytes = 160U; + uint8_t rx_raw[kRingReadBytes]; + uint8_t dummy[kRingReadBytes]; + memset(dummy, 0xFF, sizeof(dummy)); + memset(rx_raw, 0xFF, sizeof(rx_raw)); + + /* GD32 已下拉 SPIready,我们是响应方直接读 */ + esp_err_t ret = transferClocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw), 500U); + if (ret != ESP_OK) { + DEBUG_WARN("SPI", "checkGd32Request 读事务失败: %s", esp_err_to_name(ret)); + m_hsState = HandshakeState::IDLE; + return ret; + } + + /* 解析请求帧 */ + uint8_t found_type = 0; + const uint8_t *found_payload = nullptr; + size_t found_len = 0; + size_t frame_offset = 0; + if (otaFindFrame(rx_raw, sizeof(rx_raw), &found_type, &found_payload, + &found_len, &frame_offset)) { + m_ringRxLen = found_len; + m_ringRxType = found_type; + if (found_payload && found_len > 0 && found_len <= sizeof(m_ringRxBuf)) { + memcpy(m_ringRxBuf, found_payload, found_len); + } + DEBUG_INFO("SPI", "GD32 按铃请求帧: type=0x%02X len=%u", + found_type, (unsigned)found_len); + m_hsState = HandshakeState::IDLE; + /* 上层通过 takeRingRx() 取出 type/payload 进行处理 */ + return ESP_OK; + } + + DEBUG_WARN("SPI", "GD32 按铃读事务未找到合法帧"); + m_hsState = HandshakeState::IDLE; + return ESP_ERR_INVALID_RESPONSE; +} + +bool SpiMaster::takeRingRx(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len) +{ + LockGuard lock(m_mutex); + if (m_ringRxLen == 0U) { + return false; + } + if (type) { *type = m_ringRxType; } + size_t copy_len = (m_ringRxLen <= max_len) ? m_ringRxLen : max_len; + if (len) { *len = copy_len; } + if (payload && copy_len > 0U) { + memcpy(payload, m_ringRxBuf, copy_len); + } + m_ringRxLen = 0U; + m_ringRxType = 0U; + return true; } // CANFD 接口已迁移至 transport/canfd_bridge.cpp(M4 解耦) diff --git a/main/transport/spi_master.h b/main/transport/spi_master.h index dc4e7e8..4556bd0 100644 --- a/main/transport/spi_master.h +++ b/main/transport/spi_master.h @@ -1,31 +1,41 @@ -/* +/* * spi_master.h - SPI Master 驱动头文件 * CANFD 3通道发送接口 + 底层 SPI 总线 * * SpiMaster 类封装 SPI 总线初始化、CANFD 通信和 SPI OTA 协议。 + * + * 握手架构(线与 SPIready,GPIO10<->GD32 PB6 开漏单线): + * - 空闲态线 HIGH(双方释放,靠上拉)。 + * - ESP32 主动发起:下拉 SPIready -> 立刻 CS 发请求 -> CS 释放后保持下拉, + * GD32 在 onCsRelease() ISR 内完成帧处理+响应装载后释放 SPIready(高), + * ESP32 GPIO 上升沿中断唤醒后发起 CS#2 读响应。 + * - GD32 主动按铃:下拉 SPIready -> ESP32 下降沿中断唤醒后发起读事务。 */ #ifndef SPI_MASTER_H #define SPI_MASTER_H #include #include "driver/spi_master.h" +#include "driver/gpio.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" // SPI 配置常量 #define SPI_MASTER_CS_GPIO (4) // CS 片选引脚 -#define SPI_CLOCK_HZ (4 * 1000 * 1000) // 4MHz +#define SPI_CLOCK_HZ CONFIG_DCU_SPI_CLOCK_HZ // 默认 4MHz,可在 menuconfig 手动提速 #define SPI_MODE (0) // CPOL=0, CPHA=0 -#define SPI_QUEUE_SIZE (1) ///< SPI 事务队列深度 -#define SPI_READY_GPIO (10) // GD32 PB6 SPI Ready 握手输入引脚(HIGH=就绪,LOW=忙) -#define SPI_READY_TIMEOUT_MS (100) // waitSpiReady 单次超时(毫秒) -#define SPI_OTA_FRAME_MAX (133) ///< SPI OTA 单帧最大字节数 +#define SPI_QUEUE_SIZE (4) ///< SPI 事务队列深度 +#define SPI_READY_GPIO (10) // 线与 SPIready 握手引脚(开漏,GPIO10<->GD32 PB6) +#define SPI_READY_TIMEOUT_MS (200) // 等待 SPIready 上升沿超时(毫秒) +#define SPI_OTA_FRAME_MAX (256) ///< SPI 同步帧最大字节数 +// ESP32 下拉 SPIready 后到拉 CS 之间的微秒延时(零延时起步,按需测算调大) +#define SPI_READY_DRIVE_DELAY_US (0) -// GD32 SPI OTA 帧类型 -#define SPI_OTA_SOF (0xA5) +// GD32 SPI 同步帧类型 +#define SPI_SYNC_SOF0 (0xA5) +#define SPI_SYNC_SOF1 (0x5A) #define SPI_OTA_CMD_REQ (0x01) #define SPI_OTA_CMD_DATA (0x02) -#define SPI_OTA_CMD_CANCEL (0x03) #define SPI_APP_ENTER_BOOT_CMD (0xB0) #define SPI_APP_ENTER_BOOT_ACK_OK (0xB5) // GD32 APP 固件须同步修改 ACK 字节为 0xB5 #define SPI_OTA_RSP_ACK (0x80) @@ -62,21 +72,6 @@ public: /** @brief 初始化 SPI2 总线与目标从设备。 */ esp_err_t init(); - // ============================================================ - // 底层 SPI 传输 - // ============================================================ - - /** @brief 执行一次同步 SPI 事务,先发送请求,再读取响应字节。 */ - esp_err_t syncTransfer(uint8_t cmd, const uint8_t *payload, - size_t payload_len, uint8_t *recv_buf, - size_t recv_len, uint32_t timeout_ms); - - /** @brief 仅发送 SPI 请求数据,不关心返回内容。 */ - esp_err_t send(uint8_t cmd, const uint8_t *payload, size_t len); - - /** @brief 仅接收 SPI 数据,必要时先发送命令字节。 */ - esp_err_t recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms); - // ============================================================ // SPI OTA 协议 // ============================================================ @@ -90,21 +85,50 @@ public: esp_err_t otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload, size_t resp_max, size_t *resp_len, uint32_t timeout_ms); + /** @brief 读取对端最新上报的 CREDIT(0 表示对端忙,应 POLL 等待而非重发)。 */ + uint8_t remoteCredit() const { return m_remoteCredit; } + /** @brief 通知 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。 */ esp_err_t appRequestBootloader(); + /** @brief 方案A: 通过 PB6 LOW→HIGH 序列等待 Bootloader 就绪。 + * Phase1 轮询等待 LOW (APP 复位前拉低 / Bootloader 极早拉低); + * Phase2 轮询等待 HIGH 并确认稳定 10ms (Bootloader init 完成释放)。 + * 超时返回 ESP_ERR_TIMEOUT,调用方应退化为固定延时。 */ + esp_err_t waitForBootloaderReady(uint32_t timeout_ms); + /** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */ esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version = nullptr); - /** @brief 执行带重试的 SPI 同步传输。 */ - esp_err_t transferWithRetry(uint8_t cmd, const uint8_t *payload, - size_t payload_len, uint8_t *recv_buf, - size_t recv_len, int max_retries); - // ── CANFD 接口已迁移至 transport/canfd_bridge.h(M4 解耦)── - /** @brief 获取 SPI 设备句柄(供外部状态查询)。 */ - spi_device_handle_t deviceHandle() const { return m_spiDevice; } + /** + * @brief 检查 GD32 是否有主动按铃请求,若有则发起读事务读取请求帧。 + * + * GD32 下拉 SPIready 表示「我有请求帧要给你读」。此函数检测到该状态后 + * 发起一次 SPI 读事务(发 dummy 字节,读回 GD32 准备的请求帧)。 + * 读取成功后按帧 TYPE 交给上层处理(如 CAN feedback 轮询、重启请求等)。 + * + * @param timeout_ms 等待 GD32 请求的超时(毫秒),0 表示不等待仅检查。 + * @return ESP_OK 成功读到请求帧;ESP_ERR_NOT_FOUND 当前无请求;其余为错误码。 + * + * @note 应在主循环或 OTA 空闲时周期调用,以响应 GD32 主动上报。 + */ + esp_err_t checkGd32Request(uint32_t timeout_ms = 0U); + + /** + * @brief 取出最近一次 GD32 按铃请求帧(线程安全,取出后清空内部缓冲)。 + * + * checkGd32Request() 成功读取一帧后将其 type/payload 存入内部缓冲, + * 上层调用本函数取出并消费。取出后缓冲清空,避免重复消费。 + * + * @param[out] type 请求帧 TYPE(如 0xC1)。 + * @param[out] payload 输出 payload 拷贝缓冲。 + * @param[in] max_len payload 缓冲最大长度。 + * @param[out] len 实际 payload 长度。 + * @return true 表示有数据被取出;false 表示当前无数据。 + */ + bool takeRingRx(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len); private: /** @@ -127,39 +151,63 @@ private: spi_device_handle_t m_spiDevice = NULL; SemaphoreHandle_t m_mutex = NULL; + // ── 线与 SPIready 握手状态机 ── + /** 握手状态机枚举。 */ + enum class HandshakeState : uint8_t { + IDLE = 0, ///< 空闲,线 HIGH + MASTER_SENDING, ///< ESP32 已下拉 SPIready,正在 CS#1 发送请求 + WAITING_RESPONSE, ///< CS#1 完成,等待 GD32 释放 SPIready(上升沿唤醒) + SERVING_GD32_RING, ///< 响应 GD32 按铃,正在读 GD32 请求帧 + WAITING_BOOT_READY, ///< 方案A: 等待 Bootloader 就绪(LOW→HIGH 序列) + }; + volatile HandshakeState m_hsState = HandshakeState::IDLE; + /** GD32 主动按铃标志:GPIO 下降沿 ISR 置位,SPI 任务消费。 */ + volatile bool m_gd32RequestPending = false; + /** SPI 任务句柄(用于 ISR 唤醒)。 */ + TaskHandle_t m_spiTaskHandle = NULL; + /** GD32 按铃请求帧缓冲区。 */ + uint8_t m_ringRxBuf[SPI_OTA_FRAME_MAX] = {}; + size_t m_ringRxLen = 0; + uint8_t m_ringRxType = 0; ///< GD32 按铃请求帧 TYPE(checkGd32Request 填充) + + // ── 帧内同步状态机 ── + volatile uint8_t m_localCredit = 8; ///< 本端剩余可接收帧数(信用窗口) + volatile uint8_t m_remoteCredit = 8; ///< 对端上报的剩余信用 + // RAII 锁守卫 class LockGuard { public: - /** - * @brief 构造加锁守卫,获取互斥锁。 - * @param mutex 互斥锁句柄。 - */ - explicit LockGuard(SemaphoreHandle_t mutex) : m_lock(mutex) { + explicit LockGuard(SemaphoreHandle_t lock) : m_lock(lock) { if (m_lock) { xSemaphoreTake(m_lock, portMAX_DELAY); } } - /** - * @brief 析构解锁守卫,释放互斥锁。 - */ ~LockGuard() { if (m_lock) { xSemaphoreGive(m_lock); } } - /** - * @brief 禁止复制构造。 - */ LockGuard(const LockGuard&) = delete; - /** - * @brief 禁止复制赋值。 - */ LockGuard& operator=(const LockGuard&) = delete; private: SemaphoreHandle_t m_lock; }; - /** @brief 等待 GD32 PB6 SPI Ready 握手引脚拉高(就绪),CS 断言前调用。 */ - esp_err_t waitSpiReady(uint32_t timeout_ms = SPI_READY_TIMEOUT_MS); - // waitSpiReady 超时日志限频:每 kSpiReadyWarnEvery 次超时打印一条,避免 GD32 掉线时刷屏 - static constexpr uint32_t kSpiReadyWarnEvery = 32U; ///< 超时日志打印间隔 - uint32_t m_spiReadyTimeoutCount = 0U; ///< 累计超时次数(用于限频) + /** @brief 下拉 SPIready(开漏驱动低)。 */ + void driveReadyLow(); + /** @brief 释放 SPIready(开漏高阻,靠上拉拉高)。 */ + void releaseReady(); + /** + * @brief 等 GD32 释放 SPIready(上升沿),中断驱动非忙等。 + * + * 调用前须将 m_hsState 置为 WAITING_RESPONSE,ISR 上升沿检测到该状态 + * 后发 0x02 通知唤醒本任务。GD32 写 Flash 期间保持 PB6 低,本函数阻塞, + * 写完拉高 → 唤醒 → 继续,自然避免 overrun。 + * + * @param timeout_ms 超时(毫秒),0 表示不等待。 + * @return ESP_OK 线已高;ESP_ERR_TIMEOUT 超时。 + */ + esp_err_t waitSpiReady(uint32_t timeout_ms); + + /** @brief GPIO 边沿中断回调(注册到 ESP-IDF GPIO ISR 服务)。 */ + static void IRAM_ATTR onReadyEdgeISR(void *arg); + // 内部 SPI 传输 /** * @brief 执行底层 SPI 时钟同步传输。 @@ -173,21 +221,6 @@ private: esp_err_t transferClocked(const uint8_t *tx_buf, size_t tx_len, uint8_t *rx_buf, size_t rx_len, uint32_t timeout_ms = 0U); - /** - * @brief 执行 SPI 请求-响应组合传输。 - * @param cmd 命令字节(0 表示无命令)。 - * @param payload 负载数据缓冲区。 - * @param payload_len 负载数据长度。 - * @param rx_buf 接收数据缓冲区。 - * @param rx_len 期望接收的数据长度。 - * @param timeout_ms 超时时间(毫秒),0 表示无限等待。 - * @return ESP_OK 表示成功,否则返回错误码。 - */ - esp_err_t transferRequestResponse(uint8_t cmd, const uint8_t *payload, size_t payload_len, - uint8_t *rx_buf, size_t rx_len, - uint32_t timeout_ms = 0U); - - // 无锁内部版本(供已持锁的公共方法调用,避免递归死锁) /** * @brief 无锁版本的 OTA 响应轮询(调用者须已持有锁)。 @@ -201,50 +234,12 @@ private: esp_err_t otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_payload, size_t resp_max, size_t *resp_len, uint32_t timeout_ms); // OTA 帧工具(静态,无状态) - /** - * @brief 计算 OTA 帧的 8 位 CRC 校验和。 - * @param data 数据缓冲区。 - * @param len 数据长度。 - * @return 计算得到的 CRC 值。 - */ - static uint8_t otaCrc8(const uint8_t *data, size_t len); - /** - * @brief 构建 OTA 协议帧。 - * @param type 帧类型。 - * @param payload 负载数据缓冲区。 - * @param payload_len 负载数据长度。 - * @param out 输出帧缓冲区。 - * @param out_len 输出实际帧长度。 - * @return true 表示构建成功,false 表示失败。 - */ + static uint32_t otaCrc32(const uint8_t *data, size_t len); static bool otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len, uint8_t *out, size_t *out_len); - /** - * @brief 解析 OTA 协议帧。 - * @param frame 帧数据缓冲区。 - * @param frame_len 帧数据长度。 - * @param type 输出解析得到的帧类型。 - * @param payload 输出指向负载数据的指针。 - * @param payload_len 输出负载数据长度。 - * @return true 表示解析成功,false 表示失败。 - */ - static bool otaParseFrame(const uint8_t *frame, size_t frame_len, - uint8_t *type, const uint8_t **payload, size_t *payload_len); - /** - * @brief 在数据缓冲区中搜索并解析 OTA 帧。 - * @param buf 数据缓冲区。 - * @param buf_len 数据缓冲区长度。 - * @param type 输出帧类型。 - * @param payload 输出指向负载数据的指针。 - * @param payload_len 输出负载数据长度。 - * @param frame_offset 输出帧在缓冲区中的偏移位置。 - * @return true 表示找到并成功解析,false 表示未找到。 - */ static bool otaFindFrame(const uint8_t *buf, size_t buf_len, uint8_t *type, const uint8_t **payload, size_t *payload_len, size_t *frame_offset); }; #endif // SPI_MASTER_H - - diff --git a/sdkconfig.defaults b/sdkconfig.defaults index be2aec2..7cd5f3e 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -1,6 +1,14 @@ # WiFi perf -- OTA acceleration CONFIG_ESP_WIFI_RX_BA_WIN=6 CONFIG_ESP_WIFI_TX_BA_WIN=6 +CONFIG_DCU_OTA_POLL_BACKOFF_MS=5 +CONFIG_DCU_OTA_FINISH_TIMEOUT_MS=10000 + +# DCU SPI clock -- default 4 MHz, can be raised via menuconfig to speed up GD32 OTA +CONFIG_DCU_SPI_CLOCK_HZ=4000000 + +# DCU OTA packet interval -- default 1 ms, can be lowered to speed up GD32 OTA +CONFIG_DCU_OTA_PACKET_INTERVAL_MS=1 # LWIP: HTTP server max_open_sockets=4 + WiFi/LWIP 内部 ~3 + UDP debug log + 余量 # 原值 7 与 httpd max_open_sockets=4 仅余 3 个 socket,一旦 handler 阻塞即 accept(EMFILE) diff --git a/serial_monitor.py b/serial_monitor.py index 358c234..a112755 100644 --- a/serial_monitor.py +++ b/serial_monitor.py @@ -1,24 +1,22 @@ -#!/usr/bin/env python3 -""" -serial_monitor.py - ESP32 串口日志采集工具 +import serial, time, sys -用法: python serial_monitor.py -依赖: pip install pyserial -功能: 监听 COM15 串口 (115200 波特率),将输出记录到 serial_log.txt, - 最多运行 5 分钟。 -""" -import serial, sys, time -ser = serial.Serial('COM15', 115200, timeout=1) # COM 口和波特率根据实际修改 -outfile = open('G:\\Robot_v2\\DCU\\ESP32_COMM\\serial_log.txt', 'w', encoding='utf-8') # 输出文件路径 -start = time.time() -while time.time() - start < 300: # 最多 5 分钟 - line = ser.readline() - if line: - try: - text = line.decode('utf-8', errors='replace').rstrip() - outfile.write(text + '\n') - outfile.flush() - except: - pass -outfile.close() -ser.close() +port = 'COM15' +baud = 115200 +outfile = r'G:\Robot_v2\DCU\ESP32_COMM\build\monitor_log.txt' + +print('Monitoring %s at %d baud...' % (port, baud), flush=True) + +try: + s = serial.Serial(port, baud, timeout=1) + with open(outfile, 'w') as f: + while True: + data = s.readline() + if data: + line = data.decode('utf-8', errors='replace').rstrip() + ts = time.strftime('%H:%M:%S') + msg = '[%s] %s' % (ts, line) + print(msg, flush=True) + f.write(msg + '\n') + f.flush() +except Exception as e: + print('Error: %s' % e, flush=True) \ No newline at end of file diff --git a/tools/app.js b/tools/app.js index 707556f..c924cb8 100644 --- a/tools/app.js +++ b/tools/app.js @@ -860,9 +860,12 @@ async function testConnection() { restorePolling(); } // 立即发一次请求检测 + // 手动检测使用独立的 AbortController,避免打断递归调度的 pollStatus + var testAbort = new AbortController(); + var testTimer = setTimeout(function() { try { testAbort.abort(); } catch(_e) {} }, 3000); try { var resp = await fetch(getBaseUrl() + '/system/status', { - signal: statusAbortCtrl.signal + signal: testAbort.signal }); if (resp.ok) { var data = await resp.json(); @@ -884,6 +887,8 @@ async function testConnection() { var diag = await diagnoseNetwork(); addLog('W', '诊断', diag); showToast('无法连接: ' + diag, 'error'); + } finally { + clearTimeout(testTimer); } } async function refreshSystemStatusNow(event) { @@ -891,10 +896,12 @@ async function refreshSystemStatusNow(event) { event.preventDefault(); event.stopPropagation(); } - // ★ ESP32 上传期间也允许日志轮询,让用户看到完整进展 + // 手动刷新使用独立的 AbortController,避免与递归调度的 pollStatus 互相干扰 + var manualAbort = new AbortController(); + var manualTimer = setTimeout(function() { try { manualAbort.abort(); } catch(_e) {} }, 3000); try { var resp = await fetch(getBaseUrl() + '/system/status', { - signal: statusAbortCtrl.signal + signal: manualAbort.signal }); if (!resp.ok) { addLog('E', '状态', '刷新失败: HTTP ' + resp.status); @@ -910,6 +917,8 @@ async function refreshSystemStatusNow(event) { } catch (e) { addLog('E', '状态', '刷新失败: ' + e.message); showToast('状态刷新失败', 'error'); + } finally { + clearTimeout(manualTimer); } } // ==================== CAN 帧发送 ==================== @@ -1757,7 +1766,9 @@ var lastDiagTime = 0; // 上次诊断时间 var diagResult = ''; // 最近一次诊断结果 var lastSysStateCode = -1; // 上次成功的 sys_state_code,用于失败时判断设备是否在忙 var lastRelayState = 0; // 上次成功的 relay_state -var statusAbortCtrl = null; // 取消上一次 in-flight pollStatus,防止堆积刷新 +var statusAbortCtrl = null; // 当前 /system/status 请求的 AbortController(3s 超时用) +var statusPollIntervalMs = 1000; // 状态轮询目标间隔 +var statusPollInFlight = false; // 是否正在请求 /system/status,防止重叠 // 从设备 IP 推导网关 IP(取前三段 + .1) function getGatewayIp() { var parts = getEspIp().split('.'); @@ -1798,64 +1809,88 @@ async function diagnoseNetwork() { diagResult = result.join(';'); return diagResult; } +/** @description 调度下一次 /system/status 轮询(递归 setTimeout)。 + * 上一次请求完成(或 3s 超时)后才调度下一次,避免 setInterval 把慢响应 + * 直接 abort 而导致堆/任务栈/CPU 等卡片刷新率不稳。 + */ +function scheduleStatusPoll(delayMs) { + if (statusTimerId) clearTimeout(statusTimerId); + statusTimerId = setTimeout(function() { + statusTimerId = null; + pollStatus(); + }, delayMs !== undefined ? delayMs : statusPollIntervalMs); +} + /** @description 启动定时轮询(状态 + 日志)。 */ + function startPolling() { consecutivePollFailures = 0; pollSlowMode = false; - if (statusTimerId) clearInterval(statusTimerId); + statusPollIntervalMs = 1000; + if (statusTimerId) clearTimeout(statusTimerId); if (logsTimerId) clearInterval(logsTimerId); - statusTimerId = setInterval(pollStatus, 1000); + scheduleStatusPoll(0); // 立即开始第一次,完成后按 interval 递归调度 logsTimerId = setInterval(pollLogs, 1000); } function slowDownPolling() { pollSlowMode = true; - if (statusTimerId) clearInterval(statusTimerId); + statusPollIntervalMs = 10000; + // 不中断正在进行的请求;它完成后会按新的 10s 间隔调度下一次 + if (!statusPollInFlight) { + if (statusTimerId) clearTimeout(statusTimerId); + scheduleStatusPoll(statusPollIntervalMs); + } if (logsTimerId) clearInterval(logsTimerId); - statusTimerId = setInterval(pollStatus, 10000); logsTimerId = setInterval(pollLogs, 10000); } function restorePolling() { if (pollSlowMode) { pollSlowMode = false; consecutivePollFailures = 0; - if (statusTimerId) clearInterval(statusTimerId); + statusPollIntervalMs = 1000; if (logsTimerId) clearInterval(logsTimerId); - statusTimerId = setInterval(pollStatus, 1000); logsTimerId = setInterval(pollLogs, 1000); } + // 如果当前没有请求在飞,立即触发一次,确保 UI 尽快恢复 + if (!statusPollInFlight) { + if (statusTimerId) clearTimeout(statusTimerId); + scheduleStatusPoll(0); + } } function pausePollingForUpload() { if (pollingPausedForUpload) return; pollingPausedForUpload = true; uploadPollChip = activeUploadChip || window.flashTarget || null; consecutivePollFailures = 0; - if (statusTimerId) clearInterval(statusTimerId); - if (logsTimerId) clearInterval(logsTimerId); + statusPollIntervalMs = uploadPollChip === "gd32" ? 2500 : 3000; // ★ ESP32/GD32 上传期间均保持轮询,让用户在网页上看到完整 OTA 进展 // ESP32 用 3s 间隔(上传为主),GD32 用 2.5s(中继进度依赖 status) - statusTimerId = setInterval(pollStatus, uploadPollChip === "gd32" ? 2500 : 3000); - logsTimerId = setInterval(pollLogs, uploadPollChip === "gd32" ? 2500 : 3000); + if (statusTimerId) clearTimeout(statusTimerId); + if (logsTimerId) clearInterval(logsTimerId); + scheduleStatusPoll(statusPollIntervalMs); + logsTimerId = setInterval(pollLogs, statusPollIntervalMs); } function resumePollingAfterUpload() { if (!pollingPausedForUpload) return; pollingPausedForUpload = false; uploadPollChip = null; consecutivePollFailures = 0; - if (pollSlowMode) { - statusTimerId = setInterval(pollStatus, 10000); - logsTimerId = setInterval(pollLogs, 10000); + statusPollIntervalMs = pollSlowMode ? 10000 : 1000; + if (logsTimerId) clearInterval(logsTimerId); + logsTimerId = setInterval(pollLogs, statusPollIntervalMs); + // 上传结束立即刷新一次状态;若当前已在请求中,则让它完成后按新间隔继续 + if (statusPollInFlight) { + if (statusTimerId) clearTimeout(statusTimerId); } else { - statusTimerId = setInterval(pollStatus, 1000); - logsTimerId = setInterval(pollLogs, 1000); + scheduleStatusPoll(0); } - pollStatus(); pollLogs(); } /** @description 轮询 ESP32 系统状态(GET /status)并更新 UI。 */ async function pollStatus() { - // ★ 上传期间状态轮询间隔已在 pausePollingForUpload 中缩短,此处不再拦截 - // 取消上一次 in-flight 请求,防止慢响应堆积导致 UI 瞬间刷新多次 - if (statusAbortCtrl) { try { statusAbortCtrl.abort(); } catch(_e) {} } + // 递归 setTimeout 调度,正常情况下不会重叠;做个保险避免并发请求 + if (statusPollInFlight) return; + statusPollInFlight = true; statusAbortCtrl = new AbortController(); var pollTimer = setTimeout(function() { try { statusAbortCtrl.abort(); } catch(_e) {} }, 3000); try { @@ -1942,6 +1977,9 @@ async function pollStatus() { updateConnectionUi(false, '未连接'); applyDisconnectedRuntimeState(); } + } finally { + statusPollInFlight = false; + scheduleStatusPoll(); } } startPolling(); @@ -1953,7 +1991,13 @@ document.addEventListener('visibilitychange', function () { pageVisible = !document.hidden; if (!pageVisible) return; consecutivePollFailures = 0; - pollStatus(); + // 切回前台立即刷新状态;若当前已在请求中,取消等待中的下一次让它尽快重排 + if (statusPollInFlight) { + if (statusTimerId) clearTimeout(statusTimerId); + } else { + if (statusTimerId) clearTimeout(statusTimerId); + scheduleStatusPoll(0); + } pollLogs(); // 之前若在慢速模式(10s),切回前台先用 1s 观察一会儿 if (pollSlowMode) restorePolling();