From 7c05496b8968edb075c41a46ee7023cf3628c2c7 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Mon, 13 Jul 2026 03:10:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(spi):=20=E6=9B=B4=E6=96=B0SPI=E5=91=BD?= =?UTF-8?q?=E4=BB=A4v2=EF=BC=8C=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=92=8C=E9=A1=B9=E7=9B=AE=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在cproject.yml中添加spi_link模块组和相关头文件路径 - 修改boot_request.cpp中的ACK响应机制,改用标准帧格式并优化CAN反馈处理 - 重构ping.cpp响应payload结构,包含版本信息、运行时间等16字节数据 - 添加systick.h依赖用于获取系统运行时间 - 优化启动bootloader流程,避免SPI中断阻塞问题 --- .../GD32E508_DCU_APP.cproject.yml | 4 + .../device/spi/app_spi_cmd_boot_request.cpp | 26 +- .../device/spi/app_spi_cmd_ping.cpp | 44 +- .../device/spi/app_spi_command.cpp | 580 +++++++++--------- GD32E508_DCU_APP/device/spi/app_spi_command.h | 51 +- .../device/spi/app_spi_command_defs.h | 6 +- GD32E508_DCU_APP/sys/ota_boot_request.cpp | 39 +- GD32E508_DCU_APP/sys/ota_boot_request.h | 11 +- GD32E508_DCU_APP/sys/task_runner.cpp | 6 + 9 files changed, 422 insertions(+), 345 deletions(-) diff --git a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml index 848eee7..4bd7f59 100644 --- a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml +++ b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml @@ -46,6 +46,9 @@ project: - file: ./device/spi/app_spi_cmd_can_bridge.cpp - file: ./device/spi/app_spi_cmd_can_feedback.cpp - file: ./device/spi/app_spi_cmd_id_snapshot.cpp + - group: spi_link + files: + - file: ../../spi_link/spi_link_v2.cpp - group: comm files: - file: ./comm/esc_exmc_bus.h @@ -97,6 +100,7 @@ project: - -fshort-enums - -fshort-wchar - -Wno-register + - -IG:/Robot_v2/DCU/spi_link C: - -std=gnu11 CPP: diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_boot_request.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_boot_request.cpp index 552dc73..471129a 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_cmd_boot_request.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_boot_request.cpp @@ -18,24 +18,16 @@ public: { bus.postLog("BOOT_REQ → 进BOOT(异步)"); - /* 立即装 0xB5 ACK,让 ESP32 尽快收到确认。 - * Flash 操作延迟到主循环执行,避免阻塞 SPI ISR。 - * - * ESP32 appRequestBootloader() 发送单字节 0xB0 后,逐字节轮询读回 0xB5。 - * 必须返回裸字节 ACK,不能用 buildResponse() 构建帧式 ACK - * (帧式 ACK 以 0xA5 开头,ESP32 永远匹配不到 0xB5)。 - * tx_buf[0] 必须是 ack 字节本身,因为 ESP32 只读首个 MISO 字节。 */ - if (ctx.tx_buf && ctx.tx_len && ctx.tx_buf_size >= 4U) { - ctx.tx_buf[0] = CONFIG_SPI_APP_OTA_BOOT_ACK_OK; - ctx.tx_buf[1] = CONFIG_SPI_APP_OTA_BOOT_CMD; - ctx.tx_buf[2] = 0x4FU; - ctx.tx_buf[3] = 0x54U; - *ctx.tx_len = 4U; - } + /* 先取消 CAN feedback 按铃,避免 0xC1 残留帧盖住本 ACK / 干扰复位。 */ + bus.cancelRing(); - /* 设置 pending 标志,主循环执行 Flash 操作 + 复位。 - * 原来在 ISR 内直接调 requestEnterBoot() 会执行 Flash 页擦除(20-30ms), - * 导致 CPU 总线停顿、SPI RBNE 中断无法响应、ESP32 ACK 轮询丢字节。 */ + /* 立即装标准帧 ACK:type=0xB0 payload=[0xB5],勿用通用 0x80。 + * Flash 擦写放到主循环,避免阻塞 SPI ISR。 */ + uint8_t status = CONFIG_SPI_APP_OTA_BOOT_ACK_OK; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kTypeBootReq, &status, 1U); + + /* pending + 立即冻结 SPI 业务;并拉高优先级心跳,尽快 process() 擦页复位。 */ OtaBootRequest::requestEnterBootAsync(); TaskManager::set(1UL << TASK_HEARTBEAT); } diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_ping.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_ping.cpp index 5323686..eba9760 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_cmd_ping.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_ping.cpp @@ -2,6 +2,7 @@ #include "device/spi/app_spi_command.h" #include "device/spi/app_spi_command_defs.h" #include "sys/config.h" +#include "sys/systick.h" #include "main.h" #include @@ -16,32 +17,39 @@ public: void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override { - uint8_t alive_payload[19]; - std::memset(alive_payload, 0, sizeof(alive_payload)); - alive_payload[0] = 0xA1U; /* magic */ - alive_payload[1] = 0x55U; /* APP version tag */ - alive_payload[2] = (uint8_t)APP_FW_VERSION_MAJOR; - alive_payload[3] = (uint8_t)APP_FW_VERSION_MINOR; - alive_payload[4] = (uint8_t)APP_FW_VERSION_PATCH; - alive_payload[5] = (uint8_t)APP_FW_VERSION_BUILD; - alive_payload[6] = 0xAAU; /* BOOT version tag */ + /* v2 PING 响应 payload(16B): + * [0..3] APP version (major/minor/patch/build) + * [4..7] BOOT version (major/minor/patch/build) + * [8..11] uptime_ms (4B LE) + * [12..15] build_hash (4B, 暂用 0 占位) */ + uint8_t ping_payload[16]; + std::memset(ping_payload, 0, sizeof(ping_payload)); + ping_payload[0] = (uint8_t)APP_FW_VERSION_MAJOR; + ping_payload[1] = (uint8_t)APP_FW_VERSION_MINOR; + ping_payload[2] = (uint8_t)APP_FW_VERSION_PATCH; + ping_payload[3] = (uint8_t)APP_FW_VERSION_BUILD; + + /* BOOT 版本号从 OTA 配置页 Flash 读取 */ const volatile uint32_t *bv0 = (const volatile uint32_t *)CONFIG_BOOT_VERSION_ADDR0; const volatile uint32_t *bv1 = (const volatile uint32_t *)CONFIG_BOOT_VERSION_ADDR1; uint32_t bw0 = *bv0; uint32_t bw1 = *bv1; - alive_payload[7] = (uint8_t)(bw0 & 0xFFU); - alive_payload[8] = (uint8_t)((bw0 >> 16) & 0xFFU); - alive_payload[9] = (uint8_t)(bw1 & 0xFFU); - alive_payload[10] = (uint8_t)((bw1 >> 16) & 0xFFU); + ping_payload[4] = (uint8_t)(bw0 & 0xFFU); + ping_payload[5] = (uint8_t)((bw0 >> 16) & 0xFFU); + ping_payload[6] = (uint8_t)(bw1 & 0xFFU); + ping_payload[7] = (uint8_t)((bw1 >> 16) & 0xFFU); - alive_payload[11] = (uint8_t)(g_app_uptime_sec & 0xFFU); - alive_payload[12] = (uint8_t)((g_app_uptime_sec >> 8) & 0xFFU); - alive_payload[13] = (uint8_t)((g_app_uptime_sec >> 16) & 0xFFU); - alive_payload[14] = (uint8_t)((g_app_uptime_sec >> 24) & 0xFFU); + const uint32_t uptime_ms = SysTickTimer::getMs(); + ping_payload[8] = (uint8_t)(uptime_ms & 0xFFU); + ping_payload[9] = (uint8_t)((uptime_ms >> 8) & 0xFFU); + ping_payload[10] = (uint8_t)((uptime_ms >> 16) & 0xFFU); + ping_payload[11] = (uint8_t)((uptime_ms >> 24) & 0xFFU); + + /* build_hash 占位 0(ping_payload[12..15] 已由 memset 清零) */ bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, - SpiDefs::kRspAck, alive_payload, sizeof(alive_payload)); + SpiDefs::kRspAck, ping_payload, sizeof(ping_payload)); } }; diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp index df9f68b..65fad46 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp @@ -12,15 +12,19 @@ * 通过 registerHandler() 注册进来,后续新增 SPI 业务时 * 只需要新增命令处理器,不再继续膨胀本文件。 * - * 握手架构(线与 SPIready,PB6<->ESP32 GPIO10 开漏单线): + * 握手架构(v2 DATA_READY,PB6<->ESP32 GPIO10 开漏单线): + * - PB6 低 = 有待发数据(m_txLen>0 或 m_ringPending),高 = 队列空。 * - 命令处理在 onCsRelease() EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame), - * 不再推迟到主循环,消除 PB6 拉高延迟。 - * - GD32 可通过 requestEsp32Read() 下拉 SPIready 主动按铃请求 ESP32 来读。 + * 完成后由 updateDataReady() 依据待发数据驱动 PB6,不再延迟拉高。 + * - GD32 可通过 requestEsp32Read() 设 m_ringPending 并由 updateDataReady() + * 拉低 PB6 主动按铃,请求 ESP32 来读。 */ #include "device/spi/app_spi_command.h" #include "device/spi/spi_command_handler.h" #include "device/spi/app_spi_command_defs.h" +#include "spi_link_v2.h" +#include "sys/ota_boot_request.h" extern "C" { #include "gd32e50x_exti.h" @@ -33,7 +37,6 @@ extern "C" { #include "sys/config.h" #include "sys/critical.h" #include "sys/task_manager.h" -#include "sys/ota_boot_request.h" #include "sys/systick.h" #include "main.h" #include @@ -42,7 +45,6 @@ extern "C" { /* ============================================================ * 本地别名:保持当前实现文件内部风格不变 * ============================================================ */ -static constexpr uint8_t kSof = SpiDefs::kSof; static constexpr uint8_t kTypeBootReq = SpiDefs::kTypeBootReq; static constexpr uint8_t kTypeCanSend = SpiDefs::kTypeCanSend; static constexpr uint8_t kTypeOtaReq = SpiDefs::kTypeOtaReq; @@ -52,7 +54,6 @@ static constexpr uint8_t kTypeOtaCancel = SpiDefs::kTypeOtaCancel; static constexpr uint8_t kRspAck = SpiDefs::kRspAck; static constexpr uint8_t kRspWait = SpiDefs::kRspWait; static constexpr uint8_t kRspError = SpiDefs::kRspError; -static constexpr uint16_t kFrameOverhead = SpiDefs::kFrameOverhead; /* DMA 配置(与 BOOT SpiOta 一致:DMA0_CH2 用于 SPI0 TX) */ #define SPI_TX_DMA_PERIPH DMA0 @@ -90,12 +91,10 @@ static bool shouldIgnoreTransaction(const uint8_t *data, uint16_t len) return true; } - /* APP 侧只接受两类事务: - * 1) 旧式单字节 0xB0 - * 2) 完整协议帧(至少 5 字节) - * 短于 5 字节且不是 0xB0 的事务只能是启动切换期的残包,直接忽略。 */ - return (len < kFrameOverhead) && - !(len == 1U && data[0] == CONFIG_SPI_APP_OTA_BOOT_CMD); + /* v2 协议帧最小长度为 V2_OVERHEAD(20B)。 + * 短于该长度的事务只能是启动切换期的残包,直接忽略。 + * (0xB0 进 BOOT 现已走标准 v2 帧 handler,不再有裸字节通道。) */ + return (len < V2_OVERHEAD); } static bool isFlashRelatedFrame(uint8_t frame_type) @@ -111,155 +110,67 @@ static bool isFlashRelatedFrame(uint8_t frame_type) } } -/* ============================================================ - * CRC8 / 帧构建 / 帧解析(与 BOOT SpiOta 一致) - * ============================================================ */ - -static constexpr uint8_t kSyncSof0 = 0xA5U; -static constexpr uint8_t kSyncSof1 = 0x5AU; -static constexpr uint16_t kSyncHeaderLen = 6U; -static constexpr uint16_t kSyncCrcLen = 4U; -static constexpr uint16_t kSyncOverhead = (uint16_t)(kSyncHeaderLen + kSyncCrcLen); - -/* 扫描前 16 字节查找同步帧头 SOF0 SOF1,容忍 RXORERR 导致的首字节偏移。 - * 与 spi_try_parse_frame 的偏移扫描逻辑对齐。 */ -static bool containsSyncSof(const uint8_t *data, uint16_t len) -{ - if (!data || len < 2U) { return false; } - uint16_t last = (len < 16U) ? (uint16_t)(len - 1U) : 16U; - for (uint16_t i = 0U; i < last; ++i) { - if (data[i] == kSyncSof0 && data[i + 1U] == kSyncSof1) { return true; } - } - return false; -} - /* GD32 APP 侧同步状态机(ISR 与主循环只读/原子写,避免临界区开销)。 */ static volatile uint8_t g_localCredit = 8U; static volatile uint8_t g_remoteCredit = 8U; -static void putU16Le(uint8_t *pp, uint16_t v) -{ - pp[0] = (uint8_t)(v & 0xFFU); - pp[1] = (uint8_t)((v >> 8) & 0xFFU); -} +/* SPI Link v2 会话/序号状态机(ISR 写、主循环读写,均原子,无需临界区)。 */ +static volatile uint32_t g_sessionId = 0; /* v2 会话 ID(0=未建立) */ +static volatile uint8_t g_txSeq = 0; /* GD32 发送序号(事件/响应帧) */ +static volatile uint8_t g_expectedRxSeq = 0; /* 期望收到的 ESP32 请求序号 */ +static volatile bool g_v2Active = false; /* v2 session 是否激活 */ +/* 当前请求的 TX_SEQ,写入响应 ACK_SEQ,供 ESP32 做响应归属匹配(延迟一拍)。 */ +static volatile uint8_t g_pendingAckSeq = 0; -static uint16_t getU16Le(const uint8_t *pp) -{ - return (uint16_t)(pp[0] | ((uint16_t)pp[1] << 8)); -} +/* 响应缓存:用于重复请求重发同一响应(避免业务 handler 重复执行)。 */ +static uint8_t s_lastReqSeq = 0xFF; +static uint8_t s_lastRespBuf[256]; +static uint16_t s_lastRespLen = 0; +static bool s_hasCachedResp = false; -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); -} +/* ============================================================ + * v2 帧构建 / 帧解析(委托 spi_link_v2.h,三工程共用) + * ============================================================ */ -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); -} - -static uint32_t spi_crc32(const uint8_t *data, uint16_t len) -{ - uint32_t crc = 0xFFFFFFFFU; - for (uint16_t i = 0U; i < len; ++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; -} - -static uint8_t spi_crc8(const uint8_t *data, uint16_t len) -{ - uint8_t crc = 0x00U; - for (uint16_t i = 0U; i < len; ++i) { crc ^= data[i]; } - return crc; -} - -/* 构建新同步帧;上层接口(type/payload)保持不变,handler 无需改动。 - * kFrameOverhead 旧常量仍保留供旧路径使用。 */ +/* 构建响应帧;上层接口(type/payload)保持不变,handler 无需改动。 + * 内部调用 v2_build_frame,FLAGS 默认 V2_FLAG_RESPONSE,session/seq/credit + * 从 v2 状态全局变量取。session 未建立时 session_id 填 0。 + * ACK_SEQ = g_pendingAckSeq(对端请求的 TX_SEQ),不能写死 0,否则 ESP32 + * otaFindFrame 在 m_v2Active 时会把合法延迟响应当成 stale 丢弃。 */ static bool spi_build_frame(uint8_t type, const uint8_t *payload, uint16_t payload_len, uint8_t *out, uint16_t *out_len) { if (!out || !out_len) { return false; } - uint16_t total = (uint16_t)(kSyncOverhead + payload_len); - if (total > AppSpiCommand::kTxBufSize) { return false; } + if ((uint32_t)V2_OVERHEAD + payload_len > AppSpiCommand::kTxBufSize) { return false; } - out[0] = kSyncSof0; - out[1] = kSyncSof1; - out[2] = g_localCredit; - out[3] = type; - putU16Le(&out[4], payload_len); - if (payload && payload_len > 0U) { - memcpy(&out[kSyncHeaderLen], payload, payload_len); - } - uint32_t crc = spi_crc32(out, (uint16_t)(kSyncHeaderLen + payload_len)); - putU32Le(&out[kSyncHeaderLen + payload_len], crc); - *out_len = total; + const uint32_t sess = g_v2Active ? g_sessionId : 0U; + size_t built = 0U; + const bool ok = v2_build_frame(type, V2_FLAG_RESPONSE, sess, + g_txSeq, g_pendingAckSeq, (uint8_t)g_localCredit, + payload, payload_len, out, &built); + if (!ok) { return false; } + *out_len = (uint16_t)built; return true; } -/* 解析:新同步帧优先;失败回退旧 0xA5 帧(含首字节偏移容错)。 */ +/* 解析 v2 帧;委托 v2_parse_frame,扫描 SOF 并校验 VERSION/CRC32。 + * 旧的 0xA5 CRC8 帧兼容路径已移除(v2 不再向后兼容 v1)。 */ static bool spi_try_parse_frame(const uint8_t *frame, uint16_t frame_len, - uint8_t *type, const uint8_t **payload, uint16_t *payload_len) + uint8_t *type, const uint8_t **payload, uint16_t *payload_len, + v2_header_t *out_hdr = nullptr) { if (!frame || !type || !payload || !payload_len) { return false; } - for (uint16_t s = 0U; s + kSyncOverhead <= frame_len; ++s) { - if (frame[s] != kSyncSof0 || frame[s + 1U] != kSyncSof1) { continue; } - uint16_t plen = getU16Le(&frame[s + 4U]); - uint16_t total = (uint16_t)(kSyncOverhead + plen); - if ((uint16_t)(frame_len - s) < total) { continue; } - uint32_t expect = getU32Le(&frame[s + kSyncHeaderLen + plen]); - uint32_t calc = spi_crc32(&frame[s], (uint16_t)(kSyncHeaderLen + plen)); - if (expect != calc) { continue; } + v2_header_t hdr; + const uint8_t *pload = nullptr; + const bool ok = v2_parse_frame(frame, frame_len, &hdr, &pload); + if (!ok) { return false; } - uint8_t credit = frame[s + 2U]; - *type = frame[s + 3U]; - *payload = (plen > 0U) ? &frame[s + kSyncHeaderLen] : nullptr; - *payload_len = plen; - g_remoteCredit = credit; - return true; - } - - /* 旧 0xA5 帧兼容:首字节可能偏移,扫描 SOF。 */ - uint16_t s = 0U; - while (s < frame_len && frame[s] != kSof) { ++s; } - if (s > 0U && s < frame_len) { - const uint16_t shifted_len = (uint16_t)(frame_len - s); - if (shifted_len >= kFrameOverhead) { - const uint16_t len_field = (uint16_t)frame[s + 2U] | ((uint16_t)frame[s + 3U] << 8); - const uint16_t expected = (uint16_t)(kFrameOverhead + len_field); - if (shifted_len >= expected && - spi_crc8(&frame[s], (uint16_t)(4U + len_field)) == frame[s + 4U + len_field]) { - *type = frame[s + 1U]; - *payload = &frame[s + 4U]; - *payload_len = len_field; - return true; - } - } - } - - if (frame_len < kFrameOverhead) { return false; } - if (frame[0] != kSof) { return false; } - - uint16_t len = (uint16_t)frame[2] | ((uint16_t)frame[3] << 8); - uint16_t expected = (uint16_t)(kFrameOverhead + len); - if (frame_len < expected) { return false; } - if (spi_crc8(frame, (uint16_t)(4U + len)) != frame[4U + len]) { return false; } - - *type = frame[1]; - *payload = &frame[4]; - *payload_len = len; + g_remoteCredit = hdr.credit; + *type = hdr.type; + *payload = pload; + *payload_len = hdr.payload_len; + if (out_hdr) { *out_hdr = hdr; } return true; } @@ -322,9 +233,9 @@ void AppSpiCommand::configureGpio() gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4); gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15); - /* PB6 复用为 SPI Ready 握手线(开漏输出,线与)。 + /* PB6 复用为 DATA_READY 握手线(开漏输出,线与)。 * 开漏模式:写 BRR = 下拉驱动低;写 BSRR = 高阻释放,靠外部/对端上拉拉高。 - * 默认释放(HIGH),允许主机立刻发起首帧。 */ + * 语义:低 = 有待发数据,高 = 队列空;默认释放(HIGH)。 */ gpio_init(CONFIG_SPI_READY_PORT, GPIO_MODE_OUT_OD, GPIO_OSPEED_50MHZ, CONFIG_SPI_READY_PIN); gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); } @@ -340,6 +251,14 @@ void AppSpiCommand::setSpiReady(bool ready) } } +/* v2 DATA_READY: PB6 低 = 有待发数据,高 = 队列空。 + * 替代旧 BUSY/READY 语义中所有 setSpiReady 调用。 */ +void AppSpiCommand::updateDataReady() +{ + const bool has_data = (m_txLen > 0U) || m_ringPending; + setSpiReady(!has_data); +} + void AppSpiCommand::configureSpi0() { rcu_periph_clock_enable(RCU_SPI0); @@ -362,7 +281,7 @@ void AppSpiCommand::configureSpi0() spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_RBNE); spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_ERR); spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_TBE); - nvic_irq_enable(SPI0_IRQn, CONFIG_IRQ_PREEMPT_SPI_USART, 0U); + nvic_irq_enable(SPI0_IRQn, 2U, 0U); spi_enable(SPI0); } @@ -393,15 +312,14 @@ void AppSpiCommand::configureExti() exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH); exti_interrupt_enable(EXTI_15); exti_interrupt_flag_clear(EXTI_15); - nvic_irq_enable(EXTI10_15_IRQn, CONFIG_IRQ_PREEMPT_SPI_USART, 1U); + nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U); } void AppSpiCommand::configureSpiReadyExti() { - /* PB6 已在 configureGpio() 配置为开漏输出,此处启用 EXTI6 上升沿检测。 - * EXTI 检测引脚实际电平(IDR),与 GPIO 输出模式兼容。 - * 用于检测 ESP32 releaseReady 产生的上升沿(线从忙碌→空闲), - * 作为 ESP32 完成上一帧处理的可靠信号。 */ + /* PB6 已在 configureGpio() 配置为开漏输出,此处保留 EXTI6 中断使能。 + * v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 上升沿无业务含义, + * onSpiReadyExti() 仅清标志避免反复触发。 */ gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOB, GPIO_PIN_SOURCE_6); exti_init(EXTI_6, EXTI_INTERRUPT, EXTI_TRIG_RISING); exti_interrupt_enable(EXTI_6); @@ -411,10 +329,10 @@ void AppSpiCommand::configureSpiReadyExti() void AppSpiCommand::onSpiReadyExti() { + /* v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 上升沿不再有意义。 + * 清除中断标志避免反复触发。 */ if (exti_interrupt_flag_get(EXTI_6) != RESET) { exti_interrupt_flag_clear(EXTI_6); - /* 线升高(空闲):ESP32 已 releaseReady,上一帧处理完 */ - m_linkState.onEsp32Complete(); } } @@ -478,100 +396,202 @@ void AppSpiCommand::processCommand() return; } - /* 尝试按协议帧解析 */ + /* 0xB0 已受理、正在写 OTA 配置页 / 即将复位:不再处理业务帧,避免打断 Flash。 */ + if (OtaBootRequest::isSpiFrozenForBoot() || OtaBootRequest::isRebootPending()) { + m_txLen = 0U; + m_txPos = 0U; + updateDataReady(); + return; + } + + /* 尝试按 v2 协议帧解析;同时取回完整帧头用于 session/seq 校验 */ uint8_t frame_type = 0U; const uint8_t *payload = nullptr; uint16_t payload_len = 0U; + v2_header_t hdr; - if (spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len)) { - if (CONFIG_SPI_APP_LOG_ENABLE != 0 && !isFlashRelatedFrame(frame_type)) { - postLogFmt("[SPI-APP] 收到非刷写 SPI type=0x%02X", frame_type); - } - - /* 合法帧:按 TYPE 分发给已注册命令处理器 */ + if (!spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr)) { + /* 无法解析为合法 v2 帧:构建 ERROR 响应 */ + postLogFmt("[SPI-APP] 非v2帧 0x%02X -> ERROR", m_rxBuf[0]); uint16_t tx_len = 0U; - bool handled = false; - - SpiCommandContext ctx{}; - ctx.frame_type = frame_type; - ctx.payload = payload; - ctx.payload_len = payload_len; - ctx.tx_buf = m_txBuf; - ctx.tx_buf_size = kTxBufSize; - ctx.tx_len = &tx_len; - - for (uint8_t i = 0U; i < m_commandCount; ++i) { - ISpiCommandHandler *handler = m_commands[i]; - if (handler && handler->canHandle(frame_type)) { - handler->handle(AppSpiCommandBus, ctx); - handled = true; - break; - } - } - - if (!handled) { - postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type); - uint8_t err = 0xEEU; - spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); - } - - if (tx_len == 0U) { - /* 兜底:避免残留上一次应答帧 */ - uint8_t err = 0xEEU; - spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); - } - + uint8_t err = 0xEEU; + spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); m_txLen = tx_len; m_txPos = 0U; - } else { - /* 非标准帧:保留旧式单字节 0xB0 通道,后续新命令建议走注册式帧协议 */ - const uint8_t first_valid = m_rxBuf[0U]; - if (first_valid == CONFIG_SPI_APP_OTA_BOOT_CMD) { - postLog("旧式0xB0 -> 进BOOT"); - uint16_t tx_len_legacy = 0U; - SpiCommandContext ctx{}; - ctx.frame_type = first_valid; - ctx.payload = nullptr; - ctx.payload_len = 0U; - ctx.tx_buf = m_txBuf; - ctx.tx_buf_size = kTxBufSize; - ctx.tx_len = &tx_len_legacy; - - bool handled = false; - for (uint8_t i = 0U; i < m_commandCount; ++i) { - ISpiCommandHandler *handler = m_commands[i]; - if (handler && handler->canHandle(first_valid)) { - handler->handle(AppSpiCommandBus, ctx); - handled = true; - break; - } - } - - if (!handled) { - /* 兜底路径(handler 未注册时的死代码保护): - * 同样使用异步路径,避免在 ISR 内执行 Flash 操作。 - * 先装 0xB5 ACK,再设 pending 标志。 */ - m_txBuf[0] = CONFIG_SPI_APP_OTA_BOOT_ACK_OK; - m_txBuf[1] = CONFIG_SPI_APP_OTA_BOOT_CMD; - m_txBuf[2] = 0x4FU; - m_txBuf[3] = 0x54U; - tx_len_legacy = 4U; - OtaBootRequest::requestEnterBootAsync(); - TaskManager::set(1UL << TASK_HEARTBEAT); - } - - m_txLen = tx_len_legacy; - m_txPos = 0U; - } else { - /* 无法识别 */ - postLogFmt("[SPI-APP] 非协议 0x%02X -> ERROR", first_valid); - uint16_t tx_len = 0U; - uint8_t err = 0xEEU; - spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); - m_txLen = tx_len; - m_txPos = 0U; + if (m_logPending) { + TaskManager::set(1UL << TASK_SPI_LOG); } + if (m_txLen > 0U) { + m_ringPending = true; + m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; + } + updateDataReady(); + return; + } + + if (CONFIG_SPI_APP_LOG_ENABLE != 0 && !isFlashRelatedFrame(frame_type)) { + postLogFmt("[SPI-APP] 收到非刷写 SPI type=0x%02X", frame_type); + } + + /* 响应归属:ACK_SEQ 必须回显请求 TX_SEQ(延迟一拍 + ESP32 otaFindFrame 匹配)。 */ + g_pendingAckSeq = hdr.tx_seq; + + /* ---------- HELLO 建链路处理(在 session 校验之前) ---------- */ + if (frame_type == V2_TYPE_LINK_HELLO) { + /* 接受 ESP32 提供的 session_id,重置本端序号状态机 */ + g_sessionId = hdr.session_id; + g_txSeq = 0; + g_expectedRxSeq = 0; + g_v2Active = true; + s_hasCachedResp = false; + + /* 构建 HELLO_ACK 响应帧(回显 session_id + 角色 + credit_max) */ + size_t built_len = 0U; + v2_build_hello_ack_frame(g_sessionId, V2_ROLE_APP, (uint8_t)g_localCredit, + 0, hdr.tx_seq, m_txBuf, &built_len); + m_txLen = (uint16_t)built_len; + m_txPos = 0U; + + /* 缓存 HELLO_ACK 便于重复 HELLO 重发 */ + s_lastReqSeq = hdr.tx_seq; + memcpy(s_lastRespBuf, m_txBuf, m_txLen); + s_lastRespLen = m_txLen; + s_hasCachedResp = true; + + if (m_logPending) { + TaskManager::set(1UL << TASK_SPI_LOG); + } + if (m_txLen > 0U) { + m_ringPending = true; + m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; + } + updateDataReady(); + return; + } + + /* ---------- SESSION / SEQ 校验(仅在 v2 session 激活后生效) ---------- */ + if (g_v2Active) { + /* session_id 不匹配:构建 LINK_ERROR(V2_ERR_SESSION),跳过 handler */ + if (hdr.session_id != g_sessionId) { + postLogFmt("[SPI-APP] session 不匹配 0x%02X -> LINK_ERROR", + (uint8_t)(hdr.session_id & 0xFFU)); + size_t built_len = 0U; + v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq, + (uint8_t)g_localCredit, V2_ERR_SESSION, + (uint8_t)g_expectedRxSeq, m_txBuf, &built_len); + m_txLen = (uint16_t)built_len; + m_txPos = 0U; + + if (m_logPending) { + TaskManager::set(1UL << TASK_SPI_LOG); + } + if (m_txLen > 0U) { + m_ringPending = true; + m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; + } + updateDataReady(); + return; + } + + /* 序号回绕距离判定 */ + const uint8_t dist = v2_seq_distance(hdr.tx_seq, g_expectedRxSeq); + if (dist == 0U) { + /* 正常:序号匹配,推进期望序号 */ + g_expectedRxSeq = (uint8_t)(g_expectedRxSeq + 1U); + } else if (dist >= 128U) { + /* 重复请求:重发缓存的响应帧,跳过 handler */ + if (s_hasCachedResp && hdr.tx_seq == s_lastReqSeq) { + postLogFmt("[SPI-APP] 重复请求 seq=0x%02X -> 重发缓存", hdr.tx_seq); + memcpy(m_txBuf, s_lastRespBuf, s_lastRespLen); + m_txLen = s_lastRespLen; + m_txPos = 0U; + + if (m_logPending) { + TaskManager::set(1UL << TASK_SPI_LOG); + } + if (m_txLen > 0U) { + m_ringPending = true; + m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; + } + updateDataReady(); + return; + } + /* 无缓存可用:按正常请求处理 */ + } else { + /* dist 1-127:序号跳变。对齐期望序号后: + * - 普通业务:回 LINK_ERROR 并丢弃(避免副作用) + * - 0xB0 ENTER_BOOT:控制面命令,仍必须执行,否则永远进不了 BOOT */ + postLogFmt("[SPI-APP] SEQ jump rx=0x%02X", hdr.tx_seq); + g_expectedRxSeq = (uint8_t)(hdr.tx_seq + 1U); + + if (frame_type != kTypeBootReq) { + size_t built_len = 0U; + v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq, + (uint8_t)g_localCredit, V2_ERR_SEQ_JUMP, + (uint8_t)g_expectedRxSeq, m_txBuf, &built_len); + m_txLen = (uint16_t)built_len; + m_txPos = 0U; + + if (m_logPending) { + TaskManager::set(1UL << TASK_SPI_LOG); + } + if (m_txLen > 0U) { + m_ringPending = true; + m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; + } + updateDataReady(); + return; + } + postLog("SEQ jump but BOOT_REQ still handled"); + /* fall through → 业务分发 */ + } + } + + /* ---------- 业务命令分发 ---------- */ + uint16_t tx_len = 0U; + bool handled = false; + + SpiCommandContext ctx{}; + ctx.frame_type = frame_type; + ctx.payload = payload; + ctx.payload_len = payload_len; + ctx.tx_buf = m_txBuf; + ctx.tx_buf_size = kTxBufSize; + ctx.tx_len = &tx_len; + + for (uint8_t i = 0U; i < m_commandCount; ++i) { + ISpiCommandHandler *handler = m_commands[i]; + if (handler && handler->canHandle(frame_type)) { + handler->handle(AppSpiCommandBus, ctx); + handled = true; + break; + } + } + + if (!handled) { + postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type); + uint8_t err = 0xEEU; + spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); + } + + if (tx_len == 0U) { + /* 兜底:避免残留上一次应答帧 */ + uint8_t err = 0xEEU; + spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); + } + + m_txLen = tx_len; + m_txPos = 0U; + + /* 缓存本次响应,便于重复请求时重发(避免 handler 重复执行副作用) */ + if (g_v2Active) { + s_lastReqSeq = hdr.tx_seq; + memcpy(s_lastRespBuf, m_txBuf, m_txLen); + s_lastRespLen = m_txLen; + s_hasCachedResp = true; + /* 推进本端发送序号(响应帧占用一个 tx_seq 槽位) */ + g_txSeq = (uint8_t)(g_txSeq + 1U); } if (m_logPending) { @@ -579,7 +599,7 @@ void AppSpiCommand::processCommand() TaskManager::set(1UL << TASK_SPI_LOG); } - /* 应答帧已就绪:拉高 PB6(释放 SPIready)通知主机可发起下一笔事务。 + /* 应答帧已就绪:由 updateDataReady() 依据 m_txLen 驱动 PB6(有待发=低)。 * 命令处理已在 ISR 内同步完成,无需 m_cmdPending 推迟机制。 */ /* 有响应待发时,设置按铃标志阻止主循环 canFeedbackPushStep() 覆盖 m_txBuf。 @@ -589,11 +609,11 @@ void AppSpiCommand::processCommand() m_ringPending = true; m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; } - setSpiReady(true); + updateDataReady(); } /* ============================================================ - * GD32 主动按铃:下拉 SPIready 请求 ESP32 来读 + * GD32 主动按铃:设 m_ringPending + updateDataReady() 拉低 PB6 请求 ESP32 来读 * ============================================================ */ void AppSpiCommand::cancelRing() @@ -605,8 +625,9 @@ void AppSpiCommand::cancelRing() m_txPos = 0U; /* 超时后允许重试,不依赖 ESP32 releaseReady 上升沿 */ m_linkState.onRingTimeout(); - /* 释放 SPIready(高阻),避免永久占用线 */ - setSpiReady(true); + /* 释放 SPIready(高阻),避免永久占用线。 + * cancelRing 已清 m_txLen 和 m_ringPending,updateDataReady() 会释放为高。 */ + updateDataReady(); } bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len) @@ -616,22 +637,32 @@ bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, return false; } - /* 构建请求帧到 m_txBuf(ESP32 下次读事务时通过 DMA 输出到 MISO) */ - uint16_t built_len = 0U; - if (!spi_build_frame(frame_type, payload, payload_len, m_txBuf, &built_len)) { + /* 构建 v2 EVENT 帧到 m_txBuf(ESP32 下次读事务时通过 DMA 输出到 MISO)。 + * 主动按铃帧使用 V2_FLAG_EVENT,session/seq 从 v2 状态全局变量取; + * session 未建立时 session_id 填 0。 */ + const uint32_t sess = g_v2Active ? g_sessionId : 0U; + size_t built_len = 0U; + if (!v2_build_frame(frame_type, V2_FLAG_EVENT, sess, + g_txSeq, 0U, (uint8_t)g_localCredit, + payload, payload_len, m_txBuf, &built_len)) { return false; } + /* EVENT 帧占用一个发送序号槽位 */ + if (g_v2Active) { + g_txSeq = (uint8_t)(g_txSeq + 1U); + } - m_ringTxLen = built_len; - m_txLen = built_len; + m_ringTxLen = (uint16_t)built_len; + m_txLen = (uint16_t)built_len; m_txPos = 0U; m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; m_ringPending = true; /* 消费完成 flag,等 ESP32 releaseReady 触发 EXTI6 上升沿 */ m_linkState.onRingStart(); - /* 下拉 SPIready,通知 ESP32「我有请求帧要给你读」 */ - setSpiReady(false); + /* v2 DATA_READY: 已设 m_ringPending=true 且 m_txLen>0, + * updateDataReady() 据此拉低 PB6,通知 ESP32「有待发数据」。 */ + updateDataReady(); return true; } @@ -681,10 +712,8 @@ void AppSpiCommand::onCsAssert() return; } m_csActive = true; - /* CS 下降沿立即拉低 PB6:表示从机即将忙碌, - * 防止主机在 onCsRelease 准备好响应前抢跑发起下一笔 CS。 - * (若此帧被 shouldIgnoreTransaction 忽略,onCsRelease 会恢复 HIGH) */ - setSpiReady(false); + /* v2 DATA_READY: CS 本身即互斥,PB6 不再表示「忙碌」,不在 CS 下降沿拉低。 + * PB6 仅由 updateDataReady() 依据待发数据驱动,避免与队列语义冲突。 */ m_rxLen = 0U; m_txPos = 0U; m_txActiveThisCs = false; @@ -740,24 +769,27 @@ void AppSpiCommand::onCsRelease() /* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧, * 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。 * - * 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 0xB0 命令(OTA 请求)。 - * 若 GD32 正在按铃时 ESP32 发 0xB0,会因 m_ringPending=true 被误吞, - * 导致 OTA 无法启动。检测到 0xB0 时取消按铃并走正常命令处理。 */ + * 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 v2 协议帧(如 PING/OTA/0xB0)。 + * 若 GD32 正在按铃时 ESP32 发协议帧,会因 m_ringPending=true 被误吞, + * 导致命令无法处理。检测到 v2 SOF 时取消按铃并走正常命令处理。 */ if (m_ringPending) { - if (m_rxLen >= 1U && m_rxBuf[0] == CONFIG_SPI_APP_OTA_BOOT_CMD) { - /* ESP32 发的是 0xB0 Bootloader 请求,不是按铃响应。 - * 取消按铃,清空 TX 让 processCommand() 重新装载 0xB5 应答。 - * 不释放 SPIready,processCommand() 末尾会释放。 */ - m_ringPending = false; - m_ringTxLen = 0U; - m_txLen = 0U; - m_txPos = 0U; - memset(m_txBuf, 0xFF, sizeof(m_txBuf)); - /* 继续走 shouldIgnoreTransaction + processCommand(),不 return */ - } else if (containsSyncSof(m_rxBuf, m_rxLen)) { - /* ESP32 发的是协议帧(如 PING/OTA 等),不是按铃响应。 + /* 判断本次收到的数据是否为 v2 协议帧(扫描 SOF0 SOF1)。 + * 取代旧 containsSyncSof,直接用 V2_SOF0/V2_SOF1。 + * 0xB0 进 BOOT 现已走标准 v2 帧 handler,不再单独检测裸字节。 */ + bool is_v2_frame = false; + if (m_rxLen >= 2U) { + const uint16_t last = (m_rxLen < 16U) ? (uint16_t)(m_rxLen - 1U) : 16U; + for (uint16_t i = 0U; i < last; ++i) { + if (m_rxBuf[i] == V2_SOF0 && m_rxBuf[i + 1U] == V2_SOF1) { + is_v2_frame = true; + break; + } + } + } + if (is_v2_frame) { + /* ESP32 发的是 v2 协议帧(如 PING/OTA/0xB0 等),不是按铃响应。 * 取消按铃,清空 TX 让 processCommand() 重新装载对应应答。 - * 不释放 SPIready,processCommand() 末尾会释放。 */ + * processCommand() 末尾由 updateDataReady() 依据新 m_txLen 驱动 PB6。 */ m_ringPending = false; m_ringTxLen = 0U; m_txLen = 0U; @@ -772,8 +804,10 @@ void AppSpiCommand::onCsRelease() m_txLen = 0U; m_txPos = 0U; memset(m_txBuf, 0xFF, sizeof(m_txBuf)); - /* 释放 SPIready,通知 ESP32 本帧完成 */ - setSpiReady(true); + /* v2 DATA_READY: 已清 m_txLen=0 与 m_ringPending=false, + * updateDataReady() 释放为高;ESP32 完成 CS 即视为已读走确认。 */ + m_linkState.onEsp32Complete(); + updateDataReady(); /* 上层若需要 ESP32 处理该请求帧,由 ESP32 侧读到的帧 TYPE 决定后续动作 */ return; } @@ -787,9 +821,9 @@ void AppSpiCommand::onCsRelease() memset(m_txBuf, 0xFF, sizeof(m_txBuf)); } m_txActiveThisCs = false; - /* 忽略事务(空轮询/残包)后恢复 PB6=HIGH, - * 避免 PB6 卡 LOW 导致 ESP32 等待上升沿超时 */ - setSpiReady(true); + /* v2 DATA_READY: shouldIgnoreTransaction 路径已清 m_txLen=0, + * updateDataReady() 释放为高,避免 PB6 卡 LOW。 */ + updateDataReady(); return; } diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.h b/GD32E508_DCU_APP/device/spi/app_spi_command.h index 1e39eb0..30cd9e7 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.h +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.h @@ -6,26 +6,26 @@ class ISpiCommandHandler; /** - * @brief GD32 侧 SPI 链路状态机:封装按铃状态 + ESP32 完成信号。 + * @brief GD32 侧 SPI 链路状态机:封装按铃状态。 * - * 线状态语义(对齐 OTA): - * - 线 HIGH = 空闲(双方释放),GD32 可发起新按铃 - * - 线 LOW = 忙碌(GD32 按铃下拉 或 ESP32 checkGd32Request 期间下拉) + * v2 DATA_READY 语义:PB6 低 = 有待发数据,高 = 队列空。 + * ESP32 不再操作 PB6,无独立「完成」信号;按铃完成由 ESP32 + * 的 CS 读事务本身确认(onCsRelease 按铃响应路径调 onEsp32Complete)。 * - * 状态流转: - * - canRingNow()=true(初始/ESP32 完成后)→ requestEsp32Read() → canRingNow()=false - * - ESP32 releaseReady 上升沿 → onEsp32Complete() → canRingNow()=true - * - 按铃超时 → onRingTimeout() → canRingNow()=true(允许重试) + * canRingNow() 总是返回 true,由 requestEsp32Read() 的 m_ringPending + * 检查控制重复按铃。 */ class SpiLinkState { public: - /** 是否允许发起新按铃(ESP32 已完成上一帧 或 初始态)。 */ - bool canRingNow() const { return m_esp32Ready; } + /** 是否允许发起新按铃。 + * v2 DATA_READY: ESP32 不再操作 PB6,无独立完成信号; + * 总是允许,由 requestEsp32Read() 的 m_ringPending 检查控制重复。 */ + bool canRingNow() const { return true; } - /** 发起按铃时调用(消费完成 flag)。 */ + /** 发起按铃时调用(v2 下仅保留兼容写法,无实际门禁作用)。 */ void onRingStart() { m_esp32Ready = false; } - /** ESP32 releaseReady 上升沿触发(EXTI6 ISR 调用)。 */ + /** ESP32 CS 读事务完成确认(onCsRelease 按铃响应路径调用)。 */ void onEsp32Complete() { m_esp32Ready = true; } /** 按铃超时取消时调用(允许重试,不依赖 EXTI)。 */ @@ -48,14 +48,14 @@ private: * 具体命令处理逻辑不再放在这里,而是拆到 device/spi/app_spi_cmd_*.cpp, * 通过 registerHandler() 注册进来,保持本模块可扩展。 * - * 握手架构(线与 SPIready,PB6<->ESP32 GPIO10 开漏单线): - * - 空闲态线 HIGH(双方释放,靠上拉)。 - * - ESP32 主动发起:下拉 SPIready -> 立刻 CS 发请求 -> CS 释放后保持下拉, - * GD32 在 onCsRelease() ISR 内完成帧处理+响应装载后释放 SPIready(高), - * ESP32 检测到上升沿后发起 CS#2 读响应。 - * - GD32 主动按铃:下拉 SPIready -> ESP32 下降沿中断唤醒后发起读事务, - * GD32 在 onCsRelease() 中装入请求帧并释放 SPIready。 + * 握手架构(v2 DATA_READY,PB6<->ESP32 GPIO10 开漏单线): + * - PB6 低 = 有待发数据(m_txLen>0 或 m_ringPending),高 = 队列空。 + * - ESP32 主动发起:CS 发请求,CS 释放后 GD32 在 onCsRelease() ISR 内 + * 完成帧处理+响应装载,由 updateDataReady() 据 m_txLen 驱动 PB6。 + * - GD32 主动按铃:requestEsp32Read() 设 m_ringPending=true 且 m_txLen>0, + * updateDataReady() 拉低 PB6,ESP32 检测下降沿后发起读事务读走请求帧。 * - PB6 配置为开漏输出,setSpiReady(true)=高阻释放,setSpiReady(false)=下拉。 + * - ESP32 不再操作 PB6,EXTI6 上升沿仅清标志,无业务含义。 */ class AppSpiCommand { public: @@ -71,7 +71,8 @@ public: /** @brief CS 边沿中断处理入口。 */ void onCsExtiInterrupt(); - /** @brief SPI Ready(PB6) EXTI 上升沿中断处理入口(ESP32 完成信号)。 */ + /** @brief SPI Ready(PB6) EXTI6 中断入口。 + * v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 仅清标志。 */ void onSpiReadyExti(); /** @@ -120,6 +121,10 @@ public: /** @brief 查询当前是否有未读按铃(ESP32 尚未来读)。 */ bool isRingPending() const { return m_ringPending; } + /** @brief 取消未完成的按铃(释放 SPIready、清 m_ringPending)。 + * 0xB0 进 BOOT 等路径需公开调用,避免 0xC1 残留盖住 ACK。 */ + void cancelRing(); + /** @brief 查询 ESP32 是否已完成上一帧处理(SPIready 上升沿触发)。 * 初始为 true 允许第一次按铃;每次 requestEsp32Read 后置 false, * ESP32 releaseReady 产生上升沿后由 EXTI 置 true。 */ @@ -172,8 +177,10 @@ private: */ void setSpiReady(bool ready); - /** @brief 取消未完成的按铃(释放 SPIready、清 m_ringPending)。 */ - void cancelRing(); + /** @brief v2 DATA_READY 语义:依据待发数据驱动 PB6。 + * PB6 低 = 有待发数据(m_txLen>0 或 m_ringPending),高 = 队列空。 + * 替代旧 BUSY/READY 语义中所有 setSpiReady 调用。 */ + void updateDataReady(); }; /** diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h b/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h index 4cf2c2c..af0894d 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h +++ b/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h @@ -14,7 +14,9 @@ namespace SpiDefs { -static constexpr uint8_t kSof = 0xA5U; +/* 链路层帧类型(v2) */ +static constexpr uint8_t kTypeLinkHello = 0x00U; +static constexpr uint8_t kTypeLinkError = 0x7FU; /* 帧类型 */ static constexpr uint8_t kTypeOtaReq = 0x01U; @@ -32,8 +34,6 @@ static constexpr uint8_t kRspAck = 0x80U; static constexpr uint8_t kRspWait = 0x81U; static constexpr uint8_t kRspError = 0x82U; -static constexpr uint16_t kFrameOverhead = 5U; - } // namespace SpiDefs #endif /* DEVICE_APP_SPI_COMMAND_DEFS_H */ diff --git a/GD32E508_DCU_APP/sys/ota_boot_request.cpp b/GD32E508_DCU_APP/sys/ota_boot_request.cpp index a71a84b..166ce59 100644 --- a/GD32E508_DCU_APP/sys/ota_boot_request.cpp +++ b/GD32E508_DCU_APP/sys/ota_boot_request.cpp @@ -90,6 +90,7 @@ static bool requestMagicValid(const uint8_t *data, uint8_t length) /** @brief 已写入 OTA 标志并等待软复位的挂起标志。 */ volatile bool OtaBootRequest::s_rebootPending = false; volatile bool OtaBootRequest::s_bootRequestPending = false; +volatile bool OtaBootRequest::s_spiFrozenForBoot = false; /** * @brief 初始化进入 Bootloader 请求模块的运行态。 @@ -98,6 +99,7 @@ void OtaBootRequest::init() { s_rebootPending = false; s_bootRequestPending = false; + s_spiFrozenForBoot = false; } /** @@ -115,6 +117,9 @@ bool OtaBootRequest::requestEnterBoot() if(ok) { s_rebootPending = true; + } else { + /* 写配置页失败则永远停在 APP,ESP32 会看到持续 role=1 */ + /* 无 printf 依赖:由调用方 process() 恢复 SPI 后继续运行 */ } return ok; } @@ -127,6 +132,9 @@ bool OtaBootRequest::requestEnterBoot() void OtaBootRequest::requestEnterBootAsync() { s_bootRequestPending = true; + /* 立即冻结 SPI 业务:不必等 process()(原依赖 1Hz 心跳,最坏拖 ~1s)。 + * 否则 ESP32 紧跟着 HELLO/0xC1 会继续进 processCommand,干扰擦页/复位。 */ + s_spiFrozenForBoot = true; } /** @@ -157,13 +165,25 @@ bool OtaBootRequest::handleCanFrame(uint8_t channel, uint32_t id, const uint8_t */ void OtaBootRequest::process() { - /* 异步处理 0xB0 请求:在主循环执行 Flash 操作(20-30ms),不阻塞 SPI ISR */ + /* 异步处理 0xB0:主循环写 Flash(可能擦页 20–30ms)。 + * 期间冻结 SPI 业务处理,避免 ESP32 紧跟着 HELLO/按铃读 打断擦写。 */ if(s_bootRequestPending) { s_bootRequestPending = false; + s_spiFrozenForBoot = true; + nvic_irq_disable(SPI0_IRQn); + nvic_irq_disable(EXTI10_15_IRQn); + nvic_irq_disable(EXTI5_9_IRQn); + const bool ok = requestEnterBoot(); - if(ok) { - s_rebootPending = true; + if(!ok) { + s_spiFrozenForBoot = false; + nvic_irq_enable(SPI0_IRQn, 2U, 0U); + nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U); + nvic_irq_enable(EXTI5_9_IRQn, CONFIG_IRQ_PREEMPT_SPI_USART, 1U); + /* requestEnterBoot 失败时 s_rebootPending 仍为 false */ + return; } + s_rebootPending = true; } if(!s_rebootPending) { @@ -171,13 +191,14 @@ void OtaBootRequest::process() } s_rebootPending = false; + s_spiFrozenForBoot = true; + nvic_irq_disable(SPI0_IRQn); + nvic_irq_disable(EXTI10_15_IRQn); + nvic_irq_disable(EXTI5_9_IRQn); - /* 方案A: 拉低 PB6 通知 ESP32 "即将复位"。 - * ESP32 通过 LOW→HIGH 序列识别 Bootloader 就绪: - * APP 拉低 → 复位(μs级HIGH) → Bootloader极早拉低 → init完成释放HIGH - * 20ms 延时确保 ESP32 5ms 轮询能捕获到 LOW 信号后再复位。 */ - gpio_bit_reset(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); - + /* v2 DATA_READY: Boot 切换不再用 PB6 通知对端, + * BOOT 起机后再由 HELLO role=boot 确认。 */ + /* SysTick 中断仍开,delayMs 可用;SPI 相关 IRQ 已关。 */ SysTickTimer::delayMs(kRebootDelayMs); __disable_irq(); NVIC_SystemReset(); diff --git a/GD32E508_DCU_APP/sys/ota_boot_request.h b/GD32E508_DCU_APP/sys/ota_boot_request.h index e41d4d5..238e163 100644 --- a/GD32E508_DCU_APP/sys/ota_boot_request.h +++ b/GD32E508_DCU_APP/sys/ota_boot_request.h @@ -42,13 +42,18 @@ public: /** @brief 若已确认进入 BOOT 请求,则延迟片刻后复位。 */ static void process(); - /** @brief 查询是否已收到进入 Bootloader 请求(等待复位中)。 - * 供主循环在复位前停止主动按铃等可能干扰 OTA 的操作。 */ - static bool isRebootPending() { return s_rebootPending; } + /** @brief 查询是否已进入“即将进 BOOT”路径(异步 pending 或已写标志待复位)。 + * 供 CAN 按铃等模块在复位前停止干扰 SPI 的操作。 */ + static bool isRebootPending() { return s_rebootPending || s_bootRequestPending; } + + /** @brief SPI 命令路径查询:true 时忽略业务帧,仅释放 ready。 */ + static bool isSpiFrozenForBoot() { return s_spiFrozenForBoot; } private: static volatile bool s_rebootPending; static volatile bool s_bootRequestPending; ///< 0xB0 异步请求标志,process() 中执行 Flash 操作 + /** 写 OTA 配置页期间冻结 SPI 业务,避免 Flash 擦写与 SPI ISR 互踩。 */ + static volatile bool s_spiFrozenForBoot; }; #endif /* SYS_OTA_BOOT_REQUEST_H */ diff --git a/GD32E508_DCU_APP/sys/task_runner.cpp b/GD32E508_DCU_APP/sys/task_runner.cpp index 69558a5..a176bcd 100644 --- a/GD32E508_DCU_APP/sys/task_runner.cpp +++ b/GD32E508_DCU_APP/sys/task_runner.cpp @@ -367,6 +367,12 @@ void TaskRunner::runPending() * 放在 canFeedbackPushStep() 前确保卡死的按铃先被清掉再推送下一帧。 */ AppSpiCommandBus.poll(); + /* 0xB0 进 BOOT:必须每圈检查,不能只靠 1Hz TASK_HEARTBEAT。 + * 否则 Flash 写/复位被拖到下一秒,ESP32 HELLO 一直 role=app 超时。 */ + if (OtaBootRequest::isRebootPending()) { + OtaBootRequest::process(); + } + /* CAN 反馈主动推送:GD32 APP 循环推送 3 通道 × 2 页反馈帧到 ESP32, * 替代 ESP32 主动轮询。非阻塞:上一帧未读走时立即返回。 * 每次主循环迭代调用一次,ESP32 读走一帧后 CS 释放中断唤醒主循环继续推送。 */