#ifndef DEVICE_CANFD_H #define DEVICE_CANFD_H #include #include "sys/config.h" /** @brief 每通道反馈快照中保存的最近一帧最大数据长度(本地诊断用)。 */ #define CANFD_FEEDBACK_LAST_DATA_MAX 32U /** @brief 每通道不重复 ID 快照表最大条目数(CONFIG_CANFD_ID_SNAPSHOT_MAX)。 */ #define CANFD_ID_SNAPSHOT_MAX CONFIG_CANFD_ID_SNAPSHOT_MAX /** @brief 快照条目中最大数据字节数(CONFIG_CANFD_ID_SNAPSHOT_DATA_MAX)。 */ #define CANFD_ID_SNAPSHOT_DATA_MAX CONFIG_CANFD_ID_SNAPSHOT_DATA_MAX /** * @brief CANFD 通信模块。 * @note 负责 GPIO/模式控制、波特率配置、异步接收缓存与按需发送能力。 */ class CanFd { public: /** * @brief CANFD 接收过滤模式枚举。 * @note 默认模式为 kModeAllPass,即三路控制器关闭硬件 ID 过滤,全部接收。 */ enum class Mode : uint8_t { kModeAllPass = 0U, /**< 全部接收模式:禁用硬件 ID/掩码匹配,所有 11/29 位帧都进入 FIFO0。 */ kModeFilterId = 1U, /**< 过滤模式:按 CONFIG_CAN_RX_FILTER_ID/MASK 做标准帧掩码匹配。 */ kModeBridge0To12 = 2U, /**< CAN0 桥接模式:硬件过滤等同 AllPass,CAN0 收到的每一帧按原帧格式自动转发到 CAN1 与 CAN2。 */ }; /** * @brief CAN 发送结果枚举,枚举值即 SPI 错误响应码,便于 handler 直接透传。 * @note 0xE0/0xE1 保留给 handler 入口校验(payload 过短/通道非法), * 不经 transmitFrame 返回;0xE2~0xE5 由 transmitFrame 区分四种失败原因; * 0xE6=通道处于收发器 Silent(只听不发),在入口即拒绝,不计任何错误计数。 * 0xE7=通道被电机 OTA 独占锁定(EtherCAT/SPI 跨总线排他),同样不计错误计数。 */ enum class CanTxResult : uint8_t { kOk = 0x00U, /**< 发送成功,已进入硬件邮箱。 */ kErrBusOff = 0xE2U, /**< 通道处于 BUS_OFF,不发送。 */ kErrTecHigh = 0xE3U, /**< TEC > 96,暂停发送保护通道。 */ kErrMailbox = 0xE4U, /**< 3 个硬件邮箱全 PENDING,abort+重投仍失败。 */ kErrInvalid = 0xE5U, /**< 入口参数无效(channelIndex/dlc/data 等)。 */ kErrSilent = 0xE6U, /**< 通道收发器处于 Silent(驱动器关断),拒绝发送。 */ kErrOtaLocked = 0xE7U, /**< 通道被电机 OTA 独占(跨总线排他),拒绝发送,不计错误计数。 */ }; /** * @brief CANFD 运行诊断统计信息。 */ struct Diag { uint32_t tx_count; uint32_t rx_count; uint32_t error_count; uint32_t rx_drop_count; uint32_t rx_fifo_full_count; uint32_t rx_fifo_overfull_count; /** 取帧期间被硬件换帧、整帧丢弃的次数(1.6.6,TS/DLC 前后不一致即判定)。 * 这是"读到半条帧"的唯一可观测量,稳态下应当只在高负载时缓慢增长。 */ uint32_t rx_torn_count; /** 释放邮箱后等待 RFD 硬件自清超时的次数(手册 26.3.4 第 3 步要求等待)。 */ uint32_t rx_release_timeout_count; /** 内容级拼接帧拦截次数(1.6.11):(id, data) 结构自相矛盾(STATUS 的 * 温度/电压出量程、CONFIG 的未使用区非 0)⇒ 整帧作废。与 rx_torn 互补: * torn 挡"取数期间窗口被换",这个挡"窗口没换但 FIFO 缓冲指针错位"。 */ uint32_t rx_content_reject_count; uint32_t tx_abort_count; uint32_t last_rx_id; uint32_t last_tx_id; uint8_t last_rx_channel; uint8_t last_tx_channel; uint8_t last_payload_length; uint8_t last_rx_fd_flag; uint8_t last_rx_brs; uint8_t last_rx_esi; }; /** * @brief 单通道运行计数(每路独立累计)。 */ struct ChannelStats { uint32_t rx_count; /**< 该通道累计成功接收帧数。 */ uint32_t tx_count; /**< 该通道累计由硬件 MTFNERR 确认的成功发送帧数。 */ uint32_t rx_drop_count; /**< 该通道累计软件丢帧数。 */ uint32_t error_count; /**< 该通道累计错误计数(与发送邮箱失败合并)。 */ uint32_t last_rx_id; /**< 该通道最近一帧报文标识符。 */ uint8_t last_rx_dlc; /**< 该通道最近一帧报文数据长度。 */ uint8_t last_rx_fd_flag; /**< 该通道最近一帧 FD 帧标志。 */ uint8_t last_rx_brs; /**< 该通道最近一帧 BRS 标志。 */ uint8_t last_rx_esi; /**< 该通道最近一帧 ESI 标志。 */ uint8_t last_rx_data[CANFD_FEEDBACK_LAST_DATA_MAX]; uint8_t tec; /**< 最近一次采样时硬件发送错误计数器 (0~255)。 */ uint8_t rec; /**< 最近一次采样时硬件接收错误计数器 (0~255)。 */ uint8_t bus_state; /**< 总线状态位图:bit0=init_mode, bit1=sleep, bit2=error_passive, bit3=bus_off, bit4=error_warning, bit5=tx_silent(收发器 Silent,只听不发)。bit0~4 是控制器/总线侧状态,bit5 是本端 S 脚状态。 */ }; /** * @brief 单通道 1Hz 反馈快照(每秒刷新一次)。 */ struct ChannelFeedback { uint32_t snapshot_seq; /**< 第几次刷新(自启动起递增)。 */ ChannelStats stats; /**< 抓拍时刻的该通道统计与最近一帧信息。 */ }; /** * @brief 单个不重复 ID 的最新帧快照。 */ struct IdSnapshotEntry { uint32_t id; /**< CAN 报文标识符。 */ uint32_t seq; /**< 该条目被更新的序号(自启动起递增)。 */ uint32_t rx_count; /**< 该 ID 累计接收帧数。 */ uint8_t dlc; /**< 最新一帧数据长度。 */ uint8_t fd_flag; /**< 最新一帧 FD 标志。 */ uint8_t brs; /**< 最新一帧 BRS 标志。 */ uint8_t esi; /**< 最新一帧 ESI 标志。 */ uint8_t data[CANFD_ID_SNAPSHOT_DATA_MAX]; /**< 最新一帧数据。 */ bool occupied; /**< 条目是否已被占用。 */ }; /** * @brief 每通道不重复 ID 快照表(1Hz 刷新,SPI 命令分页拉取)。 */ struct IdSnapshotTable { IdSnapshotEntry entries[CANFD_ID_SNAPSHOT_MAX]; uint8_t count; /**< 当前占用条目数。 */ uint32_t seq; /**< 全局更新序号(每次 refresh 时递增)。 */ }; static void init(); /** L0 自检故障原因码(selfTestL0 输出;经 0xA3 fault_codes[] 上报,高 3 位带通道号见 initial.h) */ enum SelfTestFault : uint8_t { kSelfTestOk = 0U, /**< 通过 */ kSelfTestHwInit = 1U, /**< 控制器初始化失败(重试 5 次后仍不成功) */ kSelfTestStuckInit = 2U, /**< 卡在初始化模式(IWS 未清零) */ kSelfTestStbLevel = 3U, /**< 收发器 STB 输出未拉低(配置未生效) */ }; /** * @brief L0 轻量自检(不向物理总线发帧),仅校验 GD32 侧控制器自身状态。 * @param[out] fault_code 首个失败通道的 SelfTestFault 原因码;全部通过时为 kSelfTestOk。允许空。 * @param[out] fault_channel 首个失败通道号 0/1/2;全部通过时无意义。允许空。 * @retval true 所有已使能通道通过;false 无使能通道或任一路失败。 * @note 检查:硬件 init 结果、退出 init mode、收发器 STB 已置 Normal。 * 1.5.35 移除 BUS_OFF 与 TEC/REC 检查:它们是总线侧状态, * 通道未接从站(RX 浮空噪声)时会误报——「没接设备」不等于错误; * 总线健康经 0xC1 反馈帧(bus_state/TEC/REC)运行期上报,网页 CAN 页可见。 */ static bool selfTestL0(uint8_t *fault_code = nullptr, uint8_t *fault_channel = nullptr); /** * @brief 查询指定通道是否已使能。 * @param[in] channelIndex 通道索引 0/1/2。 * @retval bool true=使能,false=禁用。 */ static bool isChannelEnabled(uint8_t channelIndex); /** * @brief 切换 CANFD 接收过滤模式。 */ static void setMode(Mode mode); /** @brief 查询当前 CANFD 接收过滤模式。 */ static Mode mode(); static bool hasRxFrame(); static bool consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel = nullptr); /** * @brief 直接向指定 CAN 通道发送一帧(绕过软件队列)。 * @param allow_abort 邮箱全占时是否允许 abort 一个 PENDING 邮箱并重投一次。 * - true(底层非阻塞顶替):适合最新单发覆盖场景;高总线负载下慎用(可能 abort 掉正在等仲裁的帧); * - false(突发/队列路径):邮箱满直接返回 kErrMailbox,交由 enqueueTx * 的软件队列缓存,由 TX 完成中断回灌——避免 abort 把同通道已装载的 * 旧帧静默丢弃(>3 帧同通道突发只剩最后 3 帧上线的根因)。 */ static CanTxResult transmitFrame(uint8_t channelIndex, uint32_t ext_id, const uint8_t *data, uint8_t dlc, bool periodic = false, bool allow_abort = true); /** @brief 立即停掉指定通道所有 PENDING 发送邮箱(阻止 auto_retrans 残留重发,供停等式协议用)。 */ static void abortPendingTx(uint8_t channelIndex); /** @brief 运行时切换指定通道 CAN 自动重发(ARD 位,需 init-mode);停等式 OTA 期间关闭、结束后恢复。 */ static void setChannelAutoRetrans(uint8_t channelIndex, bool enable); /** @brief 运行时压制指定通道 TX 帧的 BRS:压制中 FD 帧数据段回退仲裁段速率。 * 电机 BOOT 无法正确接收 BRS 高速段(见 sys/config.h CONFIG_CAN_FD_BRS_ENABLE 注释), * 电机 OTA relay 经 enterState 钩子与 setChannelAutoRetrans 同点切换,OTA 结束必恢复。 */ static void setChannelBrsSuppress(uint8_t channelIndex, bool suppress); /** @brief 切换指定通道收发器(TCAN1057AV)的 Silent 模式:只听不发。 * S 脚(CAN0=PA8/CAN1=PB14/CAN2=PB9)置高 → 驱动器关断、**接收机照常工作** * (手册 Table 8-4:RXD 仍镜像总线),所以静默期间接收与上报完全不受影响; * 置低 → 回 Normal。这是硬件级禁止发送,S 脚内部上拉,MCU 复位/浮空即为 Silent。 * @note 进 Silent 前先 abort 本通道 PENDING 邮箱:驱动器已关,残留邮箱只会让 * 控制器因收不到 ACK 而白涨 TEC。 * 退 Silent 时忙等收发器模式建立时间 tMODE=20µs 再返回,否则紧跟的第一帧 * 驱动不上线。用 DWT CYCCNT 忙等,可在 CS EXTI ISR 上下文调用 * (SysTickTimer::delayMs 在该上下文会死等)。 * 与 canfd.cpp 内 s_chanQuiet(空通道上报节流)无关,也别混为一谈。 * 静默不持久化:上电 init 一律回 Normal。 * @retval true 通道合法且已生效;false 通道非法。 */ static bool setTxSilent(uint8_t channelIndex, bool silent); /** @brief 查询指定通道是否处于收发器 Silent(唯一真源 s_txSilent,0xC1 bus_state bit5 用它)。 */ static bool isTxSilent(uint8_t channelIndex); /** @brief 读取指定通道 CAN_ERR 寄存器原始值(事件时刻只读快照,不依赖 1Hz 采样)。 * 解码:TEC=bit16-23,REC=bit24-31,ERRN=bit4-6;无效通道返回 0。仅供诊断日志,禁用于发送门控。 */ static uint32_t readChannelErrorReg(uint8_t channelIndex); /** @brief 读取并清除指定通道的 TX 错误事件(ARD=1 单发下 MTE 置位 = 帧已确认丢失)。 * 供停等式 OTA 在丢帧后立即重发,免等 ACK 超时;仅主循环调用。 */ static bool consumeTxError(uint8_t channelIndex); static const Diag *diag(); /** * @brief 刷新三路 1Hz 反馈快照。 * @note 通常由任务调度器在 1 秒节拍调用,将各通道实时计数与最近一帧信息抓拍到 s_feedback。 */ static void refreshFeedbackSnapshot(); /** * @brief 拷贝指定通道的最新反馈快照到调用方缓冲。 * @param[in] channelIndex 目标 CAN 通道号(0/1/2)。 * @param[out] out 目标快照缓冲;为空或通道非法时返回 false。 * @retval bool true 表示成功拷贝,false 表示参数非法。 */ static bool copyFeedbackSnapshot(uint8_t channelIndex, ChannelFeedback *out); /** * @brief 拷贝指定通道的不重复 ID 快照表。 * @param[in] channelIndex 目标通道 0/1/2。 * @param[out] out 输出缓冲。 * @retval true 成功。 */ static bool copyIdSnapshotTable(uint8_t channelIndex, IdSnapshotTable *out); static void handleRxInterrupt(uint8_t channelIndex); static void handleTxInterrupt(uint8_t channelIndex); /** * @brief 突发排入软件 TX 队列(供 SPI 0xC7 burst handler / 突发场景使用)。 * * 解决「3 个发送邮箱是物理上限、>3 帧突发必丢」的问题: * - 队列空时尝试直通 transmitFrame(不排队、零延迟)。 * - 邮箱满时把本帧追加到软件队列,TX 完成中断里持续出队回灌, * 保证任意大小突发(同通道 ≤ 队列深度)都不丢帧。 * * 同步:本函数可在 EXTI15 ISR 内运行(SPI burst handler),与 CAN TX ISR * 并发;队列 Push/Pop 通过关中段互斥,无需 LDREX/STREX。 * * @param[in] channelIndex 目标通道(0/1/2)。 * @param[in] ext_id 29 位扩展 ID。 * @param[in] data 数据负载;dlc>0 时必填。 * @param[in] dlc 数据长度 0..64。 * @retval CanTxResult::kOk 已进入硬件邮箱 OR 已入软件队列(统一视为已受理)。 * @retval CanTxResult::kErrMailbox 软件队列也满。 * @retval CanTxResult::kErrInvalid 参数非法。 */ static CanTxResult enqueueTx(uint8_t channelIndex, uint32_t ext_id, const uint8_t *data, uint8_t dlc); /** @brief 软件 TX 队列当前积压帧数(统计/诊断用)。 */ static uint32_t txQueueBacklog(uint8_t channelIndex); /** @brief 接收队列容量深度(2 的幂;8kHz PDO 下主循环间隙短,加深缓冲防 STATUS 溢出丢帧)。 */ static constexpr uint8_t rxQueueDepth() { return kRxQueueDepth; } /** @brief 查询指定通道软件接收队列当前积压帧数(无锁 SPSC,由 tail 与 head 计算)。 */ static uint32_t rxQueueCount(uint8_t channelIndex); private: static constexpr uint8_t kRxQueueDepth = 64U; /* 16→64:1kHz STATUS×3 在 PDI 风暴下不再轻易打满 */ static constexpr uint8_t kRxQueueMask = kRxQueueDepth - 1U; /** 每通道软件突发队列深度:覆盖整块突发(CAN_BURST_HARD_MAX=16 帧)中同一通道的 * 全部超载帧(3 邮箱之外最多 13 帧 = 16-3),保证任意 DLC 的整块突发不因队列满丢帧。 */ static constexpr uint8_t kTxQueueDepth = 13U; struct RxFrame { uint8_t channel; uint32_t id; uint8_t data[64]; uint8_t length; }; /** * @brief Lock-Free 单生产者单消费者 (SPSC) 环形接收队列。 * @note 生产者为 CAN RX ISR (Preempt 0) 写入 tail,消费者为 consumeRxFrame (Thread) 推进 head。 * 移除共享 count 变量,消除 BASEPRI=0x10 临界区对 Preempt 0 无效导致的并发撕裂与计数下溢风险。 */ struct RxQueue { RxFrame frames[kRxQueueDepth]; volatile uint8_t head; volatile uint8_t tail; }; /** 排队等待硬件邮箱空位的 CAN 帧(仅 SDO 突发用,payload ≤ 64B)。 */ struct TxQueuedFrame { uint32_t id; uint8_t data[64]; uint8_t dlc; }; struct TxQueue { TxQueuedFrame frames[kTxQueueDepth]; volatile uint8_t head; volatile uint8_t tail; volatile uint8_t count; }; /** @brief CAN TX ISR 内驱动:把队列里能装的全部装进硬件邮箱。 */ static void drainTxQueueIsr(uint8_t channelIndex); static void initGpio(); /** * @brief 根据当前模式为指定控制器配置接收过滤器。 * @param[in] canPeriph 目标 CAN 外设实例。 * @param[in] filterNumber 过滤器编号。 * @param[in] mode 目标接收过滤模式。 */ static void configureRxFilter(uint32_t canPeriph, uint8_t filterNumber, Mode mode); static bool initNominalTiming(uint32_t canPeriph); static bool initFdMode(uint32_t canPeriph); static void printClockInfo(); static void initChannelHardware(uint8_t i); static void configureAllFilters(); static void printInitResult(); static void printErrorState(); static void runSelfTest(); static void runLoopbackTest(); static void dumpRegisters(); static Diag s_diag; static RxQueue s_rxQueues[3]; static TxQueue s_txQueues[3]; /**< CAN 突发软件队列(邮箱装不下时缓存) */ static uint8_t s_nextConsumeChannel; static volatile Mode s_mode; static ChannelStats s_channelStats[3]; static bool s_brsSuppress[3]; /**< TX BRS 压制(电机 OTA 期间该通道 DLC>8 帧禁 BRS) */ static volatile uint8_t s_txSilent[3]; /**< 收发器 Silent(只听不发):1=S 脚已置高,TX 入口拒绝 */ static ChannelFeedback s_feedback[3]; /** 各通道硬件 init(时序+退出 init mode)是否成功 */ static bool s_channelHwOk[3]; static uint32_t s_feedbackSeq; static IdSnapshotTable s_idSnapshot[3]; }; #endif /* DEVICE_CANFD_H */