Files
DCU_APP/GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp
T
cbb 959e2aef6e feat(comm): 1.6.14~1.6.20 —— CAN-FD 数据段 5M、批量区改定长覆盖窗口(输入 PDO 496→600)、drop 口径修复、差分写仪表
1.6.14 CAN-FD 数据段 2Mbps→5Mbps:CONFIG_CAN_DATA_PRESCALER 9→3、BS1 3→4TQ
  (90MHz/(3×6TQ),采样点 80%→83.3%),仲裁段仍 1M。TDC filter/offset 维持 4 不做
  等比缩放(TDCMOD=CALC_AND_OFFSET 下硬件已自动测回路延时;放到 12 会让本节点回采
  错开约 6 个数据位,自发热帧被判错误帧)。配套电机 APP/BOOT data_prescaler=2。

1.6.15 段4 批量接收由"环形队列 + 变长紧排排空"改为 5×44B 定长覆盖窗口,输入 PDO
  496→600(75×64bit),段5 诊断区 484→584。动机两条:现场 4000 帧/s 超旧容量 3 帧/拍
  恒丢 1 帧;更关键是排空赤字把队列长期压在饱和态,而 1.6.9~1.6.13 追的错标/拼接竞态
  正是饱和态喂出来的(判满用的 tail 滞后→生产者写进消费者刚读过的槽)。定长消灭
  tail/判满/pending,边界越界写在结构上不可能再犯。每槽带 u32 seqlock:buildInput 在
  PDI_Isr(preempt=0) 抢占主循环写侧,无它则主站会拿到"新 ID + 旧数据"拼出的假帧。
  out+in=984 ≤ 1024 硬上限,DPRAM 留 120B。语义变更:批量区从"不丢的事件流"变"最近
  N 帧窗口",去重与丢帧判据移交主站按 seq 归一。三端同步 size.h/bridge/dc_app/
  gen_eeprom/DCU_ESI.xml,必须重烧固件 + 烧 EEPROM + 断电冷启动,只烧一边 SAFEOP 报
  AL 0x001A/0x0011。

1.6.16 段1/段3 快照 drop 计数口径修复(纯判据,PDO 布局逐字节不动):旧判据
  s_rxValid 置位后永不清零 ⇒ 开机首帧后每帧都算丢弃,drop ≡ rx−1,是零灵敏度假仪表
  且把网页状态栏刷成 ERR。新增 s_rxPublished,判据改 s_rxValid && !s_rxPublished。

1.6.17~19 [DIFF-STATS] 差分写取证仪表,零行为变更,只加 0x2000 sub13..23(11 个只读
  UINT32) ⇒ 不动 PDO 长度/映射,无需重生成 SII。实测:每拍 27.4% 字节真变化(164B)、
  run 峰值 12~13、段2 占变化量 ~53%、段4 ~25%。
  1.6.19 推翻 1.6.18 的自我诊断:比对开销虚报不是 -Oz 把 memcpy 编成调用(手拼移位
  反而 21.3µs→41µs),真根因是 alignas(4) 到 const uint8_t* 形参处丢失,优化器无法
  证明 4 字节对齐。__builtin_assume_aligned + memcpy 后循环体缩成 ldr/ldr/cmp/bne、
  bl 归零。教训:测量工具自身的开销会直接变成结论。

1.6.20 基线对照版(当前提交状态 CONFIG_ECAT_DIFF_STATS=0):同工况(电机在线无指令)
  实测 load 49.1% vs 仪表开的 51.6% ⇒ 仪表自身成本 2.4 个点,差分改造收益 = 0。
  按实测变化量推算 A1(段粒度条件写) 只值约 1 点(段2/段4 每拍都变,必然整块重写),
  仅 A2(8B 块粒度 + run 上限退化为整写) 有空间 —— 推算值,未做 A/B 实测不作为结论。
2026-09-09 03:04:53 +08:00

485 lines
22 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.cpp
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.5.56)。
*/
#include "comm/ethercat_can_bridge.h"
#include "device/canfd.h"
#include "device/motor_state.h"
#include "RTE_Components.h"
#include CMSIS_device_header /* __DMB():seqlock 的编译器/总线屏障 */
#include <cstring>
volatile uint8_t EthercatCanBridge::s_lastSeq = 0U;
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;
volatile uint32_t EthercatCanBridge::s_xrxSeq = 0U;
volatile uint8_t EthercatCanBridge::s_rxCh = 0U;
volatile uint8_t EthercatCanBridge::s_rxDlc = 0U;
volatile bool EthercatCanBridge::s_rxTruncated = false;
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};
namespace {
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端,不依赖编译器结构体布局。 */
inline uint32_t rd32le(const uint8_t *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
inline void wr32le(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24);
}
inline void wr16le(uint8_t *p, uint16_t v)
{
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
}
} // 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 完全一致 */
}
if (want > kEcatTxBatchMax) {
want = kEcatTxBatchMax;
}
const bool stopOnError = (out[ECAT_TXB_OFF_FLAGS] & 0x01U) != 0U;
uint8_t sent = 0U;
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;
}
const uint8_t *f = &out[off];
uint8_t dlc = f[ECAT_FRM_OFF_DLC];
if (dlc > kEcatCanFdDataMax) {
dlc = kEcatCanFdDataMax;
}
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc) > kEcatPdoOutBytes) {
break;
}
const uint8_t ch = f[ECAT_FRM_OFF_CH];
CanFd::CanTxResult r;
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);
}
if (r == CanFd::CanTxResult::kOk) {
++s_txCount;
++sent;
} else {
s_lastTxResult = (uint8_t)r;
if (stopOnError) {
break;
}
}
off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc);
}
return sent;
}
void EthercatCanBridge::fillRxWindow(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 是"自上次回填以来"的增量 */
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; /* 生产者正在写这槽:本拍不报,下拍自愈 */
}
__DMB(); /* 判稳先于取值:不许把下面的读重排到奇偶检查之前 */
if ((sl.flags & 0x01U) == 0U) {
continue; /* 从未写过:留零,主站按 flags bit0 判空 */
}
/* 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;
}
}
bool EthercatCanBridge::peekOutput(const uint8_t *head)
{
if (head == nullptr) {
return false;
}
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;
/* 上电/重连后首帧只记基准(与 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;
}
return (head[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U;
}
void EthercatCanBridge::processOutput(const uint8_t *out)
{
if (out == nullptr) {
return;
}
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; /* 没有新的发送请求 */
}
s_lastSeq = seq;
const uint8_t ch = out[ECAT_CAN_TX_OFF_CH];
uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC];
if (dlc > kEcatCanTxDataMax) {
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);
s_lastTxResult = (uint8_t)r;
if (r == CanFd::CanTxResult::kOk) {
++s_txCount;
}
}
}
/* 批量区:同一个 seq 沿里一次性发 N 帧,供多轴同步下发。 */
(void)sendBatch(out);
}
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 逐字节兼容 ── */
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) {
uint8_t flags = 0x01U;
if (s_rxTruncated) {
flags |= 0x02U;
}
in[ECAT_CAN_RX_OFF_CH] = s_rxCh;
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;
}
const uint8_t ch = s_rxValid ? s_rxCh : 0U;
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;
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<uint8_t>(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/数据。 */
if (xrxStable && s_rxValid) {
in[ECAT_XRX_OFF_CH] = s_rxCh;
in[ECAT_XRX_OFF_DLC] = s_rxDlc;
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
wr32le(&in[ECAT_XRX_OFF_ID], s_rxId);
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
}
/* ── 段 4(360..583):CANFD 批量接收覆盖窗口 ──
* 段 1/段 3 只带"最近一帧",高速多帧场景下周期间到达的帧会互相覆盖
* (s_dropCount 就是在数这个)。批量区保留最近 N 帧的窗口,让主站在一个
* 周期内拿到帧序列而不是单个采样点;1.6.15 起改为定长覆盖(见 fillRxWindow)。 */
fillRxWindow(in);
}
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
const uint8_t *data, uint8_t length)
{
if (data == nullptr) {
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; /* → 奇数:快照进入"正在写" */
__DMB();
/* 「未取先覆」计数:上一帧还没被任何一拍 PDO 发布就被覆盖才算丢。
* 原判据只判 s_rxValid,而它置位后永不清零 → 第一帧之后每帧都计,
* drop 恒等于 rx−1(u16 回绕),变成跟着收帧速率涨、零灵敏度的假仪表。
* 放在奇数窗口内:消费者判到奇会跳过段 1/段 3、不会在此刻置 published,
* 故"读上一帧发布状态 + 自增"对消费者等效原子,不会多计一次。 */
if (s_rxValid && !s_rxPublished) {
++s_dropCount;
}
memcpy(s_rxData, data, copyLen);
if (copyLen < kEcatCanXRxDataMax) {
memset(&s_rxData[copyLen], 0, (size_t)(kEcatCanXRxDataMax - copyLen));
}
s_rxCh = channel;
s_rxId = id;
s_rxDlc = length; /* 报告原始长度,便于主站发现被截断 */
s_rxTruncated = (length > kEcatCanRxDataMax);
++s_rxCount;
s_rxValid = true;
s_rxPublished = false; /* 这一帧还没被任何一拍 PDO 发布 */
__DMB();
++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);
}
}
extern "C" {
void EcatCan_ProcessOutput(const uint8_t *out)
{
EthercatCanBridge::processOutput(out);
}
void EcatCan_BuildInput(uint8_t *in)
{
EthercatCanBridge::buildInput(in);
}
} /* extern "C" */