2026-07-16 23:00:52 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @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/systick.h"
|
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
|
#include <cstring>
|
|
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
|
* 本地别名:保持当前实现文件内部风格不变
|
|
|
|
|
|
* ============================================================ */
|
|
|
|
|
|
static constexpr uint8_t kTypeBootReq = SpiDefs::kTypeBootReq;
|
|
|
|
|
|
static constexpr uint8_t kTypeOtaReq = SpiDefs::kTypeOtaReq;
|
|
|
|
|
|
static constexpr uint8_t kTypeOtaData = SpiDefs::kTypeOtaData;
|
|
|
|
|
|
static constexpr uint8_t kTypeOtaCancel = SpiDefs::kTypeOtaCancel;
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-20 02:36:56 +08:00
|
|
|
|
/* GD32 APP 侧 CREDIT:运行时单响应缓冲 0/1(有待发=0,空闲=1)。
|
|
|
|
|
|
* 初值必须为 1,禁止用 BOOT 的 8 槽语义;否则 HELLO 前被读到会误导主机 remoteCredit。 */
|
|
|
|
|
|
static volatile uint8_t g_localCredit = 1U;
|
2026-07-16 23:00:52 +08:00
|
|
|
|
|
|
|
|
|
|
/* 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。
|
|
|
|
|
|
* 仅 v2:v2_parse_frame 扫 SOF0/SOF1 + CRC32;禁止再加 v1 CRC8 分支。 */
|
|
|
|
|
|
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; }
|
|
|
|
|
|
|
|
|
|
|
|
*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);
|
2026-09-02 02:38:35 +08:00
|
|
|
|
extern void registerSpiInitStatusCommand(AppSpiCommand &bus);
|
2026-07-16 23:00:52 +08:00
|
|
|
|
extern void registerSpiCanBridgeCommand(AppSpiCommand &bus);
|
2026-08-09 03:27:21 +08:00
|
|
|
|
extern void registerSpiCanBurstCommand(AppSpiCommand &bus);
|
2026-07-16 23:00:52 +08:00
|
|
|
|
extern void registerSpiCanFeedbackCommand(AppSpiCommand &bus);
|
|
|
|
|
|
extern void registerSpiIdSnapshotCommand(AppSpiCommand &bus);
|
2026-07-20 02:36:56 +08:00
|
|
|
|
extern void registerSpiAdcFeedbackCommand(AppSpiCommand &bus);
|
2026-07-26 20:39:59 +08:00
|
|
|
|
extern void registerSpiMotorStateCommand(AppSpiCommand &bus);
|
|
|
|
|
|
extern void registerSpiMotorOtaCommand(AppSpiCommand &bus);
|
2026-08-09 03:27:21 +08:00
|
|
|
|
extern void registerSpiExmcDiagCommand(AppSpiCommand &bus);
|
2026-07-16 23:00:52 +08:00
|
|
|
|
AppSpiCommand AppSpiCommandBus;
|
|
|
|
|
|
void AppSpiCommand::init()
|
|
|
|
|
|
{
|
|
|
|
|
|
m_csActive = false;
|
|
|
|
|
|
m_rxLen = 0U;
|
|
|
|
|
|
m_txLen = 0U;
|
|
|
|
|
|
m_logPending = false;
|
|
|
|
|
|
m_commandCount = 0U;
|
|
|
|
|
|
m_ringPending = false;
|
|
|
|
|
|
m_ringDeadline = 0U;
|
|
|
|
|
|
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);
|
2026-09-02 02:38:35 +08:00
|
|
|
|
registerSpiInitStatusCommand(AppSpiCommandBus);
|
2026-07-16 23:00:52 +08:00
|
|
|
|
registerSpiCanBridgeCommand(AppSpiCommandBus);
|
2026-08-09 03:27:21 +08:00
|
|
|
|
registerSpiCanBurstCommand(AppSpiCommandBus);
|
2026-07-16 23:00:52 +08:00
|
|
|
|
registerSpiCanFeedbackCommand(AppSpiCommandBus);
|
|
|
|
|
|
registerSpiIdSnapshotCommand(AppSpiCommandBus);
|
2026-07-20 02:36:56 +08:00
|
|
|
|
registerSpiAdcFeedbackCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */
|
2026-07-26 20:39:59 +08:00
|
|
|
|
registerSpiMotorStateCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */
|
|
|
|
|
|
registerSpiMotorOtaCommand(AppSpiCommandBus);
|
2026-08-09 03:27:21 +08:00
|
|
|
|
registerSpiExmcDiagCommand(AppSpiCommandBus);
|
2026-07-16 23:00:52 +08:00
|
|
|
|
|
|
|
|
|
|
configureGpio();
|
|
|
|
|
|
configureDma();
|
|
|
|
|
|
configureSpi0();
|
2026-07-17 02:03:00 +08:00
|
|
|
|
/* 必须先预装 DMA,再开 CS EXTI。
|
|
|
|
|
|
* OTA 跳转后 ESP32 可能立刻打 HELLO:若 EXTI 已开而 DMA 未 arm,
|
|
|
|
|
|
* 首笔 CS 会让 SPI 从机失步,之后长期 MISO 噪声/全 FF。 */
|
2026-07-16 23:00:52 +08:00
|
|
|
|
armSpiDmasForNextCs();
|
2026-07-17 02:03:00 +08:00
|
|
|
|
configureExti();
|
2026-07-16 23:00:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** @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)
|
|
|
|
|
|
{
|
2026-07-26 20:39:59 +08:00
|
|
|
|
if (!handler) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (m_commandCount >= kMaxCommands) {
|
|
|
|
|
|
/* 表满静默丢弃会导致命令无声失效(motor_ota 曾因此 0xD0 回 0xEE)。
|
|
|
|
|
|
* init 在任务上下文,printf 走 USART0 中断环,启动日志可见。 */
|
|
|
|
|
|
printf("[SPI-APP] registerHandler: 注册表满(%u),handler 被丢弃! 请增大 CONFIG_SPI_MAX_COMMANDS\r\n",
|
|
|
|
|
|
static_cast<unsigned>(kMaxCommands));
|
2026-07-16 23:00:52 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
m_commands[m_commandCount++] = handler;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void AppSpiCommand::finishResponseAndReady()
|
|
|
|
|
|
{
|
2026-08-09 03:27:21 +08:00
|
|
|
|
/* 有响应待发时 arm 按铃(ESP32 主动请求的响应即靠此通知;
|
|
|
|
|
|
* 推送源已随 2026-08 请求驱动改造移除,无其他写者争用 m_txBuf)。
|
2026-07-16 23:00:52 +08:00
|
|
|
|
* updateDataReady 按 m_txLen/m_ringPending 驱动 PB6。
|
|
|
|
|
|
* 延迟日志由 poll() 无条件消费 m_logPending,无需再置任务位。 */
|
|
|
|
|
|
if (m_txLen > 0U) {
|
|
|
|
|
|
armRingPending();
|
|
|
|
|
|
}
|
|
|
|
|
|
updateDataReady();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void AppSpiCommand::processCommand()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m_rxLen == 0U) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 0xB0 已受理、正在写 OTA 配置页 / 即将复位:不再处理业务帧,避免打断 Flash。 */
|
|
|
|
|
|
if (OtaBootRequest::isSpiFrozenForBoot() || OtaBootRequest::isRebootPending()) {
|
|
|
|
|
|
m_txLen = 0U;
|
|
|
|
|
|
updateDataReady();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)) {
|
|
|
|
|
|
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;
|
|
|
|
|
|
finishResponseAndReady();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (CONFIG_SPI_APP_LOG_ENABLE != 0 && !isFlashRelatedFrame(frame_type)) {
|
|
|
|
|
|
postLogFmt("[SPI-APP] 收到非刷写 SPI type=0x%02X", frame_type);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
g_pendingAckSeq = hdr.tx_seq;
|
|
|
|
|
|
|
|
|
|
|
|
if (frame_type == V2_TYPE_LINK_HELLO) {
|
|
|
|
|
|
g_sessionId = hdr.session_id;
|
|
|
|
|
|
g_txSeq = 0;
|
|
|
|
|
|
g_expectedRxSeq = 0;
|
|
|
|
|
|
g_v2Active = true;
|
|
|
|
|
|
s_hasCachedResp = false;
|
|
|
|
|
|
|
2026-07-17 02:03:00 +08:00
|
|
|
|
/* 与 requestEsp32Read() 一致(L5 8bc01dd):建帧前 credit=1,
|
|
|
|
|
|
* 避免 TX 占用时 g_localCredit=0 写入 HELLO_ACK,导致主机 remoteCredit=0 拒发 PING。 */
|
|
|
|
|
|
g_localCredit = 1U;
|
2026-07-20 02:36:56 +08:00
|
|
|
|
/* 建连 + 开机身份一次完成:HELLO_ACK = 6B 链路 + 16B 身份(版本/开关) */
|
|
|
|
|
|
uint8_t identity[V2_APP_IDENTITY_LEN];
|
|
|
|
|
|
fillGd32AppIdentityPayload(identity);
|
2026-07-16 23:00:52 +08:00
|
|
|
|
size_t built_len = 0U;
|
|
|
|
|
|
v2_build_hello_ack_frame(g_sessionId, V2_ROLE_APP, (uint8_t)g_localCredit,
|
2026-07-20 02:36:56 +08:00
|
|
|
|
0, hdr.tx_seq, identity, sizeof(identity),
|
|
|
|
|
|
m_txBuf, &built_len);
|
2026-07-16 23:00:52 +08:00
|
|
|
|
m_txLen = (uint16_t)built_len;
|
|
|
|
|
|
|
|
|
|
|
|
s_lastReqSeq = hdr.tx_seq;
|
|
|
|
|
|
memcpy(s_lastRespBuf, m_txBuf, m_txLen);
|
|
|
|
|
|
s_lastRespLen = m_txLen;
|
|
|
|
|
|
s_hasCachedResp = true;
|
|
|
|
|
|
|
|
|
|
|
|
finishResponseAndReady();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (g_v2Active) {
|
|
|
|
|
|
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;
|
|
|
|
|
|
finishResponseAndReady();
|
|
|
|
|
|
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) {
|
|
|
|
|
|
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;
|
|
|
|
|
|
finishResponseAndReady();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
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;
|
|
|
|
|
|
finishResponseAndReady();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
postLog("SEQ jump but BOOT_REQ still handled");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
if (g_v2Active) {
|
|
|
|
|
|
s_lastReqSeq = hdr.tx_seq;
|
|
|
|
|
|
memcpy(s_lastRespBuf, m_txBuf, m_txLen);
|
|
|
|
|
|
s_lastRespLen = m_txLen;
|
|
|
|
|
|
s_hasCachedResp = true;
|
|
|
|
|
|
g_txSeq = (uint8_t)(g_txSeq + 1U);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
finishResponseAndReady();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
|
* GD32 主动按铃:设 m_ringPending + updateDataReady() 拉低 PB6 请求 ESP32 来读
|
|
|
|
|
|
* ============================================================ */
|
|
|
|
|
|
|
|
|
|
|
|
void AppSpiCommand::armRingPending()
|
|
|
|
|
|
{
|
|
|
|
|
|
m_ringPending = true;
|
|
|
|
|
|
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void AppSpiCommand::cancelRing()
|
|
|
|
|
|
{
|
|
|
|
|
|
m_ringPending = false;
|
|
|
|
|
|
/* 清除待发响应,防止超时后残留帧在下一次 CS assert 时被错误输出 */
|
|
|
|
|
|
m_txLen = 0U;
|
|
|
|
|
|
/* 释放 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_txLen = (uint16_t)built_len;
|
|
|
|
|
|
armRingPending();
|
|
|
|
|
|
|
|
|
|
|
|
/* 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)。 */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void AppSpiCommand::onCsRelease()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!m_csActive) {
|
|
|
|
|
|
/* EXTI 丢失下降沿时:仍尝试收尾并 re-arm,避免永久失步(对齐 BOOT) */
|
|
|
|
|
|
m_rxLen = stopSpiDmas(kRxBufSize);
|
|
|
|
|
|
drainSpiRxHw();
|
|
|
|
|
|
if (m_txLen > 0U) {
|
|
|
|
|
|
m_txLen = 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_txLen = 0U;
|
|
|
|
|
|
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
|
|
|
|
|
/* 继续走 shouldIgnoreTransaction + processCommand(),不 return */
|
|
|
|
|
|
} else {
|
|
|
|
|
|
/* 正常按铃响应:请求帧已被读走,取消按铃状态 */
|
|
|
|
|
|
m_ringPending = false;
|
|
|
|
|
|
/* 请求帧已被读走,清空 TX 防止下次事务重复输出 */
|
|
|
|
|
|
m_txLen = 0U;
|
|
|
|
|
|
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
|
|
|
|
|
/* 重新预装下一笔 DMA,保证下一 CS 首字节即可收发 */
|
|
|
|
|
|
armSpiDmasForNextCs();
|
|
|
|
|
|
updateDataReady();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) {
|
|
|
|
|
|
m_rxLen = 0U;
|
|
|
|
|
|
/* v2 DATA_READY: 无待处理帧时 updateDataReady() 释放 PB6。 */
|
|
|
|
|
|
armSpiDmasForNextCs();
|
|
|
|
|
|
updateDataReady();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
m_txLen = 0U;
|
|
|
|
|
|
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
|
|
|
|
|
|
2026-07-26 20:39:59 +08:00
|
|
|
|
/* ★ 关键:在 processCommand 之前同步 g_localCredit。
|
|
|
|
|
|
* 此时 m_txLen=0, m_ringPending=false → g_localCredit=1。
|
|
|
|
|
|
* 若不做,buildResponse 会读到上一次 pushStatus/requestEsp32Read 留下的
|
|
|
|
|
|
* g_localCredit=0 → 响应帧携带 credit=0 → ESP32 m_remoteCredit=0 → 后续发送死锁。
|
|
|
|
|
|
* 这是电机 OTA 吐量只有 1 chunk/2s 的根因。 */
|
|
|
|
|
|
updateDataReady();
|
|
|
|
|
|
|
2026-07-16 23:00:52 +08:00
|
|
|
|
/* 命令处理在 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();
|
|
|
|
|
|
}
|