feat(SpiOta): 实现SPI OTA同步协议和主动按铃机制

- 添加SPI OTA同步协议,使用新的帧格式包含信用控制和CRC32校验
- 实现BOOT主动按铃功能,通过SPI Ready线通知ESP32读取数据
- 添加按铃超时保护机制,防止死锁情况发生
- 优化SPI中断处理,统一在CS释放时处理帧避免RX溢出错误
- 修改GPIO配置为开漏输出模式,实现硬件握手信号
- 添加同步帧解析功能,支持帧头偏移扫描和完整性校验
- 实现信用控制机制,当flash缓冲区满时阻止ESP32继续发送数据
- 修复BOOT命令响应逻辑,确保一致性处理裸字节和帧式命令
- 将OTA完成验证移到主循环处理,避免SPI ready线阻塞过久
- 添加详细的调试日志输出便于问题排查

refactor(SpiOta): 重构SPI OTA协议栈

- 使用CRC32替代CRC8进行帧校验,提高数据完整性
- 重命名帧相关常量以匹配新的同步协议格式
- 移除旧的帧处理方法,采用新的同步帧解析流程
- 添加大小端转换辅助函数用于数据编码解码
- 优化内存布局减少RAM使用
- 统一错误处理流程,添加帧校验失败日志记录
This commit is contained in:
cbb
2026-07-08 02:51:57 +08:00
parent 52e59876cf
commit 59c2bf5527
7 changed files with 349 additions and 114 deletions
+207 -73
View File
@@ -6,6 +6,7 @@
#include "gd32e50x_misc.h" #include "gd32e50x_misc.h"
#include "gd32e50x_spi.h" #include "gd32e50x_spi.h"
#include "gd32e50x_dma.h" #include "gd32e50x_dma.h"
#include "sys/systick.h"
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@@ -23,6 +24,9 @@
*/ */
/* SPI0_TX = DMA0_CH2 (GD32E50x 固定映射) */ /* SPI0_TX = DMA0_CH2 (GD32E50x 固定映射) */
/* 主动按铃超时时间(毫秒) */
static constexpr u32 kRingTimeoutMs = 500U;
#define SPI_TX_DMA_PERIPH DMA0 #define SPI_TX_DMA_PERIPH DMA0
#define SPI_TX_DMA_CH DMA_CH2 #define SPI_TX_DMA_CH DMA_CH2
@@ -47,28 +51,70 @@ volatile u16 SpiOta::s_txLen = 0U;
volatile bool SpiOta::s_txPending = false; volatile bool SpiOta::s_txPending = false;
volatile bool SpiOta::s_isrResponded = false; volatile bool SpiOta::s_isrResponded = false;
volatile bool SpiOta::s_otaSessionActive = 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;
/* ============================================================ /* ============================================================
* CRC8 / 帧构建 / 帧解析 * CRC8 / 帧构建 / 帧解析
* ============================================================ */ * ============================================================ */
u8 SpiOta::crc8(const u8 *data, u16 len) { static constexpr u8 kSyncSof0 = 0xA5U;
u8 crc = 0x00U; static constexpr u8 kSyncSof1 = 0x5AU;
for (u16 i = 0; i < len; ++i) { crc ^= data[i]; } static constexpr u16 kSyncHeaderLen = 6U;
static constexpr u16 kSyncCrcLen = 4U;
static constexpr u16 kSyncOverhead = (u16)(kSyncHeaderLen + kSyncCrcLen);
/* 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);
}
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;
}
}
}
return crc; return crc;
} }
bool SpiOta::buildFrame(spi_ota_frame_type_t type, const u8 *payload, u16 payload_len, bool SpiOta::buildFrame(spi_ota_frame_type_t type, const u8 *payload, u16 payload_len,
u8 *out, u16 *out_len) { u8 *out, u16 *out_len) {
if (!out || !out_len) { return false; } if (!out || !out_len) { return false; }
u16 total_len = (u16)(kFrameOverhead + payload_len); u16 total_len = (u16)(kSyncOverhead + payload_len);
if (total_len > kTxBufSize) { return false; } if (total_len > kTxBufSize) { return false; }
out[0] = kSof; out[0] = kSyncSof0;
out[1] = (u8)type; out[1] = kSyncSof1;
out[2] = (u8)(payload_len & 0xFFU); out[2] = g_localCredit;
out[3] = (u8)((payload_len >> 8) & 0xFFU); out[3] = (u8)type;
if (payload && payload_len > 0U) { memcpy(&out[4], payload, payload_len); } putU16Le(&out[4], payload_len);
out[4 + payload_len] = crc8(out, (u16)(4U + 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; *out_len = total_len;
return true; return true;
} }
@@ -77,17 +123,29 @@ bool SpiOta::tryParseFrame(const u8 *frame, u16 frame_len,
spi_ota_frame_type_t *type, spi_ota_frame_type_t *type,
const u8 **payload, const u8 **payload,
u16 *payload_len) { u16 *payload_len) {
if (!frame || frame_len < kFrameOverhead) { return false; } if (!frame || frame_len < kSyncOverhead) {
if (frame[0] != kSof) { return false; } return false;
u16 len = (u16)(frame[2] | ((u16)frame[3] << 8)); }
u16 expected = (u16)(kFrameOverhead + len);
if (expected != frame_len) { return false; } /* 同步帧:允许首字节偏移,扫描 SOF0 SOF1。 */
if (crc8(frame, (u16)(4U + len)) != frame[4U + len]) { return false; } for (u16 s = 0U; s + kSyncOverhead <= frame_len; ++s) {
if (type) { *type = (spi_ota_frame_type_t)frame[1]; } if (frame[s] != kSyncSof0 || frame[s + 1U] != kSyncSof1) { continue; }
if (payload) { *payload = &frame[4]; } u16 plen = getU16Le(&frame[s + 4U]);
if (payload_len){ *payload_len = len; } 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; return true;
} }
return false;
}
ota_link_msg_type_t SpiOta::mapMsgType(spi_ota_frame_type_t type) { ota_link_msg_type_t SpiOta::mapMsgType(spi_ota_frame_type_t type) {
switch (type) { switch (type) {
@@ -139,30 +197,18 @@ void SpiOta::queueTxFrame(const u8 *frame, u16 len) {
s_txStreamPos = 0U; s_txStreamPos = 0U;
if (len < kTxBufSize) { memset(&s_txBuf[len], 0xFF, kTxBufSize - len); } if (len < kTxBufSize) { memset(&s_txBuf[len], 0xFF, kTxBufSize - len); }
exitCritical(primask); exitCritical(primask);
spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); /* ★ 不在此处启动 DMA。
startSpiTxDma(s_txBuf, s_txLen); *
} * 本函数可从 RBNE ISR 中调用(isrProcessFrame → handleLinkMessage
* → send_structured_ack → sender → queueTxFrame),此时 CS 仍为 LOW,
/* ============================================================ * DMA 正在发送上一次 onCsAssert() 装载的旧响应。
* RBNE 中断内即时帧检测与处理 *
* ============================================================ */ * 若在此处 startSpiTxDma(),GD32 DMA 控制器会先 CHEN=0 再 CHEN=1,
* 导致 DMA 计数器清零并从 s_txBuf[0] 重新开始,MISO 输出被破坏
void SpiOta::tryProcessCompleteFrame() { * (旧响应尾部 + 新响应头部拼接),ESP32 的 otaFindFrame() 无法解析。
u16 rx_len = s_rxStreamLen; *
if (rx_len == 0U) { return; } * 正确做法:只缓冲响应到 s_txBuf,由下一次 onCsAssert() 检测
* s_txPending==true 后统一装载 DMA。这遵循"延迟一拍"协议模型。 */
/* 单字节 BOOT 命令留到事务结束后再判定。 */
if (s_rxStream[0] == SPI_OTA_CMD_BOOT) { return; }
if (s_rxStream[0] != kSof) { return; }
if (rx_len < kFrameOverhead) { return; }
u16 payload_len = (u16)(s_rxStream[2] | ((u16)s_rxStream[3] << 8));
u16 expected = (u16)(kFrameOverhead + payload_len);
if (expected > kRxBufSize) { return; }
if (rx_len < expected) { return; }
isrProcessFrame();
} }
/* ============================================================ /* ============================================================
@@ -177,12 +223,11 @@ void SpiOta::isrProcessFrame() {
s_isrResponded = true; s_isrResponded = true;
if (rx_len == 1U && s_rxStream[0] == SPI_OTA_CMD_BOOT) { if (rx_len == 1U && s_rxStream[0] == SPI_OTA_CMD_BOOT) {
if (s_otaHandler != nullptr && s_otaHandler->isActive()) { /* 0xB0 始终回复 0xB5 并重置 OTA 状态机。
s_rxStreamLen = 0U; * 旧代码在 isActive() 时静默忽略,导致上次 OTA 失败后
return; * 所有后续 0xB0 都被吞掉,ESP32 永远收不到 0xB5。 */
} u8 ack = 0xB5U;
u8 ack[4] = {0xB0U, 0xA5U, 0x4FU, 0x54U}; queueTxFrame(&ack, 1U);
queueTxFrame(ack, sizeof(ack));
if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); } if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); }
s_otaSessionActive = false; s_otaSessionActive = false;
s_rxStreamLen = 0U; s_rxStreamLen = 0U;
@@ -190,23 +235,29 @@ void SpiOta::isrProcessFrame() {
return; return;
} }
if (s_rxStream[0] != kSof) { /* 新同步帧校验。 */
if (rx_len < kSyncOverhead ||
s_rxStream[0] != kSyncSof0 || s_rxStream[1] != kSyncSof1) {
return; return;
} }
if (rx_len < kFrameOverhead) { return; }
u16 payload_len = (u16)(s_rxStream[2] | ((u16)s_rxStream[3] << 8)); u16 payload_len = getU16Le(&s_rxStream[4U]);
u16 expected = (u16)(kFrameOverhead + payload_len); u16 expected = (u16)(kSyncOverhead + payload_len);
if (expected > rx_len) { return; } if (expected > rx_len) { return; }
if (crc8(s_rxStream, (u16)(4U + payload_len)) != s_rxStream[4U + payload_len]) { /* 帧收齐日志:确认 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 = 0xEEU;
u8 err_frame[kTxBufSize] = {}; u8 err_frame[kTxBufSize] = {};
u16 built = 0U; u16 built = 0U;
if (buildFrame(SPI_OTA_RSP_ERROR, &err, 1U, err_frame, &built)) { if (buildFrame(SPI_OTA_RSP_ERROR, &err, 1U, err_frame, &built)) {
queueTxFrame(err_frame, built); queueTxFrame(err_frame, built);
} }
isrLogFrameEx("[CRC-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); isrLogFrameEx("[CRC-ERR]", s_rxStream, (rx_len < 18U) ? rx_len : 18U);
return; return;
} }
@@ -214,22 +265,26 @@ void SpiOta::isrProcessFrame() {
const u8 *payload = nullptr; const u8 *payload = nullptr;
u16 p_len = 0U; u16 p_len = 0U;
if (!tryParseFrame(s_rxStream, expected, &frame_type, &payload, &p_len)) { 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)));
}
#endif
isrLogFrameEx("[PARSE-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U); isrLogFrameEx("[PARSE-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
return; return;
} }
if (frame_type == SPI_OTA_CMD_BOOT) { if (frame_type == SPI_OTA_CMD_BOOT) {
if (s_otaHandler != nullptr && s_otaHandler->isActive()) { /* 帧式 0xB0 与裸字节 0xB0 行为一致:始终回裸字节 0xB5 + resetToIdle()。
s_rxStreamLen = 0U; * 旧代码在 isActive() 时静默吞帧且回 0xA5 帧式 ACK,违反
s_isrResponded = true; * "0xB0 始终回 0xB5" 约束,且与裸字节路径不一致(0xA5 vs 0xB5)。
return; * ESP32 appRequestBootloader() 只读首字节期望 0xB5,帧式 ACK 永不匹配。 */
} u8 ack = 0xB5U;
u8 ack_ok = 0xA5U; queueTxFrame(&ack, 1U);
u8 ack_frame[kTxBufSize] = {};
u16 ack_len = 0U;
if (buildFrame(SPI_OTA_RSP_ACK, &ack_ok, 1U, ack_frame, &ack_len)) {
queueTxFrame(ack_frame, ack_len);
}
if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); } if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); }
s_otaSessionActive = false; s_otaSessionActive = false;
s_rxStreamLen = 0U; s_rxStreamLen = 0U;
@@ -308,14 +363,21 @@ void SpiOta::configureGpio() {
gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4); 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(GPIOB, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_7);
gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15); gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15);
/* PB6 复用为 SPI Ready 握手输出,默认 HIGH(就绪),允许主机立刻发起首帧。 /* PB6 复用为 SPI Ready 握手线(开漏输出,线与)。
* 与 DCU_APP app_spi_command.cpp configureGpio 行为一致, * 开漏模式:写 BRR = 下拉驱动低;写 BSRR = 高阻释放,靠外部/对端上拉拉高。
* 适配 ESP32 GPIO10 输入监测 PB6 电平的硬件握手时序。 */ * 默认释放(HIGH),允许主机立刻发起首帧。
gpio_init(CONFIG_SPI_READY_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, CONFIG_SPI_READY_PIN); * 与 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); gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN);
} }
void SpiOta::setSpiReady(bool ready) { void SpiOta::setSpiReady(bool ready) {
/* 开漏线与:ready=true 释放(BSRR,高阻,靠上拉拉高);
* ready=false 下拉(BRR,驱动低)。 */
if (ready) { if (ready) {
gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN);
} else { } else {
@@ -323,6 +385,46 @@ void SpiOta::setSpiReady(bool ready) {
} }
} }
/* ============================================================
* BOOT 主动按铃:下拉 SPIready 请求 ESP32 来读
* ============================================================ */
bool SpiOta::requestEsp32Read(u8 frame_type, const u8 *payload, u16 payload_len) {
if (s_ringPending) {
return false;
}
u8 frame_buf[kTxBufSize];
u16 built_len = 0U;
if (!buildFrame((spi_ota_frame_type_t)frame_type, payload, payload_len, frame_buf, &built_len)) {
return false;
}
uint32_t primask = enterCritical();
queueTxFrame(frame_buf, built_len);
s_ringTxLen = built_len;
s_ringPending = true;
s_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
setSpiReady(false);
exitCritical(primask);
return true;
}
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));
setSpiReady(true);
exitCritical(primask);
}
void SpiOta::configureSpi0() { void SpiOta::configureSpi0() {
rcu_periph_clock_enable(RCU_SPI0); rcu_periph_clock_enable(RCU_SPI0);
spi_i2s_deinit(SPI0); spi_i2s_deinit(SPI0);
@@ -477,16 +579,34 @@ void SpiOta::onCsRelease() {
} }
/* 如果这是 ESP32 响应 BOOT 主动按铃来读请求帧,直接清空按铃状态并释放 ready。
* 按铃帧已在 CS 期间通过 DMA 输出到 MISO,不需要再解析本次接收到的 dummy 字节。 */
if (s_ringPending) {
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));
setSpiReady(true);
return;
}
if (s_rxStreamLen > 0U && !s_isrResponded) { if (s_rxStreamLen > 0U && !s_isrResponded) {
isrProcessFrame(); isrProcessFrame();
} }
/* BOOT 在 ISR 内同步完成帧处理,响应帧已就绪(或为空轮询/残包无需响应), /* BOOT 在 ISR 内同步完成帧处理,响应帧已就绪(或为空轮询/残包无需响应),
* 拉高 PB6 通知主机可发起下一笔事务。 * 拉高 PB6 通知主机可发起下一笔事务。
* 与 DCU_APP processCommand() 末尾 setSpiReady(true) 行为一致; * flash_buf 满(g_localCredit==0)时不拉高,等 drainFlashBuffer 写完 Flash
* 空轮询事务同样恢复 HIGH,避免 PB6 卡 LOW 导致 ESP32 waitSpiReady 超时。 */ * 再拉高,形成硬件级背压,避免 ESP32 疯狂重发导致 overrun。 */
if (g_localCredit > 0U) {
setSpiReady(true); setSpiReady(true);
} }
}
/* ============================================================ /* ============================================================
* SPI0 中断处理 — 只处理 RBNE(接收)和错误,TBE 已由 DMA 替代 * SPI0 中断处理 — 只处理 RBNE(接收)和错误,TBE 已由 DMA 替代
@@ -503,12 +623,15 @@ void SpiOta::onSpiInterrupt() {
if (s_rxStreamLen < kRxBufSize) { if (s_rxStreamLen < kRxBufSize) {
s_rxStream[s_rxStreamLen++] = byte; s_rxStream[s_rxStreamLen++] = byte;
} }
tryProcessCompleteFrame(); /* 帧处理统一在 onCsRelease() (EXTI CS 释放中断) 里执行,
* 不在 RBNE 中断内调 isrProcessFrame(),避免 CRC32+AES+buildFrame
* 重操作(10-20us)期间 SPI 外设继续收字节导致 RXORERR。 */
} }
/* TBE 已由 DMA 替代,不再处理 */ /* TBE 已由 DMA 替代,不再处理 */
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
(void)spi_i2s_data_receive(SPI0); (void)spi_i2s_data_receive(SPI0);
s_rxStreamLen = 0U; s_rxStreamLen = 0U;
isrLogFrameEx("[RX-OVR]", NULL, 0U);
} }
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) {
spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR);
@@ -537,7 +660,18 @@ void SpiOta::poll() {
s_logReadIdx = (s_logReadIdx + 1U) % kLogSlotCount; s_logReadIdx = (s_logReadIdx + 1U) % kLogSlotCount;
s_logCount--; s_logCount--;
__enable_irq(); __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) { s_otaHandler->process(); } if (s_otaHandler != nullptr) { s_otaHandler->process(); }
} }
+18 -4
View File
@@ -24,10 +24,8 @@ typedef enum {
class SpiOta { class SpiOta {
public: public:
static constexpr u8 kSof = 0xA5U;
static constexpr u16 kRxBufSize = 256U; static constexpr u16 kRxBufSize = 256U;
static constexpr u16 kTxBufSize = 128U; static constexpr u16 kTxBufSize = 128U;
static constexpr u16 kFrameOverhead = 5U;
static constexpr u16 kIsrLogBufSize = 128U; static constexpr u16 kIsrLogBufSize = 128U;
static void init(); static void init();
@@ -46,6 +44,18 @@ public:
static bool hasActiveSession(); static bool hasActiveSession();
static u16 copyTxFrame(u8 *out, u16 max_len); static u16 copyTxFrame(u8 *out, u16 max_len);
/** @brief BOOT 主动按铃:构建请求帧并拉低 SPIready,通知 ESP32 来读。
* @param[in] frame_type 请求帧类型。
* @param[in] payload 请求帧负载。
* @param[in] payload_len 负载长度。
* @return true 按铃成功,false 当前已有按铃未处理或帧构建失败。
* @note 与 DCU_APP AppSpiCommand::requestEsp32Read 对齐。
*/
static bool requestEsp32Read(u8 frame_type, const u8 *payload, u16 payload_len);
/** @brief 取消当前按铃请求,释放 SPIready。 */
static void cancelRing();
/** @brief 翻转 SPI Ready 握手引脚(PB6)电平。 /** @brief 翻转 SPI Ready 握手引脚(PB6)电平。
* @param[in] ready true=就绪(HIGH), false=忙(LOW)。 * @param[in] ready true=就绪(HIGH), false=忙(LOW)。
* @note GPIO BSRR/BRR 写入原子,无需临界区;可由 ISR 与主循环共同调用。 * @note GPIO BSRR/BRR 写入原子,无需临界区;可由 ISR 与主循环共同调用。
@@ -53,7 +63,6 @@ public:
static void setSpiReady(bool ready); static void setSpiReady(bool ready);
static void isrProcessFrame(); static void isrProcessFrame();
static void tryProcessCompleteFrame();
static void isrLogFrame(const char *tag, const u8 *data, u16 len); static void isrLogFrame(const char *tag, const u8 *data, u16 len);
static OTAHandler *s_otaHandler; static OTAHandler *s_otaHandler;
@@ -66,11 +75,13 @@ public:
static volatile bool s_txPending; static volatile bool s_txPending;
static volatile bool s_isrResponded; static volatile bool s_isrResponded;
static volatile bool s_otaSessionActive; static volatile bool s_otaSessionActive;
static volatile bool s_ringPending; ///< 是否有未读走的主动按铃请求
static volatile u16 s_ringTxLen; ///< 按铃帧长度
static volatile u32 s_ringDeadline; ///< 按铃超时截止时间(ms)
static void sender(ota_link_tx_type_t type, u8 len, const u8 *data); static void sender(ota_link_tx_type_t type, u8 len, const u8 *data);
private: private:
static u8 crc8(const u8 *data, u16 len);
static bool buildFrame(spi_ota_frame_type_t type, const u8 *payload, u16 payload_len, static bool buildFrame(spi_ota_frame_type_t type, const u8 *payload, u16 payload_len,
u8 *out, u16 *out_len); u8 *out, u16 *out_len);
static bool tryParseFrame(const u8 *frame, u16 frame_len, static bool tryParseFrame(const u8 *frame, u16 frame_len,
@@ -83,6 +94,9 @@ private:
static void exitCritical(uint32_t primask); static void exitCritical(uint32_t primask);
}; };
/* OTA.cpp 通过此变量更新 SPI 响应帧的 CREDIT 字段 */
extern volatile u8 g_localCredit;
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
+10 -1
View File
@@ -1,4 +1,4 @@
#include "main.h" #include "main.h"
#include "device/Can.h" #include "device/Can.h"
#include "device/SpiOta.h" #include "device/SpiOta.h"
#include "sys/config.h" #include "sys/config.h"
@@ -333,6 +333,15 @@ void BootloaderSystem::run() {
} }
int main(void) { int main(void) {
/* 方案A: 极早夺回 PB6 控制权,拉低 SPIready 线。
* 复位后 GPIO 高阻,外部上拉会短暂拉高 PB6 (μs级);
* 此处尽快配为推挽输出 LOW,让 ESP32 能通过 LOW→HIGH 序列
* 识别 "Bootloader 已接管" → "Bootloader 就绪"。
* 后续 SpiOta::configureGpio() 会重配为开漏并释放 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.initHardware();
sys.run(); sys.run();
+89 -31
View File
@@ -1,4 +1,4 @@
#include "OTA.h" #include "OTA.h"
#include "Can.h" #include "Can.h"
#include "SpiOta.h" #include "SpiOta.h"
#include "aes128.h" #include "aes128.h"
@@ -242,6 +242,11 @@ static inline void write_u32_to_4bytes(uint8_t *array, uint32_t value) {
*/ */
static inline void send_structured_ack(uint32_t seq_or_flag) { static inline void send_structured_ack(uint32_t seq_or_flag) {
u8 send_data[6] = {0xBA, 0xC0, 0x00, 0x00, 0x00, 0x00}; u8 send_data[6] = {0xBA, 0xC0, 0x00, 0x00, 0x00, 0x00};
#if DEBUG_PRINT
if (seq_or_flag <= 5U || seq_or_flag == 0xF0F0F0F0U) {
printf("[OTA] send_structured_ack seq=%u\r\n", (unsigned)seq_or_flag);
}
#endif
write_u32_to_4bytes(&send_data[2], seq_or_flag); write_u32_to_4bytes(&send_data[2], seq_or_flag);
send_ack_frame(6, send_data); send_ack_frame(6, send_data);
} }
@@ -317,6 +322,39 @@ void OTAHandler::process() {
pending_finish_ack = false; pending_finish_ack = false;
} }
/** FINISH_OTA 已在 ISR 里回 ACK,等 Flash 缓冲 drain 完再做校验/CRC/写标志/跳转,
* 避免在 CS 释放 ISR 里长时间阻塞 SPIready。 */
if (pending_finish_ota && flash_buf_cnt == 0U) {
pending_finish_ota = false;
if (!validateApplication()) {
ota_error = OTA_ERROR_FLASH_ERROR;
ota_state = OTA_STATE_IDLE;
send_error_frame(0xFFFFFFFFU, OTA_ERROR_FLASH_ERROR);
} else {
/** 计算并写入 A区固件 CRC32,供后续启动时完整性校验。 */
u32 latest_crc = calculateAppCrc(APPLATEST_START_ADDR, APP_FIRMWARE_SIZE / 4U);
writeAppCrc(APPLATEST_START_ADDR, latest_crc);
OTA_SET(n_APP_LATEST_CHECK_ADDR, OTA_AVAL);
OTA_SET(n_APP_LATEST_AVAL_ADDR, OTA_AVAL);
OTA_SET(n_APP_LATEST_EN_ADDR, OTA_AVAL);
#if DEBUG_PRINT
/* OTA 完成后立即 dump Reset_Handler 地址 */
{
uint32_t rh_addr = *(__IO uint32_t *)(APPLATEST_START_ADDR + 4U) & ~1U;
printf("[OTA-DONE2] RH@0x%08lX: ", (unsigned long)rh_addr);
const uint8_t *p = (const uint8_t *)rh_addr;
for (int i = 0; i < 16; i++) printf("%02X ", p[i]);
printf("\r\n");
}
#endif
pending_app_jump = true;
}
}
if (pending_app_jump) { if (pending_app_jump) {
pending_app_jump = false; pending_app_jump = false;
delay_1ms(20); delay_1ms(20);
@@ -580,6 +618,11 @@ void OTAHandler::drainFlashBuffer() {
#if DEBUG_PRINT #if DEBUG_PRINT
u32 drain_cnt = 0; u32 drain_cnt = 0;
#endif #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);
while (1) { while (1) {
FlashSlot slot = {}; FlashSlot slot = {};
bool has_slot = false; bool has_slot = false;
@@ -596,6 +639,8 @@ void OTAHandler::drainFlashBuffer() {
has_slot = true; has_slot = true;
} }
__enable_irq(); __enable_irq();
/* 缓冲被消费,恢复 CREDIT 通知 ESP32 可继续发数据。 */
g_localCredit = (u8)(kFlashBufSlots - flash_buf_cnt);
if (!has_slot) { if (!has_slot) {
break; break;
@@ -606,6 +651,10 @@ void OTAHandler::drainFlashBuffer() {
drain_cnt++; drain_cnt++;
#endif #endif
} }
/* Flash 写入完成,恢复 CREDIT 并释放 PB6。
* ESP32 检测到 PB6 拉高后会继续发数据包。 */
g_localCredit = (u8)(kFlashBufSlots - flash_buf_cnt);
SpiOta::setSpiReady(true);
#if DEBUG_PRINT #if DEBUG_PRINT
if (drain_cnt > 0U) { if (drain_cnt > 0U) {
printf("[FW] drain %u pkts\r\n", (unsigned)drain_cnt); printf("[FW] drain %u pkts\r\n", (unsigned)drain_cnt);
@@ -694,15 +743,32 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
*/ */
if (len == 64U && ota_state == OTA_STATE_RECEIVING_FIRMWARE) { if (len == 64U && ota_state == OTA_STATE_RECEIVING_FIRMWARE) {
u32 receive_session_num = extract_u32_from_4bytes(data); u32 receive_session_num = extract_u32_from_4bytes(data);
(void)receive_session_num; u32 expected_session_num = extract_u32_from_4bytes(nonce_server);
if (receive_session_num != expected_session_num) {
ota_error = OTA_ERROR_INVALID_PACKET;
send_error_frame(extract_u32_from_4bytes(data + 4), OTA_ERROR_INVALID_PACKET);
return;
}
u32 receive_packet_num = extract_u32_from_4bytes(data + 4); u32 receive_packet_num = extract_u32_from_4bytes(data + 4);
u32 receive_IFEND = extract_u32_from_4bytes(data + 56); u32 receive_IFEND = extract_u32_from_4bytes(data + 56);
#if DEBUG_PRINT
if (receive_packet_num <= 5U) {
printf("[OTA] DATA in: seq=%u next=%u flash_cnt=%u\r\n",
(unsigned)receive_packet_num, (unsigned)next_expected_packet, (unsigned)flash_buf_cnt);
}
#endif
const u8 *receive_encrypted_data = data + 8; const u8 *receive_encrypted_data = data + 8;
const u8 *receive_dataCRC32 = data + 60; const u8 *receive_dataCRC32 = data + 60;
/** 去重:跳过已经确认接收过的包号(ESP32 超时重传)。 */ /** 去重:跳过已经确认接收过的包号(ESP32 超时重传)。 */
if (receive_packet_num < next_expected_packet) { if (receive_packet_num < next_expected_packet) {
u32 request_packet = receive_packet_num + 1U; u32 request_packet = receive_packet_num + 1U;
#if DEBUG_PRINT
if (receive_packet_num <= 5U) {
printf("[OTA] DATA dup seq=%u (next=%u) -> ack %u\r\n",
(unsigned)receive_packet_num, (unsigned)next_expected_packet, (unsigned)request_packet);
}
#endif
send_structured_ack(request_packet); send_structured_ack(request_packet);
return; return;
} }
@@ -717,6 +783,9 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
/** 校验分包 CRC32(密钥参与 CRC,防止简单重放/篡改)。 */ /** 校验分包 CRC32(密钥参与 CRC,防止简单重放/篡改)。 */
calculate_crc32_with_key(current_K_session, decrypted_data, calculated_dataCRC32); calculate_crc32_with_key(current_K_session, decrypted_data, calculated_dataCRC32);
if (memcmp(receive_dataCRC32, calculated_dataCRC32, SIZE_U32) != 0) { if (memcmp(receive_dataCRC32, calculated_dataCRC32, SIZE_U32) != 0) {
#if DEBUG_PRINT
printf("[OTA] DATA crc err seq=%u\r\n", (unsigned)receive_packet_num);
#endif
ota_error = OTA_ERROR_INVALID_CRC; ota_error = OTA_ERROR_INVALID_CRC;
send_error_frame(receive_packet_num, OTA_ERROR_INVALID_CRC); send_error_frame(receive_packet_num, OTA_ERROR_INVALID_CRC);
return; return;
@@ -729,6 +798,7 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
return; return;
} }
if (flash_buf_cnt >= kFlashBufSlots) { if (flash_buf_cnt >= kFlashBufSlots) {
g_localCredit = 0U;
send_wait_frame(1, (const u8 *)"\x03"); send_wait_frame(1, (const u8 *)"\x03");
return; return;
} }
@@ -739,7 +809,17 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
__disable_irq(); __disable_irq();
flash_buf_cnt++; flash_buf_cnt++;
__enable_irq(); __enable_irq();
/* 更新 CREDIT:剩余可接收槽位。缓冲将满时置 0,
* ESP32 收到 CREDIT=0 后会改为 POLL 等待而非盲目重发。 */
g_localCredit = (flash_buf_cnt >= kFlashBufSlots) ? 0U
: (u8)(kFlashBufSlots - flash_buf_cnt);
next_expected_packet = receive_packet_num + 1U; next_expected_packet = receive_packet_num + 1U;
#if DEBUG_PRINT
if (receive_packet_num <= 5U) {
printf("[OTA] DATA ok seq=%u -> ack %u\r\n",
(unsigned)receive_packet_num, (unsigned)next_expected_packet);
}
#endif
} }
/** /**
@@ -788,35 +868,13 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
/** 结束态下要求显式收到 0x0A AC,确认可以切换到 APP。 */ /** 结束态下要求显式收到 0x0A AC,确认可以切换到 APP。 */
else if (len == 2U && ota_state == OTA_STATE_FINISHED) { else if (len == 2U && ota_state == OTA_STATE_FINISHED) {
if (data[0] == 0x0AU && data[1] == 0xACU) { if (data[0] == 0x0AU && data[1] == 0xACU) {
if (!validateApplication()) {
ota_error = OTA_ERROR_FLASH_ERROR;
ota_state = OTA_STATE_IDLE;
send_error_frame(0xFFFFFFFFU, OTA_ERROR_FLASH_ERROR);
return;
}
/** 计算并写入 A区固件 CRC32,供后续启动时完整性校验。 */
u32 latest_crc = calculateAppCrc(APPLATEST_START_ADDR, APP_FIRMWARE_SIZE / 4U);
writeAppCrc(APPLATEST_START_ADDR, latest_crc);
OTA_SET(n_APP_LATEST_CHECK_ADDR, OTA_AVAL);
OTA_SET(n_APP_LATEST_AVAL_ADDR, OTA_AVAL);
OTA_SET(n_APP_LATEST_EN_ADDR, OTA_AVAL);
#if DEBUG_PRINT
/* OTA 完成后立即 dump Reset_Handler 地址 */
{
uint32_t rh_addr = *(__IO uint32_t *)(APPLATEST_START_ADDR + 4U) & ~1U;
printf("[OTA-DONE2] RH@0x%08lX: ", (unsigned long)rh_addr);
const uint8_t *p = (const uint8_t *)rh_addr;
for (int i = 0; i < 16; i++) printf("%02X ", p[i]);
printf("\r\n");
}
#endif
u8 finish_ack[2] = {0x0A, 0xAC}; u8 finish_ack[2] = {0x0A, 0xAC};
send_ack_frame(2, finish_ack); send_ack_frame(2, finish_ack);
pending_app_jump = true; /** 重活放到主循环 process() 里做,ISR 只回 ACK 并置标志,
* 避免 SPIready 被拉低太久导致 ESP32 超时。 */
if (!pending_finish_ota) {
pending_finish_ota = true;
}
return; return;
} }
@@ -836,8 +894,8 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
else { else {
ota_error = OTA_ERROR_INVALID_PACKET; ota_error = OTA_ERROR_INVALID_PACKET;
#if DEBUG_PRINT #if DEBUG_PRINT
printf("[OTA] INV_PKT: l=%u s=%u\r\n", printf("[OTA] DATA ignored/INV_PKT: type=%u len=%u state=%u\r\n",
(unsigned)len, (unsigned)ota_state); (unsigned)type, (unsigned)len, (unsigned)ota_state);
#endif #endif
ota_state = OTA_STATE_IDLE; ota_state = OTA_STATE_IDLE;
send_error_frame(0xFFFFFFFFU, OTA_ERROR_INVALID_PACKET); send_error_frame(0xFFFFFFFFU, OTA_ERROR_INVALID_PACKET);
+6 -2
View File
@@ -328,9 +328,13 @@ extern "C" void EXTI5_9_IRQHandler(void)
/** /**
* @brief External interrupt handler for EXTI line group 10 to 15. * @brief External interrupt handler for EXTI line group 10 to 15.
* @return none *
* PA15 (SPI CS from ESP32) maps to EXTI_15, which fires this handler.
* Must directly dispatch to the SPI OTA CS handler — NOT through
* BootSleepManager::onExtiInterrupt(), which only handles EXTI_8
* (PB8 deep-sleep wake) and knows nothing about EXTI_15.
*/ */
extern "C" void EXTI10_15_IRQHandler(void) extern "C" void EXTI10_15_IRQHandler(void)
{ {
BootSleepManager::onExtiInterrupt(); SpiOta::onCsExtiInterrupt();
} }
+12
View File
@@ -11,6 +11,7 @@
*/ */
volatile uint32_t SysTickTimer::s_delayCountdown = 0U; volatile uint32_t SysTickTimer::s_delayCountdown = 0U;
volatile uint32_t SysTickTimer::s_msTicks = 0U;
/** /**
* @brief 配置 SysTick 为 1ms 周期中断。 * @brief 配置 SysTick 为 1ms 周期中断。
@@ -87,6 +88,7 @@ void SysTickTimer::decrement()
if (s_delayCountdown != 0U) { if (s_delayCountdown != 0U) {
s_delayCountdown--; s_delayCountdown--;
} }
s_msTicks++;
} }
/** /**
@@ -115,6 +117,16 @@ void SysTickTimer::softReset()
} }
} }
/**
* @brief 获取上电以来的毫秒数。
*
* @return 毫秒计数。
*/
uint32_t SysTickTimer::getMs()
{
return s_msTicks;
}
/** /**
* @brief Configure SysTick via the timer helper. * @brief Configure SysTick via the timer helper.
* @return none * @return none
+5 -1
View File
@@ -1,4 +1,4 @@
#ifndef SYSTICK_H #ifndef SYSTICK_H
#define SYSTICK_H #define SYSTICK_H
#ifdef __cplusplus #ifdef __cplusplus
@@ -28,9 +28,13 @@ public:
static void onInterrupt(); static void onInterrupt();
/** @brief 触发软件复位。 */ /** @brief 触发软件复位。 */
static void softReset(); static void softReset();
/** @brief 获取上电以来的毫秒数。
* @note 供按铃超时等非阻塞超时场景使用,不用于精确阻塞延时。 */
static uint32_t getMs();
private: private:
static volatile uint32_t s_delayCountdown; static volatile uint32_t s_delayCountdown;
static volatile uint32_t s_msTicks;
}; };
/** /**