Files
DCU_APP/GD32E508_DCU_APP/device/spi/app_spi_command.cpp
T
cbb a52d94687b refactor(SPI): 实现全双工DMA传输以提升通信可靠性
- 配置DMA0_CH1用于SPI0_RX接收,替代原有的RBNE中断逐字节处理
- 添加startSpiRxDma、stopSpiDmas、drainSpiRxHw等DMA控制函数
- 在CS空闲期预装下一笔DMA传输,避免EXTI CS下降沿相对于SCK偏晚导致收0字节
- 与BOOT工程SpiOta的DMA配置对齐,确保通信稳定性

refactor(SPI): 优化CS边沿处理逻辑和命令解析时机

- 移除原有的EXTI5_9_IRQHandler用于SPI Ready引脚处理
- 将命令处理从异步任务调度改为在CS释放中断中同步完成
- 重构onCsAssert和onCsRelease逻辑,简化DMA启停流程
- 删除TASK_SPI_CMD_PROCESS任务类型,减少不必要的任务调度

feat(CANFD): 修改transmitFrame返回类型为枚举值以提供详细错误信息

- 将CanFd::transmitFrame返回类型从bool改为CanTxResult枚举
- 新增CanTxResult枚举定义,包含kOk、kErrBusOff、kErrTecHigh、kErrMailbox、kErrInvalid
- 更新SPI桥接命令处理中对transmitFrame返回值的处理逻辑
- 枚举值设计与SPI错误响应码对齐,便于直接透传错误信息

refactor(SPI): 实现全双工DMA传输以提升通信可靠性

- 配置DMA0_CH1用于SPI0_RX接收,替代原有的RBNE中断逐字节处理
- 添加startSpiRxDma、stopSpiDmas、drainSpiRxHw等DMA控制函数
- 在CS空闲期预装下一笔DMA传输,避免EXTI CS下降沿相对于SCK偏晚导致收0字节
- 与BOOT工程SpiOta的DMA配置对齐,确保通信稳定性

refactor(SPI): 优化CS边沿处理逻辑和命令解析时机

- 移除原有的EXTI5_9_IRQHandler用于SPI Ready引脚处理
- 将命令处理从异步任务调度改为在CS释放中断中同步完成
- 重构onCsAssert和onCsRelease逻辑,简化DMA启停流程
- 删除TASK_SPI_CMD_PROCESS任务类型,减少不必要的任务调度

feat(OTA): 添加BOOT版本号读取功能并优化版本信息获取

- 在ota_boot_request模块中添加readBootVersion函数
- 从OTA配置页Flash读取BOOT版本号信息
- 在SPI PING命令响应中使用新的版本读取接口
- 统一版本号获取方式,避免裸指针访问Flash地址

refactor(task): 调整诊断打印任务调度周期为5秒间隔

- 新增div_diag_print调度分频计数器用于诊断打印
- 将诊断打印任务从心跳任务中分离,独立控制执行周期
- 修改诊断打印函数注释,明确其由5秒节拍触发
- 优化任务调度逻辑,提高系统运行效率
2026-07-15 00:34:56 +08:00

956 lines
35 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file app_spi_command.cpp
* @brief APP 运行期 SPI 链路与命令路由实现。
*
* 本模块现在只负责:
* - SPI0 从机链路初始化(GPIO/SPI/DMA/EXTI)
* - CS、DMA、中断驱动的帧接收与发送
* - 公共帧格式解析与应答构建
* - 业务命令的注册与分发
*
* 具体命令处理逻辑已经拆到独立的 app_spi_cmd_*.cpp,
* 通过 registerHandler() 注册进来,后续新增 SPI 业务时
* 只需要新增命令处理器,不再继续膨胀本文件。
*
* 握手架构(v2 DATA_READY,PB6<->ESP32 GPIO10 开漏单线):
* - PB6 低 = 有待发数据(m_txLen>0 或 m_ringPending),高 = 队列空。
* - 命令处理在 onCsRelease() EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame),
* 完成后由 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"
}
#include "gd32e50x_gpio.h"
#include "gd32e50x_misc.h"
#include "gd32e50x_rcu.h"
#include "gd32e50x_dma.h"
#include "gd32e50x_spi.h"
#include "sys/config.h"
#include "sys/critical.h"
#include "sys/task_manager.h"
#include "sys/systick.h"
#include "main.h"
#include <cstdio>
#include <cstring>
/* ============================================================
* 本地别名:保持当前实现文件内部风格不变
* ============================================================ */
static constexpr uint8_t kTypeBootReq = SpiDefs::kTypeBootReq;
static constexpr uint8_t kTypeCanSend = SpiDefs::kTypeCanSend;
static constexpr uint8_t kTypeOtaReq = SpiDefs::kTypeOtaReq;
static constexpr uint8_t kTypeAppPing = SpiDefs::kTypeAppPing;
static constexpr uint8_t kTypeOtaData = SpiDefs::kTypeOtaData;
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;
/* DMA 全双工配置(与 BOOT SpiOta 一致):
* DMA0_CH1: SPI0_RX DMA0_CH2: SPI0_TX
* RX 走 DMA 后禁用 RBNE 逐字节中断,避免 256B 事务 RX-OVR/SOF 丢失。 */
#define SPI_TX_DMA_PERIPH DMA0
#define SPI_TX_DMA_CH DMA_CH2
#define SPI_RX_DMA_PERIPH DMA0
#define SPI_RX_DMA_CH DMA_CH1
static void startSpiTxDma(const uint8_t *buf, uint16_t len)
{
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN;
DMA_INTC(SPI_TX_DMA_PERIPH) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE, SPI_TX_DMA_CH);
DMA_CHMADDR(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = (uint32_t)buf;
DMA_CHCNT(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = len;
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) |= DMA_CHXCTL_CHEN;
}
/** @brief 启动 DMA 接收 SPI0_DATA → m_rxBuf(容量 kRxBufSize)。
* 每次 CS 空闲期预装,下一笔 CS 上升沿时由 stopSpiDmas() 读 CHCNT 反算实际长度。 */
static void startSpiRxDma(uint8_t *rx_buf, uint16_t rx_cap)
{
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)rx_buf;
DMA_CHCNT(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) = rx_cap;
DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) |= DMA_CHXCTL_CHEN;
}
/** @brief 清空 SPI 接收通路残留(含 OVR),对齐 BOOT drainSpiRxHw。 */
static void drainSpiRxHw(void)
{
while (spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE) != RESET) {
(void)spi_i2s_data_receive(SPI0);
}
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
(void)spi_i2s_data_receive(SPI0);
(void)SPI_STAT(SPI0);
}
}
/** @brief 停止双 DMA;返回本 CS 实际 RX 字节数(必须在 disable 前读 CHCNT)。
* 对齐 BOOT stopSpiDmas:先读 remaining 再 disable,否则计数器被清。 */
static uint16_t stopSpiDmas(uint16_t rx_cap)
{
const uint16_t remaining = (uint16_t)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);
uint16_t got = 0U;
if (remaining <= rx_cap) {
got = (uint16_t)(rx_cap - remaining);
}
return got;
}
static bool isAllBytesValue(const uint8_t *data, uint16_t len, uint8_t value)
{
if (!data || len == 0U) {
return false;
}
for (uint16_t i = 0U; i < len; ++i) {
if (data[i] != value) {
return false;
}
}
return true;
}
static bool shouldIgnoreTransaction(const uint8_t *data, uint16_t len)
{
if (!data || len == 0U) {
return true;
}
if (isAllBytesValue(data, len, 0xFFU) || isAllBytesValue(data, len, 0x00U)) {
return true;
}
/* v2 协议帧最小长度为 V2_OVERHEAD(20B)。
* 短于该长度的事务只能是启动切换期的残包,直接忽略。
* (0xB0 进 BOOT 现已走标准 v2 帧 handler,不再有裸字节通道。) */
return (len < V2_OVERHEAD);
}
static bool isFlashRelatedFrame(uint8_t frame_type)
{
switch (frame_type) {
case kTypeOtaReq:
case kTypeOtaData:
case kTypeOtaCancel:
case kTypeBootReq:
return true;
default:
return false;
}
}
/* GD32 APP 侧同步状态机(ISR 与主循环只读/原子写,避免临界区开销)。 */
static volatile uint8_t g_localCredit = 8U;
static volatile uint8_t g_remoteCredit = 8U;
/* 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;
/* 响应缓存:用于重复请求重发同一响应(避免业务 handler 重复执行)。 */
static uint8_t s_lastReqSeq = 0xFF;
static uint8_t s_lastRespBuf[256];
static uint16_t s_lastRespLen = 0;
static bool s_hasCachedResp = false;
/* ============================================================
* v2 帧构建 / 帧解析(委托 spi_link_v2.h,三工程共用)
* ============================================================ */
/* 构建响应帧;上层接口(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; }
if ((uint32_t)V2_OVERHEAD + payload_len > AppSpiCommand::kTxBufSize) { return false; }
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;
}
/* 解析 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,
v2_header_t *out_hdr = nullptr)
{
if (!frame || !type || !payload || !payload_len) { return false; }
v2_header_t hdr;
const uint8_t *pload = nullptr;
const bool ok = v2_parse_frame(frame, frame_len, &hdr, &pload);
if (!ok) { return false; }
g_remoteCredit = hdr.credit;
*type = hdr.type;
*payload = pload;
*payload_len = hdr.payload_len;
if (out_hdr) { *out_hdr = hdr; }
return true;
}
/* ============================================================
* 调试日志辅助
* ============================================================ */
void AppSpiCommand::postLog(const char *msg)
{
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
if (!msg) return;
snprintf(m_logBuf, kLogBufSize, "[SPI-APP] %s", msg);
m_logPending = true;
#else
(void)msg;
#endif
}
void AppSpiCommand::postLogFmt(const char *fmt, uint8_t val)
{
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
if (!fmt) return;
snprintf(m_logBuf, kLogBufSize, fmt, val);
m_logPending = true;
#else
(void)fmt;
(void)val;
#endif
}
void AppSpiCommand::buildResponse(uint8_t *tx_buf, uint16_t tx_buf_size,
uint16_t *tx_len, uint8_t rsp_type,
const uint8_t *payload, uint16_t payload_len)
{
if (!tx_buf || !tx_len || tx_buf_size == 0U) {
return;
}
uint16_t built_len = 0U;
const bool ok = spi_build_frame(rsp_type, payload, payload_len, tx_buf, &built_len);
if (ok) {
*tx_len = built_len;
}
}
/* ============================================================
* 硬件配置
* ============================================================ */
void AppSpiCommand::configureGpio()
{
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_GPIOB);
rcu_periph_clock_enable(RCU_AF);
gpio_pin_remap_config(GPIO_SPI0_REMAP, ENABLE);
gpio_pin_remap_config(GPIO_SWJ_SWDPENABLE_REMAP, ENABLE);
gpio_init(GPIOB, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_3 | GPIO_PIN_5);
gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4);
gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15);
/* PB6 复用为 DATA_READY 握手线(开漏输出,线与)。
* 开漏模式:写 BRR = 下拉驱动低;写 BSRR = 高阻释放,靠外部/对端上拉拉高。
* 语义:低 = 有待发数据,高 = 队列空;默认释放(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);
}
void AppSpiCommand::setSpiReady(bool ready)
{
/* 开漏线与:ready=true 释放(BSRR,高阻,靠上拉拉高);
* ready=false 下拉(BRR,驱动低)。 */
if (ready) {
gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN);
} else {
gpio_bit_reset(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN);
}
}
/* v2 DATA_READY: PB6 低 = 有待发数据,高 = 队列空。
* L5:同步 g_localCredit(单缓冲 0/1)。 */
void AppSpiCommand::updateDataReady()
{
const bool has_data = (m_txLen > 0U) || m_ringPending;
setSpiReady(!has_data);
g_localCredit = has_data ? 0U : 1U;
}
void AppSpiCommand::configureSpi0()
{
rcu_periph_clock_enable(RCU_SPI0);
spi_i2s_deinit(SPI0);
(void)spi_i2s_data_receive(SPI0); /* 清空 SPI_DATA 残留值 */
spi_parameter_struct spi_init_struct;
spi_struct_para_init(&spi_init_struct);
spi_init_struct.device_mode = SPI_SLAVE;
spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX;
spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT;
spi_init_struct.nss = SPI_NSS_SOFT;
spi_init_struct.endian = SPI_ENDIAN_MSB;
spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;
spi_init_struct.prescale = SPI_PSC_8;
spi_init(SPI0, &spi_init_struct);
/* 内部 NSS 常低:SPI 从机始终就绪,不依赖 EXTI ISR 切换 internal NSS。
* PA15 仅作 EXTI 事务边界信号,用于 onCsAssert/onCsRelease 控制 DMA 装载。 */
spi_nss_internal_low(SPI0);
(void)spi_i2s_data_receive(SPI0);
spi_i2s_data_transmit(SPI0, 0xFFU);
/* RX 改走 DMA0_CH1:禁用 RBNE 逐字节中断,避免 256B 事务 RX-OVR/SOF 丢失。 */
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);
nvic_irq_enable(SPI0_IRQn, 2U, 0U);
spi_enable(SPI0);
}
void AppSpiCommand::configureDma()
{
rcu_periph_clock_enable(RCU_DMA0);
/* TX: memory → SPI0_DATA */
dma_deinit(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH);
dma_parameter_struct dma_cfg;
dma_struct_para_init(&dma_cfg);
dma_cfg.periph_addr = (uint32_t)(SPI0 + 0x0CU); /* SPI0_DATA 寄存器 */
dma_cfg.periph_width = DMA_PERIPHERAL_WIDTH_8BIT;
dma_cfg.memory_addr = (uint32_t)m_txBuf;
dma_cfg.memory_width = DMA_MEMORY_WIDTH_8BIT;
dma_cfg.number = 0U;
dma_cfg.priority = DMA_PRIORITY_HIGH;
dma_cfg.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_cfg.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
dma_cfg.direction = DMA_MEMORY_TO_PERIPHERAL;
dma_init(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH, &dma_cfg);
dma_circulation_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH);
dma_channel_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH);
/* RX: SPI0_DATA → m_rxBuf(替代旧 RBNE 逐字节累积)。
* 与 BOOT SpiOta 对齐:DMA0_CH1 固定映射 SPI0_RX。 */
dma_deinit(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH);
dma_struct_para_init(&dma_cfg);
dma_cfg.periph_addr = (uint32_t)(SPI0 + 0x0CU);
dma_cfg.periph_width = DMA_PERIPHERAL_WIDTH_8BIT;
dma_cfg.memory_addr = (uint32_t)m_rxBuf;
dma_cfg.memory_width = DMA_MEMORY_WIDTH_8BIT;
dma_cfg.number = kRxBufSize;
dma_cfg.priority = DMA_PRIORITY_HIGH;
dma_cfg.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_cfg.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
dma_cfg.direction = DMA_PERIPHERAL_TO_MEMORY;
dma_init(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH, &dma_cfg);
dma_circulation_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH);
dma_channel_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH);
}
void AppSpiCommand::configureExti()
{
gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15);
exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH);
exti_interrupt_enable(EXTI_15);
exti_interrupt_flag_clear(EXTI_15);
nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U);
}
/* ============================================================
* 默认命令注册入口(由各 app_spi_cmd_*.cpp 提供)
* ============================================================ */
extern void registerSpiBootRequestCommand(AppSpiCommand &bus);
extern void registerSpiOtaWaitCommand(AppSpiCommand &bus);
extern void registerSpiAppPingCommand(AppSpiCommand &bus);
extern void registerSpiCanBridgeCommand(AppSpiCommand &bus);
extern void registerSpiCanFeedbackCommand(AppSpiCommand &bus);
extern void registerSpiIdSnapshotCommand(AppSpiCommand &bus);
AppSpiCommand AppSpiCommandBus;
void AppSpiCommand::init()
{
m_csActive = false;
m_rxLen = 0U;
m_txLen = 0U;
m_txPos = 0U;
m_logPending = false;
m_txActiveThisCs = false;
m_commandCount = 0U;
m_ringPending = false;
m_ringDeadline = 0U;
m_ringTxLen = 0U;
m_linkState.reset();
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
memset(m_logBuf, 0, sizeof(m_logBuf));
memset(m_commands, 0, sizeof(m_commands));
registerSpiBootRequestCommand(AppSpiCommandBus);
registerSpiOtaWaitCommand(AppSpiCommandBus);
registerSpiAppPingCommand(AppSpiCommandBus);
registerSpiCanBridgeCommand(AppSpiCommandBus);
registerSpiCanFeedbackCommand(AppSpiCommandBus);
registerSpiIdSnapshotCommand(AppSpiCommandBus);
configureGpio();
configureDma();
configureSpi0();
configureExti();
/* 首笔 CS 到来前预装 DMA,避免 EXTI 晚于首 SCK。
* 必须在 configureSpi0/configureDma 之后调用。 */
armSpiDmasForNextCs();
}
/** @brief 在 CS 空闲期预装下一笔全双工 DMA。
*
* 必须在首个 SCK 之前完成:EXTI CS 下降相对 ESP32 时钟经常偏晚,
* 若在 onCsAssert 里才 start DMA,整笔事务会收 0 字节 → 永不 HELLO_ACK → MISO 全 0xFF。
*
* TX 始终按 kTxBufSize 装载:响应帧在头部,其余 0xFF。
* 若只 DMA 实际帧长(~26B),主机打 96 时钟时 TX underrun,后续 MISO 不定。
*
* 对齐 BOOT SpiOta::armSpiDmasForNextCs。
*/
void AppSpiCommand::armSpiDmasForNextCs()
{
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 (m_txLen > 0U && m_txLen <= kTxBufSize) {
if (m_txLen < kTxBufSize) {
memset(&m_txBuf[m_txLen], 0xFF, (size_t)(kTxBufSize - m_txLen));
}
} else {
m_txLen = 0U;
memset(m_txBuf, 0xFF, kTxBufSize);
}
/* RX 缓冲清零(便于诊断时区分有效字节与残留) */
memset(m_rxBuf, 0x00, kRxBufSize);
m_rxLen = 0U;
/* 预装 SPI_DATA,避免 DMA 启动前首拍 TBE 吐旧值 */
spi_i2s_data_transmit(SPI0, m_txBuf[0]);
/* 先使能 SPI DMA 请求,再开 DMA 通道,保证首字节即可搬运 */
spi_dma_enable(SPI0, SPI_DMA_RECEIVE);
spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);
startSpiRxDma(m_rxBuf, kRxBufSize);
/* TX 永远满缓冲长度,与主机 96~256B 事务匹配,避免 underrun */
startSpiTxDma(m_txBuf, kTxBufSize);
}
/* ============================================================
* 命令路由
* ============================================================ */
void AppSpiCommand::registerHandler(ISpiCommandHandler *handler)
{
if (!handler || m_commandCount >= kMaxCommands) {
return;
}
m_commands[m_commandCount++] = handler;
}
void AppSpiCommand::processCommand()
{
if (m_rxLen == 0U) {
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, &hdr)) {
/* 无法解析为合法 v2 帧:构建 ERROR 响应 */
postLogFmt("[SPI-APP] 非v2帧 0x%02X -> ERROR", m_rxBuf[0]);
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) {
/* 置位 TASK_SPI_LOG,唤醒主循环立刻打印日志 */
TaskManager::set(1UL << TASK_SPI_LOG);
}
/* 应答帧已就绪:由 updateDataReady() 依据 m_txLen 驱动 PB6(有待发=低)。
* 命令处理已在 ISR 内同步完成,无需 m_cmdPending 推迟机制。 */
/* 有响应待发时,设置按铃标志阻止主循环 canFeedbackPushStep() 覆盖 m_txBuf。
* ESP32 下次 CS assert 时 DMA 输出响应,onCsRelease 清除 m_ringPending。
* 50ms 超时后 cancelRing() 清除,恢复 CAN 反馈推送。 */
if (m_txLen > 0U) {
m_ringPending = true;
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
}
updateDataReady();
}
/* ============================================================
* GD32 主动按铃:设 m_ringPending + updateDataReady() 拉低 PB6 请求 ESP32 来读
* ============================================================ */
void AppSpiCommand::cancelRing()
{
m_ringPending = false;
m_ringTxLen = 0U;
/* 清除待发响应,防止超时后残留帧在下一次 CS assert 时被错误输出 */
m_txLen = 0U;
m_txPos = 0U;
/* 超时后允许重试,不依赖 ESP32 releaseReady 上升沿 */
m_linkState.onRingTimeout();
/* 释放 SPIready(高阻),避免永久占用线。
* cancelRing 已清 m_txLen 和 m_ringPending,updateDataReady() 会释放为高。 */
updateDataReady();
}
bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len)
{
/* 若上一次按铃尚未被 ESP32 读走,拒绝重复按铃 */
if (m_ringPending) {
return false;
}
/* 构建 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;
/* L5:单缓冲 — 装载前 credit 报 1,装载后 updateDataReady 置 0 */
g_localCredit = 1U;
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 = (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();
/* v2 DATA_READY: 已设 m_ringPending=true 且 m_txLen>0,
* updateDataReady() 据此拉低 PB6,并置 g_localCredit=0。 */
updateDataReady();
return true;
}
/* ============================================================
* 主循环轮询:打印延迟日志 + 按铃超时看护
* ============================================================ */
void AppSpiCommand::poll()
{
/* 打印 ISR 推迟的日志 */
char log_copy[kLogBufSize] = {};
bool has_log = false;
uint32_t save = RT_CRITICAL_SAVE();
if (m_logPending) {
memcpy(log_copy, m_logBuf, sizeof(log_copy));
m_logPending = false;
has_log = true;
}
RT_CRITICAL_RESTORE(save);
if (has_log) {
printf("%s\r\n", log_copy);
}
/* 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。
* 防止 ESP32 不响应时永久占用 SPIready 线导致双方死锁。 */
if (m_ringPending) {
uint32_t save2 = RT_CRITICAL_SAVE();
if (m_ringPending) {
if ((int32_t)(SysTickTimer::getMs() - m_ringDeadline) >= 0) {
printf("[SPI-APP] 按铃超时取消 (ESP32 未响应, 释放 SPIready 重试)\r\n");
cancelRing();
}
}
RT_CRITICAL_RESTORE(save2);
}
}
/* ============================================================
* CS 边沿处理
* ============================================================ */
void AppSpiCommand::onCsAssert()
{
if (m_csActive) {
return;
}
m_csActive = true;
/* CS 下降:DMA 已在空闲期由 armSpiDmasForNextCs() 预装,此处只标记忙碌。
* v2 DATA_READY: CS 本身即互斥,PB6 不再表示「忙碌」,不在 CS 下降沿拉低。
* 切勿在这里 stop/restart DMA,否则赶不上 ESP32 已发出的 SCK(对齐 BOOT)。 */
m_txPos = 0U;
m_txActiveThisCs = false;
}
void AppSpiCommand::onCsRelease()
{
if (!m_csActive) {
/* EXTI 丢失下降沿时:仍尝试收尾并 re-arm,避免永久失步(对齐 BOOT) */
m_rxLen = stopSpiDmas(kRxBufSize);
drainSpiRxHw();
if (m_txLen > 0U) {
m_txLen = 0U;
m_txPos = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
}
if (m_rxLen >= V2_OVERHEAD) {
processCommand();
}
armSpiDmasForNextCs();
updateDataReady();
return;
}
m_csActive = false;
/* 停 DMA 并取本笔 RX 长度(先读 CHCNT 再 disable,否则计数器被清) */
m_rxLen = stopSpiDmas(kRxBufSize);
drainSpiRxHw();
/* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧,
* 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。
*
* 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 v2 协议帧(如 PING/OTA/0xB0)。
* 若 GD32 正在按铃时 ESP32 发协议帧,会因 m_ringPending=true 被误吞,
* 导致命令无法处理。检测到 v2 SOF 时取消按铃并走正常命令处理。 */
if (m_ringPending) {
/* 判断本次收到的数据是否为 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() 重新装载对应应答。
* processCommand() 末尾由 updateDataReady() 依据新 m_txLen 驱动 PB6。 */
m_ringPending = false;
m_ringTxLen = 0U;
m_txLen = 0U;
m_txPos = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
/* 继续走 shouldIgnoreTransaction + processCommand(),不 return */
} else {
/* 正常按铃响应:请求帧已被读走,取消按铃状态 */
m_ringPending = false;
m_ringTxLen = 0U;
/* 请求帧已被读走,清空 TX 防止下次事务重复输出 */
m_txLen = 0U;
m_txPos = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
/* v2 DATA_READY: 已清 m_txLen=0 与 m_ringPending=false,
* updateDataReady() 释放为高;ESP32 完成 CS 即视为已读走确认。 */
m_linkState.onEsp32Complete();
/* 重新预装下一笔 DMA,保证下一 CS 首字节即可收发 */
armSpiDmasForNextCs();
updateDataReady();
return;
}
}
if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) {
m_rxLen = 0U;
if (m_txActiveThisCs) {
m_txLen = 0U;
m_txPos = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
}
m_txActiveThisCs = false;
/* v2 DATA_READY: shouldIgnoreTransaction 路径已清 m_txLen=0,
* updateDataReady() 释放为高,避免 PB6 卡 LOW。 */
armSpiDmasForNextCs();
updateDataReady();
return;
}
m_txLen = 0U;
m_txPos = 0U;
m_txActiveThisCs = false;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
/* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式),
* 消除主循环延迟。处理完成后 processCommand() 末尾再 re-arm DMA + 释放 SPIready。 */
processCommand();
/* 处理完立即预装下一笔,保证下一 CS 首字节即可收发 */
armSpiDmasForNextCs();
updateDataReady();
}
/* ============================================================
* SPI0 / EXTI 中断处理
* ============================================================ */
void AppSpiCommand::onInterrupt()
{
/* RX 已改走 DMA0_CH1:RBNE 逐字节中断已禁用,本 ISR 只保留错误标志清理。
* CS 边沿处理全部在 onCsExtiInterrupt 的 EXTI15 ISR 里完成。 */
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
/* RXORERR 理论上不应再出现(RX 走 DMA),但兜底清理避免标志卡死。
* 读 SPI_DATA + SPI_STAT 清除 OVR 标志。 */
(void)spi_i2s_data_receive(SPI0);
(void)SPI_STAT(SPI0);
}
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) {
spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR);
}
}
void AppSpiCommand::onCsExtiInterrupt()
{
if (exti_interrupt_flag_get(EXTI_15) != RESET) {
exti_interrupt_flag_clear(EXTI_15);
if (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) {
onCsAssert();
} else {
onCsRelease();
}
}
}
extern "C" void SPI0_IRQHandler(void)
{
AppSpiCommandBus.onInterrupt();
}
extern "C" void EXTI10_15_IRQHandler(void)
{
AppSpiCommandBus.onCsExtiInterrupt();
}