refactor(spi): 优化CAN反馈推送的节流机制
将CAN反馈推送改为1Hz节流,由taskHeartbeat每秒调用 canFeedbackStartRound激活新轮次,避免与CANFD刷新频率不匹配。 添加s_roundActive标志控制推送节奏,确保每轮完成后等待下一秒 再开始新的推送循环。 --- feat(spi): 实现SPI就绪状态检测和链路状态机 新增SpiLinkState类管理SPI链路状态,通过EXTI6检测PB6引脚 的上升沿来判断ESP32是否完成上一帧处理。 配置SPI Ready引脚的外部中断,实现可靠的ESP32完成信号检测, 避免在ESP32未就绪时过早发起新的SPI通信。 修复SPI接收溢出处理,改为丢弃溢出字节而非重置整个接收缓冲区, 提高SPI通信稳定性。
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* @file canfd.cpp
|
||||
* @brief CANFD 通信模块实现。
|
||||
*/
|
||||
@@ -1284,6 +1284,14 @@ void CanFd::refreshFeedbackSnapshot()
|
||||
}
|
||||
|
||||
s_feedbackSeq++;
|
||||
/* 每 5 秒打印一次 seq 心跳,确认 refreshFeedbackSnapshot 在运行 */
|
||||
if (s_feedbackSeq % 5U == 0U) {
|
||||
printf("[CAN-FB] refresh seq=%lu rx0=%lu rx1=%lu rx2=%lu\r\n",
|
||||
(unsigned long)s_feedbackSeq,
|
||||
(unsigned long)s_channelStats[0].rx_count,
|
||||
(unsigned long)s_channelStats[1].rx_count,
|
||||
(unsigned long)s_channelStats[2].rx_count);
|
||||
}
|
||||
for (size_t i = 0; i < kChannelCount; ++i) {
|
||||
s_feedback[i].snapshot_seq = s_feedbackSeq;
|
||||
s_feedback[i].stats = s_channelStats[i];
|
||||
|
||||
@@ -238,11 +238,23 @@ static SpiCanFeedbackHandler s_handler;
|
||||
* 否则构建当前页反馈 payload 并通过 requestEsp32Read(0xC1) 主动推送到 ESP32,
|
||||
* 然后推进游标。ESP32 检测到 SPIready 下降沿后发起读事务取走该帧,
|
||||
* CS 释放后 GD32 中断唤醒主循环继续推送下一帧。
|
||||
*
|
||||
* 1Hz 节流由任务调度器驱动:taskHeartbeat() 每秒调用 canFeedbackStartRound()
|
||||
* 激活新轮次,canFeedbackPushStep() 推完一轮(回到 ch=0,page=0)后清除标志。
|
||||
* 与 CanFd::refreshFeedbackSnapshot() 的 1Hz 刷新对齐,避免重复推同一个 seq。
|
||||
*/
|
||||
static volatile bool s_roundActive = true; /* 首次允许启动,无需等 1s */
|
||||
|
||||
void canFeedbackStartRound()
|
||||
{
|
||||
s_roundActive = true;
|
||||
}
|
||||
|
||||
void canFeedbackPushStep()
|
||||
{
|
||||
static uint8_t s_channel = 0U;
|
||||
static uint8_t s_page = 0U;
|
||||
static uint8_t s_channel = 0U;
|
||||
static uint8_t s_page = 0U;
|
||||
static uint32_t s_pushCount = 0U;
|
||||
|
||||
/* 已收到 0xB0 进入 Bootloader 请求,停止主动按铃避免干扰 OTA 流程。
|
||||
* 复位前主循环仍在运行,但不再下拉 SPIready,让 ESP32 的
|
||||
@@ -251,24 +263,38 @@ void canFeedbackPushStep()
|
||||
return;
|
||||
}
|
||||
|
||||
/* 1Hz 节流:当前轮次未激活则等待 taskHeartbeat() 触发 */
|
||||
if (!s_roundActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 上一帧尚未被 ESP32 读走,等待(不占用主线带宽) */
|
||||
if (AppSpiCommandBus.isRingPending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* ESP32 尚未完成上一帧处理(未释放 SPIready),等待 EXTI6 上升沿。
|
||||
* 防止 GD32 在 ESP32 回到 IDLE 前按铃导致下降沿被 ISR 丢弃。 */
|
||||
if (!AppSpiCommandBus.canRingNow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t payload[kFeedbackPayloadLen] = {};
|
||||
uint16_t payload_len = buildFeedbackPayload(s_channel, s_page,
|
||||
payload, sizeof(payload));
|
||||
if (payload_len > 0U) {
|
||||
(void)AppSpiCommandBus.requestEsp32Read(SpiDefs::kTypeCanFeedback,
|
||||
payload, payload_len);
|
||||
++s_pushCount; /* 仅保留计数,不打印心跳日志 */
|
||||
}
|
||||
|
||||
/* 推进到下一页/下一通道,无限循环 3 通道 × 2 页 */
|
||||
/* 推进到下一页/下一通道,循环遍历 3 通道 × 2 页 */
|
||||
if (++s_page >= 2U) {
|
||||
s_page = 0U;
|
||||
if (++s_channel >= kFeedbackChannelCount) {
|
||||
s_channel = 0U;
|
||||
/* 回到 ch=0,page=0:一轮推完,等待下一秒 taskHeartbeat 激活 */
|
||||
s_roundActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,18 @@ static constexpr uint16_t kSyncHeaderLen = 6U;
|
||||
static constexpr uint16_t kSyncCrcLen = 4U;
|
||||
static constexpr uint16_t kSyncOverhead = (uint16_t)(kSyncHeaderLen + kSyncCrcLen);
|
||||
|
||||
/* 扫描前 16 字节查找同步帧头 SOF0 SOF1,容忍 RXORERR 导致的首字节偏移。
|
||||
* 与 spi_try_parse_frame 的偏移扫描逻辑对齐。 */
|
||||
static bool containsSyncSof(const uint8_t *data, uint16_t len)
|
||||
{
|
||||
if (!data || len < 2U) { return false; }
|
||||
uint16_t last = (len < 16U) ? (uint16_t)(len - 1U) : 16U;
|
||||
for (uint16_t i = 0U; i < last; ++i) {
|
||||
if (data[i] == kSyncSof0 && data[i + 1U] == kSyncSof1) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* GD32 APP 侧同步状态机(ISR 与主循环只读/原子写,避免临界区开销)。 */
|
||||
static volatile uint8_t g_localCredit = 8U;
|
||||
static volatile uint8_t g_remoteCredit = 8U;
|
||||
@@ -384,6 +396,28 @@ void AppSpiCommand::configureExti()
|
||||
nvic_irq_enable(EXTI10_15_IRQn, CONFIG_IRQ_PREEMPT_SPI_USART, 1U);
|
||||
}
|
||||
|
||||
void AppSpiCommand::configureSpiReadyExti()
|
||||
{
|
||||
/* PB6 已在 configureGpio() 配置为开漏输出,此处启用 EXTI6 上升沿检测。
|
||||
* EXTI 检测引脚实际电平(IDR),与 GPIO 输出模式兼容。
|
||||
* 用于检测 ESP32 releaseReady 产生的上升沿(线从忙碌→空闲),
|
||||
* 作为 ESP32 完成上一帧处理的可靠信号。 */
|
||||
gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOB, GPIO_PIN_SOURCE_6);
|
||||
exti_init(EXTI_6, EXTI_INTERRUPT, EXTI_TRIG_RISING);
|
||||
exti_interrupt_enable(EXTI_6);
|
||||
exti_interrupt_flag_clear(EXTI_6);
|
||||
nvic_irq_enable(EXTI5_9_IRQn, CONFIG_IRQ_PREEMPT_SPI_USART, 1U);
|
||||
}
|
||||
|
||||
void AppSpiCommand::onSpiReadyExti()
|
||||
{
|
||||
if (exti_interrupt_flag_get(EXTI_6) != RESET) {
|
||||
exti_interrupt_flag_clear(EXTI_6);
|
||||
/* 线升高(空闲):ESP32 已 releaseReady,上一帧处理完 */
|
||||
m_linkState.onEsp32Complete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
* 默认命令注册入口(由各 app_spi_cmd_*.cpp 提供)
|
||||
@@ -407,6 +441,7 @@ void AppSpiCommand::init()
|
||||
m_ringPending = false;
|
||||
m_ringDeadline = 0U;
|
||||
m_ringTxLen = 0U;
|
||||
m_linkState.reset();
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
memset(m_logBuf, 0, sizeof(m_logBuf));
|
||||
memset(m_commands, 0, sizeof(m_commands));
|
||||
@@ -422,6 +457,7 @@ void AppSpiCommand::init()
|
||||
configureSpi0();
|
||||
configureDma();
|
||||
configureExti();
|
||||
configureSpiReadyExti();
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -567,6 +603,8 @@ void AppSpiCommand::cancelRing()
|
||||
/* 清除待发响应,防止超时后残留帧在下一次 CS assert 时被错误输出 */
|
||||
m_txLen = 0U;
|
||||
m_txPos = 0U;
|
||||
/* 超时后允许重试,不依赖 ESP32 releaseReady 上升沿 */
|
||||
m_linkState.onRingTimeout();
|
||||
/* 释放 SPIready(高阻),避免永久占用线 */
|
||||
setSpiReady(true);
|
||||
}
|
||||
@@ -589,6 +627,8 @@ bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload,
|
||||
m_txPos = 0U;
|
||||
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
|
||||
m_ringPending = true;
|
||||
/* 消费完成 flag,等 ESP32 releaseReady 触发 EXTI6 上升沿 */
|
||||
m_linkState.onRingStart();
|
||||
|
||||
/* 下拉 SPIready,通知 ESP32「我有请求帧要给你读」 */
|
||||
setSpiReady(false);
|
||||
@@ -623,6 +663,7 @@ void AppSpiCommand::poll()
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -713,7 +754,7 @@ void AppSpiCommand::onCsRelease()
|
||||
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) {
|
||||
} else if (containsSyncSof(m_rxBuf, m_rxLen)) {
|
||||
/* ESP32 发的是协议帧(如 PING/OTA 等),不是按铃响应。
|
||||
* 取消按铃,清空 TX 让 processCommand() 重新装载对应应答。
|
||||
* 不释放 SPIready,processCommand() 末尾会释放。 */
|
||||
@@ -781,8 +822,11 @@ void AppSpiCommand::onInterrupt()
|
||||
/* TBE 已由 DMA 替代,不再处理 */
|
||||
|
||||
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
|
||||
/* 读取并丢弃 overrun 字节以清除标志,但保留已收到的帧数据。
|
||||
* 丢失的字节会导致 CRC 校验失败,由 spi_try_parse_frame 优雅处理
|
||||
* (返回 false -> processCommand 构建 ERROR 响应),
|
||||
* 而不是重置 m_rxLen 导致整帧丢失。 */
|
||||
(void)spi_i2s_data_receive(SPI0);
|
||||
m_rxLen = 0U;
|
||||
}
|
||||
|
||||
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) {
|
||||
@@ -811,3 +855,8 @@ extern "C" void EXTI10_15_IRQHandler(void)
|
||||
{
|
||||
AppSpiCommandBus.onCsExtiInterrupt();
|
||||
}
|
||||
|
||||
extern "C" void EXTI5_9_IRQHandler(void)
|
||||
{
|
||||
AppSpiCommandBus.onSpiReadyExti();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,39 @@
|
||||
|
||||
class ISpiCommandHandler;
|
||||
|
||||
/**
|
||||
* @brief GD32 侧 SPI 链路状态机:封装按铃状态 + ESP32 完成信号。
|
||||
*
|
||||
* 线状态语义(对齐 OTA):
|
||||
* - 线 HIGH = 空闲(双方释放),GD32 可发起新按铃
|
||||
* - 线 LOW = 忙碌(GD32 按铃下拉 或 ESP32 checkGd32Request 期间下拉)
|
||||
*
|
||||
* 状态流转:
|
||||
* - canRingNow()=true(初始/ESP32 完成后)→ requestEsp32Read() → canRingNow()=false
|
||||
* - ESP32 releaseReady 上升沿 → onEsp32Complete() → canRingNow()=true
|
||||
* - 按铃超时 → onRingTimeout() → canRingNow()=true(允许重试)
|
||||
*/
|
||||
class SpiLinkState {
|
||||
public:
|
||||
/** 是否允许发起新按铃(ESP32 已完成上一帧 或 初始态)。 */
|
||||
bool canRingNow() const { return m_esp32Ready; }
|
||||
|
||||
/** 发起按铃时调用(消费完成 flag)。 */
|
||||
void onRingStart() { m_esp32Ready = false; }
|
||||
|
||||
/** ESP32 releaseReady 上升沿触发(EXTI6 ISR 调用)。 */
|
||||
void onEsp32Complete() { m_esp32Ready = true; }
|
||||
|
||||
/** 按铃超时取消时调用(允许重试,不依赖 EXTI)。 */
|
||||
void onRingTimeout() { m_esp32Ready = true; }
|
||||
|
||||
/** 初始化(首帧允许)。 */
|
||||
void reset() { m_esp32Ready = true; }
|
||||
|
||||
private:
|
||||
volatile bool m_esp32Ready = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief APP 运行期 SPI 链路与命令路由模块。
|
||||
*
|
||||
@@ -38,6 +71,9 @@ public:
|
||||
/** @brief CS 边沿中断处理入口。 */
|
||||
void onCsExtiInterrupt();
|
||||
|
||||
/** @brief SPI Ready(PB6) EXTI 上升沿中断处理入口(ESP32 完成信号)。 */
|
||||
void onSpiReadyExti();
|
||||
|
||||
/**
|
||||
* @brief 注册一个 SPI 命令处理器,用于 TYPE 路由。
|
||||
* @param[in] handler 命令处理器指针,调用方需保证其生命周期有效。
|
||||
@@ -84,6 +120,11 @@ public:
|
||||
/** @brief 查询当前是否有未读按铃(ESP32 尚未来读)。 */
|
||||
bool isRingPending() const { return m_ringPending; }
|
||||
|
||||
/** @brief 查询 ESP32 是否已完成上一帧处理(SPIready 上升沿触发)。
|
||||
* 初始为 true 允许第一次按铃;每次 requestEsp32Read 后置 false,
|
||||
* ESP32 releaseReady 产生上升沿后由 EXTI 置 true。 */
|
||||
bool canRingNow() const { return m_linkState.canRingNow(); }
|
||||
|
||||
static constexpr uint16_t kRxBufSize = 256U;
|
||||
static constexpr uint16_t kTxBufSize = 256U;
|
||||
static constexpr uint16_t kLogBufSize = 256U;
|
||||
@@ -112,10 +153,14 @@ private:
|
||||
/** 按铃期间暂存的请求帧长度(供 onCsAssert 装入 DMA)。 */
|
||||
volatile uint16_t m_ringTxLen = 0U;
|
||||
|
||||
/** SPI 链路状态机:封装 ESP32 完成信号。 */
|
||||
SpiLinkState m_linkState;
|
||||
|
||||
void configureGpio();
|
||||
void configureSpi0();
|
||||
void configureDma();
|
||||
void configureExti();
|
||||
void configureSpiReadyExti();
|
||||
void onCsAssert();
|
||||
void onCsRelease();
|
||||
void processCommand();
|
||||
@@ -138,9 +183,15 @@ private:
|
||||
* 主动轮询。每次调用推进一帧:若上一帧尚未被 ESP32 读走则立即返回,
|
||||
* 否则构建当前 (channel, page) 的反馈 payload 并 requestEsp32Read() 推送,
|
||||
* 然后推进到下一页/下一通道。
|
||||
*
|
||||
* 1Hz 节流由 taskHeartbeat() 调用 canFeedbackStartRound() 激活每秒一轮,
|
||||
* 推完一轮后内部清除激活标志,等待下一次 taskHeartbeat() 触发。
|
||||
*/
|
||||
void canFeedbackPushStep();
|
||||
|
||||
/** @brief 激活新一轮 CAN 反馈推送(由 1Hz taskHeartbeat 调用)。 */
|
||||
void canFeedbackStartRound();
|
||||
|
||||
/** @brief 全局 SPI 命令总线实例。 */
|
||||
extern AppSpiCommand AppSpiCommandBus;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user