From 164855a36cbd3a1b58c5dbcc4f43ef090d6290dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=BE=99=E8=99=BE?= Date: Wed, 15 Jul 2026 00:35:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(spi):=20=E6=9B=B4=E6=96=B0SPI=E9=80=9A?= =?UTF-8?q?=E4=BF=A1=E5=8D=8F=E8=AE=AE=E4=B8=BA5-wire=20SPI=20Link=20v2?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E6=8F=A1=E6=89=8B=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加DATA_READY引脚(GPIO10)支持5-wire SPI拓扑 - 将otaTransfer调用替换为更通用的transact方法 - 更新SPI引脚定义和通信协议文档 - 修改README.md详细说明新的SPI Link v2协议 docs: 标记旧版OTA中继设计文档为已废弃 - 在esp32_ota_relay_design.md添加废弃警告 - 指向新的SPI Link v2协议文档作为权威来源 - 保留历史文档供参考但停止维护 refactor(can): 增强CANFD桥接错误处理机制 - 扩展canFdSend函数增加gd32_err_out参数 - 透传GD32细分错误码(0xE0-0xE5)到HTTP响应 - 更新HTTP处理器输出完整的错误信息 perf(monitor): 优化按铃事件等待机制 - 修改can_feedback_task.cpp中的事件等待逻辑 - 统一使用takeEvent方法替代checkGd32Request - 减少不必要的中间变量和函数调用 fix(status): 改进系统状态监控和CPU使用率统计 - 增加CPU任务快照缓冲区容量从16到32 - 添加CPU统计无效时的调试日志输出 - 更新SPI Link统计接口使用新的linkStatus方法 refactor(ota): 优化OTA中继传输和启动等待逻辑 - 调整APP启动等待重试策略(10次->20次, 500ms->300ms) - 移除数据包传输进度的日志输出减少冗余信息 - 统一使用transact方法进行SPI事务处理 bump(version): 更新固件版本到0.3.44 - 版本号从0.3.43提升至0.3.44 - 更新编译时间和Git哈希值 --- README.md | 3 +- docs/esp32_ota_relay_design.md | 8 + main/include/version.h | 8 +- main/monitor/can_feedback_task.cpp | 11 +- main/relay/ota_relay.cpp | 33 ++-- main/server/http_handlers.cpp | 51 ++++-- main/transport/canfd_bridge.cpp | 16 +- main/transport/canfd_bridge.h | 7 +- main/transport/spi_master.cpp | 280 ++++++----------------------- main/transport/spi_master.h | 76 +++----- tools/can.html | 2 +- tools/can.js | 50 +++++- 12 files changed, 209 insertions(+), 336 deletions(-) diff --git a/README.md b/README.md index 13c88ad..0edbf6c 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ ESP32-C3FH4 OTA 中继固件,作为 **GD32E508VET6** 的 OTA 升级桥梁。 |------|------| | **芯片** | ESP32-C3FH4 (RISC-V, 单核, 160MHz) | | **Flash** | 4MB | -| **SPI 引脚** | MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6, CS=GPIO4 | +| **SPI 引脚** | MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6, CS=GPIO4, DATA_READY=GPIO10(GD32 PB6 开漏输出,ESP32 仅输入) | +| **SPI 拓扑** | 5-wire SPI(4 线 SPI + 1 DATA_READY 通知线),SPI Link v2 协议(详见 `SPEC/spi_link_v2.md`) | | **SPI 速率** | 4MHz, Mode 0 | | **UART 调试** | TX=GPIO21, RX=GPIO20, 115200 baud | | **编译工具链** | ESP-IDF v6.0, riscv32-esp-elf | diff --git a/docs/esp32_ota_relay_design.md b/docs/esp32_ota_relay_design.md index d01475e..f1f6134 100644 --- a/docs/esp32_ota_relay_design.md +++ b/docs/esp32_ota_relay_design.md @@ -1,5 +1,13 @@ # ESP32-C3FH4 OTA 中继架构设计 +> **⚠️ v1 已废弃**(2026-07-14 标注) +> +> 本文档描述的是 v1 OTA 中继设计(旧帧格式 0xEB/6B/CRC16、旧引脚 GPIO6-9、旧命令 0xF0-0xF7), +> 与当前代码实现不符。当前 SPI 通信协议以 **`SPEC/spi_link_v2.md`** 为单一权威来源 +> (SPI Link v2:16B 头帧、SESSION/SEQ/CREDIT、5-wire SPI、HELLO 握手)。 +> +> 本文档保留作历史参考,不再维护。所有新代码与文档改动请参照 SPEC。 + ## 1. 系统概述 ``` diff --git a/main/include/version.h b/main/include/version.h index ccf3cd1..a2c8f4b 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 43 -#define FW_VERSION_STRING "0.3.43" +#define FW_VERSION_PATCH 44 +#define FW_VERSION_STRING "0.3.44" // 编译时间 -#define BUILD_TIME "2026-07-14 12:05:28" +#define BUILD_TIME "2026-07-15 00:28:19" // Git 信息 -#define GIT_HASH "7678234" +#define GIT_HASH "adb3386" diff --git a/main/monitor/can_feedback_task.cpp b/main/monitor/can_feedback_task.cpp index 6117730..58c3766 100644 --- a/main/monitor/can_feedback_task.cpp +++ b/main/monitor/can_feedback_task.cpp @@ -65,15 +65,10 @@ static void taskLoop(void *) continue; } - /* 阻塞等待 GD32 按铃并读取一帧(超时则重试) */ - esp_err_t ret = SpiMaster::instance().checkGd32Request(kWaitRingMs); - if (ret != ESP_OK) { - continue; - } - - /* 取出 checkGd32Request 存入的 type/payload */ + /* 阻塞等待 GD32 按铃事件(超时则重试) */ if (!SpiMaster::instance().takeEvent(&ring_type, ring_payload, - sizeof(ring_payload), &ring_len)) { + sizeof(ring_payload), &ring_len, + kWaitRingMs)) { continue; } diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index d93c0cc..944b610 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -856,9 +856,9 @@ ota_result_t OtaRelay::doHandshakeReq(uint32_t attempts) esp_err_to_name(hr), SpiMaster::instance().gd32Role()); } - /* otaTransfer 已含延迟一拍两步法;再经 pollUntil 处理 WAIT/陈旧 ACK。 */ + /* transact 已含延迟一拍两步法;再经 pollUntil 处理 WAIT/陈旧 ACK。 */ const uint32_t total_timeout_ms = kHandshakeReqTimeoutMs + kHandshakePollTimeoutMs; - esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, payload, sizeof(payload), + esp_err_t err = SpiMaster::instance().transact(SPI_OTA_CMD_REQ, payload, sizeof(payload), &resp_type, resp_payload, sizeof(resp_payload), &resp_len, total_timeout_ms); last_err = err; @@ -906,7 +906,7 @@ ota_result_t OtaRelay::doChallengeAndAuth() DEBUG_INFO("OTA", "--- 认证步骤 1: 发送 CHALLENGE ---"); DEBUG_INFO("OTA", "发送 nonce_server (16B), timestamp=0x%08" PRIX32, timestamp); - esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, m_nonceServer, 16, + esp_err_t err = SpiMaster::instance().transact(SPI_OTA_CMD_REQ, m_nonceServer, 16, &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 5000); logRespPayload("CHALLENGE", resp_type, resp_payload, resp_len); err = pollUntilExpectedAckShape("CHALLENGE", &resp_type, resp_payload, @@ -937,7 +937,7 @@ ota_result_t OtaRelay::doChallengeAndAuth() DEBUG_INFO("OTA", "--- 认证步骤 2: 发送 SESSION_KEY/CONFIRM ---"); resp_len = 0; - err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, encrypted_confirm, 16, + err = SpiMaster::instance().transact(SPI_OTA_CMD_REQ, encrypted_confirm, 16, &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); logRespPayload("CONFIRM", resp_type, resp_payload, resp_len); err = pollUntilExpectedAckShape("CONFIRM", &resp_type, resp_payload, @@ -976,7 +976,7 @@ ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size) size_payload[2] = (uint8_t)((size >> 16) & 0xFF); size_payload[3] = (uint8_t)((size >> 24) & 0xFF); - esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, size_payload, sizeof(size_payload), + esp_err_t err = SpiMaster::instance().transact(SPI_OTA_CMD_DATA, size_payload, sizeof(size_payload), &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); logRespPayload("SEND_SIZE", resp_type, resp_payload, resp_len); /* GD32 收到 SIZE 后擦 Flash,先 WAIT 再 ACK(seq=0);进度由前端读 status,不打过程 INFO */ @@ -1070,7 +1070,7 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], } else { resp_len = 0; resp_type = 0; - esp_err_t err = SpiMaster::instance().otaTransfer( + esp_err_t err = SpiMaster::instance().transact( SPI_OTA_CMD_DATA, tx_buf, 64, &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1200); logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len); @@ -1081,10 +1081,6 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) { SpiMaster::instance().confirmTxSeqAccepted(); m_transferFlashOffset += (uint32_t)chunk_size; - if (seq < 5U || (seq % 10U) == 0U || seq + 1U == m_transferTotalPackets) { - DEBUG_INFO("OTA", "转发进度 %" PRIu32 "/%" PRIu32, - seq + 1U, m_transferTotalPackets); - } return false; } } @@ -1110,10 +1106,6 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) { SpiMaster::instance().confirmTxSeqAccepted(); m_transferFlashOffset += (uint32_t)chunk_size; - if (seq < 5U || (seq % 10U) == 0U || seq + 1U == m_transferTotalPackets) { - DEBUG_INFO("OTA", "转发进度 %" PRIu32 "/%" PRIu32, - seq + 1U, m_transferTotalPackets); - } return false; } uint32_t err_seq = 0; @@ -1256,7 +1248,7 @@ ota_result_t OtaRelay::doSendFinishOta() resp_len = 0; memset(resp_payload, 0, sizeof(resp_payload)); - esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, finish_payload, sizeof(finish_payload), + esp_err_t err = SpiMaster::instance().transact(SPI_OTA_CMD_DATA, finish_payload, sizeof(finish_payload), &resp_type, resp_payload, sizeof(resp_payload), &resp_len, CONFIG_DCU_OTA_FINISH_TIMEOUT_MS); logRespPayload("FINISH_OTA", resp_type, resp_payload, resp_len); @@ -1317,15 +1309,18 @@ ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms) // 以 SESSION mismatch 拒绝(旧 session_id vs 新 APP session_id=0)。 bool session_established = false; - for (uint32_t attempt = 0; attempt < 10; ++attempt) { - vTaskDelay(pdMS_TO_TICKS(500)); + /* APP 完整初始化(含 EthercatSlave::init)需 5-6 秒,偶发 >10 秒。 + * 增加重试次数到 20、缩短延迟到 300ms,总窗口 ~26 秒, + * APP 启动后最快 300ms 内检测到(原 500ms)。 */ + for (uint32_t attempt = 0; attempt < 20; ++attempt) { + vTaskDelay(pdMS_TO_TICKS(300)); if (!session_established) { esp_err_t hr = SpiMaster::instance().hello(1500U, V2_ROLE_APP); if (hr == ESP_OK) { session_established = true; } else { - DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/10)", attempt + 1); + DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1); continue; } } @@ -1333,7 +1328,7 @@ ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms) if (probeAndCacheAppAlive(timeout_ms)) { break; } - DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/10)", attempt + 1); + DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1); } if (!getGd32AppAlive()) { diff --git a/main/server/http_handlers.cpp b/main/server/http_handlers.cpp index 3615743..34f1fc0 100644 --- a/main/server/http_handlers.cpp +++ b/main/server/http_handlers.cpp @@ -776,8 +776,9 @@ esp_err_t can_send_handler(httpd_req_t *req) cJSON_Delete(root); uint8_t resp_data[CAN_MAX_DLC] = {0}; + uint8_t gd32_err = 0U; int resp_len = canFdSend(ch, ext_id, tx_data, tx_dlc, - resp_data, sizeof(resp_data), 2000); + resp_data, sizeof(resp_data), 2000, &gd32_err); char resp[512]; if (resp_len >= 0) { @@ -790,7 +791,8 @@ esp_err_t can_send_handler(httpd_req_t *req) ch, (unsigned long)ext_id, tx_dlc, resp_len, resp_hex); } else { snprintf(resp, sizeof(resp), - "{\"status\": \"error\", \"code\": %d}", resp_len); + "{\"status\": \"error\", \"code\": %d, \"gd32_err\": %u}", + resp_len, (unsigned)gd32_err); } httpd_resp_set_type(req, "application/json"); @@ -878,7 +880,7 @@ esp_err_t spi_send_handler(httpd_req_t *req) static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT; /* 状态 JSON 用静态缓冲:OTA 后 largest free 常 <2KB,calloc(8KB) 会 500 "no memory"。 */ static constexpr size_t SYSTEM_STATUS_JSON_CAP = 6144; -static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 16; +static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 32; static char s_status_json[SYSTEM_STATUS_JSON_CAP]; static SemaphoreHandle_t s_status_json_mux = nullptr; static portMUX_TYPE s_status_json_init_mux = portMUX_INITIALIZER_UNLOCKED; @@ -974,6 +976,8 @@ static void sample_cpu_usage(CpuUsageSnapshot& result) UBaseType_t captured_count = uxTaskGetSystemState(tasks, task_count, &total_runtime); int64_t now_us = esp_timer_get_time(); if (captured_count == 0 || captured_count > task_count) { + DEBUG_WARN("STATUS", "CPU stats: uxTaskGetSystemState 返回 0 (actual_tasks=%lu, buf_slots=%lu)", + (unsigned long)uxTaskGetNumberOfTasks(), (unsigned long)task_count); free(tasks); return; } @@ -1038,6 +1042,15 @@ static void sample_cpu_usage(CpuUsageSnapshot& result) result.usage_pct = std::max(0.0, 100.0 - result.idle_pct); } + if (!result.valid) { + DEBUG_DBG("STATUS", "CPU stats: valid=false had_prev=%d total=%lu prev_total=%lu " + "can_compute=%d delta=%lu captured=%lu", + (int)had_previous, + (unsigned long)total_runtime, (unsigned long)previous_total, + (int)can_compute, (unsigned long)total_delta, + (unsigned long)captured_count); + } + free(previous_samples); free(tasks); #else @@ -1236,34 +1249,36 @@ esp_err_t system_status_handler(httpd_req_t *req) len = append_cpu_status_json(resp, SYSTEM_STATUS_JSON_CAP, len, *cpu); - /* SPI Link v2 统计(SPEC §12.1)— linkStats 用 5ms 有界超时取 SPI 锁, + /* SPI Link v2 统计(SPEC §12.1)— linkStatus 内部用 5ms 有界超时取 SPI 锁, * OTA 中继期间拿不到锁时返回零值统计,绝不阻塞 /system/status 响应。 */ { - SpiMaster::SpiLinkStats ls = SpiMaster::instance().linkStats(); + SpiMaster::SpiLinkStatus ls = SpiMaster::instance().linkStatus(); const char *role_str = "unknown"; - switch (SpiMaster::instance().gd32Role()) { + switch (ls.gd32_role) { case 1: role_str = "app"; break; case 2: role_str = "boot"; break; default: break; } len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len, - ",\"spi_link\":{\"version\":2,\"gd32_role\":\"%s\",\"session_id\":\"0x%08lX\"," - "\"v2_active\":%s,\"remote_credit\":%u," + ",\"spi_link\":{\"version\":%u,\"gd32_role\":\"%s\",\"session_id\":\"0x%08lX\"," + "\"v2_active\":%s,\"local_credit\":%u,\"remote_credit\":%u," "\"crc_errors\":%lu,\"duplicate_requests\":%lu,\"seq_errors\":%lu," "\"retries\":%lu,\"ready_timeouts\":%lu,\"rx_overruns\":%lu," "\"resyncs\":%lu,\"max_resp_latency_us\":%lu}", + (unsigned)ls.version, role_str, - (unsigned long)SpiMaster::instance().sessionId(), + (unsigned long)ls.session_id, SpiMaster::instance().v2Active() ? "true" : "false", - (unsigned)SpiMaster::instance().remoteCredit(), - (unsigned long)ls.crc_errors, - (unsigned long)ls.duplicate_requests, - (unsigned long)ls.seq_errors, - (unsigned long)ls.retries, - (unsigned long)ls.ready_timeouts, - (unsigned long)ls.rx_overruns, - (unsigned long)ls.resyncs, - (unsigned long)ls.max_resp_latency_us); + (unsigned)ls.local_credit, + (unsigned)ls.remote_credit, + (unsigned long)ls.stats.crc_errors, + (unsigned long)ls.stats.duplicate_requests, + (unsigned long)ls.stats.seq_errors, + (unsigned long)ls.stats.retries, + (unsigned long)ls.stats.ready_timeouts, + (unsigned long)ls.stats.rx_overruns, + (unsigned long)ls.stats.resyncs, + (unsigned long)ls.stats.max_resp_latency_us); } // 前导逗号与前一块分隔;之前因 append_cpu_status_json 尾随逗号叠加 diff --git a/main/transport/canfd_bridge.cpp b/main/transport/canfd_bridge.cpp index 9e0d84b..b096ca7 100644 --- a/main/transport/canfd_bridge.cpp +++ b/main/transport/canfd_bridge.cpp @@ -13,7 +13,8 @@ constexpr uint8_t kSpiCanFrameType = 0xC0U; static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id, const uint8_t *data, uint8_t dlc, uint8_t *recv_buf, uint8_t recv_max, - uint32_t timeout_ms) + uint32_t timeout_ms, + uint8_t *gd32_err_out) { uint8_t tx_buf[72]; size_t tx_len = 0; @@ -30,7 +31,7 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id, uint8_t resp_type = 0U; size_t resp_len = 0U; - /* 两步法:otaTransfer 发命令(丢弃延迟一拍的过期响应)+ otaPollResponse 读真实响应。 + /* 两步法:transact 发命令(丢弃延迟一拍的过期响应)+ otaPollResponse 读真实响应。 * CAN 帧非幂等,命令只发一次,不重发。 */ esp_err_t ret = SpiMaster::instance().transact( kSpiCanFrameType, @@ -40,17 +41,21 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id, if (ret != ESP_OK) { DEBUG_WARN("CAN", "CAN_SEND SPI失败: ch=%u id=0x%08lX err=%s", (unsigned)channel, (unsigned long)ext_id, esp_err_to_name(ret)); + if (gd32_err_out) *gd32_err_out = 0U; /* SPI 失败时清零 */ return -2; /* SPI 通信失败 */ } if (resp_type == SPI_OTA_RSP_ACK) { if (recv_buf == nullptr || recv_max == 0U) { + if (gd32_err_out) *gd32_err_out = 0U; return static_cast(resp_len); } const size_t copied_len = (resp_len < recv_max) ? resp_len : recv_max; + if (gd32_err_out) *gd32_err_out = 0U; return static_cast(copied_len); } /* resp_type == ERROR: 读取 GD32 返回的错误码 */ uint8_t gd32_err = (resp_len > 0 && recv_buf) ? recv_buf[0] : 0xFF; + if (gd32_err_out) *gd32_err_out = gd32_err; /* 透传细分错误码 */ DEBUG_WARN("CAN", "CAN_SEND GD32拒绝: ch=%u id=0x%08lX resp=0x%02X err=0x%02X", (unsigned)channel, (unsigned long)ext_id, resp_type, gd32_err); return -3; /* GD32 返回 ERROR */ @@ -73,16 +78,19 @@ static inline uint32_t readU32Le(const uint8_t *p) { * @brief 发送一帧 CANFD 数据,可选等待并读取响应。 * 通过 SPI 协议帧发送给 GD32 APP,并读取 ACK/ERROR 响应。 * 返回值: >=0 成功(接收数据长度), -1 参数错误, -2 SPI通信失败, -3 GD32拒绝。 + * gd32_err_out 在返回 -3 时输出 GD32 细分错误码,其它情况清零。 */ int canFdSend(can_channel_t channel, uint32_t ext_id, const uint8_t *data, uint8_t dlc, uint8_t *recv_buf, uint8_t recv_max, - uint32_t timeout_ms) + uint32_t timeout_ms, + uint8_t *gd32_err_out) { if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) { + if (gd32_err_out) *gd32_err_out = 0U; return -1; } - return sendCanFrameOverSpi(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms); + return sendCanFrameOverSpi(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms, gd32_err_out); } /** diff --git a/main/transport/canfd_bridge.h b/main/transport/canfd_bridge.h index bbda318..6264d96 100644 --- a/main/transport/canfd_bridge.h +++ b/main/transport/canfd_bridge.h @@ -43,12 +43,15 @@ typedef struct { * @param recv_buf 接收数据缓冲区(可为 nullptr)。 * @param recv_max 接收缓冲区最大长度。 * @param timeout_ms 超时时间(毫秒)。 - * @return 成功时返回接收数据长度,失败返回 -1。 + * @param gd32_err_out 可选输出,GD32 返回的细分错误码(0xE0/0xE1/0xE2/0xE3/0xE4/0xE5); + * 仅在返回 -3 时有效,其它情况清零。可为 nullptr。 + * @return 成功时返回接收数据长度,失败返回 -1/-2/-3。 */ int canFdSend(can_channel_t channel, uint32_t ext_id, const uint8_t *data, uint8_t dlc, uint8_t *recv_buf, uint8_t recv_max, - uint32_t timeout_ms); + uint32_t timeout_ms, + uint8_t *gd32_err_out = nullptr); // ============================================================ // 不重复 ID 快照表(SPI 0xC3 协议配套) diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index 499fa45..50d684b 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -18,7 +18,6 @@ #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_random.h" // esp_random() for HELLO session_id #include "esp_intr_alloc.h" // ESP_INTR_FLAG_IRAM @@ -183,15 +182,9 @@ esp_err_t SpiMaster::serviceRingOnce() return ESP_ERR_NOT_FOUND; } - driveReadyLow(); - if (SPI_READY_DRIVE_DELAY_US > 0U) { - esp_rom_delay_us(SPI_READY_DRIVE_DELAY_US); - } - uint8_t rx_raw[SPI_OTA_FRAME_MAX]; size_t rx_len = 0U; esp_err_t ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 500U); - releaseReady(); m_linkState.exitBusy(); if (ret != ESP_OK) { @@ -341,30 +334,12 @@ void SpiMaster::workerLoop() } } -/** - * @brief 下拉 SPIready(v2 DATA_READY 模式下 ESP32 不再操作 PB6)。 - * 保留函数体兼容调用方,实际为空操作。 - */ -void SpiMaster::driveReadyLow() -{ - /* v2 DATA_READY: PB6 仅 GD32 下拉,ESP32 GPIO10 只输入。 */ -} - -/** - * @brief 释放 SPIready(v2 DATA_READY 模式下空操作)。 - */ -void SpiMaster::releaseReady() -{ - /* v2 DATA_READY: ESP32 不操作 PB6。 */ -} - /** * @brief GPIO 边沿中断回调(SPIready 线跳变)。 * * - 下降沿:GD32 主动按铃(请求 ESP32 来读)。若当前空闲则置 * m_gd32RequestPending 并唤醒 SPI 任务。 - * - 上升沿:Bootloader init 完成(释放 SPIready),唤醒等待 - * WAITING_BOOT_READY 的任务。 + * - 上升沿:GD32 释放 SPIready(WAITING_RESPONSE 态),唤醒等待任务。 * * ISR 内仅做标志翻转 + 任务通知,不做 SPI 传输。 */ @@ -386,14 +361,6 @@ void IRAM_ATTR SpiMaster::onReadyEdgeISR(void *arg) eSetBits, &hp); portYIELD_FROM_ISR(hp); } - } else if (self->m_linkState.state() == 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 { /* 上升沿 */ @@ -405,14 +372,6 @@ void IRAM_ATTR SpiMaster::onReadyEdgeISR(void *arg) eSetBits, &hp); portYIELD_FROM_ISR(hp); } - } else if (self->m_linkState.state() == 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); - } } } } @@ -640,26 +599,9 @@ esp_err_t SpiMaster::readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_ // ============================================================ // OTA 帧工具 // ============================================================ -/** - * @brief 计算 CRC32(MPEG-2,与 GD32 三端一致)。 - * 委托给 v2_crc32,算法完全相同。 - */ -uint32_t SpiMaster::otaCrc32(const uint8_t *data, size_t len) -{ - return v2_crc32(data, len); -} - -/* v1 同步帧头已升级为 v2 16B 头(见 spi_link_v2.h)。 - * 旧 6B 头常量保留为兼容注释,不再用于构建/解析。 */ -static constexpr size_t kSyncHeaderLen = V2_HEADER_LEN; /* 16 */ -static constexpr size_t kSyncCrcLen = V2_CRC_LEN; /* 4 */ -static constexpr size_t kSyncOverhead = V2_OVERHEAD; /* 20 */ - -/* v1 LE 工具已迁移至 spi_link_v2.h 的 v2_put/v2_get 内联函数。 */ /* - * v2 shim:旧 otaBuildFrame 接口委托给 v2_build_frame。 - * 使用当前 session/seq 成员填充 v2 帧头,FLAGS=REQUEST。 + * otaBuildFrame:使用当前 session/seq 成员填充 v2 帧头,FLAGS=REQUEST。 * 记录 m_lastSentSeq 供响应归属匹配。 */ bool SpiMaster::otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len, @@ -681,7 +623,7 @@ bool SpiMaster::otaBuildFrame(uint8_t type, const uint8_t *payload, size_t paylo } /* - * v2 shim:旧 otaFindFrame 接口委托给 v2_parse_frame + SESSION/SEQ 校验。 + * otaFindFrame:委托 v2_parse_frame + SESSION/SEQ 校验。 * ESP32 是主机,收到 GD32 的 RESPONSE/EVENT 帧: * - ack_seq 携带"GD32 确认收到的 ESP32 请求序号",用于响应归属匹配 * - tx_seq 是 GD32 的发送序号 @@ -734,9 +676,7 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len, * 策略:CRC+session 合法的 RESPONSE 一律交给调用方;用 m_lastRxAckMatched * 标记是否归属当前请求。业务层以结构化 payload(BA C0+seq)做最终判定。 */ if (hdr.flags & V2_FLAG_RESPONSE) { - const bool ack_match = (hdr.ack_seq == self.m_lastSentSeq); - const bool legacy_zero_ack = (hdr.ack_seq == 0U); - self.m_lastRxAckMatched = (ack_match || legacy_zero_ack); + self.m_lastRxAckMatched = (hdr.ack_seq == self.m_lastSentSeq); if (!self.m_lastRxAckMatched) { static uint32_t s_stale_logs = 0U; if ((++s_stale_logs % 32U) == 1U) { @@ -745,10 +685,6 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len, (unsigned long)s_stale_logs); } } else { - if (!ack_match && legacy_zero_ack) { - DEBUG_DBG("SPI", "v2 legacy ack_seq=0 accepted: type=0x%02X expected=%u", - hdr.type, self.m_lastSentSeq); - } self.m_rxAckedSeq = self.m_lastSentSeq; } } else { @@ -843,9 +779,6 @@ esp_err_t SpiMaster::init() 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) { @@ -914,27 +847,6 @@ esp_err_t SpiMaster::init() // ============================================================ // SPI OTA 协议 // ============================================================ -/** - * @brief 按 GD32 SPI OTA 最小协议执行一次请求加响应读取。 - * @param type 请求帧类型。 - * @param payload 负载数据缓冲区。 - * @param payload_len 负载数据长度。 - * @param resp_type 输出响应类型。 - * @param resp_payload 输出响应负载缓冲区。 - * @param resp_max 响应负载缓冲区最大长度。 - * @param resp_len 输出实际响应长度。 - * @param timeout_ms 超时时间(毫秒)。 - * @return ESP_OK 表示成功,否则返回错误码。 - */ -esp_err_t SpiMaster::otaTransfer(uint8_t type, const uint8_t *payload, size_t payload_len, - uint8_t *resp_type, uint8_t *resp_payload, - size_t resp_max, size_t *resp_len, uint32_t timeout_ms) -{ - /* 公开 otaTransfer = transact(L4 worker 串行)。 */ - return transact(type, payload, payload_len, - resp_type, resp_payload, resp_max, resp_len, timeout_ms); -} - /** * @brief 无锁版本的 otaTransfer(调用者须已持有锁)。 */ @@ -966,11 +878,9 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s if (waitSpiDataReady(20U) == ESP_OK) { uint8_t drain[SPI_OTA_FRAME_MAX]; size_t drain_len = 0U; - driveReadyLow(); if (readV2FrameVarlen(drain, sizeof(drain), &drain_len, 100U) == ESP_OK) { (void)otaFindFrame(drain, drain_len, nullptr, nullptr, nullptr, nullptr); } - releaseReady(); } else { vTaskDelay(pdMS_TO_TICKS(5)); } @@ -992,16 +902,10 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s * ~4.2s 纯浪费,最终拖垮转发预算导致卡死。CS 本身即互斥,无需额外就绪等待。 */ m_linkState.enterBusy(HandshakeState::MASTER_SENDING); - driveReadyLow(); - if (SPI_READY_DRIVE_DELAY_US > 0U) { - esp_rom_delay_us(SPI_READY_DRIVE_DELAY_US); - } - const size_t xfer_len = spiXferBytes(tx_len); uint8_t tx_padded[SPI_OTA_FRAME_MAX]; if (tx_len > sizeof(tx_padded) || xfer_len > sizeof(tx_padded)) { DEBUG_ERR("SPI", "OTA frame %u exceeds tx_padded %u", (unsigned)tx_len, (unsigned)sizeof(tx_padded)); - releaseReady(); m_linkState.exitBusy(); return ESP_ERR_INVALID_SIZE; } @@ -1012,7 +916,6 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s memset(rx_raw, 0xFF, xfer_len); esp_err_t ret = transferClocked(tx_padded, xfer_len, rx_raw, xfer_len, timeout_ms); - releaseReady(); m_linkState.exitBusy(); if (ret != ESP_OK) { DEBUG_WARN("SPI", "OTA sync transfer failed: %s", esp_err_to_name(ret)); @@ -1130,9 +1033,7 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p uint8_t rx_raw[SPI_OTA_FRAME_MAX]; size_t rx_len = 0U; - driveReadyLow(); esp_err_t ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, spi_timeout); - releaseReady(); did_spi_read = true; if (ret != ESP_OK) { if (ret == ESP_ERR_INVALID_RESPONSE) { @@ -1219,7 +1120,7 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p /** * @brief ESP32→GD32 发送命令 + 轮询读取真实响应(两步法,原子持锁)。 * - * 解决 GD32 "延迟一拍"协议:otaTransfer 首笔 CS 读到过期响应, + * 解决 GD32 "延迟一拍"协议:transact 首笔 CS 读到过期响应, * otaPollResponse 读取本笔命令的真实响应。命令只发送一次,不重发。 * 适用于所有 ESP32→GD32 方向的命令(CAN 发送/模式切换/ID 快照等)。 */ @@ -1249,33 +1150,49 @@ esp_err_t SpiMaster::transact(uint8_t type, const uint8_t *payload, size_t paylo resp_type, resp_payload, resp_max, resp_len, timeout_ms); } -bool SpiMaster::takeEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len) +bool SpiMaster::takeEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len, + uint32_t timeout_ms) { - LockGuard lock(m_mutex); - if (popEvent(type, payload, max_len, len)) { - /* 与 pushEvent 的 counting sem 对齐 */ - if (m_eventSem) { - (void)xSemaphoreTake(m_eventSem, 0); + /* 快速路径:先非阻塞尝试一次 */ + { + LockGuard lock(m_mutex); + if (popEvent(type, payload, max_len, len)) { + if (m_eventSem) { + (void)xSemaphoreTake(m_eventSem, 0); + } + return true; + } + if (m_ringRxLen != 0U) { + /* 兼容单槽回落 */ + 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; } - return true; } - /* 兼容:事件队列空时回落单槽 */ - if (m_ringRxLen == 0U) { + + /* 队列空:若 timeout_ms==0 直接返回,否则在信号量上等待 */ + if (timeout_ms == 0U || m_eventSem == nullptr) { return false; } - if (type) { - *type = m_ringRxType; + + /* 循环等待:信号量到达后重新持锁取,取空则继续等(防虚假唤醒) */ + const TickType_t deadline_ticks = pdMS_TO_TICKS(timeout_ms); + while (true) { + if (xSemaphoreTake(m_eventSem, deadline_ticks) != pdTRUE) { + return false; /* 超时 */ + } + LockGuard lock(m_mutex); + if (popEvent(type, payload, max_len, len)) { + return true; + } + /* 信号量被消费但队列空(可能被并发 takeEvent 抢走),继续等 */ } - 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; } esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t payload_len, @@ -1386,14 +1303,12 @@ esp_err_t SpiMaster::transactWithRetry(uint8_t type, const uint8_t *payload, siz m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE); (void)waitSpiReady(SPI_READY_TIMEOUT_MS); m_linkState.enterBusy(HandshakeState::MASTER_SENDING); - driveReadyLow(); uint8_t tx_p[SPI_OTA_FRAME_MAX]; uint8_t rx_r[SPI_OTA_FRAME_MAX]; memset(tx_p, 0xFF, xfer_len); memcpy(tx_p, hello_frame, hello_len); memset(rx_r, 0xFF, xfer_len); esp_err_t hr = transferClocked(tx_p, xfer_len, rx_r, xfer_len, 2000U); - releaseReady(); m_linkState.exitBusy(); if (hr != ESP_OK) { continue; @@ -1450,9 +1365,7 @@ esp_err_t SpiMaster::appRequestBootloader() uint8_t rx_raw[SPI_OTA_FRAME_MAX]; size_t rx_len = 0U; m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE); - driveReadyLow(); (void)readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 50U); - releaseReady(); m_linkState.exitBusy(); vTaskDelay((pdMS_TO_TICKS(5) > 0) ? pdMS_TO_TICKS(5) : 1); } @@ -1487,9 +1400,7 @@ esp_err_t SpiMaster::appRequestBootloader() memcpy(tx_p, hf, hl); memset(rx_r, 0xFF, xlen); m_linkState.enterBusy(HandshakeState::MASTER_SENDING); - driveReadyLow(); (void)transferClocked(tx_p, xlen, rx_r, xlen, 200U); - releaseReady(); m_linkState.exitBusy(); v2_header_t h; const uint8_t *pl = nullptr; @@ -1530,9 +1441,7 @@ esp_err_t SpiMaster::appRequestBootloader() uint8_t rx_raw[SPI_OTA_FRAME_MAX]; size_t rx_len = 0U; - driveReadyLow(); ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 80U); - releaseReady(); m_linkState.exitBusy(); if (ret != ESP_OK) { vTaskDelay(gap); @@ -1691,7 +1600,9 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver uint8_t resp_payload[32]; size_t resp_len = 0U; - const uint32_t send_timeout = (timeout_ms > 500U) ? 500U : timeout_ms; + /* PING 无 payload,send 只发 1 帧 SPI(<50ms),不需要 500ms 预留。 + * 省下的时间给 poll,让 APP 命令层有更长窗口响应。 */ + const uint32_t send_timeout = (timeout_ms > 100U) ? 100U : timeout_ms; esp_err_t send_ret = otaTransferUnlocked(kAppPingType, nullptr, 0U, nullptr, nullptr, 0U, nullptr, send_timeout, /*expect_stale=*/true); @@ -1745,92 +1656,6 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver 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::checkGd32Request(uint32_t timeout_ms) -{ - /* L4:不改 m_spiTaskHandle(必须始终是 worker)。 - * 已有事件则直接 OK;否则提交 ServiceRing 或等待 eventSem。 */ - if (m_suppressRingService) { - return ESP_ERR_NOT_FOUND; - } - { - LockGuard lock(m_mutex); - if (m_eventCount > 0U || m_ringRxLen > 0U) { - return ESP_OK; - } - } - - if (onWorker()) { - return serviceRingOnce(); - } - - const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS( - timeout_ms > 0U ? timeout_ms : 1U); - for (;;) { - { - LockGuard lock(m_mutex); - if (m_eventCount > 0U || m_ringRxLen > 0U) { - return ESP_OK; - } - } - - if (timeout_ms == 0U) { - /* 单次尝试:让 worker 立刻 service 一次 */ - Job job{}; - job.kind = JobKind::ServiceRing; - job.timeout_ms = 50U; - esp_err_t r = submitJob(job); - if (r == ESP_OK) { - return ESP_OK; - } - return ESP_ERR_NOT_FOUND; - } - - if (xTaskGetTickCount() >= deadline) { - return ESP_ERR_NOT_FOUND; - } - - /* 唤醒 worker 读按铃;再等事件信号 */ - if (m_workerTask) { - xTaskNotify(m_workerTask, 0x01U, eSetBits); - } - Job job{}; - job.kind = JobKind::ServiceRing; - job.timeout_ms = 100U; - if (submitJob(job) == ESP_OK) { - return ESP_OK; - } - - if (m_eventSem) { - TickType_t now = xTaskGetTickCount(); - TickType_t left = (deadline > now) ? (deadline - now) : 0; - if (left == 0) { - return ESP_ERR_NOT_FOUND; - } - if (xSemaphoreTake(m_eventSem, left) == pdTRUE) { - /* 信号量被 take 后计数减一,但事件仍在队列里;补回计数给 takeEvent */ - (void)xSemaphoreGive(m_eventSem); - return ESP_OK; - } - } else { - vTaskDelay(pdMS_TO_TICKS(10)); - } - } -} - -bool SpiMaster::takeRingRx(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len) -{ - return takeEvent(type, payload, max_len, len); -} - // ============================================================ // SPI Link v2 链路状态 // ============================================================ @@ -1848,6 +1673,19 @@ SpiMaster::SpiLinkStats SpiMaster::linkStats() return ret; } +SpiMaster::SpiLinkStatus SpiMaster::linkStatus() +{ + /* 状态字段为 volatile 单字节/双字读,原子性足够;统计复用 linkStats() 的有界取锁。 */ + SpiLinkStatus st{}; + st.version = 2U; + st.gd32_role = m_gd32Role; + st.session_id = m_sessionId; + st.local_credit = m_localCredit; + st.remote_credit = m_remoteCredit; + st.stats = linkStats(); + return st; +} + esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint) { if (!onWorker() && m_workerTask != nullptr) { @@ -1898,7 +1736,6 @@ esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint) m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE); (void)waitSpiReady(SPI_READY_TIMEOUT_MS); m_linkState.enterBusy(HandshakeState::MASTER_SENDING); - driveReadyLow(); uint8_t tx_padded[SPI_OTA_FRAME_MAX]; memset(tx_padded, 0xFF, xfer_len); @@ -1906,7 +1743,6 @@ esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint) memset(rx_raw, 0xFF, xfer_len); esp_err_t ret = transferClocked(tx_padded, xfer_len, rx_raw, xfer_len, 200U); - releaseReady(); m_linkState.exitBusy(); if (ret != ESP_OK) { DEBUG_WARN("SPI", "HELLO transfer failed[%lu]: %s", diff --git a/main/transport/spi_master.h b/main/transport/spi_master.h index cf69299..ddb6e81 100644 --- a/main/transport/spi_master.h +++ b/main/transport/spi_master.h @@ -31,12 +31,8 @@ #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 同步帧类型 -#define SPI_SYNC_SOF0 (0xA5) -#define SPI_SYNC_SOF1 (0x5A) #define SPI_OTA_CMD_REQ (0x01) #define SPI_OTA_CMD_DATA (0x02) #define SPI_APP_ENTER_BOOT_CMD (0xB0) @@ -79,19 +75,13 @@ public: // SPI OTA 协议 // ============================================================ - /** @brief 按 GD32 SPI OTA 协议执行一次请求 + 响应读取(延迟一拍两步法)。 - * 内部等价于 otaSendAndPoll:首笔 CS 发请求(忽略旧响应),次笔 CS 收真实响应。 */ - esp_err_t otaTransfer(uint8_t type, const uint8_t *payload, size_t payload_len, - uint8_t *resp_type, uint8_t *resp_payload, - size_t resp_max, size_t *resp_len, uint32_t timeout_ms); - /** @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。 */ 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 ESP32→GD32 发送命令 + 轮询读取真实响应(两步法,原子持锁)。 * - * 解决 GD32 "延迟一拍"协议:otaTransfer 首笔 CS 读到过期响应, + * 解决 GD32 "延迟一拍"协议:transact 首笔 CS 读到过期响应, * otaPollResponse 读取本笔命令的真实响应。命令只发送一次,不重发。 * 适用于所有 ESP32→GD32 方向的命令(CAN 发送/模式切换/ID 快照等)。 * @@ -112,6 +102,9 @@ public: /** @brief 读取对端最新上报的 CREDIT(0 表示对端忙,应 POLL 等待而非重发)。 */ uint8_t remoteCredit() const { return m_remoteCredit; } + /** @brief 读取本端剩余可接收 credit(SPEC §10.1,/system/status 展示用)。 */ + uint8_t localCredit() const { return m_localCredit; } + /** * @brief OTA 中继期间禁止 worker 按铃读。 * @@ -136,8 +129,11 @@ public: /** * @brief L4 取出 GD32 EVENT 帧(worker 已读入事件队列)。 + * @param timeout_ms 等待超时(ms)。0 = 非阻塞(原语义)。 + * @return true 取到事件;false 队列空且超时未到达。 */ - bool takeEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len); + bool takeEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len, + uint32_t timeout_ms = 0U); // ============================================================ // SPI Link v2 链路层(阶段1:帧格式 + 状态成员就位) @@ -155,6 +151,16 @@ public: uint32_t max_resp_latency_us; ///< 最大响应延迟(微秒) }; + /** @brief v2 完整链路状态(SPEC §10.1,会话/credit + 统计)。 */ + struct SpiLinkStatus { + uint8_t version; ///< 协议版本(固定 2) + uint8_t gd32_role; ///< 0=unknown, 1=app, 2=boot + uint32_t session_id; ///< 当前会话 ID + uint8_t local_credit; ///< 本端剩余 credit + uint8_t remote_credit; ///< 对端剩余 credit + SpiLinkStats stats; ///< 8 个统计计数器 + }; + /** @brief 当前会话 ID(0 = 未建立 session)。 */ uint32_t sessionId() const { return m_sessionId; } @@ -167,8 +173,8 @@ public: /** @brief 获取链路统计快照(线程安全,持锁读取)。 */ SpiLinkStats linkStats(); - /** @brief L4 链路状态(linkStats 别名)。 */ - SpiLinkStats linkStatus() { return linkStats(); } + /** @brief 获取完整链路状态(SPEC §10.1,会话/credit + 统计)。 */ + SpiLinkStatus linkStatus(); /** @brief 发起 HELLO 握手建立 v2 session(SPEC §5.1)。 * 生成随机非零 SESSION_ID,发送 LINK_HELLO,等待 HELLO_ACK。 @@ -192,34 +198,6 @@ public: // ── CANFD 接口已迁移至 transport/canfd_bridge.h(M4 解耦)── - /** - * @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: /** * @brief 私有默认构造函数。 @@ -248,7 +226,6 @@ private: MASTER_SENDING, ///< ESP32 已下拉 SPIready,正在 CS#1 发送请求 WAITING_RESPONSE, ///< CS#1 完成,等待 GD32 释放 SPIready(上升沿唤醒) SERVING_GD32_RING, ///< 响应 GD32 按铃,正在读 GD32 请求帧 - WAITING_BOOT_READY, ///< 方案A: 等待 Bootloader 就绪(LOW→HIGH 序列) }; /** @@ -258,15 +235,15 @@ private: * - 线 HIGH = 空闲(双方释放),可发起新事务 * - 线 LOW = 忙碌(某方下拉占用),禁止发起新事务 * - * ESP32 作为主动方(otaTransfer/appPing):enterBusy() 占用线 → transferClocked → exitBusy() 释放线 - * ESP32 作为按铃响应方(checkGd32Request):同上,releaseReady 上升沿通知 GD32 完成 + * ESP32 作为主动方(transact/appPing):enterBusy() 占用线 → transferClocked → exitBusy() 释放线 + * ESP32 作为按铃响应方(takeEvent):同上 */ class SpiLinkState { public: - /** 进入忙碌态(driveReadyLow 前/后调用)。 */ + /** 进入忙碌态。 */ void enterBusy(HandshakeState hs) { m_hsState = hs; } - /** 退出忙碌态(releaseReady 后调用),回到 IDLE。 */ + /** 退出忙碌态,回到 IDLE。 */ void exitBusy() { m_hsState = HandshakeState::IDLE; } /** 当前是否空闲(可接受新按铃下降沿)。 */ @@ -380,10 +357,6 @@ private: SemaphoreHandle_t m_lock; }; - /** @brief 下拉 SPIready(开漏驱动低)。 */ - void driveReadyLow(); - /** @brief 释放 SPIready(开漏高阻,靠上拉拉高)。 */ - void releaseReady(); /** * @brief 等 GD32 释放 SPIready(上升沿),中断驱动非忙等。 * @@ -461,7 +434,6 @@ private: uint8_t *resp_type, uint8_t *resp_payload, size_t resp_max, size_t *resp_len, uint32_t timeout_ms); // OTA 帧工具(静态,无状态) - 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); static bool otaFindFrame(const uint8_t *buf, size_t buf_len, diff --git a/tools/can.html b/tools/can.html index 127f88f..fc3e51c 100644 --- a/tools/can.html +++ b/tools/can.html @@ -213,6 +213,6 @@ 操作成功 - + diff --git a/tools/can.js b/tools/can.js index 9c4e547..bd5e2ad 100644 --- a/tools/can.js +++ b/tools/can.js @@ -185,6 +185,7 @@ var messageMap = {}; var comments = { messages: {}, signals: {} }; var valueTables = {}; + var sigDefaults = {}; var nodeNames = []; var current = null; @@ -234,6 +235,7 @@ comment: '', valueType: 'int', enumMap: {}, + defaultValue: null, multiplexRole: muxInfo.role, multiplexValue: muxInfo.value }); @@ -272,6 +274,15 @@ if (signal) { signal.valueType = valTypeMatch[3] === '1' ? 'float' : valTypeMatch[3] === '2' ? 'double' : 'int'; } + continue; + } + + /* BA_ "GenSigDefaultValue" SG_ ; + * 注意该值是物理值(已应用 factor/offset),与 GenSigStartValue(raw)不同 */ + var sigDefaultMatch = line.match(/^BA_\s+"GenSigDefaultValue"\s+SG_\s+(\d+)\s+([A-Za-z0-9_]+)\s+(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*;$/); + if (sigDefaultMatch) { + sigDefaults[sigDefaultMatch[1] + ':' + sigDefaultMatch[2]] = sigDefaultMatch[3]; + continue; } } @@ -280,6 +291,8 @@ msg.signals.forEach(function (signal) { signal.comment = comments.signals[String(msg.idRaw) + ':' + signal.name] || ''; signal.enumMap = valueTables[String(msg.idRaw) + ':' + signal.name] || {}; + var dv = sigDefaults[String(msg.idRaw) + ':' + signal.name]; + signal.defaultValue = (dv != null && Number.isFinite(Number(dv))) ? dv : null; }); }); @@ -704,10 +717,10 @@ } function getDefaultSignalValue(signal) { - var enumKeys = Object.keys(signal.enumMap || {}); - if (enumKeys.length) return enumKeys[0]; - if (Number.isFinite(signal.min)) return String(signal.min); - if (signal.factor === 0) return '0'; + /* 优先使用 DBC 中 BA_ "GenSigDefaultValue" 声明的物理值,否则回退为 0 */ + if (signal && signal.defaultValue != null && Number.isFinite(Number(signal.defaultValue))) { + return String(signal.defaultValue); + } return '0'; } @@ -1086,6 +1099,10 @@ updateBatchUi(); var channel = parseInt(el.defaultChannel.value, 10) || 0; var ok = 0, fail = 0; + /* BATCH 间隔:GD32 SPI 从机用 RBNE 中断逐字节接收,无 RX DMA, + * 连续高频 CS 事务易触发 RXORERR -> 帧字节丢失 -> CRC 失败 -> 0xEE。 + * 30ms 间隔给 GD32 足够时间完成 onCsRelease 清理 + EXTI 重新待命。 */ + var BATCH_INTERVAL_MS = 30; for (var i = 0; i < keys.length; i++) { var key = keys[i]; var msg = null; @@ -1144,6 +1161,10 @@ resultText: msg.name + ' · ' + err.message }); } + /* 帧间间隔,避免 GD32 SPI RXORERR(最后一帧不需要等) */ + if (i < keys.length - 1) { + await new Promise(function (r) { setTimeout(r, BATCH_INTERVAL_MS); }); + } } state.batchInFlight = false; updateBatchUi(); @@ -1470,7 +1491,17 @@ var CAN_SEND_ERR_DESC = { '-1': '参数错误(通道/ID/DLC 无效)', '-2': 'SPI 通信失败(ESP32 与 GD32 通信异常)', - '-3': 'GD32 CAN 总线拒绝(BUS_OFF / TEC 过高 / 邮箱满)', + '-3': 'GD32 CAN 总线拒绝', + }; + + var GD32_ERR_DESC = { + 0xE0: 'payload 过短', + 0xE1: '通道号非法', + 0xE2: 'BUS_OFF(总线离线,检查接线/终端电阻/对端上电)', + 0xE3: 'TEC 过高(错误计数 >96,对端 ACK 失败率高)', + 0xE4: '邮箱满(3 个硬件邮箱全 PENDING,对端不 ACK)', + 0xE5: '参数无效(通道未启用/DLC 越界)', + 0xEE: 'SPI 帧解析失败(GD32 收到的帧 CRC/SOF 校验失败,通常是 SPI 字节丢失或 CS 时序冲突)' }; function buildSendResponseText(result) { @@ -1479,6 +1510,15 @@ } var code = String(result.code); var desc = CAN_SEND_ERR_DESC[code] || '未知错误'; + /* 用 GD32 细分错误码补充具体原因 */ + if (result.gd32_err != null && result.gd32_err !== 0) { + var gd32Desc = GD32_ERR_DESC[result.gd32_err]; + if (gd32Desc) { + desc = desc + ' · ' + gd32Desc; + } else { + desc = desc + ' · gd32_err=0x' + result.gd32_err.toString(16).toUpperCase(); + } + } return '发送失败: ' + desc + ' (code=' + result.code + ')'; }