Files
DCU_APP/GD32E508_DCU_APP/device/spi/app_spi_command.cpp
T
cbb 3691cf2aba refactor(spi): 优化CAN反馈推送的节流机制
将CAN反馈推送改为1Hz节流,由taskHeartbeat每秒调用
canFeedbackStartRound激活新轮次,避免与CANFD刷新频率不匹配。

添加s_roundActive标志控制推送节奏,确保每轮完成后等待下一秒
再开始新的推送循环。

---

feat(spi): 实现SPI就绪状态检测和链路状态机

新增SpiLinkState类管理SPI链路状态,通过EXTI6检测PB6引脚
的上升沿来判断ESP32是否完成上一帧处理。

配置SPI Ready引脚的外部中断,实现可靠的ESP32完成信号检测,
避免在ESP32未就绪时过早发起新的SPI通信。

修复SPI接收溢出处理,改为丢弃溢出字节而非重置整个接收缓冲区,
提高SPI通信稳定性。
2026-07-09 02:47:22 +08:00

863 lines
30 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 业务时
* 只需要新增命令处理器,不再继续膨胀本文件。
*
* 握手架构(线与 SPIready,PB6<->ESP32 GPIO10 开漏单线):
* - 命令处理在 onCsRelease() EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame),
* 不再推迟到主循环,消除 PB6 拉高延迟。
* - GD32 可通过 requestEsp32Read() 下拉 SPIready 主动按铃请求 ESP32 来读。
*/
#include "device/spi/app_spi_command.h"
#include "device/spi/spi_command_handler.h"
#include "device/spi/app_spi_command_defs.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/ota_boot_request.h"
#include "sys/systick.h"
#include "main.h"
#include <cstdio>
#include <cstring>
/* ============================================================
* 本地别名:保持当前实现文件内部风格不变
* ============================================================ */
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;
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;
static constexpr uint16_t kFrameOverhead = SpiDefs::kFrameOverhead;
/* DMA 配置(与 BOOT SpiOta 一致:DMA0_CH2 用于 SPI0 TX) */
#define SPI_TX_DMA_PERIPH DMA0
#define SPI_TX_DMA_CH DMA_CH2
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;
}
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;
}
/* APP 侧只接受两类事务:
* 1) 旧式单字节 0xB0
* 2) 完整协议帧(至少 5 字节)
* 短于 5 字节且不是 0xB0 的事务只能是启动切换期的残包,直接忽略。 */
return (len < kFrameOverhead) &&
!(len == 1U && data[0] == CONFIG_SPI_APP_OTA_BOOT_CMD);
}
static bool isFlashRelatedFrame(uint8_t frame_type)
{
switch (frame_type) {
case kTypeOtaReq:
case kTypeOtaData:
case kTypeOtaCancel:
case kTypeBootReq:
return true;
default:
return false;
}
}
/* ============================================================
* 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);
}
static uint16_t getU16Le(const uint8_t *pp)
{
return (uint16_t)(pp[0] | ((uint16_t)pp[1] << 8));
}
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);
}
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 旧常量仍保留供旧路径使用。 */
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; }
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;
return true;
}
/* 解析:新同步帧优先;失败回退旧 0xA5 帧(含首字节偏移容错)。 */
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)
{
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; }
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;
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 复用为 SPI 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);
}
}
void AppSpiCommand::configureSpi0()
{
rcu_periph_clock_enable(RCU_SPI0);
spi_i2s_deinit(SPI0);
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);
spi_nss_internal_low(SPI0);
(void)spi_i2s_data_receive(SPI0);
spi_i2s_data_transmit(SPI0, 0xFFU);
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);
spi_enable(SPI0);
}
void AppSpiCommand::configureDma()
{
rcu_periph_clock_enable(RCU_DMA0);
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);
}
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, CONFIG_IRQ_PREEMPT_SPI_USART, 1U);
}
void AppSpiCommand::configureSpiReadyExti()
{
/* PB6 已在 configureGpio() 配置为开漏输出,此处启用 EXTI6 上升沿检测。
* EXTI 检测引脚实际电平(IDR),与 GPIO 输出模式兼容。
* 用于检测 ESP32 releaseReady 产生的上升沿(线从忙碌→空闲),
* 作为 ESP32 完成上一帧处理的可靠信号。 */
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);
exti_interrupt_flag_clear(EXTI_6);
nvic_irq_enable(EXTI5_9_IRQn, CONFIG_IRQ_PREEMPT_SPI_USART, 1U);
}
void AppSpiCommand::onSpiReadyExti()
{
if (exti_interrupt_flag_get(EXTI_6) != RESET) {
exti_interrupt_flag_clear(EXTI_6);
/* 线升高(空闲):ESP32 已 releaseReady,上一帧处理完 */
m_linkState.onEsp32Complete();
}
}
/* ============================================================
* 默认命令注册入口(由各 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();
configureSpi0();
configureDma();
configureExti();
configureSpiReadyExti();
}
/* ============================================================
* 命令路由
* ============================================================ */
void AppSpiCommand::registerHandler(ISpiCommandHandler *handler)
{
if (!handler || m_commandCount >= kMaxCommands) {
return;
}
m_commands[m_commandCount++] = handler;
}
void AppSpiCommand::processCommand()
{
if (m_rxLen == 0U) {
return;
}
/* 尝试按协议帧解析 */
uint8_t frame_type = 0U;
const uint8_t *payload = nullptr;
uint16_t payload_len = 0U;
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 分发给已注册命令处理器 */
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;
} 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) {
/* 置位 TASK_SPI_LOG,唤醒主循环立刻打印日志 */
TaskManager::set(1UL << TASK_SPI_LOG);
}
/* 应答帧已就绪:拉高 PB6(释放 SPIready)通知主机可发起下一笔事务。
* 命令处理已在 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;
}
setSpiReady(true);
}
/* ============================================================
* GD32 主动按铃:下拉 SPIready 请求 ESP32 来读
* ============================================================ */
void AppSpiCommand::cancelRing()
{
m_ringPending = false;
m_ringTxLen = 0U;
/* 清除待发响应,防止超时后残留帧在下一次 CS assert 时被错误输出 */
m_txLen = 0U;
m_txPos = 0U;
/* 超时后允许重试,不依赖 ESP32 releaseReady 上升沿 */
m_linkState.onRingTimeout();
/* 释放 SPIready(高阻),避免永久占用线 */
setSpiReady(true);
}
bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len)
{
/* 若上一次按铃尚未被 ESP32 读走,拒绝重复按铃 */
if (m_ringPending) {
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)) {
return false;
}
m_ringTxLen = built_len;
m_txLen = built_len;
m_txPos = 0U;
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
m_ringPending = true;
/* 消费完成 flag,等 ESP32 releaseReady 触发 EXTI6 上升沿 */
m_linkState.onRingStart();
/* 下拉 SPIready,通知 ESP32「我有请求帧要给你读」 */
setSpiReady(false);
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 下降沿立即拉低 PB6:表示从机即将忙碌,
* 防止主机在 onCsRelease 准备好响应前抢跑发起下一笔 CS。
* (若此帧被 shouldIgnoreTransaction 忽略,onCsRelease 会恢复 HIGH) */
setSpiReady(false);
m_rxLen = 0U;
m_txPos = 0U;
m_txActiveThisCs = false;
/* 停止 DMA 并清理旧状态 */
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN;
spi_dma_disable(SPI0, SPI_DMA_TRANSMIT);
/* 与 BOOT 一致:清除 SPI 双缓冲中的残留数据 */
spi_i2s_data_transmit(SPI0, 0xFFU);
if (spi_i2s_flag_get(SPI0, SPI_FLAG_TBE) != RESET) {
spi_i2s_data_transmit(SPI0, 0xFFU);
}
/* 清除错误标志 */
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);
}
/* 启动 DMA 发送响应帧(与 BOOT 完全一致) */
if (m_txLen > 0U) {
m_txActiveThisCs = true;
spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);
startSpiTxDma(m_txBuf, m_txLen);
} else {
/* 无响应时发送全 0xFF 填充 */
spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);
startSpiTxDma(m_txBuf, kTxBufSize);
}
}
void AppSpiCommand::onCsRelease()
{
if (!m_csActive) {
return;
}
m_csActive = false;
/* 停止 DMA */
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN;
spi_dma_disable(SPI0, SPI_DMA_TRANSMIT);
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) {
uint8_t last_byte = (uint8_t)(spi_i2s_data_receive(SPI0) & 0xFFU);
if (m_rxLen < sizeof(m_rxBuf)) {
m_rxBuf[m_rxLen++] = last_byte;
}
}
/* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧,
* 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。
*
* 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 0xB0 命令(OTA 请求)。
* 若 GD32 正在按铃时 ESP32 发 0xB0,会因 m_ringPending=true 被误吞,
* 导致 OTA 无法启动。检测到 0xB0 时取消按铃并走正常命令处理。 */
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 等),不是按铃响应。
* 取消按铃,清空 TX 让 processCommand() 重新装载对应应答。
* 不释放 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 {
/* 正常按铃响应:请求帧已被读走,取消按铃状态 */
m_ringPending = false;
m_ringTxLen = 0U;
/* 请求帧已被读走,清空 TX 防止下次事务重复输出 */
m_txLen = 0U;
m_txPos = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
/* 释放 SPIready,通知 ESP32 本帧完成 */
setSpiReady(true);
/* 上层若需要 ESP32 处理该请求帧,由 ESP32 侧读到的帧 TYPE 决定后续动作 */
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;
/* 忽略事务(空轮询/残包)后恢复 PB6=HIGH,
* 避免 PB6 卡 LOW 导致 ESP32 等待上升沿超时 */
setSpiReady(true);
return;
}
m_txLen = 0U;
m_txPos = 0U;
m_txActiveThisCs = false;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
/* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式),
* 消除主循环延迟。处理完成后 processCommand() 末尾释放 SPIready。 */
processCommand();
}
/* ============================================================
* SPI0 / EXTI 中断处理
* ============================================================ */
void AppSpiCommand::onInterrupt()
{
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) {
if (!m_csActive && gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) {
onCsAssert();
}
uint8_t byte = (uint8_t)(spi_i2s_data_receive(SPI0) & 0xFFU);
if (m_csActive && m_rxLen < sizeof(m_rxBuf)) {
m_rxBuf[m_rxLen++] = byte;
}
}
/* TBE 已由 DMA 替代,不再处理 */
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
/* 读取并丢弃 overrun 字节以清除标志,但保留已收到的帧数据。
* 丢失的字节会导致 CRC 校验失败,由 spi_try_parse_frame 优雅处理
* (返回 false -> processCommand 构建 ERROR 响应),
* 而不是重置 m_rxLen 导致整帧丢失。 */
(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);
}
}
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();
}
extern "C" void EXTI5_9_IRQHandler(void)
{
AppSpiCommandBus.onSpiReadyExti();
}