将CAN反馈推送改为1Hz节流,由taskHeartbeat每秒调用 canFeedbackStartRound激活新轮次,避免与CANFD刷新频率不匹配。 添加s_roundActive标志控制推送节奏,确保每轮完成后等待下一秒 再开始新的推送循环。 --- feat(spi): 实现SPI就绪状态检测和链路状态机 新增SpiLinkState类管理SPI链路状态,通过EXTI6检测PB6引脚 的上升沿来判断ESP32是否完成上一帧处理。 配置SPI Ready引脚的外部中断,实现可靠的ESP32完成信号检测, 避免在ESP32未就绪时过早发起新的SPI通信。 修复SPI接收溢出处理,改为丢弃溢出字节而非重置整个接收缓冲区, 提高SPI通信稳定性。
863 lines
30 KiB
C++
863 lines
30 KiB
C++
/**
|
||
* @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();
|
||
}
|