feat(spi): 更新SPI命令v2,处理逻辑和项目配置

- 在cproject.yml中添加spi_link模块组和相关头文件路径
- 修改boot_request.cpp中的ACK响应机制,改用标准帧格式并优化CAN反馈处理
- 重构ping.cpp响应payload结构,包含版本信息、运行时间等16字节数据
- 添加systick.h依赖用于获取系统运行时间
- 优化启动bootloader流程,避免SPI中断阻塞问题
This commit is contained in:
cbb
2026-07-13 03:10:37 +08:00
parent 10989939e9
commit 7c05496b89
9 changed files with 422 additions and 345 deletions
+307 -273
View File
@@ -12,15 +12,19 @@
* 通过 registerHandler() 注册进来,后续新增 SPI 业务时
* 只需要新增命令处理器,不再继续膨胀本文件。
*
* 握手架构(线与 SPIready,PB6<->ESP32 GPIO10 开漏单线):
* 握手架构(v2 DATA_READY,PB6<->ESP32 GPIO10 开漏单线):
* - PB6 低 = 有待发数据(m_txLen>0 或 m_ringPending),高 = 队列空。
* - 命令处理在 onCsRelease() EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame),
* 不再推迟到主循环,消除 PB6 拉高延迟。
* - GD32 可通过 requestEsp32Read() 下拉 SPIready 主动按铃请求 ESP32 来读。
* 完成后由 updateDataReady() 依据待发数据驱动 PB6,不再延迟拉高。
* - GD32 可通过 requestEsp32Read() 设 m_ringPending 并由 updateDataReady()
* 拉低 PB6 主动按铃,请求 ESP32 来读。
*/
#include "device/spi/app_spi_command.h"
#include "device/spi/spi_command_handler.h"
#include "device/spi/app_spi_command_defs.h"
#include "spi_link_v2.h"
#include "sys/ota_boot_request.h"
extern "C" {
#include "gd32e50x_exti.h"
@@ -33,7 +37,6 @@ extern "C" {
#include "sys/config.h"
#include "sys/critical.h"
#include "sys/task_manager.h"
#include "sys/ota_boot_request.h"
#include "sys/systick.h"
#include "main.h"
#include <cstdio>
@@ -42,7 +45,6 @@ extern "C" {
/* ============================================================
* 本地别名:保持当前实现文件内部风格不变
* ============================================================ */
static constexpr uint8_t kSof = SpiDefs::kSof;
static constexpr uint8_t kTypeBootReq = SpiDefs::kTypeBootReq;
static constexpr uint8_t kTypeCanSend = SpiDefs::kTypeCanSend;
static constexpr uint8_t kTypeOtaReq = SpiDefs::kTypeOtaReq;
@@ -52,7 +54,6 @@ static constexpr uint8_t kTypeOtaCancel = SpiDefs::kTypeOtaCancel;
static constexpr uint8_t kRspAck = SpiDefs::kRspAck;
static constexpr uint8_t kRspWait = SpiDefs::kRspWait;
static constexpr uint8_t kRspError = SpiDefs::kRspError;
static constexpr uint16_t kFrameOverhead = SpiDefs::kFrameOverhead;
/* DMA 配置(与 BOOT SpiOta 一致:DMA0_CH2 用于 SPI0 TX) */
#define SPI_TX_DMA_PERIPH DMA0
@@ -90,12 +91,10 @@ static bool shouldIgnoreTransaction(const uint8_t *data, uint16_t len)
return true;
}
/* APP 侧只接受两类事务:
* 1) 旧式单字节 0xB0
* 2) 完整协议帧(至少 5 字节)
* 短于 5 字节且不是 0xB0 的事务只能是启动切换期的残包,直接忽略。 */
return (len < kFrameOverhead) &&
!(len == 1U && data[0] == CONFIG_SPI_APP_OTA_BOOT_CMD);
/* v2 协议帧最小长度为 V2_OVERHEAD(20B)。
* 短于该长度的事务只能是启动切换期的残包,直接忽略。
* (0xB0 进 BOOT 现已走标准 v2 帧 handler,不再有裸字节通道。) */
return (len < V2_OVERHEAD);
}
static bool isFlashRelatedFrame(uint8_t frame_type)
@@ -111,155 +110,67 @@ static bool isFlashRelatedFrame(uint8_t frame_type)
}
}
/* ============================================================
* CRC8 / 帧构建 / 帧解析(与 BOOT SpiOta 一致)
* ============================================================ */
static constexpr uint8_t kSyncSof0 = 0xA5U;
static constexpr uint8_t kSyncSof1 = 0x5AU;
static constexpr uint16_t kSyncHeaderLen = 6U;
static constexpr uint16_t kSyncCrcLen = 4U;
static constexpr uint16_t kSyncOverhead = (uint16_t)(kSyncHeaderLen + kSyncCrcLen);
/* 扫描前 16 字节查找同步帧头 SOF0 SOF1,容忍 RXORERR 导致的首字节偏移。
* 与 spi_try_parse_frame 的偏移扫描逻辑对齐。 */
static bool containsSyncSof(const uint8_t *data, uint16_t len)
{
if (!data || len < 2U) { return false; }
uint16_t last = (len < 16U) ? (uint16_t)(len - 1U) : 16U;
for (uint16_t i = 0U; i < last; ++i) {
if (data[i] == kSyncSof0 && data[i + 1U] == kSyncSof1) { return true; }
}
return false;
}
/* GD32 APP 侧同步状态机(ISR 与主循环只读/原子写,避免临界区开销)。 */
static volatile uint8_t g_localCredit = 8U;
static volatile uint8_t g_remoteCredit = 8U;
static void putU16Le(uint8_t *pp, uint16_t v)
{
pp[0] = (uint8_t)(v & 0xFFU);
pp[1] = (uint8_t)((v >> 8) & 0xFFU);
}
/* SPI Link v2 会话/序号状态机(ISR 写、主循环读写,均原子,无需临界区)。 */
static volatile uint32_t g_sessionId = 0; /* v2 会话 ID(0=未建立) */
static volatile uint8_t g_txSeq = 0; /* GD32 发送序号(事件/响应帧) */
static volatile uint8_t g_expectedRxSeq = 0; /* 期望收到的 ESP32 请求序号 */
static volatile bool g_v2Active = false; /* v2 session 是否激活 */
/* 当前请求的 TX_SEQ,写入响应 ACK_SEQ,供 ESP32 做响应归属匹配(延迟一拍)。 */
static volatile uint8_t g_pendingAckSeq = 0;
static uint16_t getU16Le(const uint8_t *pp)
{
return (uint16_t)(pp[0] | ((uint16_t)pp[1] << 8));
}
/* 响应缓存:用于重复请求重发同一响应(避免业务 handler 重复执行)。 */
static uint8_t s_lastReqSeq = 0xFF;
static uint8_t s_lastRespBuf[256];
static uint16_t s_lastRespLen = 0;
static bool s_hasCachedResp = false;
static void putU32Le(uint8_t *pp, uint32_t v)
{
pp[0] = (uint8_t)(v & 0xFFU);
pp[1] = (uint8_t)((v >> 8) & 0xFFU);
pp[2] = (uint8_t)((v >> 16) & 0xFFU);
pp[3] = (uint8_t)((v >> 24) & 0xFFU);
}
/* ============================================================
* v2 帧构建 / 帧解析(委托 spi_link_v2.h,三工程共用)
* ============================================================ */
static uint32_t getU32Le(const uint8_t *pp)
{
return (uint32_t)pp[0] | ((uint32_t)pp[1] << 8) |
((uint32_t)pp[2] << 16) | ((uint32_t)pp[3] << 24);
}
static uint32_t spi_crc32(const uint8_t *data, uint16_t len)
{
uint32_t crc = 0xFFFFFFFFU;
for (uint16_t i = 0U; i < len; ++i) {
crc ^= (uint32_t)data[i] << 24;
for (int b = 0; b < 8; ++b) {
if (crc & 0x80000000U) {
crc = (crc << 1) ^ 0x04C11DB7U;
} else {
crc <<= 1;
}
}
}
return crc;
}
static uint8_t spi_crc8(const uint8_t *data, uint16_t len)
{
uint8_t crc = 0x00U;
for (uint16_t i = 0U; i < len; ++i) { crc ^= data[i]; }
return crc;
}
/* 构建新同步帧;上层接口(type/payload)保持不变,handler 无需改动。
* kFrameOverhead 旧常量仍保留供旧路径使用。 */
/* 构建响应帧;上层接口(type/payload)保持不变,handler 无需改动。
* 内部调用 v2_build_frame,FLAGS 默认 V2_FLAG_RESPONSE,session/seq/credit
* 从 v2 状态全局变量取。session 未建立时 session_id 填 0。
* ACK_SEQ = g_pendingAckSeq(对端请求的 TX_SEQ),不能写死 0,否则 ESP32
* otaFindFrame 在 m_v2Active 时会把合法延迟响应当成 stale 丢弃。 */
static bool spi_build_frame(uint8_t type, const uint8_t *payload, uint16_t payload_len,
uint8_t *out, uint16_t *out_len)
{
if (!out || !out_len) { return false; }
uint16_t total = (uint16_t)(kSyncOverhead + payload_len);
if (total > AppSpiCommand::kTxBufSize) { return false; }
if ((uint32_t)V2_OVERHEAD + payload_len > AppSpiCommand::kTxBufSize) { return false; }
out[0] = kSyncSof0;
out[1] = kSyncSof1;
out[2] = g_localCredit;
out[3] = type;
putU16Le(&out[4], payload_len);
if (payload && payload_len > 0U) {
memcpy(&out[kSyncHeaderLen], payload, payload_len);
}
uint32_t crc = spi_crc32(out, (uint16_t)(kSyncHeaderLen + payload_len));
putU32Le(&out[kSyncHeaderLen + payload_len], crc);
*out_len = total;
const uint32_t sess = g_v2Active ? g_sessionId : 0U;
size_t built = 0U;
const bool ok = v2_build_frame(type, V2_FLAG_RESPONSE, sess,
g_txSeq, g_pendingAckSeq, (uint8_t)g_localCredit,
payload, payload_len, out, &built);
if (!ok) { return false; }
*out_len = (uint16_t)built;
return true;
}
/* 解析:新同步帧优先;失败回退旧 0xA5 帧(含首字节偏移容错)。 */
/* 解析 v2 帧;委托 v2_parse_frame,扫描 SOF 并校验 VERSION/CRC32。
* 旧的 0xA5 CRC8 帧兼容路径已移除(v2 不再向后兼容 v1)。 */
static bool spi_try_parse_frame(const uint8_t *frame, uint16_t frame_len,
uint8_t *type, const uint8_t **payload, uint16_t *payload_len)
uint8_t *type, const uint8_t **payload, uint16_t *payload_len,
v2_header_t *out_hdr = nullptr)
{
if (!frame || !type || !payload || !payload_len) { return false; }
for (uint16_t s = 0U; s + kSyncOverhead <= frame_len; ++s) {
if (frame[s] != kSyncSof0 || frame[s + 1U] != kSyncSof1) { continue; }
uint16_t plen = getU16Le(&frame[s + 4U]);
uint16_t total = (uint16_t)(kSyncOverhead + plen);
if ((uint16_t)(frame_len - s) < total) { continue; }
uint32_t expect = getU32Le(&frame[s + kSyncHeaderLen + plen]);
uint32_t calc = spi_crc32(&frame[s], (uint16_t)(kSyncHeaderLen + plen));
if (expect != calc) { continue; }
v2_header_t hdr;
const uint8_t *pload = nullptr;
const bool ok = v2_parse_frame(frame, frame_len, &hdr, &pload);
if (!ok) { return false; }
uint8_t credit = frame[s + 2U];
*type = frame[s + 3U];
*payload = (plen > 0U) ? &frame[s + kSyncHeaderLen] : nullptr;
*payload_len = plen;
g_remoteCredit = credit;
return true;
}
/* 旧 0xA5 帧兼容:首字节可能偏移,扫描 SOF。 */
uint16_t s = 0U;
while (s < frame_len && frame[s] != kSof) { ++s; }
if (s > 0U && s < frame_len) {
const uint16_t shifted_len = (uint16_t)(frame_len - s);
if (shifted_len >= kFrameOverhead) {
const uint16_t len_field = (uint16_t)frame[s + 2U] | ((uint16_t)frame[s + 3U] << 8);
const uint16_t expected = (uint16_t)(kFrameOverhead + len_field);
if (shifted_len >= expected &&
spi_crc8(&frame[s], (uint16_t)(4U + len_field)) == frame[s + 4U + len_field]) {
*type = frame[s + 1U];
*payload = &frame[s + 4U];
*payload_len = len_field;
return true;
}
}
}
if (frame_len < kFrameOverhead) { return false; }
if (frame[0] != kSof) { return false; }
uint16_t len = (uint16_t)frame[2] | ((uint16_t)frame[3] << 8);
uint16_t expected = (uint16_t)(kFrameOverhead + len);
if (frame_len < expected) { return false; }
if (spi_crc8(frame, (uint16_t)(4U + len)) != frame[4U + len]) { return false; }
*type = frame[1];
*payload = &frame[4];
*payload_len = len;
g_remoteCredit = hdr.credit;
*type = hdr.type;
*payload = pload;
*payload_len = hdr.payload_len;
if (out_hdr) { *out_hdr = hdr; }
return true;
}
@@ -322,9 +233,9 @@ void AppSpiCommand::configureGpio()
gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4);
gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15);
/* PB6 复用为 SPI Ready 握手线(开漏输出,线与)。
/* PB6 复用为 DATA_READY 握手线(开漏输出,线与)。
* 开漏模式:写 BRR = 下拉驱动低;写 BSRR = 高阻释放,靠外部/对端上拉拉高。
* 默认释放(HIGH),允许主机立刻发起首帧。 */
* 语义:低 = 有待发数据,高 = 队列空;默认释放(HIGH)。 */
gpio_init(CONFIG_SPI_READY_PORT, GPIO_MODE_OUT_OD, GPIO_OSPEED_50MHZ, CONFIG_SPI_READY_PIN);
gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN);
}
@@ -340,6 +251,14 @@ void AppSpiCommand::setSpiReady(bool ready)
}
}
/* v2 DATA_READY: PB6 低 = 有待发数据,高 = 队列空。
* 替代旧 BUSY/READY 语义中所有 setSpiReady 调用。 */
void AppSpiCommand::updateDataReady()
{
const bool has_data = (m_txLen > 0U) || m_ringPending;
setSpiReady(!has_data);
}
void AppSpiCommand::configureSpi0()
{
rcu_periph_clock_enable(RCU_SPI0);
@@ -362,7 +281,7 @@ void AppSpiCommand::configureSpi0()
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);
nvic_irq_enable(SPI0_IRQn, CONFIG_IRQ_PREEMPT_SPI_USART, 0U);
nvic_irq_enable(SPI0_IRQn, 2U, 0U);
spi_enable(SPI0);
}
@@ -393,15 +312,14 @@ void AppSpiCommand::configureExti()
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, CONFIG_IRQ_PREEMPT_SPI_USART, 1U);
nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U);
}
void AppSpiCommand::configureSpiReadyExti()
{
/* PB6 已在 configureGpio() 配置为开漏输出,此处启用 EXTI6 上升沿检测。
* EXTI 检测引脚实际电平(IDR),与 GPIO 输出模式兼容。
* 用于检测 ESP32 releaseReady 产生的上升沿(线从忙碌→空闲),
* 作为 ESP32 完成上一帧处理的可靠信号。 */
/* PB6 已在 configureGpio() 配置为开漏输出,此处保留 EXTI6 中断使能。
* v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 上升沿无业务含义,
* onSpiReadyExti() 仅清标志避免反复触发。 */
gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOB, GPIO_PIN_SOURCE_6);
exti_init(EXTI_6, EXTI_INTERRUPT, EXTI_TRIG_RISING);
exti_interrupt_enable(EXTI_6);
@@ -411,10 +329,10 @@ void AppSpiCommand::configureSpiReadyExti()
void AppSpiCommand::onSpiReadyExti()
{
/* v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 上升沿不再有意义。
* 清除中断标志避免反复触发。 */
if (exti_interrupt_flag_get(EXTI_6) != RESET) {
exti_interrupt_flag_clear(EXTI_6);
/* 线升高(空闲):ESP32 已 releaseReady,上一帧处理完 */
m_linkState.onEsp32Complete();
}
}
@@ -478,100 +396,202 @@ void AppSpiCommand::processCommand()
return;
}
/* 尝试按协议帧解析 */
/* 0xB0 已受理、正在写 OTA 配置页 / 即将复位:不再处理业务帧,避免打断 Flash。 */
if (OtaBootRequest::isSpiFrozenForBoot() || OtaBootRequest::isRebootPending()) {
m_txLen = 0U;
m_txPos = 0U;
updateDataReady();
return;
}
/* 尝试按 v2 协议帧解析;同时取回完整帧头用于 session/seq 校验 */
uint8_t frame_type = 0U;
const uint8_t *payload = nullptr;
uint16_t payload_len = 0U;
v2_header_t hdr;
if (spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len)) {
if (CONFIG_SPI_APP_LOG_ENABLE != 0 && !isFlashRelatedFrame(frame_type)) {
postLogFmt("[SPI-APP] 收到非刷写 SPI type=0x%02X", frame_type);
}
/* 合法帧:按 TYPE 分发给已注册命令处理器 */
if (!spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr)) {
/* 无法解析为合法 v2 帧:构建 ERROR 响应 */
postLogFmt("[SPI-APP] 非v2帧 0x%02X -> ERROR", m_rxBuf[0]);
uint16_t tx_len = 0U;
bool handled = false;
SpiCommandContext ctx{};
ctx.frame_type = frame_type;
ctx.payload = payload;
ctx.payload_len = payload_len;
ctx.tx_buf = m_txBuf;
ctx.tx_buf_size = kTxBufSize;
ctx.tx_len = &tx_len;
for (uint8_t i = 0U; i < m_commandCount; ++i) {
ISpiCommandHandler *handler = m_commands[i];
if (handler && handler->canHandle(frame_type)) {
handler->handle(AppSpiCommandBus, ctx);
handled = true;
break;
}
}
if (!handled) {
postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type);
uint8_t err = 0xEEU;
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
}
if (tx_len == 0U) {
/* 兜底:避免残留上一次应答帧 */
uint8_t err = 0xEEU;
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
}
uint8_t err = 0xEEU;
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
m_txLen = tx_len;
m_txPos = 0U;
} else {
/* 非标准帧:保留旧式单字节 0xB0 通道,后续新命令建议走注册式帧协议 */
const uint8_t first_valid = m_rxBuf[0U];
if (first_valid == CONFIG_SPI_APP_OTA_BOOT_CMD) {
postLog("旧式0xB0 -> 进BOOT");
uint16_t tx_len_legacy = 0U;
SpiCommandContext ctx{};
ctx.frame_type = first_valid;
ctx.payload = nullptr;
ctx.payload_len = 0U;
ctx.tx_buf = m_txBuf;
ctx.tx_buf_size = kTxBufSize;
ctx.tx_len = &tx_len_legacy;
bool handled = false;
for (uint8_t i = 0U; i < m_commandCount; ++i) {
ISpiCommandHandler *handler = m_commands[i];
if (handler && handler->canHandle(first_valid)) {
handler->handle(AppSpiCommandBus, ctx);
handled = true;
break;
}
}
if (!handled) {
/* 兜底路径(handler 未注册时的死代码保护):
* 同样使用异步路径,避免在 ISR 内执行 Flash 操作。
* 先装 0xB5 ACK,再设 pending 标志。 */
m_txBuf[0] = CONFIG_SPI_APP_OTA_BOOT_ACK_OK;
m_txBuf[1] = CONFIG_SPI_APP_OTA_BOOT_CMD;
m_txBuf[2] = 0x4FU;
m_txBuf[3] = 0x54U;
tx_len_legacy = 4U;
OtaBootRequest::requestEnterBootAsync();
TaskManager::set(1UL << TASK_HEARTBEAT);
}
m_txLen = tx_len_legacy;
m_txPos = 0U;
} else {
/* 无法识别 */
postLogFmt("[SPI-APP] 非协议 0x%02X -> ERROR", first_valid);
uint16_t tx_len = 0U;
uint8_t err = 0xEEU;
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
m_txLen = tx_len;
m_txPos = 0U;
if (m_logPending) {
TaskManager::set(1UL << TASK_SPI_LOG);
}
if (m_txLen > 0U) {
m_ringPending = true;
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
}
updateDataReady();
return;
}
if (CONFIG_SPI_APP_LOG_ENABLE != 0 && !isFlashRelatedFrame(frame_type)) {
postLogFmt("[SPI-APP] 收到非刷写 SPI type=0x%02X", frame_type);
}
/* 响应归属:ACK_SEQ 必须回显请求 TX_SEQ(延迟一拍 + ESP32 otaFindFrame 匹配)。 */
g_pendingAckSeq = hdr.tx_seq;
/* ---------- HELLO 建链路处理(在 session 校验之前) ---------- */
if (frame_type == V2_TYPE_LINK_HELLO) {
/* 接受 ESP32 提供的 session_id,重置本端序号状态机 */
g_sessionId = hdr.session_id;
g_txSeq = 0;
g_expectedRxSeq = 0;
g_v2Active = true;
s_hasCachedResp = false;
/* 构建 HELLO_ACK 响应帧(回显 session_id + 角色 + credit_max) */
size_t built_len = 0U;
v2_build_hello_ack_frame(g_sessionId, V2_ROLE_APP, (uint8_t)g_localCredit,
0, hdr.tx_seq, m_txBuf, &built_len);
m_txLen = (uint16_t)built_len;
m_txPos = 0U;
/* 缓存 HELLO_ACK 便于重复 HELLO 重发 */
s_lastReqSeq = hdr.tx_seq;
memcpy(s_lastRespBuf, m_txBuf, m_txLen);
s_lastRespLen = m_txLen;
s_hasCachedResp = true;
if (m_logPending) {
TaskManager::set(1UL << TASK_SPI_LOG);
}
if (m_txLen > 0U) {
m_ringPending = true;
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
}
updateDataReady();
return;
}
/* ---------- SESSION / SEQ 校验(仅在 v2 session 激活后生效) ---------- */
if (g_v2Active) {
/* session_id 不匹配:构建 LINK_ERROR(V2_ERR_SESSION),跳过 handler */
if (hdr.session_id != g_sessionId) {
postLogFmt("[SPI-APP] session 不匹配 0x%02X -> LINK_ERROR",
(uint8_t)(hdr.session_id & 0xFFU));
size_t built_len = 0U;
v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq,
(uint8_t)g_localCredit, V2_ERR_SESSION,
(uint8_t)g_expectedRxSeq, m_txBuf, &built_len);
m_txLen = (uint16_t)built_len;
m_txPos = 0U;
if (m_logPending) {
TaskManager::set(1UL << TASK_SPI_LOG);
}
if (m_txLen > 0U) {
m_ringPending = true;
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
}
updateDataReady();
return;
}
/* 序号回绕距离判定 */
const uint8_t dist = v2_seq_distance(hdr.tx_seq, g_expectedRxSeq);
if (dist == 0U) {
/* 正常:序号匹配,推进期望序号 */
g_expectedRxSeq = (uint8_t)(g_expectedRxSeq + 1U);
} else if (dist >= 128U) {
/* 重复请求:重发缓存的响应帧,跳过 handler */
if (s_hasCachedResp && hdr.tx_seq == s_lastReqSeq) {
postLogFmt("[SPI-APP] 重复请求 seq=0x%02X -> 重发缓存", hdr.tx_seq);
memcpy(m_txBuf, s_lastRespBuf, s_lastRespLen);
m_txLen = s_lastRespLen;
m_txPos = 0U;
if (m_logPending) {
TaskManager::set(1UL << TASK_SPI_LOG);
}
if (m_txLen > 0U) {
m_ringPending = true;
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
}
updateDataReady();
return;
}
/* 无缓存可用:按正常请求处理 */
} else {
/* dist 1-127:序号跳变。对齐期望序号后:
* - 普通业务:回 LINK_ERROR 并丢弃(避免副作用)
* - 0xB0 ENTER_BOOT:控制面命令,仍必须执行,否则永远进不了 BOOT */
postLogFmt("[SPI-APP] SEQ jump rx=0x%02X", hdr.tx_seq);
g_expectedRxSeq = (uint8_t)(hdr.tx_seq + 1U);
if (frame_type != kTypeBootReq) {
size_t built_len = 0U;
v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq,
(uint8_t)g_localCredit, V2_ERR_SEQ_JUMP,
(uint8_t)g_expectedRxSeq, m_txBuf, &built_len);
m_txLen = (uint16_t)built_len;
m_txPos = 0U;
if (m_logPending) {
TaskManager::set(1UL << TASK_SPI_LOG);
}
if (m_txLen > 0U) {
m_ringPending = true;
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
}
updateDataReady();
return;
}
postLog("SEQ jump but BOOT_REQ still handled");
/* fall through → 业务分发 */
}
}
/* ---------- 业务命令分发 ---------- */
uint16_t tx_len = 0U;
bool handled = false;
SpiCommandContext ctx{};
ctx.frame_type = frame_type;
ctx.payload = payload;
ctx.payload_len = payload_len;
ctx.tx_buf = m_txBuf;
ctx.tx_buf_size = kTxBufSize;
ctx.tx_len = &tx_len;
for (uint8_t i = 0U; i < m_commandCount; ++i) {
ISpiCommandHandler *handler = m_commands[i];
if (handler && handler->canHandle(frame_type)) {
handler->handle(AppSpiCommandBus, ctx);
handled = true;
break;
}
}
if (!handled) {
postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type);
uint8_t err = 0xEEU;
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
}
if (tx_len == 0U) {
/* 兜底:避免残留上一次应答帧 */
uint8_t err = 0xEEU;
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
}
m_txLen = tx_len;
m_txPos = 0U;
/* 缓存本次响应,便于重复请求时重发(避免 handler 重复执行副作用) */
if (g_v2Active) {
s_lastReqSeq = hdr.tx_seq;
memcpy(s_lastRespBuf, m_txBuf, m_txLen);
s_lastRespLen = m_txLen;
s_hasCachedResp = true;
/* 推进本端发送序号(响应帧占用一个 tx_seq 槽位) */
g_txSeq = (uint8_t)(g_txSeq + 1U);
}
if (m_logPending) {
@@ -579,7 +599,7 @@ void AppSpiCommand::processCommand()
TaskManager::set(1UL << TASK_SPI_LOG);
}
/* 应答帧已就绪:拉高 PB6(释放 SPIready)通知主机可发起下一笔事务。
/* 应答帧已就绪:由 updateDataReady() 依据 m_txLen 驱动 PB6(有待发=低)。
* 命令处理已在 ISR 内同步完成,无需 m_cmdPending 推迟机制。 */
/* 有响应待发时,设置按铃标志阻止主循环 canFeedbackPushStep() 覆盖 m_txBuf。
@@ -589,11 +609,11 @@ void AppSpiCommand::processCommand()
m_ringPending = true;
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
}
setSpiReady(true);
updateDataReady();
}
/* ============================================================
* GD32 主动按铃:下拉 SPIready 请求 ESP32 来读
* GD32 主动按铃:设 m_ringPending + updateDataReady() 拉低 PB6 请求 ESP32 来读
* ============================================================ */
void AppSpiCommand::cancelRing()
@@ -605,8 +625,9 @@ void AppSpiCommand::cancelRing()
m_txPos = 0U;
/* 超时后允许重试,不依赖 ESP32 releaseReady 上升沿 */
m_linkState.onRingTimeout();
/* 释放 SPIready(高阻),避免永久占用线 */
setSpiReady(true);
/* 释放 SPIready(高阻),避免永久占用线。
* cancelRing 已清 m_txLen 和 m_ringPending,updateDataReady() 会释放为高。 */
updateDataReady();
}
bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len)
@@ -616,22 +637,32 @@ bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload,
return false;
}
/* 构建请求帧到 m_txBuf(ESP32 下次读事务时通过 DMA 输出到 MISO) */
uint16_t built_len = 0U;
if (!spi_build_frame(frame_type, payload, payload_len, m_txBuf, &built_len)) {
/* 构建 v2 EVENT 帧到 m_txBuf(ESP32 下次读事务时通过 DMA 输出到 MISO)。
* 主动按铃帧使用 V2_FLAG_EVENT,session/seq 从 v2 状态全局变量取;
* session 未建立时 session_id 填 0。 */
const uint32_t sess = g_v2Active ? g_sessionId : 0U;
size_t built_len = 0U;
if (!v2_build_frame(frame_type, V2_FLAG_EVENT, sess,
g_txSeq, 0U, (uint8_t)g_localCredit,
payload, payload_len, m_txBuf, &built_len)) {
return false;
}
/* EVENT 帧占用一个发送序号槽位 */
if (g_v2Active) {
g_txSeq = (uint8_t)(g_txSeq + 1U);
}
m_ringTxLen = built_len;
m_txLen = built_len;
m_ringTxLen = (uint16_t)built_len;
m_txLen = (uint16_t)built_len;
m_txPos = 0U;
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
m_ringPending = true;
/* 消费完成 flag,等 ESP32 releaseReady 触发 EXTI6 上升沿 */
m_linkState.onRingStart();
/* 下拉 SPIready,通知 ESP32「我有请求帧要给你读」 */
setSpiReady(false);
/* v2 DATA_READY: 已设 m_ringPending=true 且 m_txLen>0,
* updateDataReady() 据此拉低 PB6,通知 ESP32「有待发数据」。 */
updateDataReady();
return true;
}
@@ -681,10 +712,8 @@ void AppSpiCommand::onCsAssert()
return;
}
m_csActive = true;
/* CS 下降沿立即拉低 PB6:表示从机即将忙碌,
* 防止主机在 onCsRelease 准备好响应前抢跑发起下一笔 CS。
* (若此帧被 shouldIgnoreTransaction 忽略,onCsRelease 会恢复 HIGH) */
setSpiReady(false);
/* v2 DATA_READY: CS 本身即互斥,PB6 不再表示「忙碌」,不在 CS 下降沿拉低。
* PB6 仅由 updateDataReady() 依据待发数据驱动,避免与队列语义冲突。 */
m_rxLen = 0U;
m_txPos = 0U;
m_txActiveThisCs = false;
@@ -740,24 +769,27 @@ void AppSpiCommand::onCsRelease()
/* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧,
* 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。
*
* 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 0xB0 命令(OTA 请求)。
* 若 GD32 正在按铃时 ESP32 发 0xB0,会因 m_ringPending=true 被误吞,
* 导致 OTA 无法启动。检测到 0xB0 时取消按铃并走正常命令处理。 */
* 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 v2 协议帧(如 PING/OTA/0xB0)。
* 若 GD32 正在按铃时 ESP32 发协议帧,会因 m_ringPending=true 被误吞,
* 导致命令无法处理。检测到 v2 SOF 时取消按铃并走正常命令处理。 */
if (m_ringPending) {
if (m_rxLen >= 1U && m_rxBuf[0] == CONFIG_SPI_APP_OTA_BOOT_CMD) {
/* ESP32 发的是 0xB0 Bootloader 请求,不是按铃响应。
* 取消按铃,清空 TX 让 processCommand() 重新装载 0xB5 应答。
* 不释放 SPIready,processCommand() 末尾会释放。 */
m_ringPending = false;
m_ringTxLen = 0U;
m_txLen = 0U;
m_txPos = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
/* 继续走 shouldIgnoreTransaction + processCommand(),不 return */
} else if (containsSyncSof(m_rxBuf, m_rxLen)) {
/* ESP32 发的是协议帧(如 PING/OTA 等),不是按铃响应。
/* 判断本次收到的数据是否为 v2 协议帧(扫描 SOF0 SOF1)。
* 取代旧 containsSyncSof,直接用 V2_SOF0/V2_SOF1。
* 0xB0 进 BOOT 现已走标准 v2 帧 handler,不再单独检测裸字节。 */
bool is_v2_frame = false;
if (m_rxLen >= 2U) {
const uint16_t last = (m_rxLen < 16U) ? (uint16_t)(m_rxLen - 1U) : 16U;
for (uint16_t i = 0U; i < last; ++i) {
if (m_rxBuf[i] == V2_SOF0 && m_rxBuf[i + 1U] == V2_SOF1) {
is_v2_frame = true;
break;
}
}
}
if (is_v2_frame) {
/* ESP32 发的是 v2 协议帧(如 PING/OTA/0xB0 等),不是按铃响应。
* 取消按铃,清空 TX 让 processCommand() 重新装载对应应答。
* 不释放 SPIready,processCommand() 末尾会释放。 */
* processCommand() 末尾由 updateDataReady() 依据新 m_txLen 驱动 PB6。 */
m_ringPending = false;
m_ringTxLen = 0U;
m_txLen = 0U;
@@ -772,8 +804,10 @@ void AppSpiCommand::onCsRelease()
m_txLen = 0U;
m_txPos = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
/* 释放 SPIready,通知 ESP32 本帧完成 */
setSpiReady(true);
/* v2 DATA_READY: 已清 m_txLen=0 与 m_ringPending=false,
* updateDataReady() 释放为高;ESP32 完成 CS 即视为已读走确认。 */
m_linkState.onEsp32Complete();
updateDataReady();
/* 上层若需要 ESP32 处理该请求帧,由 ESP32 侧读到的帧 TYPE 决定后续动作 */
return;
}
@@ -787,9 +821,9 @@ void AppSpiCommand::onCsRelease()
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
}
m_txActiveThisCs = false;
/* 忽略事务(空轮询/残包)后恢复 PB6=HIGH,
* 避免 PB6 卡 LOW 导致 ESP32 等待上升沿超时 */
setSpiReady(true);
/* v2 DATA_READY: shouldIgnoreTransaction 路径已清 m_txLen=0,
* updateDataReady() 释放为高,避免 PB6 卡 LOW。 */
updateDataReady();
return;
}