Files
DCU_APP/GD32E508_DCU_APP/device/spi/app_spi_command.h
T
cbb a52d94687b refactor(SPI): 实现全双工DMA传输以提升通信可靠性
- 配置DMA0_CH1用于SPI0_RX接收,替代原有的RBNE中断逐字节处理
- 添加startSpiRxDma、stopSpiDmas、drainSpiRxHw等DMA控制函数
- 在CS空闲期预装下一笔DMA传输,避免EXTI CS下降沿相对于SCK偏晚导致收0字节
- 与BOOT工程SpiOta的DMA配置对齐,确保通信稳定性

refactor(SPI): 优化CS边沿处理逻辑和命令解析时机

- 移除原有的EXTI5_9_IRQHandler用于SPI Ready引脚处理
- 将命令处理从异步任务调度改为在CS释放中断中同步完成
- 重构onCsAssert和onCsRelease逻辑,简化DMA启停流程
- 删除TASK_SPI_CMD_PROCESS任务类型,减少不必要的任务调度

feat(CANFD): 修改transmitFrame返回类型为枚举值以提供详细错误信息

- 将CanFd::transmitFrame返回类型从bool改为CanTxResult枚举
- 新增CanTxResult枚举定义,包含kOk、kErrBusOff、kErrTecHigh、kErrMailbox、kErrInvalid
- 更新SPI桥接命令处理中对transmitFrame返回值的处理逻辑
- 枚举值设计与SPI错误响应码对齐,便于直接透传错误信息

refactor(SPI): 实现全双工DMA传输以提升通信可靠性

- 配置DMA0_CH1用于SPI0_RX接收,替代原有的RBNE中断逐字节处理
- 添加startSpiRxDma、stopSpiDmas、drainSpiRxHw等DMA控制函数
- 在CS空闲期预装下一笔DMA传输,避免EXTI CS下降沿相对于SCK偏晚导致收0字节
- 与BOOT工程SpiOta的DMA配置对齐,确保通信稳定性

refactor(SPI): 优化CS边沿处理逻辑和命令解析时机

- 移除原有的EXTI5_9_IRQHandler用于SPI Ready引脚处理
- 将命令处理从异步任务调度改为在CS释放中断中同步完成
- 重构onCsAssert和onCsRelease逻辑,简化DMA启停流程
- 删除TASK_SPI_CMD_PROCESS任务类型,减少不必要的任务调度

feat(OTA): 添加BOOT版本号读取功能并优化版本信息获取

- 在ota_boot_request模块中添加readBootVersion函数
- 从OTA配置页Flash读取BOOT版本号信息
- 在SPI PING命令响应中使用新的版本读取接口
- 统一版本号获取方式,避免裸指针访问Flash地址

refactor(task): 调整诊断打印任务调度周期为5秒间隔

- 新增div_diag_print调度分频计数器用于诊断打印
- 将诊断打印任务从心跳任务中分离,独立控制执行周期
- 修改诊断打印函数注释,明确其由5秒节拍触发
- 优化任务调度逻辑,提高系统运行效率
2026-07-15 00:34:56 +08:00

214 lines
9.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 链路状态机:封装按铃状态。
*
* v2 DATA_READY 语义:PB6 低 = 有待发数据,高 = 队列空。
* ESP32 不再操作 PB6,无独立「完成」信号;按铃完成由 ESP32
* 的 CS 读事务本身确认(onCsRelease 按铃响应路径调 onEsp32Complete)。
*
* canRingNow() 总是返回 true,由 requestEsp32Read() 的 m_ringPending
* 检查控制重复按铃。
*/
class SpiLinkState {
public:
/** 是否允许发起新按铃。
* v2 DATA_READY: ESP32 不再操作 PB6,无独立完成信号;
* 总是允许,由 requestEsp32Read() 的 m_ringPending 检查控制重复。 */
bool canRingNow() const { return true; }
/** 发起按铃时调用(v2 下仅保留兼容写法,无实际门禁作用)。 */
void onRingStart() { m_esp32Ready = false; }
/** ESP32 CS 读事务完成确认(onCsRelease 按铃响应路径调用)。 */
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() 注册进来,保持本模块可扩展。
*
* 握手架构(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 L4 别名:主动事件入队(当前等同 requestEsp32Read)。 */
bool enqueueEvent(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len) {
return requestEsp32Read(frame_type, payload, payload_len);
}
/** @brief 查询当前是否有未读按铃(ESP32 尚未来读)。 */
bool isRingPending() const { return m_ringPending; }
/** @brief 取消未完成的按铃(释放 SPIready、清 m_ringPending)。
* 0xB0 进 BOOT 等路径需公开调用,避免 0xC1 残留盖住 ACK。 */
void cancelRing();
/** @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;
/** 本笔 CS 实际收到的字节数(CS 上升沿时由 RX DMA CHCNT 反算得到)。 */
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;
/** 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;
/** 按铃期间暂存的请求帧长度(供 onCsAssert 装入 DMA)。 */
volatile uint16_t m_ringTxLen = 0U;
/** SPI 链路状态机:封装 ESP32 完成信号。 */
SpiLinkState m_linkState;
void configureGpio();
void configureSpi0();
void configureDma();
void configureExti();
void onCsAssert();
void onCsRelease();
void processCommand();
/** @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();
};
/**
* @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 */