Files

903 lines
36 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>DCU EtherCAT PDO · 字节布局 1.6.21 · 固件 1.6.24</title>
<style>
:root{
--bg:#070b10; --panel:#101820; --panel2:#141e28;
--line:#1c2834; --line2:#2a3a4a;
--ink:#d8e4f0; --dim:#6d8299; --dim2:#4a5d72;
--cyan:#00d4a0; --blue:#4c9aff; --amber:#f0b429; --violet:#a78bfa;
--rose:#ff5c7a; --slate:#8b9cb0;
--mono:"Cascadia Code","Cascadia Mono","Consolas",monospace;
--sans:"Segoe UI","Microsoft YaHei",system-ui,sans-serif;
}
*{box-sizing:border-box}
body{
margin:0;padding:0 0 72px;
background:
radial-gradient(1100px 500px at 8% -8%, rgba(0,212,160,.07), transparent 50%),
radial-gradient(800px 420px at 95% 0%, rgba(76,154,255,.05), transparent 45%),
var(--bg);
color:var(--ink);
font:14px/1.7 var(--sans);
-webkit-font-smoothing:antialiased;
}
.wrap{max-width:1120px;margin:0 auto;padding:0 28px}
/* header */
header{
border-bottom:1px solid var(--line);
background:rgba(7,11,16,.92);
backdrop-filter:blur(10px);
position:sticky;top:0;z-index:40;
}
.hd-in{
max-width:1120px;margin:0 auto;padding:14px 28px;
display:flex;align-items:center;gap:20px;flex-wrap:wrap;
}
.brand{display:flex;align-items:center;gap:12px}
.brand h1{margin:0;font-size:18px;font-weight:650;letter-spacing:.2px}
.ver{
font:650 11px/1 var(--mono);color:#07120f;background:var(--cyan);
padding:5px 8px;border-radius:5px;
}
.sub{color:var(--dim);font-size:12.5px}
.stats{display:flex;gap:8px;margin-left:auto;flex-wrap:wrap}
.stat{
padding:6px 12px;border:1px solid var(--line);border-radius:7px;
background:var(--panel);min-width:86px;
}
.stat b{display:block;font:650 14px/1.3 var(--mono)}
.stat span{display:block;font-size:10.5px;color:var(--dim);letter-spacing:.3px}
/* sections */
section{padding:40px 0 4px}
.sec-head{
display:flex;align-items:center;gap:12px;
margin:0 0 18px;padding-bottom:10px;border-bottom:1px solid var(--line);
}
.sec-no{
font:700 11px/1 var(--mono);color:#07120f;background:var(--cyan);
width:24px;height:24px;border-radius:50%;display:grid;place-items:center;
}
.sec-head h2{margin:0;font-size:17px;font-weight:650}
.sec-head .tagline{margin-left:auto;color:var(--dim);font:12px var(--mono)}
/* cards */
.card{
background:var(--panel);border:1px solid var(--line);border-radius:12px;
overflow:hidden;margin-bottom:16px;
}
.card-hd{
display:flex;align-items:center;gap:10px;flex-wrap:wrap;
padding:12px 16px;border-bottom:1px solid var(--line);background:var(--panel2);
}
.card-hd h3{margin:0;font-size:13.5px;font-weight:650}
.card-hd .meta{margin-left:auto;color:var(--dim);font:11.5px var(--mono)}
.card-bd{padding:16px}
/* callout */
.callout{
border:1px solid var(--line);border-left:3px solid var(--cyan);
border-radius:10px;background:var(--panel);
padding:14px 16px;margin:0 0 16px;font-size:13.5px;
}
.callout.warn{border-left-color:var(--amber)}
.callout.danger{border-left-color:var(--rose)}
.callout h4{margin:0 0 8px;font-size:13px;font-weight:650;color:var(--cyan)}
.callout.warn h4{color:var(--amber)}
.callout.danger h4{color:var(--rose)}
.callout ul{margin:8px 0 0;padding-left:20px}
.callout li{margin:5px 0}
.callout p{margin:0 0 8px}
.callout p:last-child{margin:0}
/* pills / legend */
.pills{display:flex;gap:8px;flex-wrap:wrap;margin:22px 0 18px}
.pill{
font:11.5px var(--mono);color:var(--dim);
border:1px solid var(--line);border-radius:99px;
padding:5px 11px;background:var(--panel);
}
.pill b{color:var(--ink);font-weight:650}
.legend{
display:flex;flex-wrap:wrap;gap:10px 18px;
margin:0 0 18px;font-size:12px;color:var(--dim);
}
.legend i{
display:inline-block;width:11px;height:11px;border-radius:3px;
margin-right:6px;vertical-align:-1px;
}
/* svg maps */
.map-wrap{margin:0 0 8px}
.map-wrap + .map-wrap{margin-top:14px}
.map-wrap svg{display:block;width:100%;height:auto}
.seg-label{
font:600 11px var(--mono);color:var(--dim);
margin:18px 0 8px;letter-spacing:.2px;
}
.seg-label:first-child{margin-top:0}
/* field bar (HTML, 防 SVG 叠压) */
.fbar{
display:flex;gap:2px;height:52px;
border:1px solid var(--line);border-radius:6px;overflow:hidden;
background:#0a1218;
}
.fcell{
position:relative;display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:2px;
min-width:0;padding:4px 2px;overflow:hidden;
border-right:1px solid rgba(0,0,0,.45);
}
.fcell:last-child{border-right:0}
.fcell .fn{
font:650 11px/1.2 var(--mono);color:var(--ink);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;
}
.fcell .fo{
font:10px/1.2 var(--mono);color:var(--dim);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;
}
.fcell.narrow .fn{font-size:10px}
.fcell.pad .fn,.fcell.pad .fo{color:var(--dim2)}
.ruler{
display:flex;gap:2px;margin-top:4px;
font:10px var(--mono);color:var(--dim2);
}
.ruler span{min-width:0;overflow:hidden;white-space:nowrap}
/* tables in cards */
.tbl-card{margin-top:0}
table{border-collapse:collapse;width:100%;font-size:13px}
th,td{
border-bottom:1px solid var(--line);
padding:9px 12px;text-align:left;vertical-align:top;
}
th{
color:var(--dim);font-weight:600;font-size:11px;
letter-spacing:.4px;text-transform:uppercase;
background:var(--panel2);
}
td.n,th.n{text-align:right;font-family:var(--mono);white-space:nowrap}
td.mono{font-family:var(--mono);white-space:nowrap;color:var(--dim)}
tbody tr:hover td{background:rgba(0,212,160,.035)}
tbody tr:last-child td{border-bottom:0}
tfoot td{font-weight:650;background:var(--panel2);border-bottom:0}
.dot{
display:inline-block;width:9px;height:9px;border-radius:2px;
margin-right:8px;vertical-align:0;
}
.chip{
display:inline-block;padding:2px 8px;border-radius:99px;
font:600 11px/1.5 var(--mono);
}
.chip.fix{background:rgba(255,92,122,.12);color:var(--rose)}
.chip.adp{background:rgba(0,212,160,.12);color:var(--cyan)}
.chip.hdr{background:rgba(240,180,41,.12);color:var(--amber)}
.chip.gone{background:rgba(167,139,250,.12);color:var(--violet)}
.chip.note{background:rgba(139,156,176,.12);color:var(--slate)}
code{
font-family:var(--mono);font-size:12px;
background:#0a1218;padding:1px 6px;border-radius:3px;border:1px solid var(--line);
}
pre{
margin:10px 0 0;padding:12px 14px;
background:#0a1218;border:1px solid var(--line);border-radius:8px;
overflow-x:auto;font:12px/1.6 var(--mono);color:var(--ink);
}
pre code{background:none;border:0;padding:0}
/* cost */
.cost-grid{
display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:0 0 16px;
}
@media(max-width:800px){.cost-grid{grid-template-columns:1fr}}
.cost-card{
background:var(--panel);border:1px solid var(--line);border-radius:10px;
padding:14px 16px;
}
.cost-card .lbl{font-size:11px;color:var(--dim);letter-spacing:.4px;text-transform:uppercase}
.cost-card .val{font:650 32px/1.2 var(--mono);margin:6px 0 2px}
.cost-card .unit{font-size:12.5px;color:var(--dim)}
.cost-card .bar{height:5px;border-radius:3px;background:var(--line);margin-top:12px;overflow:hidden}
.cost-card .bar i{display:block;height:100%;border-radius:3px}
.cost-card.c-cyan .val{color:var(--cyan)}
.cost-card.c-cyan .bar i{background:var(--cyan)}
.cost-card.c-blue .val{color:var(--blue)}
.cost-card.c-blue .bar i{background:var(--blue)}
.cost-card.c-amber .val{color:var(--amber)}
.cost-card.c-amber .bar i{background:var(--amber)}
footer{
margin-top:48px;padding-top:18px;border-top:1px solid var(--line);
color:var(--dim);font-size:12px;
}
footer code{font-size:11px}
@media(max-width:720px){
.wrap,.hd-in{padding-left:16px;padding-right:16px}
.stats{width:100%;margin-left:0}
.fbar{height:46px}
.fcell .fn{font-size:10px}
}
</style>
</head>
<body>
<header>
<div class="hd-in">
<div class="brand">
<h1>DCU EtherCAT PDO 布局</h1>
<span class="ver">布局 1.6.21</span>
<span class="sub">固件 1.6.24 · 字段级 · 稀疏写</span>
</div>
<div class="stats">
<div class="stat"><b>384 B</b><span>输出 SM2</span></div>
<div class="stat"><b>600 B</b><span>输入 SM3</span></div>
<div class="stat"><b>96–600 B</b><span>稀疏写实占</span></div>
<div class="stat"><b>40 B</b><span>DPRAM 余量</span></div>
</div>
</div>
</header>
<div class="wrap">
<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>从站侧</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>
</div>
<div class="callout">
<h4>布局 1.6.21 · 固件 1.6.24 · 写侧 1.6.22 起稀疏</h4>
<ul>
<li><b>字节布局仍是 1.6.21</b>:段 2 密排、删段 4、魔数 <code>@35=0xA5</code>;out 384 + in 600 不变,SII/EEPROM 无需重刷。</li>
<li><b>1.6.22 稀疏写 ESC</b>(<code>CONFIG_ECAT_SPARSE_ESC_WRITE=1</code>):按 <code>count</code> 只写「段1+段2有效区」与「段3+段5」,跳过空槽区。主站解码不变,仍只按 count 迭代。</li>
<li><b>1.6.23/1.6.24</b> 为 LAN9252 电源管理诊断(pmDrive D1/D2/D3),不影响 PDO 布局。</li>
<li>回退整包 600B 的条件:开关置 0 / magic≠0xA5 / count 越界 / count 撑满(9 电机两段相接)。</li>
</ul>
</div>
<div class="legend">
<span><i style="background:var(--blue)"></i>桥状态 / 快照</span>
<span><i style="background:var(--amber)"></i>段头 / 元数据</span>
<span><i style="background:var(--cyan)"></i>电机密排槽</span>
<span><i style="background:var(--violet)"></i>批量发送区</span>
<span><i style="background:var(--rose)"></i>诊断</span>
<span><i style="background:var(--slate)"></i>保留 / 填充</span>
</div>
<!-- ═══ 输入 PDO ═══ -->
<section id="in">
<div class="sec-head">
<span class="sec-no">1</span>
<h2>输入 PDO · 600 B · 从站 → 主站</h2>
<span class="tagline">稀疏写 96–600 B · 整包 ≈265 µs · 3 电机 ≈116 µs</span>
</div>
<div class="card">
<div class="card-hd">
<h3>段总览</h3>
<span class="meta">SM3 · 0x1880–0x1F67 · 3-buffer · 写侧稀疏</span>
</div>
<div class="card-bd">
<div class="map-wrap" id="in-overview"></div>
</div>
</div>
<div class="card">
<div class="card-hd">
<h3>段内字段</h3>
<span class="meta">每段独占全宽 · 宽度 ∝ 字节数</span>
</div>
<div class="card-bd" id="in-fields"></div>
</div>
<div class="card tbl-card">
<div class="card-hd"><h3>字段明细</h3></div>
<table>
<thead>
<tr>
<th>段 / 字段</th><th class="n">偏移</th><th class="n">宽</th>
<th>类型</th><th>内容</th><th>属性</th>
</tr>
</thead>
<tbody id="in-tbody"></tbody>
<tfoot>
<tr>
<td colspan="2">合计</td>
<td class="n">600</td>
<td></td>
<td>逻辑总长;写侧按 count 稀疏(见下)</td>
<td class="n">96–600 B</td>
</tr>
</tfoot>
</table>
</div>
</section>
<!-- ═══ 输出 PDO ═══ -->
<section id="out">
<div class="sec-head">
<span class="sec-no">2</span>
<h2>输出 PDO · 384 B · 主站 → 从站</h2>
<span class="tagline">pramRead(384) · 96 DWORD · ≈169 µs 折算</span>
</div>
<div class="card">
<div class="card-hd">
<h3>段总览</h3>
<span class="meta">SM2 · 0x1400–0x187F · WIN@8 忽略</span>
</div>
<div class="card-bd">
<div class="map-wrap" id="out-overview"></div>
</div>
</div>
<div class="card">
<div class="card-hd">
<h3>段内字段</h3>
<span class="meta">每段独占全宽 · 宽度 ∝ 字节数</span>
</div>
<div class="card-bd" id="out-fields"></div>
</div>
<div class="card tbl-card">
<div class="card-hd"><h3>字段明细</h3></div>
<table>
<thead>
<tr>
<th>段 / 字段</th><th class="n">偏移</th><th class="n">宽</th>
<th>类型</th><th>内容</th><th>属性</th>
</tr>
</thead>
<tbody id="out-tbody"></tbody>
<tfoot>
<tr>
<td colspan="2">合计</td>
<td class="n">384</td>
<td></td>
<td>每拍一次 <code>pramRead(384)</code>(未实测)</td>
<td class="n">≈169 µs</td>
</tr>
</tfoot>
</table>
</div>
</section>
<!-- ═══ 电机槽 ═══ -->
<section id="slot">
<div class="sec-head">
<span class="sec-no">3</span>
<h2>电机槽内部 · 56 B × ≤9</h2>
<span class="tagline">PDO 地址 = 40 + i×56 · node_id 升序</span>
</div>
<div class="card">
<div class="card-hd">
<h3>密排槽区 @40..543</h3>
<span class="meta">空节点不占位 · count 决定有效槽数</span>
</div>
<div class="card-bd">
<div class="map-wrap" id="slot-strip"></div>
<div class="seg-label">单槽 56 B 字段</div>
<div id="slot-fields"></div>
</div>
</div>
<div class="card">
<div class="card-hd"><h3>主站解码</h3></div>
<div class="card-bd">
<pre><code>count = in[32]; // 0..9
magic = in[35]; // 必须 0xA5
for (i = 0; i &lt; count; i++) {
p = in + 40 + i*56;
node = p[0]; ch = p[1]; online = p[2];
seq = le32(p+4);
status = p+8; // 48 B
}</code></pre>
<p style="margin:14px 0 0;color:var(--dim);font-size:13px">
同号多通道取 <b style="color:var(--ink)">seq 最新</b>的一条;写侧 seqlock 奇数(写入中)的槽本拍省略、下拍自愈。
节点集合与各 seq/online 未变时整段复用影子区(CPU 差分),<b style="color:var(--ink)">PRAM 写侧仍按 count 决定字节数</b>。
<b style="color:var(--amber)">1.6.22+ 稀疏写</b>:ESC 中 <code>40+count×56 .. 543</code> 为上次残留,
主站<b style="color:var(--ink)">只许按 count 迭代</b>,不得读空洞区。
</p>
</div>
</div>
<div class="card tbl-card">
<div class="card-hd"><h3>单槽字段明细</h3></div>
<table>
<thead>
<tr><th>字段</th><th class="n">槽内偏移</th><th class="n">宽</th><th>类型</th><th>内容</th></tr>
</thead>
<tbody id="slot-tbody"></tbody>
<tfoot>
<tr>
<td colspan="2">单槽合计</td><td class="n">56</td>
<td colspan="2">8 B 头 + 48 B STATUS</td>
</tr>
</tfoot>
</table>
</div>
</section>
<!-- ═══ 成本 ═══ -->
<section id="cost">
<div class="sec-head">
<span class="sec-no">4</span>
<h2>每拍成本与 SM 预算</h2>
<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">—</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">—</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">
<div class="lbl">输出读侧 · 探针命中</div>
<div class="val">12</div>
<div class="unit">B 探针 · 无沿时跳过 384B 全量读</div>
<div class="bar"><i style="width:5%"></i></div>
</div>
</div>
<div class="callout">
<h4>1.6.22 稀疏写 ESC · 写侧字节随 count 缩</h4>
<p><code>CONFIG_ECAT_SPARSE_ESC_WRITE=1</code>(默认开)。<code>HW_EscWriteIsr</code> 只写两段:</p>
<pre><code>段A = [0 .. 40+count×56) // 段1 桥状态 + 段2 头 + 有效密排槽
段B = [544 .. 600) // 段3 长帧 + 段5 诊断 + 保留
跳过 = [40+count×56 .. 544) // 空槽区(ESC 中为 SM 缓冲上次残留)
count=0 → 96 B ≈ 42 µs
count=3 → 264 B ≈ 116 µs
count=8 → 544 B ≈ 240 µs
count=9 → 整包 600 B(两段相接,回退)</code></pre>
<p style="margin-top:10px;color:var(--dim);font-size:12.5px">
APPL 仍整填 <code>aPdInputData</code>(对齐 SDO 0x6000);省的是 PRAM 搬运时间,不是 RAM 拷贝。
主站必须继续只按 <code>count</code> 迭代——空洞区内容是上次残留,不可读。
</p>
</div>
<div class="callout warn">
<h4>实测锚点:从站自报负载</h4>
<p><b>1 ms 档 49.2% → 0.5 ms 档 81.4% → 0.25 ms 档 98.7%</b>,同时 λ 从 6000 帧/s 塌到 317 帧/s —— 饱和发生在从站,不在主站的 Npcap 或钉核上。</p>
<p style="color:var(--dim);font-size:12.5px">
上表为 1.6.21 整包基线。1.6.22 稀疏写开启后写侧字节随 count 缩,同工况 load_perm 应显著下降;
验收看 0x2000 sub10(EscWrite 周期)与 load_perm,同工况开关各测一轮。
</p>
</div>
<div class="card">
<div class="card-hd">
<h3>DPRAM 预算 · out + in ≤ 1024 B</h3>
<span class="meta">LAN9252 · 过程区 0x1400–0x1FFF · 3-buffer</span>
</div>
<div class="card-bd">
<div class="map-wrap" id="dpram-map"></div>
<p style="margin:14px 0 0;color:var(--dim);font-size:12.5px">
邮箱 SM0/SM1 各 256 B 固定占 0x1000–0x13FF;SM2/SM3 均为 3-buffer(control 0x64/0x20,bit1=0),实占 (out+in)×3。
要再大只有两条路:邮箱压小(上限约 1280 B,代价 SDO/FoE 变慢)或改 1-buffer(上限 3072 B,代价失去三缓冲并发保护)。
</p>
</div>
</div>
</section>
<!-- ═══ 段4 删除 ═══ -->
<section id="seg4">
<div class="sec-head">
<span class="sec-no">5</span>
<h2>段 4 历史覆盖窗口 · 已删除</h2>
<span class="tagline">1.6.15 引入 → 1.6.21 删除</span>
</div>
<div class="callout danger">
<h4>为什么删</h4>
<p>1.6.15 曾把批量接收区做成「定长覆盖窗口」:5 个固定格子(每格 44 B = 12 B 头 + 32 B 数据)+ 写游标绕圈。它是「最近 5 帧」而不是队列 —— 第 6 帧盖掉第 1 帧,未取走的计入 <code>OVF</code>。</p>
<ul>
<li>覆盖窗口反复重发最近几条非 STATUS 帧,属于「历史帧 / 重复帧」,对电机状态视图是噪声;</li>
<li>用户实测病理:接 3 个电机,网页却看到 5 个状态帧 —— 旧段 2 固定 5 槽里空槽 <code>node_id=0</code> 也占位,主站不滤空槽就多报。</li>
</ul>
<p style="margin-top:10px">本版语义:<b>每一拍只携带「总线上已识别电机的完整一轮状态」</b>。版本/CONFIG 仍走段 1/段 3「最近一帧」快照。对象字典 sub21 恒 0;<code>596..599</code> 保留空洞。</p>
</div>
</section>
<footer>
布局真源 <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> ·
成本说明:从站写 ESC 与主站 rx 是两段时间 · 布局魔数 <code>kEcatMotLayoutMagic = 0xA5</code>
</footer>
</div>
<script>
const C = {
blue:'#4c9aff', amber:'#f0b429', cyan:'#00d4a0',
violet:'#a78bfa', rose:'#ff5c7a', slate:'#8b9cb0',
dim:'#6d8299', line:'#1c2834', ink:'#d8e4f0',
};
/* ── 段定义(总览用)── */
const IN_SEGS = [
{name:'段1 桥状态', off:0, len:32, color:C.blue},
{name:'段2 头', off:32, len:8, color:C.amber},
{name:'段2 密排槽', off:40, len:504, color:C.cyan},
{name:'段3 长帧', off:544, len:40, color:C.blue},
{name:'段5 诊断', off:584, len:12, color:C.rose},
{name:'保留', off:596, len:4, color:C.slate},
];
const OUT_SEGS = [
{name:'段1 单帧发送', off:0, len:48, color:C.blue},
{name:'段2 批量发送', off:48, len:336, color:C.violet},
];
/* ── 字段(按段分组)── */
const IN_GROUPS = [
{title:'段 1 · 桥状态 0..31 · 32 B', total:32, fields:[
{off:0,len:1,name:'ack',color:C.blue,type:'u8',txt:'回显已执行的发送序号',attr:'固定'},
{off:1,len:1,name:'ch',color:C.blue,type:'u8',txt:'该帧来自哪个通道 0/1/2',attr:'固定'},
{off:2,len:1,name:'dlc',color:C.blue,type:'u8',txt:'原始 DLC(截断前)',attr:'固定'},
{off:3,len:1,name:'flags',color:C.blue,type:'u8',txt:'bit0=有过接收;bit1=截断到 8B',attr:'固定'},
{off:4,len:4,name:'id',color:C.blue,type:'u32le',txt:'最近一帧 CAN ID',attr:'固定'},
{off:8,len:8,name:'data',color:C.blue,type:'u8[8]',txt:'短快照(长帧见段 3)',attr:'固定'},
{off:16,len:4,name:'rx_count',color:C.blue,type:'u32le',txt:'累计接收帧数',attr:'固定'},
{off:20,len:1,name:'txres',color:C.blue,type:'u8',txt:'最近发送结果(0=OK)',attr:'固定'},
{off:21,len:1,name:'backlog',color:C.blue,type:'u8',txt:'该通道软件 TX 队列积压',attr:'固定'},
{off:22,len:2,name:'txcnt',color:C.blue,type:'u16le',txt:'累计发送受理帧数',attr:'固定'},
{off:24,len:2,name:'drop',color:C.blue,type:'u16le',txt:'快照「未取先覆」次数',attr:'固定'},
{off:26,len:6,name:'pad',color:C.slate,type:'pad',txt:'段 1 尾部填充',attr:'保留'},
]},
{title:'段 2 · 头 32..39 · 8 B', total:8, fields:[
{off:32,len:1,name:'count',color:C.amber,type:'u8',txt:'本拍密排电机数 0..9',attr:'头部'},
{off:33,len:1,name:'active',color:C.amber,type:'u8',txt:'在线电机数(可与 count 不同)',attr:'头部'},
{off:34,len:1,name:'boot',color:C.amber,type:'u8',txt:'1=近 3s 收到过 0x103 BOOT',attr:'头部'},
{off:35,len:1,name:'magic',color:C.amber,type:'u8',txt:'恒 0xA5:密排标识',attr:'头部'},
{off:36,len:4,name:'gseq',color:C.amber,type:'u32le',txt:'状态表全局刷新序号',attr:'头部'},
]},
{title:'段 2 · 密排槽 40..543 · 最多 9×56 B', total:504, fields:[
{off:40,len:504,name:'slots[9]',color:C.cyan,type:'9×56B',txt:'密排:每槽 node/ch/online/dlc/seq + 48B STATUS;空节点不占位',attr:'密排'},
]},
{title:'段 3 · 长帧快照 544..583 · 40 B', total:40, fields:[
{off:544,len:1,name:'ch',color:C.blue,type:'u8',txt:'通道',attr:'固定'},
{off:545,len:1,name:'dlc',color:C.blue,type:'u8',txt:'原始 DLC(可达 32)',attr:'固定'},
{off:546,len:1,name:'flags',color:C.blue,type:'u8',txt:'bit0=有效',attr:'固定'},
{off:547,len:1,name:'rsvd',color:C.slate,type:'u8',txt:'保留',attr:'保留'},
{off:548,len:4,name:'id',color:C.blue,type:'u32le',txt:'CAN ID',attr:'固定'},
{off:552,len:32,name:'data',color:C.blue,type:'u8[32]',txt:'完整 payload(CONFIG / 版本帧)',attr:'固定'},
]},
{title:'段 5 · 诊断 584..595 · 12 B', total:12, fields:[
{off:584,len:2,name:'load',color:C.rose,type:'u16le',txt:'负载率 0.1%(1000=100%)',attr:'固定'},
{off:586,len:2,name:'torn',color:C.rose,type:'u16le',txt:'CAN 取帧撕裂丢帧数',attr:'固定'},
{off:588,len:2,name:'seq',color:C.rose,type:'u16le',txt:'诊断区序号,1s 窗口 +1',attr:'固定'},
{off:590,len:2,name:'RFO0',color:C.rose,type:'u16le',txt:'CAN RX FIFO 满溢计数',attr:'固定'},
{off:592,len:2,name:'relto',color:C.rose,type:'u16le',txt:'RFD0 释放握手超时计数',attr:'固定'},
{off:594,len:2,name:'reject',color:C.rose,type:'u16le',txt:'内容级拼接帧命中计数',attr:'固定'},
]},
{title:'保留 596..599 · 4 B', total:4, fields:[
{off:596,len:4,name:'—',color:C.slate,type:'pad',txt:'原段 4 空洞',attr:'保留'},
]},
];
const OUT_GROUPS = [
{title:'段 1 · 单帧发送 0..47 · 48 B', total:48, fields:[
{off:0,len:1,name:'seq',color:C.blue,type:'u8',txt:'变化沿触发一次发送',attr:'固定'},
{off:1,len:1,name:'ch',color:C.blue,type:'u8',txt:'目标通道 0/1/2',attr:'固定'},
{off:2,len:1,name:'dlc',color:C.blue,type:'u8',txt:'0..32;0=只发批量',attr:'固定'},
{off:3,len:1,name:'flags',color:C.blue,type:'u8',txt:'bit0=1 请求清零统计',attr:'固定'},
{off:4,len:4,name:'id',color:C.blue,type:'u32le',txt:'CAN ID(29 位扩展帧)',attr:'固定'},
{off:8,len:1,name:'win',color:C.slate,type:'u8',txt:'1.6.21 起忽略',attr:'忽略'},
{off:9,len:7,name:'pad',color:C.slate,type:'pad',txt:'对齐到 16B 边界',attr:'保留'},
{off:16,len:32,name:'data',color:C.blue,type:'u8[32]',txt:'一帧电机 CMD 数据区',attr:'固定'},
]},
{title:'段 2 · 批量发送 48..383 · 336 B', total:336, fields:[
{off:48,len:1,name:'count',color:C.violet,type:'u8',txt:'本次要发的帧数',attr:'头部'},
{off:49,len:1,name:'flags',color:C.violet,type:'u8',txt:'bit0=1 遇错即停',attr:'头部'},
{off:50,len:2,name:'rsvd',color:C.slate,type:'pad',txt:'帧数组 8B 对齐',attr:'保留'},
{off:52,len:332,name:'items[]',color:C.violet,type:'变长',txt:'每项 8B 头 + dlc 数据(CANFD ≤64B/帧)',attr:'批量'},
]},
];
const SLOT_FIELDS = [
{off:0,len:1,name:'node',color:C.cyan,type:'u8',txt:'节点号 1..9(密排下不会出现 0)',attr:'头部'},
{off:1,len:1,name:'ch',color:C.cyan,type:'u8',txt:'CAN 通道 0/1/2',attr:'头部'},
{off:2,len:1,name:'online',color:C.cyan,type:'u8',txt:'1=在线',attr:'头部'},
{off:3,len:1,name:'dlc',color:C.cyan,type:'u8',txt:'最后一帧 STATUS 的 DLC(正常 48)',attr:'头部'},
{off:4,len:4,name:'seq',color:C.amber,type:'u32le',txt:'STATUS 刷新序号',attr:'头部'},
{off:8,len:48,name:'status',color:C.blue,type:'u8[48]',txt:'原始 STATUS payload',attr:'固定'},
];
/* ── SVG helpers ── */
function svgEl(tag, attrs, text) {
const e = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (const k in attrs) e.setAttribute(k, attrs[k]);
if (text != null) e.textContent = text;
return e;
}
/* 段总览:只画大块,标签宽敞 */
function drawOverview(container, segs, total) {
const W = 1000, pad = 6, barY = 28, barH = 56, H = barY + barH + 28;
const inner = W - pad * 2;
const svg = svgEl('svg', { viewBox:`0 0 ${W} ${H}`, role:'img' });
const x0 = b => pad + (b / total) * inner;
const xw = n => (n / total) * inner;
// ruler
const tick = total >= 400 ? 100 : 50;
for (let t = 0; t <= total; t += tick) {
const x = x0(t);
svg.appendChild(svgEl('line', { x1:x,y1:16,x2:x,y2:24, stroke:C.line,'stroke-width':1 }));
svg.appendChild(svgEl('text', {
x, y:12, 'text-anchor': t===0 ? 'start' : t===total ? 'end' : 'middle',
fill:C.dim, 'font-size':10, 'font-family':'Cascadia Code,Consolas,monospace'
}, String(t)));
}
svg.appendChild(svgEl('line', { x1:pad,y1:24,x2:W-pad,y2:24, stroke:C.line,'stroke-width':1 }));
segs.forEach(s => {
const x = x0(s.off), w = xw(s.len);
const g = svgEl('g');
g.appendChild(svgEl('title', {}, `${s.name} @${s.off} · ${s.len} B`));
g.appendChild(svgEl('rect', {
x, y:barY, width:w, height:barH,
fill:s.color, 'fill-opacity':0.22,
stroke:s.color, 'stroke-width':1.2, rx:4
}));
g.appendChild(svgEl('rect', {
x, y:barY, width:w, height:5, fill:s.color, rx:2
}));
if (w >= 56) {
g.appendChild(svgEl('text', {
x:x+w/2, y:barY+barH/2-4, 'text-anchor':'middle', 'dominant-baseline':'central',
fill:C.ink, 'font-size': w>=120 ? 13 : 11,
'font-family':'Cascadia Code,Consolas,monospace', 'font-weight':650
}, s.name));
g.appendChild(svgEl('text', {
x:x+w/2, y:barY+barH/2+14, 'text-anchor':'middle',
fill:C.dim, 'font-size':10, 'font-family':'Cascadia Code,Consolas,monospace'
}, `${s.off}..${s.off+s.len-1} · ${s.len}B`));
} else {
// 窄段:标签放在下方
g.appendChild(svgEl('line', {
x1:x+w/2, y1:barY+barH, x2:x+w/2, y2:barY+barH+8,
stroke:s.color, 'stroke-width':1
}));
g.appendChild(svgEl('text', {
x:x+w/2, y:barY+barH+20, 'text-anchor':'middle',
fill:s.color, 'font-size':10, 'font-family':'Cascadia Code,Consolas,monospace'
}, `${s.name.replace('段','')}`));
}
svg.appendChild(g);
});
container.innerHTML = '';
container.appendChild(svg);
}
/* 段内字段条:HTML flex,宽度 ∝ 字节,不叠压 */
function drawFieldBar(host, group) {
const wrap = document.createElement('div');
wrap.style.marginBottom = '22px';
const label = document.createElement('div');
label.className = 'seg-label';
label.textContent = group.title;
wrap.appendChild(label);
const bar = document.createElement('div');
bar.className = 'fbar';
group.fields.forEach(f => {
const cell = document.createElement('div');
cell.className = 'fcell' + (f.len <= 2 ? ' narrow' : '') + (f.attr === '保留' || f.name === 'pad' || f.name === '—' || f.name === 'rsvd' ? ' pad' : '');
cell.style.flex = `${f.len} 1 0`;
cell.style.background = f.color;
cell.style.opacity = f.attr === '保留' || f.name === 'pad' || f.name === '—' || f.name === 'rsvd' ? '0.35' : '0.85';
cell.title = `${f.name} @${f.off} · ${f.len} B · ${f.type}\n${f.txt}`;
const fn = document.createElement('div');
fn.className = 'fn';
fn.textContent = f.name;
cell.appendChild(fn);
if (f.len >= 2 || group.total <= 16) {
const fo = document.createElement('div');
fo.className = 'fo';
fo.textContent = `@${f.off}`;
cell.appendChild(fo);
}
bar.appendChild(cell);
});
wrap.appendChild(bar);
// 底部偏移尺
const ruler = document.createElement('div');
ruler.className = 'ruler';
group.fields.forEach(f => {
const sp = document.createElement('span');
sp.style.flex = `${f.len} 1 0`;
sp.textContent = f.len >= 4 ? String(f.off) : '';
ruler.appendChild(sp);
});
wrap.appendChild(ruler);
host.appendChild(wrap);
}
function drawFieldGroups(hostId, groups) {
const host = document.getElementById(hostId);
host.innerHTML = '';
groups.forEach(g => drawFieldBar(host, g));
}
/* 槽区示意 */
function drawSlotStrip(container) {
const W = 1000, H = 70;
const svg = svgEl('svg', { viewBox:`0 0 ${W} ${H}`, role:'img' });
const pad = 4, gap = 6;
const n = 9;
const totalGaps = gap * (n - 1);
const slotW = (W - pad * 2 - totalGaps) / n;
svg.appendChild(svgEl('text', {
x:pad, y:14, fill:C.dim, 'font-size':11,
'font-family':'Cascadia Code,Consolas,monospace'
}, '9 个等宽槽 · 每槽 56 B · 密排后有效槽 = count · 空节点不占位'));
for (let i = 0; i < n; i++) {
const x = pad + i * (slotW + gap);
const g = svgEl('g');
g.appendChild(svgEl('title', {}, `slot[${i}] · 56 B`));
g.appendChild(svgEl('rect', {
x, y:26, width:slotW, height:34,
fill:C.cyan, 'fill-opacity': i===0 ? 0.4 : 0.14,
stroke:C.cyan, 'stroke-width': i===0 ? 1.6 : 0.8,
rx:4
}));
g.appendChild(svgEl('text', {
x:x+slotW/2, y:43, 'text-anchor':'middle', 'dominant-baseline':'central',
fill: i===0 ? C.ink : C.dim, 'font-size':12,
'font-family':'Cascadia Code,Consolas,monospace', 'font-weight': i===0 ? 650 : 400
}, i===0 ? 'slot 0' : `slot ${i}`));
svg.appendChild(g);
}
container.innerHTML = '';
container.appendChild(svg);
}
/* DPRAM 预算 */
function drawDpram(container) {
const W = 1000, H = 100;
const svg = svgEl('svg', { viewBox:`0 0 ${W} ${H}`, role:'img' });
const pad = 10, inner = W - pad * 2, total = 1024;
const y = 30, h = 36;
const x0 = b => pad + (b / total) * inner;
const xw = n => (n / total) * inner;
svg.appendChild(svgEl('text', {
x:pad, y:16, fill:C.dim, 'font-size':11,
'font-family':'Cascadia Code,Consolas,monospace'
}, '逻辑预算 (out+in) ≤ 1024 B · 3-buffer 等效 · 1.6.22+ 写侧按 count 稀疏'));
function block(from, len, color, label, opacity) {
const g = svgEl('g');
g.appendChild(svgEl('title', {}, `${label} · ${len} B`));
g.appendChild(svgEl('rect', {
x:x0(from), y, width:xw(len), height:h,
fill:color, 'fill-opacity':opacity, stroke:color, 'stroke-width':1.2, rx:4
}));
if (xw(len) > 48) {
g.appendChild(svgEl('text', {
x:x0(from)+xw(len)/2, y:y+h/2,
'text-anchor':'middle', 'dominant-baseline':'central',
fill:C.ink, 'font-size':12, 'font-family':'Cascadia Code,Consolas,monospace', 'font-weight':650
}, label));
}
svg.appendChild(g);
}
block(0, 384, C.blue, 'OUT 384', 0.28);
block(384, 600, C.cyan, 'IN 600', 0.28);
block(984, 40, C.slate, '', 0.22);
// 余量标注放在下方,避免与 984/1024 刻度挤在一起
svg.appendChild(svgEl('text', {
x:x0(1004), y:y+h+18, 'text-anchor':'middle',
fill:C.slate, 'font-size':10, 'font-family':'Cascadia Code,Consolas,monospace'
}, '余 40'));
[0, 384, 984, 1024].forEach((t, i) => {
const x = x0(t);
svg.appendChild(svgEl('line', { x1:x,y1:y,x2:x,y2:y-6, stroke:C.line,'stroke-width':1 }));
svg.appendChild(svgEl('text', {
x, y:y-10,
'text-anchor': i===0 ? 'start' : i===3 ? 'end' : 'middle',
fill:C.dim, 'font-size':10, 'font-family':'Cascadia Code,Consolas,monospace'
}, String(t)));
});
svg.appendChild(svgEl('text', {
x:pad, y:H-8, fill:C.dim, 'font-size':11,
'font-family':'Cascadia Code,Consolas,monospace'
}, 'SM2 0x1400–0x187F (3×384) · SM3 0x1880–0x1F67 (3×600) · DPRAM 侧余 120 B'));
container.innerHTML = '';
container.appendChild(svg);
}
/* 表格 */
function attrChip(a) {
const map = {固定:'fix',密排:'adp',头部:'hdr',保留:'note',忽略:'gone',批量:'adp'};
return `<span class="chip ${map[a]||'note'}">${a}</span>`;
}
function fillTable(tbody, groups, segOf) {
const rows = [];
groups.forEach(g => {
g.fields.forEach(f => {
rows.push(`
<tr>
<td>
<span class="dot" style="background:${f.color}"></span>
<code>${f.name}</code>
<span style="color:var(--dim);font-size:12px;margin-left:6px">${segOf(g)}</span>
</td>
<td class="n">${f.off}</td>
<td class="n">${f.len}</td>
<td class="mono">${f.type}</td>
<td>${f.txt}</td>
<td>${attrChip(f.attr)}</td>
</tr>`);
});
});
tbody.innerHTML = rows.join('');
}
function fillFlatTable(tbody, fields) {
tbody.innerHTML = fields.map(f => `
<tr>
<td><span class="dot" style="background:${f.color}"></span><code>${f.name}</code></td>
<td class="n">+${f.off}</td>
<td class="n">${f.len}</td>
<td class="mono">${f.type}</td>
<td>${f.txt}</td>
</tr>`).join('');
}
/* boot */
drawOverview(document.getElementById('in-overview'), IN_SEGS, 600);
drawOverview(document.getElementById('out-overview'), OUT_SEGS, 384);
drawFieldGroups('in-fields', IN_GROUPS);
drawFieldGroups('out-fields', OUT_GROUPS);
drawSlotStrip(document.getElementById('slot-strip'));
const slotHost = document.getElementById('slot-fields');
[{title:'单槽 56 B', total:56, fields:SLOT_FIELDS}].forEach(g => drawFieldBar(slotHost, g));
drawDpram(document.getElementById('dpram-map'));
fillTable(document.getElementById('in-tbody'), IN_GROUPS, g => g.title.split('·')[0].trim());
fillTable(document.getElementById('out-tbody'), OUT_GROUPS, g => g.title.split('·')[0].trim());
fillFlatTable(document.getElementById('slot-tbody'), SLOT_FIELDS);
</script>
</body>
</html>