2026-07-06 19:13:56 +08:00
|
|
|
|
#ifndef DEVICE_APP_SPI_COMMAND_H
|
2026-05-30 00:24:31 +08:00
|
|
|
|
#define DEVICE_APP_SPI_COMMAND_H
|
|
|
|
|
|
|
2026-06-23 01:49:33 +08:00
|
|
|
|
#include <cstdint>
|
2026-07-16 23:00:52 +08:00
|
|
|
|
#include "sys/config.h"
|
2026-06-23 01:49:33 +08:00
|
|
|
|
|
|
|
|
|
|
class ISpiCommandHandler;
|
2026-05-30 00:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
2026-06-23 01:49:33 +08:00
|
|
|
|
* @brief APP 运行期 SPI 链路与命令路由模块。
|
2026-05-30 00:24:31 +08:00
|
|
|
|
*
|
2026-06-23 01:49:33 +08:00
|
|
|
|
* 本类现在主要承担两件事:
|
|
|
|
|
|
* - 维护 SPI0 从机的硬件链路(GPIO/SPI/DMA/EXTI)
|
|
|
|
|
|
* - 将接收到的合法帧按 TYPE 注册式分发给 ISpiCommandHandler
|
2026-05-30 00:24:31 +08:00
|
|
|
|
*
|
2026-07-08 02:51:24 +08:00
|
|
|
|
* 具体命令处理逻辑不再放在这里,而是拆到 device/spi/app_spi_cmd_*.cpp,
|
|
|
|
|
|
* 通过 registerHandler() 注册进来,保持本模块可扩展。
|
|
|
|
|
|
*
|
2026-07-13 03:10:37 +08:00
|
|
|
|
* 握手架构(v2 DATA_READY,PB6<->ESP32 GPIO10 开漏单线):
|
|
|
|
|
|
* - PB6 低 = 有待发数据(m_txLen>0 或 m_ringPending),高 = 队列空。
|
|
|
|
|
|
* - ESP32 主动发起:CS 发请求,CS 释放后 GD32 在 onCsRelease() ISR 内
|
|
|
|
|
|
* 完成帧处理+响应装载,由 updateDataReady() 据 m_txLen 驱动 PB6。
|
|
|
|
|
|
* - GD32 主动按铃:requestEsp32Read() 设 m_ringPending=true 且 m_txLen>0,
|
|
|
|
|
|
* updateDataReady() 拉低 PB6,ESP32 检测下降沿后发起读事务读走请求帧。
|
2026-07-08 02:51:24 +08:00
|
|
|
|
* - PB6 配置为开漏输出,setSpiReady(true)=高阻释放,setSpiReady(false)=下拉。
|
2026-07-13 03:10:37 +08:00
|
|
|
|
* - ESP32 不再操作 PB6,EXTI6 上升沿仅清标志,无业务含义。
|
2026-05-30 00:24:31 +08:00
|
|
|
|
*/
|
|
|
|
|
|
class AppSpiCommand {
|
|
|
|
|
|
public:
|
2026-06-23 01:49:33 +08:00
|
|
|
|
/** @brief 初始化 SPI0 从机命令通道并注册默认命令处理器。 */
|
|
|
|
|
|
void init();
|
2026-05-30 00:24:31 +08:00
|
|
|
|
|
2026-07-08 02:51:24 +08:00
|
|
|
|
/** @brief 主循环轮询:打印延迟日志 + GD32 主动按铃超时看护。 */
|
2026-06-23 01:49:33 +08:00
|
|
|
|
void poll();
|
2026-05-30 00:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
/** @brief SPI0 中断处理入口。 */
|
2026-06-23 01:49:33 +08:00
|
|
|
|
void onInterrupt();
|
2026-05-30 00:24:31 +08:00
|
|
|
|
|
|
|
|
|
|
/** @brief CS 边沿中断处理入口。 */
|
2026-06-23 01:49:33 +08:00
|
|
|
|
void onCsExtiInterrupt();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @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);
|
2026-05-30 00:24:31 +08:00
|
|
|
|
|
2026-07-08 02:51:24 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @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; }
|
|
|
|
|
|
|
2026-07-13 03:10:37 +08:00
|
|
|
|
/** @brief 取消未完成的按铃(释放 SPIready、清 m_ringPending)。
|
|
|
|
|
|
* 0xB0 进 BOOT 等路径需公开调用,避免 0xC1 残留盖住 ACK。 */
|
|
|
|
|
|
void cancelRing();
|
|
|
|
|
|
|
2026-07-16 23:00:52 +08:00
|
|
|
|
static constexpr uint16_t kRxBufSize = CONFIG_SPI_RX_BUF_SIZE;
|
|
|
|
|
|
static constexpr uint16_t kTxBufSize = CONFIG_SPI_TX_BUF_SIZE;
|
|
|
|
|
|
static constexpr uint16_t kLogBufSize = CONFIG_SPI_LOG_BUF_SIZE;
|
2026-05-30 00:24:31 +08:00
|
|
|
|
|
2026-06-23 01:49:33 +08:00
|
|
|
|
private:
|
2026-07-16 23:00:52 +08:00
|
|
|
|
static constexpr uint8_t kMaxCommands = CONFIG_SPI_MAX_COMMANDS;
|
|
|
|
|
|
/** GD32 主动按铃超时(ms):见 CONFIG_SPI_RING_TIMEOUT_MS。 */
|
|
|
|
|
|
static constexpr uint32_t kRingTimeoutMs = CONFIG_SPI_RING_TIMEOUT_MS;
|
2026-05-30 00:24:31 +08:00
|
|
|
|
|
2026-06-23 01:49:33 +08:00
|
|
|
|
volatile bool m_csActive = false;
|
2026-07-15 00:34:56 +08:00
|
|
|
|
/** 本笔 CS 实际收到的字节数(CS 上升沿时由 RX DMA CHCNT 反算得到)。 */
|
2026-07-08 02:51:24 +08:00
|
|
|
|
volatile uint16_t m_rxLen = 0U;
|
2026-07-15 02:49:37 +08:00
|
|
|
|
volatile uint16_t m_txLen = 0U;
|
2026-06-23 01:49:33 +08:00
|
|
|
|
volatile bool m_logPending = false;
|
2026-07-15 00:34:56 +08:00
|
|
|
|
/** RX DMA 接收缓冲(替代旧 RBNE 逐字节累积的 m_rxBuf)。
|
|
|
|
|
|
* CS 上升沿时 m_rxLen 为本笔实际收到的字节数,本缓冲前 m_rxLen 字节有效。 */
|
2026-06-23 01:49:33 +08:00
|
|
|
|
uint8_t m_rxBuf[kRxBufSize] = {};
|
|
|
|
|
|
uint8_t m_txBuf[kTxBufSize] = {};
|
|
|
|
|
|
char m_logBuf[kLogBufSize] = {};
|
|
|
|
|
|
uint8_t m_commandCount = 0U;
|
|
|
|
|
|
ISpiCommandHandler *m_commands[kMaxCommands] = {};
|
|
|
|
|
|
|
2026-07-08 02:51:24 +08:00
|
|
|
|
/** GD32 主动按铃状态:true 表示已下拉 SPIready 等待 ESP32 来读。 */
|
|
|
|
|
|
volatile bool m_ringPending = false;
|
|
|
|
|
|
/** 按铃超时截止时刻(ms,基于 SysTickTimer::getMs)。 */
|
|
|
|
|
|
volatile uint32_t m_ringDeadline = 0U;
|
2026-07-09 02:47:22 +08:00
|
|
|
|
|
2026-06-23 01:49:33 +08:00
|
|
|
|
void configureGpio();
|
|
|
|
|
|
void configureSpi0();
|
|
|
|
|
|
void configureDma();
|
|
|
|
|
|
void configureExti();
|
|
|
|
|
|
void onCsAssert();
|
|
|
|
|
|
void onCsRelease();
|
|
|
|
|
|
void processCommand();
|
2026-07-06 19:13:56 +08:00
|
|
|
|
|
2026-07-16 23:00:52 +08:00
|
|
|
|
/** @brief 响应装载完成后的统一收尾:按铃 + updateDataReady。 */
|
|
|
|
|
|
void finishResponseAndReady();
|
|
|
|
|
|
|
|
|
|
|
|
/** @brief 设置 m_ringPending=true 并启动超时看护截止时刻。 */
|
2026-07-15 02:49:37 +08:00
|
|
|
|
void armRingPending();
|
|
|
|
|
|
|
2026-07-15 00:34:56 +08:00
|
|
|
|
/** @brief 在 CS 空闲期预装下一笔全双工 DMA(TX + RX)。
|
|
|
|
|
|
* 必须在首个 SCK 之前完成,否则 EXTI CS 下降相对 ESP32 时钟偏晚,
|
|
|
|
|
|
* 整笔事务会收 0 字节。对齐 BOOT SpiOta::armSpiDmasForNextCs。 */
|
|
|
|
|
|
void armSpiDmasForNextCs();
|
|
|
|
|
|
|
2026-07-08 02:51:24 +08:00
|
|
|
|
/** @brief 翻转 SPI Ready 握手引脚(PB6)电平(开漏线与)。
|
|
|
|
|
|
* @param[in] ready true=释放(高阻,靠上拉拉高), false=下拉(驱动低)。
|
|
|
|
|
|
* @note GPIO BRR 写入原子,无需临界区;可由 ISR 与主循环共同调用。
|
|
|
|
|
|
* 开漏模式下 setSpiReady(true) 仅写 BRR 不驱动高,由外部上拉拉高。
|
2026-07-06 19:13:56 +08:00
|
|
|
|
*/
|
|
|
|
|
|
void setSpiReady(bool ready);
|
2026-07-08 02:51:24 +08:00
|
|
|
|
|
2026-07-13 03:10:37 +08:00
|
|
|
|
/** @brief v2 DATA_READY 语义:依据待发数据驱动 PB6。
|
|
|
|
|
|
* PB6 低 = 有待发数据(m_txLen>0 或 m_ringPending),高 = 队列空。
|
|
|
|
|
|
* 替代旧 BUSY/READY 语义中所有 setSpiReady 调用。 */
|
|
|
|
|
|
void updateDataReady();
|
2026-05-30 00:24:31 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-08 02:51:24 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief CAN 反馈主动推送一步(非阻塞,由主循环每次迭代调用)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* GD32 APP 主动循环推送 3 通道 × 2 页 CAN 反馈帧到 ESP32,替代 ESP32
|
|
|
|
|
|
* 主动轮询。每次调用推进一帧:若上一帧尚未被 ESP32 读走则立即返回,
|
|
|
|
|
|
* 否则构建当前 (channel, page) 的反馈 payload 并 requestEsp32Read() 推送,
|
|
|
|
|
|
* 然后推进到下一页/下一通道。
|
2026-07-09 02:47:22 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 1Hz 节流由 taskHeartbeat() 调用 canFeedbackStartRound() 激活每秒一轮,
|
|
|
|
|
|
* 推完一轮后内部清除激活标志,等待下一次 taskHeartbeat() 触发。
|
2026-07-08 02:51:24 +08:00
|
|
|
|
*/
|
|
|
|
|
|
void canFeedbackPushStep();
|
|
|
|
|
|
|
2026-07-09 02:47:22 +08:00
|
|
|
|
/** @brief 激活新一轮 CAN 反馈推送(由 1Hz taskHeartbeat 调用)。 */
|
|
|
|
|
|
void canFeedbackStartRound();
|
|
|
|
|
|
|
2026-07-20 02:36:56 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief ADC 调试反馈(0xC4)主动推送一步。
|
|
|
|
|
|
* @note CONFIG_SPI_ADC_FEEDBACK_ENABLE=0 时为空操作。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void adcFeedbackPushStep();
|
|
|
|
|
|
|
|
|
|
|
|
/** @brief 激活一次 ADC 调试反馈推送(1Hz taskHeartbeat)。 */
|
|
|
|
|
|
void adcFeedbackStartRound();
|
|
|
|
|
|
|
2026-07-26 20:39:59 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 多电机状态(0xC5)主动推送一步。
|
|
|
|
|
|
* @note CONFIG_MOTOR_STATE_ENABLE=0 时为空操作。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void motorStatePushStep();
|
|
|
|
|
|
|
|
|
|
|
|
/** @brief 激活新一轮多电机状态推送(1Hz taskHeartbeat)。 */
|
|
|
|
|
|
void motorStateStartRound();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 多电机版本(0xC6)事件驱动推送步进:有 dirty 版本且 ring 空闲时按铃推一帧。
|
|
|
|
|
|
* @note CONFIG_MOTOR_VERSION_ENABLE=0 时为空操作。优先级低于 0xC1/0xC5/0xC4。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void motorVersionPushStep();
|
|
|
|
|
|
|
2026-06-23 01:49:33 +08:00
|
|
|
|
/** @brief 全局 SPI 命令总线实例。 */
|
|
|
|
|
|
extern AppSpiCommand AppSpiCommandBus;
|
|
|
|
|
|
|
2026-07-20 02:36:56 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 填充 APP 身份 16B(版本 + 功能开关),供 HELLO_ACK 与 PING 共用。
|
|
|
|
|
|
* @param out 至少 V2_APP_IDENTITY_LEN 字节。
|
|
|
|
|
|
*/
|
|
|
|
|
|
void fillGd32AppIdentityPayload(uint8_t *out);
|
|
|
|
|
|
|
2026-05-30 20:03:31 +08:00
|
|
|
|
#endif /* DEVICE_APP_SPI_COMMAND_H */
|