- 主循环发送ACK时同步更新重发缓存,解决ISR先写WAIT后主循环发ACK不更新 缓存的问题,避免重发时返回过期WAIT导致ESP32永远接收WAIT而中止传输 - 在stay_in_ota状态结束跳转应用前增加CHECK/AVAL标志门控,防止在 OTA中途取消时verifyAppCrc的FLASH_EMPTY兜底逻辑误判通过,跳入半成品 固件导致GD32永久失联
1022 lines
41 KiB
C++
1022 lines
41 KiB
C++
#include "SpiOta.h"
|
||
#include "core_cm33.h"
|
||
#include "gd32e50x_gpio.h"
|
||
#include "gd32e50x_exti.h"
|
||
#include "gd32e50x_rcu.h"
|
||
#include "gd32e50x_misc.h"
|
||
#include "gd32e50x_spi.h"
|
||
#include "gd32e50x_dma.h"
|
||
#include "sys/systick.h"
|
||
#include "spi_link_v2.h"
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
/**
|
||
* @file SpiOta.cpp
|
||
* @brief Bootloader SPI OTA 从机模块实现(DMA 全双工版)。
|
||
*
|
||
* CS 释放后,EXTI ISR 直接完成帧校验+处理+响应准备,
|
||
* 响应帧写入 s_txBuf,DMA0_CH2 自动推到 SPI0 TX 寄存器。
|
||
* 接收走 DMA0_CH1(SPI0_RX),不再用 RBNE 逐字节 ISR。
|
||
* ESP32 固定打 256B 时钟时,RBNE 路径必现 RX-OVR/SOF 丢失/PARSE-ERR。
|
||
*
|
||
* GD32E50x DMA 通道映射(固定):
|
||
* DMA0_CH0: ADC0, DMA0_CH1: SPI0_RX, DMA0_CH2: SPI0_TX
|
||
* DMA0_CH3: SPI1_RX, DMA0_CH4: SPI1_TX
|
||
*/
|
||
|
||
/* 主动按铃超时时间(毫秒) */
|
||
static constexpr u32 kRingTimeoutMs = CONFIG_SPI_RING_TIMEOUT_MS;
|
||
#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 constexpr u16 kLogSlotCount = 8U;
|
||
static constexpr u16 kLogSlotSize = 80U;
|
||
static u8 s_logSlots[kLogSlotCount][kLogSlotSize] = {};
|
||
static volatile u8 s_logWriteIdx = 0U;
|
||
static volatile u8 s_logReadIdx = 0U;
|
||
static volatile u8 s_logCount = 0U;
|
||
|
||
static void isrLogFrameEx(const char *tag, const u8 *data, u16 len);
|
||
|
||
|
||
OTAHandler *SpiOta::s_otaHandler = nullptr;
|
||
volatile bool SpiOta::s_csActive = false;
|
||
volatile u16 SpiOta::s_rxStreamLen = 0U;
|
||
volatile u16 SpiOta::s_txStreamPos = 0U;
|
||
u8 SpiOta::s_rxStream[SpiOta::kRxBufSize] = {};
|
||
u8 SpiOta::s_txBuf[SpiOta::kTxBufSize] = {};
|
||
volatile u16 SpiOta::s_txLen = 0U;
|
||
volatile bool SpiOta::s_txPending = false;
|
||
volatile bool SpiOta::s_isrResponded = false;
|
||
volatile bool SpiOta::s_otaSessionActive = false;
|
||
volatile bool SpiOta::s_ringPending = false;
|
||
volatile u16 SpiOta::s_ringTxLen = 0U;
|
||
volatile u32 SpiOta::s_ringDeadline = 0U;
|
||
SpiLinkState SpiOta::s_linkState;
|
||
|
||
/* ============================================================
|
||
* SPI Link v2 SESSION/SEQ 状态 + 响应缓存(ISR 内读写)
|
||
* ============================================================ */
|
||
|
||
static volatile u32 s_sessionId = 0U;
|
||
static volatile u8 s_txSeq = 0U;
|
||
static volatile u8 s_expectedRxSeq = 0U;
|
||
static volatile bool s_v2Active = false;
|
||
|
||
/* 响应缓存:用于重复帧重发(dist 128~255 分支)。
|
||
* 长度对齐 SpiOta::kTxBufSize(128);payload 上限 = kTxBufSize - V2_OVERHEAD = 108。 */
|
||
static u8 s_lastReqSeq = 0xFFU;
|
||
static u8 s_lastRespBuf[SpiOta::kTxBufSize] = {};
|
||
static u16 s_lastRespLen = 0U;
|
||
static bool s_hasCachedResp = false;
|
||
|
||
/* ============================================================
|
||
* 帧构建 / 帧解析(v2 委托 spi_link_v2.cpp)
|
||
* ============================================================ */
|
||
|
||
/* BOOT 侧同步状态机(ISR 内只读/原子写)。 */
|
||
volatile u8 g_localCredit = 8U; /* 非 static:供 OTA.cpp extern 引用 */
|
||
static volatile u8 g_remoteCredit = 8U;
|
||
|
||
/* Boot TX buffer 仅 128 字节,最大 payload = 128 - V2_OVERHEAD(20) = 108 字节。
|
||
* v2_build_frame 只检查 payload_len > V2_MAX_PAYLOAD(236),Boot 侧需额外检查
|
||
* total <= kTxBufSize。 */
|
||
static constexpr u16 kBootMaxPayload = (u16)(SpiOta::kTxBufSize - V2_OVERHEAD);
|
||
|
||
/* 当前请求的 TX_SEQ,写入响应 ACK_SEQ,供 ESP32 做响应归属匹配(延迟一拍)。 */
|
||
static volatile u8 s_pendingAckSeq = 0U;
|
||
|
||
/*
|
||
* OTA 固件分包(64B DATA)延后到主循环处理。
|
||
* AES-128 + CRC + 入 flash 队列若在 CS EXTI ISR 内执行,会长时间关抢占:
|
||
* ESP32 下一笔 CS 到达时 DMA 尚未 re-arm → 中后期(约几十~上百包后)卡死。
|
||
* ISR 只入队;主循环 SpiOta::poll() 调 handleLinkMessage 再回 ACK。
|
||
*/
|
||
static constexpr u8 kOtaDataSlots = CONFIG_OTA_ISR_DATA_SLOTS;
|
||
static u8 s_otaDataSlots[kOtaDataSlots][64];
|
||
static volatile u8 s_otaDataWi = 0U;
|
||
static volatile u8 s_otaDataRi = 0U;
|
||
static volatile u8 s_otaDataCnt = 0U;
|
||
|
||
static bool enqueueOtaDataIsr(const u8 *payload)
|
||
{
|
||
if (payload == nullptr || s_otaDataCnt >= kOtaDataSlots) {
|
||
return false;
|
||
}
|
||
memcpy(s_otaDataSlots[s_otaDataWi], payload, 64U);
|
||
s_otaDataWi = (u8)((s_otaDataWi + 1U) % kOtaDataSlots);
|
||
s_otaDataCnt++;
|
||
return true;
|
||
}
|
||
|
||
static bool dequeueOtaData(u8 out[64])
|
||
{
|
||
if (out == nullptr || s_otaDataCnt == 0U) {
|
||
return false;
|
||
}
|
||
uint32_t primask = __get_PRIMASK();
|
||
__disable_irq();
|
||
if (s_otaDataCnt == 0U) {
|
||
__set_PRIMASK(primask);
|
||
return false;
|
||
}
|
||
memcpy(out, s_otaDataSlots[s_otaDataRi], 64U);
|
||
s_otaDataRi = (u8)((s_otaDataRi + 1U) % kOtaDataSlots);
|
||
s_otaDataCnt--;
|
||
__set_PRIMASK(primask);
|
||
return true;
|
||
}
|
||
|
||
bool SpiOta::buildFrame(spi_ota_frame_type_t type, const u8 *payload, u16 payload_len,
|
||
u8 *out, u16 *out_len) {
|
||
if (!out || !out_len) { return false; }
|
||
if (payload_len > kBootMaxPayload) { return false; }
|
||
/* FLAGS=RESPONSE:Boot 作为从机只发响应帧;填入当前 session/seq/credit。
|
||
* ACK_SEQ = 对端请求 TX_SEQ,不能写死 0,否则 ESP32 otaFindFrame 会丢弃合法响应。 */
|
||
size_t built = 0U;
|
||
bool ok = v2_build_frame((uint8_t)type, V2_FLAG_RESPONSE,
|
||
(uint32_t)s_sessionId,
|
||
(uint8_t)s_txSeq, (uint8_t)s_pendingAckSeq, (uint8_t)g_localCredit,
|
||
(const uint8_t *)payload, (size_t)payload_len,
|
||
(uint8_t *)out, &built);
|
||
if (!ok) { return false; }
|
||
if (built > kTxBufSize) { return false; }
|
||
*out_len = (u16)built;
|
||
return true;
|
||
}
|
||
|
||
bool SpiOta::tryParseFrame(const u8 *frame, u16 frame_len,
|
||
spi_ota_frame_type_t *type,
|
||
const u8 **payload,
|
||
u16 *payload_len) {
|
||
if (!frame || frame_len < V2_OVERHEAD) {
|
||
return false;
|
||
}
|
||
v2_header_t hdr;
|
||
const uint8_t *pload = nullptr;
|
||
if (!v2_parse_frame((const uint8_t *)frame, (size_t)frame_len, &hdr, &pload)) {
|
||
return false;
|
||
}
|
||
if (type) { *type = (spi_ota_frame_type_t)hdr.type; }
|
||
if (payload) { *payload = (const u8 *)pload; }
|
||
if (payload_len) { *payload_len = (u16)hdr.payload_len; }
|
||
g_remoteCredit = hdr.credit;
|
||
return true;
|
||
}
|
||
|
||
ota_link_msg_type_t SpiOta::mapMsgType(spi_ota_frame_type_t type) {
|
||
switch (type) {
|
||
case SPI_OTA_CMD_REQ: return OTA_LINK_MSG_REQ;
|
||
case SPI_OTA_CMD_DATA: return OTA_LINK_MSG_DATA;
|
||
default: return OTA_LINK_MSG_CANCEL;
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* 回包发送适配器(ISR 安全)
|
||
* ============================================================ */
|
||
|
||
void SpiOta::sender(ota_link_tx_type_t type, u8 len, const u8 *data) {
|
||
spi_ota_frame_type_t rsp_type = SPI_OTA_RSP_ACK;
|
||
if (type == OTA_LINK_TX_WAIT) { rsp_type = SPI_OTA_RSP_WAIT; }
|
||
u8 frame_buf[kTxBufSize] = {};
|
||
u16 built_len = 0U;
|
||
if (buildFrame(rsp_type, data, len, frame_buf, &built_len)) {
|
||
submitResponse(frame_buf, built_len);
|
||
/* ★ 主循环发 ACK 时同步更新重发缓存(仅 ACK,不缓存 WAIT)。
|
||
* 旧 bug:ISR 先写 WAIT 入缓存,主循环后续发 ACK 却不更新缓存,
|
||
* 一旦该 ACK 帧因时序丢失,ESP32 重发触发 DUP-SEQ 时只能回放过期
|
||
* WAIT → ESP32 永远只见 WAIT → stall 中止(特别在 8MHz 下必现)。
|
||
* 现在重发会回放最终 ACK,实现“重发快且不漏”。 */
|
||
if (type == OTA_LINK_TX_ACK && built_len > 0U && built_len <= sizeof(s_lastRespBuf)) {
|
||
uint32_t primask = enterCritical();
|
||
memcpy(s_lastRespBuf, frame_buf, built_len);
|
||
s_lastRespLen = built_len;
|
||
s_hasCachedResp = true;
|
||
exitCritical(primask);
|
||
}
|
||
}
|
||
}
|
||
|
||
uint32_t SpiOta::enterCritical() {
|
||
uint32_t primask = __get_PRIMASK();
|
||
__disable_irq();
|
||
return primask;
|
||
}
|
||
|
||
void SpiOta::exitCritical(uint32_t primask) {
|
||
__set_PRIMASK(primask);
|
||
}
|
||
|
||
/** @brief 启动 DMA 传输 s_txBuf → SPI0_DATA */
|
||
static void startSpiTxDma(const u8 *buf, u16 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 → s_rxStream(容量 kRxBufSize)。 */
|
||
static void startSpiRxDma(void) {
|
||
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)SpiOta::s_rxStream;
|
||
DMA_CHCNT(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) = SpiOta::kRxBufSize;
|
||
DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) |= DMA_CHXCTL_CHEN;
|
||
}
|
||
|
||
/** @brief 清空 SPI 接收通路残留(含 OVR)。 */
|
||
static void drainSpiRxHw(void) {
|
||
while (spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE) != RESET) {
|
||
(void)spi_i2s_data_receive(SPI0);
|
||
}
|
||
if (spi_i2s_flag_get(SPI0, SPI_FLAG_RXORERR) != RESET) {
|
||
(void)spi_i2s_data_receive(SPI0);
|
||
(void)SPI_STAT(SPI0);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 在 CS 空闲期预装下一笔全双工 DMA。
|
||
*
|
||
* 必须在首个 SCK 之前完成:EXTI CS 下降相对 ESP32 时钟经常偏晚,
|
||
* 若在 onCsAssert 里才 start DMA,整笔事务会收 0 字节 → 永不 HELLO_ACK → MISO 全 0xFF。
|
||
*
|
||
* TX 始终按 kTxBufSize 装载:响应帧在头部,其余 0xFF。
|
||
* 若只 DMA 实际帧长(~26B),主机打 96 时钟时 TX underrun,后续 MISO 不定。
|
||
*/
|
||
static void armSpiDmasForNextCs(void) {
|
||
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 (SpiOta::s_txPending && SpiOta::s_txLen > 0U && SpiOta::s_txLen <= SpiOta::kTxBufSize) {
|
||
if (SpiOta::s_txLen < SpiOta::kTxBufSize) {
|
||
memset(&SpiOta::s_txBuf[SpiOta::s_txLen], 0xFF,
|
||
(size_t)(SpiOta::kTxBufSize - SpiOta::s_txLen));
|
||
}
|
||
} else {
|
||
SpiOta::s_txPending = false;
|
||
SpiOta::s_txLen = 0U;
|
||
memset(SpiOta::s_txBuf, 0xFF, SpiOta::kTxBufSize);
|
||
}
|
||
|
||
memset(SpiOta::s_rxStream, 0x00, sizeof(SpiOta::s_rxStream));
|
||
SpiOta::s_rxStreamLen = 0U;
|
||
|
||
/* 预装 SPI_DATA,避免 DMA 启动前首拍 TBE 吐旧值 */
|
||
spi_i2s_data_transmit(SPI0, SpiOta::s_txBuf[0]);
|
||
|
||
/* 先使能 SPI DMA 请求,再开 DMA 通道,保证首字节即可搬运 */
|
||
spi_dma_enable(SPI0, SPI_DMA_RECEIVE);
|
||
spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);
|
||
startSpiRxDma();
|
||
/* TX 永远满缓冲长度,与主机 96~128B 事务匹配 */
|
||
startSpiTxDma(SpiOta::s_txBuf, SpiOta::kTxBufSize);
|
||
}
|
||
|
||
/** @brief 停止双 DMA;返回本 CS 实际 RX 字节数(必须在 disable 前读 CHCNT)。 */
|
||
static u16 stopSpiDmas(void) {
|
||
const u16 remaining = (u16)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);
|
||
u16 got = 0U;
|
||
if (remaining <= SpiOta::kRxBufSize) {
|
||
got = (u16)(SpiOta::kRxBufSize - remaining);
|
||
}
|
||
return got;
|
||
}
|
||
|
||
void SpiOta::submitResponse(const u8 *frame, u16 len) {
|
||
if (!frame || len == 0U || len > kTxBufSize) { return; }
|
||
uint32_t primask = enterCritical();
|
||
memcpy(s_txBuf, frame, len);
|
||
s_txLen = len;
|
||
s_txPending = true;
|
||
s_txStreamPos = 0U;
|
||
if (len < kTxBufSize) { memset(&s_txBuf[len], 0xFF, kTxBufSize - len); }
|
||
exitCritical(primask);
|
||
/* ★ 不在此处启动 DMA。
|
||
*
|
||
* 本函数可从 RBNE ISR 中调用(isrProcessFrame → handleLinkMessage
|
||
* → send_structured_ack → sender → submitResponse),此时 CS 仍为 LOW,
|
||
* DMA 正在发送上一次 onCsAssert() 装载的旧响应。
|
||
*
|
||
* 若在此处 startSpiTxDma(),GD32 DMA 控制器会先 CHEN=0 再 CHEN=1,
|
||
* 导致 DMA 计数器清零并从 s_txBuf[0] 重新开始,MISO 输出被破坏
|
||
* (旧响应尾部 + 新响应头部拼接),ESP32 的 otaFindFrame() 无法解析。
|
||
*
|
||
* 正确做法:只缓冲响应到 s_txBuf,由下一次 onCsAssert() 检测
|
||
* s_txPending==true 后统一装载 DMA。这遵循"延迟一拍"协议模型。 */
|
||
}
|
||
|
||
/* ============================================================
|
||
* CS 释放后 ISR 直接帧处理
|
||
* ============================================================ */
|
||
|
||
void SpiOta::isrProcessFrame() {
|
||
u16 rx_len = s_rxStreamLen;
|
||
if (rx_len == 0U) { return; }
|
||
|
||
/* 提前置位,避免同一事务被重复处理。 */
|
||
s_isrResponded = true;
|
||
|
||
/* 裸字节 0xB0/0xB5 路径已删除:v2 协议下 ENTER_BOOT 走标准帧。
|
||
* 短于 v2 帧头的输入视为残包/空轮询,直接丢弃。 */
|
||
if (rx_len < V2_OVERHEAD) { return; }
|
||
|
||
/* ---- 1. v2 帧解析(内部扫描 SOF、校验 VERSION/CRC32)---- */
|
||
v2_header_t hdr;
|
||
const u8 *payload = nullptr;
|
||
if (!v2_parse_frame((const uint8_t *)s_rxStream, (size_t)rx_len, &hdr,
|
||
(const uint8_t **)&payload)) {
|
||
/* ESP32 otaPoll 发全 0xFF dummy 属延迟一拍常态,不刷 PARSE-ERR。 */
|
||
bool all_ff = (rx_len > 0U);
|
||
const u16 chk = (rx_len < 8U) ? rx_len : 8U;
|
||
for (u16 i = 0U; i < chk; ++i) {
|
||
if (s_rxStream[i] != 0xFFU) { all_ff = false; break; }
|
||
}
|
||
if (!all_ff) {
|
||
isrLogFrameEx("[PARSE-ERR]", s_rxStream, chk);
|
||
}
|
||
return;
|
||
}
|
||
|
||
u16 p_len = (u16)hdr.payload_len;
|
||
/* 正常收帧不打 [RX-FRM]:OTA 千包级会刷爆串口;异常路径仍有 PARSE-ERR/SEQ-JMP 等。 */
|
||
g_remoteCredit = hdr.credit;
|
||
/* 响应归属:ACK_SEQ 回显请求 TX_SEQ(延迟一拍 + ESP32 otaFindFrame 匹配)。 */
|
||
s_pendingAckSeq = hdr.tx_seq;
|
||
|
||
/* ---- 2. TYPE=0x00 HELLO:建立/重置 session ---- */
|
||
if (hdr.type == V2_TYPE_LINK_HELLO) {
|
||
s_sessionId = (u32)hdr.session_id;
|
||
s_txSeq = 0U;
|
||
s_expectedRxSeq = 0U;
|
||
s_v2Active = true;
|
||
u8 hello_ack[kTxBufSize] = {};
|
||
size_t built = 0U;
|
||
/* BOOT 仅 6B 链路 ACK,无 APP 身份块 */
|
||
if (v2_build_hello_ack_frame((uint32_t)s_sessionId, V2_ROLE_BOOT,
|
||
(uint8_t)g_localCredit,
|
||
(uint8_t)s_txSeq, hdr.tx_seq,
|
||
nullptr, 0U,
|
||
hello_ack, &built)) {
|
||
submitResponse(hello_ack, (u16)built);
|
||
/* 缓存 HELLO_ACK 以应对 ESP32 重试同一 HELLO。 */
|
||
memcpy(s_lastRespBuf, hello_ack, built);
|
||
s_lastRespLen = (u16)built;
|
||
s_lastReqSeq = hdr.tx_seq;
|
||
s_hasCachedResp = true;
|
||
}
|
||
isrLogFrameEx("[HELLO]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
|
||
s_rxStreamLen = 0U;
|
||
return;
|
||
}
|
||
|
||
/* ---- 3. v2 已激活:SESSION/SEQ 校验 ---- */
|
||
if (s_v2Active) {
|
||
/* 3a. session 校验:不匹配回 LINK_ERROR(0x02)。 */
|
||
if ((u32)hdr.session_id != s_sessionId) {
|
||
u8 err_frame[kTxBufSize] = {};
|
||
size_t built = 0U;
|
||
if (v2_build_error_frame((uint32_t)s_sessionId, (uint8_t)s_txSeq, hdr.tx_seq,
|
||
(uint8_t)g_localCredit, V2_ERR_SESSION,
|
||
(uint8_t)s_expectedRxSeq,
|
||
err_frame, &built)) {
|
||
submitResponse(err_frame, (u16)built);
|
||
}
|
||
isrLogFrameEx("[SES-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
|
||
s_rxStreamLen = 0U;
|
||
return;
|
||
}
|
||
|
||
/* 3b. SEQ 距离判定。 */
|
||
u8 dist = v2_seq_distance(hdr.tx_seq, (uint8_t)s_expectedRxSeq);
|
||
if (dist == 0U) {
|
||
/* 正常:序号匹配,推进期望序号(volatile 原子写)。 */
|
||
s_expectedRxSeq = (u8)(s_expectedRxSeq + 1U);
|
||
} else if (dist >= 128U) {
|
||
/* 重复请求(rx_seq 落后于 expected):重发上一条缓存响应,
|
||
* 不推进序号。若 DATA 仍在主循环排队,回 WAIT 让 ESP32 继续 poll。
|
||
*
|
||
* 注意:缓存若仍是 ISR 即时 WAIT,而主循环已处理完并写过 ACK,
|
||
* 应优先回放 ACK(type=0x80),避免 ESP32 长时间只见 WAIT → 断续。 */
|
||
if (s_hasCachedResp && s_lastRespLen > 0U) {
|
||
submitResponse(s_lastRespBuf, s_lastRespLen);
|
||
} else if (s_otaDataCnt > 0U) {
|
||
u8 wait_hint = 0x03U;
|
||
sender(OTA_LINK_TX_WAIT, 1U, &wait_hint);
|
||
} else if (s_otaHandler != nullptr && s_otaHandler->isActive()) {
|
||
/* 无缓存但会话仍活跃:回 WAIT 促使 ESP32 继续 poll,勿静默丢弃 */
|
||
u8 wait_hint = 0x03U;
|
||
sender(OTA_LINK_TX_WAIT, 1U, &wait_hint);
|
||
}
|
||
isrLogFrameEx("[DUP-SEQ]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
|
||
s_rxStreamLen = 0U;
|
||
return;
|
||
} else {
|
||
/* 1..127:跳序(丢失了 dist 帧),回 LINK_ERROR(0x04)。 */
|
||
u8 err_frame[kTxBufSize] = {};
|
||
size_t built = 0U;
|
||
if (v2_build_error_frame((uint32_t)s_sessionId, (uint8_t)s_txSeq, hdr.tx_seq,
|
||
(uint8_t)g_localCredit, V2_ERR_SEQ_JUMP,
|
||
(uint8_t)s_expectedRxSeq,
|
||
err_frame, &built)) {
|
||
submitResponse(err_frame, (u16)built);
|
||
}
|
||
isrLogFrameEx("[SEQ-JMP]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
|
||
s_rxStreamLen = 0U;
|
||
return;
|
||
}
|
||
}
|
||
|
||
/* ---- 4. TYPE=0xB0 ENTER_BOOT:标准帧 ACK + resetToIdle ---- */
|
||
if (hdr.type == SPI_OTA_CMD_BOOT) {
|
||
u8 ack_frame[kTxBufSize] = {};
|
||
u16 built = 0U;
|
||
if (buildFrame(SPI_OTA_RSP_ACK, nullptr, 0U, ack_frame, &built)) {
|
||
submitResponse(ack_frame, built);
|
||
/* 缓存 ACK 响应以应对重复的 0xB0。 */
|
||
memcpy(s_lastRespBuf, ack_frame, built);
|
||
s_lastRespLen = built;
|
||
s_lastReqSeq = hdr.tx_seq;
|
||
s_hasCachedResp = true;
|
||
}
|
||
if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); }
|
||
/* resetToIdle 已 clearOtaSessionQueues;此处再清 RX 流边界 */
|
||
s_rxStreamLen = 0U;
|
||
return;
|
||
}
|
||
|
||
/* ---- 5. 走原有 OTA 状态机分发 ---- */
|
||
spi_ota_frame_type_t frame_type = (spi_ota_frame_type_t)hdr.type;
|
||
/* 仅转发 BOOT 实际支持的 OTA 协议帧(REQ/DATA/CANCEL)给 OTA 状态机。
|
||
* 其它帧类型(如 0xC0~0xC3 CAN bridge 命令,由 DCU_APP 注册处理器处理,
|
||
* BOOT 无此功能)直接返回 ERROR,避免 mapMsgType 的 default 分支把
|
||
* 未知类型误当成 CANCEL,导致 ESP32 待机时 CAN 反馈推送(0xC1)
|
||
* 不断重置 OTA 状态机,干扰正常 OTA 握手。 */
|
||
if (frame_type != SPI_OTA_CMD_REQ &&
|
||
frame_type != SPI_OTA_CMD_DATA &&
|
||
frame_type != SPI_OTA_CMD_CANCEL) {
|
||
u8 err = 0xEEU;
|
||
u8 err_frame[kTxBufSize] = {};
|
||
u16 built = 0U;
|
||
if (buildFrame(SPI_OTA_RSP_ERROR, &err, 1U, err_frame, &built)) {
|
||
submitResponse(err_frame, built);
|
||
}
|
||
isrLogFrameEx("[UNK-TYPE]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
|
||
s_rxStreamLen = 0U;
|
||
return;
|
||
}
|
||
|
||
if (s_otaHandler != nullptr) {
|
||
s_otaSessionActive = true;
|
||
s_otaHandler->setTransportSender(sender);
|
||
ota_link_msg_type_t msg_type = mapMsgType(frame_type);
|
||
|
||
/* 64B 固件分包:只入队,AES/Flash 放到主循环,避免 ISR 过长。 */
|
||
if (frame_type == SPI_OTA_CMD_DATA && p_len == 64U && payload != nullptr) {
|
||
if (!enqueueOtaDataIsr(payload)) {
|
||
g_localCredit = 0U;
|
||
u8 wait_hint = 0x03U;
|
||
sender(OTA_LINK_TX_WAIT, 1U, &wait_hint);
|
||
isrLogFrameEx("[DATA-QFULL]", s_rxStream, 8U);
|
||
} else {
|
||
/* 立即 WAIT:告诉 ESP32“已收包、处理中”,主循环完成后再 ACK。
|
||
* 若主循环在 ESP32 首 poll 前已写好 ACK,会覆盖本 WAIT。 */
|
||
u8 wait_hint = 0x03U;
|
||
sender(OTA_LINK_TX_WAIT, 1U, &wait_hint);
|
||
}
|
||
} else {
|
||
s_otaHandler->handleLinkMessage(msg_type, const_cast<u8 *>(payload), (u8)p_len);
|
||
}
|
||
}
|
||
|
||
/* ---- 6. 分发后缓存响应到 s_lastRespBuf ---- */
|
||
if (s_txPending && s_txLen > 0U && s_txLen <= sizeof(s_lastRespBuf)) {
|
||
memcpy(s_lastRespBuf, s_txBuf, s_txLen);
|
||
s_lastRespLen = s_txLen;
|
||
s_lastReqSeq = hdr.tx_seq;
|
||
s_hasCachedResp = true;
|
||
}
|
||
|
||
s_isrResponded = true;
|
||
}
|
||
|
||
/* ============================================================
|
||
* ISR 内延迟日志
|
||
* ============================================================ */
|
||
|
||
void SpiOta::isrLogFrame(const char *tag, const u8 *data, u16 len) {
|
||
isrLogFrameEx(tag, data, len);
|
||
}
|
||
|
||
static void isrLogFrameEx(const char *tag, const u8 *data, u16 len) {
|
||
if (s_logCount >= kLogSlotCount) { return; }
|
||
u8 *buf = s_logSlots[s_logWriteIdx];
|
||
u16 pos = 0U;
|
||
if (tag) {
|
||
while (*tag && pos < kLogSlotSize - 3U) { buf[pos++] = (u8)*tag++; }
|
||
buf[pos++] = ' ';
|
||
}
|
||
const u16 preview = (len < 24U) ? len : 24U;
|
||
for (u16 i = 0U; i < preview && pos + 2U < kLogSlotSize; ++i) {
|
||
static const char hx[] = "0123456789ABCDEF";
|
||
buf[pos++] = (u8)hx[data[i] >> 4];
|
||
buf[pos++] = (u8)hx[data[i] & 0x0FU];
|
||
}
|
||
buf[pos] = 0U;
|
||
s_logWriteIdx = (s_logWriteIdx + 1U) % kLogSlotCount;
|
||
s_logCount++;
|
||
}
|
||
|
||
/* ============================================================
|
||
* 硬件初始化
|
||
* ============================================================ */
|
||
|
||
void SpiOta::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(GPIOB, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_7);
|
||
gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15);
|
||
/* PB6 复用为 SPI Ready 握手线(开漏输出,线与)。
|
||
* 开漏模式:写 BRR = 下拉驱动低;写 BSRR = 高阻释放,靠外部/对端上拉拉高。
|
||
* 默认释放(HIGH),允许主机立刻发起首帧。
|
||
* 与 DCU_APP app_spi_command.cpp configureGpio 行为一致。
|
||
*
|
||
* 方案A: 此处 PB6 从 main() 极早的推挽 LOW 切换为开漏 HIGH,
|
||
* 产生 LOW→HIGH 上升沿 = "Bootloader 就绪"信号,
|
||
* ESP32 waitForBootloaderReady() 据此唤醒并发起握手。 */
|
||
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 SpiOta::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 低 = 有待发数据,高 = 队列空。
|
||
* 替代旧 BUSY/READY 语义中所有 setSpiReady 调用。 */
|
||
static void updateDataReady()
|
||
{
|
||
const bool has_data = (SpiOta::s_txPending && SpiOta::s_txLen > 0U) || SpiOta::s_ringPending;
|
||
SpiOta::setSpiReady(!has_data);
|
||
}
|
||
|
||
/* ============================================================
|
||
* BOOT 主动按铃:下拉 SPIready 请求 ESP32 来读
|
||
* ============================================================ */
|
||
|
||
bool SpiOta::requestEsp32Read(u8 frame_type, const u8 *payload, u16 payload_len) {
|
||
if (s_ringPending) {
|
||
return false;
|
||
}
|
||
/*
|
||
* OTA 状态机活跃时禁止主动按铃:s_txBuf 专供 ACK/WAIT/ERROR RESPONSE,
|
||
* 按铃 EVENT(含 CREDIT_UPDATE)会覆盖待发响应,导致 ESP32 丢 ACK。
|
||
* CREDIT 改由 RESPONSE 帧头携带。
|
||
*/
|
||
if (s_otaHandler != nullptr && s_otaHandler->isActive()) {
|
||
return false;
|
||
}
|
||
/* 已有 OTA ACK/WAIT 待发时禁止 CREDIT_UPDATE 等按铃覆盖 s_txBuf,
|
||
* 否则 ESP32 读到 EVENT 而非 ACK → 超时重发 → DUP-SEQ 断续。
|
||
* CREDIT 仍随下一笔 RESPONSE 帧头更新。 */
|
||
if (s_txPending && s_txLen > 0U) {
|
||
return false;
|
||
}
|
||
if (!s_linkState.canRingNow()) {
|
||
/* ESP32 尚未完成上一帧读取(PB6 未出现上升沿),拒绝发起按铃,
|
||
* 避免下降沿被 ESP32 ISR 在非 IDLE 态丢弃。 */
|
||
return false;
|
||
}
|
||
if (payload_len > kBootMaxPayload) {
|
||
return false;
|
||
}
|
||
|
||
/* L5:主动上报必须用 EVENT(非 RESPONSE),与 App requestEsp32Read 对齐。 */
|
||
u8 frame_buf[kTxBufSize];
|
||
size_t built = 0U;
|
||
if (!v2_build_frame(frame_type, V2_FLAG_EVENT,
|
||
(uint32_t)s_sessionId,
|
||
(uint8_t)s_txSeq, 0U, (uint8_t)g_localCredit,
|
||
payload, (size_t)payload_len,
|
||
frame_buf, &built)) {
|
||
return false;
|
||
}
|
||
if (built > kTxBufSize) {
|
||
return false;
|
||
}
|
||
const u16 built_len = (u16)built;
|
||
if (s_v2Active) {
|
||
s_txSeq = (u8)(s_txSeq + 1U);
|
||
}
|
||
|
||
uint32_t primask = enterCritical();
|
||
submitResponse(frame_buf, built_len);
|
||
s_ringTxLen = built_len;
|
||
s_ringPending = true;
|
||
s_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
|
||
s_linkState.onRingStart();
|
||
updateDataReady();
|
||
exitCritical(primask);
|
||
return true;
|
||
}
|
||
|
||
void SpiOta::notifyCreditRestore() {
|
||
static u8 s_prevCredit = 8U;
|
||
const u8 cur = g_localCredit;
|
||
if (s_prevCredit == 0U && cur > 0U) {
|
||
const u8 pl[1] = { cur };
|
||
/* 队列忙时失败可接受:下一笔响应帧头仍带 CREDIT,ESP32 会更新 */
|
||
(void)requestEsp32Read(V2_TYPE_CREDIT_UPDATE, pl, 1U);
|
||
}
|
||
s_prevCredit = cur;
|
||
}
|
||
|
||
void SpiOta::cancelRing() {
|
||
if (!s_ringPending) {
|
||
return;
|
||
}
|
||
uint32_t primask = enterCritical();
|
||
s_ringPending = false;
|
||
s_ringTxLen = 0U;
|
||
s_txPending = false;
|
||
s_txLen = 0U;
|
||
s_txStreamPos = 0U;
|
||
memset(s_txBuf, 0xFF, sizeof(s_txBuf));
|
||
s_linkState.onRingTimeout();
|
||
updateDataReady();
|
||
exitCritical(primask);
|
||
}
|
||
|
||
void SpiOta::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 NSS 由 ESP32 的 CS 线 (GPIO4→PA15) 控制。\外部 CS 高时:
|
||
* spi_nss_internal_high(SPI0) 让 SPI 外设进入未选中状态,
|
||
* 这样 SPI0_DATA 输出 0xFF(而非 0x00),
|
||
* DMA 只在 CS 低时才被 SPI 外设允许推数据到 MISO。 */
|
||
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 始终保持 LOW,使 SPI0 从机一直处于“已选中”状态。
|
||
*
|
||
* APP 侧的稳定实现就是这样做的:PA15 只作为 EXTI 事务边界信号,
|
||
* 不再依赖 EXTI ISR 去“赶在首个 SCK 之前”切 internal NSS。
|
||
*
|
||
* 之前 BOOT 的问题正出在这里:
|
||
* - 空闲时 spi_nss_internal_high(SPI0)
|
||
* - CS↓ 后靠 EXTI ISR 再切 internal_low
|
||
* - 但 ESP32 在 CS↓ 后几乎立刻出时钟
|
||
* - 于是 GD32 常常在 internal NSS 真正拉低前丢掉首字节/首几位
|
||
* → 请求帧首字节 0xA5 被吃掉,看到 0x80 / 0xFF / 随机 0x07
|
||
*
|
||
* 现在改为 internal NSS 常低:
|
||
* - SPI 从机始终就绪,不会因为 EXTI 延迟丢首字节
|
||
* - PA15 仍用于 onCsAssert/onCsRelease 做缓冲区清零和 DMA 重装
|
||
*/
|
||
spi_nss_internal_low(SPI0);
|
||
/* 初始化时清空 SPI_DATA 寄存器,防止残留值影响首次传输 */
|
||
(void)spi_i2s_data_receive(SPI0);
|
||
/* 接收改走 DMA0_CH1:禁用 RBNE 逐字节中断,避免 256B 事务 RX-OVR。 */
|
||
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);
|
||
spi_enable(SPI0);
|
||
}
|
||
|
||
void SpiOta::configureDma() {
|
||
rcu_periph_clock_enable(RCU_DMA0);
|
||
|
||
/* TX: SPI0 → memory? no, memory → SPI0 */
|
||
dma_deinit(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH);
|
||
dma_parameter_struct dma_init_struct;
|
||
dma_struct_para_init(&dma_init_struct);
|
||
dma_init_struct.periph_addr = (uint32_t)(SPI0 + 0x0CU);
|
||
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT;
|
||
dma_init_struct.memory_addr = (uint32_t)s_txBuf;
|
||
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT;
|
||
dma_init_struct.number = 0U;
|
||
dma_init_struct.priority = DMA_PRIORITY_HIGH;
|
||
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
|
||
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
|
||
dma_init_struct.direction = DMA_MEMORY_TO_PERIPHERAL;
|
||
dma_init(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH, &dma_init_struct);
|
||
dma_circulation_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH);
|
||
dma_channel_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH);
|
||
|
||
/* RX: SPI0 → s_rxStream */
|
||
dma_deinit(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH);
|
||
dma_struct_para_init(&dma_init_struct);
|
||
dma_init_struct.periph_addr = (uint32_t)(SPI0 + 0x0CU);
|
||
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT;
|
||
dma_init_struct.memory_addr = (uint32_t)s_rxStream;
|
||
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT;
|
||
dma_init_struct.number = kRxBufSize;
|
||
dma_init_struct.priority = DMA_PRIORITY_HIGH;
|
||
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
|
||
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
|
||
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY;
|
||
dma_init(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH, &dma_init_struct);
|
||
dma_circulation_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH);
|
||
dma_channel_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH);
|
||
}
|
||
|
||
void SpiOta::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 SpiOta::clearOtaSessionQueues() {
|
||
/* ISR 与主循环都可能碰 DATA 队列/CREDIT,关中断做原子清理。 */
|
||
uint32_t primask = enterCritical();
|
||
s_otaDataWi = 0U;
|
||
s_otaDataRi = 0U;
|
||
s_otaDataCnt = 0U;
|
||
s_otaSessionActive = false;
|
||
/* 恢复 CREDIT,避免失败后 CREDIT=0 卡住后续握手/重传。 */
|
||
g_localCredit = CONFIG_OTA_FLASH_BUF_SLOTS;
|
||
exitCritical(primask);
|
||
}
|
||
|
||
void SpiOta::init() {
|
||
s_csActive = false;
|
||
s_rxStreamLen = 0U;
|
||
s_txStreamPos = 0U;
|
||
s_txLen = 0U;
|
||
s_txPending = false;
|
||
s_otaSessionActive = false;
|
||
memset(s_rxStream, 0, sizeof(s_rxStream));
|
||
memset(s_txBuf, 0xFF, sizeof(s_txBuf));
|
||
/* v2 链路状态复位。 */
|
||
s_sessionId = 0U;
|
||
s_txSeq = 0U;
|
||
s_expectedRxSeq = 0U;
|
||
s_v2Active = false;
|
||
s_lastReqSeq = 0xFFU;
|
||
s_lastRespLen = 0U;
|
||
s_hasCachedResp = false;
|
||
memset(s_lastRespBuf, 0, sizeof(s_lastRespBuf));
|
||
s_otaDataWi = 0U;
|
||
s_otaDataRi = 0U;
|
||
s_otaDataCnt = 0U;
|
||
g_localCredit = CONFIG_OTA_FLASH_BUF_SLOTS;
|
||
configureGpio();
|
||
configureDma();
|
||
configureSpi0();
|
||
s_linkState.reset();
|
||
/* 必须先预装 DMA,再开 CS EXTI(与 APP 一致,避免首笔 CS 赶在 arm 前) */
|
||
armSpiDmasForNextCs();
|
||
configureExti();
|
||
}
|
||
|
||
/* ============================================================
|
||
* CS 边沿处理
|
||
* ============================================================ */
|
||
|
||
void SpiOta::onCsAssert() {
|
||
if (s_csActive) { return; }
|
||
s_csActive = true;
|
||
/* CS 下降:DMA 已在空闲期预装,此处只标记忙碌。
|
||
* v2 DATA_READY: 不在此处操作 PB6,CS 本身即互斥,
|
||
* PB6 电平完全由队列状态(updateDataReady)决定。
|
||
* 切勿在这里 stop/restart DMA,否则赶不上 ESP32 已发出的 SCK。 */
|
||
s_txStreamPos = 0U;
|
||
s_isrResponded = false;
|
||
}
|
||
void SpiOta::onCsRelease() {
|
||
if (!s_csActive) {
|
||
/* EXTI 丢失下降沿时:仍尝试收尾并 re-arm,避免永久失步 */
|
||
s_rxStreamLen = stopSpiDmas();
|
||
drainSpiRxHw();
|
||
/* 本 CS 已输出预装 TX:先标记交付,避免 pending 永久为 true。 */
|
||
if (s_txPending && s_txLen > 0U) {
|
||
s_txPending = false;
|
||
s_txLen = 0U;
|
||
s_txStreamPos = 0U;
|
||
}
|
||
if (s_rxStreamLen >= V2_OVERHEAD && !s_isrResponded) {
|
||
isrProcessFrame();
|
||
}
|
||
armSpiDmasForNextCs();
|
||
updateDataReady();
|
||
return;
|
||
}
|
||
s_csActive = false;
|
||
|
||
/* 停 DMA 并取本笔 RX 长度(先读 CHCNT 再 disable) */
|
||
s_rxStreamLen = stopSpiDmas();
|
||
drainSpiRxHw();
|
||
|
||
/* ★ 关键:本 CS 已时钟输出预装的 TX 帧(延迟一拍响应)。
|
||
* armSpiDmasForNextCs() 装载后并不清除 s_txPending,若此处不标记交付,
|
||
* pending 会永远为 true → updateDataReady() 把 PB6 永久拉低,
|
||
* ESP32 侧 DATA_READY 常低(收路径依赖 waitSpiDataReady/盲 poll)。
|
||
* 先清交付态;随后 isrProcessFrame/submitResponse 可再置 pending。
|
||
* 重复帧缓存在 s_lastRespBuf,不依赖 s_txPending 保活。
|
||
*
|
||
* ★ 修复 ACK 丢失死锁(2026-07-14):
|
||
* 旧代码无条件清 s_txPending,ESP32 poll 盲读(0xFF dummy)也会清掉
|
||
* GD32 待发 ACK。如果 ESP32 读到 ACK 但 CRC 解析失败,ACK 永久丢失,
|
||
* ESP32 永远等不到该 ACK → 卡死。
|
||
* 修复:只在 ESP32 发送了有效 v2 请求帧(含 SOF0/SOF1)时才清交付态;
|
||
* 纯 dummy poll(全 0xFF / 无 SOF)保留 s_txPending,让 ESP32 下次
|
||
* poll 能重新读到同一个 ACK。 */
|
||
bool has_v2_request = false;
|
||
if (s_rxStreamLen >= 2U) {
|
||
const u16 chk = (s_rxStreamLen < 16U) ? (u16)(s_rxStreamLen - 1U) : 16U;
|
||
for (u16 i = 0U; i < chk; ++i) {
|
||
if (s_rxStream[i] == V2_SOF0 && s_rxStream[i + 1U] == V2_SOF1) {
|
||
has_v2_request = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (has_v2_request) {
|
||
if (s_txPending && s_txLen > 0U) {
|
||
s_txPending = false;
|
||
s_txLen = 0U;
|
||
s_txStreamPos = 0U;
|
||
}
|
||
}
|
||
/* dummy poll:保留 s_txPending,ACK 可被下次 CS 重新读走 */
|
||
|
||
/* 按铃中若 ESP32 发的是 v2 协议帧(HELLO/OTA 等),不能当 dummy 读吞掉 */
|
||
if (s_ringPending) {
|
||
bool is_v2_frame = false;
|
||
if (s_rxStreamLen >= 2U) {
|
||
const u16 last = (s_rxStreamLen < 16U) ? (u16)(s_rxStreamLen - 1U) : 16U;
|
||
for (u16 i = 0U; i < last; ++i) {
|
||
if (s_rxStream[i] == V2_SOF0 && s_rxStream[i + 1U] == V2_SOF1) {
|
||
is_v2_frame = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (is_v2_frame) {
|
||
s_ringPending = false;
|
||
s_ringTxLen = 0U;
|
||
/* 保留 s_txBuf 由 process 覆盖;pending 已在上方清掉 */
|
||
s_txStreamPos = 0U;
|
||
/* v2: ESP32 CS 完成即确认读走,补充 onEsp32Complete 信号
|
||
* (ESP32 不再操作 PB6,EXTI6 上升沿信号源消失)。 */
|
||
s_linkState.onEsp32Complete();
|
||
/* 继续 isrProcessFrame */
|
||
} else {
|
||
s_ringPending = false;
|
||
s_ringTxLen = 0U;
|
||
s_txPending = false;
|
||
s_txLen = 0U;
|
||
s_txStreamPos = 0U;
|
||
s_rxStreamLen = 0U;
|
||
s_isrResponded = true;
|
||
memset(s_txBuf, 0xFF, sizeof(s_txBuf));
|
||
armSpiDmasForNextCs();
|
||
updateDataReady();
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (s_rxStreamLen > 0U && !s_isrResponded) {
|
||
isrProcessFrame();
|
||
} else if (s_rxStreamLen == 0U) {
|
||
/* 诊断:预装 DMA 仍收 0 字节时打点(日志槽满则静默) */
|
||
isrLogFrameEx("[RX0]", NULL, 0U);
|
||
}
|
||
|
||
/* 处理完立即预装下一笔,保证下一 CS 首字节即可收发 */
|
||
armSpiDmasForNextCs();
|
||
|
||
/* v2 DATA_READY: PB6 电平由队列状态决定。
|
||
* flash_buf 满时 CREDIT=0 已通知 ESP32 停止发送,不再用 PB6 背压。 */
|
||
updateDataReady();
|
||
}
|
||
|
||
void SpiOta::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(); }
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* 主循环接口
|
||
* ============================================================ */
|
||
|
||
void SpiOta::servicePendingTx() {
|
||
if (!s_csActive && s_txPending && s_txLen > 0U) {
|
||
armSpiDmasForNextCs();
|
||
updateDataReady();
|
||
}
|
||
}
|
||
|
||
void SpiOta::poll() {
|
||
if (s_isrResponded) {
|
||
s_isrResponded = false;
|
||
}
|
||
while (s_logCount > 0U) {
|
||
__disable_irq();
|
||
if (s_logCount == 0U) { __enable_irq(); break; }
|
||
s_logReadIdx = (s_logReadIdx + 1U) % kLogSlotCount;
|
||
s_logCount--;
|
||
__enable_irq();
|
||
#if DEBUG_PRINT
|
||
printf("[SPI] %s\r\n", (const char*)s_logSlots[s_logReadIdx]);
|
||
#endif
|
||
}
|
||
/* 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃,
|
||
* 防止永久占用 SPIready 线导致双方死锁。 */
|
||
if (s_ringPending) {
|
||
if ((int32_t)(SysTickTimer::getMs() - s_ringDeadline) >= 0) {
|
||
cancelRing();
|
||
}
|
||
}
|
||
|
||
if (s_otaHandler != nullptr) {
|
||
/* 先 process 排空 flash_buf,再消化 ISR 入队的 64B DATA。
|
||
* 旧顺序先 handleLinkMessage 再 process,当 flash_buf 满时
|
||
* handleLinkMessage 会丢弃当前包(ISR 已推进 seq),导致永久 DUP-SEQ。 */
|
||
/* 先消化 ISR DATA 队列并尽快回 ACK,再 process 写 Flash。
|
||
* 旧顺序先 drainFlash 会把 ACK 推迟到整批 fmc_word_program 之后,
|
||
* ESP32 侧易超时重发 → DUP-SEQ 断续。flash 满时 handleLinkMessage
|
||
* 内部仍会 drainFlashBuffer 腾槽,不会永久丢包。 */
|
||
u8 data_pkt[64];
|
||
u8 drained = 0U;
|
||
#if DEBUG_PRINT
|
||
/* 限频:满速 OTA 时每包 printf 会拖慢主循环、放大断续 */
|
||
static u32 s_poll_log_n = 0U;
|
||
if (s_otaDataCnt > 0U && ((++s_poll_log_n % 32U) == 1U)) {
|
||
printf("[POLL] otaData=%u txPending=%d txLen=%u credit=%u\r\n",
|
||
(unsigned)s_otaDataCnt, (int)s_txPending, (unsigned)s_txLen,
|
||
(unsigned)g_localCredit);
|
||
}
|
||
#endif
|
||
while (drained < kOtaDataSlots && dequeueOtaData(data_pkt)) {
|
||
s_otaHandler->setTransportSender(sender);
|
||
s_otaHandler->handleLinkMessage(OTA_LINK_MSG_DATA, data_pkt, 64U);
|
||
if (s_txPending && s_txLen > 0U && s_txLen <= sizeof(s_lastRespBuf)) {
|
||
memcpy(s_lastRespBuf, s_txBuf, s_txLen);
|
||
s_lastRespLen = s_txLen;
|
||
s_hasCachedResp = true;
|
||
}
|
||
drained++;
|
||
}
|
||
/* ACK 已入队:CS 空闲则立刻 re-arm + 拉低 PB6,再去做慢速 Flash */
|
||
if (!s_csActive && s_txPending && s_txLen > 0U) {
|
||
armSpiDmasForNextCs();
|
||
updateDataReady();
|
||
}
|
||
s_otaHandler->process();
|
||
}
|
||
|
||
/* process()/DATA 处理可能在主循环排队 ACK;CS 空闲时 re-arm + 拉低 PB6。 */
|
||
if (!s_csActive && s_txPending && s_txLen > 0U) {
|
||
armSpiDmasForNextCs();
|
||
updateDataReady();
|
||
}
|
||
}
|
||
|
||
void SpiOta::setSender(OTAHandler *handler) { s_otaHandler = handler; }
|
||
|
||
bool SpiOta::hasTxFrame() { return s_txPending; }
|
||
|
||
|
||
u8 SpiOta::pendingDataCount() {
|
||
return s_otaDataCnt;
|
||
}
|