feat(GD32E50X_BOOT): 集成SPIv2命令,并添加头文件路径配置

- 添加spi_link_v2.cpp到项目中,分组为spi_link
- 新增G:/Robot_v2/DCU/spi_link目录到编译器包含路径
- 优化项目结构以支持SPI通信功能
This commit is contained in:
cbb
2026-07-13 03:10:55 +08:00
parent dfc12611ed
commit c644c29a4b
6 changed files with 534 additions and 254 deletions
+4
View File
@@ -23,6 +23,9 @@ project:
- file: ./device/Can.cpp
- file: ./device/SpiOta.h
- file: ./device/SpiOta.cpp
- group: spi_link
files:
- file: ../../spi_link/spi_link_v2.cpp
output:
type:
@@ -62,6 +65,7 @@ project:
- -fshort-enums
- -fshort-wchar
- -Wno-register
- -IG:/Robot_v2/DCU/spi_link
C:
- -std=gnu11
CPP:
+464 -225
View File
@@ -7,28 +7,30 @@
#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 发送版)。
* @brief Bootloader SPI OTA 从机模块实现(DMA 全双工版)。
*
* CS 释放后,EXTI ISR 直接完成帧校验+处理+响应准备,
* 响应帧写入 s_txBuf,DMA0_CH2 自动推到 SPI0 TX 寄存器。
* 硬件 DMA 替代 TBE 中断逐字节发送,消除字节错位问题。
* 接收走 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
*/
/* SPI0_TX = DMA0_CH2 (GD32E50x 固定映射) */
/* 主动按铃超时时间(毫秒) */
static constexpr u32 kRingTimeoutMs = 500U;
#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;
@@ -57,66 +59,93 @@ volatile u32 SpiOta::s_ringDeadline = 0U;
SpiLinkState SpiOta::s_linkState;
/* ============================================================
* CRC8 / 帧构建 / 帧解析
* SPI Link v2 SESSION/SEQ 状态 + 响应缓存(ISR 内读写)
* ============================================================ */
static constexpr u8 kSyncSof0 = 0xA5U;
static constexpr u8 kSyncSof1 = 0x5AU;
static constexpr u16 kSyncHeaderLen = 6U;
static constexpr u16 kSyncCrcLen = 4U;
static constexpr u16 kSyncOverhead = (u16)(kSyncHeaderLen + kSyncCrcLen);
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 分支)。
* Boot 的 TX buffer 最多 128 字节,缓存长度对齐 kTxBufSize。 */
static u8 s_lastReqSeq = 0xFFU;
static u8 s_lastRespBuf[128] = {}; /* kTxBufSize=128 */
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;
static void putU16Le(u8 *pp, u16 v) {
pp[0] = (u8)(v & 0xFFU);
pp[1] = (u8)((v >> 8) & 0xFFU);
}
static u16 getU16Le(const u8 *pp) {
return (u16)(pp[0] | ((u16)pp[1] << 8));
}
static void putU32Le(u8 *pp, u32 v) {
pp[0] = (u8)(v & 0xFFU);
pp[1] = (u8)((v >> 8) & 0xFFU);
pp[2] = (u8)((v >> 16) & 0xFFU);
pp[3] = (u8)((v >> 24) & 0xFFU);
}
static u32 getU32Le(const u8 *pp) {
return (u32)pp[0] | ((u32)pp[1] << 8) |
((u32)pp[2] << 16) | ((u32)pp[3] << 24);
/* 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 = 4U;
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 u32 spiCrc32(const u8 *data, u16 len) {
u32 crc = 0xFFFFFFFFU;
for (u16 i = 0U; i < len; ++i) {
crc ^= (u32)data[i] << 24;
for (int b = 0; b < 8; ++b) {
if (crc & 0x80000000U) {
crc = (crc << 1) ^ 0x04C11DB7U;
} else {
crc <<= 1;
}
}
static bool dequeueOtaData(u8 out[64])
{
if (out == nullptr || s_otaDataCnt == 0U) {
return false;
}
return crc;
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; }
u16 total_len = (u16)(kSyncOverhead + payload_len);
if (total_len > kTxBufSize) { return false; }
out[0] = kSyncSof0;
out[1] = kSyncSof1;
out[2] = g_localCredit;
out[3] = (u8)type;
putU16Le(&out[4], payload_len);
if (payload && payload_len > 0U) { memcpy(&out[kSyncHeaderLen], payload, payload_len); }
u32 crc = spiCrc32(out, (u16)(kSyncHeaderLen + payload_len));
putU32Le(&out[kSyncHeaderLen + payload_len], crc);
*out_len = total_len;
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;
}
@@ -124,28 +153,19 @@ 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 < kSyncOverhead) {
if (!frame || frame_len < V2_OVERHEAD) {
return false;
}
/* 同步帧:允许首字节偏移,扫描 SOF0 SOF1。 */
for (u16 s = 0U; s + kSyncOverhead <= frame_len; ++s) {
if (frame[s] != kSyncSof0 || frame[s + 1U] != kSyncSof1) { continue; }
u16 plen = getU16Le(&frame[s + 4U]);
u16 total = (u16)(kSyncOverhead + plen);
if ((u16)(frame_len - s) < total) { continue; }
u32 expect = getU32Le(&frame[s + kSyncHeaderLen + plen]);
u32 calc = spiCrc32(&frame[s], (u16)(kSyncHeaderLen + plen));
if (expect != calc) { continue; }
u8 credit = frame[s + 2U];
if (type) { *type = (spi_ota_frame_type_t)frame[s + 3U]; }
if (payload) { *payload = (plen > 0U) ? &frame[s + kSyncHeaderLen] : nullptr; }
if (payload_len){ *payload_len = plen; }
g_remoteCredit = credit;
return true;
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;
}
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) {
@@ -189,6 +209,82 @@ static void startSpiTxDma(const u8 *buf, u16 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::queueTxFrame(const u8 *frame, u16 len) {
if (!frame || len == 0U || len > kTxBufSize) { return; }
uint32_t primask = enterCritical();
@@ -223,76 +319,130 @@ void SpiOta::isrProcessFrame() {
/* 提前置位,避免同一事务被重复处理。 */
s_isrResponded = true;
if (rx_len == 1U && s_rxStream[0] == SPI_OTA_CMD_BOOT) {
/* 0xB0 始终回复 0xB5 并重置 OTA 状态机。
* 旧代码在 isActive() 时静默忽略,导致上次 OTA 失败后
* 所有后续 0xB0 都被吞掉,ESP32 永远收不到 0xB5。 */
u8 ack = 0xB5U;
queueTxFrame(&ack, 1U);
if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); }
s_otaSessionActive = false;
s_rxStreamLen = 0U;
s_isrResponded = true;
return;
}
/* 裸字节 0xB0/0xB5 路径已删除:v2 协议下 ENTER_BOOT 走标准帧。
* 短于 v2 帧头的输入视为残包/空轮询,直接丢弃。 */
if (rx_len < V2_OVERHEAD) { return; }
/* 新同步帧校验。 */
if (rx_len < kSyncOverhead ||
s_rxStream[0] != kSyncSof0 || s_rxStream[1] != kSyncSof1) {
return;
}
u16 payload_len = getU16Le(&s_rxStream[4U]);
u16 expected = (u16)(kSyncOverhead + payload_len);
if (expected > rx_len) { return; }
/* 帧收齐日志:确认 BOOT 收到了完整帧 */
isrLogFrameEx("[RX-FRM]", s_rxStream, (expected < 18U) ? expected : 18U);
u32 expect = getU32Le(&s_rxStream[kSyncHeaderLen + payload_len]);
u32 calc = spiCrc32(s_rxStream, (u16)(kSyncHeaderLen + payload_len));
if (expect != calc) {
u8 err = 0xEEU;
u8 err_frame[kTxBufSize] = {};
u16 built = 0U;
if (buildFrame(SPI_OTA_RSP_ERROR, &err, 1U, err_frame, &built)) {
queueTxFrame(err_frame, built);
}
isrLogFrameEx("[CRC-ERR]", s_rxStream, (rx_len < 18U) ? rx_len : 18U);
return;
}
spi_ota_frame_type_t frame_type;
/* ---- 1. v2 帧解析(内部扫描 SOF、校验 VERSION/CRC32)---- */
v2_header_t hdr;
const u8 *payload = nullptr;
u16 p_len = 0U;
if (!tryParseFrame(s_rxStream, expected, &frame_type, &payload, &p_len)) {
#if DEBUG_PRINT
static volatile u8 s_parseFailCnt = 0U;
if (s_parseFailCnt < 10U) {
s_parseFailCnt++;
printf("[SPI] parse fail rx=%u expected=%u sof=0x%02X type=0x%02X plen=%u\r\n",
rx_len, expected, s_rxStream[0], s_rxStream[1],
(u16)(s_rxStream[2] | ((u16)s_rxStream[3] << 8)));
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);
}
#endif
isrLogFrameEx("[PARSE-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
return;
}
if (frame_type == SPI_OTA_CMD_BOOT) {
/* 帧式 0xB0 与裸字节 0xB0 行为一致:始终回裸字节 0xB5 + resetToIdle()。
* 旧代码在 isActive() 时静默吞帧且回 0xA5 帧式 ACK,违反
* "0xB0 始终回 0xB5" 约束,且与裸字节路径不一致(0xA5 vs 0xB5)。
* ESP32 appRequestBootloader() 只读首字节期望 0xB5,帧式 ACK 永不匹配。 */
u8 ack = 0xB5U;
queueTxFrame(&ack, 1U);
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;
if (v2_build_hello_ack_frame((uint32_t)s_sessionId, V2_ROLE_BOOT,
(uint8_t)g_localCredit,
(uint8_t)s_txSeq, hdr.tx_seq,
hello_ack, &built)) {
queueTxFrame(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)) {
queueTxFrame(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。 */
if (s_hasCachedResp && s_lastRespLen > 0U) {
queueTxFrame(s_lastRespBuf, s_lastRespLen);
} else if (s_otaDataCnt > 0U) {
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)) {
queueTxFrame(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)) {
queueTxFrame(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(); }
s_otaSessionActive = false;
s_otaDataWi = 0U;
s_otaDataRi = 0U;
s_otaDataCnt = 0U;
s_rxStreamLen = 0U;
s_isrResponded = true;
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 分支把
@@ -309,7 +459,6 @@ void SpiOta::isrProcessFrame() {
}
isrLogFrameEx("[UNK-TYPE]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
s_rxStreamLen = 0U;
s_isrResponded = true;
return;
}
@@ -317,7 +466,31 @@ void SpiOta::isrProcessFrame() {
s_otaSessionActive = true;
s_otaHandler->setTransportSender(sender);
ota_link_msg_type_t msg_type = mapMsgType(frame_type);
s_otaHandler->handleLinkMessage(msg_type, const_cast<u8 *>(payload), (u8)p_len);
/* 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;
@@ -386,6 +559,14 @@ void SpiOta::setSpiReady(bool ready) {
}
}
/* 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 来读
* ============================================================ */
@@ -412,7 +593,7 @@ bool SpiOta::requestEsp32Read(u8 frame_type, const u8 *payload, u16 payload_len)
s_ringPending = true;
s_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
s_linkState.onRingStart();
setSpiReady(false);
updateDataReady();
exitCritical(primask);
return true;
}
@@ -429,7 +610,7 @@ void SpiOta::cancelRing() {
s_txStreamPos = 0U;
memset(s_txBuf, 0xFF, sizeof(s_txBuf));
s_linkState.onRingTimeout();
setSpiReady(true);
updateDataReady();
exitCritical(primask);
}
@@ -470,18 +651,17 @@ void SpiOta::configureSpi0() {
spi_nss_internal_low(SPI0);
/* 初始化时清空 SPI_DATA 寄存器,防止残留值影响首次传输 */
(void)spi_i2s_data_receive(SPI0);
spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_RBNE);
spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_ERR);
/* 接收改走 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);
/* SPI0 RBNE 中断优先级设为 (2, 0),低于 EXTI CS 中断 (1, 1)。
* 这确保 CS 下降沿的 onCsAssert() 在任何 RBNE 之前执行,
* 否则 s_csActive 仍为 false,RBNE 中断丢弃接收字节。 */
nvic_irq_enable(SPI0_IRQn, 2U, 0U);
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);
@@ -497,6 +677,22 @@ void SpiOta::configureDma() {
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() {
@@ -509,18 +705,22 @@ void SpiOta::configureExti() {
void SpiOta::configureSpiReadyExti() {
/* EXTI6 上升沿:ESP32 releaseReady 通知 BOOT "读取完成"。
* EXTI5_9_IRQn 已被 BootSleepManager 以 (0,0) 占用(PB8 深睡唤醒),
* 这里仅使能 EXTI_6 line,中断入口在 EXTI5_9_IRQHandler 中分发。 */
* EXTI5_9_IRQn 已被 BootSleepManager 以 (0,0) 占用(PB8 深睡唤醒)。
* 此处显式使能 EXTI5_9 NVIC 并设置抢占优先级 (3,1),低于 CS 中断 (1,1)
* 与 SPI0 RBNE (2,0),确保 Ready 上升沿不会打断正在进行的帧处理。
* 中断入口在 EXTI5_9_IRQHandler 中分发到 onSpiReadyExti()。 */
gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOB, GPIO_PIN_SOURCE_6);
exti_init(EXTI_6, EXTI_INTERRUPT, EXTI_TRIG_RISING);
exti_interrupt_enable(EXTI_6);
exti_interrupt_flag_clear(EXTI_6);
nvic_irq_enable(EXTI5_9_IRQn, 3U, 1U);
}
void SpiOta::onSpiReadyExti() {
/* v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 上升沿不再有意义。
* 仅清理可能残留的挂起标志,避免 NVIC 重复进入。 */
if (exti_interrupt_flag_get(EXTI_6) != RESET) {
exti_interrupt_flag_clear(EXTI_6);
s_linkState.onEsp32Complete();
}
}
@@ -533,12 +733,26 @@ void SpiOta::init() {
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;
configureGpio();
configureDma();
configureSpi0();
configureExti();
configureSpiReadyExti();
s_linkState.reset();
/* 首笔 CS 到来前预装 DMA,避免 EXTI 晚于首 SCK */
armSpiDmasForNextCs();
}
/* ============================================================
@@ -548,117 +762,113 @@ void SpiOta::init() {
void SpiOta::onCsAssert() {
if (s_csActive) { return; }
s_csActive = true;
/* CS 下降沿立即拉低 PB6:表示从机即将忙碌,
* 防止主机在 onCsRelease 准备好响应前抢跑发起下一笔 CS。
* (若此帧为空轮询/残包,onCsRelease 会恢复 HIGH)
* 与 DCU_APP AppSpiCommand::onCsAssert 行为一致。 */
setSpiReady(false);
s_rxStreamLen = 0U;
/* CS 下降:DMA 已在空闲期预装,此处只标记忙碌。
* v2 DATA_READY: 不在此处操作 PB6,CS 本身即互斥,
* PB6 电平完全由队列状态(updateDataReady)决定。
* 切勿在这里 stop/restart DMA,否则赶不上 ESP32 已发出的 SCK。 */
s_txStreamPos = 0U;
s_isrResponded = false;
/* PA15 仅作为事务边界信号;这里重装 DMA 并清理旧状态。 */
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN;
spi_dma_disable(SPI0, SPI_DMA_TRANSMIT);
/* 覆盖 TBE 里的旧字节,避免下一次事务先吐出残留 ACK。 */
spi_i2s_data_transmit(SPI0, 0xFFU);
if (spi_i2s_flag_get(SPI0, SPI_FLAG_TBE) != RESET) {
spi_i2s_data_transmit(SPI0, 0xFFU);
}
/* 只清错误标志,不主动消费 RBNE。 */
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
(void)spi_i2s_data_receive(SPI0);
}
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) {
spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR);
}
/* 3. 配置并启动 DMA */
if (s_txPending && s_txLen > 0U) {
spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);
startSpiTxDma(s_txBuf, s_txLen);
} else {
s_txPending = false;
s_txLen = kTxBufSize;
memset(s_txBuf, 0xFF, kTxBufSize);
spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);
startSpiTxDma(s_txBuf, s_txLen);
}
/* 4. 重新使能 RBNE 中断,开始接收 MOSI 数据 */
spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_RBNE);
}
void SpiOta::onCsRelease() {
if (!s_csActive) { return; }
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;
/* 内部 NSS 保持常 LOW;这里只停止 DMA。 */
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN;
spi_dma_disable(SPI0, SPI_DMA_TRANSMIT);
/* 停 DMA 并取本笔 RX 长度(先读 CHCNT 再 disable) */
s_rxStreamLen = stopSpiDmas();
drainSpiRxHw();
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) {
u8 last_byte = (u8)(spi_i2s_data_receive(SPI0) & 0xFFU);
if (s_rxStreamLen < kRxBufSize) {
s_rxStream[s_rxStreamLen++] = last_byte;
}
}
/* 如果这是 ESP32 响应 BOOT 主动按铃来读请求帧,直接清空按铃状态并释放 ready。
* 按铃帧已在 CS 期间通过 DMA 输出到 MISO,不需要再解析本次接收到的 dummy 字节。 */
if (s_ringPending) {
s_ringPending = false;
s_ringTxLen = 0U;
/* ★ 关键:本 CS 已时钟输出预装的 TX 帧(延迟一拍响应)。
* armSpiDmasForNextCs() 装载后并不清除 s_txPending,若此处不标记交付,
* pending 会永远为 true → updateDataReady() 把 PB6 永久拉低 →
* ESP32 waitSpiReady(等 HIGH) 每包空等 SPI_READY_TIMEOUT_MS,
* 1116 包 OTA 表现为“卡在 1/1116 十几分钟”。
* 先清交付态;随后 isrProcessFrame/queueTxFrame 可再置 pending。
* 重复帧缓存在 s_lastRespBuf,不依赖 s_txPending 保活。 */
if (s_txPending && s_txLen > 0U) {
s_txPending = false;
s_txLen = 0U;
s_txStreamPos = 0U;
s_rxStreamLen = 0U;
s_isrResponded = true;
memset(s_txBuf, 0xFF, sizeof(s_txBuf));
setSpiReady(true);
return;
}
/* 按铃中若 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);
}
/* BOOT 在 ISR 内同步完成帧处理,响应帧已就绪(或为空轮询/残包无需响应),
* 拉高 PB6 通知主机可发起下一笔事务。
* flash_buf 满(g_localCredit==0)时不拉高,等 drainFlashBuffer 写完 Flash
* 再拉高,形成硬件级背压,避免 ESP32 疯狂重发导致 overrun。 */
if (g_localCredit > 0U) {
setSpiReady(true);
}
/* 处理完立即预装下一笔,保证下一 CS 首字节即可收发 */
armSpiDmasForNextCs();
/* v2 DATA_READY: PB6 电平由队列状态决定。
* flash_buf 满时 CREDIT=0 已通知 ESP32 停止发送,不再用 PB6 背压。 */
updateDataReady();
}
/* ============================================================
* SPI0 中断处理 — 只处理 RBNE(接收)和错误,TBE 已由 DMA 替代
* SPI0 中断处理 — RX 已改 DMA,保留空壳兼容旧 IRQ 向量
* ============================================================ */
void SpiOta::onSpiInterrupt() {
/* 遗留 ERR 标志清理(RBNE 已禁用) */
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) {
/* EXTI 略晚时,在首个 RBNE 到来时补做 onCsAssert()。 */
if (!s_csActive && s_rxStreamLen == 0U && gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) {
onCsAssert();
}
u8 byte = (u8)(spi_i2s_data_receive(SPI0) & 0xFFU);
if (s_rxStreamLen < kRxBufSize) {
s_rxStream[s_rxStreamLen++] = byte;
}
/* 帧处理统一在 onCsRelease() (EXTI CS 释放中断) 里执行,
* 不在 RBNE 中断内调 isrProcessFrame(),避免 CRC32+AES+buildFrame
* 重操作(10-20us)期间 SPI 外设继续收字节导致 RXORERR。 */
(void)spi_i2s_data_receive(SPI0);
}
/* TBE 已由 DMA 替代,不再处理 */
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
(void)spi_i2s_data_receive(SPI0);
s_rxStreamLen = 0U;
isrLogFrameEx("[RX-OVR]", NULL, 0U);
(void)SPI_STAT(SPI0);
}
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) {
spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR);
@@ -677,6 +887,13 @@ void SpiOta::onCsExtiInterrupt() {
* 主循环接口
* ============================================================ */
void SpiOta::servicePendingTx() {
if (!s_csActive && s_txPending && s_txLen > 0U) {
armSpiDmasForNextCs();
updateDataReady();
}
}
void SpiOta::poll() {
if (s_isrResponded) {
s_isrResponded = false;
@@ -699,7 +916,28 @@ void SpiOta::poll() {
}
}
if (s_otaHandler != nullptr) { s_otaHandler->process(); }
if (s_otaHandler != nullptr) {
/* 先消化 ISR 入队的 64B DATA(AES+入 flash 槽),再 process 擦除/drain。 */
u8 data_pkt[64];
u8 drained = 0U;
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++;
}
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; }
@@ -734,6 +972,7 @@ u16 SpiOta::copyTxFrame(u8 *out, u16 max_len) {
void spi_ota_init(void) { SpiOta::init(); }
void spi_ota_poll(void) { SpiOta::poll(); }
void spi_ota_service_pending_tx(void) { SpiOta::servicePendingTx(); }
void spi_ota_set_sender(OTAHandler *handler) { SpiOta::setSender(handler); }
void spi_ota_on_cs_assert(void) { SpiOta::onCsAssert(); }
void spi_ota_on_cs_release(void) { SpiOta::onCsRelease(); }
+10 -5
View File
@@ -25,15 +25,16 @@ typedef enum {
/**
* @brief SPI 链路节奏控制状态机(与 DCU_APP AppSpiCommand::SpiLinkState 对齐)。
*
* BOOT 主动按铃(requestEsp32Read)后,必须等待 ESP32 读取完成
* (SPIready/PB6 上升沿,由 EXTI6 捕获)后才允许发起下一帧,
* 避免下降沿被 ESP32 ISR 在非 IDLE 态丢弃。
* v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 上升沿信号源消失。
* canRingNow 总是返回 true,靠 requestEsp32Read 内部 s_ringPending
* 检查防止重复按铃;onEsp32Complete 改由 onCsRelease 的 ring pending
* 分支补充调用(ESP32 CS 完成即确认读走)。
*/
class SpiLinkState {
public:
bool canRingNow() const { return m_esp32Ready; } ///< 是否允许发起新按铃
bool canRingNow() const { return true; } ///< v2: 总是允许(s_ringPending 已防重复)
void onRingStart() { m_esp32Ready = false; } ///< 发起按铃时消费完成 flag
void onEsp32Complete() { m_esp32Ready = true; } ///< ESP32 releaseReady 上升沿触发(EXTI6 ISR 调用)
void onEsp32Complete() { m_esp32Ready = true; } ///< ESP32 CS 完成即确认读走(onCsRelease 调用)
void onRingTimeout() { m_esp32Ready = true; } ///< 按铃超时取消时允许重试
void reset() { m_esp32Ready = true; } ///< 初始化(首帧允许)
private:
@@ -81,6 +82,9 @@ public:
/** @brief 取消当前按铃请求,释放 SPIready。 */
static void cancelRing();
/** @brief 主循环入队 TX 后装载 DMA + 更新 DATA_READY(擦除中途刷 WAIT 用)。 */
static void servicePendingTx();
/** @brief 翻转 SPI Ready 握手引脚(PB6)电平。
* @param[in] ready true=就绪(HIGH), false=忙(LOW)。
* @note GPIO BSRR/BRR 写入原子,无需临界区;可由 ISR 与主循环共同调用。
@@ -130,6 +134,7 @@ extern "C" {
void spi_ota_init(void);
void spi_ota_poll(void);
void spi_ota_service_pending_tx(void);
void spi_ota_set_sender(OTAHandler *handler);
void spi_ota_on_cs_assert(void);
void spi_ota_on_cs_release(void);
+3 -7
View File
@@ -333,14 +333,10 @@ void BootloaderSystem::run() {
}
int main(void) {
/* 方案A: 极早夺回 PB6 控制权,拉低 SPIready 线。
* 复位后 GPIO 高阻,外部上拉会短暂拉高 PB6 (μs级);
* 此处尽快配为推挽输出 LOW,让 ESP32 能通过 LOW→HIGH 序列
* 识别 "Bootloader 已接管" → "Bootloader 就绪"。
* 后续 SpiOta::configureGpio() 会重配为开漏并释放 HIGH。 */
/* v2 DATA_READY: 仅启用 GPIOB 时钟,不配 PB6 为推挽低。
* PB6 由 SpiOta::configureGpio() 初始化为开漏高(释放态)。
* 复位后 GPIO 高阻,靠外部上拉保持 HIGH,不再制造 LOW→HIGH 上升沿。 */
rcu_periph_clock_enable(RCU_GPIOB);
gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_6);
gpio_bit_reset(GPIOB, GPIO_PIN_6);
sys.initHardware();
sys.run();
+49 -15
View File
@@ -315,6 +315,7 @@ void OTAHandler::process() {
printf("[OTA] erase done, send SPI ACK seq0\r\n");
#endif
send_structured_ack(0U);
SpiOta::servicePendingTx();
}
drainFlashBuffer();
@@ -582,26 +583,55 @@ void OTAHandler::eraseApplicationArea() {
OTA_SET(n_APP_LATEST_EN_ADDR, OTA_NOT_AVAL);
OTA_SET(n_APP_LATEST_AVAL_ADDR, OTA_NOT_AVAL);
__disable_irq();
/* 只擦「本轮固件需要」的页,而不是整片 APP_MAX_SIZE(~108 页)。
* 旧实现整片擦 + 全程关中断可达数秒:SPI 无响应,ESP32 卡在 SEND_SIZE WAIT。
* 53KB 固件约 27 页,体感从数秒降到约 1 秒内。 */
u32 erase_bytes = expected_size;
if (erase_bytes == 0U || erase_bytes > APP_MAX_SIZE) {
erase_bytes = APP_MAX_SIZE;
}
/* 向上对齐页;额外 +1 页余量,避免边界少擦 */
u32 erase_pages = (erase_bytes + FMC_PAGE_SIZE - 1U) / FMC_PAGE_SIZE;
if (erase_pages < (APP_MAX_SIZE / FMC_PAGE_SIZE)) {
erase_pages += 1U;
}
u32 erase_end = APPLATEST_START_ADDR + erase_pages * FMC_PAGE_SIZE;
if (erase_end > (APPLATEST_START_ADDR + APP_MAX_SIZE)) {
erase_end = APPLATEST_START_ADDR + APP_MAX_SIZE;
}
fmc_unlock();
#if DEBUG_PRINT
printf("[FW] E app area 0x%08lX ~ 0x%08lX (%lu pages)\r\n",
printf("[FW] E app 0x%08lX ~ 0x%08lX (%lu pages, fw=%lu)\r\n",
(unsigned long)APPLATEST_START_ADDR,
(unsigned long)(APPLATEST_START_ADDR + APP_MAX_SIZE),
(unsigned long)((APP_MAX_SIZE + FMC_PAGE_SIZE - 1U) / FMC_PAGE_SIZE));
(unsigned long)erase_end,
(unsigned long)((erase_end - APPLATEST_START_ADDR) / FMC_PAGE_SIZE),
(unsigned long)expected_size);
#endif
for (u32 addr = APPLATEST_START_ADDR;
addr < (APPLATEST_START_ADDR + APP_MAX_SIZE); addr += FMC_PAGE_SIZE) {
u32 page_i = 0U;
for (u32 addr = APPLATEST_START_ADDR; addr < erase_end; addr += FMC_PAGE_SIZE) {
/* 仅在单页擦除窗口关中断,避免整片擦时 SPI/SysTick 假死 */
__disable_irq();
fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGAERR | FMC_FLAG_PGERR);
fmc_state_enum status = fmc_page_erase(addr);
__enable_irq();
if (status != FMC_READY) {
ota_error = OTA_ERROR_FLASH_ERROR;
ota_bind_spi_transport();
send_error_frame(0xFFFFFFFFU, OTA_ERROR_FLASH_ERROR);
break;
}
++page_i;
/* 每 8 页刷一次 WAIT,让 ESP32 SEND_SIZE 轮询看到“仍在忙”而非全 0xFF */
if ((page_i & 7U) == 0U) {
ota_bind_spi_transport();
u8 wait_hint[1] = {0x02};
send_wait_frame(1, wait_hint);
/* process() 内嵌擦除时 poll 尚未 re-arm,这里立刻装 DMA */
SpiOta::servicePendingTx();
}
}
fmc_lock();
__enable_irq();
}
/**
@@ -619,10 +649,9 @@ void OTAHandler::drainFlashBuffer() {
u32 drain_cnt = 0;
#endif
if (flash_buf_cnt == 0U) { return; }
/* Flash 写入期间拉低 PB6,阻止 ESP32 发起新 SPI 事务。
* fmc_word_program 会让 CPU 总线停顿,SPI RBNE ISR 来不及响应 → RX-OVR。
* 写完 drain 后恢复 HIGH,ESP32 的 PB6 等待循环会自动放行。 */
SpiOta::setSpiReady(false);
/* v2 DATA_READY: Flash 写入不再用 PB6 背压,改由 CREDIT=0 通知 ESP32 停止发送。
* fmc_word_program 会让 CPU 总线停顿,但 ESP32 在 CREDIT=0 后改为 POLL 等待,
* 不会盲目发新事务导致 RX-OVR。 */
while (1) {
FlashSlot slot = {};
bool has_slot = false;
@@ -651,13 +680,18 @@ void OTAHandler::drainFlashBuffer() {
drain_cnt++;
#endif
}
/* Flash 写入完成,恢复 CREDIT 并释放 PB6。
* ESP32 检测到 PB6 拉高后会继续发数据包。 */
/* Flash 写入完成,恢复 CREDIT 通知 ESP32 可继续发数据。
* v2 DATA_READY: 不再操作 PB6,PB6 由队列状态自动维护。 */
g_localCredit = (u8)(kFlashBufSlots - flash_buf_cnt);
SpiOta::setSpiReady(true);
#if DEBUG_PRINT
/* 每累计 10 包打一次,避免 OTA 过程刷屏。 */
static u32 s_drain_since_log = 0U;
if (drain_cnt > 0U) {
printf("[FW] drain %u pkts\r\n", (unsigned)drain_cnt);
s_drain_since_log += drain_cnt;
if (s_drain_since_log >= 10U) {
printf("[FW] drain %u pkts\r\n", (unsigned)s_drain_since_log);
s_drain_since_log = 0U;
}
}
#endif
}
+4 -2
View File
@@ -270,8 +270,10 @@ void BootSleepManager::onExtiInterrupt(void)
/** EXTI_6 (PB6 SPIready) 上升沿:通知 SpiOta 状态机 ESP32 已完成读取。 */
SpiOta::onSpiReadyExti();
/** 继续分发给 SPI OTA 的 CS 边沿处理逻辑。 */
spi_ota_cs_exti_handler();
/* 注意:SPI OTA 的 CS 边沿信号 PA15 映射到 EXTI_15,属于 EXTI10_15_IRQn,
* 由 EXTI10_15_IRQHandler 直接分发到 SpiOta::onCsExtiInterrupt(),
* 不应在此处(EXTI5_9_Handler)调用 spi_ota_cs_exti_handler()。
* 该调用属于死代码,会导致 PA15 边沿在错误的 ISR 中被重复处理。 */
}
/**