重大更正(2026-09-12 复核):220µs 的归因被动摇,两个变量被混在一起
用户质疑"5 个 STATUS 变 3 个,怎么会差 20 倍"——顺着查,发现两件原调查没做的事:
-- 机制核对(代码层):两版 buildInput 逐行对比,纯 CPU 差仅 1~3µs(旧:5×48B + 5×44B 无条件拷贝;新:27 槽选最新 + 影子比对 + 3×48B)。对 ESC 的 PRAM 写两版同为整块 600B(DWT 实测 0.44µs/B → ≈265µs/拍,
ethercat_pdo_size.h),且 A/B/A 证明它对主站往返不可见——"删掉覆盖窗 / 差分填充省出的 CPU"解释不了 229µs。另:差分填充在本台工况下基本不命中(STATUS ~1.25 帧/拍/台,槽 seq 几乎每拍都变,重建路径照样走)。
+- 机制核对(代码层):两版 buildInput 逐行对比,纯 CPU 差仅 1~3µs(旧:5×48B + 5×44B 无条件拷贝;新:27 槽选最新 + 影子比对 + 3×48B)。对 ESC 的 PRAM 写两版同为整块 600B(从站侧写 ESC 成本,与主站 rx 不是同一段时间),且 A/B/A 证明它对主站往返不可见——"删掉覆盖窗 / 差分填充省出的 CPU"解释不了 229µs。另:差分填充在本台工况下基本不命中(STATUS ~1.25 帧/拍/台,槽 seq 几乎每拍都变,重建路径照样走)。
- 反例数据(9/8 存量扫描):
lab/ecat_rate_sweep.csv——win 布局、0.5ms 档 cyc_p50=124.6µs、达成 100%;1ms=126.9µs;0.125ms=119.5µs 且实测 7780Hz(单拍必须 ≤128µs 才可能)。与 dense 的 124µs 同水平。
混淆的变量(刷板台账 git 史 + 主站 git 史):9/8 板上镜像 201832b5…(win)→ 9/10 前已刷 4e84a354…(win 期 1.6.15→1.6.20 演进);主站 9/9 03:05 提交 97fab23(三布局自适应、发现窗基址 {0,7,14,20}→{0,5,10,15,20,22}、覆盖窗按 seq 归一)。345µs 出现在这两个改动之后、9/8 之前——原归因把回归记在 1.6.21 密排头上,但密排也可能只是"绕开"了回归(dense 下主站强制 n_bases=1,不轮转窗口)。
@@ -103,7 +103,7 @@ ul{padding-left:22px}
| 段 | 0.5ms 实测 p50 | 从外到内的组成 | 出处 / 尚未拆开的部分 |
| 发送(蓝) | 28.6µs | 组帧 ≈0.2µs(组 LRW + 填输出区)→ pcap_sendpacket ≈28.4µs:NPF 栈把 1016B 交给 I210 驱动并提交发送 | /api/motor/dbg 拆段(sd_us 列)。驱动/DMA 内部无仪表;B 臂曾见发送 p99 348.9µs 的偶发阻塞(单窗样本,未立论) |
| 线路(粉) | 81.3µs | 帧 1016B = 过程数据 984B(输出 384 + 输入 600)+ 以太网头 14 + EtherType 2 + 数据报头 10 + WKC 2 + FCS 4;8128bit ÷ 100Mbps;再加前导 8B + 帧间隙 12B ≈ +1.6µs;从站 ESC 硬件转发 µs 级 | 由帧构成计算,两布局逐字节同长——结构上不可能解释 220µs 差 |
-
| 从站准备(红)· win | ≈229µs | 1.6.15 每拍在 PDI 里:① 按 win_base 轮转准备 27 槽表 ② 段4 覆盖窗 5×44B 拷贝(fillRxWindow/RxWinSlot)③ MotorState 重读 + 逐槽 48B 拷贝 ④ 600B 经 HBI/PRAM 推入 ESC RAM | 实测差值 310−81.3。但 ①②③ 的纯 CPU 差按代码对比仅 1~3µs,④ 与整块 600B PRAM 写(≈265µs/拍)两布局同在且 A/B/A 证明不影响往返——229µs 的载体机制未闭环,且 9/8 同布局实测 124.6µs 表明"345µs=win 固有成本"的前提不成立(见"重大更正"卡) |
+
| 从站准备(红)· win | ≈229µs | 1.6.15 每拍在 PDI 里:① 按 win_base 轮转准备 27 槽表 ② 段4 覆盖窗 5×44B 拷贝 ③ MotorState 重读 + 逐槽 48B 拷贝 ④ 整块 600B 写入 ESC | 实测差值 310−81.3。①②③ 纯 CPU 差仅 1~3µs;④ 是从站写 ESC(两布局同在、不影响主站往返)——229µs 载体未闭环;9/8 同布局 124.6µs 表明"345µs=win 固有"不成立 |
| 从站准备(红)· dense | ≈10µs | 差分填充命中:节点集合与各 seq 未变 → 整段复用影子,①②③全免;只剩 count 个槽的更新推送 + ESC 取数 | rx 92.1 − 81.3。220µs 的差全部消失在这里 |
口径备注:win 时代插桩拆段亦记录到 pcap_sendpacket 181.1µs、其后续中位 1 次 poll 即得帧(§四表)——该口径下发送调用内部吸收了部分等待,发送/等回包两段的分界随仪表口径浮动;但整拍总量(345)与"扣除线路后的从站侧总延迟"(≈229)不受此影响。另 1ms 档每拍 next_ex 轮询中位 79 次(轮询间隔 ~1.2µs),0.5ms 档为紧轮询。
diff --git a/lab/_last_gd32_sha.txt b/lab/_last_gd32_sha.txt
index fa4a0ec..4b342f8 100644
--- a/lab/_last_gd32_sha.txt
+++ b/lab/_last_gd32_sha.txt
@@ -1 +1 @@
-dda92d11876f059d27390232e88b5c9fa7c0eb57e442365f5247d4acdefb9b20
\ No newline at end of file
+c6827f84e9b8fa07288d0fc2081d0efc48a6ef2c5b8124b85a406f3a7a02042a
\ No newline at end of file
diff --git a/lab/lan9252_pm.html b/lab/lan9252_pm.html
index e54421f..29c3689 100644
--- a/lab/lan9252_pm.html
+++ b/lab/lan9252_pm.html
@@ -260,19 +260,31 @@
}
/* 跨标签/重载保持睡眠:HBI 读会唤醒芯片,页面销毁后现场丢失会导致
- * 切回时自动 poll(1E) 把睡眠叫醒。用 sessionStorage 记档位,重载时跳过自动读。 */
+ * 切回时自动 poll(1E) 把睡眠叫醒。sessionStorage 记本标签档位;另存一份
+ * localStorage 供换标签页/重开浏览器时兜底(宁可按睡眠处理少一次自动读,
+ * 也不能自动 1E 把芯片叫醒)。 */
var SS_SLEEP = "lan9252_pm_asleep";
+ var LS_SLEEP = "lan9252_pm_asleep_any";
function saveSleepFlag(n) {
try { sessionStorage.setItem(SS_SLEEP, String(n)); } catch (e) {}
+ /* localStorage: "n@ms" —— 带时间戳,换标签/重开浏览器可追溯入睡时刻 */
+ try { localStorage.setItem(LS_SLEEP, n + "@" + Date.now()); } catch (e) {}
}
function clearSleepFlag() {
try { sessionStorage.removeItem(SS_SLEEP); } catch (e) {}
+ try { localStorage.removeItem(LS_SLEEP); } catch (e) {}
}
function loadSleepFlag() {
try {
var v = sessionStorage.getItem(SS_SLEEP);
if (v === "1" || v === "2" || v === "3") return parseInt(v, 10);
} catch (e) {}
+ try {
+ var lv = localStorage.getItem(LS_SLEEP) || "";
+ /* 兼容旧值 "1"/"2"/"3" 与新值 "1@1760029000000" */
+ var m = String(lv).match(/^([123])(?:@(\d+))?$/);
+ if (m) return parseInt(m[1], 10);
+ } catch (e) {}
return 0;
}
function renderAssumedSleep(n) {
@@ -299,6 +311,7 @@
/* 仅在确认芯片已离开睡眠(唤醒成功 / 读到 SLEEP_EN=0 / NRST)时清 flag */
function markAwake() {
clearSleepFlag();
+ pmHold(0); /* 芯片已醒:解除主站看门狗豁免,恢复正常自愈 */
$("sleepBanner").textContent =
"🌙 芯片处于低功耗档 —— EtherCAT 通信已中断,点「手动唤醒」恢复";
}
@@ -396,6 +409,17 @@
function ecatMotorStart(timeoutMs) {
return ecatPost("motor/start", {}, timeoutMs || 30000);
}
+ /* 主站楔死看门狗豁免:休眠期间 ecat_native 不判死不重拉(否则芯片醒来
+ * 即被偷偷拉回 OP——2026-09-12 实测休眠 ~20s 自动"复活")。解除时主站
+ * 自带 60s 冷却,让本页唤醒流程先走完。best-effort:native 未起只记日志。 */
+ function pmHold(on) {
+ return ecatPost("pm/hold", { on: on }, 5000).then(function (r) {
+ log("主站看门狗豁免" + (on ? "置位" : "解除") +
+ (r.ok ? "" : "失败: " + r.log.join(";")), r.ok ? "ok" : "warn");
+ }).catch(function (e) {
+ log("主站看门狗豁免" + (on ? "置位" : "解除") + "失败: " + e.message, "warn");
+ });
+ }
function ecatEnsureOpen() {
/* bus_open=0 时 motor/start 直接回「未连接」。与电机页 ensureOpen 同源。 */
return ecatPost("native/health", {}, 6000).then(function (h) {
@@ -420,6 +444,7 @@
});
}
function recoverEcatMaster() {
+ pmHold(0); /* 唤醒流程开始即解除豁免(主站给 60s 冷却,不干扰本流程) */
log("重连 EtherCAT 主站:stop → open → NRST → motor/start …", "warn");
/* 先停在跑的 PDO 循环,避免与 NRST/init 抢总线 */
return ecatPost("motor/stop", {}, 8000).catch(function () { return { ok: true, log: ["stop 跳过"] }; })
@@ -519,6 +544,7 @@
pollPaused = true;
setConn(true, "睡眠中 · 轮询已暂停");
saveSleepFlag(n);
+ pmHold(1); /* 请主站看门狗整段静默,别把睡着/刚醒的芯片偷偷拉回 OP */
renderStatus(b);
/* 固件返回可能是新 status 或旧字节;强制按 D{n} 展示并记住 */
setCell("stSleep", "1", "v-warn");
@@ -568,6 +594,9 @@
renderAssumedSleep(restoredSleep);
setConn(true, "上次睡眠中 · 轮询已暂停");
log("恢复上次会话:D" + restoredSleep + " 睡眠中。为避免 HBI 读唤醒芯片,已跳过自动状态刷新;点「读状态」或「手动唤醒」继续。", "warn");
+ /* 新标签/重开浏览器:native 可能刚重启导致 g_pm_hold=0,楔死线程会趁
+ * 芯片睡着时重拉(D1/D2 报文即唤醒)。必须重新置位豁免。 */
+ pmHold(1);
probeEspOnly();
} else {
log("就绪。状态每 2s 自动刷新;改设备 IP 请到 OTA 中继页(全局共享)。", "warn");
diff --git a/lab/pdo_layout.html b/lab/pdo_layout.html
index 3354333..3887365 100644
--- a/lab/pdo_layout.html
+++ b/lab/pdo_layout.html
@@ -252,7 +252,7 @@ footer code{font-size:11px}
真源 ethercat_can_bridge.h
尺寸 ethercat_pdo_size.h
-
PRAM 0.441 µs/B
+
PRAM 写 从站侧(≠ 主站 rx)
魔数 @35=0xA5
电机槽 ≤9 × 56 B
稀疏写 CONFIG=1
@@ -437,20 +437,20 @@ for (i = 0; i < count; i++) {
4
每拍成本与 SM 预算
- PRAM ≈0.441 µs/B · 稀疏写 1.6.22+
+ 从站写 ESC 成本 · 稀疏写 1.6.22+(不是主站 rx)
输入写侧 · 3 电机稀疏
-
116
-
µs/拍 · 264 B×0.441 · 11.6% @1ms
+
—
+
从站写 ESC · 按实占字节估(≠ 主站 rx)
输入写侧 · 整包基线
-
265
-
µs/拍 · 600×0.441 · 26.5% @1ms
+
—
+
从站写 ESC · 600B 整包(≠ 主站 rx)
@@ -524,7 +524,7 @@ count=9 → 整包 600 B(两段相接,回退)
布局真源 comm/ethercat_can_bridge.h(1.6.21)· 尺寸 comm/ethercat_pdo_size.h ·
固件版本 config.h APP_FW_VERSION = 1.6.24 ·
稀疏写 ethercat_pdo.cpp sparseEscWritePlan + ecatappl.c PDO_InputMapping ·
- 成本系数 0.441 µs/B · 布局魔数 kEcatMotLayoutMagic = 0xA5
+ 成本说明:从站写 ESC 与主站 rx 是两段时间 · 布局魔数 kEcatMotLayoutMagic = 0xA5
diff --git a/lab/spi_test.html b/lab/spi_test.html
index 1eb541b..d784f75 100644
--- a/lab/spi_test.html
+++ b/lab/spi_test.html
@@ -1,1869 +1,1842 @@
-
-
-
-
-
-
-
链路测速台 · SPI / EtherCAT
-
-
-
-
-
-
-
-
-
-
-
-
本次结果 三段计时均取自固件原始行,不做二次推断
-
-
-
单帧分段平均(固件五段拆解,占总往返比例)
-
-
跑一次测速后出现
-
延迟分布(固件 7 档计数,条长按最大档归一)
-
-
跑一次测速后出现
-
-
- 设备原始返回
- 等待测试…
-
-
-
-
-
本页历史 仅存内存,刷新即清空 —— 改参数前后对比用
-
-
-
-
-
-
-
-
观测控制 本页只读过程数据缓存,不连接、不重启、不停 PDO 循环
-
-
-
-
-
-
- 未开始
- 链路 —
- AL —
-
-
- 热改周期走 /api/motor/period(循环每拍重读的变量,全程喂狗不断流,随时可调、可反复调)。
- 低于 1ms 档后端自动用自旋节拍,会占满一个 CPU 核;从站固件修好前别低于 1ms —— 实测
- 0.25 / 0.125ms 档 GD32 顶到 99.5%,CAN 到帧率 λ 从 4000/s 塌到 149/s(瓶颈在从站不在主站,
- 只看主站达成率会整条看漏)。
-
-
-
-
-
刷新率 实测 = Δcycles/Δt;名义 = 1000/period_ms 仅作对照,两者差值就是软件调度开销
-
-
-
-
-
-
单电机状态刷新
—Hz
slot_period_ms —
-
CAN 到帧率 λ
—/s
Δglobal_seq (u32)
-
真丢帧
—帧/拍
主站 seq 空洞 · GD32 OVF(抽样单拍值)
-
-
从站 CPU 负载
—
load_perm · age —
-
-
撕裂 / 重入
—
rx_torn / pdo_reentry
-
本页请求耗时
—ms
本页→代理→native 最慢一个请求的往返(≠节拍耗时)
-
-
- 判读:达成率 < 100% = 节拍被软件拖住;λ ÷ 实测Hz = 每拍到达几帧 CAN(当前 1ms 档实测 6 帧/拍)。
- drop 不判好坏:它是段 1/段 3 那个"最近一帧"单格被未取先覆的次数,
- 而单格每拍只能发布 1 帧;且自增点在 STATUS 过滤之前(ethercat_can_bridge.cpp:398
- 对 :435),所以每拍到的 6 帧全算进来 → 恒等于 λ − 实测Hz,零独立信息量。
- 这些帧并没有丢:STATUS 由段 2 按槽常驻上报,其余进段 4 覆盖窗口(实测 lost/OVF 均 0)。
- drop 哪天才有权报警:让只走单格的帧(版本/节点应答)承担可靠投递时。
- 真丢帧逐层看:① CAN 控制器 err/TEC/REC;② CAN 软件 RX 队列满(深度 16)的
- rx_drop_count —— 只在 ESP32 /can/feedback 有,过程数据里没有;
- ③ 段 4 覆盖窗口 主站侧按 seq 空洞算的 rx_batch.lost(step/2−1,
- 发布值恒为偶数,按 step−1 会把每帧虚报成丢 1 个);④ 段 4 GD32 侧未取先覆
- rx_batch.pending(= OVF,与 ③ 是两个独立仪表,正常应当同步增长);
- ⑤ CAN 取帧撕裂 rx_torn。③④ 是本页 10Hz 抽样看到的单拍值,只能当下界。
- 撕裂 / 重入是饱和计数(到 65535 停),封顶时只能报下界。
-
-
-
-
-
实时曲线 滚动 90s · 紫 = 实测 PDO Hz(左轴) · 青 = 单拍 p50 µs(右轴)
-
-
-
-
-
-
总线与从站 /api/status · /api/native/health · state.head
-
-
窗口自适应:win_bases 是当前收缩出的接收窗口基址数,
- 单槽刷新 = period_ms × win_bases.length;开机首圈先扫全表(6 拍)。
-
-
-
桥接计数 state.bridge · 桥全局,无按通道拆分 / 无 TEC·REC
-
-
过程数据里没有逐通道 TEC/REC —— 没有的就显示 —,
- 不编 0 让"总线正常"和"没这个数据"长得一模一样。
-
-
-
-
-
收到的东西 ① 电机状态槽 state.motors[] · 48B STATUS 解码(DBC Motor_CAN_FD v0.74)
-
-
逐槽原始 hex(48B 全长,16B 一行、多列排)
- —
-
-
「速度参考」= RT_Velocity,是内部斜坡参考值而非实测转速,
- 不能拿去回答"三轴实际是否同步"。InitStatus 显示 — 表示这条链路没送
- B32-35 —— 补 0 会把"无数据"显示成"初始化正常"。
-
-
-
收到的东西 ② 密排整轮 + 长帧事件 rxraw.parsed · 1.6.21 密排:段2整轮槽 + 段3长帧变更
-
- 本拍新帧 / seq 空洞 / GD32 OVF —
- 本页帧样本 —
-
-
-
-
- 密排整轮槽(GD32 每拍原样拷出 count×56B:node ch online dlc seq + 48B STATUS)。
- 末列「本拍新数据」:是 = seq 比上一拍大(差分命中,状态真变了);否 = seq 未变,固件本拍跳过填充。
-
-
按子码汇总(每 sub 一行的采样次数 / 最近载荷)
-
-
-
- 密排整轮(1.6.21):接 N 个电机本拍就发 N 个状态,空槽不发送。
- STATUS 全在上方状态槽解码;本表是同一拍的线级原样(seq / online / 前 8B 载荷),
- 用来核对"差分是否真的跳过了未变电机"。
- 长帧(CONFIG 24B / 版本应答):段 3「最近一帧」,内容变了才进上面的事件表
- (主站按 ch/dlc/id/data 逐字节比对,相同不重复上报)。逐字节的真凭实据在下面第 ③ 区。
- 本表看不到的还有三层 —— CAN 控制器 err/TEC/REC、GD32 软件 RX 队列满丢(深度 16,
- 只在 ESP32 /can/feedback 有)、CAN 取帧撕裂 rx_torn。
- 逐层对账跑 scripts/can_bridge_reconcile.py。
-
-
-
-
收到的东西 ③ 原始接收窗口 + 长帧 + 逐拍环 /api/motor/rxraw · state.xrx · /api/motor/dbg(全为只读缓存拷贝)
-
-
-
-
- layout:dense=1.6.21 密排整轮 / win=1.6.15 定长覆盖窗口 / slim=变长 / old=旧布局,
- 解析器必须按 layout 选,否则拿变长游标解定长窗口会整片错位。
-
-
xrx 长帧完整载荷(kv 里那行是截断的,这里不截)
-
—
-
-
-
-
-
逐拍环未开启。开启后实测每次约 160KB(后端 RING=2048 拍),
- 只在需要看抖动分布时开,别常年挂着 —— 仪表本身也是负载。
-
-
-
-
接收区原始字节 —— dense:8B 整轮头 + count×56B 槽;win:44B/槽 覆盖窗口
-
—
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
链路测速台 · SPI / EtherCAT
+
+
+
+
+
+
+
+
+
+
+
+
本次结果 三段计时均取自固件原始行,不做二次推断
+
+
+
单帧分段平均(固件五段拆解,占总往返比例)
+
+
跑一次测速后出现
+
延迟分布(固件 7 档计数,条长按最大档归一)
+
+
跑一次测速后出现
+
+
+ 设备原始返回
+ 等待测试…
+
+
+
+
+
本页历史 仅存内存,刷新即清空 —— 改参数前后对比用
+
+
+
+
+
+
+
+
观测控制 本页只读过程数据缓存,不连接、不重启、不停 PDO 循环
+
+
+
+
+
+ 空闲
+ 未开始
+ 链路 —
+ AL —
+
+
+ 热改周期走 /api/motor/period(循环每拍重读的变量,全程喂狗不断流,随时可调、可反复调)。
+ 低于 1ms 档后端自动用自旋节拍,会占满一个 CPU 核;从站固件修好前别低于 1ms —— 实测
+ 0.25 / 0.125ms 档 GD32 顶到 99.5%,CAN 到帧率 λ 从 4000/s 塌到 149/s(瓶颈在从站不在主站,
+ 只看主站达成率会整条看漏)。
+ 观测默认 10Hz。逐拍环 / 原始窗口 / 每拍重建都在各自模块卡片里开关(默认关)。标签页切到后台会自动暂停。
+
+
+
+
+
刷新率 实测 = Δcycles/Δt;名义 = 1000/period_ms 仅作对照,两者差值就是软件调度开销
+
+
+
+
+
+
单拍分段 p50
—
work / send / rx µs
+
CAN 到帧率 λ
—/s
Δglobal_seq (u32)
+
+
+
从站 CPU 负载
—
load_perm · age —
+
+
CAN 病理仪表
—
撕裂 · RFD超时 · 满溢 · 拼接
+
本页请求耗时
—ms
本页→代理→native 最慢一个请求的往返(≠节拍耗时)
+
+
+ 判读:达成率 < 100% = 节拍被软件拖住;λ ÷ 实测Hz = 每拍到达几帧 CAN(当前 1ms 档实测 6 帧/拍)。
+ drop 不判好坏:它是段 1/段 3 那个"最近一帧"单格被未取先覆的次数,
+ 而单格每拍只能发布 1 帧;且自增点在 STATUS 过滤之前(ethercat_can_bridge.cpp:398
+ 对 :435),所以每拍到的 6 帧全算进来 → 恒等于 λ − 实测Hz,零独立信息量。
+ 这些帧并没有丢:STATUS 由段 2 按槽常驻上报,其余进段 4 覆盖窗口(实测 lost/OVF 均 0)。
+ drop 哪天才有权报警:让只走单格的帧(版本/节点应答)承担可靠投递时。
+ 真丢帧逐层看:① CAN 控制器 err/TEC/REC;② CAN 软件 RX 队列满(深度 16)的
+ rx_drop_count —— 只在 ESP32 /can/feedback 有,过程数据里没有;
+ ③ 段 4 覆盖窗口 主站侧按 seq 空洞算的 rx_batch.lost(step/2−1,
+ 发布值恒为偶数,按 step−1 会把每帧虚报成丢 1 个);④ 段 4 GD32 侧未取先覆
+ rx_batch.pending(= OVF,与 ③ 是两个独立仪表,正常应当同步增长);
+ ⑤ CAN 取帧撕裂 rx_torn;⑥ FIFO 满溢 overfull;⑦ 内容拼接 reject。
+ ③④ 是本页 10Hz 抽样看到的单拍值,只能当下界。密排 STATUS 不走段4,
+ 真丢帧仪表只盖长帧/win 路径。
+ 撕裂 / 重入是饱和计数(到 65535 停),封顶时只能报下界。
+
+
+
+
+
实时曲线 滚动 90s · 紫 = 实测 PDO Hz(左轴) · 青 = 单拍 p50 µs(右轴)
+
+
+
+ 开 max / 跳拍 可叠加显示;右轴 µs,跳拍独立缩放
+
+
+
+
+
+
+
总线与从站 /api/status · /api/native/health · state.head
+
+
窗口自适应:win_bases 是当前收缩出的接收窗口基址数,
+ 单槽刷新 = period_ms × win_bases.length;开机首圈先扫全表(6 拍)。
+
+
+
桥接计数 state.bridge · 桥全局,无按通道拆分 / 无 TEC·REC
+
+
过程数据里没有逐通道 TEC/REC —— 没有的就显示 —,
+ 不编 0 让"总线正常"和"没这个数据"长得一模一样。
+
+
+
+
+
收到的东西 ① 电机状态槽 state.motors[] · 48B STATUS 解码(DBC Motor_CAN_FD v0.74)
+
+
逐槽原始 hex(48B 全长,16B 一行、多列排)
+ —
+
+
「速度参考」= RT_Velocity,是内部斜坡参考值而非实测转速,
+ 不能拿去回答"三轴实际是否同步"。InitStatus 显示 — 表示这条链路没送
+ B32-35 —— 补 0 会把"无数据"显示成"初始化正常"。
+
+
+
收到的东西 ② 密排整轮 + 长帧事件 rxraw.parsed · 1.6.21 密排:段2整轮槽 + 段3长帧变更
+
+ 本拍新帧 / seq 空洞 / GD32 OVF —
+ 本页帧样本 —
+
+
+
+
+
+ 密排整轮槽(GD32 每拍原样拷出 count×56B:node ch online dlc seq + 48B STATUS)。
+ 末列「本拍新数据」:是 = seq 比上一拍大(差分命中,状态真变了);否 = seq 未变,固件本拍跳过填充。
+
+
按子码汇总(每 sub 一行的采样次数 / 最近载荷)
+
+
+
+ 密排整轮(1.6.21):接 N 个电机本拍就发 N 个状态,空槽不发送。
+ STATUS 全在上方状态槽解码;本表是同一拍的线级原样(seq / online / 前 8B 载荷),
+ 用来核对"差分是否真的跳过了未变电机"。
+ 长帧(CONFIG 24B / 版本应答):段 3「最近一帧」,内容变了才进上面的事件表
+ (主站按 ch/dlc/id/data 逐字节比对,相同不重复上报)。逐字节的真凭实据在下面第 ③ 区。
+ 本表看不到的还有三层 —— CAN 控制器 err/TEC/REC、GD32 软件 RX 队列满丢(深度 16,
+ 只在 ESP32 /can/feedback 有)、CAN 取帧撕裂 rx_torn。
+ 逐层对账跑 scripts/can_bridge_reconcile.py。
+
+
+
+
收到的东西 ③ 原始接收窗口 + 长帧 + 逐拍环 /api/motor/rxraw · state.xrx · /api/motor/dbg(全为只读缓存拷贝)
+
+
+
+
+
+
+
+
+
+ layout:dense=1.6.21 密排整轮 / win=1.6.15 定长覆盖窗口 / slim=变长 / old=旧布局,
+ 解析器必须按 layout 选,否则拿变长游标解定长窗口会整片错位。
+
+
xrx 长帧完整载荷(kv 里那行是截断的,这里不截)
+
—
+
+
+
+
+
逐拍环未开启。开启后实测每次约 160KB(后端 RING=2048 拍),
+ 只在需要看抖动分布时开,别常年挂着 —— 仪表本身也是负载。
+
+
+
+
接收区原始字节 —— dense:8B 整轮头 + count×56B 槽;win:44B/槽 覆盖窗口
+
—
+
+
+
+
+
+
+