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 实测不作为结论。
This commit is contained in:
@@ -85,6 +85,9 @@ project:
|
||||
- file: ./comm/ethercat_can_bridge.cpp
|
||||
- file: ./comm/ethercat_pdo.h
|
||||
- file: ./comm/ethercat_pdo.cpp
|
||||
# [DIFF-STATS] 临时仪表:差分写取证,删除时连同本两行与 comm/ethercat_diff_stats.* 一并移除
|
||||
- file: ./comm/ethercat_diff_stats.h
|
||||
- file: ./comm/ethercat_diff_stats.cpp
|
||||
- group: comm/ssc
|
||||
files:
|
||||
- file: ./comm/ssc/inc/ecatslv.h
|
||||
|
||||
@@ -19,6 +19,7 @@ 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;
|
||||
@@ -27,11 +28,12 @@ volatile uint32_t EthercatCanBridge::s_rxId = 0U;
|
||||
volatile uint32_t EthercatCanBridge::s_rxCount = 0U;
|
||||
uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0};
|
||||
|
||||
EthercatCanBridge::RxRingItem
|
||||
EthercatCanBridge::s_rxRing[kEcatRxRingSlots] = {};
|
||||
volatile uint8_t EthercatCanBridge::s_rxRingHead = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_rxRingTail = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_rxRingLost = 0U;
|
||||
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 惯例固定小端,不依赖编译器结构体布局。 */
|
||||
@@ -106,64 +108,55 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||
return sent;
|
||||
}
|
||||
|
||||
void EthercatCanBridge::fillRxBatch(uint8_t *in)
|
||||
void EthercatCanBridge::fillRxWindow(uint8_t *in)
|
||||
{
|
||||
uint8_t count = 0U;
|
||||
uint16_t off = ECAT_RXB_OFF_ITEMS;
|
||||
uint8_t tail = s_rxRingTail;
|
||||
const uint8_t head = s_rxRingHead; /* 快照一次:生产者可能边填边推进 */
|
||||
/* ── 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 是"自上次回填以来"的增量 */
|
||||
|
||||
while (tail != head && count < kEcatRxBatchMax) {
|
||||
/* 整条先拷到局部副本再用:生产者(主循环 onCanFrame)与本函数(PDI_Isr)
|
||||
* 共用这块槽,直接按引用逐字段取会让 id/dlc/data 分属不同帧。 */
|
||||
const RxRingItem it = s_rxRing[tail];
|
||||
uint8_t dlc = it.dlc;
|
||||
if (dlc > kEcatCanFdDataMax) {
|
||||
dlc = kEcatCanFdDataMax;
|
||||
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.6.1: PDO 内数据条目 32B 上限——DLC 字节填**真值**,超出部分截断并打
|
||||
* flags bit1;环形队列里仍是全量 64B,截断只发生在 PDO 快照这一步。
|
||||
* 主站按 "8 + min(dlc, kEcatRxBatchDataMax)" 推进游标。 */
|
||||
uint8_t dlcWire = dlc;
|
||||
if (dlcWire > kEcatRxBatchDataMax) {
|
||||
dlcWire = kEcatRxBatchDataMax;
|
||||
}
|
||||
/* 装不下就停,**且不推进 tail** —— 该帧留到下个周期,不丢。
|
||||
* 边界是段 5 诊断区起点(ECAT_DIAG_OFF_LOAD=484),不是 PDO 全长(496):
|
||||
* 1.6.10 修正。旧判据允许条目尾部最多越进诊断区 12B,把负载率/撕裂计数/
|
||||
* 序号盖成帧数据 —— 当前 kEcatRxBatchMax=3 时最坏占用恰好 484 触不到,
|
||||
* 属潜伏 bug,但容量一旦按瘦身布局放宽(32B 步长)就会立刻咬上来。 */
|
||||
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlcWire) > ECAT_DIAG_OFF_LOAD) {
|
||||
break;
|
||||
__DMB(); /* 判稳先于取值:不许把下面的读重排到奇偶检查之前 */
|
||||
if ((sl.flags & 0x01U) == 0U) {
|
||||
continue; /* 从未写过:留零,主站按 flags bit0 判空 */
|
||||
}
|
||||
|
||||
uint8_t *p = &in[off];
|
||||
p[ECAT_FRM_OFF_CH] = it.ch;
|
||||
p[ECAT_FRM_OFF_DLC] = dlc;
|
||||
p[ECAT_FRM_OFF_FLAGS] = (uint8_t)(0x01U | (dlc > kEcatRxBatchDataMax ? 0x02U : 0x00U));
|
||||
wr32le(&p[ECAT_FRM_OFF_ID], it.id);
|
||||
memcpy(&p[ECAT_FRM_HDR_SIZE], it.data, dlcWire);
|
||||
/* 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];
|
||||
|
||||
off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlcWire);
|
||||
tail = (uint8_t)((tail + 1U) % kEcatRxRingSlots);
|
||||
++count;
|
||||
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);
|
||||
|
||||
/* **每条读完立刻发布 tail**(1.6.9)。原来攒到循环末才发布:生产者判满
|
||||
* 用的正是这个滞后的 s_rxRingTail,于是它以为队列里还有本拍已被读走的
|
||||
* 那几个空位没腾出来 —— 反过来说,它会把新帧写进消费者**刚读过但未发布**
|
||||
* 的槽。队列一旦饱和(实测 pending 恒 8/12、lost 几乎每拍都有、count 恒
|
||||
* 取满 3),这个窗口每拍都在,于是拷进 PDO 的 id/dlc/data 来自不同帧,
|
||||
* 表现为"CONFIG 的 ID 配 STATUS 的内容"这类错标(实测 1.77%)。
|
||||
* 逐条发布后生产者的判满始终基于真实进度,覆写窗口消失。 */
|
||||
__DMB(); /* 本条读取先于 tail 发布落地 */
|
||||
s_rxRingTail = tail;
|
||||
/* 标记"主站已取过":生产者下次覆盖本槽时就不算丢帧。只有"未取先覆"
|
||||
* 才计 OVF —— 覆盖已送达的帧是窗口的设计意图,不是损失。 */
|
||||
s_rxWinSeen[i] = 1U;
|
||||
}
|
||||
|
||||
const uint8_t pending = (uint8_t)((head + kEcatRxRingSlots - tail) % kEcatRxRingSlots);
|
||||
in[ECAT_RXB_OFF_COUNT] = count;
|
||||
in[ECAT_RXB_OFF_LOST] = s_rxRingLost;
|
||||
in[ECAT_RXB_OFF_PENDING] = pending;
|
||||
s_rxRingLost = 0U; /* 回填后清零:lost 是"自上次回填以来"的增量 */
|
||||
}
|
||||
|
||||
bool EthercatCanBridge::peekOutput(const uint8_t *head)
|
||||
@@ -220,6 +213,9 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
s_txCount = 0U;
|
||||
s_dropCount = 0U;
|
||||
s_rxCount = 0U;
|
||||
/* 快照区里残留的那帧早已发布过;不置 published 会让清零后的第一帧
|
||||
* 被误计成一次"未取先覆",清零口径就脏了。 */
|
||||
s_rxPublished = true;
|
||||
}
|
||||
|
||||
if (seq == s_lastSeq) {
|
||||
@@ -276,7 +272,8 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
|
||||
/* seqlock 判稳(见 onCanFrame):奇数=生产者正在写这组快照。单核下 ISR
|
||||
* 执行期间生产者冻结,读一次即可定性;本周期跳过段 1/段 3(开头已清零,
|
||||
* 语义=无有效快照),下周期自愈。批量区(段 4)走环形队列,不受影响。 */
|
||||
* 语义=无有效快照),下周期自愈。批量区(段 4)每槽另有自己的 seqlock,
|
||||
* 不受本组影响。 */
|
||||
const bool xrxStable = ((s_xrxSeq & 1U) == 0U);
|
||||
|
||||
if (xrxStable && s_rxValid) {
|
||||
@@ -290,6 +287,9 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
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;
|
||||
@@ -354,11 +354,11 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
|
||||
}
|
||||
|
||||
/* ── 段 4(360..639):CANFD 批量接收区 ──
|
||||
/* ── 段 4(360..583):CANFD 批量接收覆盖窗口 ──
|
||||
* 段 1/段 3 只带"最近一帧",高速多帧场景下周期间到达的帧会互相覆盖
|
||||
* (s_dropCount 就是在数这个)。批量区从环形队列里一次回收多帧,
|
||||
* 让主站在一个周期内拿到完整的帧序列而不是采样点。 */
|
||||
fillRxBatch(in);
|
||||
* (s_dropCount 就是在数这个)。批量区保留最近 N 帧的窗口,让主站在一个
|
||||
* 周期内拿到帧序列而不是单个采样点;1.6.15 起改为定长覆盖(见 fillRxWindow)。 */
|
||||
fillRxWindow(in);
|
||||
}
|
||||
|
||||
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
@@ -368,12 +368,6 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
return;
|
||||
}
|
||||
|
||||
/* 上一帧还没被 PDO 周期取走就来了新帧:记一次丢弃(覆盖旧帧,
|
||||
* 保证主站看到的永远是最新状态,而不是卡在一帧陈旧数据上)。 */
|
||||
if (s_rxValid) {
|
||||
++s_dropCount;
|
||||
}
|
||||
|
||||
/* 缓存区已扩到 32B(段 3 用),truncated 仍按段 1 的 8B 口径上报,
|
||||
* 这样 ecat.html 的 CAN 调试页语义不变。 */
|
||||
uint8_t copyLen = length;
|
||||
@@ -396,6 +390,15 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
++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));
|
||||
@@ -407,44 +410,62 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
s_rxTruncated = (length > kEcatCanRxDataMax);
|
||||
++s_rxCount;
|
||||
s_rxValid = true;
|
||||
s_rxPublished = false; /* 这一帧还没被任何一拍 PDO 发布 */
|
||||
|
||||
__DMB();
|
||||
++s_xrxSeq; /* → 偶数:快照整体一致 */
|
||||
|
||||
/* ── 推入批量接收环形队列(1.5.56)──
|
||||
* 与上面的"最近一帧快照"并行维护:快照会被后帧覆盖(这是它的设计语义),
|
||||
* 队列则保序累积,供 fillRxBatch 一次回收多帧。
|
||||
* 队列满时丢**新**帧并计数,而不是覆盖旧帧 —— 批量区的价值在于保序完整,
|
||||
* 覆盖会在序列中间留下空洞,比明确丢尾更难排查。
|
||||
/* ── 写入批量接收覆盖窗口(1.6.15,取代 1.5.56 的环形队列)──
|
||||
* 与上面的"最近一帧快照"并行维护:快照只留最新一帧,窗口留最近 N 帧的序列。
|
||||
* 生产者按自己的游标写、绕圈覆盖,**不等消费者** —— 没有判满、没有背压、
|
||||
* 没有 pending。代价是语义从"事件流"变成"最近 N 帧窗口":一帧在窗口里活到
|
||||
* 被覆盖为止,主站按每槽 seq 去重后才归一成事件流。
|
||||
*
|
||||
* **STATUS 帧不入队**(1.6.9):批量区存在的意义是捞那些段 2 装不下的帧
|
||||
* **STATUS 帧不入窗**(1.6.9):批量区存在的意义是捞那些段 2 装不下的帧
|
||||
* (CONFIG / 版本 / 节点应答),而完整 STATUS 本来就由段 2 电机状态窗口按槽
|
||||
* 上报;主站 transport-ecat.js 的 drainRx() 拿到 STATUS 也是直接 continue
|
||||
* 丢掉。让它占队列是纯重复,而且正是它把队列压爆的:实测 CAN 侧约 4000 帧/s,
|
||||
* 批量区每拍最多 3 条(=3000 帧/s),净积压使 pending 恒 8/12、lost 几乎每拍
|
||||
* 都有 —— 队列长期饱和,才逼出了生产者覆写消费者槽位的竞态(错标率 1.77%,
|
||||
* 表现为"CONFIG 的 ID 配 STATUS 的内容")。滤掉 STATUS 后入队流量降到 CONFIG
|
||||
* 量级,队列不再饱和,竞态窗口随之消失。 */
|
||||
* 丢掉。让它占窗口是纯重复,而且正是它把队列压爆的:实测 CAN 侧约 4000 帧/s,
|
||||
* 旧批量区每拍最多回收 3 条(=3000 帧/s),净积压使队列长期饱和,才逼出了
|
||||
* 生产者覆写消费者槽位的竞态(错标率 1.77%,表现为"CONFIG 的 ID 配 STATUS
|
||||
* 的内容")。滤掉 STATUS 后入窗流量降到 CONFIG 量级。
|
||||
*
|
||||
* OVF 只数"**未取先覆**":覆盖一帧主站已经取走的数据是窗口的设计意图(不计),
|
||||
* 覆盖一帧主站还没来得及取的数据才是真丢帧(计数)。该数与主站按 seq 空洞
|
||||
* 算出的丢帧数应当一致 —— 两侧独立仪表互相对账,任一侧骗人都当场暴露。 */
|
||||
if ((id & 0xFFFFU) == 0x0100U) {
|
||||
return;
|
||||
}
|
||||
const uint8_t next = (uint8_t)((s_rxRingHead + 1U) % kEcatRxRingSlots);
|
||||
if (next == s_rxRingTail) {
|
||||
if (s_rxRingLost < 0xFFU) {
|
||||
++s_rxRingLost;
|
||||
{
|
||||
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 取走就被覆盖 */
|
||||
}
|
||||
} else {
|
||||
RxRingItem &it = s_rxRing[s_rxRingHead];
|
||||
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;
|
||||
}
|
||||
it.id = id;
|
||||
it.ch = channel;
|
||||
it.dlc = n;
|
||||
memcpy(it.data, data, n);
|
||||
__DMB(); /* 数据写入先于 head 发布落地(volatile 不约束非 volatile 的 memcpy) */
|
||||
s_rxRingHead = next; /* 最后推进 head:消费者读到时数据必已写完 */
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,9 @@
|
||||
*
|
||||
* 新增两个批量区,每条帧带 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)
|
||||
* 输入 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);
|
||||
@@ -96,7 +97,8 @@ 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, /**< 桥接层丢帧数(投递时上一帧还没被取走),小端 uint16 */
|
||||
ECAT_CAN_RX_OFF_DROP = 24U, /**< 段 1/段 3 快照「未取先覆」次数:上一帧尚未被任何一拍
|
||||
PDO 发布就被新帧覆盖才 +1(u16,累计回绕)。 */
|
||||
};
|
||||
|
||||
/** 输入 PDO 段 2(32..319):电机状态窗口头 + 槽数组。 */
|
||||
@@ -129,44 +131,86 @@ enum : uint16_t {
|
||||
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)" 推进游标。 */
|
||||
/** 输入 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 空洞算出被覆盖掉的帧数。 */
|
||||
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_* */
|
||||
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 定长 */
|
||||
};
|
||||
|
||||
/** 输入 PDO 段 5(484..491):设备诊断区(1.5.61 尾部追加,1.6.1 随批量区
|
||||
* 数据条目 64→32B 前移:364+3×40=484,诊断区锚在紧随其后的 8 字节,
|
||||
* 写侧是 EthercatPdo::fillDiagTail())。 */
|
||||
/** 覆盖窗口单槽布局(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 = 484U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%)。
|
||||
ECAT_DIAG_OFF_LOAD = 584U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%)。
|
||||
中断 + 非延迟处理段(任务派发/OTA relay)占墙钟比,
|
||||
不含常开轮询与空转;1s 滚动窗口(见 sys/cpu_load.h) */
|
||||
ECAT_DIAG_OFF_TORN = 486U, /**< CAN 取帧撕裂丢帧数(低 16 位,小端,饱和不回绕)。
|
||||
ECAT_DIAG_OFF_TORN = 586U, /**< CAN 取帧撕裂丢帧数(低 16 位,小端,饱和不回绕)。
|
||||
1.6.6 起:ISR 取帧时若 MP 的 TS/DLENC 前后不一致,
|
||||
说明邮箱窗口在取数途中被硬件换帧,该帧整帧丢弃并
|
||||
在此计数。这是"读到半条帧"的唯一可观测量 —— 主站
|
||||
据此判断修复是否生效、总线负载是否高到频繁触发。 */
|
||||
ECAT_DIAG_OFF_SEQ = 488U, /**< 诊断区序号,小端 uint16(1.6.12 起,原 u32 让出
|
||||
ECAT_DIAG_OFF_SEQ = 588U, /**< 诊断区序号,小端 uint16(1.6.12 起,原 u32 让出
|
||||
高 2 字节),每完成一个 1s 统计窗口 +1,主站据此
|
||||
判断负载值是否仍在滚动刷新。u16 足够:主站只判
|
||||
"非 0 且在涨",18h 才回绕。 */
|
||||
ECAT_DIAG_OFF_OVERFULL = 490U, /**< CAN RX FIFO 满溢计数 RFO0(u16 饱和,1.6.12)。
|
||||
ECAT_DIAG_OFF_OVERFULL = 590U, /**< CAN RX FIFO 满溢计数 RFO0(u16 饱和,1.6.12)。
|
||||
1.6.11 把溢出策略改成"满则丢新帧"后拼接并未消失
|
||||
(段5@494 以 ~384/s 增长),必须直接观测溢出本身
|
||||
(段5@594 以 ~384/s 增长),必须直接观测溢出本身
|
||||
与拼接率是否同涨,才能判定残余拼接是否仍源自溢出
|
||||
(即本 IP 在 DISCARD 模式下照样毁窗口)。 */
|
||||
ECAT_DIAG_OFF_RELTIMEOUT = 492U, /**< RFD0 释放握手超时计数(u16 饱和,1.6.12)。
|
||||
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 = 494U, /**< CAN 内容级拼接帧命中率(u16 饱和,1.6.11)。
|
||||
ECAT_DIAG_OFF_REJECT = 594U, /**< CAN 内容级拼接帧命中率(u16 饱和,1.6.11)。
|
||||
入队前结构自洽校验(STATUS 温度量程+斜率、CONFIG
|
||||
未使用区/模式)命中即计数。1.6.12 起**只计不丢**
|
||||
—— 数据管道必须诚实,该计数退化为"源头是否还在
|
||||
@@ -192,14 +236,16 @@ 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 */
|
||||
constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 75×64bit = 600B */
|
||||
|
||||
/** CANFD 单帧数据上限(DLC 64)—— 环形队列/发送批量区按此上限做容量推导。 */
|
||||
/** CANFD 单帧数据上限(DLC 64)—— 覆盖窗口/发送批量区按此上限做容量推导。 */
|
||||
constexpr uint8_t kEcatCanFdDataMax = 64U;
|
||||
|
||||
/** 批量**接收**条目在 PDO 内的数据字节数上限(1.6.1 从 64 压到 32)。
|
||||
/** 批量**接收**槽在 PDO 内的数据字节数上限(1.6.1 从 64 压到 32)。
|
||||
* 真机采样 6086 帧 DLC 只有 24/32 两档;超长帧在装填时截断(flags bit1),
|
||||
* 环形队列 RxRingItem 仍存全量 64B,截断只发生在 PDO 快照这一步。 */
|
||||
* 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)帧数上限。 */
|
||||
@@ -207,22 +253,11 @@ constexpr uint16_t kEcatTxBatchBytes = (uint16_t)(kEcatPdoOutBytes - ECAT_TXB_OF
|
||||
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_XRX_OFF_DATA + kEcatCanXRxDataMax <= ECAT_RXB_OFF_SLOT_COUNT,
|
||||
"长帧快照区与批量接收区重叠");
|
||||
static_assert(ECAT_CAN_TX_OFF_DATA + kEcatCanTxDataMax <= ECAT_TXB_OFF_COUNT,
|
||||
"单帧发送数据区与批量发送区重叠");
|
||||
@@ -230,19 +265,32 @@ 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诊断区重叠");
|
||||
/* 槽长必须容得下头 + 线上数据上限;改任一字段宽度都在这里拦下。 */
|
||||
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, "覆盖窗口槽数低于现场每拍到达帧数");
|
||||
|
||||
/**
|
||||
* @brief EtherCAT ↔ CAN 转发桥。
|
||||
* @note 全静态;PDO 侧函数在 Sync0_Isr(preempt=0) 或主循环 MainLoop 上下文执行,
|
||||
* onCanFrame() 在主循环 taskCanRxParse() 上下文执行。共享状态用 volatile +
|
||||
* 单生产者/单消费者语义,不需要临界区(投递只写、消费只读+清标志)。
|
||||
* 例外:段 1/段 3 的"最近一帧快照"是多字段一组(id/dlc/data 必须配套),
|
||||
* 用 s_xrxSeq seqlock 保证消费者不会读到"新数据+旧 ID"的混搭——
|
||||
* 例外是"多字段一组"的三处,全部用 seqlock 而不是握手:
|
||||
* - 段 1/段 3 的"最近一帧快照"(id/dlc/data 必须配套)→ s_xrxSeq;
|
||||
* - 段 2 电机槽(48B payload 一次 memcpy)→ MotorSlot.wr_seq;
|
||||
* - 段 4 覆盖窗口每槽(1.6.15)→ RxWinSlot.seq。
|
||||
* Sync0 是 preempt=0,critical.h 的 BASEPRI 方案挡不住它,故只能无锁协议。
|
||||
* 单核下 ISR 执行期间生产者必然冻结,所以读侧判一次奇偶即可定性。
|
||||
*/
|
||||
class EthercatCanBridge {
|
||||
public:
|
||||
@@ -290,8 +338,8 @@ private:
|
||||
*/
|
||||
static uint8_t sendBatch(const uint8_t *out);
|
||||
|
||||
/** @brief 把接收环形队列里的帧尽量装进输入 PDO 批量区。 */
|
||||
static void fillRxBatch(uint8_t *in);
|
||||
/** @brief 把批量接收覆盖窗口整块拷进输入 PDO 段 4(1.6.15)。 */
|
||||
static void fillRxWindow(uint8_t *in);
|
||||
|
||||
static volatile uint8_t s_lastSeq; /**< 上一次已执行的发送序号 */
|
||||
static volatile bool s_seqPrimed; /**< 是否已记录过基准 seq(防上电首帧误发) */
|
||||
@@ -306,6 +354,11 @@ private:
|
||||
* 没有它,Sync0_Isr 抢占在"数据已写完、id 未更新"之间会把新帧数据
|
||||
* 配旧帧 ID 发出去(STATUS 被标成 CONFIG → 前端解出离谱目标值)。 */
|
||||
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;
|
||||
static volatile uint8_t s_rxDlc;
|
||||
@@ -314,20 +367,28 @@ private:
|
||||
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 {
|
||||
/* ── 批量接收定长覆盖窗口(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;
|
||||
uint8_t data[kEcatCanFdDataMax];
|
||||
uint8_t dlc; /**< 原始 DLC 0..64 */
|
||||
uint8_t flags; /**< bit0=写过; bit1=线上截断到 32B */
|
||||
uint8_t data[kEcatCanFdDataMax]; /**< RAM 侧存全量 64B,PDO 拷贝时才截断 */
|
||||
};
|
||||
static RxRingItem s_rxRing[kEcatRxRingSlots];
|
||||
static volatile uint8_t s_rxRingHead; /**< 生产者写入位置 */
|
||||
static volatile uint8_t s_rxRingTail; /**< 消费者读取位置 */
|
||||
static volatile uint8_t s_rxRingLost; /**< 队列满导致的丢帧数(累计,回填后清零) */
|
||||
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 取过 */
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* @file ethercat_diff_stats.cpp
|
||||
* @brief [DIFF-STATS] 输入镜像差分统计实现(度量定义与存在理由见 ethercat_diff_stats.h)。
|
||||
*
|
||||
* 整文件由 CONFIG_ECAT_DIFF_STATS 门控:置 0 时不参与编译,挂接点各自也有 #if,
|
||||
* 因此关掉之后固件与改造前逐字节等价。
|
||||
*/
|
||||
#include "comm/ethercat_diff_stats.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "RTE_Components.h"
|
||||
#include CMSIS_device_header
|
||||
|
||||
#include "comm/ethercat_pdo_size.h"
|
||||
#include "sys/config.h"
|
||||
|
||||
#if CONFIG_ECAT_DIFF_STATS
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint16_t kInBytes = (uint16_t)ECAT_PDO_IN_BYTES;
|
||||
constexpr uint32_t kInWords = (uint32_t)kInBytes / 4U; /* 600/4=150,无尾巴字节 */
|
||||
constexpr uint8_t kSegCount = 5U;
|
||||
|
||||
/**
|
||||
* @brief 各段字节上界(开区间),用于把变化量归到布局段落。
|
||||
* @note 584..599 的"段5 诊断 + 4B 保留"合并计:差分写不会单独优化保留区,
|
||||
* 拆开只是多一档,但保留区若在涨就说明有东西在越界写,所以仍要看得见。
|
||||
*/
|
||||
constexpr uint16_t kSegEnd[kSegCount] = {32U, 320U, 360U, 584U, kInBytes};
|
||||
|
||||
/** @brief 上一份已发布的镜像。alignas(4) 是让优化器能把 4 字节 memcpy 折成单条
|
||||
* LDR 的前提——丢了它,比对会从 ~150 次字访问退化成 600 次字节访问。 */
|
||||
alignas(4) uint8_t s_prev[kInBytes];
|
||||
bool s_havePrev = false;
|
||||
|
||||
/* ── 正在累积的窗口 ── */
|
||||
uint32_t s_curCycles = 0U;
|
||||
uint32_t s_curChangedMax = 0U;
|
||||
uint32_t s_curRunsTotal = 0U;
|
||||
uint32_t s_curRunsMax = 0U;
|
||||
uint32_t s_curCompareCyc = 0U;
|
||||
uint32_t s_curSeg[kSegCount] = {0U, 0U, 0U, 0U, 0U};
|
||||
uint32_t s_winStart = 0U;
|
||||
|
||||
/* ── 上一个已完成的窗口(getter 只读这份,保证主站拿到的是稳定值)── */
|
||||
uint32_t s_winCycles = 0U;
|
||||
uint32_t s_winChangedMax = 0U;
|
||||
uint32_t s_winRunsTotal = 0U;
|
||||
uint32_t s_winRunsMax = 0U;
|
||||
uint32_t s_winCompareCyc = 0U;
|
||||
uint32_t s_winSeg[kSegCount] = {0U, 0U, 0U, 0U, 0U};
|
||||
uint32_t s_windowSeq = 0U;
|
||||
|
||||
/** @brief 字节偏移所属段号。只对"确实变了"的字调用,线性 5 次比较足够。 */
|
||||
uint8_t segOf(uint32_t byteOff)
|
||||
{
|
||||
for (uint8_t i = 0U; i < kSegCount; ++i) {
|
||||
if (byteOff < (uint32_t)kSegEnd[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return (uint8_t)(kSegCount - 1U); /* 理论上到不了:kSegEnd[4] == kInBytes */
|
||||
}
|
||||
|
||||
/** @brief 结算窗口:把累加值搬成"上一个完整窗口",再清零重新计时。 */
|
||||
void rollWindow(uint32_t now)
|
||||
{
|
||||
s_winCycles = s_curCycles;
|
||||
s_winChangedMax = s_curChangedMax;
|
||||
s_winRunsTotal = s_curRunsTotal;
|
||||
s_winRunsMax = s_curRunsMax;
|
||||
s_winCompareCyc = s_curCompareCyc;
|
||||
for (uint8_t i = 0U; i < kSegCount; ++i) {
|
||||
s_winSeg[i] = s_curSeg[i];
|
||||
s_curSeg[i] = 0U;
|
||||
}
|
||||
s_curCycles = 0U;
|
||||
s_curChangedMax = 0U;
|
||||
s_curRunsTotal = 0U;
|
||||
s_curRunsMax = 0U;
|
||||
s_curCompareCyc = 0U;
|
||||
s_winStart = now;
|
||||
s_windowSeq++;
|
||||
}
|
||||
|
||||
} /* namespace */
|
||||
|
||||
/**
|
||||
* @brief 与上一份已发布镜像比对并累计统计。
|
||||
* @note .ramfunc:每拍在 PDI_Isr 里跑。放 flash 会带来等待态,既污染本仪表
|
||||
* 自己要测的 compareCyc,也给被测的写侧窗口添无谓的额外占用。
|
||||
*/
|
||||
__attribute__((section(".ramfunc"), noinline))
|
||||
void EthercatDiffStats::observe(const uint8_t *cur) noexcept
|
||||
{
|
||||
const uint32_t t0 = DWT->CYCCNT;
|
||||
|
||||
if (!s_havePrev) {
|
||||
/* 首拍只建基准:否则第一拍会把"整 600B 都变了"记成样本,把 changedMax
|
||||
* 和段计数全钉在无意义的上限上。 */
|
||||
std::memcpy(s_prev, __builtin_assume_aligned(cur, 4), kInBytes);
|
||||
s_havePrev = true;
|
||||
s_winStart = DWT->CYCCNT;
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t changed = 0U;
|
||||
uint32_t runs = 0U;
|
||||
bool inRun = false;
|
||||
|
||||
/* 对齐必须在调用点再声明一次:alignas(4) 只到形参为止,函数签名是
|
||||
* const uint8_t*,优化器看不见对齐就无从折字访问。实测两版都因此慢到不能用——
|
||||
* 移位版反汇编出 8 条 ldrb.w/字(41µs/拍),memcpy 版 21µs/拍 —— 而这个数会被
|
||||
* 直接读成"差分判断很贵",是仪表自己造的假象,不是差分的成本。 */
|
||||
const uint8_t *prevW = static_cast<const uint8_t *>(__builtin_assume_aligned(s_prev, 4));
|
||||
const uint8_t *curW = static_cast<const uint8_t *>(__builtin_assume_aligned(cur, 4));
|
||||
|
||||
for (uint32_t w = 0U; w < kInWords; ++w) {
|
||||
const uint32_t o = w * 4U;
|
||||
uint32_t a;
|
||||
uint32_t b;
|
||||
/* assume_aligned + memcpy:不碰严格别名,又能让 -Oz 折成单条 LDR。 */
|
||||
std::memcpy(&a, prevW + o, 4U);
|
||||
std::memcpy(&b, curW + o, 4U);
|
||||
if (a == b) {
|
||||
inRun = false;
|
||||
continue;
|
||||
}
|
||||
changed += 4U;
|
||||
if (!inRun) {
|
||||
runs++;
|
||||
inRun = true;
|
||||
}
|
||||
s_curSeg[segOf(o)] += 4U;
|
||||
}
|
||||
|
||||
s_curCycles++;
|
||||
s_curRunsTotal += runs;
|
||||
if (changed > s_curChangedMax) { s_curChangedMax = changed; }
|
||||
if (runs > s_curRunsMax) { s_curRunsMax = runs; }
|
||||
|
||||
std::memcpy(s_prev, curW, kInBytes);
|
||||
|
||||
const uint32_t t1 = DWT->CYCCNT;
|
||||
/* 比对自身开销:不含上面那次 600B 快照搬运(那是差分写真要做时要付的成本之一,
|
||||
* 单列出来才看得清"光判断"值多少钱)。 */
|
||||
s_curCompareCyc += (uint32_t)(t1 - t0);
|
||||
|
||||
/* SystemCoreClock 个计数 = 1s(与 sys/cpu_load.cpp 同法;DWT 32 位约 23.8s
|
||||
* 绕回,窗口远短于此,无绕回问题)。 */
|
||||
if ((uint32_t)(t1 - s_winStart) >= SystemCoreClock) {
|
||||
rollWindow(t1);
|
||||
}
|
||||
}
|
||||
|
||||
void EthercatDiffStats::reset() noexcept
|
||||
{
|
||||
s_havePrev = false;
|
||||
}
|
||||
|
||||
uint32_t EthercatDiffStats::windowSeq() noexcept { return s_windowSeq; }
|
||||
uint32_t EthercatDiffStats::cycles() noexcept { return s_winCycles; }
|
||||
uint32_t EthercatDiffStats::changedMax() noexcept { return s_winChangedMax; }
|
||||
uint32_t EthercatDiffStats::runsTotal() noexcept { return s_winRunsTotal; }
|
||||
uint32_t EthercatDiffStats::runsMax() noexcept { return s_winRunsMax; }
|
||||
uint32_t EthercatDiffStats::seg1Bytes() noexcept { return s_winSeg[0]; }
|
||||
uint32_t EthercatDiffStats::seg2Bytes() noexcept { return s_winSeg[1]; }
|
||||
uint32_t EthercatDiffStats::seg3Bytes() noexcept { return s_winSeg[2]; }
|
||||
uint32_t EthercatDiffStats::seg4Bytes() noexcept { return s_winSeg[3]; }
|
||||
uint32_t EthercatDiffStats::seg5Bytes() noexcept { return s_winSeg[4]; }
|
||||
uint32_t EthercatDiffStats::compareCyc() noexcept { return s_winCompareCyc; }
|
||||
|
||||
/* ── 供 dc_app.c(C 侧对象字典) 取值:一次拿全 11 个窗口值,避免 11 次跨语言调用 ── */
|
||||
extern "C" {
|
||||
|
||||
/**
|
||||
* @brief [DIFF-STATS] 读上一个完整 1s 窗口的统计值。
|
||||
* @param[out] out 依次对应 0x2000 sub13..sub23。
|
||||
* 形参用 uint32_t 而非 SSC 的 UINT32 别名:本翻译单元不引 SSC 头,
|
||||
* 两者同为 32 位无符号,C 侧声明仍按 UINT32 写。
|
||||
*/
|
||||
void EcatDiffStats_GetWindow(uint32_t *out)
|
||||
{
|
||||
if (out == nullptr) {
|
||||
return;
|
||||
}
|
||||
out[0] = EthercatDiffStats::windowSeq();
|
||||
out[1] = EthercatDiffStats::cycles();
|
||||
out[2] = EthercatDiffStats::changedMax();
|
||||
out[3] = EthercatDiffStats::runsTotal();
|
||||
out[4] = EthercatDiffStats::runsMax();
|
||||
out[5] = EthercatDiffStats::seg1Bytes();
|
||||
out[6] = EthercatDiffStats::seg2Bytes();
|
||||
out[7] = EthercatDiffStats::seg3Bytes();
|
||||
out[8] = EthercatDiffStats::seg4Bytes();
|
||||
out[9] = EthercatDiffStats::seg5Bytes();
|
||||
out[10] = EthercatDiffStats::compareCyc();
|
||||
}
|
||||
|
||||
} /* extern "C" */
|
||||
|
||||
#else /* !CONFIG_ECAT_DIFF_STATS */
|
||||
/* 关掉时仍导出同名取值函数并填 0:这样 dc_app.c 的挂接点不需要再套一层
|
||||
* #if,子索引数量也保持与开启版一致(主站读到的恒 0,脚本按 winseq==0 判'未启用')。
|
||||
* 留一个空函数体反而会漏掉符号——1.6.20 首编就是栽在这里(L6218E)。 */
|
||||
extern "C" void EcatDiffStats_GetWindow(uint32_t *out)
|
||||
{
|
||||
if (out == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (uint32_t i = 0U; i < 11U; ++i) {
|
||||
out[i] = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_ECAT_DIFF_STATS */
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @file ethercat_diff_stats.h
|
||||
* @brief [DIFF-STATS] 临时仪表:量化"若把输入 PDO 改成差分写,每拍真正需要写多少字节、
|
||||
* 变化点碎成几个连续段(run)"。只测量,不改变任何现有写行为。
|
||||
*
|
||||
* 存在的理由:差分写(方案 A)的收益完全取决于"每拍有多少字节真的变了",而 run 碎片
|
||||
* 数量决定它会不会把一次 600B 整块写退化成几百次带 AVAIL 轮询的小写。这两个数今天
|
||||
* 都没有实测依据,所以先只上仪表,拿分布再决定 A1(语义分区条件写) 值不值得做、
|
||||
* A2(块粒度差分) 的碎片上限 N 该取几。
|
||||
*
|
||||
* 顺带把"比较本身要多久"也测出来(sub23 累计 DWT 周期)——这是差分方案最容易被
|
||||
* 质疑的成本项,用实测代替拍脑袋。
|
||||
*
|
||||
* @note 结果按 1s 窗口滚动(与 sys/cpu_load.cpp 同法,用 DWT 周期判窗口边界),
|
||||
* getter 返回**上一个已完成的完整窗口**,因此主站任何时刻 SDO 读到的都是
|
||||
* 稳定值,不会拿到半累积的窗口。窗口未满时 windowSeq()==0,读侧应显示
|
||||
* "等待首个完整窗口"而不是当 0 解读。
|
||||
*
|
||||
* @note 删除方法:删本文件与 ethercat_diff_stats.cpp + 删全工程 [DIFF-STATS] 标记行
|
||||
* + 把 sys/config.h 的 CONFIG_ECAT_DIFF_STATS 置 0。对象字典 0x2000 的
|
||||
* sub13..23 字段建议保留(恒 0,不影响任何既有子索引)。
|
||||
*/
|
||||
#ifndef COMM_ETHERCAT_DIFF_STATS_H
|
||||
#define COMM_ETHERCAT_DIFF_STATS_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* @brief 输入镜像差分统计。
|
||||
* @note 全静态;observe() 在 PDI_Isr 上下文每拍调用一次,故内部状态无需临界区
|
||||
* (唯一写者),getter 由 APPL_InputMapping 同上下文读取。
|
||||
*/
|
||||
class EthercatDiffStats {
|
||||
public:
|
||||
/**
|
||||
* @brief 与上一份已发布的输入镜像比对并累计统计。
|
||||
* @param[in] cur 刚发布完成的完整输入 PDO 镜像(kEcatPdoInBytes 字节)。
|
||||
* @note 内部自持上一份副本,调用方无需额外维护缓冲、也不必自己 memcpy。
|
||||
* 首次调用只建立基准,不产生样本(否则会把"全 600B 都变了"记成第一拍)。
|
||||
*/
|
||||
static void observe(const uint8_t *cur) noexcept;
|
||||
|
||||
/** @brief 丢弃基准,下一次调用重新起算(AL 状态跳变、布局变更时用)。 */
|
||||
static void reset() noexcept;
|
||||
|
||||
/* ── 对象字典 0x2000 sub13..23 的取值接口,均为"上一个完整 1s 窗口" ── */
|
||||
static uint32_t windowSeq() noexcept; /**< sub13 已完成窗口数,0=还没有完整窗口 */
|
||||
static uint32_t cycles() noexcept; /**< sub14 窗口内参与比对的拍数 */
|
||||
static uint32_t changedMax() noexcept; /**< sub15 单拍变化字节峰值 */
|
||||
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 seg5Bytes() noexcept; /**< sub22 段5(584..599) 诊断区+保留 */
|
||||
static uint32_t compareCyc() noexcept; /**< sub23 比对自身累计 DWT 周期(本征开销) */
|
||||
};
|
||||
|
||||
#endif /* COMM_ETHERCAT_DIFF_STATS_H */
|
||||
@@ -5,15 +5,24 @@
|
||||
#include "comm/ethercat_pdo.h"
|
||||
#include "comm/ethercat_can_bridge.h"
|
||||
#include "device/canfd.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/cpu_load.h"
|
||||
/* [DIFF-STATS] 临时仪表挂接(删除:去掉本块与 process() 内的 observe 块)。
|
||||
* 上一行的 sys/config.h 必须在此之前:宏未定义时 #if 会静默按 0 求值,
|
||||
* 开关就会被悄悄当成"关掉"。 */
|
||||
#if CONFIG_ECAT_DIFF_STATS
|
||||
#include "comm/ethercat_diff_stats.h"
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
|
||||
uint8_t EthercatPdo::s_out[EthercatPdo::kPdoOutBytes] = {0};
|
||||
uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0};
|
||||
/* alignas(4) 必须在类内声明与出类定义上同时标注:AC6(Clang) 否则报
|
||||
* "'alignas' must be specified on definition"。用途见 ethercat_pdo.h。 */
|
||||
alignas(4) uint8_t EthercatPdo::s_out[EthercatPdo::kPdoOutBytes] = {0};
|
||||
alignas(4) uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0};
|
||||
/* 输入 PDO 的工作缓冲:buildInput 填这里,填完整块发布到 s_in(见 process())。
|
||||
* 读侧 writeInputs 只读 s_in,因此永远看不到"填到一半"的镜像。 */
|
||||
uint8_t EthercatPdo::s_build[EthercatPdo::kPdoInBytes] = {0};
|
||||
alignas(4) uint8_t EthercatPdo::s_build[EthercatPdo::kPdoInBytes] = {0};
|
||||
|
||||
volatile uint32_t EthercatPdo::s_outputCycles = 0U;
|
||||
volatile uint32_t EthercatPdo::s_inputCycles = 0U;
|
||||
@@ -41,13 +50,17 @@ void EthercatPdo::process()
|
||||
* EXTI1(ESC IRQ),**挡不住 EXTI0(Sync0,preempt=0) 和 EXTI2(Sync1,preempt=1)**。
|
||||
*
|
||||
* 竞态:MainLoop 把 s_in 填到一半(如刚写完批量区第 1 条)时被 Sync0/Sync1
|
||||
* 抢占 → 内层重入本函数,memset 清空后重填整个 496B,随后 PDO_InputMapping()
|
||||
* 抢占 → 内层重入本函数,memset 清空后重填整个输入 PDO,随后 PDO_InputMapping()
|
||||
* 把这一份发给 ESC;外层回来又按自己那轮的进度接着往下写。于是发出去的镜像里
|
||||
* 批量区相邻两条来自**不同轮次**,而两轮之间环形队列 tail 已推进 —— 第 2 条
|
||||
* 就带上了另一条帧的内容。实测特征与此吻合:批量区第 1 条几乎不坏(0.06%)、
|
||||
* 第 2 条 30.75% 坏且内容是一条完整 CONFIG 帧;段 1/段 3 干净,因为它们每轮
|
||||
* 内容相同,被覆盖也看不出来。
|
||||
*
|
||||
* 1.6.15 批量区改成定长覆盖窗口后,跨轮次混填不再产生"半新半旧"的假帧(每槽
|
||||
* 自带 seqlock,且槽位固定、语义本就是"最近 N 帧"),但本防护仍然保留:省掉
|
||||
* 一次无谓的整块重填,并让镜像保持同一轮次的一致性。
|
||||
*
|
||||
* 处理:已在填充中就直接返回。输入 PDO 是周期量,跳过一次下一拍即补,
|
||||
* 远好过发出一份跨轮次拼接的镜像。 */
|
||||
static volatile bool s_building = false;
|
||||
@@ -73,6 +86,13 @@ void EthercatPdo::process()
|
||||
fillDiagTail(s_build);
|
||||
memcpy(s_in, s_build, kPdoInBytes);
|
||||
|
||||
#if CONFIG_ECAT_DIFF_STATS
|
||||
/* [DIFF-STATS] 只测量:与上一份已发布镜像比对并累计到 1s 窗口,经 0x2000
|
||||
* sub13..23 上报。不改变上面的整块写行为,因此本版本固件的输出与 1.6.16
|
||||
* 逐字节一致。仪表自持上一份副本,这里不需要额外缓冲。 */
|
||||
EthercatDiffStats::observe(s_in);
|
||||
#endif
|
||||
|
||||
s_building = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -98,14 +98,17 @@ private:
|
||||
*/
|
||||
static void fillDiagTail(uint8_t *in);
|
||||
|
||||
/** @brief 最近一份主站下发的输出数据。 */
|
||||
static uint8_t s_out[kPdoOutBytes];
|
||||
/** @brief 最近一份主站下发的输出数据。
|
||||
* @note alignas(4):让整块 memcpy(writeInputs / [DIFF-STATS] 的快照拷贝)按
|
||||
* 字宽走而非逐字节。比对主循环本身不依赖它——已改成手拼 4 字节移位,
|
||||
* 因为 -Oz 下 memcpy(&a,p,4) 会编成函数调用,见 ethercat_diff_stats.cpp。 */
|
||||
alignas(4) static uint8_t s_out[kPdoOutBytes];
|
||||
/** @brief 待交回主站的输入数据(已发布的完整镜像,writeInputs 只读它)。 */
|
||||
static uint8_t s_in[kPdoInBytes];
|
||||
alignas(4) static uint8_t s_in[kPdoInBytes];
|
||||
/** @brief 输入 PDO 工作缓冲:buildInput 填这里,填完整块发布到 s_in。
|
||||
* ECAT_Application() 会在 PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 四个
|
||||
* 上下文被调用并互相抢占,直接写 s_in 会让读侧取到跨轮次的半成品。 */
|
||||
static uint8_t s_build[kPdoInBytes];
|
||||
alignas(4) static uint8_t s_build[kPdoInBytes];
|
||||
/** @brief process() 被重入拦下的次数(诊断用,见 reentryCount())。 */
|
||||
static volatile uint32_t s_reentryCount;
|
||||
|
||||
|
||||
@@ -51,23 +51,39 @@
|
||||
#define ECAT_PDO_OUT_BYTES 384
|
||||
|
||||
/**
|
||||
* 输入过程数据字节数(从站→主站,SM3):62×64bit = 496B。
|
||||
*
|
||||
* ── 1.6.1 瘦身 640 → 496(2026-09-05 真机剖分后的决定)──
|
||||
* 输入过程数据字节数(从站→主站,SM3):75×64bit = 600B。
|
||||
*
|
||||
* ── 1.6.1 瘦身 640 → 496(2026-09-05)──
|
||||
* 固件 DWT 探针(0x2000 sub9/sub10)实测:PDO_InputMapping 里 HW_EscWriteIsr
|
||||
* 640B 写恒为 296µs/拍(29.7% CPU),且与 SYNC0 相位**无关**(主站 dc_bench
|
||||
* 五形态对拍极差 0.04%)——PRAM 通道 ≈0.463µs/B 线性,写多少省多少,唯一的
|
||||
* 减法就是减字节。真机 CAN 流量采样(6086 帧)DLC 只有 24/32 两档,批量接收
|
||||
* 区每帧 64B 数据是纯富余:
|
||||
* - 批量接收条目 8B 头 + 64B 数据 → 8B 头 + 32B 数据(DLC 真值保留,
|
||||
* 超长帧截断、flags bit1 标记;环形队列仍存全量 64B,不丢原始数据);
|
||||
* - 段 5 诊断区从 632..639(批量区最坏占用后的尾部追加)挪到 484..491;
|
||||
* - 段 1 / 电机窗口 / 长帧快照全部不动(0..359 与 1.5.61 逐字节兼容)。
|
||||
* 496 = 62×8;SM3 实占 3×496=1488B(0x1880-0x1E4F),out+in=880 ≤ 1024 硬上限,
|
||||
* DPRAM 反而留出 432B 余量。改这里必须同步重生成 SII(gen_eeprom.py)并断电冷启动。
|
||||
* 区每帧 64B 数据是纯富余:条目数据 64→32B(DLC 真值保留,超长帧截断、
|
||||
* flags bit1 标记),诊断区前移,段 1/电机窗口/长帧快照全部不动。
|
||||
*
|
||||
* ── 1.6.15 扩到 600:批量接收改定长覆盖窗口(2026-09-08)──
|
||||
* 瘦身后实测 496B 写 218.7µs/拍(21.9%)。批量区当时是"变长紧排 + 环形队列
|
||||
* 排空",容量 3 帧/拍(=3000 帧/s),现场 4000 帧/s 下每拍恒丢 1 帧,且排空赤字
|
||||
* 把队列长期压在饱和态——1.6.9~1.6.13 追的错标/拼接竞态正是饱和态喂出来的
|
||||
* (判满用的 tail 一旦滞后,生产者就会写进消费者刚读过的槽)。
|
||||
*
|
||||
* 本版把批量区改成**定长覆盖窗口**:生产者按自己的游标写、绕圈覆盖,消费者
|
||||
* 每拍整块取值,不再有 tail/判满/pending。代价是每槽要带一个 u32 seqlock
|
||||
* (PDI_Isr 会抢占主循环写侧,没有它主站就能拿到"新 ID + 旧数据"的假帧),
|
||||
* 所以 5 槽的预算是 5×44 而不是 5×40:
|
||||
* 段1..3 0..359(不动) + 头 4B(360..363) + 5×44B(364..583)
|
||||
* + 段5 诊断 12B(584..595) + 保留 4B = 600B
|
||||
* 5 槽 = 5000 帧/s,对 4000 帧/s 留出 25% 余量;写侧 600×0.441 ≈ 265µs/拍
|
||||
* (26.5%,仍远好于旧 640B 的 296µs,更是 4kHz 周期的 87.5% 占空不可行方案的
|
||||
* 零头,见 tools/ecat/dc_bench.py 固化结论)。
|
||||
*
|
||||
* 覆盖窗口**不创造容量**:PDO 每拍能带几帧只由字节数决定,要接住 N 帧/拍,
|
||||
* 槽数必须 ≥ N,否则一帧在两次拷贝之间就被覆盖掉(改完可看主站按 seq 空洞
|
||||
* 算出的丢帧数与从站侧 OVF 是否一致)。
|
||||
* 600 = 75×8;SM3 实占 3×600=1800B(0x1880-0x1F67),out+in=984 ≤ 1024 硬上限,
|
||||
* DPRAM 仍留 120B 余量。改这里必须同步重生成 SII(gen_eeprom.py)并断电冷启动。
|
||||
*/
|
||||
#define ECAT_PDO_IN_BYTES 496
|
||||
#define ECAT_PDO_IN_BYTES 600
|
||||
|
||||
/** DPRAM 中过程数据区起址与 SM 缓冲数 —— 上限推导的依据,供静态断言使用。 */
|
||||
#define ECAT_PD_AREA_START 0x1400
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<!-- SM2: Process Data Output (RxPDO, Master -> Slave) -->
|
||||
<Sm DefaultSize="384" StartAddress="#x1400" ControlByte="#x64" Enable="1">Outputs</Sm>
|
||||
<!-- SM3: Process Data Input (TxPDO, Slave -> Master) -->
|
||||
<Sm DefaultSize="496" StartAddress="#x1880" ControlByte="#x20" Enable="1">Inputs</Sm>
|
||||
<Sm DefaultSize="600" StartAddress="#x1880" ControlByte="#x20" Enable="1">Inputs</Sm>
|
||||
<!-- RxPDO (Output, Master -> Slave): 索引 0x1600-0x17FF,映射 0x7000:01..48
|
||||
1.5.56 扩到 48 条目(384B):兼容单帧区 48B + CANFD 批量发送区 336B。
|
||||
与 dc_app.c 的 PDO_OUT_ENTRIES / gen_eeprom.py 的 PD_OUT_SIZE 必须一致,
|
||||
@@ -370,12 +370,13 @@
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
</RxPdo>
|
||||
<!-- TxPDO (Input, Slave -> Master): 索引 0x1A00-0x1BFF,映射 0x6000:01..80
|
||||
<!-- TxPDO (Input, Slave -> Master): 索引 0x1A00-0x1BFF,映射 0x6000:01..75
|
||||
1.5.56 扩到 80 条目(640B):电机状态窗口 + CANFD 批量接收区 280B。
|
||||
SM3 起址随之挪到 #x1880 —— SM2 是 3-buffer 模式,384B×3 占到 #x187F。
|
||||
(384+640)×3 = 3072 = 过程数据区 0x1400-0x1FFF 全长,恰好用满。
|
||||
1.6.1 输入瘦身 640→496: 批量接收条目数据 64→32B(截断见 bridge.h),
|
||||
条目 80→62, SM3 实占 3×496=1488B (0x1880-0x1E4F), DPRAM 留 432B。
|
||||
1.6.1 输入瘦身 640→496: 批量接收条目数据 64→32B, 条目 80→62。
|
||||
1.6.15 496→600: 批量接收改定长覆盖窗口 5 槽(每槽 12B 头含 u32 seqlock
|
||||
+ 32B 数据), 条目 62→75, SM3 实占 3×600=1800B (0x1880-0x1F67),
|
||||
out+in=984 <= 1024 硬上限。推导见 comm/ethercat_pdo_size.h。
|
||||
撞址会被 SSC ecatslv.c 重叠检查拦下,报 INVALIDSMOUTCFG。 -->
|
||||
<TxPdo Fixed="1" Mandatory="1" Sm="3">
|
||||
<Index>#x1A00</Index>
|
||||
@@ -814,24 +815,97 @@
|
||||
<Name>Status Data 62</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>63</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 63</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>64</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 64</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>65</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 65</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>66</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 66</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>67</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 67</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>68</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 68</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>69</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 69</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>70</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 70</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>71</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 71</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>72</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 72</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>73</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 73</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>74</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 74</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<Index>#x6000</Index>
|
||||
<SubIndex>75</SubIndex>
|
||||
<BitLen>64</BitLen>
|
||||
<Name>Status Data 75</Name>
|
||||
<DataType>ULINT</DataType>
|
||||
</Entry>
|
||||
</TxPdo>
|
||||
<Mailbox DataLinkLayer="true">
|
||||
<CoE SdoInfo="true" CompleteAccess="true" SegmentedSdo="true"/>
|
||||
|
||||
@@ -42,13 +42,15 @@
|
||||
* 1.5.56 扩到 DPRAM 硬上限:输出 48×64bit=384B,输入 80×64bit=640B(非对称)。
|
||||
* 1.6.1 输入瘦身 640→496(62×64bit):批量接收条目数据 64→32B + 诊断区前移,
|
||||
* 依据与推导见 comm/ethercat_pdo_size.h 的 1.6.1 注。
|
||||
* 1.6.15 输入 496→600(75×64bit):批量接收改定长覆盖窗口 5 槽(每槽带 u32
|
||||
* seqlock,槽长 44B),诊断区随之后移到 584。推导见该头文件的 1.6.15 注。
|
||||
* 长度的唯一真源是 comm/ethercat_pdo_size.h;本文件用 _Static_assert 与之绑定,
|
||||
* 改一处漏一处会编译失败。为何硬上限是 out+in≤1024B 见该头文件的推导。
|
||||
*---------------------------------------------------------------------------*/
|
||||
|
||||
/** 输出/输入 PDO 的 64bit 条目数。 */
|
||||
#define PDO_OUT_ENTRIES 48u /* 48×8 = 384 B */
|
||||
#define PDO_IN_ENTRIES 62u /* 62×8 = 496 B (1.6.1 瘦身,原 80×8=640) */
|
||||
#define PDO_IN_ENTRIES 75u /* 75×8 = 600 B (1.6.15 覆盖窗口,原 62×8=496) */
|
||||
|
||||
/** 输出数据对象 0x7000 (主站→从站, RxPDO) */
|
||||
typedef struct OBJ_STRUCT_PACKED_START {
|
||||
@@ -73,7 +75,7 @@ static TOBJ6000 s_inputObj = {PDO_IN_ENTRIES, {0}};
|
||||
_Static_assert(offsetof(TOBJ7000, u64Data) == 2, "TOBJ7000 未紧排:检查 OBJ_STRUCT_PACKED_START");
|
||||
_Static_assert(offsetof(TOBJ6000, u64Data) == 2, "TOBJ6000 未紧排:检查 OBJ_STRUCT_PACKED_START");
|
||||
_Static_assert(sizeof(TOBJ7000) == 2 + 8 * PDO_OUT_ENTRIES, "TOBJ7000 尺寸应为 2+384");
|
||||
_Static_assert(sizeof(TOBJ6000) == 2 + 8 * PDO_IN_ENTRIES, "TOBJ6000 尺寸应为 2+496");
|
||||
_Static_assert(sizeof(TOBJ6000) == 2 + 8 * PDO_IN_ENTRIES, "TOBJ6000 尺寸应为 2+600");
|
||||
|
||||
/* 与 C++ 侧桥接层的长度约定绑定:任一处改了尺寸而忘了另一处,这里直接编译失败,
|
||||
* 而不是等到运行期 SAFEOP 报 AL 0x001A/0x0011 再回头查。 */
|
||||
@@ -105,10 +107,25 @@ typedef struct OBJ_STRUCT_PACKED_START {
|
||||
UINT32 u32InWriteCyc; /* sub10: 其中 HW_EscWriteIsr 输入PDO 过程数据写 */
|
||||
UINT32 u32AlCyc; /* sub11: PDI_Isr 内 AL 事件读×2+超限应答 */
|
||||
UINT32 u32AppCyc; /* sub12: ECAT_Application(桥 processOutput) */
|
||||
/* [DIFF-STATS] 差分写取证仪表(sub13..23):上一个完整 1s 窗口的值,
|
||||
* 取自 comm/ethercat_diff_stats.cpp。CONFIG_ECAT_DIFF_STATS 置 0 时恒 0,
|
||||
* 字段保留(不影响任何既有子索引,也不动 PDO/映射,故无需重刷 SII)。 */
|
||||
UINT32 u32DsWinSeq; /* sub13: 已完成窗口数,0=还没有完整窗口(勿把 0 当"无变化") */
|
||||
UINT32 u32DsCycles; /* sub14: 窗口内参与比对的拍数 */
|
||||
UINT32 u32DsChangedMax; /* sub15: 单拍变化字节峰值(差分写的理论上限=600×拍数) */
|
||||
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 u32DsSeg5; /* sub22: 段5(584..599) 诊断区+保留 */
|
||||
UINT32 u32DsCompareCyc; /* sub23: 比对自身累计 DWT 周期(u32 粒度,不含快照拷贝) */
|
||||
} OBJ_STRUCT_PACKED_END
|
||||
TOBJ2000;
|
||||
|
||||
static TOBJ2000 s_syncDiag = {12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
static TOBJ2000 s_syncDiag = {23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
|
||||
/* 同上:UINT32 首条目必须落在字节 2(未紧排时为 4)。 */
|
||||
_Static_assert(offsetof(TOBJ2000, u32Sync0) == 2, "TOBJ2000 未紧排:检查 OBJ_STRUCT_PACKED_START");
|
||||
@@ -129,6 +146,9 @@ extern volatile UINT32 g_dbg_inWriteCyc;
|
||||
extern volatile UINT32 g_dbg_alCyc;
|
||||
extern volatile UINT32 g_dbg_appCyc;
|
||||
|
||||
/* [DIFF-STATS] 差分写统计窗口值(comm/ethercat_diff_stats.cpp),out[0..10] 对应 sub13..23 */
|
||||
void EcatDiffStats_GetWindow(UINT32 *out);
|
||||
|
||||
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim, 1.5.54)。
|
||||
* 1.5.53 时这里直接调 EcatCan_* 转发桥;1.5.54 把"一个 EtherCAT 周期怎么收发"
|
||||
* 收拢进 EthercatPdo 类,本文件退回纯 SSC 移植层:只留对象字典 + 三行转发。 */
|
||||
@@ -182,13 +202,12 @@ static TOBJ_RXPDO_MAPPING sRxPDOMap = {PDO_OUT_ENTRIES, {
|
||||
PDO_MAP_8(0x7000, 24), PDO_MAP_8(0x7000, 32), PDO_MAP_8(0x7000, 40)
|
||||
}};
|
||||
|
||||
/* 0x1A00: 映射 0x6000:01..62, 每个 64 bits(1.6.1 瘦身 80→62:7×8 + 6) */
|
||||
/* 0x1A00: 映射 0x6000:01..75, 每个 64 bits(1.6.15 覆盖窗口 62→75:9×8 + 3) */
|
||||
static TOBJ_TXPDO_MAPPING sTxPDOMap = {PDO_IN_ENTRIES, {
|
||||
PDO_MAP_8(0x6000, 0), PDO_MAP_8(0x6000, 8), PDO_MAP_8(0x6000, 16),
|
||||
PDO_MAP_8(0x6000, 24), PDO_MAP_8(0x6000, 32), PDO_MAP_8(0x6000, 40),
|
||||
PDO_MAP_8(0x6000, 48),
|
||||
PDO_MAP_ENTRY(0x6000, 57), PDO_MAP_ENTRY(0x6000, 58), PDO_MAP_ENTRY(0x6000, 59),
|
||||
PDO_MAP_ENTRY(0x6000, 60), PDO_MAP_ENTRY(0x6000, 61), PDO_MAP_ENTRY(0x6000, 62)
|
||||
PDO_MAP_8(0x6000, 48), PDO_MAP_8(0x6000, 56), PDO_MAP_8(0x6000, 64),
|
||||
PDO_MAP_ENTRY(0x6000, 73), PDO_MAP_ENTRY(0x6000, 74), PDO_MAP_ENTRY(0x6000, 75)
|
||||
}};
|
||||
|
||||
_Static_assert(sizeof(sRxPDOMap.aEntries) / 4 == PDO_OUT_ENTRIES, "0x1600 条目数不符");
|
||||
@@ -237,22 +256,36 @@ OBJCONST TSDOINFOENTRYDESC OBJMEM asInputEntryDesc[] = {
|
||||
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}
|
||||
};
|
||||
|
||||
/* SYNC 诊断 0x2000: REC, Sub0..Sub12 共 13 项, 纯只读不参与 PDO 映射。
|
||||
* sub5..12 为 1.6.0 剖分埋点(DWT 周期/计数累加), 定位负载构成后移除。 */
|
||||
/* SYNC 诊断 0x2000: REC, sub0..sub23 共 24 项, 纯只读不参与 PDO 映射,
|
||||
* 改子索引数无需重刷 SII/EEPROM。sub5..12 为 1.6.0 剖分埋点(DWT 周期/计数
|
||||
* 累加,定位负载构成后移除);sub13..23 为 [DIFF-STATS] 差分写取证仪表。
|
||||
* 行数必须与 TOBJ2000 字段一一对应:少一行不会编译报错,只会让 SDO 从错误的
|
||||
* 偏移取值 —— 故逐行标号便于核对。 */
|
||||
OBJCONST TSDOINFOENTRYDESC OBJMEM asSyncDiagEntryDesc[] = {
|
||||
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}
|
||||
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* sub0 : 子索引个数 = 23 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub1 : SYNC0 计数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub2 : SYNC1 计数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub3 : ESC IRQ 计数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub4 : uptime 秒 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub5 : PDI_Isr 累计周期 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub6 : PDO_OutputMapping */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub7 : MainLoop free-run 块 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub8 : PDI_Isr 次数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub9 : PDO_InputMapping 全程 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub10 : 其中输入 PDO 过程数据写 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub11 : AL 事件读×2+超限应答 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub12 : ECAT_Application */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub13 : [DIFF-STATS] 已完成窗口数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub14 : [DIFF-STATS] 窗口内拍数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub15 : [DIFF-STATS] 单拍变化字节峰值 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub16 : [DIFF-STATS] run 总数 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub17 : [DIFF-STATS] 单拍 run 峰值 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub18 : [DIFF-STATS] 段1 变化字节 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub19 : [DIFF-STATS] 段2 变化字节 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub20 : [DIFF-STATS] 段3 变化字节 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub21 : [DIFF-STATS] 段4 变化字节 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub22 : [DIFF-STATS] 段5 变化字节 */
|
||||
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub23 : [DIFF-STATS] 比对自身 DWT 周期 */
|
||||
};
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
@@ -297,7 +330,7 @@ TOBJECT OBJMEM ApplicationObjDicList[] = {
|
||||
asInputEntryDesc, aName0x6000, &s_inputObj, NULL, NULL, 0x0000},
|
||||
|
||||
/* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路; sub5..8 为 1.6.0 剖分埋点) */
|
||||
{NULL, NULL, 0x2000, {DEFTYPE_RECORD, 12 | (OBJCODE_REC << 8)},
|
||||
{NULL, NULL, 0x2000, {DEFTYPE_RECORD, 23 | (OBJCODE_REC << 8)},
|
||||
asSyncDiagEntryDesc, aName0x2000, &s_syncDiag, NULL, NULL, 0x0000},
|
||||
|
||||
/* 终止条目 */
|
||||
@@ -311,7 +344,7 @@ TOBJECT OBJMEM * VARMEM ApplicationObjDic = ApplicationObjDicList;
|
||||
*---------------------------------------------------------------------------*/
|
||||
UINT16 APPL_GenerateMapping(UINT16 *pInputSize, UINT16 *pOutputSize)
|
||||
{
|
||||
*pInputSize = ECAT_PDO_IN_BYTES; /* 62×UINT64 = 496 bytes (1.6.1) */
|
||||
*pInputSize = ECAT_PDO_IN_BYTES; /* 75×UINT64 = 600 bytes (1.6.15) */
|
||||
*pOutputSize = ECAT_PDO_OUT_BYTES; /* 48×UINT64 = 384 bytes */
|
||||
return 0;
|
||||
}
|
||||
@@ -381,6 +414,24 @@ void APPL_InputMapping(UINT16 *pInputData)
|
||||
s_syncDiag.u32AlCyc = g_dbg_alCyc;
|
||||
s_syncDiag.u32AppCyc = g_dbg_appCyc;
|
||||
|
||||
/* [DIFF-STATS] 差分写统计快照(sub13..23):一次取全 11 个窗口值,
|
||||
* 取的都是"上一个已完成的 1s 窗口",故 SDO 读不会拿到半累积值。 */
|
||||
{
|
||||
UINT32 ds[11];
|
||||
EcatDiffStats_GetWindow(ds);
|
||||
s_syncDiag.u32DsWinSeq = ds[0];
|
||||
s_syncDiag.u32DsCycles = ds[1];
|
||||
s_syncDiag.u32DsChangedMax = ds[2];
|
||||
s_syncDiag.u32DsRunsTotal = ds[3];
|
||||
s_syncDiag.u32DsRunsMax = ds[4];
|
||||
s_syncDiag.u32DsSeg1 = ds[5];
|
||||
s_syncDiag.u32DsSeg2 = ds[6];
|
||||
s_syncDiag.u32DsSeg3 = ds[7];
|
||||
s_syncDiag.u32DsSeg4 = ds[8];
|
||||
s_syncDiag.u32DsSeg5 = ds[9];
|
||||
s_syncDiag.u32DsCompareCyc = ds[10];
|
||||
}
|
||||
|
||||
/* 1.5.54:先让 EthercatPdo 把本周期的输入数据写进 0x6000 的对象存储,
|
||||
* 再交给 ESC。经由对象存储中转(而非直接写 pInputData)是为了让主站
|
||||
* SDO 读 0x6000 时看到与过程数据一致的内容。 */
|
||||
|
||||
@@ -70,8 +70,10 @@ MBX_TX_SIZE = 256
|
||||
# 邮箱 SM0/SM1 各 256B 固定占 0x1000-0x13FF,过程数据区只剩 0x1400-0x1FFF=3072B。
|
||||
# SM2/SM3 都是 3-buffer 模式(control 0x64 / 0x20,bit1=0),实际占用 3×size:
|
||||
# SM2 384B -> 0x1400-0x187F (384×3 = 1152B)
|
||||
# SM3 640B -> 0x1880-0x1FFF (640×3 = 1920B) ← 恰好用满最后一字节
|
||||
# (384+640)×3 = 3072 = 过程数据区全长,一字节不剩也一字节不越界。
|
||||
# SM3 600B -> 0x1880-0x1F67 (600×3 = 1800B) ← 1.6.15,DPRAM 留 120B 余量
|
||||
# (384+600)×3 = 2952 ≤ 3072 = 过程数据区全长。历史上 1.5.56 曾取 640B
|
||||
# ((384+640)×3 = 3072 恰好用满),1.6.1 瘦身到 496B 换 CPU,1.6.15 回到 600B
|
||||
# 换批量区容量——推导与取舍见 comm/ethercat_pdo_size.h。
|
||||
# SSC 在 ecatslv.c:708-721 按 (start + size*buffer) 做重叠检查,撞了会在
|
||||
# PREOP->SAFEOP 返回 ALSTATUSCODE_INVALIDSMOUTCFG,不是静默出错。
|
||||
# 要再更大只能压邮箱(拖慢 SDO/FoE)或改 1-buffer(失去读写并发保护),见
|
||||
@@ -79,10 +81,15 @@ MBX_TX_SIZE = 256
|
||||
PD_OUT_OFFSET = 0x1400 # SM2: RxPDO (Master -> Slave outputs)
|
||||
PD_OUT_SIZE = 384 # 48×64bit
|
||||
PD_IN_OFFSET = 0x1880 # SM3: TxPDO (Slave -> Master inputs)
|
||||
PD_IN_SIZE = 496 # 62×64bit (1.6.1 瘦身,原 80×64bit=640)
|
||||
PD_IN_SIZE = 600 # 75×64bit (1.6.15 覆盖窗口;1.6.1 曾瘦身到 496=62×64bit,更早 640=80×64bit)
|
||||
# 1.6.1 瘦身 640→496:批量接收条目数据 64→32B + 段5诊断区前移到 484。
|
||||
# SM3 实占 3×496=1488B (0x1880-0x1E4F),DPRAM 留 432B 余量;
|
||||
# 依据见 comm/ethercat_pdo_size.h 的 1.6.1 注。改后必须重烧 EEPROM 并断电冷启动。
|
||||
# 1.6.15 496→600:批量接收改**定长覆盖窗口** 5 槽(每槽 12B 头含 u32 seqlock
|
||||
# + 32B 数据 = 44B,共 220B,落在 364..583),段5诊断区 484→584。
|
||||
# 动机:旧"环形队列+变长紧排"容量 3 帧/拍,现场 4000 帧/s 下每拍恒丢 1 帧,
|
||||
# 且饱和态喂出了 1.6.9~1.6.13 追的错标/拼接竞态。5 槽 = 5000 帧/s@1ms。
|
||||
# SM3 实占 3×600=1800B (0x1880-0x1F67),out+in=984 ≤ 1024 硬上限,留 120B 余量;
|
||||
# 依据见 comm/ethercat_pdo_size.h 的 1.6.1 / 1.6.15 注。
|
||||
# 改后必须重烧 EEPROM 并断电冷启动。
|
||||
|
||||
# 3-buffer 占用与 DPRAM 上限自检:改尺寸/偏移时立刻在生成阶段拦住,
|
||||
# 而不是等烧进 EEPROM、冷启动后再看 AL 错误码。
|
||||
|
||||
@@ -400,7 +400,7 @@ bool CanFd::initNominalTiming(uint32_t canPeriph)
|
||||
* @param[in] canPeriph 目标 CAN 外设实例。
|
||||
* @param[out] 无。
|
||||
* @retval 无返回值。
|
||||
* @note 时钟源为 APB1=90MHz;按 CONFIG_CAN_DATA_* 宏配置,目标 2.0 Mbps。
|
||||
* @note 时钟源为 APB1=90MHz;按 CONFIG_CAN_DATA_* 宏配置,目标 5.0 Mbps。
|
||||
* 实际数据段波特率 = 90MHz / (CONFIG_CAN_DATA_PRESCALER × (1+BS1+BS2))。
|
||||
* BRS 受 CONFIG_CAN_FD_BRS_ENABLE 控制。
|
||||
*/
|
||||
@@ -414,6 +414,9 @@ bool CanFd::initFdMode(uint32_t canPeriph)
|
||||
can_fd_parameter.fd_frame = ENABLE;
|
||||
can_fd_parameter.excp_event_detect = ENABLE;
|
||||
can_fd_parameter.delay_compensation = ENABLE;
|
||||
/* TDCF/TDCO 不随数据波特率等比缩放:TDCMOD=CALC_AND_OFFSET 下硬件已自动测得
|
||||
* 回路延时(TDCV),这里只是微调,经验取 TDCO ≈ 数据段 BS1 的 tq 数(4TQ→4)。
|
||||
* 放到 12 会让本节点回采时错开约 6 个数据位,自己发出的 FD+BRS 帧被判成错误帧。 */
|
||||
can_fd_tdc_parameter.tdc_filter = 0x04U;
|
||||
can_fd_tdc_parameter.tdc_mode = CAN_TDCMOD_CALC_AND_OFFSET;
|
||||
can_fd_tdc_parameter.tdc_offset = 0x04U;
|
||||
|
||||
@@ -87,6 +87,12 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
|
||||
* 并避免每次上电/每 20s 把 EXMC 引脚切成 GPIO 手动拉一遍。换板/补焊需重新排查浮空时置 1。
|
||||
* 网页万用表通断测试 continuityDrive() 不受此开关影响,始终可用。 */
|
||||
#define CONFIG_ESC_EXMC_DIAG 0
|
||||
/** @brief [DIFF-STATS] 输入镜像差分统计(0x2000 sub13..23,量化差分写能省多少字节、
|
||||
* run 碎片有多严重、比对自身要多少钱)。临时仪表,取证完置 0 即与改造前逐字节等价。
|
||||
* @note 开启时每拍在 PDI_Isr 内多一次 600B 字粒度比对 + 一次快照拷贝(约 10µs 量级,
|
||||
* 实测见 sub23),因此 **sub9/sub10 的写侧耗时会包含这部分**,不能直接与历史
|
||||
* 296µs/265µs 对拍——要看差分潜力就只看 sub13..22,要看真实写侧耗时请置 0 重测。 */
|
||||
#define CONFIG_ECAT_DIFF_STATS 0
|
||||
/** @brief 启动横幅与 main 进度打印。 */
|
||||
#define CONFIG_APP_BANNER_LOG_ENABLE 1
|
||||
|
||||
@@ -122,7 +128,14 @@ 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 = 13U; /* 1.6.13:根治 —— 1.6.12 的 PDI 入口预抽实测(60s 正弦指令负载,链路活性已断言):overfull 50/s→0 但指纹违规仍 ~92/s、torn 在无溢出阶段 1-7/s 持续涨 —— 判决:溢出只是共犯之一,**357µs 的 PDI 阻塞窗口本身**才是病理母体(窗口内 FIFO 满溢毁窗口 + 被冻结的取帧路径留着不依赖溢出的换帧路径,MI/MP 只抓到少数)。本版釜底抽薪:CAN RX ISR 提到 preempt=0(CONFIG_IRQ_PREEMPT_CAN_RX,与 SYNC0 同级互不抢占)—— RX 不再被 PDI/SYNC1/SCHED 压住,FIFO 深度 ≤2、溢出与冻结同时消失,receiveFrameChecked 的多字取数自此真正原子;TX ISR 留在 2(PDI 上下文有 TX 提交,提 0 会引入提交/释放竞争)。代价:SYNC0/PDI 偶发 µs 级延迟(RX ISR ~3µs/帧×8kHz≈2.4% 占空),相对 1ms 周期是零头。PDI 预抽 pumpAllChannels 随之撤除(失去意义且引入 PDI/ISR 双消费者风险)。保留 1.6.12 的诚实管道(校验只计不丢)与诊断布局(overfull@490、relto@492、seq u16@488)作为验证仪表:修好 = reject/torn/overfull 恒 0。 */
|
||||
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。 */
|
||||
/* 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 重测。 */
|
||||
/* 1.6.16:修段 1/段 3 快照 drop 的计数口径(纯判据修复,PDO 布局逐字节不动)。病理:`onCanFrame` 里判的是 `if (s_rxValid) ++s_dropCount`,而 s_rxValid 置位后**永不清零**(只有 flags bit0 清统计会归零计数,不动 valid),于是开机收到第一帧之后**每一帧都算一次丢弃**,drop ≡ rx−1 (mod 2^16)。实测 period=1ms、CAN 侧 6000 帧/s 时 drop 同样 6000/s,与 rx_count 逐拍 1:1(Δ 完全相等),(rx−1) mod 65536 与上报值精确吻合。后果:主站 `/api/motor/state` 的 bridge.drop 被 transport-ecat.js 当状态栏 **ERR** 显示、且它是桥全局值三通道同数 → 网页"ERR 多得离谱"其实只是收帧速率,零灵敏度、纯假仪表;calibrate_ecat_metrics 一类拿 drop 做分子分母的判据恒得 100%。本版加 s_rxPublished(写侧置 false / buildInput 段 1 发布分支置 true,与段 4 的 s_rxWinSeen 同一套"未被任何一拍取走才算丢"约定),判据改 `s_rxValid && !s_rxPublished`;计数落在 xrxSeq 奇数窗口内,消费者判奇会跳过段 1/段 3 故不会并发置位,读+自增对消费者等效原子。flags bit0 清统计时一并置 published=true,免得清零后的第一帧被误计。s_rxValid 语义不变("有过接收",flags bit0 与 backlog 取通道都依赖它常驻),故 ecat.html 短快照、daemon 的 xrx 分派、ESP32 侧 0xC1 全部零改动。**修完不会归零**:段 1/段 3 是单槽、每拍只能发布 1 帧,6000 帧/s @1ms 下仍约 5000/s(≈rx 的 0.83),这个数自此真正的含义是"快照槽位跟不上收帧速率",不是总线错误;真·CAN 错误仍看 ESP32 `/can/feedback` 的 err/TEC/REC,真·入窗丢帧看段 4 的 OVF(362) 与 seq 空洞。 */
|
||||
/* 1.6.15:批量接收改**定长覆盖窗口**,输入 PDO 496→600(75×64bit)。动机:1.6.1 瘦身后批量区是"环形队列 + 变长紧排排空",容量 3 帧/拍(=3000 帧/s),现场 4000 帧/s 下每拍恒丢 1 帧(can_cadence lost=512/drain FAIL);更糟的是排空赤字把队列长期压在饱和态,而 1.6.9~1.6.13 追的错标/拼接竞态正是饱和态喂出来的(判满用的 tail 一旦滞后,生产者就写进消费者刚读过的槽)。本版按用户裁定改成覆盖式:生产者(onCanFrame)按自己游标写、绕圈覆盖,消费者(fillRxWindow)每拍整块取值 —— 没有 tail/判满/pending,那个滞后握手的载体直接不存在。定长(5×44B@364)让主站不必推游标,1.6.10 那类边界越界写诊断区的错在结构上不可能再犯。**每槽必须带 u32 seqlock**:buildInput 跑在 PDI_Isr(preempt=0) 会抢占主循环写侧,没有它主站能拿到"新 ID + 旧数据"拼出的假帧(不是"差一个周期",是现实中不存在但看起来合法的帧,与刚修的拼接同形)。seq 兼作主站去重与丢帧判据(空洞数=被覆盖未取的帧),与从站侧新加的 OVF("未取先覆"次数) 互为对账仪表。语义变更:批量区从"保证不丢的事件流"变成"最近 N 帧窗口"——同一帧会连续几拍重复出现,低频一次性帧(版本/节点应答)在突发下可能被静默覆盖;主站 daemon 层按 seq 去重后归一成事件流,故 motor.js/ecat.html/can_cadence.py 判据不变、前端零改动。容量:5 槽=5000 帧/s@1ms,对 4000 帧/s 留 25% 余量(STATUS 自 1.6.9 起不入窗,实际入窗量是 CONFIG 量级);覆盖窗口**不创造容量**,槽数 < 帧/拍 仍会静默丢帧。写侧 600×0.441≈265µs/拍(26.5%),仍优于旧 640B 的 296µs,更是"提到 4kHz"方案 87.5% 占空不可行结论(dc_bench 固化)的零头。段1..3(0..359) 逐字节不动,段5 诊断区 484→584。三端同步:ethercat_pdo_size.h / bridge.h+cpp / dc_app.c(62→75) / gen_eeprom.py / DCU_ESI.xml / ecat_native.c decode_input(新增 600..639 分支,向后自适应 496 与 640)。必须重烧固件 + 烧 EEPROM + 断电冷启动,只烧一边 SAFEOP 报 AL 0x001A/0x0011。 */
|
||||
/* 1.6.14:CAN-FD 数据段 2Mbps→5Mbps(CONFIG_CAN_DATA_PRESCALER 9→3、CONFIG_CAN_DATA_BS1 3TQ→4TQ ⇒ 90MHz/(3×6TQ),采样点 80%→83.3%;TDC filter/offset 4→12 tq 维持约 400ns,因 TDCF/TDCO 以数据段 tq 为单位)。动机:电机 STATUS48B+CONFIG24B×2 节点@1kHz 加 1kHz 下行 CMD,2Mbps 下负载约 120%(实测总线帧数被钉在 ~4993 帧/s),高频下发时 RX FIFO 拥塞。仲裁段仍 1Mbps 不变。配套:电机 APP 与 BOOT 的 data_prescaler 同步改 2(60MHz/(2×6TQ)=5Mbps)——FD 数据段速率必须全总线一致,节点 TX 都带 fd_brs。 */
|
||||
/* 1.6.13:根治 —— 1.6.12 的 PDI 入口预抽实测(60s 正弦指令负载,链路活性已断言):overfull 50/s→0 但指纹违规仍 ~92/s、torn 在无溢出阶段 1-7/s 持续涨 —— 判决:溢出只是共犯之一,**357µs 的 PDI 阻塞窗口本身**才是病理母体(窗口内 FIFO 满溢毁窗口 + 被冻结的取帧路径留着不依赖溢出的换帧路径,MI/MP 只抓到少数)。本版釜底抽薪:CAN RX ISR 提到 preempt=0(CONFIG_IRQ_PREEMPT_CAN_RX,与 SYNC0 同级互不抢占)—— RX 不再被 PDI/SYNC1/SCHED 压住,FIFO 深度 ≤2、溢出与冻结同时消失,receiveFrameChecked 的多字取数自此真正原子;TX ISR 留在 2(PDI 上下文有 TX 提交,提 0 会引入提交/释放竞争)。代价:SYNC0/PDI 偶发 µs 级延迟(RX ISR ~3µs/帧×8kHz≈2.4% 占空),相对 1ms 周期是零头。PDI 预抽 pumpAllChannels 随之撤除(失去意义且引入 PDI/ISR 双消费者风险)。保留 1.6.12 的诚实管道(校验只计不丢)与诊断布局(overfull@490、relto@492、seq u16@488)作为验证仪表:修好 = reject/torn/overfull 恒 0。 */
|
||||
/* 1.6.12:源头修复 —— 用户裁定"过滤是遮羞布,修采集源头,前端只许诚实反馈"。1.6.11 上线后实测(repro_cmdload 正弦负载)reject@494 仍以 ~384/s 增长:RFOD=DISABLE 挡不住溢出毁窗口(本 IP 满溢时 DISCARD 语义存疑,或毁帧发生在满态下的读并发路径),内容校验丢弃只是把坏数据拦在计数前。本版两处:(1) **源头**:CanFd::pumpAllChannels() 挂进 onEscIrqExti 的 PDI_Isr 入口 —— 在 buildInput 357µs 长阻塞(CAN ISR preempt=2 全程被压)开始之前把三路 FIFO 抽干,阻塞窗口起点深度=0、窗口内到达 ≤2-3 帧,3 深度 FIFO 从"必然打满"变成"必然不满";preempt=1 + SYNC0 已关的上下文保证与 CAN ISR 取帧互斥,与主循环批量队列生产零交集。(2) **诚实管道**:内容校验改为只计不丢(reject@494 恒 0 = 源头修好;还在涨 = 源头未愈且页面如实显示),诊断区加挂判决性测量:seq 488 拆 u16、overfull(RFO0)@490、release_timeout@492(原重入计数位,恒 0 让出)。reject 与 overfull 是否同涨 = "残余拼接是否仍源自溢出"的判决。 */
|
||||
/* 1.6.11:根治残余拼接 —— 1.6.10 的 settle 延迟实测无效(正弦指令负载下拼接率不变),节点指纹实验(两电机反相正弦+速度限 200/150 打指纹)实锤拼接是**跨节点**的:node3 的 STATUS 帧头 24B = node2 的完整 CONFIG(速度指纹 200.0、角度 1142° 均属 node2 轨迹),且 CONFIG↔CONFIG 同型拼接结构校验抓不到(两边都合法)—— 电机 TX 不可能发出别的电机的字节 ⇒ 混合点在 GD32 RX 侧。真根因:can_init 的 rec_fifo_overwrite=ENABLE —— FIFO 满时覆盖最旧邮箱,而最旧邮箱正是 CPU 正在读的输出窗口;buildInput 在 PDI_Isr 占 ~357µs 压住 CAN ISR,发指令时总线 ~8kHz、阻塞窗口内 ~3 帧打满 FIFO 深度 → 覆盖进行中被读走 = 拼接(MI/MP 回读挡不住:MI 是覆盖序列最后更新的一环;settle 延迟也挡不住:非时间竞态而是溢出策略)。本版:(1) rec_fifo_overwrite=DISABLE —— 溢出丢新帧置 RFO0(rx_fifo_overfull_count 已统计),周期量丢一拍下拍即补,与软件队列"满丢新不覆旧"同则;(2) 撤 settle 延迟(无效且白烧 CPU);(3) 保留 1.6.10 的重取循环删除与边界修正,并新增入队前内容校验(STATUS 温度/电压量程+帧间斜率、CONFIG 未使用区/模式)丢结构矛盾帧、计数段5@494 —— 作为溢出之外的纵深防御(实测能挡跨型拼接,挡不住同型 CONFIG↔CONFIG,故必须配合 (1))。验证:正弦指令负载 60s,指纹违规 40→0。 */
|
||||
/* 1.6.10:根治拼接帧 —— 1.6.9 的"队列覆写"定性被自测推翻为**次要**:段 2 电机槽(MotorState 路径,与批量环形队列完全独立)出现同签名拼接(node2 槽头 = node3 CONFIG 前缀 03c12200,解出 mcuTemp=-161.25),且 rx_torn/pdo_reentry 自启动恒 0 —— 两条独立下游同时中招 ⇒ 混合点在共同上游 CAN RX ISR;校验恒不报 ⇒ 拼接发生在 MI/MP 判据盲区。真根因=1.6.7 的"校验失败原地重取":RFIFOMDATA0 读指针随读自动前进且不因换帧复位,attempt1 失败后 attempt2 从错位指针开读,MI/MP 比对的是窗口身份、管不住指针错位,于是"校验通过、内容为上一帧尾巴拼下一帧开头"。实测(自测脚本 scripts/selftest_torn.py,按拍去重+灵敏度自证):空闲 0%(35k 条),电机活跃时 9.155%(56835/620838,STATUS 错标为 CONFIG 占绝对多数)。本版:(1) 去掉重取循环,校验失败即 release+丢帧+rx_torn 计数(1.6.6 语义,STATUS/CONFIG 均 1kHz 周期量,丢一拍下拍即补);(2) fillRxBatch 边界改判段 5 起点 ECAT_DIAG_OFF_LOAD(484) 而非 PDO 全长(496)——潜伏越界写诊断区,当前容量下触不到,容量放宽即咬人;(3) releaseFifo0 自清后补 settle 延迟 —— 烧(1)(2)后实测活跃流量下仍残余 0.11% 跨节点拼接(node2 槽头=node3 CONFIG 前缀,且 rx_torn 已正常计数 146,证明窗口换帧路径已被挡住),定性为 RFD 自清只代表 dequeue 握手完成、下一帧 MDATA 还在逐字搬运(MI/MP 先落数据区后落),while 连排的第二次读紧跟 release 即拿到陈旧头+新鲜尾,MI/MP 回读对这种陈旧全盲;空闲每 ISR 单帧故 0%、活跃连排才中招,与实测分布吻合。 */
|
||||
@@ -207,7 +220,7 @@ constexpr uint32_t CONFIG_ESP32_RESET_PIN = GPIO_PIN_9;
|
||||
* → APB1=/2=90MHz (CAN_CLK)
|
||||
*
|
||||
* 仲裁段目标: 1.0 Mbps = 90MHz / (9 × 10 TQ)
|
||||
* 数据段目标: 2.0 Mbps = 90MHz / (9 × 5 TQ)
|
||||
* 数据段目标: 5.0 Mbps = 90MHz / (3 × 6 TQ),采样点 83.3%
|
||||
*
|
||||
* @note RCU_MODIFY_UP 宏在 system_clock_180m_hxtal() 末尾将 AHB 从 /4
|
||||
* 临时切到 /1,这是 GigaDevice 官方行为,不可删除。
|
||||
@@ -225,12 +238,13 @@ constexpr uint32_t CONFIG_CAN_NOMINAL_BS1 = CAN_BT_BS1_7TQ;
|
||||
/** @brief 仲裁段时间段 2(PHSEG2)。 */
|
||||
constexpr uint32_t CONFIG_CAN_NOMINAL_BS2 = CAN_BT_BS2_2TQ;
|
||||
|
||||
/** @brief 数据段预分频值。 */
|
||||
constexpr uint32_t CONFIG_CAN_DATA_PRESCALER = 9U;
|
||||
/** @brief 数据段再同步跳跃宽度。 */
|
||||
/** @brief 数据段预分频值。
|
||||
* CAN_DBT 的 DBAUDPSC[9:0] 独立于仲裁段 BAUDPSC,数据段可自取分频。 */
|
||||
constexpr uint32_t CONFIG_CAN_DATA_PRESCALER = 3U;
|
||||
/** @brief 数据段再同步跳跃宽度。上限受 BS2=1TQ 约束,不可放宽。 */
|
||||
constexpr uint32_t CONFIG_CAN_DATA_SJW = CAN_BT_SJW_1TQ;
|
||||
/** @brief 数据段时间段 1。 */
|
||||
constexpr uint32_t CONFIG_CAN_DATA_BS1 = CAN_BT_BS1_3TQ;
|
||||
/** @brief 数据段时间段 1。4TQ + 1TQ(同步) + 1TQ(BS2) = 6TQ → 5 Mbps。 */
|
||||
constexpr uint32_t CONFIG_CAN_DATA_BS1 = CAN_BT_BS1_4TQ;
|
||||
/** @brief 数据段时间段 2。 */
|
||||
constexpr uint32_t CONFIG_CAN_DATA_BS2 = CAN_BT_BS2_1TQ;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user