Files
DCU_APP/GD32E508_DCU_APP/device/spi/app_spi_command.cpp
T
cbb f3d7fc0c30 feat(canfd): 添加收发器静默模式支持和SPI命令接口
添加了CAN FD收发器静默模式功能,允许通过硬件引脚控制发送使能:
- 新增setTxSilent/isTxSilent接口控制TCAN1057收发器Silent模式
- 静默时驱动器关断但接收机继续工作,支持只听不发场景
- 添加SPI命令0xC8用于ESP32远程控制静默模式切换
- 在总线状态位图中增加bit5表示静默状态
- 新增canBusyWaitUs函数用于ISR安全的微秒延时

fix(esc_lan9252): 改进warm jump后LAN9252探测逻辑

修复warm jump导致的LAN9252初始化问题:
- 识别warm jump后芯片非POR状态(idx0!=0x1234)的情况
- 自动补发硬件复位对齐到POR状态后再进行正常探测
- 改进调试信息输出,区分POR状态和warm jump残值情况
- 修正失败路径的复位标志初始化逻辑

refactor(spi): 整理SPI命令文件结构

调整SPI命令相关文件的组织结构:
- 移动诊断相关文件到更合适的位置
- 重新整理SPI命令处理文件的分组
2026-10-10 02:25:56 +08:00

1376 lines
54 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 "device/spi/app_spi_diag.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 "sys/cpu_load.h"
#include "sys/esp32_en.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;
}
}
/* GD32 APP 侧 CREDIT:运行时单响应缓冲 0/1(有待发=0,空闲=1)。
* 初值必须为 1,禁止用 BOOT 的 8 槽语义;否则 HELLO 前被读到会误导主机 remoteCredit。 */
static volatile uint8_t g_localCredit = 1U;
/* 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;
/* ============================================================
* SPI 诊断统计与现场快照
* ============================================================ */
#if (CONFIG_SPI_DIAG_ENABLE != 0)
#define SPI_DIAG_RECORD(...) do { __VA_ARGS__; } while (0)
static SpiDiagStats s_diag = {};
const SpiDiagStats& getSpiDiagStats()
{
s_diag.current_local_credit = (uint8_t)g_localCredit;
s_diag.expected_rx_seq = g_expectedRxSeq;
return s_diag;
}
void resetSpiDiagStats()
{
memset(&s_diag, 0, sizeof(s_diag));
}
const char* spiParseResultToString(SpiParseResult res)
{
switch (res) {
case SPI_PARSE_OK: return "OK";
case SPI_PARSE_ERR_TOO_SHORT: return "TOO_SHORT";
case SPI_PARSE_ERR_ALL_FF: return "ALL_FF";
case SPI_PARSE_ERR_ALL_00: return "ALL_00";
case SPI_PARSE_ERR_NO_SOF: return "NO_SOF";
case SPI_PARSE_ERR_BAD_VERSION: return "BAD_VERSION";
case SPI_PARSE_ERR_BAD_LEN: return "BAD_LEN";
case SPI_PARSE_ERR_CRC_MISMATCH: return "CRC_MISMATCH";
default: return "UNKNOWN";
}
}
#else
#define SPI_DIAG_RECORD(...) ((void)0)
#endif
bool AppSpiCommand::isV2Active() const { return g_v2Active; }
uint32_t AppSpiCommand::getSessionId() const { return g_sessionId; }
uint8_t AppSpiCommand::getLocalCredit() const { return (uint8_t)g_localCredit; }
void AppSpiCommand::dumpDiag() const { dumpSpiDiag(); }
#if (CONFIG_SPI_DIAG_ENABLE != 0)
void dumpSpiDiag()
{
const SpiDiagStats &st = getSpiDiagStats();
const uint32_t now = SysTickTimer::getMs();
const uint8_t cs_pin = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U;
const uint8_t rdy_pin = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U;
const uint16_t stat = (uint16_t)SPI_STAT(SPI0);
printf("\r\n==================== [GD32 SPI0 LINK DIAGNOSTICS] ====================\r\n");
printf("Status : v2_active=%d session_id=0x%08lX local_credit=%u uptime=%lums\r\n",
AppSpiCommandBus.isV2Active() ? 1 : 0, (unsigned long)AppSpiCommandBus.getSessionId(),
(unsigned)AppSpiCommandBus.getLocalCredit(), (unsigned long)now);
printf("Pins & HW: CS(PA15)=%u DATA_READY(PB6)=%u ring_pending=%d cs_active=%d\r\n",
cs_pin, rdy_pin, AppSpiCommandBus.isRingPending() ? 1 : 0, AppSpiCommandBus.isCsActive() ? 1 : 0);
printf("SPI_STAT : 0x%04X (TRANS:%d TBE:%d RBNE:%d OVR:%d FERR:%d)\r\n",
stat, (stat & SPI_FLAG_TRANS) ? 1 : 0, (stat & SPI_FLAG_TBE) ? 1 : 0,
(stat & SPI_FLAG_RBNE) ? 1 : 0, (stat & SPI_FLAG_RXORERR) ? 1 : 0,
(stat & SPI_FLAG_FERR) ? 1 : 0);
printf("CS Stats : assert=%lu release=%lu missed=%lu short(<20B)=%lu zero_len=%lu\r\n",
(unsigned long)st.total_cs_assert, (unsigned long)st.total_cs_release,
(unsigned long)st.missed_cs_assert, (unsigned long)st.short_rx_len_count,
(unsigned long)st.zero_rx_len_count);
printf("Frames : valid_v2=%lu parse_fail=%lu all_FF=%lu all_00=%lu\r\n",
(unsigned long)st.valid_v2_frames, (unsigned long)st.parse_fail_count,
(unsigned long)st.all_ff_count, (unsigned long)st.all_00_count);
printf("ParseErr : CRC_err=%lu no_SOF=%lu SOF_drift=%lu bad_ver=%lu bad_len=%lu\r\n",
(unsigned long)st.err_crc_mismatch, (unsigned long)st.err_no_sof,
(unsigned long)st.err_sof_drift, (unsigned long)st.err_bad_version,
(unsigned long)st.err_bad_len);
printf("Hw Errors: RXORERR(ovr)=%lu FERR(fmt)=%lu\r\n",
(unsigned long)st.rx_overrun_count, (unsigned long)st.format_error_count);
printf("Session : HELLO_cnt=%lu mismatch=%lu seq_ok=%lu seq_jump=%lu seq_dup=%lu\r\n",
(unsigned long)st.hello_count, (unsigned long)st.session_mismatch,
(unsigned long)st.seq_ok_count, (unsigned long)st.seq_jump_count,
(unsigned long)st.seq_dup_count);
printf("Sequences: last_rx_seq=0x%02X exp_rx_seq=0x%02X last_tx_seq=0x%02X\r\n",
st.last_rx_seq, st.expected_rx_seq, st.last_tx_seq);
printf("Commands : last_rx_type=0x%02X last_tx_type=0x%02X error_rsp(0x82)=%lu (last_code=0x%02X)\r\n",
st.last_rx_type, st.last_tx_type, (unsigned long)st.error_rsp_count, st.last_error_code);
printf("Routing : unknown_type=%lu (last=0x%02X) handler_empty=%lu (last=0x%02X)\r\n",
(unsigned long)st.unknown_type_count, st.last_unknown_type,
(unsigned long)st.handler_empty_count, st.last_empty_type);
printf("Ring Push: req=%lu timeout=%lu collision=%lu\r\n",
(unsigned long)st.ring_request_count, (unsigned long)st.ring_timeout_count,
(unsigned long)st.ring_collision_count);
if (st.last_error_uptime_ms > 0U) {
printf("LastErr : at %lums, rxLen=%u, reason=%s(0x%02X), SOF_off=%u\r\n",
(unsigned long)st.last_error_uptime_ms, (unsigned)st.last_error_rx_len,
spiParseResultToString((SpiParseResult)st.last_error_reason),
st.last_error_code, (unsigned)st.last_error_sof_offset);
printf("LastBytes: [%02X %02X %02X %02X %02X %02X %02X %02X]\r\n",
st.last_error_bytes[0], st.last_error_bytes[1], st.last_error_bytes[2], st.last_error_bytes[3],
st.last_error_bytes[4], st.last_error_bytes[5], st.last_error_bytes[6], st.last_error_bytes[7]);
if (st.last_error_reason == SPI_PARSE_ERR_CRC_MISMATCH) {
printf("LastCRC : calc=0x%08lX exp=0x%08lX\r\n",
(unsigned long)st.last_error_calc_crc, (unsigned long)st.last_error_expected_crc);
}
}
printf("======================================================================\r\n\r\n");
}
#endif
/* ============================================================
* 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;
}
/* 细粒度诊断帧解析:逐项分析失败原因(无SOF/相位漂移/版本不符/长度截断/CRC位错)。 */
static bool spi_diagnose_and_parse_frame(
const uint8_t *buf, size_t buf_len,
uint8_t *out_type, const uint8_t **out_payload, uint16_t *out_payload_len,
v2_header_t *out_hdr,
SpiParseResult *out_res,
uint32_t *out_calc_crc, uint32_t *out_exp_crc,
uint8_t *out_sof_offset)
{
if (!buf || buf_len == 0U) {
if (out_res) *out_res = SPI_PARSE_ERR_TOO_SHORT;
if (out_sof_offset) *out_sof_offset = 0xFFU;
return false;
}
if (isAllBytesValue(buf, (uint16_t)buf_len, 0xFFU)) {
if (out_res) *out_res = SPI_PARSE_ERR_ALL_FF;
if (out_sof_offset) *out_sof_offset = 0xFFU;
return false;
}
if (isAllBytesValue(buf, (uint16_t)buf_len, 0x00U)) {
if (out_res) *out_res = SPI_PARSE_ERR_ALL_00;
if (out_sof_offset) *out_sof_offset = 0xFFU;
return false;
}
if (buf_len < V2_OVERHEAD) {
if (out_res) *out_res = SPI_PARSE_ERR_TOO_SHORT;
if (out_sof_offset) *out_sof_offset = 0xFFU;
return false;
}
bool found_sof = false;
size_t first_sof_offset = 0xFFU;
SpiParseResult best_err = SPI_PARSE_ERR_NO_SOF;
for (size_t i = 0; i + V2_OVERHEAD <= buf_len; ++i) {
if (buf[i + V2_OFF_SOF0] != V2_SOF0 || buf[i + V2_OFF_SOF1] != V2_SOF1) {
continue;
}
if (!found_sof) {
found_sof = true;
first_sof_offset = i;
}
const uint8_t version = buf[i + V2_OFF_VERSION];
if (version != V2_VERSION) {
best_err = SPI_PARSE_ERR_BAD_VERSION;
continue;
}
const uint16_t plen = v2_get_u16le(&buf[i + V2_OFF_LEN]);
if (plen > V2_MAX_PAYLOAD) {
best_err = SPI_PARSE_ERR_BAD_LEN;
continue;
}
const size_t total = V2_OVERHEAD + plen;
if (i + total > buf_len) {
best_err = SPI_PARSE_ERR_BAD_LEN;
continue;
}
const uint32_t expect = v2_get_u32le(&buf[i + V2_HEADER_LEN + plen]);
const uint32_t calc = v2_crc32(&buf[i], V2_HEADER_LEN + plen);
if (expect != calc) {
best_err = SPI_PARSE_ERR_CRC_MISMATCH;
if (out_calc_crc) *out_calc_crc = calc;
if (out_exp_crc) *out_exp_crc = expect;
continue;
}
if (out_hdr) {
out_hdr->version = version;
out_hdr->flags = buf[i + V2_OFF_FLAGS];
out_hdr->session_id = v2_get_u32le(&buf[i + V2_OFF_SESSION]);
out_hdr->tx_seq = buf[i + V2_OFF_TX_SEQ];
out_hdr->ack_seq = buf[i + V2_OFF_ACK_SEQ];
out_hdr->credit = buf[i + V2_OFF_CREDIT];
out_hdr->type = buf[i + V2_OFF_TYPE];
out_hdr->payload_len = plen;
out_hdr->frame_offset = i;
}
if (out_type) *out_type = buf[i + V2_OFF_TYPE];
if (out_payload) *out_payload = (plen > 0U) ? &buf[i + V2_HEADER_LEN] : nullptr;
if (out_payload_len) *out_payload_len = plen;
if (out_res) *out_res = SPI_PARSE_OK;
if (out_sof_offset) *out_sof_offset = (uint8_t)i;
if (out_calc_crc) *out_calc_crc = calc;
if (out_exp_crc) *out_exp_crc = expect;
return true;
}
if (out_res) *out_res = best_err;
if (out_sof_offset) *out_sof_offset = (uint8_t)first_sof_offset;
return false;
}
/* ============================================================
* 调试日志辅助(环形队列,避免事件并发时丢日志)
* ============================================================ */
void AppSpiCommand::postLog(const char *msg)
{
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
if (!msg) return;
uint8_t next = (uint8_t)((m_logHead + 1U) % kLogSlotCount);
if (next != m_logTail) {
strncpy(m_logQueue[m_logHead], msg, kLogSlotLen - 1U);
m_logQueue[m_logHead][kLogSlotLen - 1U] = '\0';
m_logHead = next;
}
#else
(void)msg;
#endif
}
void AppSpiCommand::postLogFmt(const char *fmt, uint8_t val)
{
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
if (!fmt) return;
char tmp[kLogSlotLen];
snprintf(tmp, sizeof(tmp), fmt, val);
postLog(tmp);
#else
(void)fmt;
(void)val;
#endif
}
void AppSpiCommand::postLogFmt32(const char *fmt, uint32_t v1, uint32_t v2)
{
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
if (!fmt) return;
char tmp[kLogSlotLen];
snprintf(tmp, sizeof(tmp), fmt, (unsigned long)v1, (unsigned long)v2);
postLog(tmp);
#else
(void)fmt;
(void)v1;
(void)v2;
#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);
/* 先清 STAT 再开 IMR。warm jump 下 BOOT 关中断跳转前 ESP32 持续拉 CS,
* EXTI15 STAT 大概率残留;若先 enable IMR,残留位会立即回放成一次假 CS
* 沿进 NVIC。 */
exti_interrupt_flag_clear(EXTI_15);
exti_interrupt_enable(EXTI_15);
/* SPI CS EXTI 抢占级 0。抢占级 1 与 PDI/SYNC1/SCHED 同级不抢占
* (PDI 窗口可达数百 µs),且被 RT_CRITICAL(BASEPRI≥1) 完全屏蔽;
* release ISR 一旦拖过 ESP32 的 CS 间隔(1ms),re-arm 落空,
* 下一笔查询帧头部丢失。提到 0 后仅与 SYNC0/CAN-RX
* (µs 级)同级排队,release 处理确定性 µs 级完成。代价:0xA2 pmDrive 的
* 忙等(≤10ms,手动 PM 诊断才用)会阻塞 SYNC0——诊断窗口,可接受。 */
nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U);
}
/* ============================================================
* 默认命令注册入口(由各 app_spi_cmd_*.cpp 提供)
* ============================================================ */
extern void registerSpiBootRequestCommand(AppSpiCommand &bus);
extern void registerSpiOtaWaitCommand(AppSpiCommand &bus);
extern void registerSpiAppPingCommand(AppSpiCommand &bus);
extern void registerSpiInitStatusCommand(AppSpiCommand &bus);
extern void registerSpiCanBridgeCommand(AppSpiCommand &bus);
extern void registerSpiCanBurstCommand(AppSpiCommand &bus);
extern void registerSpiCanFeedbackCommand(AppSpiCommand &bus);
extern void registerSpiIdSnapshotCommand(AppSpiCommand &bus);
extern void registerSpiAdcFeedbackCommand(AppSpiCommand &bus);
extern void registerSpiMotorStateCommand(AppSpiCommand &bus);
extern void registerSpiMotorOtaCommand(AppSpiCommand &bus);
extern void registerSpiExmcDiagCommand(AppSpiCommand &bus);
extern void registerSpiDiagCommand(AppSpiCommand &bus);
AppSpiCommand AppSpiCommandBus;
void AppSpiCommand::init()
{
m_csActive = false;
m_rxLen = 0U;
m_txLen = 0U;
m_logHead = 0U;
m_logTail = 0U;
m_commandCount = 0U;
m_ringPending = false;
m_ringDeadline = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
memset(m_logQueue, 0, sizeof(m_logQueue));
memset(m_commands, 0, sizeof(m_commands));
registerSpiBootRequestCommand(AppSpiCommandBus);
registerSpiOtaWaitCommand(AppSpiCommandBus);
registerSpiAppPingCommand(AppSpiCommandBus);
registerSpiInitStatusCommand(AppSpiCommandBus);
registerSpiCanBridgeCommand(AppSpiCommandBus);
registerSpiCanBurstCommand(AppSpiCommandBus);
registerSpiCanFeedbackCommand(AppSpiCommandBus);
registerSpiIdSnapshotCommand(AppSpiCommandBus);
registerSpiAdcFeedbackCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */
registerSpiMotorStateCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */
registerSpiMotorOtaCommand(AppSpiCommandBus);
registerSpiExmcDiagCommand(AppSpiCommandBus);
registerSpiDiagCommand(AppSpiCommandBus);
configureGpio();
configureDma();
configureSpi0();
/* 必须先预装 DMA,再开 CS EXTI。
* OTA 跳转后 ESP32 可能立刻打 HELLO:若 EXTI 已开而 DMA 未 arm,
* 首笔 CS 会让 SPI 从机失步,之后长期 MISO 噪声/全 FF。 */
armSpiDmasForNextCs();
configureExti();
}
/** @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) {
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));
return;
}
m_commands[m_commandCount++] = handler;
}
void AppSpiCommand::finishResponseAndReady()
{
/* 有响应待发时 arm 按铃(ESP32 主动请求的响应即靠此通知;
* 推送源已随 2026-08 请求驱动改造移除,无其他写者争用 m_txBuf)。
* 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;
SpiParseResult parse_res = SPI_PARSE_OK;
uint32_t calc_crc = 0U;
uint32_t exp_crc = 0U;
uint8_t sof_offset = 0xFFU;
const bool parse_ok = spi_diagnose_and_parse_frame(
m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr,
&parse_res, &calc_crc, &exp_crc, &sof_offset);
if (!parse_ok) {
SPI_DIAG_RECORD(
s_diag.parse_fail_count++;
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
s_diag.last_error_rx_len = m_rxLen;
s_diag.last_error_calc_crc = calc_crc;
s_diag.last_error_expected_crc = exp_crc;
s_diag.last_error_sof_offset = sof_offset;
s_diag.last_error_reason = (uint8_t)parse_res;
s_diag.last_error_code = 0xE8U;
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
);
#if (CONFIG_SPI_APP_LOG_ENABLE != 0 && CONFIG_SPI_DIAG_ENABLE != 0)
switch (parse_res) {
case SPI_PARSE_ERR_CRC_MISMATCH: {
SPI_DIAG_RECORD(s_diag.err_crc_mismatch++);
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] CRC mismatch: calc=0x%08lX exp=0x%08lX rxLen=%u",
(unsigned long)calc_crc, (unsigned long)exp_crc, (unsigned)m_rxLen);
postLog(log_msg);
break;
}
case SPI_PARSE_ERR_NO_SOF: {
SPI_DIAG_RECORD(s_diag.err_no_sof++);
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] No SOF: rxLen=%u raw=[%02X %02X %02X %02X]",
(unsigned)m_rxLen, m_rxBuf[0], m_rxBuf[1], m_rxBuf[2], m_rxBuf[3]);
postLog(log_msg);
break;
}
case SPI_PARSE_ERR_BAD_VERSION: {
SPI_DIAG_RECORD(s_diag.err_bad_version++);
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] Bad ver=0x%02X (exp 0x02) SOF_off=%u rxLen=%u",
(sof_offset < m_rxLen - 2) ? m_rxBuf[sof_offset + 2] : 0xFFU,
(unsigned)sof_offset, (unsigned)m_rxLen);
postLog(log_msg);
break;
}
case SPI_PARSE_ERR_BAD_LEN: {
SPI_DIAG_RECORD(s_diag.err_bad_len++);
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] Bad len at SOF_off=%u rxLen=%u",
(unsigned)sof_offset, (unsigned)m_rxLen);
postLog(log_msg);
break;
}
default: {
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] Parse fail res=%d rxLen=%u",
(int)parse_res, (unsigned)m_rxLen);
postLog(log_msg);
break;
}
}
#endif
if (sof_offset > 0U && sof_offset != 0xFFU) {
SPI_DIAG_RECORD(s_diag.err_sof_drift++);
}
/* 0xE8 payload 携带接收诊断——[0]=0xE8, [1]=rxLen, [2..3]=rxBuf 前 2 字节, [4]=parse_res, [5]=sof_offset */
uint16_t tx_len = 0U;
uint8_t diag[8];
diag[0] = 0xE8U;
diag[1] = (m_rxLen > 255U) ? 255U : (uint8_t)m_rxLen;
diag[2] = (m_rxLen > 0U) ? m_rxBuf[0] : 0xFFU;
diag[3] = (m_rxLen > 1U) ? m_rxBuf[1] : 0xFFU;
diag[4] = (uint8_t)parse_res;
diag[5] = sof_offset;
diag[6] = 0x5AU;
diag[7] = 0xA5U;
SPI_DIAG_RECORD(s_diag.error_rsp_count++);
spi_build_frame(kRspError, diag, sizeof(diag), m_txBuf, &tx_len);
m_txLen = tx_len;
finishResponseAndReady();
return;
}
// 解析合法 v2 帧成功
SPI_DIAG_RECORD(
s_diag.valid_v2_frames++;
s_diag.last_rx_type = frame_type;
s_diag.last_rx_seq = hdr.tx_seq;
);
if (sof_offset > 0U && sof_offset != 0xFFU) {
SPI_DIAG_RECORD(s_diag.err_sof_drift++);
}
g_pendingAckSeq = hdr.tx_seq;
if (frame_type == V2_TYPE_LINK_HELLO) {
SPI_DIAG_RECORD(s_diag.hello_count++);
Esp32En::cancelRelinkFromSpi();
g_sessionId = hdr.session_id;
g_txSeq = 0;
g_expectedRxSeq = 0;
g_v2Active = true;
s_hasCachedResp = false;
g_localCredit = 1U;
uint8_t identity[V2_APP_IDENTITY_LEN];
fillGd32AppIdentityPayload(identity);
size_t built_len = 0U;
v2_build_hello_ack_frame(g_sessionId, V2_ROLE_APP, (uint8_t)g_localCredit,
0, hdr.tx_seq, identity, sizeof(identity),
m_txBuf, &built_len);
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;
SPI_DIAG_RECORD(
s_diag.last_tx_type = V2_TYPE_LINK_HELLO;
s_diag.last_tx_seq = 0;
);
finishResponseAndReady();
return;
}
if (g_v2Active) {
if (hdr.session_id != g_sessionId) {
SPI_DIAG_RECORD(
s_diag.session_mismatch++;
s_diag.last_error_code = V2_ERR_SESSION;
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
s_diag.error_rsp_count++;
);
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] Session mismatch: rx=0x%08lX != cur=0x%08lX",
(unsigned long)hdr.session_id, (unsigned long)g_sessionId);
postLog(log_msg);
#endif
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;
SPI_DIAG_RECORD(
s_diag.last_tx_type = V2_TYPE_LINK_ERROR;
s_diag.last_tx_seq = g_txSeq;
);
finishResponseAndReady();
return;
}
const uint8_t dist = v2_seq_distance(hdr.tx_seq, g_expectedRxSeq);
if (dist == 0U) {
SPI_DIAG_RECORD(s_diag.seq_ok_count++);
g_expectedRxSeq = (uint8_t)(g_expectedRxSeq + 1U);
} else if (dist >= 128U) {
SPI_DIAG_RECORD(s_diag.seq_dup_count++);
if (s_hasCachedResp && hdr.tx_seq == s_lastReqSeq) {
postLogFmt("[SPI-DIAG][INFO] 重复请求 seq=0x%02X -> 重发缓存", hdr.tx_seq);
memcpy(m_txBuf, s_lastRespBuf, s_lastRespLen);
m_txLen = s_lastRespLen;
finishResponseAndReady();
return;
}
} else {
SPI_DIAG_RECORD(
s_diag.seq_jump_count++;
s_diag.last_error_code = V2_ERR_SEQ_JUMP;
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
);
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][WARN] SEQ jump: rx=0x%02X exp=0x%02X (gap=%u)",
hdr.tx_seq, g_expectedRxSeq, (unsigned)dist);
postLog(log_msg);
#endif
g_expectedRxSeq = (uint8_t)(hdr.tx_seq + 1U);
if (frame_type != kTypeBootReq) {
SPI_DIAG_RECORD(s_diag.error_rsp_count++);
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;
SPI_DIAG_RECORD(
s_diag.last_tx_type = V2_TYPE_LINK_ERROR;
s_diag.last_tx_seq = g_txSeq;
);
finishResponseAndReady();
return;
}
postLog("[SPI-DIAG] 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) {
SPI_DIAG_RECORD(
s_diag.unknown_type_count++;
s_diag.last_unknown_type = frame_type;
s_diag.last_error_code = 0xE9U;
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
s_diag.error_rsp_count++;
);
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] 未知 type=0x%02X -> ERROR 0xE9", frame_type);
postLog(log_msg);
#endif
uint8_t err = 0xE9U; /* E9=无 handler 认领 */
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
}
if (tx_len == 0U) {
SPI_DIAG_RECORD(
s_diag.handler_empty_count++;
s_diag.last_empty_type = frame_type;
s_diag.last_error_code = 0xEAU;
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
s_diag.error_rsp_count++;
);
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] handler t=0x%02X tx_len=0 -> ERROR 0xEA", frame_type);
postLog(log_msg);
#endif
uint8_t err = 0xEAU; /* EA=handler 命中但未产出响应 */
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
}
m_txLen = tx_len;
SPI_DIAG_RECORD(
s_diag.last_tx_type = m_txBuf[V2_OFF_TYPE];
s_diag.last_tx_seq = g_txSeq;
);
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();
}
void AppSpiCommand::resetEsp32Link()
{
/* ESP32 被 PC9 掉电可能死在半笔 CS 里,SPI0/DMA/EXTI 都可能卡死。
* 做一次完整外设重初始化(对齐 init 的硬件序列),而不是只清软件状态。
* 仅在 PC9 翻转、对端确定不在发帧时调用。 */
nvic_irq_disable(EXTI10_15_IRQn);
exti_interrupt_disable(EXTI_15);
g_v2Active = false;
g_sessionId = 0U;
g_txSeq = 0U;
g_expectedRxSeq = 0U;
g_pendingAckSeq = 0U;
g_localCredit = 1U;
s_lastReqSeq = 0xFFU;
s_lastRespLen = 0U;
s_hasCachedResp = false;
m_csActive = false;
m_rxLen = 0U;
m_ringPending = false;
m_ringDeadline = 0U;
m_txLen = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
memset(m_rxBuf, 0x00, sizeof(m_rxBuf));
/* 硬件重初始化:SPI0 + DMA + CS EXTI(与 init() 同序列) */
configureDma();
configureSpi0();
armSpiDmasForNextCs();
updateDataReady();
configureExti();
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
printf("[SPI-APP] resetEsp32Link: SPI0/DMA/EXTI re-init done\r\n");
#endif
}
bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len)
{
SPI_DIAG_RECORD(s_diag.ring_request_count++);
/* 若上一次按铃尚未被 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()
{
/* 1. 打印 ISR 推迟的日志队列(环形缓冲,避免丢弃) */
while (m_logTail != m_logHead) {
printf("%s\r\n", m_logQueue[m_logTail]);
m_logTail = (uint8_t)((m_logTail + 1U) % kLogSlotCount);
}
/* 2. 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。
* 防止 ESP32 不响应时永久占用 SPIready 线导致双方死锁。 */
if (m_ringPending) {
uint32_t save2 = RT_CRITICAL_SAVE();
if (m_ringPending) {
if ((int32_t)(SysTickTimer::getMs() - m_ringDeadline) >= 0) {
SPI_DIAG_RECORD(s_diag.ring_timeout_count++);
#if (CONFIG_SPI_DIAG_PERIODIC_PRINT != 0)
printf("[SPI-DIAG][WARN] 按铃超时取消 (ESP32 未在 %lums 内响应, 释放 SPIready 重试)\r\n",
(unsigned long)kRingTimeoutMs);
#endif
cancelRing();
}
}
RT_CRITICAL_RESTORE(save2);
}
#if (CONFIG_SPI_DIAG_ENABLE != 0 && CONFIG_SPI_DIAG_PERIODIC_PRINT != 0)
/* 3. 定期 / 增量打印 SPI 诊断摘要 */
static uint32_t s_lastDiagPrintMs = 0;
static uint32_t s_lastDiagErrCount = 0;
const uint32_t now = SysTickTimer::getMs();
const uint32_t totalErrs = s_diag.parse_fail_count + s_diag.rx_overrun_count +
s_diag.short_rx_len_count + s_diag.session_mismatch +
s_diag.seq_jump_count + s_diag.ring_timeout_count +
s_diag.unknown_type_count + s_diag.handler_empty_count;
const bool hasNewError = (totalErrs > s_lastDiagErrCount && (now - s_lastDiagPrintMs >= 2000U));
const bool periodicCheck = (s_diag.total_cs_assert > 0U && (now - s_lastDiagPrintMs >= 5000U));
if (hasNewError || periodicCheck) {
s_lastDiagPrintMs = now;
s_lastDiagErrCount = totalErrs;
const uint8_t cs_pin = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U;
const uint8_t rdy_pin = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U;
printf("[SPI-DIAG] CS:assert=%lu rls=%lu ok_v2=%lu | err[CRC:%lu noSOF:%lu short:%lu ovr:%lu sessMis:%lu jmp:%lu tmo:%lu] pins[CS:%d RDY:%d STAT:0x%02X]\r\n",
(unsigned long)s_diag.total_cs_assert, (unsigned long)s_diag.total_cs_release,
(unsigned long)s_diag.valid_v2_frames,
(unsigned long)s_diag.err_crc_mismatch, (unsigned long)s_diag.err_no_sof,
(unsigned long)s_diag.short_rx_len_count, (unsigned long)s_diag.rx_overrun_count,
(unsigned long)s_diag.session_mismatch, (unsigned long)s_diag.seq_jump_count,
(unsigned long)s_diag.ring_timeout_count, cs_pin, rdy_pin, (unsigned)(s_diag.last_spi_stat & 0xFFU));
}
#endif
}
/* ============================================================
* CS 边沿处理
* ============================================================ */
void AppSpiCommand::onCsAssert()
{
SPI_DIAG_RECORD(s_diag.total_cs_assert++);
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()
{
SPI_DIAG_RECORD(s_diag.total_cs_release++);
if (!m_csActive) {
/* EXTI 丢失下降沿时:仍尝试收尾并 re-arm,避免永久失步(对齐 BOOT) */
m_rxLen = stopSpiDmas(kRxBufSize);
SPI_DIAG_RECORD(
s_diag.missed_cs_assert++;
s_diag.last_rx_len = m_rxLen;
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
);
postLogFmt("[SPI-DIAG][WARN] CS release without assert (got %u B)", (uint8_t)m_rxLen);
drainSpiRxHw();
if (m_txLen > 0U) {
m_txLen = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
}
if (m_rxLen >= V2_OVERHEAD) {
processCommand();
} else {
/* 残包:SPI 可能已乱,重配外设 */
configureSpi0();
}
armSpiDmasForNextCs();
updateDataReady();
return;
}
m_csActive = false;
/* 停 DMA 并取本笔 RX 长度(先读 CHCNT 再 disable,否则计数器被清) */
m_rxLen = stopSpiDmas(kRxBufSize);
SPI_DIAG_RECORD(
s_diag.last_rx_len = m_rxLen;
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
);
drainSpiRxHw();
/* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧,
* 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。 */
if (m_ringPending) {
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) {
SPI_DIAG_RECORD(s_diag.ring_collision_count++);
postLog("[SPI-DIAG][INFO] Ring collision: host sent v2 frame while ring pending");
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)) {
if (m_rxLen == 0U) {
SPI_DIAG_RECORD(
s_diag.zero_rx_len_count++;
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_TOO_SHORT;
s_diag.last_error_rx_len = 0U;
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
);
postLog("[SPI-DIAG][WARN] Ignored 0-byte transaction (SCK did not toggle)");
} else if (isAllBytesValue(m_rxBuf, m_rxLen, 0xFFU)) {
SPI_DIAG_RECORD(
s_diag.all_ff_count++;
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_ALL_FF;
s_diag.last_error_rx_len = m_rxLen;
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
);
postLogFmt("[SPI-DIAG][WARN] Ignored all-0xFF transaction (%u bytes)", (uint8_t)m_rxLen);
} else if (isAllBytesValue(m_rxBuf, m_rxLen, 0x00U)) {
SPI_DIAG_RECORD(
s_diag.all_00_count++;
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_ALL_00;
s_diag.last_error_rx_len = m_rxLen;
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
);
postLogFmt("[SPI-DIAG][WARN] Ignored all-0x00 transaction (%u bytes)", (uint8_t)m_rxLen);
} else {
SPI_DIAG_RECORD(
s_diag.short_rx_len_count++;
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_TOO_SHORT;
s_diag.last_error_rx_len = m_rxLen;
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
);
postLogFmt("[SPI-DIAG][WARN] Ignored short transaction (%u bytes < 20B)", (uint8_t)m_rxLen);
}
m_rxLen = 0U;
/* 起机毛刺/相位漂移后仅 re-arm DMA 不够:SPI0 可能已 OVR 卡死。
* 顺手重配 SPI0+DMA(不动 EXTI/session),下一笔 HELLO 才能收进来。 */
configureSpi0();
armSpiDmasForNextCs();
updateDataReady();
return;
}
m_txLen = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
updateDataReady();
/* 命令处理在 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) {
SPI_DIAG_RECORD(
s_diag.rx_overrun_count++;
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
);
postLog("[SPI-DIAG][ERR] SPI0 RXORERR (RX Overrun Error)!");
(void)spi_i2s_data_receive(SPI0);
(void)SPI_STAT(SPI0);
}
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) {
SPI_DIAG_RECORD(
s_diag.format_error_count++;
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
);
postLog("[SPI-DIAG][ERR] SPI0 FERR (Frame Format Error)!");
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)
{
CpuLoad::isrEnter();
AppSpiCommandBus.onInterrupt();
CpuLoad::isrExit();
}
extern "C" void EXTI10_15_IRQHandler(void)
{
CpuLoad::isrEnter();
AppSpiCommandBus.onCsExtiInterrupt();
CpuLoad::isrExit();
}