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