Files
DCU_APP/GD32E508_DCU_APP/device/spi/app_spi_command.h
T
cbb 3691cf2aba 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通信稳定性。
2026-07-09 02:47:22 +08:00

199 lines
8.2 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.
#ifndef DEVICE_APP_SPI_COMMAND_H
#define DEVICE_APP_SPI_COMMAND_H
#include <cstdint>
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 链路与命令路由模块。
*
* 本类现在主要承担两件事:
* - 维护 SPI0 从机的硬件链路(GPIO/SPI/DMA/EXTI)
* - 将接收到的合法帧按 TYPE 注册式分发给 ISpiCommandHandler
*
* 具体命令处理逻辑不再放在这里,而是拆到 device/spi/app_spi_cmd_*.cpp,
* 通过 registerHandler() 注册进来,保持本模块可扩展。
*
* 握手架构(线与 SPIready,PB6<->ESP32 GPIO10 开漏单线):
* - 空闲态线 HIGH(双方释放,靠上拉)。
* - ESP32 主动发起:下拉 SPIready -> 立刻 CS 发请求 -> CS 释放后保持下拉,
* GD32 在 onCsRelease() ISR 内完成帧处理+响应装载后释放 SPIready(高),
* ESP32 检测到上升沿后发起 CS#2 读响应。
* - GD32 主动按铃:下拉 SPIready -> ESP32 下降沿中断唤醒后发起读事务,
* GD32 在 onCsRelease() 中装入请求帧并释放 SPIready。
* - PB6 配置为开漏输出,setSpiReady(true)=高阻释放,setSpiReady(false)=下拉。
*/
class AppSpiCommand {
public:
/** @brief 初始化 SPI0 从机命令通道并注册默认命令处理器。 */
void init();
/** @brief 主循环轮询:打印延迟日志 + GD32 主动按铃超时看护。 */
void poll();
/** @brief SPI0 中断处理入口。 */
void onInterrupt();
/** @brief CS 边沿中断处理入口。 */
void onCsExtiInterrupt();
/** @brief SPI Ready(PB6) EXTI 上升沿中断处理入口(ESP32 完成信号)。 */
void onSpiReadyExti();
/**
* @brief 注册一个 SPI 命令处理器,用于 TYPE 路由。
* @param[in] handler 命令处理器指针,调用方需保证其生命周期有效。
*/
void registerHandler(ISpiCommandHandler *handler);
/** @brief 记录一条待输出日志,主循环统一打印。 */
void postLog(const char *msg);
/** @brief 记录一条带 1 字节参数的待输出日志。 */
void postLogFmt(const char *fmt, uint8_t val);
/**
* @brief 将应答帧构建到指定发送缓冲区。
* @param[out] tx_buf 发送缓冲区。
* @param[in] tx_buf_size 发送缓冲区大小。
* @param[out] tx_len 构建完成后的帧长度。
* @param[in] rsp_type 响应类型。
* @param[in] payload 响应载荷。
* @param[in] payload_len 响应载荷长度。
*/
void 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);
/**
* @brief GD32 主动按铃:下拉 SPIready,通知 ESP32「我有请求帧要给你读」。
*
* 将提供的请求帧装入内部 TX 缓冲区(供下次 ESP32 读事务通过 DMA 输出到 MISO),
* 然后下拉 SPIready 线。ESP32 检测到下降沿后会发起一次 SPI 读事务把该帧读走。
* 调用方负责确保 frame_type 与 payload 符合协议帧格式。
*
* @param[in] frame_type 请求帧类型(复用 SPI 协议帧 TYPE)。
* @param[in] payload 请求帧载荷,可为 nullptr。
* @param[in] payload_len 载荷长度。
* @return true 成功下拉并装入 TX;false 当前已有未读按铃或帧构建失败。
*
* @note 可在主循环或 ISR 中调用。若上一次按铃尚未被 ESP32 读走(m_ringPending),
* 则返回 false,调用方应稍后重试。按铃后启动 m_ringDeadline 超时看护,
* 超时则放弃本次按铃(防止 ESP32 不响应时永久占用 SPIready 线)。
*/
bool requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len);
/** @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;
private:
static constexpr uint8_t kMaxCommands = 8U;
/** GD32 主动按铃超时(ms):ESP32 未在该时间内来读则放弃。 */
static constexpr uint32_t kRingTimeoutMs = 50U;
volatile bool m_csActive = false;
volatile uint16_t m_rxLen = 0U;
uint16_t m_txLen = 0U;
volatile uint8_t m_txPos = 0U;
volatile bool m_logPending = false;
bool m_txActiveThisCs = false;
uint8_t m_rxBuf[kRxBufSize] = {};
uint8_t m_txBuf[kTxBufSize] = {};
char m_logBuf[kLogBufSize] = {};
uint8_t m_commandCount = 0U;
ISpiCommandHandler *m_commands[kMaxCommands] = {};
/** GD32 主动按铃状态:true 表示已下拉 SPIready 等待 ESP32 来读。 */
volatile bool m_ringPending = false;
/** 按铃超时截止时刻(ms,基于 SysTickTimer::getMs)。 */
volatile uint32_t m_ringDeadline = 0U;
/** 按铃期间暂存的请求帧长度(供 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();
/** @brief 翻转 SPI Ready 握手引脚(PB6)电平(开漏线与)。
* @param[in] ready true=释放(高阻,靠上拉拉高), false=下拉(驱动低)。
* @note GPIO BRR 写入原子,无需临界区;可由 ISR 与主循环共同调用。
* 开漏模式下 setSpiReady(true) 仅写 BRR 不驱动高,由外部上拉拉高。
*/
void setSpiReady(bool ready);
/** @brief 取消未完成的按铃(释放 SPIready、清 m_ringPending)。 */
void cancelRing();
};
/**
* @brief CAN 反馈主动推送一步(非阻塞,由主循环每次迭代调用)。
*
* GD32 APP 主动循环推送 3 通道 × 2 页 CAN 反馈帧到 ESP32,替代 ESP32
* 主动轮询。每次调用推进一帧:若上一帧尚未被 ESP32 读走则立即返回,
* 否则构建当前 (channel, page) 的反馈 payload 并 requestEsp32Read() 推送,
* 然后推进到下一页/下一通道。
*
* 1Hz 节流由 taskHeartbeat() 调用 canFeedbackStartRound() 激活每秒一轮,
* 推完一轮后内部清除激活标志,等待下一次 taskHeartbeat() 触发。
*/
void canFeedbackPushStep();
/** @brief 激活新一轮 CAN 反馈推送(由 1Hz taskHeartbeat 调用)。 */
void canFeedbackStartRound();
/** @brief 全局 SPI 命令总线实例。 */
extern AppSpiCommand AppSpiCommandBus;
#endif /* DEVICE_APP_SPI_COMMAND_H */