Compare commits

..
4 Commits
Author SHA1 Message Date
cbb ea7a885de4 fix(spi-ota): SPI刷写初版已可完整进行刷写流程
- 在SPI OTA ISR处理函数入口立即设置已响应标志,防止帧被重复处理,避免状态机错误推进
- 修正DMA启动前预填SPI发送缓冲区,覆盖旧字节,解决MISO线输出旧ACK字节导致序号错位问题
- 改为SPI从机内部NSS固定低电平,防止因EXTI延迟丢失首字节,提升帧接收稳定性
- 优化CS下降沿处理流程,确保DMA停止后重新配置并启动,避免数据丢失和标志位误触发
- CS上升沿停止DMA但保持内部NSS低电平,提高下一次事务的接收连续性
- SPI中断处理加入对上次事务提前检测和补充分支,增强系统鲁棒性
- 降低调试打印频率,减少ISR日志开销
- 修复看门狗复位检测代码,阻止BOOT跳转至卡死的APP,进入等待OTA修复模式
- 在工程配置文件中添加bin格式输出支持
2026-05-30 00:35:56 +08:00
cbb 8590d327e6 feat(ota): 优化握手请求处理及OTA会话状态管理
- 新增 OTA 会话激活状态标志 s_otaSessionActive 及其访问接口
- 在设置传输发送器时激活 OTA 会话状态
- OTAHandler 初始化时显式重置状态机,防止旧状态残留
- 严格区分握手请求 payload,避免 SPI OTA 握手误判状态
- 允许更多 OTA 状态接收握手请求,提升协议容错性
- 非空闲状态拒绝新握手请求并发送等待响应
2026-05-28 23:39:52 +08:00
cbb 9d4572da13 fix(spi-ota): 修复SPI OTA从机接收和状态机处理逻辑
- 调整pollAllLinks执行顺序,先轮询SPI再轮询CAN,减少CAN阻塞对SPI的影响
- SPI内部NSS固定拉低,避免因NSS延迟导致首字节丢失问题
- onCsAssert中全面停止和重启DMA,确保DMA传输第一拍正确发出
- 修正CS断开时DMA停止逻辑,保持内部NSS常低,提升SPI从机响应稳定性
- 增加多缓冲ISR日志区,提升关键事件调试信息完整性
- 完善SPI中断流程,支持缺失EXTI中断时补做CS断言,优化首字节数据处理
- 实现tryProcessCompleteFrame函数,实现RBNE中断内即时帧检测和处理
- OTA模块增加getState和resetToIdle接口,支持BOOT命令重置OTA状态机
- 开启DEBUG_PRINT,启用调试打印输出,便于运行时状态监控和问题排查
2026-05-28 21:52:07 +08:00
cbb d1beb3b615 feat(device): SPI OTA模块改用DMA发送替代TBE中断
SPI OTA模块从PendSV延迟响应模式改为DMA发送模式,主要改进包括:

- 添加DMA支持,使用DMA0_CH2自动发送SPI0数据,消除字节错位问题
- EXTI ISR直接完成帧校验+处理+响应准备,不再依赖PendSV
- 移除PendSV相关代码和中断处理函数
- CS释放后直接处理帧数据,响应帧写入s_txBuf由DMA自动发送
- 更新注释文档说明新的工作流程和DMA通道映射关系
2026-05-26 02:44:25 +08:00
9 changed files with 1849 additions and 183 deletions
+1
View File
@@ -28,6 +28,7 @@ project:
type: type:
- elf - elf
- hex - hex
- bin
- map - map
components: components:
+350 -88
View File
@@ -5,18 +5,39 @@
#include "gd32e50x_rcu.h" #include "gd32e50x_rcu.h"
#include "gd32e50x_misc.h" #include "gd32e50x_misc.h"
#include "gd32e50x_spi.h" #include "gd32e50x_spi.h"
#include "gd32e50x_dma.h"
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
/** /**
* @file SpiOta.cpp * @file SpiOta.cpp
* @brief Bootloader SPI OTA 从机模块实现(PendSV 延迟响应版)。 * @brief Bootloader SPI OTA 从机模块实现(DMA 发送版)。
* *
* CS 释放后,EXTI ISR 只做轻量校验(SOF + LEN + CRC), * CS 释放后,EXTI ISR 直接完成帧校验+处理+响应准备,
* 校验通过则触发 PendSV(最低优先级 ISR),由 PendSV ISR 调用 * 响应帧写入 s_txBuf,DMA0_CH2 自动推到 SPI0 TX 寄存器。
* handleLinkMessage 完成帧处理和响应准备。 * 硬件 DMA 替代 TBE 中断逐字节发送,消除字节错位问题。
*
* 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 固定映射) */
#define SPI_TX_DMA_PERIPH DMA0
#define SPI_TX_DMA_CH DMA_CH2
/* 调试:多缓冲日志(8 条 ISR 日志缓冲区,避免关键事件丢失) */
static constexpr u16 kLogSlotCount = 8U;
static constexpr u16 kLogSlotSize = 80U;
static u8 s_logSlots[kLogSlotCount][kLogSlotSize] = {};
static u16 s_logSlotLens[kLogSlotCount] = {};
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; OTAHandler *SpiOta::s_otaHandler = nullptr;
volatile bool SpiOta::s_csActive = false; volatile bool SpiOta::s_csActive = false;
volatile u16 SpiOta::s_rxStreamLen = 0U; volatile u16 SpiOta::s_rxStreamLen = 0U;
@@ -25,10 +46,11 @@ u8 SpiOta::s_rxStream[SpiOta::kRxBufSize] = {};
u8 SpiOta::s_txBuf[SpiOta::kTxBufSize] = {}; u8 SpiOta::s_txBuf[SpiOta::kTxBufSize] = {};
volatile u16 SpiOta::s_txLen = 0U; 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_isrLogPending = false; volatile bool SpiOta::s_isrLogPending = false;
volatile bool SpiOta::s_otaSessionActive = false;
u8 SpiOta::s_isrLogBuf[kIsrLogBufSize] = {}; u8 SpiOta::s_isrLogBuf[kIsrLogBufSize] = {};
u16 SpiOta::s_isrLogLen = 0U; u16 SpiOta::s_isrLogLen = 0U;
volatile bool SpiOta::s_pendSVPending = false;
/* ============================================================ /* ============================================================
* CRC8 / 帧构建 / 帧解析 * CRC8 / 帧构建 / 帧解析
@@ -80,7 +102,7 @@ ota_link_msg_type_t SpiOta::mapMsgType(spi_ota_frame_type_t type) {
} }
/* ============================================================ /* ============================================================
* 回包发送适配器(ISR 安全) * 回包发送适配器(ISR 安全)
* ============================================================ */ * ============================================================ */
void SpiOta::sender(ota_link_tx_type_t type, u8 len, const u8 *data) { void SpiOta::sender(ota_link_tx_type_t type, u8 len, const u8 *data) {
@@ -103,28 +125,87 @@ void SpiOta::exitCritical(uint32_t primask) {
__set_PRIMASK(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;
}
void SpiOta::queueTxFrame(const u8 *frame, u16 len) { void SpiOta::queueTxFrame(const u8 *frame, u16 len) {
if (!frame || len == 0U || len > kTxBufSize) { return; } if (!frame || len == 0U || len > kTxBufSize) { return; }
bool enable_tbe = false;
uint32_t primask = enterCritical(); uint32_t primask = enterCritical();
memcpy(s_txBuf, frame, len); memcpy(s_txBuf, frame, len);
s_txLen = len; s_txLen = len;
s_txPending = true; s_txPending = true;
s_txStreamPos = 0U; s_txStreamPos = 0U;
enable_tbe = s_csActive; if (len < kTxBufSize) { memset(&s_txBuf[len], 0xFF, kTxBufSize - len); }
exitCritical(primask); exitCritical(primask);
if (enable_tbe) { spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_TBE); } spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);
startSpiTxDma(s_txBuf, s_txLen);
} }
/* ============================================================ /* ============================================================
* EXTI ISR:轻量校验 + 触发 PendSV * RBNE 中断内即时帧检测与处理
* ============================================================ */ * ============================================================ */
void SpiOta::isrValidateAndPend() { void SpiOta::tryProcessCompleteFrame() {
u16 rx_len = s_rxStreamLen; u16 rx_len = s_rxStreamLen;
if (rx_len < kFrameOverhead) { return; } if (rx_len == 0U) { return; }
if (s_rxStream[0] == SPI_OTA_CMD_BOOT) {
isrProcessFrame();
return;
}
if (s_rxStream[0] != kSof) { 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();
}
/* ============================================================
* CS 释放后 ISR 直接帧处理
* ============================================================ */
void SpiOta::isrProcessFrame() {
u16 rx_len = s_rxStreamLen;
if (rx_len == 0U) { return; }
/* 关键修复:一旦开始处理本次 CS 周期中的完整帧,立刻标记“已响应/已处理”。
*
* 否则会出现这样的竞态:
* 1) SPI0 RBNE IRQ 在收到最后一个字节后调用 isrProcessFrame()
* 2) 还没执行到函数尾部的 s_isrResponded = true
* 3) CS 已经上升,优先级更高的 EXTI IRQ 抢占进来执行 onCsRelease()
* 4) onCsRelease() 看到 s_isrResponded 仍为 false,于是再次调用 isrProcessFrame()
* 5) 同一帧被重复处理一次,状态机被错误推进(如 challenge 又被当 confirm)
*
* 所以必须在函数入口就先置位,防止 REQ/DATA 被双重分发。 */
s_isrResponded = true;
if (rx_len >= 1U && s_rxStream[0] == SPI_OTA_CMD_BOOT) {
u8 ack[4] = {0xB0U, 0xA5U, 0x4FU, 0x54U};
queueTxFrame(ack, sizeof(ack));
if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); }
s_otaSessionActive = false;
s_rxStreamLen = 0U;
isrLogFrameEx("[BOOT-CMD]", s_txBuf, s_txLen);
s_isrResponded = true;
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 payload_len = (u16)(s_rxStream[2] | ((u16)s_rxStream[3] << 8));
u16 expected = (u16)(kFrameOverhead + payload_len); u16 expected = (u16)(kFrameOverhead + payload_len);
@@ -137,42 +218,53 @@ void SpiOta::isrValidateAndPend() {
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);
return; return;
} }
/* ★ 校验通过,触发 PendSV ★ */
s_pendSVPending = true;
SCB->ICSR = SCB_ICSR_PENDSVSET_Msk;
isrLogFrame("[EXTI-OK]", s_rxStream, expected);
}
/* ============================================================
* PendSV ISR:在低优先级 ISR 中完成帧处理
* ============================================================ */
void SpiOta::onPendSvProcess() {
if (!s_pendSVPending) { return; }
s_pendSVPending = false;
u16 frame_len = s_rxStreamLen;
spi_ota_frame_type_t frame_type; spi_ota_frame_type_t frame_type;
const u8 *payload = nullptr; const u8 *payload = nullptr;
u16 payload_len = 0U; u16 p_len = 0U;
if (!tryParseFrame(s_rxStream, expected, &frame_type, &payload, &p_len)) {
if (!tryParseFrame(s_rxStream, frame_len, &frame_type, &payload, &payload_len)) { isrLogFrameEx("[PARSE-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
return; return;
} }
if (s_otaHandler == nullptr) { return; } if (frame_type == SPI_OTA_CMD_BOOT) {
u8 ack_ok = 0xA5U;
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(); }
s_otaSessionActive = false;
s_rxStreamLen = 0U;
s_isrResponded = true;
return;
}
if (s_otaHandler != nullptr) {
s_otaSessionActive = true;
s_otaHandler->setTransportSender(sender); s_otaHandler->setTransportSender(sender);
ota_link_msg_type_t msg_type = mapMsgType(frame_type); ota_link_msg_type_t msg_type = mapMsgType(frame_type);
s_otaHandler->handleLinkMessage(msg_type, const_cast<u8 *>(payload), (u8)payload_len); /* 精简诊断: 只记录帧类型+状态, 减少ISR日志开销 */
{
u8 diag[3] = {(u8)frame_type, (u8)(s_otaHandler ? s_otaHandler->getState() : 0xFFU),
(u8)p_len};
isrLogFrameEx("[OT]", diag, 3);
}
s_otaHandler->handleLinkMessage(msg_type, const_cast<u8 *>(payload), (u8)p_len);
}
isrLogFrame("[PendSV-DONE]", s_txBuf, s_txLen); /* 精简: 只在非DATA包时记录响应帧内容, DATA包只记录ACK的seq */
if (frame_type != SPI_OTA_CMD_DATA) {
isrLogFrameEx("[RD]", s_txBuf, (s_txLen < 12U) ? s_txLen : 12U);
} else {
/* DATA包响应: 只记录payload前6字节(=BAC0+seq) */
isrLogFrameEx("[RD]", s_txBuf + 4, (s_txLen > 10U) ? 6U : (s_txLen > 4U ? s_txLen - 4U : 0U));
}
s_isrResponded = true;
} }
/* ============================================================ /* ============================================================
@@ -180,21 +272,27 @@ void SpiOta::onPendSvProcess() {
* ============================================================ */ * ============================================================ */
void SpiOta::isrLogFrame(const char *tag, const u8 *data, u16 len) { void SpiOta::isrLogFrame(const char *tag, const u8 *data, u16 len) {
if (s_isrLogPending) { return; } 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; u16 pos = 0U;
if (tag) { if (tag) {
while (*tag && pos < kIsrLogBufSize - 1U) { s_isrLogBuf[pos++] = (u8)*tag++; } while (*tag && pos < kLogSlotSize - 3U) { buf[pos++] = (u8)*tag++; }
s_isrLogBuf[pos++] = ' '; buf[pos++] = ' ';
} }
const u16 preview = (len < 32U) ? len : 32U; const u16 preview = (len < 24U) ? len : 24U;
for (u16 i = 0U; i < preview && pos + 2U < kIsrLogBufSize; ++i) { for (u16 i = 0U; i < preview && pos + 2U < kLogSlotSize; ++i) {
static const char hx[] = "0123456789ABCDEF"; static const char hx[] = "0123456789ABCDEF";
s_isrLogBuf[pos++] = (u8)hx[data[i] >> 4]; buf[pos++] = (u8)hx[data[i] >> 4];
s_isrLogBuf[pos++] = (u8)hx[data[i] & 0x0FU]; buf[pos++] = (u8)hx[data[i] & 0x0FU];
} }
s_isrLogBuf[pos] = 0U; buf[pos] = 0U;
s_isrLogLen = pos; s_logSlotLens[s_logWriteIdx] = pos;
s_isrLogPending = true; s_logWriteIdx = (s_logWriteIdx + 1U) % kLogSlotCount;
s_logCount++;
} }
/* ============================================================ /* ============================================================
@@ -216,6 +314,7 @@ void SpiOta::configureGpio() {
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);
(void)spi_i2s_data_receive(SPI0); /* 清空 SPI_DATA 残留值 */
spi_parameter_struct spi_init_struct; spi_parameter_struct spi_init_struct;
spi_struct_para_init(&spi_init_struct); spi_struct_para_init(&spi_init_struct);
spi_init_struct.device_mode = SPI_SLAVE; spi_init_struct.device_mode = SPI_SLAVE;
@@ -223,25 +322,67 @@ void SpiOta::configureSpi0() {
spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT;
spi_init_struct.nss = SPI_NSS_SOFT; spi_init_struct.nss = SPI_NSS_SOFT;
spi_init_struct.endian = SPI_ENDIAN_MSB; 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.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;
spi_init_struct.prescale = SPI_PSC_8; spi_init_struct.prescale = SPI_PSC_8;
spi_init(SPI0, &spi_init_struct); 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_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_RBNE);
spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_ERR); spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_ERR);
spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_TBE); spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_TBE);
nvic_irq_enable(SPI0_IRQn, 1U, 0U); /* 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); spi_enable(SPI0);
} }
void SpiOta::configureDma() {
rcu_periph_clock_enable(RCU_DMA0);
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);
}
void SpiOta::configureExti() { void SpiOta::configureExti() {
gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15); gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15);
exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH); exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH);
exti_interrupt_enable(EXTI_15); exti_interrupt_enable(EXTI_15);
exti_interrupt_flag_clear(EXTI_15); exti_interrupt_flag_clear(EXTI_15);
nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U); nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U);
/* PendSV 优先级设最低(不阻塞 SPI/EXTI),通过 SCB->ICSR 触发,无需 NVIC_EnableIRQ */
NVIC_SetPriority(PendSV_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL);
} }
void SpiOta::init() { void SpiOta::init() {
@@ -251,10 +392,11 @@ void SpiOta::init() {
s_txLen = 0U; s_txLen = 0U;
s_txPending = false; s_txPending = false;
s_isrLogPending = false; s_isrLogPending = false;
s_pendSVPending = false; s_otaSessionActive = false;
memset(s_rxStream, 0, sizeof(s_rxStream)); memset(s_rxStream, 0, sizeof(s_rxStream));
memset(s_txBuf, 0xFF, sizeof(s_txBuf)); memset(s_txBuf, 0xFF, sizeof(s_txBuf));
configureGpio(); configureGpio();
configureDma();
configureSpi0(); configureSpi0();
configureExti(); configureExti();
} }
@@ -268,46 +410,155 @@ void SpiOta::onCsAssert() {
s_csActive = true; s_csActive = true;
s_rxStreamLen = 0U; s_rxStreamLen = 0U;
s_txStreamPos = 0U; s_txStreamPos = 0U;
if (s_txPending && s_txLen > 0U) { s_isrResponded = false;
spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_TBE);
} /* 内部 NSS 已在初始化时常置 LOW,这里不再依赖 EXTI 时序切 NSS。
* PA15 仅作为“事务开始”的边界信号,用于清接收缓冲和重装 DMA。 */
/* 关键修复:先彻底停止 DMA,再重新配置并启动。
*
* 上一版预填 SPI_DATA 方案失败原因:
* spi_i2s_data_transmit() 写入的字节进入 TBE 发送缓冲区。
* 当 ESP32 第一个 SCK 边沿到来时,TBE 的预填字节被移入移位
* 寄存器推到 MISO,同时移位寄存器中的 MOSI 数据被推入 RBNE。
* 但 GD32E50x SPI 的 RBNE 标志在 SPI_DATA 写入后可能被误触发,
* 导致 isrLogFrame 报出预填值(如 0x80)而非 MOSI 真实数据。
*
* 正确方案:不预填 SPI_DATA,而是确保 DMA 在 CS↓ 后、ESP32
* 第一个 SCK 边沿之前就绪。DMA 第一拍自动把 s_txBuf[0] 写入
* SPI_DATA,不会产生 RBNE 误触发。
*
* 操作顺序:
* 1. 禁 DMA 通道 + 禁 SPI DMA 请求(彻底停止旧传输)
* 2. 清 RBNE / RXORERR / FERR 等残留标志
* 3. 配置并启动 DMA(此时 RBNE 仍关闭,DMA 写 SPI_DATA 不会触发中断)
* 4. 重新使能 RBNE 中断,开始接收 MOSI 数据
*/
/* 1. 停止旧 DMA */
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN;
spi_dma_disable(SPI0, SPI_DMA_TRANSMIT);
/* 1.5 覆盖 SPI 发送缓冲区 (TBE) 中的旧字节
*
* 关键修复:CS 高期间,queueTxFrame() 会把新 ACK 帧写入 s_txBuf
* 并启动 DMA,DMA 把 s_txBuf[0] 写入 SPI_DATA (TBE 缓冲区)。
* CS 高时没有外部 SCK,TBE 中的字节停留在发送缓冲区不会被移位输出。
*
* 当 ESP32 下次 CS↓ 时,onCsAssert() 重启 DMA,但 TBE 中已经有
* 旧字节。如果 ESP32 第一个 SCK 边沿在 DMA 写入新数据之前到达,
* MISO 上先输出 TBE 中的旧字节,然后 DMA 才开始推 s_txBuf[0]。
* 这导致 MISO 上第一个字节是旧 ACK 帧的残留,ESP32 的
* spi_ota_find_frame 先匹配到旧帧,拿到比期望少 1 的 seq 号
* ("差一拍" 现象)。
*
* 修复:在重启 DMA 前,写 0xFF 到 SPI_DATA 覆盖 TBE 中的旧字节。
* 这样即使 ESP32 第一个 SCK 先到,MISO 输出的是 0xFF 而非旧 ACK 残留。
* spi_ota_find_frame 会跳过 0xFF(不是 SOF=0xA5),从后面 DMA 推出的
* 正确帧开始匹配。
*/
spi_i2s_data_transmit(SPI0, 0xFFU);
/* 如果 TBE FIFO 有两层,再写一个 0xFF 确保覆盖 */
if (spi_i2s_flag_get(SPI0, SPI_FLAG_TBE) != RESET) {
spi_i2s_data_transmit(SPI0, 0xFFU);
} }
/* 2. 清残留错误标志
*
* 关键:这里绝不能在 CS↓ 时“顺手读掉 RBNE”。
*
* 因为 internal NSS 现在常 LOW,ESP32 在 CS↓ 后很快就会发出首字节 A5。
* 如果 EXTI ISR 略晚于 SPI 接收,A5 可能已经先进了 RX 缓冲并置位 RBNE。
* 这时若在 onCsAssert() 里执行 spi_i2s_data_receive(),就会把真实首字节 A5
* 当成“残留垃圾”吞掉,导致后续看到的首字节变成 0x01。
*
* 所以这里仅清真正的错误标志,不主动消费 RBNE。
* 已到达的首字节留给 onSpiInterrupt() 正常读走。 */
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(RBNE 仍关闭,DMA 写 SPI_DATA 不会触发中断) */
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() { void SpiOta::onCsRelease() {
if (!s_csActive) { return; } if (!s_csActive) { return; }
s_csActive = false; s_csActive = false;
spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_TBE);
if (s_rxStreamLen > 0U && s_rxStreamLen <= kRxBufSize) { /* 内部 NSS 保持常 LOW,不在 CS↑ 时切回 HIGH。
isrValidateAndPend(); * 这样 SPI 从机始终处于就绪态,下一次 CS↓ 不会因 EXTI 延迟丢首字节。
*
* 这里只停止 DMA,避免 CS 高期间继续空转消耗 s_txBuf 内容。
* s_txPending 保持不变——如果 ISR 已准备好 ACK 帧,
* 下次 CS↓ 时 onCsAssert 会重启 DMA 推出。 */
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN;
spi_dma_disable(SPI0, SPI_DMA_TRANSMIT);
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;
}
}
/* 关键调试日志:记录本次 CS 周期结束时到底收到了多少字节。
* 这样能直接判断:
* 1) 是不是完整收到了 ESP32 发来的整帧
* 2) 还是只收到了首字节 A5 / 少数字节,导致永远无法解析回包 */
/* 精简: CS-RX只记录前4字节, 减少ISR日志开销 */
if (s_rxStreamLen > 0U) {
isrLogFrame("[CR]", s_rxStream, (s_rxStreamLen < 4U) ? s_rxStreamLen : 4U);
}
if (s_rxStreamLen > 0U && !s_isrResponded) {
isrProcessFrame();
} }
} }
/* ============================================================ /* ============================================================
* SPI0 中断处理 * SPI0 中断处理 — 只处理 RBNE(接收)和错误,TBE 已由 DMA 替代
* ============================================================ */ * ============================================================ */
void SpiOta::onSpiInterrupt() { void SpiOta::onSpiInterrupt() {
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) { if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) {
if (!s_csActive && gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) { onCsAssert(); } /* 与 APP 侧稳定实现保持一致:
* 如果 EXTI 下降沿因为中断时序略晚到达,但此时 PA15 已经为低,
* 则在首个 RBNE 到来时立即补做 onCsAssert()。
*
* 关键限制:只能在“本事务尚未收过任何字节”时补做。
* 否则若 EXTI / SPI 时序偶发抖动,重复 onCsAssert() 会把
* s_rxStreamLen 清零,导致已经收到的半帧被冲掉,最终永远拼不成完整帧。 */
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); u8 byte = (u8)(spi_i2s_data_receive(SPI0) & 0xFFU);
if (s_csActive && s_rxStreamLen < kRxBufSize) { s_rxStream[s_rxStreamLen++] = byte; } if (s_rxStreamLen < kRxBufSize) {
s_rxStream[s_rxStreamLen++] = byte;
} }
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_TBE) != RESET) { /* 调试阶段先关闭 RBNE 首字节日志。
if (!s_csActive && gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) { onCsAssert(); } * 原因:ISR 日志缓冲是单槽的,RBNE 一旦先占位,
u8 tx_byte = 0xFFU; * onCsRelease() 里的 [CS-RX] 摘要日志就会被丢弃。
if (s_csActive && s_txPending && s_txStreamPos < s_txLen) { * 目前排查重点是“本次 CS 周期总共收到了哪些字节”,
tx_byte = s_txBuf[s_txStreamPos++]; * 所以优先保留 [CS-RX],暂时关闭 [RBNE]。 */
if (s_txStreamPos >= s_txLen) { tryProcessCompleteFrame();
s_txPending = false;
s_txLen = 0U;
s_txStreamPos = 0U;
spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_TBE);
}
} else {
spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_TBE);
}
spi_i2s_data_transmit(SPI0, tx_byte);
} }
/* 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;
@@ -330,11 +581,31 @@ void SpiOta::onCsExtiInterrupt() {
* ============================================================ */ * ============================================================ */
void SpiOta::poll() { void SpiOta::poll() {
if (s_isrLogPending) { if (s_isrResponded) {
printf("[SPI-BOOT] %s\r\n", s_isrLogBuf); s_isrResponded = false;
s_isrLogPending = false; }
/* 打印 ISR 日志缓冲区 */
while (s_logCount > 0U) {
__disable_irq();
if (s_logCount == 0U) { __enable_irq(); break; }
u8 *buf = s_logSlots[s_logReadIdx];
u8 nextRead = (s_logReadIdx + 1U) % kLogSlotCount;
s_logReadIdx = nextRead;
s_logCount--;
__enable_irq();
printf("[SPI] %s\r\n", buf);
} }
if (s_otaHandler != nullptr) { s_otaHandler->process(); } if (s_otaHandler != nullptr) { s_otaHandler->process(); }
/* 调试:定期打印 SPI 从机状态 — 降低频率, 每20000次才打印一次 */
static u32 dbg_tick = 0;
dbg_tick++;
if ((dbg_tick % 20000U) == 0U) {
printf("[SPI] st=%u rx=%u tx=%u ota=%u\r\n",
(unsigned)s_csActive, (unsigned)s_rxStreamLen,
(unsigned)s_txLen,
(unsigned)(s_otaHandler ? s_otaHandler->getState() : 0xFF));
}
} }
void SpiOta::setSender(OTAHandler *handler) { s_otaHandler = handler; } void SpiOta::setSender(OTAHandler *handler) { s_otaHandler = handler; }
@@ -348,6 +619,8 @@ void SpiOta::feedRxBytes(const u8 *data, u16 len) {
bool SpiOta::hasTxFrame() { return s_txPending; } bool SpiOta::hasTxFrame() { return s_txPending; }
bool SpiOta::hasActiveSession() { return s_otaSessionActive; }
u16 SpiOta::copyTxFrame(u8 *out, u16 max_len) { u16 SpiOta::copyTxFrame(u8 *out, u16 max_len) {
if (!out || max_len == 0U) { return 0U; } if (!out || max_len == 0U) { return 0U; }
uint32_t primask = enterCritical(); uint32_t primask = enterCritical();
@@ -379,17 +652,6 @@ void spi_ota_feed_rx_bytes(const u8 *data, u16 len) { SpiOta::feedRxBytes(data,
bool spi_ota_has_tx_frame(void) { return SpiOta::hasTxFrame(); } bool spi_ota_has_tx_frame(void) { return SpiOta::hasTxFrame(); }
u16 spi_ota_copy_tx_frame(u8 *out, u16 max_len) { return SpiOta::copyTxFrame(out, max_len); } u16 spi_ota_copy_tx_frame(u8 *out, u16 max_len) { return SpiOta::copyTxFrame(out, max_len); }
/**
* @brief SPI0 中断向量入口。
*/
extern "C" void SPI0_IRQHandler(void) { extern "C" void SPI0_IRQHandler(void) {
SpiOta::onSpiInterrupt(); SpiOta::onSpiInterrupt();
} }
/**
* @brief PendSV 中断向量入口。
* @note 调用 SpiOta::onPendSvProcess() 在低优先级 ISR 中完成帧处理。
*/
extern "C" void PendSV_Handler(void) {
SpiOta::onPendSvProcess();
}
@@ -0,0 +1,608 @@
#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 <stdio.h>
#include <string.h>
/**
* @file SpiOta.cpp
* @brief Bootloader SPI OTA 从机模块实现(DMA 发送版)。
*
* CS 释放后,EXTI ISR 直接完成帧校验+处理+响应准备,
* 响应帧写入 s_txBuf,DMA0_CH2 自动推到 SPI0 TX 寄存器。
* 硬件 DMA 替代 TBE 中断逐字节发送,消除字节错位问题。
*
* 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 固定映射) */
#define SPI_TX_DMA_PERIPH DMA0
#define SPI_TX_DMA_CH DMA_CH2
/* 调试:多缓冲日志(8 条 ISR 日志缓冲区,避免关键事件丢失) */
static constexpr u16 kLogSlotCount = 8U;
static constexpr u16 kLogSlotSize = 80U;
static u8 s_logSlots[kLogSlotCount][kLogSlotSize] = {};
static u16 s_logSlotLens[kLogSlotCount] = {};
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_isrLogPending = false;
volatile bool SpiOta::s_otaSessionActive = false;
u8 SpiOta::s_isrLogBuf[kIsrLogBufSize] = {};
u16 SpiOta::s_isrLogLen = 0U;
/* ============================================================
* CRC8 / 帧构建 / 帧解析
* ============================================================ */
u8 SpiOta::crc8(const u8 *data, u16 len) {
u8 crc = 0x00U;
for (u16 i = 0; i < len; ++i) { crc ^= data[i]; }
return crc;
}
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)(kFrameOverhead + payload_len);
if (total_len > kTxBufSize) { return false; }
out[0] = kSof;
out[1] = (u8)type;
out[2] = (u8)(payload_len & 0xFFU);
out[3] = (u8)((payload_len >> 8) & 0xFFU);
if (payload && payload_len > 0U) { memcpy(&out[4], payload, payload_len); }
out[4 + payload_len] = crc8(out, (u16)(4U + payload_len));
*out_len = total_len;
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 < kFrameOverhead) { return false; }
if (frame[0] != kSof) { return false; }
u16 len = (u16)(frame[2] | ((u16)frame[3] << 8));
u16 expected = (u16)(kFrameOverhead + len);
if (expected != frame_len) { return false; }
if (crc8(frame, (u16)(4U + len)) != frame[4U + len]) { return false; }
if (type) { *type = (spi_ota_frame_type_t)frame[1]; }
if (payload) { *payload = &frame[4]; }
if (payload_len){ *payload_len = len; }
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)) {
queueTxFrame(frame_buf, built_len);
}
}
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;
}
void SpiOta::queueTxFrame(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);
spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);
startSpiTxDma(s_txBuf, s_txLen);
}
/* ============================================================
* RBNE 中断内即时帧检测与处理
* ============================================================ */
void SpiOta::tryProcessCompleteFrame() {
u16 rx_len = s_rxStreamLen;
if (rx_len == 0U) { return; }
if (s_rxStream[0] == SPI_OTA_CMD_BOOT) {
isrProcessFrame();
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();
}
/* ============================================================
* CS 释放后 ISR 直接帧处理
* ============================================================ */
void SpiOta::isrProcessFrame() {
u16 rx_len = s_rxStreamLen;
if (rx_len == 0U) { return; }
if (rx_len >= 1U && s_rxStream[0] == SPI_OTA_CMD_BOOT) {
u8 ack[4] = {0xB0U, 0xA5U, 0x4FU, 0x54U};
queueTxFrame(ack, sizeof(ack));
if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); }
isrLogFrameEx("[BOOT-CMD]", s_txBuf, s_txLen);
s_isrResponded = true;
return;
}
if (s_rxStream[0] != kSof) {
isrLogFrameEx("[NO-SOF]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
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 > rx_len) { return; }
if (crc8(s_rxStream, (u16)(4U + payload_len)) != s_rxStream[4U + payload_len]) {
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 < 8U) ? rx_len : 8U);
return;
}
spi_ota_frame_type_t frame_type;
const u8 *payload = nullptr;
u16 p_len = 0U;
if (!tryParseFrame(s_rxStream, expected, &frame_type, &payload, &p_len)) {
isrLogFrameEx("[PARSE-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
return;
}
if (frame_type == SPI_OTA_CMD_BOOT) {
u8 ack_ok = 0xA5U;
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(); }
isrLogFrameEx("[BOOT-FRAME]", s_txBuf, s_txLen);
s_isrResponded = true;
return;
}
if (s_otaHandler != nullptr) {
s_otaSessionActive = true;
s_otaHandler->setTransportSender(sender);
ota_link_msg_type_t msg_type = mapMsgType(frame_type);
isrLogFrameEx("[OTA-MSG]", (const u8*)&frame_type, 1);
s_otaHandler->handleLinkMessage(msg_type, const_cast<u8 *>(payload), (u8)p_len);
}
isrLogFrameEx("[ISR-DONE]", s_txBuf, (s_txLen < 16U) ? s_txLen : 16U);
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_logSlotLens[s_logWriteIdx] = pos;
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_6 | GPIO_PIN_7);
gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15);
}
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);
spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_RBNE);
spi_i2s_interrupt_enable(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);
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);
}
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::init() {
s_csActive = false;
s_rxStreamLen = 0U;
s_txStreamPos = 0U;
s_txLen = 0U;
s_txPending = false;
s_isrLogPending = false;
memset(s_rxStream, 0, sizeof(s_rxStream));
memset(s_txBuf, 0xFF, sizeof(s_txBuf));
configureGpio();
configureDma();
configureSpi0();
configureExti();
}
/* ============================================================
* CS 边沿处理
* ============================================================ */
void SpiOta::onCsAssert() {
if (s_csActive) { return; }
s_csActive = true;
s_rxStreamLen = 0U;
s_txStreamPos = 0U;
s_isrResponded = false;
/* 内部 NSS 已在初始化时常置 LOW,这里不再依赖 EXTI 时序切 NSS。
* PA15 仅作为“事务开始”的边界信号,用于清接收缓冲和重装 DMA。 */
/* 关键修复:先彻底停止 DMA,再重新配置并启动。
*
* 上一版预填 SPI_DATA 方案失败原因:
* spi_i2s_data_transmit() 写入的字节进入 TBE 发送缓冲区。
* 当 ESP32 第一个 SCK 边沿到来时,TBE 的预填字节被移入移位
* 寄存器推到 MISO,同时移位寄存器中的 MOSI 数据被推入 RBNE。
* 但 GD32E50x SPI 的 RBNE 标志在 SPI_DATA 写入后可能被误触发,
* 导致 isrLogFrame 报出预填值(如 0x80)而非 MOSI 真实数据。
*
* 正确方案:不预填 SPI_DATA,而是确保 DMA 在 CS↓ 后、ESP32
* 第一个 SCK 边沿之前就绪。DMA 第一拍自动把 s_txBuf[0] 写入
* SPI_DATA,不会产生 RBNE 误触发。
*
* 操作顺序:
* 1. 禁 DMA 通道 + 禁 SPI DMA 请求(彻底停止旧传输)
* 2. 清 RBNE / RXORERR / FERR 等残留标志
* 3. 配置并启动 DMA(此时 RBNE 仍关闭,DMA 写 SPI_DATA 不会触发中断)
* 4. 重新使能 RBNE 中断,开始接收 MOSI 数据
*/
/* 1. 停止旧 DMA */
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN;
spi_dma_disable(SPI0, SPI_DMA_TRANSMIT);
/* 2. 清残留错误标志
*
* 关键:这里绝不能在 CS↓ 时“顺手读掉 RBNE”。
*
* 因为 internal NSS 现在常 LOW,ESP32 在 CS↓ 后很快就会发出首字节 A5。
* 如果 EXTI ISR 略晚于 SPI 接收,A5 可能已经先进了 RX 缓冲并置位 RBNE。
* 这时若在 onCsAssert() 里执行 spi_i2s_data_receive(),就会把真实首字节 A5
* 当成“残留垃圾”吞掉,导致后续看到的首字节变成 0x01。
*
* 所以这里仅清真正的错误标志,不主动消费 RBNE。
* 已到达的首字节留给 onSpiInterrupt() 正常读走。 */
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(RBNE 仍关闭,DMA 写 SPI_DATA 不会触发中断) */
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; }
s_csActive = false;
/* 内部 NSS 保持常 LOW,不在 CS↑ 时切回 HIGH。
* 这样 SPI 从机始终处于就绪态,下一次 CS↓ 不会因 EXTI 延迟丢首字节。
*
* 这里只停止 DMA,避免 CS 高期间继续空转消耗 s_txBuf 内容。
* s_txPending 保持不变——如果 ISR 已准备好 ACK 帧,
* 下次 CS↓ 时 onCsAssert 会重启 DMA 推出。 */
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN;
spi_dma_disable(SPI0, SPI_DMA_TRANSMIT);
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;
}
}
/* 关键调试日志:记录本次 CS 周期结束时到底收到了多少字节。
* 这样能直接判断:
* 1) 是不是完整收到了 ESP32 发来的整帧
* 2) 还是只收到了首字节 A5 / 少数字节,导致永远无法解析回包 */
if (s_rxStreamLen > 0U) {
isrLogFrame("[CS-RX]", s_rxStream, (s_rxStreamLen < 12U) ? s_rxStreamLen : 12U);
}
if (s_rxStreamLen > 0U && !s_isrResponded) {
isrProcessFrame();
}
}
/* ============================================================
* SPI0 中断处理 — 只处理 RBNE(接收)和错误,TBE 已由 DMA 替代
* ============================================================ */
void SpiOta::onSpiInterrupt() {
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) {
/* 与 APP 侧稳定实现保持一致:
* 如果 EXTI 下降沿因为中断时序略晚到达,但此时 PA15 已经为低,
* 则在首个 RBNE 到来时立即补做 onCsAssert()。
*
* 关键限制:只能在“本事务尚未收过任何字节”时补做。
* 否则若 EXTI / SPI 时序偶发抖动,重复 onCsAssert() 会把
* s_rxStreamLen 清零,导致已经收到的半帧被冲掉,最终永远拼不成完整帧。 */
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;
}
/* 调试阶段先关闭 RBNE 首字节日志。
* 原因:ISR 日志缓冲是单槽的,RBNE 一旦先占位,
* onCsRelease() 里的 [CS-RX] 摘要日志就会被丢弃。
* 目前排查重点是“本次 CS 周期总共收到了哪些字节”,
* 所以优先保留 [CS-RX],暂时关闭 [RBNE]。 */
tryProcessCompleteFrame();
}
/* TBE 已由 DMA 替代,不再处理 */
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
(void)spi_i2s_data_receive(SPI0);
s_rxStreamLen = 0U;
isrLogFrame("[RXOR]", (const u8*)"OVR", 3);
}
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) {
spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR);
}
}
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::poll() {
if (s_isrResponded) {
s_isrResponded = false;
}
/* 打印 ISR 日志缓冲区 */
while (s_logCount > 0U) {
__disable_irq();
if (s_logCount == 0U) { __enable_irq(); break; }
u8 *buf = s_logSlots[s_logReadIdx];
u8 nextRead = (s_logReadIdx + 1U) % kLogSlotCount;
s_logReadIdx = nextRead;
s_logCount--;
__enable_irq();
printf("[SPI] %s\r\n", buf);
}
if (s_otaHandler != nullptr) { s_otaHandler->process(); }
/* 调试:定期打印 SPI 从机状态 */
static u32 dbg_tick = 0;
dbg_tick++;
if ((dbg_tick % 5000U) == 0U) {
printf("[SPI] csActive=%u rxLen=%u txPend=%u txLen=%u isrResp=%u otaState=%u\r\n",
(unsigned)s_csActive, (unsigned)s_rxStreamLen,
(unsigned)s_txPending, (unsigned)s_txLen,
(unsigned)s_isrResponded,
(unsigned)(s_otaHandler ? s_otaHandler->getState() : 0xFF));
}
}
void SpiOta::setSender(OTAHandler *handler) { s_otaHandler = handler; }
void SpiOta::feedRxBytes(const u8 *data, u16 len) {
if (!data || len == 0U || !s_csActive) { return; }
if (len > kRxBufSize) { data += (len - kRxBufSize); len = kRxBufSize; }
memcpy(s_rxStream, data, len);
s_rxStreamLen = len;
}
bool SpiOta::hasTxFrame() { return s_txPending; }
bool SpiOta::hasActiveSession() { return s_otaSessionActive; }
u16 SpiOta::copyTxFrame(u8 *out, u16 max_len) {
if (!out || max_len == 0U) { return 0U; }
uint32_t primask = enterCritical();
if (!s_txPending || s_txLen == 0U) { exitCritical(primask); return 0U; }
u16 copy_len = (s_txLen < max_len) ? s_txLen : max_len;
memcpy(out, s_txBuf, copy_len);
s_txPending = false;
s_txLen = 0U;
s_txStreamPos = 0U;
exitCritical(primask);
return copy_len;
}
/* ============================================================
* C 风格兼容包装
* ============================================================ */
void spi_ota_init(void) { SpiOta::init(); }
void spi_ota_poll(void) { SpiOta::poll(); }
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(); }
void spi_ota_gpio_init(void) { SpiOta::configureGpio(); }
void spi_ota_spi0_init(void) { SpiOta::configureSpi0(); }
void spi_ota_exti_init(void) { SpiOta::configureExti(); }
void spi_ota_irq_handler(void) { SpiOta::onSpiInterrupt(); }
void spi_ota_cs_exti_handler(void) { SpiOta::onCsExtiInterrupt(); }
void spi_ota_feed_rx_bytes(const u8 *data, u16 len) { SpiOta::feedRxBytes(data, len); }
bool spi_ota_has_tx_frame(void) { return SpiOta::hasTxFrame(); }
u16 spi_ota_copy_tx_frame(u8 *out, u16 max_len) { return SpiOta::copyTxFrame(out, max_len); }
extern "C" void SPI0_IRQHandler(void) {
SpiOta::onSpiInterrupt();
}
@@ -0,0 +1,626 @@
#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 <stdio.h>
#include <string.h>
/**
* @file SpiOta.cpp
* @brief Bootloader SPI OTA 从机模块实现(DMA 发送版)。
*
* CS 释放后,EXTI ISR 直接完成帧校验+处理+响应准备,
* 响应帧写入 s_txBuf,DMA0_CH2 自动推到 SPI0 TX 寄存器。
* 硬件 DMA 替代 TBE 中断逐字节发送,消除字节错位问题。
*
* 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 固定映射) */
#define SPI_TX_DMA_PERIPH DMA0
#define SPI_TX_DMA_CH DMA_CH2
/* 调试:多缓冲日志(8 条 ISR 日志缓冲区,避免关键事件丢失) */
static constexpr u16 kLogSlotCount = 8U;
static constexpr u16 kLogSlotSize = 80U;
static u8 s_logSlots[kLogSlotCount][kLogSlotSize] = {};
static u16 s_logSlotLens[kLogSlotCount] = {};
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_isrLogPending = false;
volatile bool SpiOta::s_otaSessionActive = false;
u8 SpiOta::s_isrLogBuf[kIsrLogBufSize] = {};
u16 SpiOta::s_isrLogLen = 0U;
/* ============================================================
* CRC8 / 帧构建 / 帧解析
* ============================================================ */
u8 SpiOta::crc8(const u8 *data, u16 len) {
u8 crc = 0x00U;
for (u16 i = 0; i < len; ++i) { crc ^= data[i]; }
return crc;
}
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)(kFrameOverhead + payload_len);
if (total_len > kTxBufSize) { return false; }
out[0] = kSof;
out[1] = (u8)type;
out[2] = (u8)(payload_len & 0xFFU);
out[3] = (u8)((payload_len >> 8) & 0xFFU);
if (payload && payload_len > 0U) { memcpy(&out[4], payload, payload_len); }
out[4 + payload_len] = crc8(out, (u16)(4U + payload_len));
*out_len = total_len;
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 < kFrameOverhead) { return false; }
if (frame[0] != kSof) { return false; }
u16 len = (u16)(frame[2] | ((u16)frame[3] << 8));
u16 expected = (u16)(kFrameOverhead + len);
if (expected != frame_len) { return false; }
if (crc8(frame, (u16)(4U + len)) != frame[4U + len]) { return false; }
if (type) { *type = (spi_ota_frame_type_t)frame[1]; }
if (payload) { *payload = &frame[4]; }
if (payload_len){ *payload_len = len; }
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)) {
queueTxFrame(frame_buf, built_len);
}
}
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;
}
void SpiOta::queueTxFrame(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);
spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);
startSpiTxDma(s_txBuf, s_txLen);
}
/* ============================================================
* RBNE 中断内即时帧检测与处理
* ============================================================ */
void SpiOta::tryProcessCompleteFrame() {
u16 rx_len = s_rxStreamLen;
if (rx_len == 0U) { return; }
if (s_rxStream[0] == SPI_OTA_CMD_BOOT) {
isrProcessFrame();
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();
}
/* ============================================================
* CS 释放后 ISR 直接帧处理
* ============================================================ */
void SpiOta::isrProcessFrame() {
u16 rx_len = s_rxStreamLen;
if (rx_len == 0U) { return; }
/* 关键修复:一旦开始处理本次 CS 周期中的完整帧,立刻标记“已响应/已处理”。
*
* 否则会出现这样的竞态:
* 1) SPI0 RBNE IRQ 在收到最后一个字节后调用 isrProcessFrame()
* 2) 还没执行到函数尾部的 s_isrResponded = true
* 3) CS 已经上升,优先级更高的 EXTI IRQ 抢占进来执行 onCsRelease()
* 4) onCsRelease() 看到 s_isrResponded 仍为 false,于是再次调用 isrProcessFrame()
* 5) 同一帧被重复处理一次,状态机被错误推进(如 challenge 又被当 confirm)
*
* 所以必须在函数入口就先置位,防止 REQ/DATA 被双重分发。 */
s_isrResponded = true;
if (rx_len >= 1U && s_rxStream[0] == SPI_OTA_CMD_BOOT) {
u8 ack[4] = {0xB0U, 0xA5U, 0x4FU, 0x54U};
queueTxFrame(ack, sizeof(ack));
if (s_otaHandler != nullptr) { s_otaHandler->resetToIdle(); }
isrLogFrameEx("[BOOT-CMD]", s_txBuf, s_txLen);
s_isrResponded = true;
return;
}
if (s_rxStream[0] != kSof) {
isrLogFrameEx("[NO-SOF]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
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 > rx_len) { return; }
if (crc8(s_rxStream, (u16)(4U + payload_len)) != s_rxStream[4U + payload_len]) {
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 < 8U) ? rx_len : 8U);
return;
}
spi_ota_frame_type_t frame_type;
const u8 *payload = nullptr;
u16 p_len = 0U;
if (!tryParseFrame(s_rxStream, expected, &frame_type, &payload, &p_len)) {
isrLogFrameEx("[PARSE-ERR]", s_rxStream, (rx_len < 8U) ? rx_len : 8U);
return;
}
if (frame_type == SPI_OTA_CMD_BOOT) {
u8 ack_ok = 0xA5U;
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(); }
isrLogFrameEx("[BOOT-FRAME]", s_txBuf, s_txLen);
s_isrResponded = true;
return;
}
if (s_otaHandler != nullptr) {
s_otaSessionActive = true;
s_otaHandler->setTransportSender(sender);
ota_link_msg_type_t msg_type = mapMsgType(frame_type);
isrLogFrameEx("[OTA-MSG]", (const u8*)&frame_type, 1);
/* 诊断: 记录 OTA 状态机当前状态和帧信息 */
{
u8 diag[4] = {(u8)frame_type, (u8)p_len, (u8)(p_len >> 8),
(u8)(s_otaHandler ? s_otaHandler->getState() : 0xFFU)};
isrLogFrameEx("[OTA-DIAG]", diag, 4);
}
s_otaHandler->handleLinkMessage(msg_type, const_cast<u8 *>(payload), (u8)p_len);
}
isrLogFrameEx("[ISR-DONE]", s_txBuf, (s_txLen < 16U) ? s_txLen : 16U);
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_logSlotLens[s_logWriteIdx] = pos;
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_6 | GPIO_PIN_7);
gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15);
}
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);
spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_RBNE);
spi_i2s_interrupt_enable(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);
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);
}
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::init() {
s_csActive = false;
s_rxStreamLen = 0U;
s_txStreamPos = 0U;
s_txLen = 0U;
s_txPending = false;
s_isrLogPending = false;
memset(s_rxStream, 0, sizeof(s_rxStream));
memset(s_txBuf, 0xFF, sizeof(s_txBuf));
configureGpio();
configureDma();
configureSpi0();
configureExti();
}
/* ============================================================
* CS 边沿处理
* ============================================================ */
void SpiOta::onCsAssert() {
if (s_csActive) { return; }
s_csActive = true;
s_rxStreamLen = 0U;
s_txStreamPos = 0U;
s_isrResponded = false;
/* 内部 NSS 已在初始化时常置 LOW,这里不再依赖 EXTI 时序切 NSS。
* PA15 仅作为“事务开始”的边界信号,用于清接收缓冲和重装 DMA。 */
/* 关键修复:先彻底停止 DMA,再重新配置并启动。
*
* 上一版预填 SPI_DATA 方案失败原因:
* spi_i2s_data_transmit() 写入的字节进入 TBE 发送缓冲区。
* 当 ESP32 第一个 SCK 边沿到来时,TBE 的预填字节被移入移位
* 寄存器推到 MISO,同时移位寄存器中的 MOSI 数据被推入 RBNE。
* 但 GD32E50x SPI 的 RBNE 标志在 SPI_DATA 写入后可能被误触发,
* 导致 isrLogFrame 报出预填值(如 0x80)而非 MOSI 真实数据。
*
* 正确方案:不预填 SPI_DATA,而是确保 DMA 在 CS↓ 后、ESP32
* 第一个 SCK 边沿之前就绪。DMA 第一拍自动把 s_txBuf[0] 写入
* SPI_DATA,不会产生 RBNE 误触发。
*
* 操作顺序:
* 1. 禁 DMA 通道 + 禁 SPI DMA 请求(彻底停止旧传输)
* 2. 清 RBNE / RXORERR / FERR 等残留标志
* 3. 配置并启动 DMA(此时 RBNE 仍关闭,DMA 写 SPI_DATA 不会触发中断)
* 4. 重新使能 RBNE 中断,开始接收 MOSI 数据
*/
/* 1. 停止旧 DMA */
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN;
spi_dma_disable(SPI0, SPI_DMA_TRANSMIT);
/* 2. 清残留错误标志
*
* 关键:这里绝不能在 CS↓ 时“顺手读掉 RBNE”。
*
* 因为 internal NSS 现在常 LOW,ESP32 在 CS↓ 后很快就会发出首字节 A5。
* 如果 EXTI ISR 略晚于 SPI 接收,A5 可能已经先进了 RX 缓冲并置位 RBNE。
* 这时若在 onCsAssert() 里执行 spi_i2s_data_receive(),就会把真实首字节 A5
* 当成“残留垃圾”吞掉,导致后续看到的首字节变成 0x01。
*
* 所以这里仅清真正的错误标志,不主动消费 RBNE。
* 已到达的首字节留给 onSpiInterrupt() 正常读走。 */
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(RBNE 仍关闭,DMA 写 SPI_DATA 不会触发中断) */
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; }
s_csActive = false;
/* 内部 NSS 保持常 LOW,不在 CS↑ 时切回 HIGH。
* 这样 SPI 从机始终处于就绪态,下一次 CS↓ 不会因 EXTI 延迟丢首字节。
*
* 这里只停止 DMA,避免 CS 高期间继续空转消耗 s_txBuf 内容。
* s_txPending 保持不变——如果 ISR 已准备好 ACK 帧,
* 下次 CS↓ 时 onCsAssert 会重启 DMA 推出。 */
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN;
spi_dma_disable(SPI0, SPI_DMA_TRANSMIT);
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;
}
}
/* 关键调试日志:记录本次 CS 周期结束时到底收到了多少字节。
* 这样能直接判断:
* 1) 是不是完整收到了 ESP32 发来的整帧
* 2) 还是只收到了首字节 A5 / 少数字节,导致永远无法解析回包 */
if (s_rxStreamLen > 0U) {
isrLogFrame("[CS-RX]", s_rxStream, (s_rxStreamLen < 12U) ? s_rxStreamLen : 12U);
}
if (s_rxStreamLen > 0U && !s_isrResponded) {
isrProcessFrame();
}
}
/* ============================================================
* SPI0 中断处理 — 只处理 RBNE(接收)和错误,TBE 已由 DMA 替代
* ============================================================ */
void SpiOta::onSpiInterrupt() {
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) {
/* 与 APP 侧稳定实现保持一致:
* 如果 EXTI 下降沿因为中断时序略晚到达,但此时 PA15 已经为低,
* 则在首个 RBNE 到来时立即补做 onCsAssert()。
*
* 关键限制:只能在“本事务尚未收过任何字节”时补做。
* 否则若 EXTI / SPI 时序偶发抖动,重复 onCsAssert() 会把
* s_rxStreamLen 清零,导致已经收到的半帧被冲掉,最终永远拼不成完整帧。 */
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;
}
/* 调试阶段先关闭 RBNE 首字节日志。
* 原因:ISR 日志缓冲是单槽的,RBNE 一旦先占位,
* onCsRelease() 里的 [CS-RX] 摘要日志就会被丢弃。
* 目前排查重点是“本次 CS 周期总共收到了哪些字节”,
* 所以优先保留 [CS-RX],暂时关闭 [RBNE]。 */
tryProcessCompleteFrame();
}
/* TBE 已由 DMA 替代,不再处理 */
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
(void)spi_i2s_data_receive(SPI0);
s_rxStreamLen = 0U;
isrLogFrame("[RXOR]", (const u8*)"OVR", 3);
}
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) {
spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR);
}
}
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::poll() {
if (s_isrResponded) {
s_isrResponded = false;
}
/* 打印 ISR 日志缓冲区 */
while (s_logCount > 0U) {
__disable_irq();
if (s_logCount == 0U) { __enable_irq(); break; }
u8 *buf = s_logSlots[s_logReadIdx];
u8 nextRead = (s_logReadIdx + 1U) % kLogSlotCount;
s_logReadIdx = nextRead;
s_logCount--;
__enable_irq();
printf("[SPI] %s\r\n", buf);
}
if (s_otaHandler != nullptr) { s_otaHandler->process(); }
/* 调试:定期打印 SPI 从机状态 */
static u32 dbg_tick = 0;
dbg_tick++;
if ((dbg_tick % 5000U) == 0U) {
printf("[SPI] csActive=%u rxLen=%u txPend=%u txLen=%u isrResp=%u otaState=%u\r\n",
(unsigned)s_csActive, (unsigned)s_rxStreamLen,
(unsigned)s_txPending, (unsigned)s_txLen,
(unsigned)s_isrResponded,
(unsigned)(s_otaHandler ? s_otaHandler->getState() : 0xFF));
}
}
void SpiOta::setSender(OTAHandler *handler) { s_otaHandler = handler; }
void SpiOta::feedRxBytes(const u8 *data, u16 len) {
if (!data || len == 0U || !s_csActive) { return; }
if (len > kRxBufSize) { data += (len - kRxBufSize); len = kRxBufSize; }
memcpy(s_rxStream, data, len);
s_rxStreamLen = len;
}
bool SpiOta::hasTxFrame() { return s_txPending; }
bool SpiOta::hasActiveSession() { return s_otaSessionActive; }
u16 SpiOta::copyTxFrame(u8 *out, u16 max_len) {
if (!out || max_len == 0U) { return 0U; }
uint32_t primask = enterCritical();
if (!s_txPending || s_txLen == 0U) { exitCritical(primask); return 0U; }
u16 copy_len = (s_txLen < max_len) ? s_txLen : max_len;
memcpy(out, s_txBuf, copy_len);
s_txPending = false;
s_txLen = 0U;
s_txStreamPos = 0U;
exitCritical(primask);
return copy_len;
}
/* ============================================================
* C 风格兼容包装
* ============================================================ */
void spi_ota_init(void) { SpiOta::init(); }
void spi_ota_poll(void) { SpiOta::poll(); }
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(); }
void spi_ota_gpio_init(void) { SpiOta::configureGpio(); }
void spi_ota_spi0_init(void) { SpiOta::configureSpi0(); }
void spi_ota_exti_init(void) { SpiOta::configureExti(); }
void spi_ota_irq_handler(void) { SpiOta::onSpiInterrupt(); }
void spi_ota_cs_exti_handler(void) { SpiOta::onCsExtiInterrupt(); }
void spi_ota_feed_rx_bytes(const u8 *data, u16 len) { SpiOta::feedRxBytes(data, len); }
bool spi_ota_has_tx_frame(void) { return SpiOta::hasTxFrame(); }
u16 spi_ota_copy_tx_frame(u8 *out, u16 max_len) { return SpiOta::copyTxFrame(out, max_len); }
extern "C" void SPI0_IRQHandler(void) {
SpiOta::onSpiInterrupt();
}
+13 -16
View File
@@ -5,17 +5,18 @@
/** /**
* @file SpiOta.h * @file SpiOta.h
* @brief SPI OTA 从机协议头文件(PendSV 延迟响应版)。 * @brief SPI OTA 从机协议头文件(ISR 直接响应版)。
* *
* CS 释放后,EXTI ISR 只做轻量校验(SOF + LEN + CRC), * CS 释放后,EXTI ISR 直接完成帧校验+处理+响应准备,
* 校验通过则触发 PendSV(最低优先级 ISR),由 PendSV 调用 * 响应帧在 ISR 中写入 s_txBuf,下次 CS 拉低时 DMA 自动发出。
* handleLinkMessage 完成帧处理和响应准备。 * 无需 TBE 中断,DMA 硬件逐字节推到 MISO,消除字节错位问题。
*/ */
typedef enum { typedef enum {
SPI_OTA_CMD_REQ = 0x01, SPI_OTA_CMD_REQ = 0x01,
SPI_OTA_CMD_DATA = 0x02, SPI_OTA_CMD_DATA = 0x02,
SPI_OTA_CMD_CANCEL = 0x03, SPI_OTA_CMD_CANCEL = 0x03,
SPI_OTA_CMD_BOOT = 0xB0,
SPI_OTA_RSP_ACK = 0x80, SPI_OTA_RSP_ACK = 0x80,
SPI_OTA_RSP_WAIT = 0x81, SPI_OTA_RSP_WAIT = 0x81,
SPI_OTA_RSP_ERROR = 0x82 SPI_OTA_RSP_ERROR = 0x82
@@ -36,21 +37,19 @@ public:
static void onCsRelease(); static void onCsRelease();
static void configureGpio(); static void configureGpio();
static void configureSpi0(); static void configureSpi0();
static void configureDma();
static void configureExti(); static void configureExti();
static void onSpiInterrupt(); static void onSpiInterrupt();
static void onCsExtiInterrupt(); static void onCsExtiInterrupt();
static void feedRxBytes(const u8 *data, u16 len); static void feedRxBytes(const u8 *data, u16 len);
static bool hasTxFrame(); static bool hasTxFrame();
static bool hasActiveSession();
static u16 copyTxFrame(u8 *out, u16 max_len); static u16 copyTxFrame(u8 *out, u16 max_len);
/** @brief EXTI ISR 内轻量校验,通过则触发 PendSV。 */ static void isrProcessFrame();
static void isrValidateAndPend(); static void tryProcessCompleteFrame();
/** @brief PendSV ISR 内执行帧处理和响应准备。 */
static void onPendSvProcess();
/** @brief ISR 内轻量日志写入延迟打印缓冲。 */
static void isrLogFrame(const char *tag, const u8 *data, u16 len); static void isrLogFrame(const char *tag, const u8 *data, u16 len);
/* ---- PendSV 需要访问的成员放 public ---- */
static OTAHandler *s_otaHandler; static OTAHandler *s_otaHandler;
static volatile bool s_csActive; static volatile bool s_csActive;
static volatile u16 s_rxStreamLen; static volatile u16 s_rxStreamLen;
@@ -59,10 +58,13 @@ public:
static u8 s_txBuf[kTxBufSize]; static u8 s_txBuf[kTxBufSize];
static volatile u16 s_txLen; static volatile u16 s_txLen;
static volatile bool s_txPending; static volatile bool s_txPending;
static volatile bool s_isrResponded;
static volatile bool s_isrLogPending; static volatile bool s_isrLogPending;
static volatile bool s_otaSessionActive;
static u8 s_isrLogBuf[kIsrLogBufSize]; static u8 s_isrLogBuf[kIsrLogBufSize];
static u16 s_isrLogLen; static u16 s_isrLogLen;
static volatile bool s_pendSVPending;
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 u8 crc8(const u8 *data, u16 len);
@@ -73,7 +75,6 @@ private:
const u8 **payload, const u8 **payload,
u16 *payload_len); u16 *payload_len);
static ota_link_msg_type_t mapMsgType(spi_ota_frame_type_t type); static ota_link_msg_type_t mapMsgType(spi_ota_frame_type_t type);
static void sender(ota_link_tx_type_t type, u8 len, const u8 *data);
static void queueTxFrame(const u8 *frame, u16 len); static void queueTxFrame(const u8 *frame, u16 len);
static uint32_t enterCritical(); static uint32_t enterCritical();
static void exitCritical(uint32_t primask); static void exitCritical(uint32_t primask);
@@ -83,10 +84,6 @@ private:
extern "C" { extern "C" {
#endif #endif
/**
* 引脚约定:CS=PA15, SCK=PB3, MISO=PB4, MOSI=PB5
*/
void spi_ota_init(void); void spi_ota_init(void);
void spi_ota_poll(void); void spi_ota_poll(void);
void spi_ota_set_sender(OTAHandler *handler); void spi_ota_set_sender(OTAHandler *handler);
+57 -1
View File
@@ -33,6 +33,7 @@ void BootloaderSystem::initHardware() {
*/ */
SysTickTimer::init(); SysTickTimer::init();
BootDebugUart::init(); BootDebugUart::init();
#if DEBUG_PRINT
printf("\r\n[BOOT] power on\r\n"); printf("\r\n[BOOT] power on\r\n");
printf("[BOOT] BOOT=0x%08lX size=0x%08lX APPBACK=0x%08lX APPLATEST=0x%08lX APP_MAX=0x%08lX\r\n", printf("[BOOT] BOOT=0x%08lX size=0x%08lX APPBACK=0x%08lX APPLATEST=0x%08lX APP_MAX=0x%08lX\r\n",
(unsigned long)BOOTLOADER_START_ADDR, (unsigned long)BOOTLOADER_START_ADDR,
@@ -41,6 +42,7 @@ void BootloaderSystem::initHardware() {
(unsigned long)APPLATEST_START_ADDR, (unsigned long)APPLATEST_START_ADDR,
(unsigned long)APP_MAX_SIZE); (unsigned long)APP_MAX_SIZE);
printf("[BOOT] SystemCoreClock=%lu Hz\r\n", (unsigned long)SystemCoreClock); printf("[BOOT] SystemCoreClock=%lu Hz\r\n", (unsigned long)SystemCoreClock);
#endif
CAN0_transmiter.init(); CAN0_transmiter.init();
ota.init(); ota.init();
SpiOta::init(); SpiOta::init();
@@ -51,7 +53,9 @@ void BootloaderSystem::initHardware() {
/** 检查是否需要进入深度睡眠模式。 */ /** 检查是否需要进入深度睡眠模式。 */
{ {
const int sleep_state = BootSleepManager::check(); const int sleep_state = BootSleepManager::check();
#if DEBUG_PRINT
printf("[BOOT] sleep_check=%d\r\n", sleep_state); printf("[BOOT] sleep_check=%d\r\n", sleep_state);
#endif
} }
} }
@@ -65,8 +69,17 @@ static constexpr uint32_t kHeartbeatPrintIntervalMs = 2000U;
* poll() 只负责消费 ISR 延迟日志和推进 OTA 后台任务(延迟擦除/跳转)。 * poll() 只负责消费 ISR 延迟日志和推进 OTA 后台任务(延迟擦除/跳转)。
*/ */
static void pollAllLinks(void) { static void pollAllLinks(void) {
CAN0_transmiter.communication_check(); /* 关键修正:先轮询 SPI,再轮询 CAN。
*
* 之前顺序是:CAN0_transmiter.communication_check() -> SpiOta::poll()
* 但 communication_check() 内部可能执行阻塞等待/超时处理,
* 导致 SPI ISR 延迟日志长时间不被消费,且更重要的是:
* OTA 状态机中的 process() 也被延后,进而影响 SPI 事务后的响应准备/推进。
*
* 当前我们正排查 SPI BOOT 通路,应让 SPI 的 poll() 优先执行,
* 尽快消费 ISR 结果、推进 OTA 状态机,并减少 CAN 侧对 SPI 调试的干扰。 */
SpiOta::poll(); SpiOta::poll();
CAN0_transmiter.communication_check();
} }
/** /**
@@ -80,10 +93,12 @@ static void heartbeatPrint(uint32_t &heartbeat_counter, uint32_t loop_delay_ms,
heartbeat_counter += loop_delay_ms; heartbeat_counter += loop_delay_ms;
if (heartbeat_counter >= kHeartbeatPrintIntervalMs) { if (heartbeat_counter >= kHeartbeatPrintIntervalMs) {
heartbeat_counter = 0U; heartbeat_counter = 0U;
#if DEBUG_PRINT
printf("[BOOT] \xe2\x99\xa5 %s ota_active=%d spi_tx_pending=%d\r\n", printf("[BOOT] \xe2\x99\xa5 %s ota_active=%d spi_tx_pending=%d\r\n",
path_label, path_label,
ota.isActive() ? 1 : 0, ota.isActive() ? 1 : 0,
SpiOta::hasTxFrame() ? 1 : 0); SpiOta::hasTxFrame() ? 1 : 0);
#endif
} }
} }
@@ -99,21 +114,48 @@ void BootloaderSystem::run() {
volatile uint32_t *flash_ptr; volatile uint32_t *flash_ptr;
uint32_t app_enable, app_check; uint32_t app_enable, app_check;
/** 检测 FWDGT 看门狗复位:APP 卡死导致看门狗超时。
* 将 app_enable 标记为 NOT_AVAL,阻止 BOOT 再次跳入卡死的 APP,
* 改为停留在 BOOT 等待 OTA 修复。 */
if (rcu_flag_get(RCU_FLAG_FWDGTRST) != RESET) {
rcu_all_reset_flag_clear();
#if DEBUG_PRINT
printf("[BOOT] FWDGT reset detected, blocking app jump\r\n");
#endif
OTA_SET(n_APP_LATEST_EN_ADDR, OTA_NOT_AVAL);
}
/** 检测看门狗复位:如果上次复位是由 APP 的 FWDGT 超时触发,
* 则将 app_enable 标记为 NOT_AVAL,阻止 BOOT 再次跳入卡死的 APP,
* 让 BOOT 停留在 OTA 稡式等待远程修复。 */
if (rcu_flag_get(RCU_FLAG_FWDGTRST) != RESET) {
#if DEBUG_PRINT
printf("[BOOT] FWDGT reset detected, blocking app jump\r\n");
#endif
OTA_SET(n_APP_LATEST_EN_ADDR, OTA_NOT_AVAL);
}
rcu_all_reset_flag_clear();
flash_ptr = (volatile uint32_t *)n_APP_LATEST_EN_ADDR; flash_ptr = (volatile uint32_t *)n_APP_LATEST_EN_ADDR;
app_enable = *flash_ptr; app_enable = *flash_ptr;
flash_ptr = (volatile uint32_t *)n_APP_LATEST_CHECK_ADDR; flash_ptr = (volatile uint32_t *)n_APP_LATEST_CHECK_ADDR;
app_check = *flash_ptr; app_check = *flash_ptr;
#if DEBUG_PRINT
printf("[BOOT] app_enable=0x%08lX app_check=0x%08lX\r\n", printf("[BOOT] app_enable=0x%08lX app_check=0x%08lX\r\n",
(unsigned long)app_enable, (unsigned long)app_enable,
(unsigned long)app_check); (unsigned long)app_check);
#endif
/** /**
* 情况 1:应用已校验完成,且允许启动。 * 情况 1:应用已校验完成,且允许启动。
* 直接跳转到主应用,不在 Bootloader 多停留。 * 直接跳转到主应用,不在 Bootloader 多停留。
*/ */
if (app_check == OTA_AVAL && app_enable == OTA_AVAL) { if (app_check == OTA_AVAL && app_enable == OTA_AVAL) {
#if DEBUG_PRINT
printf("[BOOT] path=jump_latest_now\r\n"); printf("[BOOT] path=jump_latest_now\r\n");
#endif
ota.jumpToApplication(); ota.jumpToApplication();
} }
/** /**
@@ -125,7 +167,9 @@ void BootloaderSystem::run() {
uint32_t heartbeat_counter = 0U; uint32_t heartbeat_counter = 0U;
int ota_requested = 0; int ota_requested = 0;
#if DEBUG_PRINT
printf("[BOOT] path=wait_ota_window timeout_ms=%lu\r\n", (unsigned long)APP_DELAY_BOOT_TIME); printf("[BOOT] path=wait_ota_window timeout_ms=%lu\r\n", (unsigned long)APP_DELAY_BOOT_TIME);
#endif
while (delay_count < APP_DELAY_BOOT_TIME) { while (delay_count < APP_DELAY_BOOT_TIME) {
pollAllLinks(); pollAllLinks();
@@ -133,7 +177,9 @@ void BootloaderSystem::run() {
if (ota.isActive()) { if (ota.isActive()) {
ota_requested = 1; ota_requested = 1;
#if DEBUG_PRINT
printf("[BOOT] ota_requested at %lu ms\r\n", (unsigned long)delay_count); printf("[BOOT] ota_requested at %lu ms\r\n", (unsigned long)delay_count);
#endif
break; break;
} }
@@ -146,7 +192,9 @@ void BootloaderSystem::run() {
* 已经进入 OTA 会话: * 已经进入 OTA 会话:
* 后续不再尝试跳应用,持续停留在 Bootloader 内处理升级。 * 后续不再尝试跳应用,持续停留在 Bootloader 内处理升级。
*/ */
#if DEBUG_PRINT
printf("[BOOT] path=stay_in_ota\r\n"); printf("[BOOT] path=stay_in_ota\r\n");
#endif
uint32_t hb_ota = 0U; uint32_t hb_ota = 0U;
while (1) { while (1) {
pollAllLinks(); pollAllLinks();
@@ -155,7 +203,9 @@ void BootloaderSystem::run() {
} }
} else { } else {
/** 等待窗口内没有任何 OTA 请求,正常启动当前应用。 */ /** 等待窗口内没有任何 OTA 请求,正常启动当前应用。 */
#if DEBUG_PRINT
printf("[BOOT] ota_window_timeout_then_jump\r\n"); printf("[BOOT] ota_window_timeout_then_jump\r\n");
#endif
ota.jumpToApplication(); ota.jumpToApplication();
} }
} }
@@ -170,9 +220,11 @@ void BootloaderSystem::run() {
flash_ptr = (volatile uint32_t *)n_APP_BACKUP_CHECK_ADDR; flash_ptr = (volatile uint32_t *)n_APP_BACKUP_CHECK_ADDR;
back_check = *flash_ptr; back_check = *flash_ptr;
#if DEBUG_PRINT
printf("[BOOT] back_aval=0x%08lX back_check=0x%08lX\r\n", printf("[BOOT] back_aval=0x%08lX back_check=0x%08lX\r\n",
(unsigned long)back_aval, (unsigned long)back_aval,
(unsigned long)back_check); (unsigned long)back_check);
#endif
if (back_check == OTA_AVAL && back_aval == OTA_AVAL) { if (back_check == OTA_AVAL && back_aval == OTA_AVAL) {
/** /**
@@ -181,7 +233,9 @@ void BootloaderSystem::run() {
* 2. 把备份应用搬运到主应用区; * 2. 把备份应用搬运到主应用区;
* 3. 跳转运行。 * 3. 跳转运行。
*/ */
#if DEBUG_PRINT
printf("[BOOT] path=restore_backup_then_jump\r\n"); printf("[BOOT] path=restore_backup_then_jump\r\n");
#endif
CAN0_transmiter.sendBackup(); CAN0_transmiter.sendBackup();
move_app_data(); move_app_data();
ota.jumpToApplication(); ota.jumpToApplication();
@@ -190,7 +244,9 @@ void BootloaderSystem::run() {
* 当前区和备份区都不可用: * 当前区和备份区都不可用:
* 只能长期停留在 Bootloader,等待外部重新下发固件。 * 只能长期停留在 Bootloader,等待外部重新下发固件。
*/ */
#if DEBUG_PRINT
printf("[BOOT] path=stay_boot_wait_image\r\n"); printf("[BOOT] path=stay_boot_wait_image\r\n");
#endif
uint32_t hb_wait = 0U; uint32_t hb_wait = 0U;
while (1) { while (1) {
pollAllLinks(); pollAllLinks();
+151 -64
View File
@@ -1,5 +1,6 @@
#include "OTA.h" #include "OTA.h"
#include "Can.h" #include "Can.h"
#include "SpiOta.h"
#include "aes128.h" #include "aes128.h"
#include "config.h" #include "config.h"
#include "core_cm33.h" #include "core_cm33.h"
@@ -19,14 +20,14 @@ OTAHandler ota;
/** /**
* OTA.cpp 模块说明 * OTA.cpp 模块说明
* ---------------- * ----------------
* 这里实现 Bootloader 的核心 OTA 逻辑: * 这里实现 Bootloader 的核心 OTA 逻辑:
* 1. 握手与会话密钥派生 * 1. 握手与会话密钥派生
* 2. 固件大小确认 * 2. 固件大小确认
* 3. 擦除应用区 * 3. 擦除应用区
* 4. 固件包校验、解密、写 Flash * 4. 固件包校验、解密、写 Flash
* 5. 超时处理、错误上报、应用跳转 * 5. 超时处理、错误上报、应用跳转
* *
* 设计上,OTA 状态机与底层传输层解耦: * 设计上,OTA 状态机与底层传输层解耦:
* - CAN 与 SPI 只负责收发原始消息 * - CAN 与 SPI 只负责收发原始消息
* - 进入本文件后统一走 handleLinkMessage() * - 进入本文件后统一走 handleLinkMessage()
*/ */
@@ -56,7 +57,7 @@ static void ota_can_sender(ota_link_tx_type_t type, u8 len, const u8 *data) {
} }
/** /**
* @brief SPI 链路回包发送占位适配器。 * @brief SPI 链路回包发送适配器。
* *
* @param type OTA 回包类型。 * @param type OTA 回包类型。
* @param len 有效载荷长度。 * @param len 有效载荷长度。
@@ -64,14 +65,19 @@ static void ota_can_sender(ota_link_tx_type_t type, u8 len, const u8 *data) {
* @return 无。 * @return 无。
*/ */
static void ota_spi_sender(ota_link_tx_type_t type, u8 len, const u8 *data) { static void ota_spi_sender(ota_link_tx_type_t type, u8 len, const u8 *data) {
(void)type;
(void)len;
(void)data;
/** /**
* 这里仍保留占位实现。 * 关键修复:这里不能是空函数!
* 当前 SPI OTA 的真正回包发送,是由 device/SpiOta.cpp 内的 spi_ota_sender() 接管的。 *
* 之前是占位实现,因为 ISR 内的回包由 SpiOta::isrProcessFrame() 直接调用
* SpiOta::sender() → queueTxFrame() → DMA 推送。
*
* 但 process() 主循环中也会发送 ACK(如擦除完成后的 ACK(seq=0)),
* 这些调用走的是 transport_send = ota_spi_sender。
* 如果这里是空的,ACK 就被吞掉了,ESP32 永远等不到擦除完成的准入信号。
*
* 所以必须委托给 SpiOta::sender(),它会构建帧并推 DMA。
*/ */
SpiOta::sender(type, len, data);
} }
/** /**
@@ -98,14 +104,14 @@ static inline fmc_state_enum write_ADDRESS(u32 address, u32 data) {
* *
* @param addr 目标配置字地址。 * @param addr 目标配置字地址。
* @param data 目标写入值。 * @param data 目标写入值。
* @return FLASH_EEPROM_OK 表示成功,FLASH_EEPROM_ERROR 表示失败。 * @return FLASH_EEPROM_OK 表示成功,FLASH_EEPROM_ERROR 表示失败。
*/ */
u8 OTA_SET(u32 addr, u32 data) { u8 OTA_SET(u32 addr, u32 data) {
/** /**
* OTA 配置页更新策略: * OTA 配置页更新策略:
* - 如果目标值本来就是 data,直接返回成功 * - 如果目标值本来就是 data,直接返回成功
* - 如果只是把某个字从擦除态写成 OTA_AVAL(0),可以直接写 * - 如果只是把某个字从擦除态写成 OTA_AVAL(0),可以直接写
* - 其它情况(尤其涉及从 0 改回 1)则必须整页擦除后重写全部 6 个配置字 * - 其它情况(尤其涉及从 0 改回 1)则必须整页擦除后重写全部 6 个配置字
*/ */
fmc_state_enum fmc_status; fmc_state_enum fmc_status;
u32 temp_val[OTA_CONFIG_NUM]; u32 temp_val[OTA_CONFIG_NUM];
@@ -117,7 +123,7 @@ u8 OTA_SET(u32 addr, u32 data) {
} }
/** /**
* Flash 只能把 bit 从 1 写成 0,所以写 OTA_AVAL(0x00000000) 时可以尝试直接编程。 * Flash 只能把 bit 从 1 写成 0,所以写 OTA_AVAL(0x00000000) 时可以尝试直接编程。
*/ */
if (data == OTA_AVAL) { if (data == OTA_AVAL) {
fmc_status = write_ADDRESS(addr, data); fmc_status = write_ADDRESS(addr, data);
@@ -126,7 +132,7 @@ u8 OTA_SET(u32 addr, u32 data) {
} }
} }
/** 读取当前配置页中的全部状态,供擦除后重写。 */ /** 读取当前配置页中的全部状态,供擦除后重写。 */
flash_ptr = (volatile u32 *)n_APP_LATEST_AVAL_ADDR; flash_ptr = (volatile u32 *)n_APP_LATEST_AVAL_ADDR;
temp_val[0] = *flash_ptr; temp_val[0] = *flash_ptr;
flash_ptr = (volatile u32 *)n_APP_BACKUP_AVAL_ADDR; flash_ptr = (volatile u32 *)n_APP_BACKUP_AVAL_ADDR;
@@ -179,8 +185,8 @@ u8 OTA_SET(u32 addr, u32 data) {
*/ */
static inline uint32_t extract_u32_from_4bytes(const uint8_t *array) { static inline uint32_t extract_u32_from_4bytes(const uint8_t *array) {
/** /**
* 协议里数值字段按小端排列存放: * 协议里数值字段按小端排列存放:
* byte0 -> 最低字节,byte3 -> 最高字节 * byte0 -> 最低字节,byte3 -> 最高字节
*/ */
return ((uint32_t)array[3] << 24) | ((uint32_t)array[2] << 16) | return ((uint32_t)array[3] << 24) | ((uint32_t)array[2] << 16) |
((uint32_t)array[1] << 8) | (uint32_t)array[0]; ((uint32_t)array[1] << 8) | (uint32_t)array[0];
@@ -213,6 +219,8 @@ static inline void send_structured_ack(uint32_t seq_or_flag) {
void OTAHandler::init() { void OTAHandler::init() {
/** 默认回包出口先绑定到 CAN。 */ /** 默认回包出口先绑定到 CAN。 */
transport_send = ota_can_sender; transport_send = ota_can_sender;
/** 显式重置状态机,防止 APP 软跳转后残留旧状态。 */
resetToIdle();
} }
/** /**
@@ -240,13 +248,33 @@ void ota_bind_spi_transport(void) {
*/ */
void OTAHandler::process() { void OTAHandler::process() {
/** /**
* 把真正耗时的 Flash 擦除动作推迟到主循环做, * 把真正耗时的 Flash 擦除动作推迟到主循环做,
* 避免在接收消息的处理路径里执行过重操作。 * 避免在接收消息的处理路径里执行过重操作。
*
* 协议关键点:SIZE 阶段不能在“刚收到大小”时立刻 ACK(seq=0),
* 否则 ESP32 会马上开始发数据包,而此时 BOOT 可能仍在整片擦 Flash。
* 擦除期间本函数会关中断,SPI 无法及时收包/回 ACK,导致包0超时。
*
* 因此改为:
* 1. ISR 收到 SIZE 后只回 WAIT,并置 pending_flash_erase=true
* 2. 这里真正擦除完成后,再切到 RECEIVING_FIRMWARE 并发送 ACK(seq=0)
*/ */
if (pending_flash_erase) { if (pending_flash_erase) {
pending_flash_erase = false; pending_flash_erase = false;
eraseApplicationArea(); eraseApplicationArea();
ota_state = OTA_STATE_RECEIVING_FIRMWARE; ota_state = OTA_STATE_RECEIVING_FIRMWARE;
/* 关键修复:SIZE 请求来自 SPI 时,擦除完成后的“准入 ACK(seq=0)”
* 必须强制走 SPI 回包。
*
* 否则如果主循环里恰好有 CAN communication_check() 把 transport_send
* 切回了 CAN,process() 这里发出的 ACK 会跑到 CAN 通道,ESP32 SPI 侧就
* 只能一直读到上一次 WAIT 帧残留(例如 0x27 CRC 尾字节)。 */
ota_bind_spi_transport();
#if DEBUG_PRINT
printf("[OTA] erase done, send SPI ACK seq0\r\n");
#endif
send_structured_ack(0U);
} }
if (pending_app_jump) { if (pending_app_jump) {
@@ -257,19 +285,15 @@ void OTAHandler::process() {
} }
if (ota_state == OTA_STATE_IDLE) { if (ota_state == OTA_STATE_IDLE) {
/**
* 空闲态持续清零超时计数,并定期发 WAIT 帧,
* 相当于告诉上位机“Bootloader 在线,尚未进入升级事务”。
*/
ota_time = 0; ota_time = 0;
static u8 zero[1] = {0}; /* 不再主动发 WAIT 帧——ISR 处理完请求后会发 ACK/WAIT/ERROR。
send_wait_frame(0, zero); * 主动发 WAIT 会覆盖 ISR 写入的响应帧,导致 ESP32 读到 WAIT 而非 ACK。 */
delay_1ms(10); delay_1ms(10);
return; return;
} else { } else {
/** /**
* 非空闲态时,按固定节奏推进超时计数。 * 非空闲态时,按固定节奏推进超时计数。
* 这里用 100us 粗略步进,不追求绝对精确,只做会话保活超时保护。 * 这里用 100us 粗略步进,不追求绝对精确,只做会话保活超时保护。
*/ */
delay_1us(100); delay_1us(100);
ota_time++; ota_time++;
@@ -277,10 +301,10 @@ void OTAHandler::process() {
if (ota_time > OTA_TIMEOUT) { if (ota_time > OTA_TIMEOUT) {
/** /**
* OTA 会话超时: * OTA 会话超时:
* - 复位状态机到空闲态 * - 复位状态机到空闲态
* - 上报超时错误 * - 上报超时错误
* - 如果当前主应用仍有效,则尝试回到主应用运行 * - 如果当前主应用仍有效,则尝试回到主应用运行
*/ */
ota_time = 0; ota_time = 0;
ota_state = OTA_STATE_IDLE; ota_state = OTA_STATE_IDLE;
@@ -334,7 +358,7 @@ void OTAHandler::sendWait(u8 len, const u8 *data) {
*/ */
bool validateApplication() { bool validateApplication() {
/** /**
* 通过应用向量表做一个最基本的合法性检查: * 通过应用向量表做一个最基本的合法性检查:
* 1. 初始 MSP 必须落在 SRAM 范围内 * 1. 初始 MSP 必须落在 SRAM 范围内
* 2. Reset_Handler 必须落在主应用区范围内 * 2. Reset_Handler 必须落在主应用区范围内
*/ */
@@ -345,6 +369,18 @@ bool validateApplication() {
bool stack_valid = (stack_ptr >= 0x20000000U && stack_ptr < 0x20020000U); bool stack_valid = (stack_ptr >= 0x20000000U && stack_ptr < 0x20020000U);
bool reset_valid = (reset_handler >= APPLATEST_START_ADDR && bool reset_valid = (reset_handler >= APPLATEST_START_ADDR &&
reset_handler < (APPLATEST_START_ADDR + APP_MAX_SIZE)); reset_handler < (APPLATEST_START_ADDR + APP_MAX_SIZE));
#if DEBUG_PRINT
printf("[BOOT] validateApp: MSP=0x%08lX (%s) Reset=0x%08lX (%s) @0x%08lX\r\n",
(unsigned long)stack_ptr, stack_valid ? "OK" : "INVALID",
(unsigned long)reset_handler, reset_valid ? "OK" : "INVALID",
(unsigned long)APPLATEST_START_ADDR);
/* 额外打印前 4 个 word 帮助诊断固件内容是否正确 */
printf("[BOOT] vector[0..3]=%08lX %08lX %08lX %08lX\r\n",
(unsigned long)app_vector[0], (unsigned long)app_vector[1],
(unsigned long)app_vector[2], (unsigned long)app_vector[3]);
#endif
return stack_valid && reset_valid; return stack_valid && reset_valid;
} }
@@ -355,14 +391,18 @@ bool validateApplication() {
*/ */
void OTAHandler::jumpToApplication() { void OTAHandler::jumpToApplication() {
if (!validateApplication()) { if (!validateApplication()) {
/** 向量表不合法时,拒绝跳转,避免飞到未知地址。 */ /** 向量表不合法时,拒绝跳转,避免飞到未知地址。 */
#if DEBUG_PRINT
printf("[BOOT] jump rejected: invalid app vector @0x%08lX\r\n", (unsigned long)APPLATEST_START_ADDR); printf("[BOOT] jump rejected: invalid app vector @0x%08lX\r\n", (unsigned long)APPLATEST_START_ADDR);
#endif
return; return;
} }
#if DEBUG_PRINT
printf("[BOOT] jumping to app @0x%08lX\r\n", (unsigned long)APPLATEST_START_ADDR); printf("[BOOT] jumping to app @0x%08lX\r\n", (unsigned long)APPLATEST_START_ADDR);
#endif
/** 把当前主应用重新标记为“有效且允许启动”。 */ /** 把当前主应用重新标记为"有效且允许启动"。 */
OTA_SET(n_APP_LATEST_AVAL_ADDR, OTA_AVAL); OTA_SET(n_APP_LATEST_AVAL_ADDR, OTA_AVAL);
OTA_SET(n_APP_LATEST_EN_ADDR, OTA_AVAL); OTA_SET(n_APP_LATEST_EN_ADDR, OTA_AVAL);
@@ -374,6 +414,11 @@ void OTAHandler::jumpToApplication() {
/** 关闭 Bootloader 运行过程中启用过的外设。 */ /** 关闭 Bootloader 运行过程中启用过的外设。 */
can_deinit(CAN0); can_deinit(CAN0);
spi_i2s_deinit(SPI0);
exti_interrupt_disable(EXTI_15);
exti_interrupt_flag_clear(EXTI_15);
DMA_CHCTL(DMA0, DMA_CH1) &= ~DMA_CHXCTL_CHEN;
DMA_CHCTL(DMA0, DMA_CH2) &= ~DMA_CHXCTL_CHEN;
rcu_system_clock_source_config(RCU_CKSYSSRC_IRC8M); rcu_system_clock_source_config(RCU_CKSYSSRC_IRC8M);
rcu_periph_clock_disable(RCU_GPIOA); rcu_periph_clock_disable(RCU_GPIOA);
@@ -381,6 +426,8 @@ void OTAHandler::jumpToApplication() {
rcu_periph_clock_disable(RCU_GPIOC); rcu_periph_clock_disable(RCU_GPIOC);
rcu_periph_clock_disable(RCU_USART0); rcu_periph_clock_disable(RCU_USART0);
rcu_periph_clock_disable(RCU_CAN0); rcu_periph_clock_disable(RCU_CAN0);
rcu_periph_clock_disable(RCU_SPI0);
rcu_periph_clock_disable(RCU_AF);
rcu_periph_clock_disable(RCU_TIMER0); rcu_periph_clock_disable(RCU_TIMER0);
rcu_periph_clock_disable(RCU_DMA0); rcu_periph_clock_disable(RCU_DMA0);
@@ -396,6 +443,9 @@ void OTAHandler::jumpToApplication() {
/** 切栈后直接跳转执行主应用。 */ /** 切栈后直接跳转执行主应用。 */
__set_MSP(msp_value); __set_MSP(msp_value);
__set_CONTROL(0);
__DSB();
__ISB();
rcu_all_reset_flag_clear(); rcu_all_reset_flag_clear();
app_entry(); app_entry();
} }
@@ -407,7 +457,7 @@ void OTAHandler::jumpToApplication() {
*/ */
void OTAHandler::eraseApplicationArea() { void OTAHandler::eraseApplicationArea() {
/** /**
* 开始真正接收新固件前,先把当前主应用状态全部标记为无效, * 开始真正接收新固件前,先把当前主应用状态全部标记为无效,
* 防止中途掉电后误跳到一个被擦了一半的应用。 * 防止中途掉电后误跳到一个被擦了一半的应用。
*/ */
OTA_SET(n_APP_LATEST_CHECK_ADDR, OTA_NOT_AVAL); OTA_SET(n_APP_LATEST_CHECK_ADDR, OTA_NOT_AVAL);
@@ -440,9 +490,9 @@ void OTAHandler::eraseApplicationArea() {
*/ */
void OTAHandler::writeApplicationData(u32 offset, u8 *data, u32 size) { void OTAHandler::writeApplicationData(u32 offset, u8 *data, u32 size) {
/** /**
* 将一个分包写入主应用区: * 将一个分包写入主应用区:
* - 先按 4 字节对齐批量写 * - 先按 4 字节对齐批量写
* - 剩余不足 4 字节时,用 0xFF 补齐一个 word 再写入 * - 剩余不足 4 字节时,用 0xFF 补齐一个 word 再写入
*/ */
fmc_unlock(); fmc_unlock();
u32 write_addr = APPLATEST_START_ADDR + offset; u32 write_addr = APPLATEST_START_ADDR + offset;
@@ -467,8 +517,15 @@ void OTAHandler::writeApplicationData(u32 offset, u8 *data, u32 size) {
* @return 无。 * @return 无。
*/ */
void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) { void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
#if DEBUG_PRINT
/* 精简: 只打印关键帧(非DATA包或状态变化时), DATA包省略 */
if (type != OTA_LINK_MSG_DATA || ota_state != OTA_STATE_RECEIVING_FIRMWARE) {
printf("[OTA] hl: t=%u l=%u s=%u\r\n",
(unsigned)type, (unsigned)len, (unsigned)ota_state);
}
#endif
/** /**
* CANCEL:无条件终止当前 OTA 会话,回到空闲态。 * CANCEL:无条件终止当前 OTA 会话,回到空闲态。
*/ */
if (type == OTA_LINK_MSG_CANCEL) { if (type == OTA_LINK_MSG_CANCEL) {
ota_state = OTA_STATE_IDLE; ota_state = OTA_STATE_IDLE;
@@ -477,13 +534,37 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
return; return;
} }
/** /**
* DATA:根据当前状态解释为“固件分包”或“固件大小信息”等。 * REQ: 握手/开始请求。
*
* 只有 2 字节 payload (0x00 0xAC) 才是真正的握手请求。
* 16 字节 payload 是协议流程中的 nonce / confirm 数据,
* 需交由后续分支正常处理。
*
* 这解决了 CAN 总线帧在 SPI OTA 等待窗口内抢先改变
* ota_state 导致 SPI HANDSHAKE_REQ 收到 WAIT 的问题。
*/
else if (type == OTA_LINK_MSG_REQ && len == 2U && data[0] == 0x00U && data[1] == 0xACU) {
ota_time = 0;
if (ota_state <= OTA_STATE_CHECKING_FIRMWARE) {
/* 可重启态:IDLE / WAITING_RANDOM / WAITING_CONFIRM / CHECKING_FIRMWARE */
u8 ack_echo[2] = {0x00, 0xAC};
send_ack_frame(2, ack_echo);
ota_state = OTA_STATE_WAITING_RANDOM;
} else {
/* 已接收固件数据,拒绝新 REQ */
u8 wait_hint[1] = {0x01};
send_wait_frame(1, wait_hint);
}
return;
}
/**
* DATA:根据当前状态解释为"固件分包"或"固件大小信息"等。
*/ */
else if (type == OTA_LINK_MSG_DATA) { else if (type == OTA_LINK_MSG_DATA) {
ota_time = 0; ota_time = 0;
/** /**
* 正常固件数据包: * 正常固件数据包:
* - 固定 64 字节 * - 固定 64 字节
* - 含会话号、包号、48 字节密文、结束标志、CRC32 * - 含会话号、包号、48 字节密文、结束标志、CRC32
*/ */
@@ -502,7 +583,7 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
aes128_set_key(&aes_ctx, current_K_session); aes128_set_key(&aes_ctx, current_K_session);
aes128_ecb_decrypt_48bytes(&aes_ctx, receive_encrypted_data, decrypted_data); aes128_ecb_decrypt_48bytes(&aes_ctx, receive_encrypted_data, decrypted_data);
/** 校验分包 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) {
ota_error = OTA_ERROR_INVALID_CRC; ota_error = OTA_ERROR_INVALID_CRC;
@@ -520,8 +601,8 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
} }
/** /**
* IFEND == 0xFFFFFFFF 表示最后一包: * IFEND == 0xFFFFFFFF 表示最后一包:
* 先反馈“全部写入完成,等待 FINISH_OTA”, * 先反馈"全部写入完成,等待 FINISH_OTA",
* 真正跳转放到收到 0x0A AC 后再执行。 * 真正跳转放到收到 0x0A AC 后再执行。
*/ */
if (receive_IFEND == 0xFFFFFFFFU) { if (receive_IFEND == 0xFFFFFFFFU) {
@@ -535,9 +616,9 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
send_structured_ack(request_packet); send_structured_ack(request_packet);
} }
/** /**
* 固件大小阶段: * 固件大小阶段:
* 在握手成功后,由上位机先发 4 字节固件大小, * 在握手成功后,由上位机先发 4 字节固件大小,
* Bootloader 校验是否超过 APP_MAX_SIZE,并准备擦除 Flash。 * Bootloader 校验是否超过 APP_MAX_SIZE,并准备擦除 Flash。
*/ */
else if (len == 4U && ota_state == OTA_STATE_CHECKING_FIRMWARE) { else if (len == 4U && ota_state == OTA_STATE_CHECKING_FIRMWARE) {
u32 new_size = *(u32 *)data; u32 new_size = *(u32 *)data;
@@ -551,13 +632,16 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
return; return;
} }
/** 把擦除动作延后到 process() 中执行。 */ /**
* 不要立刻 ACK(seq=0)。
* 先告诉 ESP32“我在忙(WAIT)”,等 process() 真正擦除完 Flash 后,
* 再发送 ACK(seq=0) 作为“现在可以开始发包”的准入信号。
*/
pending_flash_erase = true; pending_flash_erase = true;
u8 wait_hint[1] = {0x02};
/** 固件大小合法,返回严格 seq 语义 ACK:阶段起始包号 0。 */ send_wait_frame(1, wait_hint);
send_structured_ack(0U);
} }
/** 结束态下要求显式收到 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()) { if (!validateApplication()) {
@@ -582,21 +666,24 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
send_error_frame(0xFFFFFFFFU, OTA_ERROR_INVALID_PACKET); send_error_frame(0xFFFFFFFFU, OTA_ERROR_INVALID_PACKET);
return; return;
} }
/** 其它长度 / 状态组合都视作非法包。 */
else { else {
ota_error = OTA_ERROR_INVALID_PACKET; ota_error = OTA_ERROR_INVALID_PACKET;
#if DEBUG_PRINT
printf("[OTA] INV_PKT: l=%u s=%u\r\n",
(unsigned)len, (unsigned)ota_state);
#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);
} }
} }
/** /**
* REQ:用于握手阶段。 * REQ:用于握手阶段。
*/ */
else if (type == OTA_LINK_MSG_REQ) { else if (type == OTA_LINK_MSG_REQ) {
ota_time = 0; ota_time = 0;
/** /**
* 第一步:上位机发固定魔术字 0x00 0xAC,申请开始握手。 * 第一步:上位机发固定魔术字 0x00 0xAC,申请开始握手。
*/ */
if (data[0] == 0x00U && data[1] == 0xACU && len == 2U && ota_state == OTA_STATE_IDLE) { if (data[0] == 0x00U && data[1] == 0xACU && len == 2U && ota_state == OTA_STATE_IDLE) {
u8 send_data[2] = {0x00, 0xAC}; u8 send_data[2] = {0x00, 0xAC};
@@ -604,8 +691,8 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
ota_state = OTA_STATE_WAITING_RANDOM; ota_state = OTA_STATE_WAITING_RANDOM;
} }
/** /**
* 第二步:上位机发送 16 字节随机数 nonce_server。 * 第二步:上位机发送 16 字节随机数 nonce_server。
* Bootloader: * Bootloader:
* - 读取设备 UID * - 读取设备 UID
* - 用主密钥加密 nonce 派生会话密钥 K_session * - 用主密钥加密 nonce 派生会话密钥 K_session
* - 回传 UID + nonce 片段 + 加密后的 K_session * - 回传 UID + nonce 片段 + 加密后的 K_session
@@ -636,8 +723,8 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
ota_state = OTA_STATE_WAITING_CONFIRM; ota_state = OTA_STATE_WAITING_CONFIRM;
} }
/** /**
* 第三步:上位机使用会话密钥加密 (nonce_server XOR 0x55),回传 16 字节确认块。 * 第三步:上位机使用会话密钥加密 (nonce_server XOR 0x55),回传 16 字节确认块。
* Bootloader 解密后比对,成功则握手完成。 * Bootloader 解密后比对,成功则握手完成。
*/ */
else if (len == 16U && ota_state == OTA_STATE_WAITING_CONFIRM) { else if (len == 16U && ota_state == OTA_STATE_WAITING_CONFIRM) {
u8 plaintext[16]; u8 plaintext[16];
@@ -673,7 +760,7 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
} }
/** /**
* @brief 兼容旧接口:按 CAN ID 分发 OTA 消息。 * @brief 兼容旧接口:按 CAN ID 分发 OTA 消息。
* *
* @param id CAN 扩展帧 ID。 * @param id CAN 扩展帧 ID。
* @param data 指向消息数据缓冲区的指针。 * @param data 指向消息数据缓冲区的指针。
@@ -681,7 +768,7 @@ void OTAHandler::handleLinkMessage(ota_link_msg_type_t type, u8 *data, u8 len) {
* @return 无。 * @return 无。
*/ */
void OTAHandler::handleOTAMessage(u32 id, u8 *data, u8 len) { void OTAHandler::handleOTAMessage(u32 id, u8 *data, u8 len) {
/** 兼容旧的“按 CAN ID 分发”调用方式。 */ /** 兼容旧的"按 CAN ID 分发"调用方式。 */
if (id == CANFD_OTA_CANCEL_ID) { if (id == CANFD_OTA_CANCEL_ID) {
handleLinkMessage(OTA_LINK_MSG_CANCEL, data, len); handleLinkMessage(OTA_LINK_MSG_CANCEL, data, len);
} else if (id == CANFD_OTA_DATA_ID) { } else if (id == CANFD_OTA_DATA_ID) {
@@ -721,9 +808,9 @@ void send_wait_frame(u8 len, const u8 *data) {
*/ */
void send_error_frame(u32 packet_num, u8 error) { void send_error_frame(u32 packet_num, u8 error) {
/** /**
* 结构化错误帧: * 结构化错误帧:
* [0..1] = 0xBA 0xC0 * [0..1] = 0xBA 0xC0
* [2..5] = 相关包号(小端) * [2..5] = 相关包号(小端)
* [6] = 错误码 * [6] = 错误码
*/ */
u8 error_frame[7] = {0xBA, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, error}; u8 error_frame[7] = {0xBA, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, error};
@@ -756,15 +843,15 @@ void getDeviceSerialNumber(u8 *buffer) {
/** /**
* @brief 将备份应用区整份固件搬运到当前主应用区。 * @brief 将备份应用区整份固件搬运到当前主应用区。
* *
* @return FMC 状态码;CRC 校验失败时返回 0xFF。 * @return FMC 状态码;CRC 校验失败时返回 0xFF。
*/ */
fmc_state_enum move_app_data() { fmc_state_enum move_app_data() {
/** /**
* 把备份应用区整体复制到当前主应用区: * 把备份应用区整体复制到当前主应用区:
* 1. 先擦当前主应用区 * 1. 先擦当前主应用区
* 2. 计算备份区 CRC * 2. 计算备份区 CRC
* 3. 逐 word 拷贝非 0xFFFFFFFF 的数据 * 3. 逐 word 拷贝非 0xFFFFFFFF 的数据
* 4. 再计算目标区 CRC 比较,确认搬运成功 * 4. 再计算目标区 CRC 比较,确认搬运成功
*/ */
fmc_state_enum fmc_status = FMC_READY; fmc_state_enum fmc_status = FMC_READY;
__disable_irq(); __disable_irq();
@@ -820,7 +907,7 @@ fmc_state_enum move_app_data() {
*/ */
void calculate_crc32_with_key(const u8 *key, const u8 *data, u8 *output) { void calculate_crc32_with_key(const u8 *key, const u8 *data, u8 *output) {
/** /**
* 计算方式: * 计算方式:
* 1. 先把 16 字节会话密钥喂入 CRC32 * 1. 先把 16 字节会话密钥喂入 CRC32
* 2. 再把 48 字节有效数据喂入 CRC32 * 2. 再把 48 字节有效数据喂入 CRC32
* 3. 最终按小端输出 4 字节结果 * 3. 最终按小端输出 4 字节结果
+32
View File
@@ -5,7 +5,10 @@
#include "aes128.h" #include "aes128.h"
#include "config.h" #include "config.h"
#include "gd32e50x.h" #include "gd32e50x.h"
#include <stdint.h> #include <stdint.h>
#include <string.h>
/** /**
* @brief OTA 统一链路消息类型。 * @brief OTA 统一链路消息类型。
@@ -71,6 +74,35 @@ class OTAHandler {
*/ */
bool isActive() const { return ota_state != OTA_STATE_IDLE; } bool isActive() const { return ota_state != OTA_STATE_IDLE; }
/**
* @brief 获取当前 OTA 状态值(调试用)。
*/
u8 getState() const { return (u8)ota_state; }
/**
* @brief 重置 OTA 状态机到 IDLE,用于进BOOT命令等场景。
*
* 上位机发 0xB0 进BOOT命令时,如果设备已在 BOOT 中,
* 需要将 OTA 状态机重置为 IDLE,确保后续握手能正常进行。
*
* @return 无。
*/
void resetToIdle() {
ota_state = OTA_STATE_IDLE;
ota_time = 0;
ota_error = OTA_ERROR_NONE;
received_size = 0;
expected_size = 0;
memset(current_K_session, 0, sizeof(current_K_session));
memset(nonce_server, 0, sizeof(nonce_server));
memset(&aes_ctx, 0, sizeof(aes_ctx));
pending_flash_erase = false;
pending_app_jump = false;
pending_flash_offset = 0;
memset(pending_flash_data, 0, sizeof(pending_flash_data));
pending_flash_size = 0;
}
/** /**
* @brief 设置当前底层回包发送函数。 * @brief 设置当前底层回包发送函数。
* *
+2 -5
View File
@@ -8,11 +8,8 @@ typedef uint8_t u8;
typedef uint16_t u16; typedef uint16_t u16;
typedef uint32_t u32; typedef uint32_t u32;
/** 调试开关:需要时可按模块打开。 */ /** 调试打印总开关。置 1 时所有 printf 调试输出编译进去;置 0 时完全剔除,零开销。 */
#define EEP_DEBUG 0 #define DEBUG_PRINT 1
#define DEBUG_OTA 0
#define CAN_DEBUG 0
#define SLEEP_DEBUG 0
/** /**
* Flash 布局说明(GD32E508VE,512KB) * Flash 布局说明(GD32E508VE,512KB)