Files
GD32E50X_BOOT/GD32E50X_BOOT/device/SpiOta.cpp
T
cbb 1ec8aa3b5b fix(ota): 修复SPI OTA重传机制和跳转应用逻辑
- 主循环发送ACK时同步更新重发缓存,解决ISR先写WAIT后主循环发ACK不更新
  缓存的问题,避免重发时返回过期WAIT导致ESP32永远接收WAIT而中止传输

- 在stay_in_ota状态结束跳转应用前增加CHECK/AVAL标志门控,防止在
  OTA中途取消时verifyAppCrc的FLASH_EMPTY兜底逻辑误判通过,跳入半成品
  固件导致GD32永久失联
2026-08-02 17:36:26 +08:00

1022 lines
41 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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;
}