Files
DCU_APP/GD32E508_DCU_APP/comm/ethercat_can_bridge.h
T

279 lines
15 KiB
C++
Raw Normal View History

/**
* @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..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, /**< 本帧携带的有效槽数(≤7) */
ECAT_MOTW_OFF_GLOBAL_SEQ = 36U,/**< 状态表全局刷新序号,小端 uint32 */
ECAT_MOTW_OFF_SLOTS = 40U, /**< 槽数组起点,7×40B */
};
/** 电机状态槽内部布局(40 字节)。 */
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(正常 32) */
ECAT_MOT_OFF_SEQ = 4U, /**< STATUS 刷新序号,小端 uint32 */
ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,32 字节 */
};
/** 输入 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..639):CANFD 批量接收区(1.5.56 新增)。 */
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 / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */
constexpr uint8_t kEcatCanTxDataMax = 32U;
/** 段 1 短快照数据区字节数(保持 1.5.53 兼容,勿改)。 */
constexpr uint8_t kEcatCanRxDataMax = 8U;
/** 段 3 长快照数据区字节数。 */
constexpr uint8_t kEcatCanXRxDataMax = 32U;
/** 单个电机状态槽字节数。 */
constexpr uint8_t kEcatMotSlotSize = 40U;
/** 一份输入 PDO 携带的电机状态槽数。 */
constexpr uint8_t kEcatMotSlotsPerPdo = 7U;
/** 过程数据长度,唯一真源见 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; /* 80×64bit = 640B */
/** CANFD 单帧数据上限(DLC 64)—— 批量区按此上限做容量推导。 */
constexpr uint8_t kEcatCanFdDataMax = 64U;
/** 批量发送区可用字节数与最坏情况(全 DLC64)帧数上限。 */
constexpr uint16_t kEcatTxBatchBytes = (uint16_t)(kEcatPdoOutBytes - ECAT_TXB_OFF_ITEMS);
constexpr uint8_t kEcatTxBatchMax =
(uint8_t)(kEcatTxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax));
/** 批量接收区可用字节数与最坏情况帧数上限。 */
constexpr uint16_t kEcatRxBatchBytes = (uint16_t)(kEcatPdoInBytes - ECAT_RXB_OFF_ITEMS);
constexpr uint8_t kEcatRxBatchMax =
(uint8_t)(kEcatRxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax));
/** 批量接收环形队列槽数。取 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, "批量接收区装不下一帧 DLC64");
/**
* @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() 每周期调用。
*/
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 */