Files
DCU_APP/GD32E508_DCU_APP/device/spi/app_spi_command.h
T
cbb 28d8426052 feat(ethercat): 添加ESP32使能控制功能通过EtherCAT SDO
添加0x2503对象字典项用于通过EtherCAT SDO控制ESP32 WiFi使能信号,
解决WiFi关断后无法重新开启的问题。同时添加sys/esp32_en.h/.cpp文件
提供GPIO控制接口。

SPI诊断命令(0xA2)扩展支持ESP32 EN引脚控制,用于诊断备援。
ESP32掉电重启后自动清理SPI链路状态,防止通信异常。
2026-09-24 03:18:18 +08:00

181 lines
7.9 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>
#include "sys/config.h"
class ISpiCommandHandler;
/**
* @brief APP 运行期 SPI 链路与命令路由模块。
*
* 本类现在主要承担两件事:
* - 维护 SPI0 从机的硬件链路(GPIO/SPI/DMA/EXTI)
* - 将接收到的合法帧按 TYPE 注册式分发给 ISpiCommandHandler
*
* 具体命令处理逻辑不再放在这里,而是拆到 device/spi/app_spi_cmd_*.cpp,
* 通过 registerHandler() 注册进来,保持本模块可扩展。
*
* 握手架构(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 检测下降沿后发起读事务读走请求帧。
* - PB6 配置为开漏输出,setSpiReady(true)=高阻释放,setSpiReady(false)=下拉。
* - ESP32 不再操作 PB6,EXTI6 上升沿仅清标志,无业务含义。
*/
class AppSpiCommand {
public:
/** @brief 初始化 SPI0 从机命令通道并注册默认命令处理器。 */
void init();
/** @brief 主循环轮询:打印延迟日志 + GD32 主动按铃超时看护。 */
void poll();
/** @brief SPI0 中断处理入口。 */
void onInterrupt();
/** @brief CS 边沿中断处理入口。 */
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);
/**
* @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 取消未完成的按铃(释放 SPIready、清 m_ringPending)。
* 0xB0 进 BOOT 等路径需公开调用,避免 0xC1 残留盖住 ACK。 */
void cancelRing();
/**
* @brief ESP32 掉电/重上电后复位 SPI 链路(PC9 翻转时调用)。
*
* ESP32 被 EN 拉低时可能死在半笔 CS 事务里:session/序号/按铃/DMA 都会脏。
* 重上电 HELLO 前必须清干净,否则首帧对不齐 → 永远建不起 v2 会话。
* 清 session(HELLO 会重建)、取消按铃、re-arm DMA、释放 DATA_READY。
*/
void resetEsp32Link();
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;
private:
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;
volatile bool m_csActive = false;
/** 本笔 CS 实际收到的字节数(CS 上升沿时由 RX DMA CHCNT 反算得到)。 */
volatile uint16_t m_rxLen = 0U;
volatile uint16_t m_txLen = 0U;
volatile bool m_logPending = false;
/** RX DMA 接收缓冲(替代旧 RBNE 逐字节累积的 m_rxBuf)。
* CS 上升沿时 m_rxLen 为本笔实际收到的字节数,本缓冲前 m_rxLen 字节有效。 */
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;
void configureGpio();
void configureSpi0();
void configureDma();
void configureExti();
void onCsAssert();
void onCsRelease();
void processCommand();
/** @brief 响应装载完成后的统一收尾:按铃 + updateDataReady。 */
void finishResponseAndReady();
/** @brief 设置 m_ringPending=true 并启动超时看护截止时刻。 */
void armRingPending();
/** @brief 在 CS 空闲期预装下一笔全双工 DMA(TX + RX)。
* 必须在首个 SCK 之前完成,否则 EXTI CS 下降相对 ESP32 时钟偏晚,
* 整笔事务会收 0 字节。对齐 BOOT SpiOta::armSpiDmasForNextCs。 */
void armSpiDmasForNextCs();
/** @brief 翻转 SPI Ready 握手引脚(PB6)电平(开漏线与)。
* @param[in] ready true=释放(高阻,靠上拉拉高), false=下拉(驱动低)。
* @note GPIO BRR 写入原子,无需临界区;可由 ISR 与主循环共同调用。
* 开漏模式下 setSpiReady(true) 仅写 BRR 不驱动高,由外部上拉拉高。
*/
void setSpiReady(bool ready);
/** @brief v2 DATA_READY 语义:依据待发数据驱动 PB6。
* PB6 低 = 有待发数据(m_txLen>0 或 m_ringPending),高 = 队列空。
* 替代旧 BUSY/READY 语义中所有 setSpiReady 调用。 */
void updateDataReady();
};
/* 2026-08 请求驱动改造:0xC1 CAN 反馈 / 0xC5 电机状态 / 0xC4 ADC 已改
* ESP32 周期查询(本端仅被动响应 handler),主动按铃推送函数整体移除
* (canFeedbackPushStep/canFeedbackStartRound/adcFeedbackPushStep/
* adcFeedbackStartRound/motorStatePushStep/motorStateStartRound)。 */
/**
* @brief 多电机版本(0xC6)事件驱动推送步进:有 dirty 版本且 ring 空闲时按铃推一帧。
* @note CONFIG_MOTOR_VERSION_ENABLE=0 时为空操作。低频、事件驱动,保留推送。
*/
void motorVersionPushStep();
/** @brief 全局 SPI 命令总线实例。 */
extern AppSpiCommand AppSpiCommandBus;
/**
* @brief 填充 APP 身份 16B(版本 + 功能开关),供 HELLO_ACK 与 PING 共用。
* @param out 至少 V2_APP_IDENTITY_LEN 字节。
*/
void fillGd32AppIdentityPayload(uint8_t *out);
#endif /* DEVICE_APP_SPI_COMMAND_H */