From 1742d9a75905ed45f99e09cd16989825dce81de8 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Fri, 11 Sep 2026 01:08:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(comm):=201.6.21=20=E6=AE=B52=E6=94=B9?= =?UTF-8?q?=E7=94=B5=E6=9C=BA=E6=95=B4=E8=BD=AE=E5=AF=86=E6=8E=92,=20?= =?UTF-8?q?=E5=88=A0=E6=AE=B54=E5=8E=86=E5=8F=B2=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 段2(32..543): count=实际电机数(0..9), 空节点不占位, 槽序按 node_id 升序 - 同号多通道取 seq 最新; magic@35=0xA5 区分密排与旧滑窗 - 差分填充: 节点集合+seq/online 未变时整段复用影子 - 删段4 fillRxWindow; 非STATUS仍走段1/3最近一帧 - 输入PDO总长仍600B, SII无需重刷; 主站需按count密排解码 - 动机: 接3电机却看到5状态帧(空槽占位); win_base滑窗对已识别电机无意义 --- GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp | 353 +++++++---------- GD32E508_DCU_APP/comm/ethercat_can_bridge.h | 355 ++++++------------ GD32E508_DCU_APP/comm/ethercat_diff_stats.cpp | 6 +- GD32E508_DCU_APP/comm/ethercat_diff_stats.h | 6 +- GD32E508_DCU_APP/comm/ethercat_pdo.h | 5 +- GD32E508_DCU_APP/comm/ethercat_pdo_size.h | 4 + GD32E508_DCU_APP/comm/ssc/port/dc_app.c | 6 +- GD32E508_DCU_APP/sys/config.h | 2 +- 8 files changed, 254 insertions(+), 483 deletions(-) diff --git a/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp b/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp index 50abe4b..2757e12 100644 --- a/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp +++ b/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp @@ -1,6 +1,6 @@ /** * @file ethercat_can_bridge.cpp - * @brief EtherCAT PDO ↔ CAN 转发桥实现(1.5.56)。 + * @brief EtherCAT PDO ↔ CAN 转发桥实现(1.6.21:段 2 电机整轮密排)。 */ #include "comm/ethercat_can_bridge.h" #include "device/canfd.h" @@ -16,7 +16,6 @@ volatile bool EthercatCanBridge::s_seqPrimed = false; volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U; volatile uint16_t EthercatCanBridge::s_txCount = 0U; volatile uint16_t EthercatCanBridge::s_dropCount = 0U; -volatile uint8_t EthercatCanBridge::s_winBase = 0U; volatile bool EthercatCanBridge::s_rxValid = false; volatile bool EthercatCanBridge::s_rxPublished = false; @@ -28,15 +27,15 @@ volatile uint32_t EthercatCanBridge::s_rxId = 0U; volatile uint32_t EthercatCanBridge::s_rxCount = 0U; uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0}; -EthercatCanBridge::RxWinSlot - EthercatCanBridge::s_rxWin[ECAT_RXB_SLOTS] = {}; -volatile uint8_t EthercatCanBridge::s_rxWinWr = 0U; -volatile uint32_t EthercatCanBridge::s_rxWinSeq = 0U; -volatile uint8_t EthercatCanBridge::s_rxWinOvf = 0U; -volatile uint8_t EthercatCanBridge::s_rxWinSeen[ECAT_RXB_SLOTS] = {0}; +uint8_t EthercatCanBridge::s_motShadow[kEcatMotRegionBytes] = {0}; +uint32_t EthercatCanBridge::s_motPubSeq[kMotorMaxNodes + 1U] = {0}; +uint8_t EthercatCanBridge::s_motPubOnline[kMotorMaxNodes + 1U] = {0}; +uint16_t EthercatCanBridge::s_motPubMask = 0U; +uint8_t EthercatCanBridge::s_motPubCount = 0U; +bool EthercatCanBridge::s_motShadowValid = false; namespace { -/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端,不依赖编译器结构体布局。 */ +/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端。 */ inline uint32_t rd32le(const uint8_t *p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | @@ -51,13 +50,19 @@ inline void wr16le(uint8_t *p, uint16_t v) { p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8); } + +/** 段 2 头 + count 槽在影子区中的总长度。 */ +inline uint16_t motRegionLen(uint8_t count) +{ + return (uint16_t)(kEcatMotHdrSize + (uint16_t)count * kEcatMotSlotSize); +} } // namespace uint8_t EthercatCanBridge::sendBatch(const uint8_t *out) { uint8_t want = out[ECAT_TXB_OFF_COUNT]; if (want == 0U) { - return 0U; /* 未使用批量区:行为与 1.5.55 完全一致 */ + return 0U; } if (want > kEcatTxBatchMax) { want = kEcatTxBatchMax; @@ -68,8 +73,6 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out) uint16_t off = ECAT_TXB_OFF_ITEMS; for (uint8_t i = 0U; i < want; ++i) { - /* 不信任主站给的 count/dlc:每步都验证头与数据都完整落在 PDO 内。 - * 越界即停 —— 这是唯一的边界判据,count 只是上限提示。 */ if ((uint16_t)(off + ECAT_FRM_HDR_SIZE) > kEcatPdoOutBytes) { break; } @@ -87,9 +90,6 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out) if (!CanFd::isChannelEnabled(ch)) { r = CanFd::CanTxResult::kErrInvalid; } else { - /* 与单帧路径同样用 enqueueTx:批量下发必然连续打同一通道, - * transmitFrame 的 allow_abort 会让后帧顶掉前帧(历史上 7 帧 - * 只发出 3 帧的根因),队列语义才不静默丢帧。 */ r = CanFd::enqueueTx(ch, rd32le(&f[ECAT_FRM_OFF_ID]), &f[ECAT_FRM_HDR_SIZE], dlc); } @@ -108,55 +108,109 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out) return sent; } -void EthercatCanBridge::fillRxWindow(uint8_t *in) +/** + * 段 2:电机整轮密排。 + * + * 1. 扫 MotorState,每个节点号 1..9 在 3 个通道里取 seq 最新的一条; + * 2. 若节点集合与各节点 seq 均与上一拍发布相同 → 整段复用影子区(跳过重读); + * 3. 否则按 node_id 升序密排写入,空节点不占位;seqlock 奇数(写入中)的槽 + * 本拍省略,下拍自愈 —— 不发半新半旧的 48B。 + */ +uint8_t EthercatCanBridge::fillMotorRound(uint8_t *in) { - /* ── 1.6.15:定长覆盖窗口,整块取值 ── - * 生产者不等人:每拍把 N 个槽原样拷进 PDO,槽里是谁就是谁。同一帧会在连续 - * 几拍里重复出现,直到被下一帧覆盖。语义是"最近 N 帧窗口",不是"保证不丢的 - * 事件流"——主站按每槽 seq 去重后才归一成事件流上报给上层。 - * - * 相比旧的环形队列排空,这里没有 tail/head/判满/pending:1.6.9 记录的那个 - * "滞后的 tail 让生产者写进消费者刚读过的槽"错标竞态,其载体(tail 握手) - * 在本布局里根本不存在。定长也让主站不必推游标,1.6.10 那类边界越界写诊断区 - * 的错在结构上不可能再犯。 - * - * 每槽 seqlock 判稳:奇数=生产者正在写这 12B 头 + 32B 数据,本拍留零跳过该槽, - * 下拍自愈(与段 1/段 2/段 3 既有约定一致)。本函数跑在 PDI_Isr/Sync0_Isr - * (preempt=0),单核下执行期间生产者必然冻结,判一次奇偶即可定性。 */ - in[ECAT_RXB_OFF_SLOT_COUNT] = ECAT_RXB_SLOTS; - in[ECAT_RXB_OFF_WR_IDX] = s_rxWinWr; - in[ECAT_RXB_OFF_OVF] = s_rxWinOvf; - s_rxWinOvf = 0U; /* 回填后清零:OVF 是"自上次回填以来"的增量 */ + const MotorStateTable &mt = MotorState::table(); - for (uint8_t i = 0U; i < ECAT_RXB_SLOTS; ++i) { - const RxWinSlot &sl = s_rxWin[i]; - const uint32_t seq = sl.seq; - if ((seq & 1U) != 0U) { - continue; /* 生产者正在写这槽:本拍不报,下拍自愈 */ + /* ── 1. 每节点选一条最新槽 ── */ + const MotorSlot *picks[kMotorMaxNodes]; + uint8_t nodes[kMotorMaxNodes]; + uint8_t n = 0U; + + for (uint8_t node = 1U; node <= kMotorMaxNodes; ++node) { + const MotorSlot *best = nullptr; + for (uint8_t ch = 0U; ch < kMotorChannels; ++ch) { + const MotorSlot &s = + mt.slots[(uint8_t)((node - 1U) * kMotorChannels + ch)]; + if (s.node_id == 0U) { + continue; + } + if (best == nullptr || s.seq >= best->seq) { + best = &s; + } } - __DMB(); /* 判稳先于取值:不许把下面的读重排到奇偶检查之前 */ - if ((sl.flags & 0x01U) == 0U) { - continue; /* 从未写过:留零,主站按 flags bit0 判空 */ + if (best != nullptr) { + picks[n] = best; + nodes[n] = node; + ++n; } - - /* RAM 侧存全量 64B,只有拷进 PDO 这一步按 32B 截断;DLC 字节填**原始值**, - * 主站据此 + flags bit1 判断是否被截。 */ - const uint8_t dlc = sl.dlc; - const uint8_t wire = (dlc > kEcatRxBatchDataMax) ? kEcatRxBatchDataMax : dlc; - uint8_t *p = &in[ECAT_RXB_OFF_ITEMS + (uint16_t)i * ECAT_RXB_SLOT_SIZE]; - - p[ECAT_RXW_OFF_CH] = sl.ch; - p[ECAT_RXW_OFF_DLC] = dlc; - p[ECAT_RXW_OFF_FLAGS] = (uint8_t)(0x01U | - (dlc > kEcatRxBatchDataMax ? 0x02U : 0x00U)); - wr32le(&p[ECAT_RXW_OFF_ID], sl.id); - wr32le(&p[ECAT_RXW_OFF_SEQ], seq); - memcpy(&p[ECAT_RXW_OFF_DATA], sl.data, wire); - - /* 标记"主站已取过":生产者下次覆盖本槽时就不算丢帧。只有"未取先覆" - * 才计 OVF —— 覆盖已送达的帧是窗口的设计意图,不是损失。 */ - s_rxWinSeen[i] = 1U; } + + /* ── 2. 判「上一轮已填充且未变」── + * 节点集合一致 + 每节点 seq/online 一致 + 头字段(active/boot)一致 + 写侧 + * seqlock 偶数 → 复用影子。online 必须比:checkOffline 超时置 0 时不 bump + * seq,只比 seq 会把「已离线」永远报成在线。 */ + bool unchanged = s_motShadowValid && (n == s_motPubCount) + && mt.active_count == s_motShadow[ECAT_MOTW_OFF_ACTIVE - ECAT_MOTW_OFF_COUNT] + && mt.boot_active == s_motShadow[ECAT_MOTW_OFF_BOOT - ECAT_MOTW_OFF_COUNT] + && s_motShadow[ECAT_MOTW_OFF_MAGIC - ECAT_MOTW_OFF_COUNT] + == kEcatMotLayoutMagic; + if (unchanged) { + for (uint8_t i = 0U; i < n; ++i) { + const uint8_t node = nodes[i]; + const MotorSlot &s = *picks[i]; + if ((s.wr_seq & 1U) != 0U + || s.seq != s_motPubSeq[node] + || s.online != s_motPubOnline[node] + || (s_motPubMask & (uint16_t)(1U << node)) == 0U) { + unchanged = false; + break; + } + } + } + + const uint16_t region = motRegionLen(unchanged ? s_motPubCount : n); + + if (unchanged) { + memcpy(&in[ECAT_MOTW_OFF_COUNT], s_motShadow, region); + return s_motPubCount; + } + + /* ── 3. 重建密排区 ── */ + in[ECAT_MOTW_OFF_ACTIVE] = mt.active_count; + in[ECAT_MOTW_OFF_BOOT] = mt.boot_active; + in[ECAT_MOTW_OFF_MAGIC] = kEcatMotLayoutMagic; + wr32le(&in[ECAT_MOTW_OFF_GLOBAL_SEQ], mt.global_seq); + + uint8_t w = 0U; + uint16_t newMask = 0U; + for (uint8_t i = 0U; i < n; ++i) { + const MotorSlot &s = *picks[i]; + if ((s.wr_seq & 1U) != 0U) { + continue; /* 写入中:本拍省略,下拍自愈 */ + } + __DMB(); + + uint8_t *p = &in[ECAT_MOTW_OFF_SLOTS + (uint16_t)w * kEcatMotSlotSize]; + p[ECAT_MOT_OFF_NODE] = nodes[i]; + p[ECAT_MOT_OFF_CH] = s.channel; + p[ECAT_MOT_OFF_ONLINE] = s.online; + p[ECAT_MOT_OFF_DLC] = s.dlc; + wr32le(&p[ECAT_MOT_OFF_SEQ], s.seq); + memcpy(&p[ECAT_MOT_OFF_STATUS], s.data, kMotorStatusDlc); + + s_motPubSeq[nodes[i]] = s.seq; + s_motPubOnline[nodes[i]] = s.online; + newMask |= (uint16_t)(1U << nodes[i]); + ++w; + } + + in[ECAT_MOTW_OFF_COUNT] = w; + + /* 更新影子:下次集合+seq 未变时整段复用 */ + memcpy(s_motShadow, &in[ECAT_MOTW_OFF_COUNT], motRegionLen(w)); + s_motPubMask = newMask; + s_motPubCount = w; + s_motShadowValid = true; + return w; } bool EthercatCanBridge::peekOutput(const uint8_t *head) @@ -166,20 +220,14 @@ bool EthercatCanBridge::peekOutput(const uint8_t *head) } const uint8_t seq = head[ECAT_CAN_TX_OFF_SEQ]; - /* 窗口下标每周期都必须刷新(主站靠滚动 WIN 扫 27 槽),与 seq 沿无关。 - * 越界值钳到 0,避免读到槽数组外(与 processOutput 同款钳制)。 */ - const uint8_t win = head[ECAT_CAN_TX_OFF_WIN]; - s_winBase = (win < kMotorSlotCount) ? win : 0U; + /* WIN@8 不再使用:1.6.21 电机整轮密排,无滑窗。 */ - /* 上电/重连后首帧只记基准(与 processOutput 的 priming 分支语义一致) */ if (!s_seqPrimed) { s_seqPrimed = true; s_lastSeq = seq; return false; } - /* seq 沿 → 全量读;FLAGS bit0(清统计请求)允许不带 seq 沿(同 processOutput)。 - * s_lastSeq 不在这里推进:全量路径上 processOutput 的沿处理只做一次。 */ if (seq != s_lastSeq) { return true; } @@ -194,32 +242,21 @@ void EthercatCanBridge::processOutput(const uint8_t *out) const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ]; - /* WIN 不在这里取:1.6.0 起 out 只在 seq 沿那拍才是新鲜的,无沿的周期里它是 - * 上一次全量读的残值。在这里回写 s_winBase,等于用开机首拍的 0 每拍盖掉 - * peekOutput() 刚取到的新值 —— 窗口被钉死在基址 0,槽位 7..26(node3 及 - * 以上)永不上报,直到主站真发一帧触发一次全量读才复位。 - * 每拍刷新由 peekOutput()(SSC PDO_OutputMapping 的 12B 探针)独占负责。 */ - - /* 上电/重连后第一次看到输出数据时只记录基准,不发送:否则主站残留的 - * 非零 seq 会被当成一次新请求,在进 OP 的瞬间凭空发出一帧。 */ if (!s_seqPrimed) { s_seqPrimed = true; s_lastSeq = seq; return; } - /* flags bit0:清零统计(一次性,不依赖 seq 沿,便于主站随时归零) */ if ((out[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U) { s_txCount = 0U; s_dropCount = 0U; s_rxCount = 0U; - /* 快照区里残留的那帧早已发布过;不置 published 会让清零后的第一帧 - * 被误计成一次"未取先覆",清零口径就脏了。 */ s_rxPublished = true; } if (seq == s_lastSeq) { - return; /* 没有新的发送请求 */ + return; } s_lastSeq = seq; @@ -229,17 +266,10 @@ void EthercatCanBridge::processOutput(const uint8_t *out) dlc = kEcatCanTxDataMax; } - /* 单帧区先发(保持 1.5.55 语义),再发批量区。 - * dlc==0 表示本次只用批量区,不发单帧 —— 否则会凭空发出一帧 0 长度报文。 */ if (dlc > 0U) { - /* isChannelEnabled 内含越界检查,是"通道号合法且可用"的唯一判据 */ if (!CanFd::isChannelEnabled(ch)) { s_lastTxResult = (uint8_t)CanFd::CanTxResult::kErrInvalid; } else { - /* 用 enqueueTx 而非 transmitFrame:PDO 是周期性场景,可能连续多帧打到 - * 同一通道。transmitFrame 默认 allow_abort=true 会在邮箱满时顶掉已装载 - * 的旧帧(历史上 7 帧突发只发出 3 帧的根因);enqueueTx 队列空时直通、 - * 满时排队,不静默丢帧。 */ const CanFd::CanTxResult r = CanFd::enqueueTx(ch, rd32le(&out[ECAT_CAN_TX_OFF_ID]), &out[ECAT_CAN_TX_OFF_DATA], dlc); @@ -250,7 +280,6 @@ void EthercatCanBridge::processOutput(const uint8_t *out) } } - /* 批量区:同一个 seq 沿里一次性发 N 帧,供多轴同步下发。 */ (void)sendBatch(out); } @@ -259,21 +288,15 @@ void EthercatCanBridge::buildInput(uint8_t *in) if (in == nullptr) { return; } - /* 必须按新的全长清零。旧代码写死 memset(in,0,32),扩容后会把 32 字节以后 - * 的槽区留成上一周期的残留数据,主站读到的是"看起来合法但已过期"的电机状态。 */ memset(in, 0, kEcatPdoInBytes); - /* ── 段 1(0..31):桥状态 + 短快照,布局与 1.5.53 逐字节兼容 ── */ + /* ── 段 1(0..31):桥状态 + 短快照 ── */ in[ECAT_CAN_RX_OFF_ACK] = s_lastSeq; in[ECAT_CAN_RX_OFF_TXRES] = s_lastTxResult; wr16le(&in[ECAT_CAN_RX_OFF_TXCNT], s_txCount); wr16le(&in[ECAT_CAN_RX_OFF_DROP], s_dropCount); wr32le(&in[ECAT_CAN_RX_OFF_RXCNT], s_rxCount); - /* seqlock 判稳(见 onCanFrame):奇数=生产者正在写这组快照。单核下 ISR - * 执行期间生产者冻结,读一次即可定性;本周期跳过段 1/段 3(开头已清零, - * 语义=无有效快照),下周期自愈。批量区(段 4)每槽另有自己的 seqlock, - * 不受本组影响。 */ const bool xrxStable = ((s_xrxSeq & 1U) == 0U); if (xrxStable && s_rxValid) { @@ -285,10 +308,7 @@ void EthercatCanBridge::buildInput(uint8_t *in) in[ECAT_CAN_RX_OFF_DLC] = s_rxDlc; in[ECAT_CAN_RX_OFF_FLAGS] = flags; wr32le(&in[ECAT_CAN_RX_OFF_ID], s_rxId); - /* 段 1 只放前 8 字节(兼容),完整 payload 见段 3 */ memcpy(&in[ECAT_CAN_RX_OFF_DATA], s_rxData, kEcatCanRxDataMax); - /* 本拍已把这一帧发布进过程数据映像(段 3 与本块同门控,故只在此处置位一次): - * 之后再被新帧覆盖属于"最新值"设计意图,不算丢。 */ s_rxPublished = true; } @@ -296,56 +316,11 @@ void EthercatCanBridge::buildInput(uint8_t *in) const uint32_t backlog = CanFd::isChannelEnabled(ch) ? CanFd::txQueueBacklog(ch) : 0U; in[ECAT_CAN_RX_OFF_BACKLOG] = (backlog > 255U) ? 255U : (uint8_t)backlog; - /* ── 段 2(32..319):电机状态窗口 ── - * 直接读 MotorState 聚合表,不再依赖"最近一帧 CAN"。窗口起点由主站通过 - * 输出 PDO 的 WIN 字节指定,主站每周期 +5 即可 6 周期扫完 27 个槽。 - * - * 上下文安全(1.6.5 修正):状态表由主循环的 taskCanRxParse 写,而本函数跑在 - * PDI_Isr(SM 同步模式)/Sync0_Isr(DC 模式),会抢占写侧那次 - * memcpy(slot.data, 48B)。旧注释称"最坏是 seq 与 data 不同步一拍,主站按 seq - * 判新旧即可自愈"是错的:写侧先 seq 后 data,抢占留下的恰好是"新 seq + - * 半新半旧 data",而主站只拿 seq 判新鲜度,分辨不出同一 seq 下 payload 换过 - * 内容,混合值照原样进界面(边界落在 B16/B20/B24 这类 float 符号/指数字节上 - * 就是离谱值)。改用与段 1/段 3 同一套 seqlock(MotorSlot.wr_seq):奇数即 - * 跳过本槽,与本片既有的"本拍不报、下拍自愈"约定一致。 */ - const MotorStateTable &mt = MotorState::table(); - const uint8_t base = (s_winBase < kMotorSlotCount) ? s_winBase : 0U; + /* ── 段 2(32..543):电机整轮密排(空槽不发,count=实际电机数)── */ + (void)fillMotorRound(in); - in[ECAT_MOTW_OFF_ACTIVE] = mt.active_count; - in[ECAT_MOTW_OFF_BOOT] = mt.boot_active; - in[ECAT_MOTW_OFF_WIN_BASE] = base; - wr32le(&in[ECAT_MOTW_OFF_GLOBAL_SEQ], mt.global_seq); - - uint8_t count = 0U; - for (uint8_t i = 0U; i < kEcatMotSlotsPerPdo; ++i) { - const uint8_t idx = (uint8_t)(base + i); - if (idx >= kMotorSlotCount) { - break; /* 窗口尾部越界即停,不回绕:主站据 win_count 判断 */ - } - const MotorSlot &s = mt.slots[idx]; - uint8_t *p = &in[ECAT_MOTW_OFF_SLOTS + (uint16_t)i * kEcatMotSlotSize]; - - /* 槽按 i 定位,跳过的那格照样占位,所以 win_count 报的是跨度而不是实际 - * 填充数:主站 decode_input 对 node_id==0 的块直接 continue。 */ - count = static_cast(i + 1U); - if ((s.wr_seq & 1U) != 0U) { - continue; /* 生产者正在写这 48B:本拍留零,下拍自愈 */ - } - __DMB(); /* 判稳先于取值:不许把下面的读重排到奇偶检查之前 */ - - p[ECAT_MOT_OFF_NODE] = s.node_id; - p[ECAT_MOT_OFF_CH] = s.channel; - p[ECAT_MOT_OFF_ONLINE] = s.online; - p[ECAT_MOT_OFF_DLC] = s.dlc; - wr32le(&p[ECAT_MOT_OFF_SEQ], s.seq); - memcpy(&p[ECAT_MOT_OFF_STATUS], s.data, kMotorStatusDlc); - } - in[ECAT_MOTW_OFF_WIN_COUNT] = count; - - /* ── 段 3(320..359):最近一帧完整快照 ── - * 版本帧(0x00FF)/CONFIG 帧(0x0101) 都是 DLC 24,段 1 的 8 字节装不下。 - * 与段 1 共用 xrxStable 门控:两段必须来自同一帧,否则 ecat.html 的短快照 - * 和电机页的长快照会各自解出半新半旧的 ID/数据。 */ + /* ── 段 3(544..583):最近一帧完整快照 ── + * 与段 1 共用 xrxStable 门控,两段必须来自同一帧。 */ if (xrxStable && s_rxValid) { in[ECAT_XRX_OFF_CH] = s_rxCh; in[ECAT_XRX_OFF_DLC] = s_rxDlc; @@ -354,11 +329,7 @@ void EthercatCanBridge::buildInput(uint8_t *in) memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax); } - /* ── 段 4(360..583):CANFD 批量接收覆盖窗口 ── - * 段 1/段 3 只带"最近一帧",高速多帧场景下周期间到达的帧会互相覆盖 - * (s_dropCount 就是在数这个)。批量区保留最近 N 帧的窗口,让主站在一个 - * 周期内拿到帧序列而不是单个采样点;1.6.15 起改为定长覆盖(见 fillRxWindow)。 */ - fillRxWindow(in); + /* 段 4(历史覆盖窗口)已删除;段 5 由 EthercatPdo::fillDiagTail 写。 */ } void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id, @@ -368,33 +339,15 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id, return; } - /* 缓存区已扩到 32B(段 3 用),truncated 仍按段 1 的 8B 口径上报, - * 这样 ecat.html 的 CAN 调试页语义不变。 */ uint8_t copyLen = length; if (copyLen > kEcatCanXRxDataMax) { copyLen = kEcatCanXRxDataMax; } - /* ── 快照 seqlock(1.5.58)── - * s_rxData/s_rxCh/s_rxId/s_rxDlc/s_rxTruncated/s_rxValid 是一组,必须整体 - * 被 buildInput 读到。DC 模式下 buildInput 跑在 Sync0_Isr(preempt=0),会 - * 抢占本函数(主循环 taskCanRxParse)。原注释声称"valid 最后置位保证一致" - * 是错的:valid 一旦置位就永不清零,ISR 落在"新 payload 已写完、id 还没 - * 更新"的窗口里,会把新帧数据配旧帧 ID 发出去——STATUS 字节被标成 CONFIG - * (0x000N0101),前端按 CONFIG 布局解码出 -3e26 这类离谱目标值。 - * 协议:seq 奇数=正在写。消费者在单核 ISR 里生产者必然冻结,判一次奇偶 - * 即可,奇数则本周期跳过(段 1/段 3 已清零=无效),下周期自愈。 - * 不关中断:critical.h 的 BASEPRI 方案放行 preempt=0,挡不住 Sync0; - * 而 seqlock 零抖动,与本文件既有无锁约定一致。 - * __DMB 防编译器把非 volatile 的 memcpy 挪出两次 seq 自增之间。 */ - ++s_xrxSeq; /* → 奇数:快照进入"正在写" */ + /* ── 快照 seqlock:奇数=正在写,消费者本拍跳过段 1/段 3 ── */ + ++s_xrxSeq; __DMB(); - /* 「未取先覆」计数:上一帧还没被任何一拍 PDO 发布就被覆盖才算丢。 - * 原判据只判 s_rxValid,而它置位后永不清零 → 第一帧之后每帧都计, - * drop 恒等于 rx−1(u16 回绕),变成跟着收帧速率涨、零灵敏度的假仪表。 - * 放在奇数窗口内:消费者判到奇会跳过段 1/段 3、不会在此刻置 published, - * 故"读上一帧发布状态 + 自增"对消费者等效原子,不会多计一次。 */ if (s_rxValid && !s_rxPublished) { ++s_dropCount; } @@ -406,67 +359,17 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id, s_rxCh = channel; s_rxId = id; - s_rxDlc = length; /* 报告原始长度,便于主站发现被截断 */ + s_rxDlc = length; s_rxTruncated = (length > kEcatCanRxDataMax); ++s_rxCount; s_rxValid = true; - s_rxPublished = false; /* 这一帧还没被任何一拍 PDO 发布 */ + s_rxPublished = false; __DMB(); - ++s_xrxSeq; /* → 偶数:快照整体一致 */ + ++s_xrxSeq; - /* ── 写入批量接收覆盖窗口(1.6.15,取代 1.5.56 的环形队列)── - * 与上面的"最近一帧快照"并行维护:快照只留最新一帧,窗口留最近 N 帧的序列。 - * 生产者按自己的游标写、绕圈覆盖,**不等消费者** —— 没有判满、没有背压、 - * 没有 pending。代价是语义从"事件流"变成"最近 N 帧窗口":一帧在窗口里活到 - * 被覆盖为止,主站按每槽 seq 去重后才归一成事件流。 - * - * **STATUS 帧不入窗**(1.6.9):批量区存在的意义是捞那些段 2 装不下的帧 - * (CONFIG / 版本 / 节点应答),而完整 STATUS 本来就由段 2 电机状态窗口按槽 - * 上报;主站 transport-ecat.js 的 drainRx() 拿到 STATUS 也是直接 continue - * 丢掉。让它占窗口是纯重复,而且正是它把队列压爆的:实测 CAN 侧约 4000 帧/s, - * 旧批量区每拍最多回收 3 条(=3000 帧/s),净积压使队列长期饱和,才逼出了 - * 生产者覆写消费者槽位的竞态(错标率 1.77%,表现为"CONFIG 的 ID 配 STATUS - * 的内容")。滤掉 STATUS 后入窗流量降到 CONFIG 量级。 - * - * OVF 只数"**未取先覆**":覆盖一帧主站已经取走的数据是窗口的设计意图(不计), - * 覆盖一帧主站还没来得及取的数据才是真丢帧(计数)。该数与主站按 seq 空洞 - * 算出的丢帧数应当一致 —— 两侧独立仪表互相对账,任一侧骗人都当场暴露。 */ - if ((id & 0xFFFFU) == 0x0100U) { - return; - } - { - const uint8_t idx = s_rxWinWr; - RxWinSlot &sl = s_rxWin[idx]; - - if (s_rxWinSeen[idx] == 0U && sl.seq != 0U && s_rxWinOvf < 0xFFU) { - ++s_rxWinOvf; /* 上一帧还没被任何一拍 PDO 取走就被覆盖 */ - } - s_rxWinSeen[idx] = 0U; - - /* 每槽 seqlock:奇数=正在写。消费者(PDI_Isr)落在这一瞬就跳过该槽,下拍 - * 自愈。没有它,抢占在"新 payload 已写完、id 还没更新"之间,主站会拿到 - * 一条现实中不存在、但看起来完全合法的假帧 —— 那不是"差一个周期"。 - * 发布值取自全局计数器 s_rxWinSeq,故跨槽可比大小、可数空洞。 */ - sl.seq = s_rxWinSeq + 1U; /* → 奇数:本槽进入"正在写" */ - __DMB(); - - uint8_t n = length; - if (n > kEcatCanFdDataMax) { - n = kEcatCanFdDataMax; - } - sl.ch = channel; - sl.dlc = n; - sl.id = id; - sl.flags = 0x01U; /* bit0=本槽至少写过一帧 */ - memcpy(sl.data, data, n); - - s_rxWinSeq += 2U; /* 下一个发布值(恒偶) */ - __DMB(); /* 字段写入先于 seq 发布落地 */ - sl.seq = s_rxWinSeq; /* → 偶数:整槽一致 */ - - s_rxWinWr = (uint8_t)((idx + 1U) % ECAT_RXB_SLOTS); - } + /* STATUS 走 MotorState → 段 2 整轮;非 STATUS 只更新段 1/3 最近一帧快照。 + * 1.6.21 起不再写入历史覆盖窗口。 */ } extern "C" { diff --git a/GD32E508_DCU_APP/comm/ethercat_can_bridge.h b/GD32E508_DCU_APP/comm/ethercat_can_bridge.h index 30907f5..088c830 100644 --- a/GD32E508_DCU_APP/comm/ethercat_can_bridge.h +++ b/GD32E508_DCU_APP/comm/ethercat_can_bridge.h @@ -1,9 +1,9 @@ /** * @file ethercat_can_bridge.h - * @brief EtherCAT PDO ↔ CAN 转发桥(1.5.55)。 + * @brief EtherCAT PDO ↔ CAN 转发桥(1.6.21:段 2 改 9 电机密排整轮)。 * - * 输出 PDO(0x7000, 主站→从站, 64B) = 待发送的 CAN 帧(数据区 32B) - * 输入 PDO(0x6000, 从站→主站, 384B) = 桥状态 + 电机状态窗口 + 长帧快照 + * 输出 PDO(0x7000, 主站→从站, 384B) = 待发送的 CAN 帧(数据区 32B) + * 输入 PDO(0x6000, 从站→主站, 600B) = 桥状态 + 电机整轮状态 + 长帧快照 + 诊断 * * 发送用**序号沿触发**:仅当 seq 字节变化时才真正发一帧。PDO 是周期性刷新的, * 若按「内容非空即发」会把同一帧每个周期重发一次;seq 变化沿是 PDO 里表达 @@ -13,40 +13,23 @@ * TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态 * 会随机丢帧。改为由 taskCanRxParse() 的分发链回调 onCanFrame() 投递。 * - * ── 1.5.55 为电机控制台扩容(32B/32B → 64B/384B)── + * ── 1.6.21:段 2 改「一帧 = 总线电机整轮状态」,删段 4 历史帧窗口 ── * - * 三个动机: - * 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) 会被截断。 + * 旧布局的问题(用户实测:接 3 个电机网页却看到 5 个状态帧): + * 1. 段 2 是固定 5 槽滑窗(win_base 要 6 拍才扫完 27 槽),空槽 node_id=0 + * 也占位,win_count 报的是跨度不是电机数 —— 主站若不滤空槽就会多报; + * 2. 段 4 覆盖窗口把最近几条非 STATUS 帧反复重发,属于「历史帧 / 重复帧」。 * - * **输入 PDO 前 32 字节与 1.5.53 逐字节兼容**,偏移与语义全部不变。这样 - * tools/ecat 的 CAN 调试页(ecat.html)与 ecat_server.py 的 _can_decode_input - * 不改一行也照常工作,扩容只是在后面追加新区段。 + * 本版语义改为用户需求: + * · 每一拍输入 PDO 携带**总线上已识别电机的完整一轮状态**(最多 9 个); + * · **空槽不发送**:密排,count = 本拍实际打包的电机数; + * · 主站按 count 迭代,每槽自带 node_id,自动识别,无需 win_base 滑窗; + * · 不发段 4 历史帧;版本/CONFIG 仍走段 1/段 3「最近一帧」快照; + * · 上一轮已打包且 seq 未变的电机,本轮跳过 MotorState 重读与 48B 拷贝, + * 直接复用影子区(s_motShadow)。 * - * ── 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..:批量接收,一周期回收多帧而不是只带最近一帧 - * (1.6.1 数据条目 64→32B;1.6.15 改定长覆盖窗口 5×44B 到 583, - * 段 5 诊断区 484..495 → 584..595,输入总长 496 → 600) - * - * 三条兼容性铁律(改动只做追加,不动既有偏移): - * 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。 + * 输入 PDO 总长仍 600B ⇒ 无需重刷 EEPROM/SII;段 2/3 偏移变了,**主站解码 + * 必须同步**(tools/ecat 的 decode_input / transport-ecat.js 等)。 */ #ifndef COMM_ETHERCAT_CAN_BRIDGE_H #define COMM_ETHERCAT_CAN_BRIDGE_H @@ -54,20 +37,21 @@ #include #include "comm/ethercat_pdo_size.h" +#include "device/motor_state.h" /* kMotorMaxNodes / kMotorChannels */ -/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。**与 1.5.55 完全兼容**。 */ +/** 输出 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_WIN = 8U, /**< 1.6.21 起忽略(电机整轮密排,无滑窗);保留占位 */ + /* 9..15 保留:让数据区落在 16 字节边界 */ ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */ }; -/** 输出 PDO 段 2(48..383):CANFD 批量发送区(1.5.56 新增)。 */ +/** 输出 PDO 段 2(48..383):CANFD 批量发送区。 */ enum : uint16_t { ECAT_TXB_OFF_COUNT = 48U, /**< 本次要发的帧数 0..kEcatTxBatchMax;0=不发批量 */ ECAT_TXB_OFF_FLAGS = 49U, /**< bit0=1 遇错即停(默认继续发完并各自记结果) */ @@ -85,7 +69,7 @@ enum : uint8_t { ECAT_FRM_HDR_SIZE = 8U, /**< 头长度,数据紧跟其后 */ }; -/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照。**与 1.5.53 完全兼容**。 */ +/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照。 */ enum : uint8_t { ECAT_CAN_RX_OFF_ACK = 0U, /**< 回显已执行的发送序号,主站据此确认送达 */ ECAT_CAN_RX_OFF_CH = 1U, /**< 该帧来自哪个通道 */ @@ -97,231 +81,134 @@ enum : uint8_t { 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, /**< 段 1/段 3 快照「未取先覆」次数:上一帧尚未被任何一拍 - PDO 发布就被新帧覆盖才 +1(u16,累计回绕)。 */ + ECAT_CAN_RX_OFF_DROP = 24U, /**< 段 1/段 3 快照「未取先覆」次数(u16 累计回绕) */ }; -/** 输入 PDO 段 2(32..319):电机状态窗口头 + 槽数组。 */ +/** + * 输入 PDO 段 2(32..543):电机整轮状态(密排,1.6.21)。 + * + * 主站解析: + * count = in[32] // 0..9,本拍实际打包的电机数 + * for (i = 0; i < count; i++) + * slot = in[40 + i*56] // node_id/online/dlc/seq/status + * + * 空节点不占位;槽序按 node_id 升序;同号多通道取 seq 最新的一条。 + */ 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 正好相接) */ + ECAT_MOTW_OFF_COUNT = 32U, /**< 本拍密排电机数 0..9(主站按此迭代) */ + ECAT_MOTW_OFF_ACTIVE = 33U, /**< 在线电机数(与 count 可不同:离线但仍报最后一轮) */ + ECAT_MOTW_OFF_BOOT = 34U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */ + ECAT_MOTW_OFF_MAGIC = 35U, /**< 恒 kEcatMotLayoutMagic(0xA5):1.6.21 密排标识 */ + ECAT_MOTW_OFF_GLOBAL_SEQ = 36U, /**< 状态表全局刷新序号,小端 uint32 */ + ECAT_MOTW_OFF_SLOTS = 40U, /**< 密排槽数组起点,count×56B */ }; /** 电机状态槽内部布局(56 字节)。 */ enum : uint8_t { - ECAT_MOT_OFF_NODE = 0U, /**< 节点号 1..9,0=空槽 */ + 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) */ + ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,48 字节 */ }; -/** 输入 PDO 段 3(320..359):最近一帧**完整**快照(版本/CONFIG 帧要 24B)。 */ +/** 输入 PDO 段 3(544..583):最近一帧完整快照(版本/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 */ + ECAT_XRX_OFF_CH = 544U, /**< 通道 */ + ECAT_XRX_OFF_DLC = 545U, /**< 原始 DLC(可达 32) */ + ECAT_XRX_OFF_FLAGS = 546U, /**< bit0=有效 */ + ECAT_XRX_OFF_RSVD = 547U, + ECAT_XRX_OFF_ID = 548U, /**< CAN ID,小端 uint32 */ + ECAT_XRX_OFF_DATA = 552U, /**< 数据区,32 字节完整 payload */ }; -/** 输入 PDO 段 4(360..583):CANFD 批量接收**定长覆盖窗口**(1.6.15)。 - * - * 与旧"环形队列 + 变长紧排排空"的区别:生产者(onCanFrame,主循环)按自己的 - * 游标写槽、绕圈覆盖谁就是谁;消费者(fillRxWindow,PDI_Isr)每拍整块取值。 - * 没有 tail、没有判满、没有 pending —— 1.6.9 修的那个"滞后的 tail 让生产者 - * 写进消费者刚读过的槽"错标竞态,其载体(tail 握手)在本布局里不存在了。 - * - * 定长是"整块取"的前提:槽下标 = (ECAT_RXB_OFF_ITEMS + i*ECAT_RXB_SLOT_SIZE), - * 主站按固定偏移读,不再需要"8 + min(dlc,上限)"推游标,也就不可能再出现 - * 1.6.10 那种游标越进诊断区的边界错。 - * - * 每槽自带 seqlock(ECAT_RXW_OFF_SEQ):PDI_Isr(preempt=0) 会抢占主循环写侧, - * 没有它主站能拿到"新 ID + 旧数据"拼出来的假帧 —— 那不是"差一个周期",是 - * 现实中不存在但看起来完全合法的帧。seq 同时是主站去重与算丢帧的依据。 - * - * 语义变更(相对旧批量区):**最近 N 帧窗口**,不是"保证不丢的事件流"。 - * 同一帧会在连续几拍里重复出现(直到被覆盖),低频一次性帧(版本/节点应答) - * 在突发流量下可能被静默覆盖 —— 主站侧按 seq 去重后归一成事件流再上报, - * 并可用 seq 空洞算出被覆盖掉的帧数。 */ +/** 输入 PDO 段 5(584..595):设备诊断区(位置未变)。 */ enum : uint16_t { - ECAT_RXB_OFF_SLOT_COUNT = 360U, /**< 窗口槽数(编译期常量 ECAT_RXB_SLOTS) */ - ECAT_RXB_OFF_WR_IDX = 361U, /**< 最近一次写入的槽下标 0..N-1(诊断节拍用) */ - ECAT_RXB_OFF_OVF = 362U, /**< 自上次回填以来,写入了"主站尚未取过"槽的次数 - (饱和 255),回填后清零。与主站 seq 空洞是两个 - 独立仪表,但**度量窗口不同**:本计数是"还没拷进 - PDO 镜像就被覆写"(亚拍级),主站算的是"两次解码 - 之间被覆写"。主站与从站同拍(1kHz)时两者应一致; - 主站掉拍时主站数更大,那是真实差异不是仪表坏了。 */ - ECAT_RXB_OFF_RSVD = 363U, - ECAT_RXB_OFF_ITEMS = 364U, /**< 槽数组起点,N × ECAT_RXB_SLOT_SIZE 定长 */ -}; - -/** 覆盖窗口单槽布局(44 字节)。头 12B + 数据 32B。 */ -enum : uint8_t { - ECAT_RXW_OFF_CH = 0U, /**< 通道 0/1/2 */ - ECAT_RXW_OFF_DLC = 1U, /**< **原始** DLC 0..64(不是线上长度) */ - ECAT_RXW_OFF_FLAGS = 2U, /**< bit0=本槽至少写过一帧; bit1=线上数据被截断到 32B */ - ECAT_RXW_OFF_RSVD = 3U, - ECAT_RXW_OFF_ID = 4U, /**< CAN ID,小端 uint32 */ - ECAT_RXW_OFF_SEQ = 8U, /**< 全局单调 seq,小端 u32。奇数=写入中(消费者跳过); - 偶数=整槽一致。跨槽共用一个计数器,故主站可比大小 - 排序、可用空洞数出被覆盖的帧。 - **发布值恒为 2,4,6,…(奇数被"写入中"占用),所以 - 相邻两帧的 seq 差 2**:主站算空洞必须用 step/2-1, - 按 step-1 会把每帧都虚报成丢 1 个(1.6.15 首版踩过, - 症见 can_cadence 每拍 count=2/lost=2 而 OVF=0)。 */ - ECAT_RXW_OFF_DATA = 12U, /**< 数据区,32 字节(kEcatRxBatchDataMax) */ -}; -/** 覆盖窗口单槽字节数:12B 头 + 32B 数据。改任一字段宽度都要重算总预算。 */ -constexpr uint8_t ECAT_RXB_SLOT_SIZE = 44U; -/** 覆盖窗口槽数。5 槽 = 5000 帧/s@1ms;必须 ≥ 每拍到达帧数,否则静默丢帧。 */ -constexpr uint8_t ECAT_RXB_SLOTS = 5U; - -/** 输入 PDO 段 5(584..595):设备诊断区(1.5.61 尾部追加,1.6.1 前移到 484, - * 1.6.15 随批量覆盖窗口扩到 5 槽再前移到 584:364+5×44=584, - * 诊断区锚在紧随其后的 12 字节,写侧是 EthercatPdo::fillDiagTail())。 */ -enum : uint16_t { - ECAT_DIAG_OFF_LOAD = 584U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%)。 - 中断 + 非延迟处理段(任务派发/OTA relay)占墙钟比, - 不含常开轮询与空转;1s 滚动窗口(见 sys/cpu_load.h) */ - ECAT_DIAG_OFF_TORN = 586U, /**< CAN 取帧撕裂丢帧数(低 16 位,小端,饱和不回绕)。 - 1.6.6 起:ISR 取帧时若 MP 的 TS/DLENC 前后不一致, - 说明邮箱窗口在取数途中被硬件换帧,该帧整帧丢弃并 - 在此计数。这是"读到半条帧"的唯一可观测量 —— 主站 - 据此判断修复是否生效、总线负载是否高到频繁触发。 */ - ECAT_DIAG_OFF_SEQ = 588U, /**< 诊断区序号,小端 uint16(1.6.12 起,原 u32 让出 - 高 2 字节),每完成一个 1s 统计窗口 +1,主站据此 - 判断负载值是否仍在滚动刷新。u16 足够:主站只判 - "非 0 且在涨",18h 才回绕。 */ - ECAT_DIAG_OFF_OVERFULL = 590U, /**< CAN RX FIFO 满溢计数 RFO0(u16 饱和,1.6.12)。 - 1.6.11 把溢出策略改成"满则丢新帧"后拼接并未消失 - (段5@594 以 ~384/s 增长),必须直接观测溢出本身 - 与拼接率是否同涨,才能判定残余拼接是否仍源自溢出 - (即本 IP 在 DISCARD 模式下照样毁窗口)。 */ - ECAT_DIAG_OFF_RELTIMEOUT = 592U, /**< RFD0 释放握手超时计数(u16 饱和,1.6.12)。 - 原为 1.6.7 的 PDO 重入计数 ECAT_DIAG_OFF_REENTRY - (引入至今恒 0,不再上线,仍留内部计数)。取帧后 - 等 RFD0 自清 2000 次未果 = dequeue 未完成,窗口 - 与读指针可能已失同步 —— 拼接的直接前兆。 */ - ECAT_DIAG_OFF_REJECT = 594U, /**< CAN 内容级拼接帧命中率(u16 饱和,1.6.11)。 - 入队前结构自洽校验(STATUS 温度量程+斜率、CONFIG - 未使用区/模式)命中即计数。1.6.12 起**只计不丢** - —— 数据管道必须诚实,该计数退化为"源头是否还在 - 产生拼接帧"的纯测量仪表:修好 = 恒 0,没修好 = - 网页看到什么,计数就是什么的倍率。 */ + ECAT_DIAG_OFF_LOAD = 584U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%) */ + ECAT_DIAG_OFF_TORN = 586U, /**< CAN 取帧撕裂丢帧数(低 16 位,饱和) */ + ECAT_DIAG_OFF_SEQ = 588U, /**< 诊断区序号,小端 uint16,每完成 1s 窗口 +1 */ + ECAT_DIAG_OFF_OVERFULL = 590U, /**< CAN RX FIFO 满溢计数 RFO0(u16 饱和) */ + ECAT_DIAG_OFF_RELTIMEOUT = 592U, /**< RFD0 释放握手超时计数(u16 饱和) */ + ECAT_DIAG_OFF_REJECT = 594U, /**< CAN 内容级拼接帧命中计数(u16 饱和) */ }; /** 单帧数据在输出 PDO / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */ constexpr uint8_t kEcatCanTxDataMax = 32U; -/** 段 1 短快照数据区字节数(保持 1.5.53 兼容,勿改)。 */ +/** 段 1 短快照数据区字节数。 */ 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; +/** 单个电机状态槽字节数:8B 头 + 48B STATUS。 */ +constexpr uint8_t kEcatMotSlotSize = 56U; +/** 一拍最多打包的电机数 = 节点号 1..9 各一槽(同号多通道取最新)。 */ +constexpr uint8_t kEcatMotSlotsPerPdo = kMotorMaxNodes; /* 9 */ +/** 段 2 头长度(COUNT..GLOBAL_SEQ,8B)。 */ +constexpr uint8_t kEcatMotHdrSize = 8U; +/** 段 2 密排区最大字节:头 8B + 9×56B = 512B,落在 32..543。 */ +constexpr uint16_t kEcatMotRegionBytes = + (uint16_t)(kEcatMotHdrSize + + (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize); +/** 布局魔数,写在 offset 35。主站据此区分 1.6.21 密排与 1.6.15 滑窗(两者 + * Ibytes 同为 600,不能只靠长度分支)。0=旧布局。 */ +constexpr uint8_t kEcatMotLayoutMagic = 0xA5U; -/** 过程数据长度,唯一真源见 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; /* 75×64bit = 600B */ +/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h。 */ +constexpr uint16_t kEcatPdoOutBytes = ECAT_PDO_OUT_BYTES; /* 384B */ +constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 600B */ -/** CANFD 单帧数据上限(DLC 64)—— 覆盖窗口/发送批量区按此上限做容量推导。 */ +/** CANFD 单帧数据上限。 */ constexpr uint8_t kEcatCanFdDataMax = 64U; -/** 批量**接收**槽在 PDO 内的数据字节数上限(1.6.1 从 64 压到 32)。 - * 真机采样 6086 帧 DLC 只有 24/32 两档;超长帧在装填时截断(flags bit1), - * RAM 侧 RxWinSlot 仍存全量 64B,截断只发生在拷进 PDO 这一步。 - * 将来若要线上传 64B:槽长变 12+64=76,5 槽需 364+380+12=756B > 640 上限, - * 届时只能降到 3 槽(364+228+12=604 → 输入 608B),即"全量"与"帧数"二选一。 */ -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)); -/* 布局守卫:段 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_SLOT_COUNT, - "长帧快照区与批量接收区重叠"); +/* ── 布局守卫 ── */ +static_assert(ECAT_MOTW_OFF_SLOTS + + (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize + == ECAT_XRX_OFF_CH, + "段2密排区必须与段3首尾相接"); +static_assert(ECAT_XRX_OFF_DATA + kEcatCanXRxDataMax == ECAT_DIAG_OFF_LOAD, + "段3必须与段5首尾相接"); 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(ECAT_RXW_OFF_DATA + kEcatRxBatchDataMax <= ECAT_RXB_SLOT_SIZE, - "覆盖窗口槽长容不下头+数据"); -static_assert(ECAT_RXW_OFF_DATA + kEcatRxBatchDataMax == ECAT_RXB_SLOT_SIZE, - "覆盖窗口槽长有空洞:定长布局不应留空隙"); -/* 整个覆盖窗口 + 段 5 诊断区必须落在输入 PDO 内。这是 600B 这个数的来源: - * 少一个字节都会在 SAFEOP 报 AL 0x001A/0x0011,而根因在这里编译期就能抓到。 */ -static_assert(ECAT_RXB_OFF_ITEMS + (uint16_t)ECAT_RXB_SLOTS * ECAT_RXB_SLOT_SIZE - <= ECAT_DIAG_OFF_LOAD, "覆盖窗口与段5诊断区重叠"); static_assert(ECAT_DIAG_OFF_REJECT + 2U <= kEcatPdoInBytes, "段5诊断区越界"); -/* 窗口至少要能容下一拍可能到达的帧数:槽数 < 帧/拍 时超出部分会在两次 PDO - * 拷贝之间被覆盖,属静默丢帧(主站只能靠 seq 空洞事后发现)。5 槽对现场 - * 4000 帧/s(=4 帧/拍) 留 25% 余量。 */ -static_assert(ECAT_RXB_SLOTS >= 5U, "覆盖窗口槽数低于现场每拍到达帧数"); +static_assert(ECAT_MOTW_OFF_COUNT + kEcatMotRegionBytes <= ECAT_XRX_OFF_CH, + "段2密排区越界"); /** * @brief EtherCAT ↔ CAN 转发桥。 * @note 全静态;PDO 侧函数在 Sync0_Isr(preempt=0) 或主循环 MainLoop 上下文执行, * onCanFrame() 在主循环 taskCanRxParse() 上下文执行。共享状态用 volatile + - * 单生产者/单消费者语义,不需要临界区(投递只写、消费只读+清标志)。 - * 例外是"多字段一组"的三处,全部用 seqlock 而不是握手: - * - 段 1/段 3 的"最近一帧快照"(id/dlc/data 必须配套)→ s_xrxSeq; - * - 段 2 电机槽(48B payload 一次 memcpy)→ MotorSlot.wr_seq; - * - 段 4 覆盖窗口每槽(1.6.15)→ RxWinSlot.seq。 + * 单生产者/单消费者语义。多字段一组用 seqlock: + * - 段 1/段 3 的「最近一帧快照」→ s_xrxSeq; + * - 电机槽 48B payload → MotorSlot.wr_seq。 * Sync0 是 preempt=0,critical.h 的 BASEPRI 方案挡不住它,故只能无锁协议。 - * 单核下 ISR 执行期间生产者必然冻结,所以读侧判一次奇偶即可定性。 */ class EthercatCanBridge { public: /** - * @brief 处理一份输出 PDO:seq 变化时向 CAN 发一帧。 - * @param[in] out kEcatPdoOutBytes 字节输出过程数据。 - * @note 由 APPL_Application() 每周期调用。 + * @brief 输出探针:seq 变化时请求全量读。 + * @note 1.6.21 起不再刷新 win_base(电机整轮密排,无滑窗);仍处理 seq 沿 + * 与 FLAGS bit0 清统计。 */ - /** @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 字节输入过程数据(本函数负责全部写满)。 + * @brief 填充输入 PDO:桥状态 + 电机整轮密排 + 长帧快照。 + * @param[out] in kEcatPdoInBytes 字节输入过程数据。 */ static void buildInput(uint8_t *in); /** - * @brief CAN 收帧投递入口。 - * @note 由 TaskRunner::taskCanRxParse() 在分发链里调用,不消费帧(不返回 - * "已处理"),只做旁路复制,因此不影响 OTA relay / 电机状态。 + * @brief CAN 收帧投递入口(旁路复制,不消费帧)。 */ static void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t length); @@ -331,33 +218,21 @@ public: private: /** - * @brief 处理输出 PDO 的批量发送区(seq 沿内调用一次)。 - * @return 实际受理的帧数。 - * @note 逐帧解析 (8B 头 + dlc),边界不足即停 —— 不信任主站给的 count, - * 越界的 count 只会让循环提前结束,不会读到 PDO 之外。 + * @brief 填充段 2 电机整轮密排区(含差分跳过)。 + * @return 本拍打包的电机数(写入 count 字段)。 */ + static uint8_t fillMotorRound(uint8_t *in); + static uint8_t sendBatch(const uint8_t *out); - /** @brief 把批量接收覆盖窗口整块拷进输入 PDO 段 4(1.6.15)。 */ - static void fillRxWindow(uint8_t *in); - - static volatile uint8_t s_lastSeq; /**< 上一次已执行的发送序号 */ - static volatile bool s_seqPrimed; /**< 是否已记录过基准 seq(防上电首帧误发) */ + static volatile uint8_t s_lastSeq; + static volatile bool s_seqPrimed; 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 → 前端解出离谱目标值)。 */ + /* 最近一帧接收快照:taskCanRxParse 写、PDO 读。s_xrxSeq 为 seqlock。 */ static volatile bool s_rxValid; - /** 上一帧是否已被至少一拍 PDO 发布进段 1/段 3。s_rxValid 只表示"开机以来收过帧" - * (flags bit0 与 backlog 取通道都依赖它常驻),不能拿它当"未取走"判据, - * 否则 drop 会恒等于 rx 计数。写侧(onCanFrame)置 false,读侧(buildInput - * 发布分支)置 true;单核下 ISR 抢占主循环,两者不会并发写同一帧。 */ static volatile bool s_rxPublished; static volatile uint32_t s_xrxSeq; static volatile uint8_t s_rxCh; @@ -367,28 +242,16 @@ private: static volatile uint32_t s_rxCount; static uint8_t s_rxData[kEcatCanXRxDataMax]; - /* ── 批量接收定长覆盖窗口(1.6.15,取代 1.5.56 的环形队列)── - * 生产者(onCanFrame,主循环)按 s_rxWinWr 写槽并绕圈覆盖;消费者 - * (fillRxWindow,PDI_Isr/Sync0_Isr)每拍整块取值,**不消费、不清除**。 - * 因此没有 head/tail 握手,也就没有 1.6.9 那类"滞后的 tail 让生产者写进 - * 消费者刚读过的槽"的错标窗口。 - * 一致性靠每槽 seqlock:写侧 seq 置奇 → 填字段 → __DMB → seq 置偶(=++全局 - * 计数)。读侧在 ISR 里,单核下生产者必然冻结,判一次奇偶即可,奇数跳过该槽。 - * s_rxWinSeen 用于数 OVF:写到一个 seen==0 的槽 = 覆盖了一帧主站还没取过的 - * 数据,这才是真丢帧;覆盖已取过的槽是设计意图,不计。 */ - struct RxWinSlot { - volatile uint32_t seq; /**< 奇=写入中;偶=整槽一致 */ - uint32_t id; - uint8_t ch; - uint8_t dlc; /**< 原始 DLC 0..64 */ - uint8_t flags; /**< bit0=写过; bit1=线上截断到 32B */ - uint8_t data[kEcatCanFdDataMax]; /**< RAM 侧存全量 64B,PDO 拷贝时才截断 */ - }; - static RxWinSlot s_rxWin[ECAT_RXB_SLOTS]; - static volatile uint8_t s_rxWinWr; /**< 写游标(下一个要覆盖的槽) */ - static volatile uint32_t s_rxWinSeq; /**< 全局单调 seq 源(偶数发布) */ - static volatile uint8_t s_rxWinOvf; /**< 未取先覆次数(饱和,回填后清零) */ - static volatile uint8_t s_rxWinSeen[ECAT_RXB_SLOTS]; /**< 1=已被某拍 PDO 取过 */ + /* ── 段 2 差分填充影子(1.6.21)── + * 上一拍已发布的密排区副本 + 各节点上次发布的 seq/online + 头字段。 + * 节点集合、seq、online、active/boot 全未变时整段复用,跳过 MotorState + * 重读与逐槽 48B 拷贝。掩码必须 u16:节点号 1..9,bit9 超出 uint8_t。 */ + static uint8_t s_motShadow[kEcatMotRegionBytes]; + static uint32_t s_motPubSeq[kMotorMaxNodes + 1U]; /* 下标=节点号 */ + static uint8_t s_motPubOnline[kMotorMaxNodes + 1U]; /* checkOffline 不 bump seq,须单独比 */ + static uint16_t s_motPubMask; /**< bit n=1:节点 n 在影子区中 */ + static uint8_t s_motPubCount; + static bool s_motShadowValid; }; extern "C" { diff --git a/GD32E508_DCU_APP/comm/ethercat_diff_stats.cpp b/GD32E508_DCU_APP/comm/ethercat_diff_stats.cpp index 85fafb3..a157bbb 100644 --- a/GD32E508_DCU_APP/comm/ethercat_diff_stats.cpp +++ b/GD32E508_DCU_APP/comm/ethercat_diff_stats.cpp @@ -25,10 +25,10 @@ constexpr uint8_t kSegCount = 5U; /** * @brief 各段字节上界(开区间),用于把变化量归到布局段落。 - * @note 584..599 的"段5 诊断 + 4B 保留"合并计:差分写不会单独优化保留区, - * 拆开只是多一档,但保留区若在涨就说明有东西在越界写,所以仍要看得见。 + * @note 1.6.21 布局:段1(0..31) 段2(32..543) 段3(544..583) 段4 已删 段5(584..599)。 + * 段4 上界=段5 起点,区间为空、恒 0,子索引保留以免动对象字典。 */ -constexpr uint16_t kSegEnd[kSegCount] = {32U, 320U, 360U, 584U, kInBytes}; +constexpr uint16_t kSegEnd[kSegCount] = {32U, 544U, 584U, 584U, kInBytes}; /** @brief 上一份已发布的镜像。alignas(4) 是让优化器能把 4 字节 memcpy 折成单条 * LDR 的前提——丢了它,比对会从 ~150 次字访问退化成 600 次字节访问。 */ diff --git a/GD32E508_DCU_APP/comm/ethercat_diff_stats.h b/GD32E508_DCU_APP/comm/ethercat_diff_stats.h index bb79db7..530cc20 100644 --- a/GD32E508_DCU_APP/comm/ethercat_diff_stats.h +++ b/GD32E508_DCU_APP/comm/ethercat_diff_stats.h @@ -50,9 +50,9 @@ public: static uint32_t runsTotal() noexcept; /**< sub16 窗口内 run 总数(配 cycles 求均值) */ static uint32_t runsMax() noexcept; /**< sub17 单拍 run 数峰值,A2 的 N 由它定 */ static uint32_t seg1Bytes() noexcept; /**< sub18 段1(0..31) 桥状态+短快照 */ - static uint32_t seg2Bytes() noexcept; /**< sub19 段2(32..319) 电机状态窗口 */ - static uint32_t seg3Bytes() noexcept; /**< sub20 段3(320..359) 长帧快照 */ - static uint32_t seg4Bytes() noexcept; /**< sub21 段4(360..583) 批量覆盖窗 */ + static uint32_t seg2Bytes() noexcept; /**< sub19 段2(32..543) 电机整轮密排 */ + static uint32_t seg3Bytes() noexcept; /**< sub20 段3(544..583) 长帧快照 */ + static uint32_t seg4Bytes() noexcept; /**< sub21 已删(1.6.21 恒 0) */ static uint32_t seg5Bytes() noexcept; /**< sub22 段5(584..599) 诊断区+保留 */ static uint32_t compareCyc() noexcept; /**< sub23 比对自身累计 DWT 周期(本征开销) */ }; diff --git a/GD32E508_DCU_APP/comm/ethercat_pdo.h b/GD32E508_DCU_APP/comm/ethercat_pdo.h index 1d721d0..336292f 100644 --- a/GD32E508_DCU_APP/comm/ethercat_pdo.h +++ b/GD32E508_DCU_APP/comm/ethercat_pdo.h @@ -52,8 +52,9 @@ public: * @brief 输出探针(1.6.0 输出按需全量读,配合 SSC PDO_OutputMapping)。 * @param[in] head 输出 PDO 头 12B(覆盖 SEQ@0 / WIN@8 / FLAGS@3)。 * @return true=需要全量读(seq 沿或清统计请求);false=本周期跳过全量读。 - * @note 每周期由 PDI_Isr 在读 ESC 前调用:内部刷新桥的 s_winBase 并做 seq - * 基准 priming;s_lastSeq 的推进仍留在全量路径(processOutput)。 + * @note 每周期由 PDI_Isr 在读 ESC 前调用:做 seq 基准 priming 与沿判定; + * s_lastSeq 的推进仍留在全量路径(processOutput)。1.6.21 起不再 + * 维护 win_base(段 2 电机整轮密排,无滑窗)。 */ static bool outputProbe(const uint8_t *head); diff --git a/GD32E508_DCU_APP/comm/ethercat_pdo_size.h b/GD32E508_DCU_APP/comm/ethercat_pdo_size.h index 3e9aa78..b73a9ea 100644 --- a/GD32E508_DCU_APP/comm/ethercat_pdo_size.h +++ b/GD32E508_DCU_APP/comm/ethercat_pdo_size.h @@ -82,6 +82,10 @@ * 算出的丢帧数与从站侧 OVF 是否一致)。 * 600 = 75×8;SM3 实占 3×600=1800B(0x1880-0x1F67),out+in=984 ≤ 1024 硬上限, * DPRAM 仍留 120B 余量。改这里必须同步重生成 SII(gen_eeprom.py)并断电冷启动。 + * + * ── 1.6.21:总长仍 600B,内部布局改「电机整轮密排」── + * 段 2 扩到 32..543(头 8B + 最多 9×56B),段 3 前移到 544..583,段 5 仍 584。 + * 删段 4 历史覆盖窗。长度不变 ⇒ SII/EEPROM 无需重刷;主站解码偏移必须同步。 */ #define ECAT_PDO_IN_BYTES 600 diff --git a/GD32E508_DCU_APP/comm/ssc/port/dc_app.c b/GD32E508_DCU_APP/comm/ssc/port/dc_app.c index 571829f..2251bb3 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/dc_app.c +++ b/GD32E508_DCU_APP/comm/ssc/port/dc_app.c @@ -116,9 +116,9 @@ typedef struct OBJ_STRUCT_PACKED_START { UINT32 u32DsRunsTotal; /* sub16: 窗口内 run(连续变化段)总数,配 sub14 求均值 */ UINT32 u32DsRunsMax; /* sub17: 单拍 run 数峰值 —— 块粒度差分的碎片上限由它定 */ UINT32 u32DsSeg1; /* sub18: 段1(0..31) 桥状态+短快照 变化字节合计 */ - UINT32 u32DsSeg2; /* sub19: 段2(32..319) 电机状态窗口 */ - UINT32 u32DsSeg3; /* sub20: 段3(320..359) 长帧快照 */ - UINT32 u32DsSeg4; /* sub21: 段4(360..583) 批量覆盖窗 */ + UINT32 u32DsSeg2; /* sub19: 段2(32..543) 电机整轮密排 */ + UINT32 u32DsSeg3; /* sub20: 段3(544..583) 长帧快照 */ + UINT32 u32DsSeg4; /* sub21: 已删(1.6.21 恒 0) */ UINT32 u32DsSeg5; /* sub22: 段5(584..599) 诊断区+保留 */ UINT32 u32DsCompareCyc; /* sub23: 比对自身累计 DWT 周期(u32 粒度,不含快照拷贝) */ } OBJ_STRUCT_PACKED_END diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index b8c8817..89ef48e 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -128,7 +128,7 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true; * ============================================================ */ constexpr uint32_t APP_FW_VERSION_MAJOR = 1U; constexpr uint32_t APP_FW_VERSION_MINOR = 6U; -constexpr uint32_t APP_FW_VERSION_PATCH = 20U; /* 1.6.20:**基线对照版**,仅把 CONFIG_ECAT_DIFF_STATS 置 0,用来量仪表自身成本与生产口径的真实负载水位。动机:1.6.19 读到 load 51.5%,但该读数含 observe() 每拍的 600B 比对 + 快照拷贝,拿它当"差分能省 19 个点"的分母是被污染的 —— 同工况同判据各读一次,两数相减才是仪表成本。PDO 布局/映射逐字节未动,无需重刷 SII。 */ +constexpr uint32_t APP_FW_VERSION_PATCH = 21U; /* 1.6.21:段 2 改「一帧=总线电机整轮状态」。用户裁定旧需求不对:不要 win_base 滑窗扫 27 槽,也不要段 4 历史帧覆盖窗反复重发。实测病理:接 3 个电机网页却看到 5 个状态帧——固定 5 槽里空槽 node_id=0 也占位,主站不滤就多报。本版:(1) 段 2 密排,count=本拍实际电机数(0..9),空节点不占位,槽序按 node_id 升序,同号多通道取 seq 最新;主站 `for i in 0..count` 按槽内 node_id 自动识别,无需滑窗。(2) 删段 4 覆盖窗口(fillRxWindow/RxWinSlot 整套),非 STATUS 仍走段 1/3 最近一帧。(3) 差分填充:节点集合与各 seq 未变时整段复用影子,跳过 MotorState 重读与逐槽 48B 拷贝。(4) 输入 PDO 总长仍 600B,SII 无需重刷;段 2 扩到 32..543、段 3 前移到 544、段 5 仍 584。**主站解码必须同步**(decode_input/transport-ecat.js:count 密排 + 段 3 偏移 320→544)。 */ /* 1.6.19:[DIFF-STATS] 仪表成本终于测对了,并推翻 1.6.18 自己的判断。1.6.18 认定"是 -Oz 把 memcpy(&a,p,4) 编成函数调用",改成手拼 4 字节移位,结果反汇编出**每字 8 条 ldrb.w**、比开销从 21.3µs/拍涨到 41µs/拍 —— 方向完全反了。真根因不在这里:alignas(4) 标在数组声明上只到调用点为止,而 observe(const uint8_t*) 的形参签名不带对齐信息,优化器无从证明指针 4 字节对齐,两版都只能逐字节走。修法是对两个入参各做一次 __builtin_assume_aligned(p,4) 再走 memcpy —— 反汇编确认循环体已缩成 ldr.w/ldr.w/cmp/bne 每字 4 条 (150 字 ≈ 750 cycle ≈ 4.2µs/拍,与最初估算同量级),且整个函数 bl 归零(600B 快照拷贝也被折成字搬运)。教训:**测量工具的开销会直接变成结论**,"差分判断很贵"这句话差一点就被我自己造的假象坐实。被测量(变化字节数 25%、run 峰值 12)不受此影响,两版读数一致,仍然有效。 */ /* 1.6.18:修 [DIFF-STATS] 仪表自己的假象 —— 比对循环里 `memcpy(&a,p,4)` 在 -Oz 下被编成**函数调用**而非单条 LDR,首轮实测 compare 开销虚报成 21.3µs/拍(≈300 次调用×995 拍),若拿它当"差分判断很贵"的证据就结论反了。改成小端 4 字节移位拼装:合法、不碰严格别名、且是 Clang 认得折 LDR 的模式。顺带把 alignas(4) 的理由改成"服务整块 memcpy",比对本身不再依赖它。Code 52716→52768(+52B),代价是体积不是速度。 */ /* 1.6.17:新增 [DIFF-STATS] 临时仪表,为"输入 PDO 改差分写"取证,**零行为变更**。动机:差分写的收益完全取决于每拍真正变了多少字节、变化点碎成几个 run,而 run 碎片数决定它会不会把一次 600B 整块写退化成几百次带 PRAM_AVAIL 轮询的小写——这两个数今天没有实测依据,所以先只上仪表再决定做不做。实现:comm/ethercat_diff_stats.{h,cpp},每拍在 PDI_Isr 里与上一份已发布镜像做 u32 粒度比对,按 1s 窗口(DWT 周期,同 cpu_load 手法)滚动,getter 只回报"上一个完整窗口"以免主站读到半累积值;量为 cycles / changedMax / runsTotal / runsMax / 段1..段5 各自变化字节 / 比对自身 DWT 开销。经对象字典 0x2000 sub13..23 上报(新增 11 个只读 UINT32)。**只加 SDO 子索引、PDO 长度与映射逐字节不动 ⇒ 无需重生成 SII、无需断电冷启动**,可与主站独立升级。整模块由 CONFIG_ECAT_DIFF_STATS 门控,置 0 后与 1.6.16 等价;挂接点统一 [DIFF-STATS] 标记,删除=删 2 文件+删标记行。已知副作用:开启时 sub9/sub10 写侧耗时含比对开销,不能直接与历史 296µs/265µs 对拍,真实写侧基准须置 0 重测。 */