Files
DCU_APP/GD32E508_DCU_APP/comm/ethercat_can_bridge.h
T
cbb 0cd3d9bed2 fix(comm): 回退LAN9252批量读写优化并修复多个CAN-EtherCAT桥竞态问题
回退1.6.0引入的LAN9252 PRAM批量读写优化,因其AVAIL_CNT计数器对短读存在语义问题,
导致邮箱短读和探针传输时出现FIFO陈旧数据损坏问题。

同时修复EtherCAT-CAN桥中的多个竞态条件:
- 修复消费者在fillRxBatch中直接访问共享ring buffer导致的数据混淆问题
- 修复生产者-消费者间tail指针发布时机不当造成的槽位覆盖竞态
- 修复WIN窗口基址更新位置错误导致电机状态窗口被锁定的问题
- 修复buildInput中断上下文与主循环间的数据竞争和内存撕裂问题
- 添加STATUS帧过滤机制减少批量区压力并解决队列饱和问题

此外还增加了多个诊断计数器用于监控撕裂、溢出和拼接错误,并调整了
输入PDO段布局和大小参数以支持新的48字节STATUS格式。

这些修改解决了上位机观察到的帧标签错误、电机状态丢失和数据拼接问题。
2026-09-08 00:52:35 +08:00

341 lines
20 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.
/**
* @file ethercat_can_bridge.h
* @brief EtherCAT PDO ↔ CAN 转发桥(1.5.55)。
*
* 输出 PDO(0x7000, 主站→从站, 64B) = 待发送的 CAN 帧(数据区 32B)
* 输入 PDO(0x6000, 从站→主站, 384B) = 桥状态 + 电机状态窗口 + 长帧快照
*
* 发送用**序号沿触发**:仅当 seq 字节变化时才真正发一帧。PDO 是周期性刷新的,
* 若按「内容非空即发」会把同一帧每个周期重发一次;seq 变化沿是 PDO 里表达
* 「一次性事件」的标准做法,主站只需每次发送前把 seq 加一。
*
* RX 侧不能自己去 CanFd::consumeRxFrame() —— 那个队列已有唯一消费者
* TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态
* 会随机丢帧。改为由 taskCanRxParse() 的分发链回调 onCanFrame() 投递。
*
* ── 1.5.55 为电机控制台扩容(32B/32B → 64B/384B)──
*
* 三个动机:
* 1. TX 数据区 24B→32B。电机 CMD 是 DLC 32,旧 processOutput 里
* `dlc > kEcatCanTxDataMax` 直接钳位到 24,后 8 字节被**静默丢弃**且不报错。
* 2. 输入侧加电机状态窗口。单帧快照对电机控制台不够用:9 节点各 32B STATUS,
* 一周期只带回一帧、新帧还覆盖旧帧,永远取不齐。改为直接从 MotorState
* 状态表(已按 node×channel 聚合,见 device/motor_state.h)取一个窗口,
* 主站移动 win_base 即可 4 个周期扫完全部 27 槽。
* 3. 加长帧快照区。旧 RX 数据区只有 8B,版本帧(DLC24)/CONFIG 帧(DLC24) 会被截断。
*
* **输入 PDO 前 32 字节与 1.5.53 逐字节兼容**,偏移与语义全部不变。这样
* tools/ecat 的 CAN 调试页(ecat.html)与 ecat_server.py 的 _can_decode_input
* 不改一行也照常工作,扩容只是在后面追加新区段。
*
* ── 1.5.56 扩到 DPRAM 硬上限并加 CANFD 批量帧区(64/384 → 384/640)──
*
* 动机:需要一个周期里合并下发/回收**多条 CANFD 帧**。1.5.55 的单帧槽
* (一个 seq 一帧)每周期最多一帧,4ms 周期就是 250 帧/s 上限,
* 而一次多轴同步下发就要 9 帧,靠排队要 36ms 才发完 —— 各轴指令不同拍,
* 失去"同步下发"的意义。
*
* 新增两个批量区,每条帧带 8B 头(ch/dlc/flags/rsvd + id)+ 数据:
* 输出 48..383(336B):批量发送,一个 seq 沿一次性发 N 帧
* 输入 360..483(124B):批量接收,一周期回收多帧而不是只带最近一帧
* (1.6.1 数据条目 64→32B,段 5 诊断区 484..491,原 360..639/280B)
*
* 三条兼容性铁律(改动只做追加,不动既有偏移):
* 1. 输出 0..47 与 1.5.55 单帧布局完全一致(seq/ch/dlc/flags/id/win/data32);
* 2. 输入 0..359 与 1.5.55 完全一致(段1 桥状态 + 段2 电机窗口 + 段3 长帧);
* 3. 批量区**为空时**(count=0)行为与 1.5.55 完全相同,老主站照旧可用。
*
* 上限推导(为何是 384+640 而不是更大)见 comm/ethercat_pdo_size.h。
*/
#ifndef COMM_ETHERCAT_CAN_BRIDGE_H
#define COMM_ETHERCAT_CAN_BRIDGE_H
#include <cstdint>
#include "comm/ethercat_pdo_size.h"
/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。**与 1.5.55 完全兼容**。 */
enum : uint8_t {
ECAT_CAN_TX_OFF_SEQ = 0U, /**< 发送序号:变化沿触发一次发送(单帧+批量一起) */
ECAT_CAN_TX_OFF_CH = 1U, /**< 目标通道 0/1/2 */
ECAT_CAN_TX_OFF_DLC = 2U, /**< 数据长度 0..32;0 表示本次不发单帧(只发批量) */
ECAT_CAN_TX_OFF_FLAGS = 3U, /**< bit0=1 请求清零统计;其余保留 */
ECAT_CAN_TX_OFF_ID = 4U, /**< CAN ID,小端 uint32(29 位扩展帧) */
ECAT_CAN_TX_OFF_WIN = 8U, /**< 请求回传的电机槽窗口起始下标 0..26 */
/* 9..15 保留:让数据区落在 16 字节边界,hexdump 排查时对齐好读 */
ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */
};
/** 输出 PDO 段 2(48..383):CANFD 批量发送区(1.5.56 新增)。 */
enum : uint16_t {
ECAT_TXB_OFF_COUNT = 48U, /**< 本次要发的帧数 0..kEcatTxBatchMax;0=不发批量 */
ECAT_TXB_OFF_FLAGS = 49U, /**< bit0=1 遇错即停(默认继续发完并各自记结果) */
ECAT_TXB_OFF_RSVD = 50U, /**< 保留 2B,让帧数组从 52 起、每帧 8B 头对齐 */
ECAT_TXB_OFF_ITEMS = 52U, /**< 帧数组起点,每项 (8B 头 + dlc 数据) 紧密排列 */
};
/** 批量帧项内部布局(头 8 字节,之后紧跟 dlc 字节数据)。收发两向共用。 */
enum : uint8_t {
ECAT_FRM_OFF_CH = 0U, /**< 通道 0/1/2 */
ECAT_FRM_OFF_DLC = 1U, /**< 数据长度 0..64(CANFD 全量) */
ECAT_FRM_OFF_FLAGS = 2U, /**< 发送向:保留; 接收向 bit0=有效 */
ECAT_FRM_OFF_RES = 3U, /**< 发送向:从站回填该帧发送结果; 接收向:保留 */
ECAT_FRM_OFF_ID = 4U, /**< CAN ID,小端 uint32 */
ECAT_FRM_HDR_SIZE = 8U, /**< 头长度,数据紧跟其后 */
};
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照。**与 1.5.53 完全兼容**。 */
enum : uint8_t {
ECAT_CAN_RX_OFF_ACK = 0U, /**< 回显已执行的发送序号,主站据此确认送达 */
ECAT_CAN_RX_OFF_CH = 1U, /**< 该帧来自哪个通道 */
ECAT_CAN_RX_OFF_DLC = 2U, /**< 该帧数据长度(原始 DLC,未被截断前的值) */
ECAT_CAN_RX_OFF_FLAGS = 3U, /**< bit0=有过接收;bit1=数据被截断到 8B */
ECAT_CAN_RX_OFF_ID = 4U, /**< 该帧 CAN ID,小端 uint32 */
ECAT_CAN_RX_OFF_DATA = 8U, /**< 数据区起点,8 字节(长帧见段 3) */
ECAT_CAN_RX_OFF_RXCNT = 16U, /**< 累计接收帧数,小端 uint32 */
ECAT_CAN_RX_OFF_TXRES = 20U, /**< 最近一次发送结果(CanTxResult 原值,0=OK) */
ECAT_CAN_RX_OFF_BACKLOG = 21U, /**< 该通道软件 TX 队列积压 */
ECAT_CAN_RX_OFF_TXCNT = 22U, /**< 累计发送受理帧数,小端 uint16 */
ECAT_CAN_RX_OFF_DROP = 24U, /**< 桥接层丢帧数(投递时上一帧还没被取走),小端 uint16 */
};
/** 输入 PDO 段 2(32..319):电机状态窗口头 + 槽数组。 */
enum : uint16_t {
ECAT_MOTW_OFF_ACTIVE = 32U, /**< 在线电机数 */
ECAT_MOTW_OFF_BOOT = 33U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */
ECAT_MOTW_OFF_WIN_BASE = 34U, /**< 本帧携带的槽窗口起始下标(回显主站请求) */
ECAT_MOTW_OFF_WIN_COUNT = 35U, /**< 本帧携带的有效槽数(≤5) */
ECAT_MOTW_OFF_GLOBAL_SEQ = 36U,/**< 状态表全局刷新序号,小端 uint32 */
ECAT_MOTW_OFF_SLOTS = 40U, /**< 槽数组起点,5×56B(40..319,与段 3 正好相接) */
};
/** 电机状态槽内部布局(56 字节)。 */
enum : uint8_t {
ECAT_MOT_OFF_NODE = 0U, /**< 节点号 1..9,0=空槽 */
ECAT_MOT_OFF_CH = 1U, /**< CAN 通道 0/1/2 */
ECAT_MOT_OFF_ONLINE = 2U, /**< 1=在线 */
ECAT_MOT_OFF_DLC = 3U, /**< 最后一帧 STATUS 的 DLC(正常 48) */
ECAT_MOT_OFF_SEQ = 4U, /**< STATUS 刷新序号,小端 uint32 */
ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,48 字节(v0.74:B32-35=InitStatus) */
};
/** 输入 PDO 段 3(320..359):最近一帧**完整**快照(版本/CONFIG 帧要 24B)。 */
enum : uint16_t {
ECAT_XRX_OFF_CH = 320U, /**< 通道 */
ECAT_XRX_OFF_DLC = 321U, /**< 原始 DLC(可达 32) */
ECAT_XRX_OFF_FLAGS = 322U, /**< bit0=有效 */
ECAT_XRX_OFF_RSVD = 323U,
ECAT_XRX_OFF_ID = 324U, /**< CAN ID,小端 uint32 */
ECAT_XRX_OFF_DATA = 328U, /**< 数据区,32 字节完整 payload */
};
/** 输入 PDO 段 4(360..483):CANFD 批量接收区(1.5.56 新增,1.6.1 数据条目 64→32B)。
* 每项 8B 头 + 最多 32B 数据(kEcatRxBatchDataMax);DLC 字节仍填**原始值**,
* 超长帧被截断时 flags bit1=1 —— 主站按 "8 + min(dlc,32)" 推进游标。 */
enum : uint16_t {
ECAT_RXB_OFF_COUNT = 360U, /**< 本周期回收的帧数 0..kEcatRxBatchMax */
ECAT_RXB_OFF_LOST = 361U, /**< 本周期因批量区放不下而丢弃的帧数 */
ECAT_RXB_OFF_PENDING = 362U, /**< 环形队列中仍待回收的帧数(主站据此提速) */
ECAT_RXB_OFF_RSVD = 363U,
ECAT_RXB_OFF_ITEMS = 364U, /**< 帧数组起点,项布局同 ECAT_FRM_OFF_* */
};
/** 输入 PDO 段 5(484..491):设备诊断区(1.5.61 尾部追加,1.6.1 随批量区
* 数据条目 64→32B 前移:364+3×40=484,诊断区锚在紧随其后的 8 字节,
* 写侧是 EthercatPdo::fillDiagTail())。 */
enum : uint16_t {
ECAT_DIAG_OFF_LOAD = 484U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%)。
中断 + 非延迟处理段(任务派发/OTA relay)占墙钟比,
不含常开轮询与空转;1s 滚动窗口(见 sys/cpu_load.h) */
ECAT_DIAG_OFF_TORN = 486U, /**< CAN 取帧撕裂丢帧数(低 16 位,小端,饱和不回绕)。
1.6.6 起:ISR 取帧时若 MP 的 TS/DLENC 前后不一致,
说明邮箱窗口在取数途中被硬件换帧,该帧整帧丢弃并
在此计数。这是"读到半条帧"的唯一可观测量 —— 主站
据此判断修复是否生效、总线负载是否高到频繁触发。 */
ECAT_DIAG_OFF_SEQ = 488U, /**< 诊断区序号,小端 uint16(1.6.12 起,原 u32 让出
高 2 字节),每完成一个 1s 统计窗口 +1,主站据此
判断负载值是否仍在滚动刷新。u16 足够:主站只判
"非 0 且在涨",18h 才回绕。 */
ECAT_DIAG_OFF_OVERFULL = 490U, /**< CAN RX FIFO 满溢计数 RFO0(u16 饱和,1.6.12)。
1.6.11 把溢出策略改成"满则丢新帧"后拼接并未消失
(段5@494 以 ~384/s 增长),必须直接观测溢出本身
与拼接率是否同涨,才能判定残余拼接是否仍源自溢出
(即本 IP 在 DISCARD 模式下照样毁窗口)。 */
ECAT_DIAG_OFF_RELTIMEOUT = 492U, /**< RFD0 释放握手超时计数(u16 饱和,1.6.12)。
原为 1.6.7 的 PDO 重入计数 ECAT_DIAG_OFF_REENTRY
(引入至今恒 0,不再上线,仍留内部计数)。取帧后
等 RFD0 自清 2000 次未果 = dequeue 未完成,窗口
与读指针可能已失同步 —— 拼接的直接前兆。 */
ECAT_DIAG_OFF_REJECT = 494U, /**< CAN 内容级拼接帧命中率(u16 饱和,1.6.11)。
入队前结构自洽校验(STATUS 温度量程+斜率、CONFIG
未使用区/模式)命中即计数。1.6.12 起**只计不丢**
—— 数据管道必须诚实,该计数退化为"源头是否还在
产生拼接帧"的纯测量仪表:修好 = 恒 0,没修好 =
网页看到什么,计数就是什么的倍率。 */
};
/** 单帧数据在输出 PDO / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */
constexpr uint8_t kEcatCanTxDataMax = 32U;
/** 段 1 短快照数据区字节数(保持 1.5.53 兼容,勿改)。 */
constexpr uint8_t kEcatCanRxDataMax = 8U;
/** 段 3 长快照数据区字节数。 */
constexpr uint8_t kEcatCanXRxDataMax = 32U;
/** 单个电机状态槽字节数:8B 头 + 48B STATUS(v0.74)。
* 1.6.4 前是 40B(只装得下 32B STATUS),于是 buildInput 按 48B 拷贝时会溢出
* 16B 盖到段 3,且主站永远拿不到 B32-35 的 InitStatus。 */
constexpr uint8_t kEcatMotSlotSize = 56U;
/** 一份输入 PDO 携带的电机状态槽数:段 2 可用 280B(40..319)÷ 56 = 5。
* 27 槽需 ⌈27/5⌉ = 6 个基址扫完。槽数×槽长仍是 280B,输入 PDO 总长不变,
* 所以 EEPROM/SII 无需重刷。 */
constexpr uint8_t kEcatMotSlotsPerPdo = 5U;
/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h(C/C++ 共用)。 */
constexpr uint16_t kEcatPdoOutBytes = ECAT_PDO_OUT_BYTES; /* 48×64bit = 384B */
constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 62×64bit = 496B */
/** CANFD 单帧数据上限(DLC 64)—— 环形队列/发送批量区按此上限做容量推导。 */
constexpr uint8_t kEcatCanFdDataMax = 64U;
/** 批量**接收**条目在 PDO 内的数据字节数上限(1.6.1 从 64 压到 32)。
* 真机采样 6086 帧 DLC 只有 24/32 两档;超长帧在装填时截断(flags bit1),
* 环形队列 RxRingItem 仍存全量 64B,截断只发生在 PDO 快照这一步。 */
constexpr uint8_t kEcatRxBatchDataMax = 32U;
/** 批量发送区可用字节数与最坏情况(全 DLC64)帧数上限。 */
constexpr uint16_t kEcatTxBatchBytes = (uint16_t)(kEcatPdoOutBytes - ECAT_TXB_OFF_ITEMS);
constexpr uint8_t kEcatTxBatchMax =
(uint8_t)(kEcatTxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax));
/** 批量接收区可用字节数与最坏情况(全 DLC32 截断满帧)帧数上限。
* 可用区到段 5 诊断区起点为止(1.6.10 修正口径:旧式按 PDO 全长 496 算,
* 与 fillRxBatch 的旧边界判据同错;数值上 120/40 与 132/40 同为 3,无行为变化)。 */
constexpr uint16_t kEcatRxBatchBytes = (uint16_t)(ECAT_DIAG_OFF_LOAD - ECAT_RXB_OFF_ITEMS);
constexpr uint8_t kEcatRxBatchMax =
(uint8_t)(kEcatRxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatRxBatchDataMax));
/** 批量接收环形队列槽数。取 kEcatRxBatchMax 的 4 倍,让主站慢一两拍也不丢帧。
* 注意这是 RAM 成本(槽数 × 72B),不是 PDO 成本。 */
constexpr uint8_t kEcatRxRingSlots = (uint8_t)(kEcatRxBatchMax * 4U);
/* 布局守卫:段 2 的槽数组必须完整落在输入 PDO 内,段 3 也不能越界。
* 改槽数/槽大小时忘了同步 PDO 长度,会在这里编译期失败。 */
static_assert(ECAT_MOTW_OFF_SLOTS + kEcatMotSlotsPerPdo * kEcatMotSlotSize
<= ECAT_XRX_OFF_CH, "电机槽窗口与长帧快照区重叠");
static_assert(ECAT_XRX_OFF_DATA + kEcatCanXRxDataMax <= ECAT_RXB_OFF_COUNT,
"长帧快照区与批量接收区重叠");
static_assert(ECAT_CAN_TX_OFF_DATA + kEcatCanTxDataMax <= ECAT_TXB_OFF_COUNT,
"单帧发送数据区与批量发送区重叠");
static_assert(ECAT_RXB_OFF_ITEMS < kEcatPdoInBytes, "批量接收区起点越界");
static_assert(ECAT_TXB_OFF_ITEMS < kEcatPdoOutBytes, "批量发送区起点越界");
/* 批量区至少要装得下一帧满长 CANFD,否则"批量"没有意义 */
static_assert(kEcatTxBatchMax >= 1U, "批量发送区装不下一帧 DLC64");
static_assert(kEcatRxBatchMax >= 1U, "批量接收区装不下一帧 DLC32");
/* 段 5 诊断区必须在批量接收区的最坏占用之外(改批量区布局时一起核对) */
static_assert(ECAT_RXB_OFF_ITEMS + (uint16_t)kEcatRxBatchMax * (ECAT_FRM_HDR_SIZE + kEcatRxBatchDataMax)
<= ECAT_DIAG_OFF_LOAD, "批量接收区最坏占用与段5诊断区重叠");
/**
* @brief EtherCAT ↔ CAN 转发桥。
* @note 全静态;PDO 侧函数在 Sync0_Isr(preempt=0) 或主循环 MainLoop 上下文执行,
* onCanFrame() 在主循环 taskCanRxParse() 上下文执行。共享状态用 volatile +
* 单生产者/单消费者语义,不需要临界区(投递只写、消费只读+清标志)。
* 例外:段 1/段 3 的"最近一帧快照"是多字段一组(id/dlc/data 必须配套),
* 用 s_xrxSeq seqlock 保证消费者不会读到"新数据+旧 ID"的混搭——
* Sync0 是 preempt=0,critical.h 的 BASEPRI 方案挡不住它,故只能无锁协议。
*/
class EthercatCanBridge {
public:
/**
* @brief 处理一份输出 PDO:seq 变化时向 CAN 发一帧。
* @param[in] out kEcatPdoOutBytes 字节输出过程数据。
* @note 由 APPL_Application() 每周期调用。
*/
/** @brief 输出探针(1.6.0 输出按需全量读)。
*
* PDI_Isr 每周期先读输出 PDO 头 12B(覆盖 SEQ@0 与 WIN@8),由本函数判定
* 是否需要全量读 384B:
* - 始终从探针刷新 s_winBase(主站每周期移动窗口扫描,与 seq 沿无关);
* - 首帧只记 seq 基准(与 processOutput 的 priming 一致),返回 false;
* - seq 变化沿或 FLAGS bit0(清统计请求)→ 返回 true,调用方全量读。
* s_lastSeq 仍由 processOutput 在全量路径上推进,本函数不写,保证沿语义
* 只处理一次。 */
static bool peekOutput(const uint8_t *head);
static void processOutput(const uint8_t *out);
/**
* @brief 填充输入 PDO:桥状态 + 电机状态窗口 + 长帧快照。
* @param[out] in kEcatPdoInBytes 字节输入过程数据(本函数负责全部写满)。
*/
static void buildInput(uint8_t *in);
/**
* @brief CAN 收帧投递入口。
* @note 由 TaskRunner::taskCanRxParse() 在分发链里调用,不消费帧(不返回
* "已处理"),只做旁路复制,因此不影响 OTA relay / 电机状态。
*/
static void onCanFrame(uint8_t channel, uint32_t id,
const uint8_t *data, uint8_t length);
/** @brief 桥接是否已收到过至少一帧(诊断用)。 */
static bool hasReceived() { return s_rxValid; }
private:
/**
* @brief 处理输出 PDO 的批量发送区(seq 沿内调用一次)。
* @return 实际受理的帧数。
* @note 逐帧解析 (8B 头 + dlc),边界不足即停 —— 不信任主站给的 count,
* 越界的 count 只会让循环提前结束,不会读到 PDO 之外。
*/
static uint8_t sendBatch(const uint8_t *out);
/** @brief 把接收环形队列里的帧尽量装进输入 PDO 批量区。 */
static void fillRxBatch(uint8_t *in);
static volatile uint8_t s_lastSeq; /**< 上一次已执行的发送序号 */
static volatile bool s_seqPrimed; /**< 是否已记录过基准 seq(防上电首帧误发) */
static volatile uint8_t s_lastTxResult;
static volatile uint16_t s_txCount;
static volatile uint16_t s_dropCount;
/** 主站请求的电机槽窗口起始下标(输出 PDO 的 WIN 字节,每周期刷新)。 */
static volatile uint8_t s_winBase;
/* 最近一帧接收快照:taskCanRxParse 写、PDO 读。
* s_xrxSeq 为 seqlock:奇数=正在写,buildInput 本周期跳过段 1/段 3。
* 没有它,Sync0_Isr 抢占在"数据已写完、id 未更新"之间会把新帧数据
* 配旧帧 ID 发出去(STATUS 被标成 CONFIG → 前端解出离谱目标值)。 */
static volatile bool s_rxValid;
static volatile uint32_t s_xrxSeq;
static volatile uint8_t s_rxCh;
static volatile uint8_t s_rxDlc;
static volatile bool s_rxTruncated;
static volatile uint32_t s_rxId;
static volatile uint32_t s_rxCount;
static uint8_t s_rxData[kEcatCanXRxDataMax];
/* ── 批量接收环形队列(1.5.56)──
* 单生产者(taskCanRxParse 的 onCanFrame) / 单消费者(PDO 的 fillRxBatch)。
* head/tail 各由一侧独占写入,故无需临界区:生产者只推进 head、
* 消费者只推进 tail,最坏情况是某周期少读一帧,下周期即取到。 */
struct RxRingItem {
uint32_t id;
uint8_t ch;
uint8_t dlc;
uint8_t data[kEcatCanFdDataMax];
};
static RxRingItem s_rxRing[kEcatRxRingSlots];
static volatile uint8_t s_rxRingHead; /**< 生产者写入位置 */
static volatile uint8_t s_rxRingTail; /**< 消费者读取位置 */
static volatile uint8_t s_rxRingLost; /**< 队列满导致的丢帧数(累计,回填后清零) */
};
extern "C" {
/** @brief 供 dc_app.c(C 文件)调用的 shim:处理输出 PDO。 */
void EcatCan_ProcessOutput(const uint8_t *out);
/** @brief 供 dc_app.c 调用的 shim:填充输入 PDO。 */
void EcatCan_BuildInput(uint8_t *in);
}
#endif /* COMM_ETHERCAT_CAN_BRIDGE_H */