feat(spi): 实现SPI同步通信协议和主动推送机制

- 实现新的SPI同步帧格式,采用0xA5 0x5A起始符和CRC32校验,提升通信可靠性
- 添加GD32与ESP32间的握手机制,通过PB6开漏输出实现SPIready线与控制
- 实现异步OTA启动机制,将Flash操作延迟到主循环执行,避免SPI ISR阻塞
- 改进CAN反馈命令处理,提取通用payload构建函数支持主动推送模式
- 实现GD32主动按铃机制,循环推送CAN反馈数据到ESP32,减少轮询开销
- 添加SPI通信超时保护和死锁防护机制,确保系统稳定性
This commit is contained in:
cbb
2026-07-08 02:51:24 +08:00
parent b78e36ff37
commit 33037bc5a3
9 changed files with 509 additions and 163 deletions
+227 -59
View File
@@ -8,9 +8,14 @@
* - 公共帧格式解析与应答构建
* - 业务命令的注册与分发
*
* 具体命令处理逻辑已经拆到独立的 `app_spi_cmd_*.cpp`,
* 通过 `registerHandler()` 注册进来,后续新增 SPI 业务时
* 具体命令处理逻辑已经拆到独立的 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"
@@ -62,12 +67,12 @@ static void startSpiTxDma(const uint8_t *buf, uint16_t len)
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) |= DMA_CHXCTL_CHEN;
}
static bool isAllBytesValue(const uint8_t *data, uint8_t len, uint8_t value)
static bool isAllBytesValue(const uint8_t *data, uint16_t len, uint8_t value)
{
if (!data || len == 0U) {
return false;
}
for (uint8_t i = 0U; i < len; ++i) {
for (uint16_t i = 0U; i < len; ++i) {
if (data[i] != value) {
return false;
}
@@ -75,7 +80,7 @@ static bool isAllBytesValue(const uint8_t *data, uint8_t len, uint8_t value)
return true;
}
static bool shouldIgnoreTransaction(const uint8_t *data, uint8_t len)
static bool shouldIgnoreTransaction(const uint8_t *data, uint16_t len)
{
if (!data || len == 0U) {
return true;
@@ -110,40 +115,111 @@ static bool isFlashRelatedFrame(uint8_t frame_type)
* CRC8 / 帧构建 / 帧解析(与 BOOT SpiOta 一致)
* ============================================================ */
static uint8_t spi_crc8(const uint8_t *data, uint16_t len)
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);
/* 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)
{
uint8_t crc = 0x00U;
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 ^= data[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)(kFrameOverhead + payload_len);
uint16_t total = (uint16_t)(kSyncOverhead + payload_len);
if (total > AppSpiCommand::kTxBufSize) { return false; }
out[0] = kSof;
out[1] = type;
out[2] = (uint8_t)(payload_len & 0xFFU);
out[3] = (uint8_t)((payload_len >> 8) & 0xFFU);
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[4], payload, payload_len);
memcpy(&out[kSyncHeaderLen], payload, payload_len);
}
out[4 + payload_len] = spi_crc8(out, (uint16_t)(4U + 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; }
/* 兼容首字节延迟:在帧头前面可能出现若干 0xFF / 0x00 残留字节,
* 搜索到第一个 kSof 后尝试按合法帧长度校验 CRC。 */
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) {
@@ -214,15 +290,11 @@ void AppSpiCommand::buildResponse(uint8_t *tx_buf, uint16_t tx_buf_size,
const bool ok = spi_build_frame(rsp_type, payload, payload_len, tx_buf, &built_len);
if (ok) {
*tx_len = built_len;
} else {
uint8_t err = 0xEEU;
(void)spi_build_frame(kRspError, &err, 1U, tx_buf, &built_len);
*tx_len = built_len;
}
}
/* ============================================================
* 硬件配置(与 BOOT 完全一致)
* 硬件配置
* ============================================================ */
void AppSpiCommand::configureGpio()
@@ -238,13 +310,17 @@ void AppSpiCommand::configureGpio()
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 握手输出,默认 HIGH(就绪),允许主机立刻发起首帧 */
gpio_init(CONFIG_SPI_READY_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, CONFIG_SPI_READY_PIN);
/* 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 {
@@ -328,6 +404,9 @@ void AppSpiCommand::init()
m_logPending = false;
m_txActiveThisCs = false;
m_commandCount = 0U;
m_ringPending = false;
m_ringDeadline = 0U;
m_ringTxLen = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
memset(m_logBuf, 0, sizeof(m_logBuf));
memset(m_commands, 0, sizeof(m_commands));
@@ -395,7 +474,7 @@ void AppSpiCommand::processCommand()
}
if (!handled) {
postLogFmt("[SPI-APP] 未知 type=0x%02X → ERROR", frame_type);
postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type);
uint8_t err = 0xEEU;
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
}
@@ -406,13 +485,13 @@ void AppSpiCommand::processCommand()
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
}
m_txLen = (uint8_t)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");
postLog("旧式0xB0 -> 进BOOT");
uint16_t tx_len_legacy = 0U;
SpiCommandContext ctx{};
@@ -434,28 +513,27 @@ void AppSpiCommand::processCommand()
}
if (!handled) {
const bool ok = OtaBootRequest::requestEnterBoot();
uint8_t ack = ok ? CONFIG_SPI_APP_OTA_BOOT_ACK_OK
: CONFIG_SPI_APP_OTA_BOOT_ACK_ERROR;
m_txBuf[0] = CONFIG_SPI_APP_OTA_BOOT_CMD;
m_txBuf[1] = ack;
/* 兜底路径(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;
if (ok) {
TaskManager::set(1UL << TASK_HEARTBEAT);
}
OtaBootRequest::requestEnterBootAsync();
TaskManager::set(1UL << TASK_HEARTBEAT);
}
m_txLen = (uint8_t)tx_len_legacy;
m_txLen = tx_len_legacy;
m_txPos = 0U;
} else {
/* 无法识别 */
postLogFmt("[SPI-APP] 非协议 0x%02X → ERROR", first_valid);
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 = (uint8_t)tx_len;
m_txLen = tx_len;
m_txPos = 0U;
}
}
@@ -465,20 +543,65 @@ void AppSpiCommand::processCommand()
TaskManager::set(1UL << TASK_SPI_LOG);
}
/* 应答帧已就绪:若解析期间未再收到新帧(m_cmdPending 仍为 false),
* 拉高 PB6 通知主机可发起下一笔事务;否则保持 LOW,等待下一轮
* processCommand 完成后再翻高,避免主机在从机未就绪时抢跑。 */
if (!m_cmdPending) {
setSpiReady(true);
/* 应答帧已就绪:拉高 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;
/* 释放 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;
/* 下拉 SPIready,通知 ESP32「我有请求帧要给你读」 */
setSpiReady(false);
return true;
}
/* ============================================================
* 主循环轮询:打印延迟日志 + 按铃超时看护
* ============================================================ */
void AppSpiCommand::poll()
{
/* 打印 ISR 推迟的日志 */
char log_copy[kLogBufSize] = {};
bool has_log = false;
@@ -493,15 +616,18 @@ void AppSpiCommand::poll()
if (has_log) {
printf("%s\r\n", log_copy);
}
}
void AppSpiCommand::processPendingCommand()
{
if (!m_cmdPending) {
return;
/* 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。
* 防止 ESP32 不响应时永久占用 SPIready 线导致双方死锁。 */
if (m_ringPending) {
uint32_t save2 = RT_CRITICAL_SAVE();
if (m_ringPending) {
if ((int32_t)(SysTickTimer::getMs() - m_ringDeadline) >= 0) {
cancelRing();
}
}
RT_CRITICAL_RESTORE(save2);
}
m_cmdPending = false;
processCommand();
}
/* ============================================================
@@ -514,8 +640,8 @@ void AppSpiCommand::onCsAssert()
return;
}
m_csActive = true;
/* CS 下降沿立即拉低 PB6:表示从机即将忙碌,
* 防止主机在 onCsRelease 拉低 PB6 前抢跑发起下一笔 CS。
/* CS 下降沿立即拉低 PB6:表示从机即将忙碌,
* 防止主机在 onCsRelease 准备好响应前抢跑发起下一笔 CS。
* (若此帧被 shouldIgnoreTransaction 忽略,onCsRelease 会恢复 HIGH) */
setSpiReady(false);
m_rxLen = 0U;
@@ -570,6 +696,48 @@ void AppSpiCommand::onCsRelease()
}
}
/* 若本次 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 (m_rxLen >= 2U && m_rxBuf[0] == kSyncSof0 && m_rxBuf[1] == kSyncSof1) {
/* 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) {
@@ -578,8 +746,8 @@ void AppSpiCommand::onCsRelease()
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
}
m_txActiveThisCs = false;
/* 忽略事务(空轮询/残包)后恢复 PB6=HIGH,
* 避免 PB6 卡 LOW 导致 ESP32 waitSpiReady 超时 */
/* 忽略事务(空轮询/残包)后恢复 PB6=HIGH,
* 避免 PB6 卡 LOW 导致 ESP32 等待上升沿超时 */
setSpiReady(true);
return;
}
@@ -588,10 +756,10 @@ void AppSpiCommand::onCsRelease()
m_txPos = 0U;
m_txActiveThisCs = false;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
m_cmdPending = true;
/* 拉低 PB6 通知主机:从机正忙,在主循环解析完前不要再发起 CS */
setSpiReady(false);
TaskManager::set(1UL << TASK_SPI_CMD_PROCESS);
/* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式),
* 消除主循环延迟。处理完成后 processCommand() 末尾释放 SPIready。 */
processCommand();
}
/* ============================================================