/** * @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 "device/spi/app_spi_diag.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/systick.h" #include "sys/cpu_load.h" #include "sys/esp32_en.h" #include #include /* ============================================================ * 本地别名:保持当前实现文件内部风格不变 * ============================================================ */ static constexpr uint8_t kTypeBootReq = SpiDefs::kTypeBootReq; static constexpr uint8_t kTypeOtaReq = SpiDefs::kTypeOtaReq; static constexpr uint8_t kTypeOtaData = SpiDefs::kTypeOtaData; static constexpr uint8_t kTypeOtaCancel = SpiDefs::kTypeOtaCancel; static constexpr uint8_t kRspError = SpiDefs::kRspError; /* DMA 全双工配置(与 BOOT SpiOta 一致): * DMA0_CH1: SPI0_RX DMA0_CH2: SPI0_TX * RX 走 DMA 后禁用 RBNE 逐字节中断,避免 256B 事务 RX-OVR/SOF 丢失。 */ #define SPI_TX_DMA_PERIPH DMA0 #define SPI_TX_DMA_CH DMA_CH2 #define SPI_RX_DMA_PERIPH DMA0 #define SPI_RX_DMA_CH DMA_CH1 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; } /** @brief 启动 DMA 接收 SPI0_DATA → m_rxBuf(容量 kRxBufSize)。 * 每次 CS 空闲期预装,下一笔 CS 上升沿时由 stopSpiDmas() 读 CHCNT 反算实际长度。 */ static void startSpiRxDma(uint8_t *rx_buf, uint16_t rx_cap) { DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; DMA_INTC(SPI_RX_DMA_PERIPH) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE, SPI_RX_DMA_CH); DMA_CHMADDR(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) = (uint32_t)rx_buf; DMA_CHCNT(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) = rx_cap; DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) |= DMA_CHXCTL_CHEN; } /** @brief 清空 SPI 接收通路残留(含 OVR),对齐 BOOT drainSpiRxHw。 */ static void drainSpiRxHw(void) { while (spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE) != RESET) { (void)spi_i2s_data_receive(SPI0); } if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { (void)spi_i2s_data_receive(SPI0); (void)SPI_STAT(SPI0); } } /** @brief 停止双 DMA;返回本 CS 实际 RX 字节数(必须在 disable 前读 CHCNT)。 * 对齐 BOOT stopSpiDmas:先读 remaining 再 disable,否则计数器被清。 */ static uint16_t stopSpiDmas(uint16_t rx_cap) { const uint16_t remaining = (uint16_t)DMA_CHCNT(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); spi_dma_disable(SPI0, SPI_DMA_RECEIVE); uint16_t got = 0U; if (remaining <= rx_cap) { got = (uint16_t)(rx_cap - remaining); } return got; } 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 侧 CREDIT:运行时单响应缓冲 0/1(有待发=0,空闲=1)。 * 初值必须为 1,禁止用 BOOT 的 8 槽语义;否则 HELLO 前被读到会误导主机 remoteCredit。 */ static volatile uint8_t g_localCredit = 1U; /* 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; /* ============================================================ * SPI 诊断统计与现场快照 * ============================================================ */ #if (CONFIG_SPI_DIAG_ENABLE != 0) #define SPI_DIAG_RECORD(...) do { __VA_ARGS__; } while (0) static SpiDiagStats s_diag = {}; const SpiDiagStats& getSpiDiagStats() { s_diag.current_local_credit = (uint8_t)g_localCredit; s_diag.expected_rx_seq = g_expectedRxSeq; return s_diag; } void resetSpiDiagStats() { memset(&s_diag, 0, sizeof(s_diag)); } const char* spiParseResultToString(SpiParseResult res) { switch (res) { case SPI_PARSE_OK: return "OK"; case SPI_PARSE_ERR_TOO_SHORT: return "TOO_SHORT"; case SPI_PARSE_ERR_ALL_FF: return "ALL_FF"; case SPI_PARSE_ERR_ALL_00: return "ALL_00"; case SPI_PARSE_ERR_NO_SOF: return "NO_SOF"; case SPI_PARSE_ERR_BAD_VERSION: return "BAD_VERSION"; case SPI_PARSE_ERR_BAD_LEN: return "BAD_LEN"; case SPI_PARSE_ERR_CRC_MISMATCH: return "CRC_MISMATCH"; default: return "UNKNOWN"; } } #else #define SPI_DIAG_RECORD(...) ((void)0) #endif bool AppSpiCommand::isV2Active() const { return g_v2Active; } uint32_t AppSpiCommand::getSessionId() const { return g_sessionId; } uint8_t AppSpiCommand::getLocalCredit() const { return (uint8_t)g_localCredit; } void AppSpiCommand::dumpDiag() const { dumpSpiDiag(); } #if (CONFIG_SPI_DIAG_ENABLE != 0) void dumpSpiDiag() { const SpiDiagStats &st = getSpiDiagStats(); const uint32_t now = SysTickTimer::getMs(); const uint8_t cs_pin = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U; const uint8_t rdy_pin = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U; const uint16_t stat = (uint16_t)SPI_STAT(SPI0); printf("\r\n==================== [GD32 SPI0 LINK DIAGNOSTICS] ====================\r\n"); printf("Status : v2_active=%d session_id=0x%08lX local_credit=%u uptime=%lums\r\n", AppSpiCommandBus.isV2Active() ? 1 : 0, (unsigned long)AppSpiCommandBus.getSessionId(), (unsigned)AppSpiCommandBus.getLocalCredit(), (unsigned long)now); printf("Pins & HW: CS(PA15)=%u DATA_READY(PB6)=%u ring_pending=%d cs_active=%d\r\n", cs_pin, rdy_pin, AppSpiCommandBus.isRingPending() ? 1 : 0, AppSpiCommandBus.isCsActive() ? 1 : 0); printf("SPI_STAT : 0x%04X (TRANS:%d TBE:%d RBNE:%d OVR:%d FERR:%d)\r\n", stat, (stat & SPI_FLAG_TRANS) ? 1 : 0, (stat & SPI_FLAG_TBE) ? 1 : 0, (stat & SPI_FLAG_RBNE) ? 1 : 0, (stat & SPI_FLAG_RXORERR) ? 1 : 0, (stat & SPI_FLAG_FERR) ? 1 : 0); printf("CS Stats : assert=%lu release=%lu missed=%lu short(<20B)=%lu zero_len=%lu\r\n", (unsigned long)st.total_cs_assert, (unsigned long)st.total_cs_release, (unsigned long)st.missed_cs_assert, (unsigned long)st.short_rx_len_count, (unsigned long)st.zero_rx_len_count); printf("Frames : valid_v2=%lu parse_fail=%lu all_FF=%lu all_00=%lu\r\n", (unsigned long)st.valid_v2_frames, (unsigned long)st.parse_fail_count, (unsigned long)st.all_ff_count, (unsigned long)st.all_00_count); printf("ParseErr : CRC_err=%lu no_SOF=%lu SOF_drift=%lu bad_ver=%lu bad_len=%lu\r\n", (unsigned long)st.err_crc_mismatch, (unsigned long)st.err_no_sof, (unsigned long)st.err_sof_drift, (unsigned long)st.err_bad_version, (unsigned long)st.err_bad_len); printf("Hw Errors: RXORERR(ovr)=%lu FERR(fmt)=%lu\r\n", (unsigned long)st.rx_overrun_count, (unsigned long)st.format_error_count); printf("Session : HELLO_cnt=%lu mismatch=%lu seq_ok=%lu seq_jump=%lu seq_dup=%lu\r\n", (unsigned long)st.hello_count, (unsigned long)st.session_mismatch, (unsigned long)st.seq_ok_count, (unsigned long)st.seq_jump_count, (unsigned long)st.seq_dup_count); printf("Sequences: last_rx_seq=0x%02X exp_rx_seq=0x%02X last_tx_seq=0x%02X\r\n", st.last_rx_seq, st.expected_rx_seq, st.last_tx_seq); printf("Commands : last_rx_type=0x%02X last_tx_type=0x%02X error_rsp(0x82)=%lu (last_code=0x%02X)\r\n", st.last_rx_type, st.last_tx_type, (unsigned long)st.error_rsp_count, st.last_error_code); printf("Routing : unknown_type=%lu (last=0x%02X) handler_empty=%lu (last=0x%02X)\r\n", (unsigned long)st.unknown_type_count, st.last_unknown_type, (unsigned long)st.handler_empty_count, st.last_empty_type); printf("Ring Push: req=%lu timeout=%lu collision=%lu\r\n", (unsigned long)st.ring_request_count, (unsigned long)st.ring_timeout_count, (unsigned long)st.ring_collision_count); if (st.last_error_uptime_ms > 0U) { printf("LastErr : at %lums, rxLen=%u, reason=%s(0x%02X), SOF_off=%u\r\n", (unsigned long)st.last_error_uptime_ms, (unsigned)st.last_error_rx_len, spiParseResultToString((SpiParseResult)st.last_error_reason), st.last_error_code, (unsigned)st.last_error_sof_offset); printf("LastBytes: [%02X %02X %02X %02X %02X %02X %02X %02X]\r\n", st.last_error_bytes[0], st.last_error_bytes[1], st.last_error_bytes[2], st.last_error_bytes[3], st.last_error_bytes[4], st.last_error_bytes[5], st.last_error_bytes[6], st.last_error_bytes[7]); if (st.last_error_reason == SPI_PARSE_ERR_CRC_MISMATCH) { printf("LastCRC : calc=0x%08lX exp=0x%08lX\r\n", (unsigned long)st.last_error_calc_crc, (unsigned long)st.last_error_expected_crc); } } printf("======================================================================\r\n\r\n"); } #endif /* ============================================================ * 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; } /* 细粒度诊断帧解析:逐项分析失败原因(无SOF/相位漂移/版本不符/长度截断/CRC位错)。 */ static bool spi_diagnose_and_parse_frame( const uint8_t *buf, size_t buf_len, uint8_t *out_type, const uint8_t **out_payload, uint16_t *out_payload_len, v2_header_t *out_hdr, SpiParseResult *out_res, uint32_t *out_calc_crc, uint32_t *out_exp_crc, uint8_t *out_sof_offset) { if (!buf || buf_len == 0U) { if (out_res) *out_res = SPI_PARSE_ERR_TOO_SHORT; if (out_sof_offset) *out_sof_offset = 0xFFU; return false; } if (isAllBytesValue(buf, (uint16_t)buf_len, 0xFFU)) { if (out_res) *out_res = SPI_PARSE_ERR_ALL_FF; if (out_sof_offset) *out_sof_offset = 0xFFU; return false; } if (isAllBytesValue(buf, (uint16_t)buf_len, 0x00U)) { if (out_res) *out_res = SPI_PARSE_ERR_ALL_00; if (out_sof_offset) *out_sof_offset = 0xFFU; return false; } if (buf_len < V2_OVERHEAD) { if (out_res) *out_res = SPI_PARSE_ERR_TOO_SHORT; if (out_sof_offset) *out_sof_offset = 0xFFU; return false; } bool found_sof = false; size_t first_sof_offset = 0xFFU; SpiParseResult best_err = SPI_PARSE_ERR_NO_SOF; for (size_t i = 0; i + V2_OVERHEAD <= buf_len; ++i) { if (buf[i + V2_OFF_SOF0] != V2_SOF0 || buf[i + V2_OFF_SOF1] != V2_SOF1) { continue; } if (!found_sof) { found_sof = true; first_sof_offset = i; } const uint8_t version = buf[i + V2_OFF_VERSION]; if (version != V2_VERSION) { best_err = SPI_PARSE_ERR_BAD_VERSION; continue; } const uint16_t plen = v2_get_u16le(&buf[i + V2_OFF_LEN]); if (plen > V2_MAX_PAYLOAD) { best_err = SPI_PARSE_ERR_BAD_LEN; continue; } const size_t total = V2_OVERHEAD + plen; if (i + total > buf_len) { best_err = SPI_PARSE_ERR_BAD_LEN; continue; } const uint32_t expect = v2_get_u32le(&buf[i + V2_HEADER_LEN + plen]); const uint32_t calc = v2_crc32(&buf[i], V2_HEADER_LEN + plen); if (expect != calc) { best_err = SPI_PARSE_ERR_CRC_MISMATCH; if (out_calc_crc) *out_calc_crc = calc; if (out_exp_crc) *out_exp_crc = expect; continue; } if (out_hdr) { out_hdr->version = version; out_hdr->flags = buf[i + V2_OFF_FLAGS]; out_hdr->session_id = v2_get_u32le(&buf[i + V2_OFF_SESSION]); out_hdr->tx_seq = buf[i + V2_OFF_TX_SEQ]; out_hdr->ack_seq = buf[i + V2_OFF_ACK_SEQ]; out_hdr->credit = buf[i + V2_OFF_CREDIT]; out_hdr->type = buf[i + V2_OFF_TYPE]; out_hdr->payload_len = plen; out_hdr->frame_offset = i; } if (out_type) *out_type = buf[i + V2_OFF_TYPE]; if (out_payload) *out_payload = (plen > 0U) ? &buf[i + V2_HEADER_LEN] : nullptr; if (out_payload_len) *out_payload_len = plen; if (out_res) *out_res = SPI_PARSE_OK; if (out_sof_offset) *out_sof_offset = (uint8_t)i; if (out_calc_crc) *out_calc_crc = calc; if (out_exp_crc) *out_exp_crc = expect; return true; } if (out_res) *out_res = best_err; if (out_sof_offset) *out_sof_offset = (uint8_t)first_sof_offset; return false; } /* ============================================================ * 调试日志辅助(环形队列,避免事件并发时丢日志) * ============================================================ */ void AppSpiCommand::postLog(const char *msg) { #if (CONFIG_SPI_APP_LOG_ENABLE != 0) if (!msg) return; uint8_t next = (uint8_t)((m_logHead + 1U) % kLogSlotCount); if (next != m_logTail) { strncpy(m_logQueue[m_logHead], msg, kLogSlotLen - 1U); m_logQueue[m_logHead][kLogSlotLen - 1U] = '\0'; m_logHead = next; } #else (void)msg; #endif } void AppSpiCommand::postLogFmt(const char *fmt, uint8_t val) { #if (CONFIG_SPI_APP_LOG_ENABLE != 0) if (!fmt) return; char tmp[kLogSlotLen]; snprintf(tmp, sizeof(tmp), fmt, val); postLog(tmp); #else (void)fmt; (void)val; #endif } void AppSpiCommand::postLogFmt32(const char *fmt, uint32_t v1, uint32_t v2) { #if (CONFIG_SPI_APP_LOG_ENABLE != 0) if (!fmt) return; char tmp[kLogSlotLen]; snprintf(tmp, sizeof(tmp), fmt, (unsigned long)v1, (unsigned long)v2); postLog(tmp); #else (void)fmt; (void)v1; (void)v2; #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); (void)spi_i2s_data_receive(SPI0); /* 清空 SPI_DATA 残留值 */ 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); /* 内部 NSS 常低:SPI 从机始终就绪,不依赖 EXTI ISR 切换 internal NSS。 * PA15 仅作 EXTI 事务边界信号,用于 onCsAssert/onCsRelease 控制 DMA 装载。 */ spi_nss_internal_low(SPI0); (void)spi_i2s_data_receive(SPI0); spi_i2s_data_transmit(SPI0, 0xFFU); /* RX 改走 DMA0_CH1:禁用 RBNE 逐字节中断,避免 256B 事务 RX-OVR/SOF 丢失。 */ spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_RBNE); spi_i2s_interrupt_disable(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); /* TX: memory → SPI0_DATA */ 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); /* RX: SPI0_DATA → m_rxBuf(替代旧 RBNE 逐字节累积)。 * 与 BOOT SpiOta 对齐:DMA0_CH1 固定映射 SPI0_RX。 */ dma_deinit(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); dma_struct_para_init(&dma_cfg); dma_cfg.periph_addr = (uint32_t)(SPI0 + 0x0CU); dma_cfg.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; dma_cfg.memory_addr = (uint32_t)m_rxBuf; dma_cfg.memory_width = DMA_MEMORY_WIDTH_8BIT; dma_cfg.number = kRxBufSize; 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_PERIPHERAL_TO_MEMORY; dma_init(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH, &dma_cfg); dma_circulation_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); dma_channel_disable(SPI_RX_DMA_PERIPH, SPI_RX_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); /* 先清 STAT 再开 IMR。warm jump 下 BOOT 关中断跳转前 ESP32 持续拉 CS, * EXTI15 STAT 大概率残留;若先 enable IMR,残留位会立即回放成一次假 CS * 沿进 NVIC。 */ exti_interrupt_flag_clear(EXTI_15); exti_interrupt_enable(EXTI_15); /* SPI CS EXTI 抢占级 0。抢占级 1 与 PDI/SYNC1/SCHED 同级不抢占 * (PDI 窗口可达数百 µs),且被 RT_CRITICAL(BASEPRI≥1) 完全屏蔽; * release ISR 一旦拖过 ESP32 的 CS 间隔(1ms),re-arm 落空, * 下一笔查询帧头部丢失。提到 0 后仅与 SYNC0/CAN-RX * (µs 级)同级排队,release 处理确定性 µs 级完成。代价:0xA2 pmDrive 的 * 忙等(≤10ms,手动 PM 诊断才用)会阻塞 SYNC0——诊断窗口,可接受。 */ nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U); } /* ============================================================ * 默认命令注册入口(由各 app_spi_cmd_*.cpp 提供) * ============================================================ */ extern void registerSpiBootRequestCommand(AppSpiCommand &bus); extern void registerSpiOtaWaitCommand(AppSpiCommand &bus); extern void registerSpiAppPingCommand(AppSpiCommand &bus); extern void registerSpiInitStatusCommand(AppSpiCommand &bus); extern void registerSpiCanBridgeCommand(AppSpiCommand &bus); extern void registerSpiCanBurstCommand(AppSpiCommand &bus); extern void registerSpiCanFeedbackCommand(AppSpiCommand &bus); extern void registerSpiIdSnapshotCommand(AppSpiCommand &bus); extern void registerSpiAdcFeedbackCommand(AppSpiCommand &bus); extern void registerSpiMotorStateCommand(AppSpiCommand &bus); extern void registerSpiMotorOtaCommand(AppSpiCommand &bus); extern void registerSpiExmcDiagCommand(AppSpiCommand &bus); extern void registerSpiDiagCommand(AppSpiCommand &bus); AppSpiCommand AppSpiCommandBus; void AppSpiCommand::init() { m_csActive = false; m_rxLen = 0U; m_txLen = 0U; m_logHead = 0U; m_logTail = 0U; m_commandCount = 0U; m_ringPending = false; m_ringDeadline = 0U; memset(m_txBuf, 0xFF, sizeof(m_txBuf)); memset(m_logQueue, 0, sizeof(m_logQueue)); memset(m_commands, 0, sizeof(m_commands)); registerSpiBootRequestCommand(AppSpiCommandBus); registerSpiOtaWaitCommand(AppSpiCommandBus); registerSpiAppPingCommand(AppSpiCommandBus); registerSpiInitStatusCommand(AppSpiCommandBus); registerSpiCanBridgeCommand(AppSpiCommandBus); registerSpiCanBurstCommand(AppSpiCommandBus); registerSpiCanFeedbackCommand(AppSpiCommandBus); registerSpiIdSnapshotCommand(AppSpiCommandBus); registerSpiAdcFeedbackCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */ registerSpiMotorStateCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */ registerSpiMotorOtaCommand(AppSpiCommandBus); registerSpiExmcDiagCommand(AppSpiCommandBus); registerSpiDiagCommand(AppSpiCommandBus); configureGpio(); configureDma(); configureSpi0(); /* 必须先预装 DMA,再开 CS EXTI。 * OTA 跳转后 ESP32 可能立刻打 HELLO:若 EXTI 已开而 DMA 未 arm, * 首笔 CS 会让 SPI 从机失步,之后长期 MISO 噪声/全 FF。 */ armSpiDmasForNextCs(); configureExti(); } /** @brief 在 CS 空闲期预装下一笔全双工 DMA。 * * 必须在首个 SCK 之前完成:EXTI CS 下降相对 ESP32 时钟经常偏晚, * 若在 onCsAssert 里才 start DMA,整笔事务会收 0 字节 → 永不 HELLO_ACK → MISO 全 0xFF。 * * TX 始终按 kTxBufSize 装载:响应帧在头部,其余 0xFF。 * 若只 DMA 实际帧长(~26B),主机打 96 时钟时 TX underrun,后续 MISO 不定。 * * 对齐 BOOT SpiOta::armSpiDmasForNextCs。 */ void AppSpiCommand::armSpiDmasForNextCs() { DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); spi_dma_disable(SPI0, SPI_DMA_RECEIVE); drainSpiRxHw(); /* 预填 TX:有挂起响应用响应帧(头部)+ 0xFF 填充到满缓冲 */ if (m_txLen > 0U && m_txLen <= kTxBufSize) { if (m_txLen < kTxBufSize) { memset(&m_txBuf[m_txLen], 0xFF, (size_t)(kTxBufSize - m_txLen)); } } else { m_txLen = 0U; memset(m_txBuf, 0xFF, kTxBufSize); } /* RX 缓冲清零(便于诊断时区分有效字节与残留) */ memset(m_rxBuf, 0x00, kRxBufSize); m_rxLen = 0U; /* 预装 SPI_DATA,避免 DMA 启动前首拍 TBE 吐旧值 */ spi_i2s_data_transmit(SPI0, m_txBuf[0]); /* 先使能 SPI DMA 请求,再开 DMA 通道,保证首字节即可搬运 */ spi_dma_enable(SPI0, SPI_DMA_RECEIVE); spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); startSpiRxDma(m_rxBuf, kRxBufSize); /* TX 永远满缓冲长度,与主机 96~256B 事务匹配,避免 underrun */ startSpiTxDma(m_txBuf, kTxBufSize); } /* ============================================================ * 命令路由 * ============================================================ */ void AppSpiCommand::registerHandler(ISpiCommandHandler *handler) { if (!handler) { return; } if (m_commandCount >= kMaxCommands) { /* 表满静默丢弃会导致命令无声失效(motor_ota 曾因此 0xD0 回 0xEE)。 * init 在任务上下文,printf 走 USART0 中断环,启动日志可见。 */ printf("[SPI-APP] registerHandler: 注册表满(%u),handler 被丢弃! 请增大 CONFIG_SPI_MAX_COMMANDS\r\n", static_cast(kMaxCommands)); return; } m_commands[m_commandCount++] = handler; } void AppSpiCommand::finishResponseAndReady() { /* 有响应待发时 arm 按铃(ESP32 主动请求的响应即靠此通知; * 推送源已随 2026-08 请求驱动改造移除,无其他写者争用 m_txBuf)。 * updateDataReady 按 m_txLen/m_ringPending 驱动 PB6。 * 延迟日志由 poll() 无条件消费 m_logPending,无需再置任务位。 */ if (m_txLen > 0U) { armRingPending(); } updateDataReady(); } void AppSpiCommand::processCommand() { if (m_rxLen == 0U) { return; } /* 0xB0 已受理、正在写 OTA 配置页 / 即将复位:不再处理业务帧,避免打断 Flash。 */ if (OtaBootRequest::isSpiFrozenForBoot() || OtaBootRequest::isRebootPending()) { m_txLen = 0U; updateDataReady(); return; } uint8_t frame_type = 0U; const uint8_t *payload = nullptr; uint16_t payload_len = 0U; v2_header_t hdr; SpiParseResult parse_res = SPI_PARSE_OK; uint32_t calc_crc = 0U; uint32_t exp_crc = 0U; uint8_t sof_offset = 0xFFU; const bool parse_ok = spi_diagnose_and_parse_frame( m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr, &parse_res, &calc_crc, &exp_crc, &sof_offset); if (!parse_ok) { SPI_DIAG_RECORD( s_diag.parse_fail_count++; s_diag.last_error_uptime_ms = SysTickTimer::getMs(); s_diag.last_error_rx_len = m_rxLen; s_diag.last_error_calc_crc = calc_crc; s_diag.last_error_expected_crc = exp_crc; s_diag.last_error_sof_offset = sof_offset; s_diag.last_error_reason = (uint8_t)parse_res; s_diag.last_error_code = 0xE8U; memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U); ); #if (CONFIG_SPI_APP_LOG_ENABLE != 0 && CONFIG_SPI_DIAG_ENABLE != 0) switch (parse_res) { case SPI_PARSE_ERR_CRC_MISMATCH: { SPI_DIAG_RECORD(s_diag.err_crc_mismatch++); char log_msg[128]; snprintf(log_msg, sizeof(log_msg), "[SPI-DIAG][ERR] CRC mismatch: calc=0x%08lX exp=0x%08lX rxLen=%u", (unsigned long)calc_crc, (unsigned long)exp_crc, (unsigned)m_rxLen); postLog(log_msg); break; } case SPI_PARSE_ERR_NO_SOF: { SPI_DIAG_RECORD(s_diag.err_no_sof++); char log_msg[128]; snprintf(log_msg, sizeof(log_msg), "[SPI-DIAG][ERR] No SOF: rxLen=%u raw=[%02X %02X %02X %02X]", (unsigned)m_rxLen, m_rxBuf[0], m_rxBuf[1], m_rxBuf[2], m_rxBuf[3]); postLog(log_msg); break; } case SPI_PARSE_ERR_BAD_VERSION: { SPI_DIAG_RECORD(s_diag.err_bad_version++); char log_msg[128]; snprintf(log_msg, sizeof(log_msg), "[SPI-DIAG][ERR] Bad ver=0x%02X (exp 0x02) SOF_off=%u rxLen=%u", (sof_offset < m_rxLen - 2) ? m_rxBuf[sof_offset + 2] : 0xFFU, (unsigned)sof_offset, (unsigned)m_rxLen); postLog(log_msg); break; } case SPI_PARSE_ERR_BAD_LEN: { SPI_DIAG_RECORD(s_diag.err_bad_len++); char log_msg[128]; snprintf(log_msg, sizeof(log_msg), "[SPI-DIAG][ERR] Bad len at SOF_off=%u rxLen=%u", (unsigned)sof_offset, (unsigned)m_rxLen); postLog(log_msg); break; } default: { char log_msg[128]; snprintf(log_msg, sizeof(log_msg), "[SPI-DIAG][ERR] Parse fail res=%d rxLen=%u", (int)parse_res, (unsigned)m_rxLen); postLog(log_msg); break; } } #endif if (sof_offset > 0U && sof_offset != 0xFFU) { SPI_DIAG_RECORD(s_diag.err_sof_drift++); } /* 0xE8 payload 携带接收诊断——[0]=0xE8, [1]=rxLen, [2..3]=rxBuf 前 2 字节, [4]=parse_res, [5]=sof_offset */ uint16_t tx_len = 0U; uint8_t diag[8]; diag[0] = 0xE8U; diag[1] = (m_rxLen > 255U) ? 255U : (uint8_t)m_rxLen; diag[2] = (m_rxLen > 0U) ? m_rxBuf[0] : 0xFFU; diag[3] = (m_rxLen > 1U) ? m_rxBuf[1] : 0xFFU; diag[4] = (uint8_t)parse_res; diag[5] = sof_offset; diag[6] = 0x5AU; diag[7] = 0xA5U; SPI_DIAG_RECORD(s_diag.error_rsp_count++); spi_build_frame(kRspError, diag, sizeof(diag), m_txBuf, &tx_len); m_txLen = tx_len; finishResponseAndReady(); return; } // 解析合法 v2 帧成功 SPI_DIAG_RECORD( s_diag.valid_v2_frames++; s_diag.last_rx_type = frame_type; s_diag.last_rx_seq = hdr.tx_seq; ); if (sof_offset > 0U && sof_offset != 0xFFU) { SPI_DIAG_RECORD(s_diag.err_sof_drift++); } g_pendingAckSeq = hdr.tx_seq; if (frame_type == V2_TYPE_LINK_HELLO) { SPI_DIAG_RECORD(s_diag.hello_count++); Esp32En::cancelRelinkFromSpi(); g_sessionId = hdr.session_id; g_txSeq = 0; g_expectedRxSeq = 0; g_v2Active = true; s_hasCachedResp = false; g_localCredit = 1U; uint8_t identity[V2_APP_IDENTITY_LEN]; fillGd32AppIdentityPayload(identity); size_t built_len = 0U; v2_build_hello_ack_frame(g_sessionId, V2_ROLE_APP, (uint8_t)g_localCredit, 0, hdr.tx_seq, identity, sizeof(identity), m_txBuf, &built_len); m_txLen = (uint16_t)built_len; s_lastReqSeq = hdr.tx_seq; memcpy(s_lastRespBuf, m_txBuf, m_txLen); s_lastRespLen = m_txLen; s_hasCachedResp = true; SPI_DIAG_RECORD( s_diag.last_tx_type = V2_TYPE_LINK_HELLO; s_diag.last_tx_seq = 0; ); finishResponseAndReady(); return; } if (g_v2Active) { if (hdr.session_id != g_sessionId) { SPI_DIAG_RECORD( s_diag.session_mismatch++; s_diag.last_error_code = V2_ERR_SESSION; s_diag.last_error_uptime_ms = SysTickTimer::getMs(); s_diag.error_rsp_count++; ); #if (CONFIG_SPI_APP_LOG_ENABLE != 0) char log_msg[128]; snprintf(log_msg, sizeof(log_msg), "[SPI-DIAG][ERR] Session mismatch: rx=0x%08lX != cur=0x%08lX", (unsigned long)hdr.session_id, (unsigned long)g_sessionId); postLog(log_msg); #endif 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; SPI_DIAG_RECORD( s_diag.last_tx_type = V2_TYPE_LINK_ERROR; s_diag.last_tx_seq = g_txSeq; ); finishResponseAndReady(); return; } const uint8_t dist = v2_seq_distance(hdr.tx_seq, g_expectedRxSeq); if (dist == 0U) { SPI_DIAG_RECORD(s_diag.seq_ok_count++); g_expectedRxSeq = (uint8_t)(g_expectedRxSeq + 1U); } else if (dist >= 128U) { SPI_DIAG_RECORD(s_diag.seq_dup_count++); if (s_hasCachedResp && hdr.tx_seq == s_lastReqSeq) { postLogFmt("[SPI-DIAG][INFO] 重复请求 seq=0x%02X -> 重发缓存", hdr.tx_seq); memcpy(m_txBuf, s_lastRespBuf, s_lastRespLen); m_txLen = s_lastRespLen; finishResponseAndReady(); return; } } else { SPI_DIAG_RECORD( s_diag.seq_jump_count++; s_diag.last_error_code = V2_ERR_SEQ_JUMP; s_diag.last_error_uptime_ms = SysTickTimer::getMs(); ); #if (CONFIG_SPI_APP_LOG_ENABLE != 0) char log_msg[128]; snprintf(log_msg, sizeof(log_msg), "[SPI-DIAG][WARN] SEQ jump: rx=0x%02X exp=0x%02X (gap=%u)", hdr.tx_seq, g_expectedRxSeq, (unsigned)dist); postLog(log_msg); #endif g_expectedRxSeq = (uint8_t)(hdr.tx_seq + 1U); if (frame_type != kTypeBootReq) { SPI_DIAG_RECORD(s_diag.error_rsp_count++); 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; SPI_DIAG_RECORD( s_diag.last_tx_type = V2_TYPE_LINK_ERROR; s_diag.last_tx_seq = g_txSeq; ); finishResponseAndReady(); return; } postLog("[SPI-DIAG] SEQ jump but BOOT_REQ still handled"); } } 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) { SPI_DIAG_RECORD( s_diag.unknown_type_count++; s_diag.last_unknown_type = frame_type; s_diag.last_error_code = 0xE9U; s_diag.last_error_uptime_ms = SysTickTimer::getMs(); s_diag.error_rsp_count++; ); #if (CONFIG_SPI_APP_LOG_ENABLE != 0) char log_msg[128]; snprintf(log_msg, sizeof(log_msg), "[SPI-DIAG][ERR] 未知 type=0x%02X -> ERROR 0xE9", frame_type); postLog(log_msg); #endif uint8_t err = 0xE9U; /* E9=无 handler 认领 */ spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); } if (tx_len == 0U) { SPI_DIAG_RECORD( s_diag.handler_empty_count++; s_diag.last_empty_type = frame_type; s_diag.last_error_code = 0xEAU; s_diag.last_error_uptime_ms = SysTickTimer::getMs(); s_diag.error_rsp_count++; ); #if (CONFIG_SPI_APP_LOG_ENABLE != 0) char log_msg[128]; snprintf(log_msg, sizeof(log_msg), "[SPI-DIAG][ERR] handler t=0x%02X tx_len=0 -> ERROR 0xEA", frame_type); postLog(log_msg); #endif uint8_t err = 0xEAU; /* EA=handler 命中但未产出响应 */ spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); } m_txLen = tx_len; SPI_DIAG_RECORD( s_diag.last_tx_type = m_txBuf[V2_OFF_TYPE]; s_diag.last_tx_seq = g_txSeq; ); if (g_v2Active) { s_lastReqSeq = hdr.tx_seq; memcpy(s_lastRespBuf, m_txBuf, m_txLen); s_lastRespLen = m_txLen; s_hasCachedResp = true; g_txSeq = (uint8_t)(g_txSeq + 1U); } finishResponseAndReady(); } /* ============================================================ * GD32 主动按铃:设 m_ringPending + updateDataReady() 拉低 PB6 请求 ESP32 来读 * ============================================================ */ void AppSpiCommand::armRingPending() { m_ringPending = true; m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; } void AppSpiCommand::cancelRing() { m_ringPending = false; /* 清除待发响应,防止超时后残留帧在下一次 CS assert 时被错误输出 */ m_txLen = 0U; /* 释放 SPIready(高阻),避免永久占用线。 * cancelRing 已清 m_txLen 和 m_ringPending,updateDataReady() 会释放为高。 */ updateDataReady(); } void AppSpiCommand::resetEsp32Link() { /* ESP32 被 PC9 掉电可能死在半笔 CS 里,SPI0/DMA/EXTI 都可能卡死。 * 做一次完整外设重初始化(对齐 init 的硬件序列),而不是只清软件状态。 * 仅在 PC9 翻转、对端确定不在发帧时调用。 */ nvic_irq_disable(EXTI10_15_IRQn); exti_interrupt_disable(EXTI_15); g_v2Active = false; g_sessionId = 0U; g_txSeq = 0U; g_expectedRxSeq = 0U; g_pendingAckSeq = 0U; g_localCredit = 1U; s_lastReqSeq = 0xFFU; s_lastRespLen = 0U; s_hasCachedResp = false; m_csActive = false; m_rxLen = 0U; m_ringPending = false; m_ringDeadline = 0U; m_txLen = 0U; memset(m_txBuf, 0xFF, sizeof(m_txBuf)); memset(m_rxBuf, 0x00, sizeof(m_rxBuf)); /* 硬件重初始化:SPI0 + DMA + CS EXTI(与 init() 同序列) */ configureDma(); configureSpi0(); armSpiDmasForNextCs(); updateDataReady(); configureExti(); #if (CONFIG_SPI_APP_LOG_ENABLE != 0) printf("[SPI-APP] resetEsp32Link: SPI0/DMA/EXTI re-init done\r\n"); #endif } bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len) { SPI_DIAG_RECORD(s_diag.ring_request_count++); /* 若上一次按铃尚未被 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_txLen = (uint16_t)built_len; armRingPending(); /* v2 DATA_READY: 已设 m_ringPending=true 且 m_txLen>0, * updateDataReady() 据此拉低 PB6,并置 g_localCredit=0。 */ updateDataReady(); return true; } /* ============================================================ * 主循环轮询:打印延迟日志 + 按铃超时看护 + 周期/异常诊断报告 * ============================================================ */ void AppSpiCommand::poll() { /* 1. 打印 ISR 推迟的日志队列(环形缓冲,避免丢弃) */ while (m_logTail != m_logHead) { printf("%s\r\n", m_logQueue[m_logTail]); m_logTail = (uint8_t)((m_logTail + 1U) % kLogSlotCount); } /* 2. 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。 * 防止 ESP32 不响应时永久占用 SPIready 线导致双方死锁。 */ if (m_ringPending) { uint32_t save2 = RT_CRITICAL_SAVE(); if (m_ringPending) { if ((int32_t)(SysTickTimer::getMs() - m_ringDeadline) >= 0) { SPI_DIAG_RECORD(s_diag.ring_timeout_count++); #if (CONFIG_SPI_DIAG_PERIODIC_PRINT != 0) printf("[SPI-DIAG][WARN] 按铃超时取消 (ESP32 未在 %lums 内响应, 释放 SPIready 重试)\r\n", (unsigned long)kRingTimeoutMs); #endif cancelRing(); } } RT_CRITICAL_RESTORE(save2); } #if (CONFIG_SPI_DIAG_ENABLE != 0 && CONFIG_SPI_DIAG_PERIODIC_PRINT != 0) /* 3. 定期 / 增量打印 SPI 诊断摘要 */ static uint32_t s_lastDiagPrintMs = 0; static uint32_t s_lastDiagErrCount = 0; const uint32_t now = SysTickTimer::getMs(); const uint32_t totalErrs = s_diag.parse_fail_count + s_diag.rx_overrun_count + s_diag.short_rx_len_count + s_diag.session_mismatch + s_diag.seq_jump_count + s_diag.ring_timeout_count + s_diag.unknown_type_count + s_diag.handler_empty_count; const bool hasNewError = (totalErrs > s_lastDiagErrCount && (now - s_lastDiagPrintMs >= 2000U)); const bool periodicCheck = (s_diag.total_cs_assert > 0U && (now - s_lastDiagPrintMs >= 5000U)); if (hasNewError || periodicCheck) { s_lastDiagPrintMs = now; s_lastDiagErrCount = totalErrs; const uint8_t cs_pin = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U; const uint8_t rdy_pin = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U; printf("[SPI-DIAG] CS:assert=%lu rls=%lu ok_v2=%lu | err[CRC:%lu noSOF:%lu short:%lu ovr:%lu sessMis:%lu jmp:%lu tmo:%lu] pins[CS:%d RDY:%d STAT:0x%02X]\r\n", (unsigned long)s_diag.total_cs_assert, (unsigned long)s_diag.total_cs_release, (unsigned long)s_diag.valid_v2_frames, (unsigned long)s_diag.err_crc_mismatch, (unsigned long)s_diag.err_no_sof, (unsigned long)s_diag.short_rx_len_count, (unsigned long)s_diag.rx_overrun_count, (unsigned long)s_diag.session_mismatch, (unsigned long)s_diag.seq_jump_count, (unsigned long)s_diag.ring_timeout_count, cs_pin, rdy_pin, (unsigned)(s_diag.last_spi_stat & 0xFFU)); } #endif } /* ============================================================ * CS 边沿处理 * ============================================================ */ void AppSpiCommand::onCsAssert() { SPI_DIAG_RECORD(s_diag.total_cs_assert++); if (m_csActive) { return; } m_csActive = true; /* CS 下降:DMA 已在空闲期由 armSpiDmasForNextCs() 预装,此处只标记忙碌。 * v2 DATA_READY: CS 本身即互斥,PB6 不再表示「忙碌」,不在 CS 下降沿拉低。 * 切勿在这里 stop/restart DMA,否则赶不上 ESP32 已发出的 SCK(对齐 BOOT)。 */ } void AppSpiCommand::onCsRelease() { SPI_DIAG_RECORD(s_diag.total_cs_release++); if (!m_csActive) { /* EXTI 丢失下降沿时:仍尝试收尾并 re-arm,避免永久失步(对齐 BOOT) */ m_rxLen = stopSpiDmas(kRxBufSize); SPI_DIAG_RECORD( s_diag.missed_cs_assert++; s_diag.last_rx_len = m_rxLen; s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0); ); postLogFmt("[SPI-DIAG][WARN] CS release without assert (got %u B)", (uint8_t)m_rxLen); drainSpiRxHw(); if (m_txLen > 0U) { m_txLen = 0U; memset(m_txBuf, 0xFF, sizeof(m_txBuf)); } if (m_rxLen >= V2_OVERHEAD) { processCommand(); } else { /* 残包:SPI 可能已乱,重配外设 */ configureSpi0(); } armSpiDmasForNextCs(); updateDataReady(); return; } m_csActive = false; /* 停 DMA 并取本笔 RX 长度(先读 CHCNT 再 disable,否则计数器被清) */ m_rxLen = stopSpiDmas(kRxBufSize); SPI_DIAG_RECORD( s_diag.last_rx_len = m_rxLen; s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0); ); drainSpiRxHw(); /* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧, * 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。 */ if (m_ringPending) { 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) { SPI_DIAG_RECORD(s_diag.ring_collision_count++); postLog("[SPI-DIAG][INFO] Ring collision: host sent v2 frame while ring pending"); m_ringPending = false; m_txLen = 0U; memset(m_txBuf, 0xFF, sizeof(m_txBuf)); /* 继续走 shouldIgnoreTransaction + processCommand(),不 return */ } else { /* 正常按铃响应:请求帧已被读走,取消按铃状态 */ m_ringPending = false; /* 请求帧已被读走,清空 TX 防止下次事务重复输出 */ m_txLen = 0U; memset(m_txBuf, 0xFF, sizeof(m_txBuf)); /* 重新预装下一笔 DMA,保证下一 CS 首字节即可收发 */ armSpiDmasForNextCs(); updateDataReady(); return; } } if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) { if (m_rxLen == 0U) { SPI_DIAG_RECORD( s_diag.zero_rx_len_count++; s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_TOO_SHORT; s_diag.last_error_rx_len = 0U; s_diag.last_error_uptime_ms = SysTickTimer::getMs(); ); postLog("[SPI-DIAG][WARN] Ignored 0-byte transaction (SCK did not toggle)"); } else if (isAllBytesValue(m_rxBuf, m_rxLen, 0xFFU)) { SPI_DIAG_RECORD( s_diag.all_ff_count++; s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_ALL_FF; s_diag.last_error_rx_len = m_rxLen; memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U); s_diag.last_error_uptime_ms = SysTickTimer::getMs(); ); postLogFmt("[SPI-DIAG][WARN] Ignored all-0xFF transaction (%u bytes)", (uint8_t)m_rxLen); } else if (isAllBytesValue(m_rxBuf, m_rxLen, 0x00U)) { SPI_DIAG_RECORD( s_diag.all_00_count++; s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_ALL_00; s_diag.last_error_rx_len = m_rxLen; memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U); s_diag.last_error_uptime_ms = SysTickTimer::getMs(); ); postLogFmt("[SPI-DIAG][WARN] Ignored all-0x00 transaction (%u bytes)", (uint8_t)m_rxLen); } else { SPI_DIAG_RECORD( s_diag.short_rx_len_count++; s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_TOO_SHORT; s_diag.last_error_rx_len = m_rxLen; memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U); s_diag.last_error_uptime_ms = SysTickTimer::getMs(); ); postLogFmt("[SPI-DIAG][WARN] Ignored short transaction (%u bytes < 20B)", (uint8_t)m_rxLen); } m_rxLen = 0U; /* 起机毛刺/相位漂移后仅 re-arm DMA 不够:SPI0 可能已 OVR 卡死。 * 顺手重配 SPI0+DMA(不动 EXTI/session),下一笔 HELLO 才能收进来。 */ configureSpi0(); armSpiDmasForNextCs(); updateDataReady(); return; } m_txLen = 0U; memset(m_txBuf, 0xFF, sizeof(m_txBuf)); updateDataReady(); /* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式), * 消除主循环延迟。处理完成后 processCommand() 末尾再 re-arm DMA + 释放 SPIready。 */ processCommand(); /* 处理完立即预装下一笔,保证下一 CS 首字节即可收发 */ armSpiDmasForNextCs(); updateDataReady(); } /* ============================================================ * SPI0 / EXTI 中断处理 * ============================================================ */ void AppSpiCommand::onInterrupt() { /* RX 已改走 DMA0_CH1:RBNE 逐字节中断已禁用,本 ISR 只保留错误标志清理。 * CS 边沿处理全部在 onCsExtiInterrupt 的 EXTI15 ISR 里完成。 */ if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { SPI_DIAG_RECORD( s_diag.rx_overrun_count++; s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0); ); postLog("[SPI-DIAG][ERR] SPI0 RXORERR (RX Overrun Error)!"); (void)spi_i2s_data_receive(SPI0); (void)SPI_STAT(SPI0); } if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { SPI_DIAG_RECORD( s_diag.format_error_count++; s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0); ); postLog("[SPI-DIAG][ERR] SPI0 FERR (Frame Format Error)!"); 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) { CpuLoad::isrEnter(); AppSpiCommandBus.onInterrupt(); CpuLoad::isrExit(); } extern "C" void EXTI10_15_IRQHandler(void) { CpuLoad::isrEnter(); AppSpiCommandBus.onCsExtiInterrupt(); CpuLoad::isrExit(); }