/** * @file app_spi_command.cpp * @brief APP 运行期 SPI 链路与命令路由实现。 * * 本模块现在只负责: * - SPI0 从机链路初始化(GPIO/SPI/DMA/EXTI) * - CS、DMA、中断驱动的帧接收与发送 * - 公共帧格式解析与应答构建 * - 业务命令的注册与分发 * * 具体命令处理逻辑已经拆到独立的 app_spi_cmd_*.cpp, * 通过 registerHandler() 注册进来,后续新增 SPI 业务时 * 只需要新增命令处理器,不再继续膨胀本文件。 * * 握手架构(v2 DATA_READY,PB6<->ESP32 GPIO10 开漏单线): * - PB6 低 = 有待发数据(m_txLen>0 或 m_ringPending),高 = 队列空。 * - 命令处理在 onCsRelease() EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame), * 完成后由 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" } #include "gd32e50x_gpio.h" #include "gd32e50x_misc.h" #include "gd32e50x_rcu.h" #include "gd32e50x_dma.h" #include "gd32e50x_spi.h" #include "sys/config.h" #include "sys/critical.h" #include "sys/task_manager.h" #include "sys/systick.h" #include "main.h" #include #include /* ============================================================ * 本地别名:保持当前实现文件内部风格不变 * ============================================================ */ static constexpr uint8_t kTypeBootReq = SpiDefs::kTypeBootReq; static constexpr uint8_t kTypeCanSend = SpiDefs::kTypeCanSend; static constexpr uint8_t kTypeOtaReq = SpiDefs::kTypeOtaReq; static constexpr uint8_t kTypeAppPing = SpiDefs::kTypeAppPing; static constexpr uint8_t kTypeOtaData = SpiDefs::kTypeOtaData; 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; /* DMA 配置(与 BOOT SpiOta 一致:DMA0_CH2 用于 SPI0 TX) */ #define SPI_TX_DMA_PERIPH DMA0 #define SPI_TX_DMA_CH DMA_CH2 static void startSpiTxDma(const uint8_t *buf, uint16_t len) { DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; DMA_INTC(SPI_TX_DMA_PERIPH) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE, SPI_TX_DMA_CH); DMA_CHMADDR(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = (uint32_t)buf; DMA_CHCNT(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = len; DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) |= DMA_CHXCTL_CHEN; } static bool isAllBytesValue(const uint8_t *data, uint16_t len, uint8_t value) { if (!data || len == 0U) { return false; } for (uint16_t i = 0U; i < len; ++i) { if (data[i] != value) { return false; } } return true; } static bool shouldIgnoreTransaction(const uint8_t *data, uint16_t len) { if (!data || len == 0U) { return true; } if (isAllBytesValue(data, len, 0xFFU) || isAllBytesValue(data, len, 0x00U)) { return true; } /* v2 协议帧最小长度为 V2_OVERHEAD(20B)。 * 短于该长度的事务只能是启动切换期的残包,直接忽略。 * (0xB0 进 BOOT 现已走标准 v2 帧 handler,不再有裸字节通道。) */ return (len < V2_OVERHEAD); } static bool isFlashRelatedFrame(uint8_t frame_type) { switch (frame_type) { case kTypeOtaReq: case kTypeOtaData: case kTypeOtaCancel: case kTypeBootReq: return true; default: return false; } } /* GD32 APP 侧同步状态机(ISR 与主循环只读/原子写,避免临界区开销)。 */ static volatile uint8_t g_localCredit = 8U; static volatile uint8_t g_remoteCredit = 8U; /* 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; /* 响应缓存:用于重复请求重发同一响应(避免业务 handler 重复执行)。 */ static uint8_t s_lastReqSeq = 0xFF; static uint8_t s_lastRespBuf[256]; static uint16_t s_lastRespLen = 0; static bool s_hasCachedResp = false; /* ============================================================ * v2 帧构建 / 帧解析(委托 spi_link_v2.h,三工程共用) * ============================================================ */ /* 构建响应帧;上层接口(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; } if ((uint32_t)V2_OVERHEAD + payload_len > AppSpiCommand::kTxBufSize) { return false; } 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; } /* 解析 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, v2_header_t *out_hdr = nullptr) { if (!frame || !type || !payload || !payload_len) { return false; } v2_header_t hdr; const uint8_t *pload = nullptr; const bool ok = v2_parse_frame(frame, frame_len, &hdr, &pload); if (!ok) { return false; } g_remoteCredit = hdr.credit; *type = hdr.type; *payload = pload; *payload_len = hdr.payload_len; if (out_hdr) { *out_hdr = hdr; } return true; } /* ============================================================ * 调试日志辅助 * ============================================================ */ void AppSpiCommand::postLog(const char *msg) { #if (CONFIG_SPI_APP_LOG_ENABLE != 0) if (!msg) return; snprintf(m_logBuf, kLogBufSize, "[SPI-APP] %s", msg); m_logPending = true; #else (void)msg; #endif } void AppSpiCommand::postLogFmt(const char *fmt, uint8_t val) { #if (CONFIG_SPI_APP_LOG_ENABLE != 0) if (!fmt) return; snprintf(m_logBuf, kLogBufSize, fmt, val); m_logPending = true; #else (void)fmt; (void)val; #endif } void AppSpiCommand::buildResponse(uint8_t *tx_buf, uint16_t tx_buf_size, uint16_t *tx_len, uint8_t rsp_type, const uint8_t *payload, uint16_t payload_len) { if (!tx_buf || !tx_len || tx_buf_size == 0U) { return; } uint16_t built_len = 0U; const bool ok = spi_build_frame(rsp_type, payload, payload_len, tx_buf, &built_len); if (ok) { *tx_len = built_len; } } /* ============================================================ * 硬件配置 * ============================================================ */ void AppSpiCommand::configureGpio() { rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_AF); gpio_pin_remap_config(GPIO_SPI0_REMAP, ENABLE); gpio_pin_remap_config(GPIO_SWJ_SWDPENABLE_REMAP, ENABLE); gpio_init(GPIOB, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_3 | GPIO_PIN_5); 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 复用为 DATA_READY 握手线(开漏输出,线与)。 * 开漏模式:写 BRR = 下拉驱动低;写 BSRR = 高阻释放,靠外部/对端上拉拉高。 * 语义:低 = 有待发数据,高 = 队列空;默认释放(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); } void AppSpiCommand::setSpiReady(bool ready) { /* 开漏线与:ready=true 释放(BSRR,高阻,靠上拉拉高); * ready=false 下拉(BRR,驱动低)。 */ if (ready) { gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); } else { gpio_bit_reset(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); } } /* v2 DATA_READY: PB6 低 = 有待发数据,高 = 队列空。 * L5:同步 g_localCredit(单缓冲 0/1)。 */ void AppSpiCommand::updateDataReady() { const bool has_data = (m_txLen > 0U) || m_ringPending; setSpiReady(!has_data); g_localCredit = has_data ? 0U : 1U; } void AppSpiCommand::configureSpi0() { rcu_periph_clock_enable(RCU_SPI0); spi_i2s_deinit(SPI0); spi_parameter_struct spi_init_struct; spi_struct_para_init(&spi_init_struct); spi_init_struct.device_mode = SPI_SLAVE; spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX; spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; spi_init_struct.nss = SPI_NSS_SOFT; spi_init_struct.endian = SPI_ENDIAN_MSB; spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE; spi_init_struct.prescale = SPI_PSC_8; spi_init(SPI0, &spi_init_struct); spi_nss_internal_low(SPI0); (void)spi_i2s_data_receive(SPI0); spi_i2s_data_transmit(SPI0, 0xFFU); 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, 2U, 0U); spi_enable(SPI0); } void AppSpiCommand::configureDma() { rcu_periph_clock_enable(RCU_DMA0); dma_deinit(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); dma_parameter_struct dma_cfg; dma_struct_para_init(&dma_cfg); dma_cfg.periph_addr = (uint32_t)(SPI0 + 0x0CU); /* SPI0_DATA 寄存器 */ dma_cfg.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; dma_cfg.memory_addr = (uint32_t)m_txBuf; dma_cfg.memory_width = DMA_MEMORY_WIDTH_8BIT; dma_cfg.number = 0U; dma_cfg.priority = DMA_PRIORITY_HIGH; dma_cfg.periph_inc = DMA_PERIPH_INCREASE_DISABLE; dma_cfg.memory_inc = DMA_MEMORY_INCREASE_ENABLE; dma_cfg.direction = DMA_MEMORY_TO_PERIPHERAL; dma_init(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH, &dma_cfg); dma_circulation_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); dma_channel_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); } void AppSpiCommand::configureExti() { gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15); 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, 1U, 1U); } void AppSpiCommand::configureSpiReadyExti() { /* 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); exti_interrupt_flag_clear(EXTI_6); nvic_irq_enable(EXTI5_9_IRQn, CONFIG_IRQ_PREEMPT_SPI_USART, 1U); } void AppSpiCommand::onSpiReadyExti() { /* v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 上升沿不再有意义。 * 清除中断标志避免反复触发。 */ if (exti_interrupt_flag_get(EXTI_6) != RESET) { exti_interrupt_flag_clear(EXTI_6); } } /* ============================================================ * 默认命令注册入口(由各 app_spi_cmd_*.cpp 提供) * ============================================================ */ extern void registerSpiBootRequestCommand(AppSpiCommand &bus); extern void registerSpiOtaWaitCommand(AppSpiCommand &bus); extern void registerSpiAppPingCommand(AppSpiCommand &bus); extern void registerSpiCanBridgeCommand(AppSpiCommand &bus); extern void registerSpiCanFeedbackCommand(AppSpiCommand &bus); extern void registerSpiIdSnapshotCommand(AppSpiCommand &bus); AppSpiCommand AppSpiCommandBus; void AppSpiCommand::init() { m_csActive = false; m_rxLen = 0U; m_txLen = 0U; m_txPos = 0U; m_logPending = false; m_txActiveThisCs = false; m_commandCount = 0U; m_ringPending = false; m_ringDeadline = 0U; m_ringTxLen = 0U; m_linkState.reset(); memset(m_txBuf, 0xFF, sizeof(m_txBuf)); memset(m_logBuf, 0, sizeof(m_logBuf)); memset(m_commands, 0, sizeof(m_commands)); registerSpiBootRequestCommand(AppSpiCommandBus); registerSpiOtaWaitCommand(AppSpiCommandBus); registerSpiAppPingCommand(AppSpiCommandBus); registerSpiCanBridgeCommand(AppSpiCommandBus); registerSpiCanFeedbackCommand(AppSpiCommandBus); registerSpiIdSnapshotCommand(AppSpiCommandBus); configureGpio(); configureSpi0(); configureDma(); configureExti(); configureSpiReadyExti(); } /* ============================================================ * 命令路由 * ============================================================ */ void AppSpiCommand::registerHandler(ISpiCommandHandler *handler) { if (!handler || m_commandCount >= kMaxCommands) { return; } m_commands[m_commandCount++] = handler; } void AppSpiCommand::processCommand() { if (m_rxLen == 0U) { 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, &hdr)) { /* 无法解析为合法 v2 帧:构建 ERROR 响应 */ postLogFmt("[SPI-APP] 非v2帧 0x%02X -> ERROR", m_rxBuf[0]); 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) { /* 置位 TASK_SPI_LOG,唤醒主循环立刻打印日志 */ TaskManager::set(1UL << TASK_SPI_LOG); } /* 应答帧已就绪:由 updateDataReady() 依据 m_txLen 驱动 PB6(有待发=低)。 * 命令处理已在 ISR 内同步完成,无需 m_cmdPending 推迟机制。 */ /* 有响应待发时,设置按铃标志阻止主循环 canFeedbackPushStep() 覆盖 m_txBuf。 * ESP32 下次 CS assert 时 DMA 输出响应,onCsRelease 清除 m_ringPending。 * 50ms 超时后 cancelRing() 清除,恢复 CAN 反馈推送。 */ if (m_txLen > 0U) { m_ringPending = true; m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; } updateDataReady(); } /* ============================================================ * GD32 主动按铃:设 m_ringPending + updateDataReady() 拉低 PB6 请求 ESP32 来读 * ============================================================ */ void AppSpiCommand::cancelRing() { m_ringPending = false; m_ringTxLen = 0U; /* 清除待发响应,防止超时后残留帧在下一次 CS assert 时被错误输出 */ m_txLen = 0U; m_txPos = 0U; /* 超时后允许重试,不依赖 ESP32 releaseReady 上升沿 */ m_linkState.onRingTimeout(); /* 释放 SPIready(高阻),避免永久占用线。 * cancelRing 已清 m_txLen 和 m_ringPending,updateDataReady() 会释放为高。 */ updateDataReady(); } bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len) { /* 若上一次按铃尚未被 ESP32 读走,拒绝重复按铃 */ if (m_ringPending) { return false; } /* 构建 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; /* L5:单缓冲 — 装载前 credit 报 1,装载后 updateDataReady 置 0 */ g_localCredit = 1U; 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 = (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(); /* v2 DATA_READY: 已设 m_ringPending=true 且 m_txLen>0, * updateDataReady() 据此拉低 PB6,并置 g_localCredit=0。 */ updateDataReady(); return true; } /* ============================================================ * 主循环轮询:打印延迟日志 + 按铃超时看护 * ============================================================ */ void AppSpiCommand::poll() { /* 打印 ISR 推迟的日志 */ char log_copy[kLogBufSize] = {}; bool has_log = false; uint32_t save = RT_CRITICAL_SAVE(); if (m_logPending) { memcpy(log_copy, m_logBuf, sizeof(log_copy)); m_logPending = false; has_log = true; } RT_CRITICAL_RESTORE(save); if (has_log) { printf("%s\r\n", log_copy); } /* 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。 * 防止 ESP32 不响应时永久占用 SPIready 线导致双方死锁。 */ if (m_ringPending) { uint32_t save2 = RT_CRITICAL_SAVE(); if (m_ringPending) { if ((int32_t)(SysTickTimer::getMs() - m_ringDeadline) >= 0) { printf("[SPI-APP] 按铃超时取消 (ESP32 未响应, 释放 SPIready 重试)\r\n"); cancelRing(); } } RT_CRITICAL_RESTORE(save2); } } /* ============================================================ * CS 边沿处理 * ============================================================ */ void AppSpiCommand::onCsAssert() { if (m_csActive) { return; } m_csActive = true; /* v2 DATA_READY: CS 本身即互斥,PB6 不再表示「忙碌」,不在 CS 下降沿拉低。 * PB6 仅由 updateDataReady() 依据待发数据驱动,避免与队列语义冲突。 */ m_rxLen = 0U; m_txPos = 0U; m_txActiveThisCs = false; /* 停止 DMA 并清理旧状态 */ DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); /* 与 BOOT 一致:清除 SPI 双缓冲中的残留数据 */ spi_i2s_data_transmit(SPI0, 0xFFU); if (spi_i2s_flag_get(SPI0, SPI_FLAG_TBE) != RESET) { spi_i2s_data_transmit(SPI0, 0xFFU); } /* 清除错误标志 */ if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { (void)spi_i2s_data_receive(SPI0); } if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); } /* 启动 DMA 发送响应帧(与 BOOT 完全一致) */ if (m_txLen > 0U) { m_txActiveThisCs = true; spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); startSpiTxDma(m_txBuf, m_txLen); } else { /* 无响应时发送全 0xFF 填充 */ spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); startSpiTxDma(m_txBuf, kTxBufSize); } } void AppSpiCommand::onCsRelease() { if (!m_csActive) { return; } m_csActive = false; /* 停止 DMA */ DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) { uint8_t last_byte = (uint8_t)(spi_i2s_data_receive(SPI0) & 0xFFU); if (m_rxLen < sizeof(m_rxBuf)) { m_rxBuf[m_rxLen++] = last_byte; } } /* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧, * 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。 * * 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 v2 协议帧(如 PING/OTA/0xB0)。 * 若 GD32 正在按铃时 ESP32 发协议帧,会因 m_ringPending=true 被误吞, * 导致命令无法处理。检测到 v2 SOF 时取消按铃并走正常命令处理。 */ if (m_ringPending) { /* 判断本次收到的数据是否为 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() 重新装载对应应答。 * processCommand() 末尾由 updateDataReady() 依据新 m_txLen 驱动 PB6。 */ m_ringPending = false; m_ringTxLen = 0U; m_txLen = 0U; m_txPos = 0U; memset(m_txBuf, 0xFF, sizeof(m_txBuf)); /* 继续走 shouldIgnoreTransaction + processCommand(),不 return */ } else { /* 正常按铃响应:请求帧已被读走,取消按铃状态 */ m_ringPending = false; m_ringTxLen = 0U; /* 请求帧已被读走,清空 TX 防止下次事务重复输出 */ m_txLen = 0U; m_txPos = 0U; memset(m_txBuf, 0xFF, sizeof(m_txBuf)); /* v2 DATA_READY: 已清 m_txLen=0 与 m_ringPending=false, * updateDataReady() 释放为高;ESP32 完成 CS 即视为已读走确认。 */ m_linkState.onEsp32Complete(); updateDataReady(); /* 上层若需要 ESP32 处理该请求帧,由 ESP32 侧读到的帧 TYPE 决定后续动作 */ return; } } if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) { m_rxLen = 0U; if (m_txActiveThisCs) { m_txLen = 0U; m_txPos = 0U; memset(m_txBuf, 0xFF, sizeof(m_txBuf)); } m_txActiveThisCs = false; /* v2 DATA_READY: shouldIgnoreTransaction 路径已清 m_txLen=0, * updateDataReady() 释放为高,避免 PB6 卡 LOW。 */ updateDataReady(); return; } m_txLen = 0U; m_txPos = 0U; m_txActiveThisCs = false; memset(m_txBuf, 0xFF, sizeof(m_txBuf)); /* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式), * 消除主循环延迟。处理完成后 processCommand() 末尾释放 SPIready。 */ processCommand(); } /* ============================================================ * SPI0 / EXTI 中断处理 * ============================================================ */ void AppSpiCommand::onInterrupt() { if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) { if (!m_csActive && gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) { onCsAssert(); } uint8_t byte = (uint8_t)(spi_i2s_data_receive(SPI0) & 0xFFU); if (m_csActive && m_rxLen < sizeof(m_rxBuf)) { m_rxBuf[m_rxLen++] = byte; } } /* TBE 已由 DMA 替代,不再处理 */ if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { /* 读取并丢弃 overrun 字节以清除标志,但保留已收到的帧数据。 * 丢失的字节会导致 CRC 校验失败,由 spi_try_parse_frame 优雅处理 * (返回 false -> processCommand 构建 ERROR 响应), * 而不是重置 m_rxLen 导致整帧丢失。 */ (void)spi_i2s_data_receive(SPI0); } if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); } } void AppSpiCommand::onCsExtiInterrupt() { if (exti_interrupt_flag_get(EXTI_15) != RESET) { exti_interrupt_flag_clear(EXTI_15); if (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) { onCsAssert(); } else { onCsRelease(); } } } extern "C" void SPI0_IRQHandler(void) { AppSpiCommandBus.onInterrupt(); } extern "C" void EXTI10_15_IRQHandler(void) { AppSpiCommandBus.onCsExtiInterrupt(); } extern "C" void EXTI5_9_IRQHandler(void) { AppSpiCommandBus.onSpiReadyExti(); }