docs(lab): 更正 PRAM 写归因为从站侧成本;lan9252_pm 加 pmHold 看门狗豁免

This commit is contained in:
cbb
2026-09-13 04:13:28 +08:00
parent 209a869f96
commit 9bb804ec35
6 changed files with 1914 additions and 1881 deletions
@@ -0,0 +1,31 @@
# 诊断结论(本次核心交付)
你的怀疑正确,确实冲突,且共两层:
1. **主站楔死看门狗无豁免(自动拉起的推手)**
`G:\Robot_v2\DCU\tools\ecat\ecat_native.c` 的 wedge_worker 后台线程:WKC 异常持续 5s 即判死,之后每 ~8s 无限重拉 INIT→OP,直到成功,全程无「用户手动休眠」豁免。日志实锤(`build_native\native_stdout.log`):
`t=176002.9s 判死(从站掉 OP) → 重拉失败(枚举不到从站) → t=176011.0s 重拉成功已进 OP`,与你任务管理器图上 ~20s 流量缺口完全吻合。
2. **GD32 固件进睡眠时没冻结 HBI 访问者(芯片睡不住的根因)**
`EscLan9252::pmDrive()` 写完 PMT_CTRL 进入 D1-D3 后,GD32 侧 SSC 主循环 `MainLoop()→ECAT_Main()` 每圈仍无条件 HBI 读 0x220(AL 事件寄存器),另有 250ms AL mask 守护、5s AL 心跳,全部无睡眠门控。按 1.6.24 实测「任何 HBI 访问(含读)都会唤醒芯片」,**芯片进入睡眠后毫秒级就被 GD32 自己打回 D0**。你看到的低谷不是「睡了多久」,而是被唤醒后 SM/FMMU 失效、主站重建连接所需的时间。
3. 次要发现:电源页 `enterMode` 已正确暂停轮询并写 sessionStorage(同标签页安全),但换新标签页打开会丢标记 → 自动状态读(HBI 读)直接把芯片唤醒,属潜在坑。
# 修复计划
## Phase A:PC 侧(立即生效,不动固件)
- **A1** `ecat_native.c`:新增 `g_pm_hold` 标志 + `POST /api/pm/hold {on:0|1}`;在三处跳过——pdo_worker 的楔死判定(~859-908 行)、wedge_worker 巡检(~2940-2990 行)、`/api/motor/state` 路由快路径(~2262-2300 行);hold 时重置检测计时避免恢复后误触发。`build_new.bat` 重编,`verify_wedge_fix.py` 回归。
- **A2** `lab\lan9252_pm.html`:enterMode 成功后发 `pm/hold on=1`;手动唤醒/NRST 流程开头先发 `on=0`(恢复主站自愈)再走 recoverEcatMaster;banner 文案同步。
- **A3**(小改)睡眠标记另写一份带时间戳的 localStorage,新标签页打开时据此跳过自动状态读。
- 效果:休眠期间主站不再自动拉回 OP;但芯片仍会被 GD32 自己唤醒,EtherCAT 保持断开直到你手动唤醒(行为=「我睡了就是睡了」,但低功耗目的未真正达成)。
## Phase B:GD32 固件根因修复(需 cbuild 构建 + 刷板)
- **B1** `G:\Robot_v2\DCU\DCU_APP\GD32E508_DCU_APP\comm\esc_lan9252.cpp/.h`:新增全局挂起标志;pmDrive op=1/2/3 成功进睡眠时置位,op=4 唤醒成功/nrstDrive/hardwareReset 时清除。
- **B2** 门控点:`sys\task_runner.cpp` 的 `MainLoop()`、`ethercatAleMaskGuard()`、`ethercatAlHeartbeat()`;PDI/Sync0 EXTI ISR 顶部查标志直接返回(ISR 内 HBI 读同样会唤醒芯片)。0xC5/0xC4 轮询不碰 ESC,无需改。
- **B3** 核对唤醒后 SSC 阴影状态一致性(现有页面唤醒路径一律 NRST+motor/start,NRST 会全复位芯片)。
- **B4** 按 cbuild+vcpkg AC6+ninja 链构建刷板,刷完 `/api/motor/start` 恢复 OP。
- 加上 B 后芯片才能真正保持 D1-D3 低功耗态。
## 验证
- 进睡眠后任务管理器流量掉 0 并**保持**,native 日志无 WEDGE 重拉;GD32 串口 `[ECAT][HB]` 心跳停止(B 后);间隔 2s/10s/30s 查询状态仍为睡眠态;手动唤醒后流量恢复进 OP。
- hold=0 时正常楔死自愈回归不受影响。
+3 -3
View File
@@ -43,7 +43,7 @@ ul{padding-left:22px}
<h1>EtherCAT 单拍耗时 345µs→124µs:改动原理与验证</h1>
<div class="meta">调查日期 2026-09-11 · 台子:i7-14700K + Intel I210(100 Mbps 全双工)+ DCU(GD32E508 + LAN9252)+ 3 台电机 · 源报告 <code>tools/ecat/REPORT_ecat_beat_tail_20260911.md</code> · 本页所有数字已对照源码与实机复测核验(<code>layout=dense</code>、rx p50=92.1µs、wkc 3/3、v1.6.22.0)</div>
<div class="kpi"><div><b>345 → 124µs</b><span>9/11 当天 0.5ms 档整拍 p50 实测(cyc_us.work)</span></div><div><b>310 → 92µs</b><span>主站等回包 rx p50 —— 变化都在这一段</span></div><div><b>124.6µs</b><span>9/8 同为 win 布局的实测单拍(CSV)—— 345µs 并非 win 固有成本</span></div><div><b>0.44µs/B</b><span>PRAM 写单价(DWT 实测)—— 600B≈265µs/拍,两布局共同成本</span></div></div>
<div class="kpi"><div><b>345 → 124µs</b><span>9/11 当天 0.5ms 档整拍 p50 实测(cyc_us.work)</span></div><div><b>310 → 92µs</b><span>主站等回包 rx p50 —— 变化都在这一段</span></div><div><b>124.6µs</b><span>9/8 同为 win 布局的实测单拍(CSV)—— 345µs 并非 win 固有成本</span></div><div><b>线速主导</b><span>主站 rx ≈ 帧在线上的往返(~1KB @100Mbit ≈ 80µs 量级);从站写 ESC 是另一段时间</span></div></div>
<div class="card"><h3>结论(2026-09-12 修订)</h3>
<ul>
@@ -54,7 +54,7 @@ ul{padding-left:22px}
<div class="card bad"><h3>重大更正(2026-09-12 复核):220µs 的归因被动摇,两个变量被混在一起</h3>
<p>用户质疑"5 个 STATUS 变 3 个,怎么会差 20 倍"——顺着查,发现两件原调查没做的事:</p>
<ul>
<li><b>机制核对(代码层)</b>:两版 buildInput 逐行对比,纯 CPU 差仅 <b>1~3µs</b>(旧:5×48B + 5×44B 无条件拷贝;新:27 槽选最新 + 影子比对 + 3×48B)。对 ESC 的 PRAM 写两版同为整块 600B(DWT 实测 0.44µs/B → ≈265µs/拍,<code>ethercat_pdo_size.h</code>),且 A/B/A 证明它对主站往返不可见——<b>"删掉覆盖窗 / 差分填充省出的 CPU"解释不了 229µs</b>。另:差分填充在本台工况下基本不命中(STATUS ~1.25 帧/拍/台,槽 seq 几乎每拍都变,重建路径照样走)。</li>
<li><b>机制核对(代码层)</b>:两版 buildInput 逐行对比,纯 CPU 差仅 <b>1~3µs</b>(旧:5×48B + 5×44B 无条件拷贝;新:27 槽选最新 + 影子比对 + 3×48B)。对 ESC 的 PRAM 写两版同为整块 600B(从站侧写 ESC 成本,与主站 rx 不是同一段时间),且 A/B/A 证明它对主站往返不可见——<b>"删掉覆盖窗 / 差分填充省出的 CPU"解释不了 229µs</b>。另:差分填充在本台工况下基本不命中(STATUS ~1.25 帧/拍/台,槽 seq 几乎每拍都变,重建路径照样走)。</li>
<li><b>反例数据(9/8 存量扫描)</b>:<code>lab/ecat_rate_sweep.csv</code>——win 布局、0.5ms 档 cyc_p50=<b>124.6µs</b>、达成 100%;1ms=126.9µs;0.125ms=119.5µs 且实测 7780Hz(单拍必须 ≤128µs 才可能)。与 dense 的 124µs 同水平。</li>
</ul>
<p><b>混淆的变量</b>(刷板台账 git 史 + 主站 git 史):9/8 板上镜像 <code>201832b5…</code>(win)→ 9/10 前已刷 <code>4e84a354…</code>(win 期 1.6.15→1.6.20 演进);主站 9/9 03:05 提交 <code>97fab23</code>(三布局自适应、发现窗基址 {0,7,14,20}→{0,5,10,15,20,22}、覆盖窗按 seq 归一)。<b>345µs 出现在这两个改动之后、9/8 之前</b>——原归因把回归记在 1.6.21 密排头上,但密排也可能只是"绕开"了回归(dense 下主站强制 n_bases=1,不轮转窗口)。</p>
@@ -103,7 +103,7 @@ ul{padding-left:22px}
<tr><th>段</th><th>0.5ms 实测 p50</th><th>从外到内的组成</th><th>出处 / 尚未拆开的部分</th></tr>
<tr><td>发送(蓝)</td><td>28.6µs</td><td>组帧 ≈0.2µs(组 LRW + 填输出区)→ <code>pcap_sendpacket</code> ≈28.4µs:NPF 栈把 1016B 交给 I210 驱动并提交发送</td><td><code>/api/motor/dbg</code> 拆段(sd_us 列)。驱动/DMA 内部无仪表;B 臂曾见发送 p99 348.9µs 的偶发阻塞(单窗样本,未立论)</td></tr>
<tr><td>线路(粉)</td><td>81.3µs</td><td>帧 1016B = 过程数据 984B(输出 384 + 输入 600)+ 以太网头 14 + EtherType 2 + 数据报头 10 + WKC 2 + FCS 4;8128bit ÷ 100Mbps;再加前导 8B + 帧间隙 12B ≈ +1.6µs;从站 ESC 硬件转发 µs 级</td><td>由帧构成计算,两布局逐字节同长——结构上不可能解释 220µs 差</td></tr>
<tr><td>从站准备(红)· win</td><td>≈229µs</td><td>1.6.15 每拍在 PDI 里:① 按 win_base 轮转准备 27 槽表 ② 段4 覆盖窗 5×44B 拷贝(fillRxWindow/RxWinSlot)③ MotorState 重读 + 逐槽 48B 拷贝 ④ 600B 经 HBI/PRAM 推入 ESC RAM</td><td>实测差值 310−81.3。但 ①②③ 的纯 CPU 差按代码对比仅 1~3µs,④ 与整块 600B PRAM 写(≈265µs/拍)两布局同在且 A/B/A 证明不影响往返——229µs 的载体机制未闭环,且 9/8 同布局实测 124.6µs 表明"345µs=win 固有成本"的前提不成立(见"重大更正"卡)</td></tr>
<tr><td>从站准备(红)· win</td><td>≈229µs</td><td>1.6.15 每拍在 PDI 里:① 按 win_base 轮转准备 27 槽表 ② 段4 覆盖窗 5×44B 拷贝 ③ MotorState 重读 + 逐槽 48B 拷贝 ④ 整块 600B 写入 ESC</td><td>实测差值 310−81.3。①②③ 纯 CPU 差仅 1~3µs;④ 是从站写 ESC(两布局同在、不影响主站往返)——229µs 载体未闭环;9/8 同布局 124.6µs 表明"345µs=win 固有"不成立</td></tr>
<tr><td>从站准备(红)· dense</td><td>≈10µs</td><td>差分填充命中:节点集合与各 seq 未变 → 整段复用影子,①②③全免;只剩 count 个槽的更新推送 + ESC 取数</td><td>rx 92.1 − 81.3。220µs 的差全部消失在这里</td></tr>
</table>
<p class="meta">口径备注:win 时代插桩拆段亦记录到 <code>pcap_sendpacket</code> 181.1µs、其后续中位 1 次 poll 即得帧(§四表)——该口径下发送调用内部吸收了部分等待,<b>发送/等回包两段的分界随仪表口径浮动</b>;但整拍总量(345)与"扣除线路后的从站侧总延迟"(≈229)不受此影响。另 1ms 档每拍 next_ex 轮询中位 79 次(轮询间隔 ~1.2µs),0.5ms 档为紧轮询。</p>
+1 -1
View File
@@ -1 +1 @@
dda92d11876f059d27390232e88b5c9fa7c0eb57e442365f5247d4acdefb9b20
c6827f84e9b8fa07288d0fc2081d0efc48a6ef2c5b8124b85a406f3a7a02042a
+30 -1
View File
@@ -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");
+7 -7
View File
@@ -252,7 +252,7 @@ footer code{font-size:11px}
<div class="pills">
<span class="pill">真源 <b>ethercat_can_bridge.h</b></span>
<span class="pill">尺寸 <b>ethercat_pdo_size.h</b></span>
<span class="pill">PRAM <b>0.441 µs/B</b></span>
<span class="pill">PRAM 写 <b>从站侧</b>(≠ 主站 rx)</span>
<span class="pill">魔数 <b>@35=0xA5</b></span>
<span class="pill">电机槽 <b>≤9 × 56 B</b></span>
<span class="pill">稀疏写 <b>CONFIG=1</b></span>
@@ -437,20 +437,20 @@ for (i = 0; i &lt; count; i++) {
<div class="sec-head">
<span class="sec-no">4</span>
<h2>每拍成本与 SM 预算</h2>
<span class="tagline">PRAM ≈0.441 µs/B · 稀疏写 1.6.22+</span>
<span class="tagline">从站写 ESC 成本 · 稀疏写 1.6.22+(不是主站 rx)</span>
</div>
<div class="cost-grid">
<div class="cost-card c-cyan">
<div class="lbl">输入写侧 · 3 电机稀疏</div>
<div class="val">116</div>
<div class="unit">µs/拍 · 264 B×0.441 · 11.6% @1ms</div>
<div class="val">—</div>
<div class="unit">从站写 ESC · 按实占字节估(≠ 主站 rx)</div>
<div class="bar"><i style="width:11.6%"></i></div>
</div>
<div class="cost-card c-blue">
<div class="lbl">输入写侧 · 整包基线</div>
<div class="val">265</div>
<div class="unit">µs/拍 · 600×0.441 · 26.5% @1ms</div>
<div class="val">—</div>
<div class="unit">从站写 ESC · 600B 整包(≠ 主站 rx)</div>
<div class="bar"><i style="width:26.5%"></i></div>
</div>
<div class="cost-card c-amber">
@@ -524,7 +524,7 @@ count=9 → 整包 600 B(两段相接,回退)</code></pre>
布局真源 <code>comm/ethercat_can_bridge.h</code>(1.6.21)· 尺寸 <code>comm/ethercat_pdo_size.h</code> ·
固件版本 <code>config.h APP_FW_VERSION = 1.6.24</code> ·
稀疏写 <code>ethercat_pdo.cpp sparseEscWritePlan</code> + <code>ecatappl.c PDO_InputMapping</code> ·
成本系数 <code>0.441 µs/B</code> · 布局魔数 <code>kEcatMotLayoutMagic = 0xA5</code>
成本说明:从站写 ESC 与主站 rx 是两段时间 · 布局魔数 <code>kEcatMotLayoutMagic = 0xA5</code>
</footer>
</div>
+1842 -1869
View File
File diff suppressed because it is too large Load Diff