Files
DCU_APP/GD32E508_DCU_APP/comm/ethercat_can_bridge.h
T
cbb 70daa4b0c6 feat(DCU_APP): 添加CPU负载监控和EtherCAT诊断功能
- 在DCU_APP项目中添加cpu_load.h和cpu_load.cpp模块用于CPU负载率监测
- 修改csolution配置将构建目标从Debug切换为Release
- 在EtherCAT PDO协议栈中新增段5诊断区(632-639字节),包含负载率和序列号
- 在所有关键中断服务程序中添加CpuLoad::isrEnter()/isrExit()钩子用于准确统计中断占用
- 增强EtherCAT同步中断处理,改进AL事件掩码守护机制,修复OP状态下PDI中断门控问题
- 解决SSC源码中指针地址对齐警告和打包成员访问警告问题
- 在主循环中集成CPU负载率采样tick()调用,实现1秒滚动窗口统计

BREAKING CHANGE: 构建配置从Debug改为Release,EtherCAT协议栈增加新的诊断数据区
2026-09-05 20:07:22 +08:00

294 lines
16 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..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 段 5(632..639):设备诊断区(1.5.61 新增,尾部追加)。
* 批量接收区最坏情况(全 DLC64 满帧)只用到 364+3×72=580,580..639 永久空闲,
* 诊断区锚在 632 起的 8 字节,写侧是 EthercatPdo::process()。 */
enum : uint16_t {
ECAT_DIAG_OFF_LOAD = 632U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%)。
中断 + 非延迟处理段(任务派发/OTA relay)占墙钟比,
不含常开轮询与空转;1s 滚动窗口(见 sys/cpu_load.h) */
ECAT_DIAG_OFF_RSVD = 634U, /**< 保留 2B */
ECAT_DIAG_OFF_SEQ = 636U, /**< 诊断区序号,小端 uint32,每完成一个统计窗口 +1,
主站据此判断负载值是否仍在滚动刷新 */
};
/** 单帧数据在输出 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");
/* 段 5 诊断区必须在批量接收区的最坏占用之外(改批量区布局时一起核对) */
static_assert(ECAT_RXB_OFF_ITEMS + (uint16_t)kEcatRxBatchMax * (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax)
<= 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() 每周期调用。
*/
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 */