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,启用调试打印输出,便于运行时状态监控和问题排查
This commit is contained in:
cbb
2026-05-28 21:52:07 +08:00
parent d1beb3b615
commit 9d4572da13
5 changed files with 288 additions and 57 deletions
+249 -45
View File
@@ -11,13 +11,13 @@
/** /**
* @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 中断逐字节发送,消除字节错位问题。 * 硬件 DMA 替代 TBE 中断逐字节发送,消除字节错位问题。
* *
* 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
*/ */
@@ -26,6 +26,18 @@
#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
/* 调试:多缓冲日志(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;
@@ -89,7 +101,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) {
@@ -114,14 +126,11 @@ void SpiOta::exitCritical(uint32_t primask) {
/** @brief 启动 DMA 传输 s_txBuf → SPI0_DATA */ /** @brief 启动 DMA 传输 s_txBuf → SPI0_DATA */
static void startSpiTxDma(const u8 *buf, u16 len) { static void startSpiTxDma(const u8 *buf, u16 len) {
dma_channel_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); 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);
while (DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) & DMA_CHXCTL_CHEN) {}
DMA_CHMADDR(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = (uint32_t)buf; 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_CHCNT(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = 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_channel_enable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH);
} }
void SpiOta::queueTxFrame(const u8 *frame, u16 len) { void SpiOta::queueTxFrame(const u8 *frame, u16 len) {
@@ -131,23 +140,58 @@ void SpiOta::queueTxFrame(const u8 *frame, u16 len) {
s_txLen = len; s_txLen = len;
s_txPending = true; s_txPending = true;
s_txStreamPos = 0U; s_txStreamPos = 0U;
/* 填充剩余空间为 0xFF */
if (len < kTxBufSize) { memset(&s_txBuf[len], 0xFF, kTxBufSize - len); } if (len < kTxBufSize) { memset(&s_txBuf[len], 0xFF, kTxBufSize - len); }
exitCritical(primask); exitCritical(primask);
/* 启用 SPI DMA TX + 启动 DMA 通道 */
spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);
startSpiTxDma(s_txBuf, s_txLen); 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 直接帧处理 * CS 释放后 ISR 直接帧处理
* ============================================================ */ * ============================================================ */
void SpiOta::isrProcessFrame() { void SpiOta::isrProcessFrame() {
u16 rx_len = s_rxStreamLen; u16 rx_len = s_rxStreamLen;
if (rx_len < kFrameOverhead) { return; } if (rx_len == 0U) { return; }
if (s_rxStream[0] != kSof) { 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 payload_len = (u16)(s_rxStream[2] | ((u16)s_rxStream[3] << 8));
u16 expected = (u16)(kFrameOverhead + payload_len); u16 expected = (u16)(kFrameOverhead + payload_len);
@@ -160,6 +204,7 @@ void SpiOta::isrProcessFrame() {
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;
} }
@@ -167,16 +212,31 @@ void SpiOta::isrProcessFrame() {
const u8 *payload = nullptr; const u8 *payload = nullptr;
u16 p_len = 0U; u16 p_len = 0U;
if (!tryParseFrame(s_rxStream, expected, &frame_type, &payload, &p_len)) { if (!tryParseFrame(s_rxStream, expected, &frame_type, &payload, &p_len)) {
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; return;
} }
if (s_otaHandler != nullptr) { if (s_otaHandler != nullptr) {
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);
isrLogFrameEx("[OTA-MSG]", (const u8*)&frame_type, 1);
s_otaHandler->handleLinkMessage(msg_type, const_cast<u8 *>(payload), (u8)p_len); s_otaHandler->handleLinkMessage(msg_type, const_cast<u8 *>(payload), (u8)p_len);
} }
isrLogFrame("[ISR-DONE]", s_txBuf, s_txLen); isrLogFrameEx("[ISR-DONE]", s_txBuf, (s_txLen < 16U) ? s_txLen : 16U);
s_isrResponded = true; s_isrResponded = true;
} }
@@ -185,21 +245,27 @@ void SpiOta::isrProcessFrame() {
* ============================================================ */ * ============================================================ */
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++;
} }
/* ============================================================ /* ============================================================
@@ -221,6 +287,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;
@@ -228,29 +295,52 @@ 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);
/* RBNE 中断接收,TBE 由 DMA 替代 */ /* 初始化时清空 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() { void SpiOta::configureDma() {
/* DMA0_CH2 = SPI0_TX (GD32E50x 固定映射) */
rcu_periph_clock_enable(RCU_DMA0); rcu_periph_clock_enable(RCU_DMA0);
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);
dma_init_struct.periph_addr = (uint32_t)(SPI0 + 0x0CU); /* SPI0_DATA 寄存器 */ dma_init_struct.periph_addr = (uint32_t)(SPI0 + 0x0CU);
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT;
dma_init_struct.memory_addr = (uint32_t)s_txBuf; dma_init_struct.memory_addr = (uint32_t)s_txBuf;
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT;
dma_init_struct.number = 0U; /* 运行时由 queueTxFrame 设置 */ dma_init_struct.number = 0U;
dma_init_struct.priority = DMA_PRIORITY_HIGH; dma_init_struct.priority = DMA_PRIORITY_HIGH;
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
@@ -292,16 +382,97 @@ void SpiOta::onCsAssert() {
s_csActive = true; s_csActive = true;
s_rxStreamLen = 0U; s_rxStreamLen = 0U;
s_txStreamPos = 0U; s_txStreamPos = 0U;
/* DMA 已在 queueTxFrame 中启动,无需再操作 */ 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() { void SpiOta::onCsRelease() {
if (!s_csActive) { return; } if (!s_csActive) { return; }
s_csActive = false; s_csActive = false;
/* 停止 DMA 传输 */
dma_channel_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); /* 内部 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); spi_dma_disable(SPI0, SPI_DMA_TRANSMIT);
if (s_rxStreamLen > 0U && s_rxStreamLen <= kRxBufSize) {
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(); isrProcessFrame();
} }
} }
@@ -312,13 +483,33 @@ void SpiOta::onCsRelease() {
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) {
/* 与 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;
}
/* 调试阶段先关闭 RBNE 首字节日志。
* 原因:ISR 日志缓冲是单槽的,RBNE 一旦先占位,
* onCsRelease() 里的 [CS-RX] 摘要日志就会被丢弃。
* 目前排查重点是“本次 CS 周期总共收到了哪些字节”,
* 所以优先保留 [CS-RX],暂时关闭 [RBNE]。 */
tryProcessCompleteFrame();
} }
/* TBE 已由 DMA 替代,不再处理 */ /* TBE 已由 DMA 替代,不再处理 */
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
(void)spi_i2s_data_receive(SPI0); (void)spi_i2s_data_receive(SPI0);
s_rxStreamLen = 0U; s_rxStreamLen = 0U;
isrLogFrame("[RXOR]", (const u8*)"OVR", 3);
} }
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);
@@ -341,13 +532,29 @@ void SpiOta::poll() {
if (s_isrResponded) { if (s_isrResponded) {
s_isrResponded = false; s_isrResponded = false;
} }
if (s_isrLogPending) { /* 打印 ISR 日志缓冲区 */
#if DEBUG_PRINT while (s_logCount > 0U) {
printf("[SPI-BOOT] %s\r\n", s_isrLogBuf); __disable_irq();
#endif if (s_logCount == 0U) { __enable_irq(); break; }
s_isrLogPending = false; 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 从机状态 */
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::setSender(OTAHandler *handler) { s_otaHandler = handler; }
@@ -392,9 +599,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();
} }
+3 -4
View File
@@ -16,6 +16,7 @@ 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
@@ -44,12 +45,10 @@ public:
static bool hasTxFrame(); static bool hasTxFrame();
static u16 copyTxFrame(u8 *out, u16 max_len); static u16 copyTxFrame(u8 *out, u16 max_len);
/** @brief CS 释放后 ISR 中直接完成帧校验+处理+响应准备。 */
static void isrProcessFrame(); static void isrProcessFrame();
/** @brief ISR 内轻量日志写入延迟打印缓冲。 */ static void tryProcessCompleteFrame();
static void isrLogFrame(const char *tag, const u8 *data, u16 len); static void isrLogFrame(const char *tag, const u8 *data, u16 len);
/* ---- 公开成员 ---- */
static OTAHandler *s_otaHandler; static OTAHandler *s_otaHandler;
static volatile bool s_csActive; static volatile bool s_csActive;
static volatile u16 s_rxStreamLen; static volatile u16 s_rxStreamLen;
@@ -58,7 +57,7 @@ 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; // ISR 已发送响应帧,主循环本轮不再发 WAIT static volatile bool s_isrResponded;
static volatile bool s_isrLogPending; static volatile bool s_isrLogPending;
static u8 s_isrLogBuf[kIsrLogBufSize]; static u8 s_isrLogBuf[kIsrLogBufSize];
static u16 s_isrLogLen; static u16 s_isrLogLen;
+10 -1
View File
@@ -69,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();
} }
/** /**
+19
View File
@@ -71,6 +71,25 @@ 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;
}
/** /**
* @brief 设置当前底层回包发送函数。 * @brief 设置当前底层回包发送函数。
* *
+1 -1
View File
@@ -9,7 +9,7 @@ typedef uint16_t u16;
typedef uint32_t u32; typedef uint32_t u32;
/** 调试打印总开关。置 1 时所有 printf 调试输出编译进去;置 0 时完全剔除,零开销。 */ /** 调试打印总开关。置 1 时所有 printf 调试输出编译进去;置 0 时完全剔除,零开销。 */
#define DEBUG_PRINT 0 #define DEBUG_PRINT 1
/** /**
* Flash 布局说明(GD32E508VE,512KB) * Flash 布局说明(GD32E508VE,512KB)