--- feature: tiered-pdo-mapping status: designed updated: 2026-09-12 branch: master (DCU_APP) + main (tools/ecat) — 无 worktree,沿用工作树 commits: --- # Tiered PDO Mapping(按电机数分档收缩 SM3) ## Report (交付后填写) --- ## [S1] Problem 输入 PDO SM3 恒为 **600B**,与本拍实际密排电机数无关: - 3 电机时仍占用 600B SM 与 ~984B 过程数据帧,100Mbps 上单帧线路时钟地板约 **81µs**。 - 从站稀疏写(1.6.22)只省 PRAM 搬运,**不减线长**。 - 用户场景:电机数长期固定(常见 3 台),偶发插拔;希望档内无感、跨档自动重映射,且重映射 **<500ms**、不 NRST、不撞 1s 过程数据看门狗。 现链路硬约束: 1. SSC `PREOP→SAFEOP` 调 `APPL_GenerateMapping` 得 `nPdInputSize`,`CheckSmSettings` 要求 **ESC SM3.Length == nPdInputSize**(`ecatslv.c:509`)。 2. SOEM `Ibytes` 来自 CoE PDO 映射位数,**不是**从站运行时 tier;主站不改写 SM/FMMU/`Ibytes` 则线帧不变。 3. 完整 `do_op_start`(重枚举 + 250×4ms 喂帧 + 最长数秒等 OP)无法满足 <500ms。 4. `decode_input` 密排魔数 `@35=0xA5` 已可区分布局,但 **段3@544 / 段5@584 写死**,短 SM 下会读越界/错位。 --- ## [S2] Design ### S2.1 容量档与线格式 唯一真源(从站头 + 主站常量,数值必须一致): ``` ECAT_PDO_IN_MAX = 600 ECAT_PDO_IN_PREFIX = 40 // 段1 32B + 段2 头 8B,槽区起点 MOTW_OFF_SLOTS=40 ECAT_PDO_IN_XRX = 40 // 段3 长快照 ECAT_PDO_IN_DIAG = 16 // 段5 诊断(实际用 12B,对齐到 8) ECAT_MOT_SLOT_SIZE = 56 in_bytes(cap) = 40 + cap*56 + 40 + 16 = 96 + 56*cap ``` | 档 | count 范围 | cap | in_bytes | 线过程数据 out+in | 约线路时钟* | |----|------------|-----|----------|-------------------|-------------| | A | 0–3 | 3 | **264** | 384+264=648 | ~52µs | | B | 4–6 | 6 | **432** | 384+432=816 | ~65µs | | C | 7–9 | 9 | **600** | 384+600=984 | ~81µs(现状)| \*仅 eth 过程数据字节的 100Mbps 线时钟粗算,不含头/IFG 的完整帧模型见 `ECAT_BEAT_TAIL_REPORT`。 **布局语义(密排,魔数 `@35=0xA5` 不变):** ``` [0..31] 段1 桥状态(与现网一致) [32..39] 段2 头:count/active/boot/magic/gseq [40 .. 40+n*56) 密排槽,n = count ≤ cap [40+cap*56 .. +40) 段3 xrx [+40 .. +16) 段5 诊断(load/torn/seq/overfull/reltimeout/reject) ``` - **档内**(count 在同一 cap 槽位预算内变化):只更新 `count` 与槽内容,**不改 SM、不 PREOP**。 - **跨档**:走 S2.3 轻量重映射;两端同时切到目标 `in_bytes`。 - 非密排(magic≠0xA5 或 ilen≥640 旧布局)路径保持原样,本特性不碰。 主站由 `Ibytes` 反推 cap(与 tier 一一对应): ``` cap = (Ibytes - 96) / 56 // 264→3, 432→6, 600→9 xrx_off = 40 + cap*56 diag_off = xrx_off + 40 ``` 禁止再写死 544/584。 ### S2.2 从站契约(DCU_APP,目标版本 **1.6.34**) 1. **SII/EEPROM 不改**:`gen_eeprom.py` 仍 `PD_IN_SIZE=600`,冷启动 SM3 默认 600,与旧主站兼容。 2. **`APPL_GenerateMapping`**(`dc_app.c`,`PREOP→SAFEOP` 时调用): - 读 ESC 当前 SM3 `Length`(`GetSyncMan(PROCESS_DATA_IN)`)。 - 若 `Length` ∈ {264, 432, 600} → `*pInputSize = Length`(从站跟随主站)。 - 否则 → `*pInputSize = ECAT_PDO_IN_MAX`(600,保底)。 - `*pOutputSize` 仍 384,不变。 3. **`APPL_InputMapping`**:`memcpy(pInputData, …, nPdInputSize)`,不再写死 600。 4. **密排填充 / 稀疏写 / 差分写**(`ethercat_can_bridge.cpp` + diff/sparse 开关路径): - 段3/段5 物理偏移改为 `40 + cap*56` 起,其中 `cap = (nPdInputSize - 96) / 56`(在 Safe/OP 内 `nPdInputSize` 已被 GenerateMapping 更新)。 - 稀疏写范围同步:段A=`0 .. 40+count*56`,段B=`xrx_off .. nPdInputSize`。 - `count > cap` 时本拍只打包前 `cap` 个(并保持现有“撑满回退整包”仅在 cap=9 时语义不变);正常调度不应出现,出现时记诊断。 5. **对象字典 0x6000**:仍按 600B 定义(SDO 可读全镜像);过程数据长度以 SM/`nPdInputSize` 为准。不引入多 TxPDO 映射子索引(v1 范围外)。 6. **版本**:`config.h` patch → **34**,注释写明 tiered SM3 跟随主站。 **从站不主动改 SM 长度**,避免与主站写顺序竞态。 ### S2.3 主站契约(tools/ecat / `ecat_native.c`) #### 常量与状态 ``` tier_in_bytes[] = {264, 432, 600} // cap 3/6/9 g_tier_cap // 当前会话生效 cap,默认 9 g_tier_seen_count / g_tier_since // 滞回:观察到的新档 count 与起始时刻 g_tier_last_remap / g_tier_remaps // 诊断 ``` `count` 取密排头 `g_head.wcount`(或 `inp[32]`),**不用** `active`(离线电机仍占槽报最后一轮,属于“槽预算”)。 #### 首次进 OP - `do_op_start` 仍走完整 `config_init` + `config_map`(CoE 得 Ibytes=600)→ **首拍恒 Tier C**。 - 首拍 decode 得到 `count` 后进入滞回观察;不与 SAFEOP→OP 竞态。 #### 跨档轻量重映射 `do_tier_remap(target_cap)` 前置:`g_bus_open && g_op && 循环已 loop_stop(调用方持 g_bus_lock)`。 ``` 1. 记录 DC 形态 (dc_cycle_us, dc_shift_us) —— 与 wedge 恢复同源 2. OP → PREOP|ACK(不 INIT、不 config_init 重枚举) - statecheck PREOP ≤400ms;失败则回退完整 do_op_start 一次 3. 写 0x0420 = 10000(1s WD,与 do_op_start 同款) 4. 改写 SM3: - ec_slave[1].SM[3].SMlength = htoes(target_in_bytes) // StartAddr 仍 0x1880,flags 不动(3-buffer) - ecx_FPWR / ec_FPWRw 写 ESC SM3 寄存器(与 config_map 写 SM 同基址) 5. 改写输入 FMMU 逻辑长度(仅输入 FMMU,LogStart 不变,LogLength = target_in_bytes) 6. 同步 SOEM 进程数据尺寸: - ec_slave[1].Ibytes / Ibits = target_in_bytes / *8 - ec_group[0].Ibytes 调整,保证 send/receive_processdata 帧长 = O+new_I - g_expected_wkc 重算(inputsWKC 对短映射通常仍 1,需实读确认) 7. PREOP → SAFEOP|ACK(≤400ms)—— 此时从站 GenerateMapping 读回新 SM3 长度 8. 短喂帧:1ms 节拍 pump ~150 次(≈150ms,≪1s WD),再请求 OP 9. OP|ACK,继续 pump 直到 AL=OP,总预算 ≤400ms;失败一次则完整 do_op_start 10. g_tier_cap = target_cap;g_rxb_layout 保持 dense(3);清空槽表按 do_op_start 会话语义 (可选:保留 g_slots,避免 remap 窗口 UI 闪空——v1 与 do_op_start 一致选择清空+重建) ``` **目标耗时:** PREOP/SAFEOP/OP 状态检查各一次成功路径通常 10–50ms,加 150ms 喂帧,合计 **远低于 500ms**;全失败回退完整路径仍可恢复(不保证 <500ms,记日志)。 **禁止:** NRST、`ec_close`、改邮箱 SM、改输出 SM2 长度。 #### 滞回与触发点 - 仅在 **循环运行且 OP** 时评估;放在 `wedge_worker` 100ms 节拍(或 pdo_worker 每 N 拍采样后置标志,由 wedge 线程统一处理——**不得在 pdo_worker 内 loop_stop 自己**)。 - 规则: - `need_cap = cap_for_count(count)` - 若 `need_cap == g_tier_cap` → 清观察状态 - 若 `need_cap != g_tier_cap`: - 首次记下 `g_tier_seen_count` 与 `g_tier_since` - **同一 need_cap 连续 ≥500ms** 才执行 remap - 期间 count 又回到原档 → 清观察 - `pm/hold` 置位期间不 remap(与楔死看门狗一致)。 - remap 进行中 `g_tier_busy`:wedge 楔死恢复、`/api/motor/start`、SDO 独占路由互斥(复用 `g_bus_lock`)。 #### `decode_input` 密排分支 - 魔数判定不变。 - `cap = (ilen - 96) / 56`,非法(不能整除或 cap∉{3,6,9})时:按 cap=9 且 `off+size>ilen` 则 break 的现有保护,**不崩溃**;记 layout 异常。 - 段3/段5 偏移用 `xrx_off/diag_off`,替换 `XRX_DENSE_OFF_*` / `DIAG_OFF_*_WIN` 常量中的 544/584(常量改为宏函数或运行时变量)。 #### API / 观测 - `/api/motor/state` 或 busKv 增加:`tier_cap`、`tier_in_bytes`、`tier_remaps`、`tier_since`(可选)。 - `/api/pdo/tier`:`{"cap":3|6|9}` 手动钉档(立即 remap,不走 500ms 滞回);`{"auto":true}` 恢复自动。v1 至少实现手动钉档读写。 - `/api/open` 启动日志:`输入 %dB` 打印真实 `Ibytes`(remap 后),去掉写死「600B」文案。 #### 楔死恢复 / start / sdo - `wedge_worker`、`/api/motor/state` 快路径、`/api/motor/start`:重拉 `do_op_start` 后 **重置 `g_tier_cap=9`**,再允许滞回把档缩回去(简单、可预期)。 - `/api/sdo` 仍 `loop_stop` 不恢复——tier 状态在下次 start 时回到 9,可接受。 ### S2.4 前端 / 文档(次要) - `lab/spi_test.html`:bus 总线卡展示 `tier in_bytes`(若 API 提供);layout=dense 分支的段5 注释改为“偏移随 cap”。 - `lab/pdo_layout.html`:统计卡从「600 B SM3」改为「264/432/600 分档」;**不阻塞固件交付**,可同 PR 或 follow-up。 - 注释清理:避免再写“线速两布局同为 600B”这类在 tier 生效后错误的句子。 ### S2.5 错误与回退 | 情况 | 行为 | |------|------| | SM3 改写 FPWR 失败 | abort remap,保持旧 Ibytes,日志 WARN;下一滞回窗重试 | | SAFEOP 失败(SSC 拒 SM) | 读 ALstatuscode;完整 `do_op_start`(回到 600)并记 `tier_remap_fail` | | OP 失败 | 同上 | | count 突然 > cap(从站 bug/撕裂) | decode 已有越界 break;从站侧 count>cap 截断;**不**立即 remap,等滞回 | | 首拍 Ibytes 仍 600 且 count=3 | 正常进入滞回,500ms 后缩到 264 | | 主站新、从站旧(GenerateMapping 恒 600) | 主站写 SM=264 后 SSC CheckSm 失败 → 回退 600,功能与今日等价 | | 从站新、主站旧 | SM 仍被旧主站写 600,从站跟随 600,与今日等价 | ### S2.6 不做(Out of Scope 见 S3)但需预留 - 输出 SM2 分档 - 多 TxPDO 赋值(0x1C13 子索引选映射) - EEPROM/ESI 为每档发一份 SII - 改 1-buffer 换更大窗口 ### S2.7 文件级改动清单(Implement 对照表) > 行号以 2026-09-12 工作树为准,实现时以符号为准、行号仅作导航。 #### A. 从站 DCU_APP | # | 文件 | 改什么 | |---|------|--------| | A1 | `GD32E508_DCU_APP/comm/ethercat_pdo_size.h` | 新增分档纯宏(C/C++ 共用):`ECAT_PDO_IN_TIERS {264,432,600}`、`ECAT_PDO_TIER_CAPS {3,6,9}`、`ECAT_PDO_IN_PREFIX=40`、`ECAT_PDO_IN_XRX=40`、`ECAT_PDO_IN_DIAG=16`、`ECAT_PDO_IN_BYTES_FOR_CAP(cap)=(96+56*(cap))`、`ECAT_PDO_CAP_FOR_IN_BYTES(ib)`。静态断言:三档均 ≤600 且 `%8==0`,cap=9 时等于现行 `ECAT_PDO_IN_BYTES`。`ECAT_PDO_IN_BYTES` 保留为 **max=600**(数组/EEPROM 上限),语义注释改为「上限,运行期可小于」。 | | A2 | `GD32E508_DCU_APP/comm/ethercat_can_bridge.h` | 段3/段5 **枚举保持 cap=9 数值**(544/584…)作默认/静态断言基准;新增运行时偏移接口:`uint16_t ecatMotXrxOff(uint8_t cap)` / `ecatMotDiagOff(uint8_t cap)` / `ecatPdoInBytesForCap(uint8_t cap)`(inline 或声明+cpp)。`static_assert` 改为只约束 cap=9 与 max 的首尾相接。注释标明:短档时段3/段5 前移,主站必须按 Ibytes 解。 | | A3 | `GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp` `buildInput` @286–333 | 段3 填充:`ECAT_XRX_OFF_*` → `xrx = ecatMotXrxOff(cap)` 相对写。`memset(in,0,kEcatPdoInBytes)` 可改为只清 `in_bytes`(或仍清 600,多清无害但稀疏/diff 会多比——**建议只清 in_bytes**)。`cap` 来源见 A5。段1/段2 偏移不变(0–39 + slots)。 | | A4 | `GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp` `fillMotorRound` @119+ | 逻辑不变;若 `count > cap` 截断到 cap(防越界写到段3)。影子区仍按 max 分配。 | | A5 | `GD32E508_DCU_APP/comm/ssc/port/dc_app.c` `APPL_GenerateMapping` @355–360 | 读 `GetSyncMan(PROCESS_DATA_IN)`(channel=3)`Length`;∈{264,432,600} 则 `*pInputSize=Length`,否则 600。随后调用新增 `EcatPdo_OnPdoInBytes(*pInputSize)`(或写 `EthercatPdo` 静态),通知填充层当前 in_bytes/cap。输出仍 384。 | | A6 | `GD32E508_DCU_APP/comm/ssc/port/dc_app.c` `APPL_InputMapping` @408–453 | `EcatPdo_WriteInputs` 之后:`memcpy(pInputData, …, nPdInputSize)`(替换写死 `ECAT_PDO_IN_BYTES`)。 | | A7 | `GD32E508_DCU_APP/comm/ethercat_pdo.h` / `.cpp` | 新增静态 `s_inBytes`/`s_cap`(默认 600/9)+ `OnPdoInBytes(uint16)`。`fillDiagTail` @114–155:`ECAT_DIAG_OFF_*` → `diag=ecatMotDiagOff(cap)` 相对。`sparseEscWritePlan` @166–194:`ECAT_XRX_OFF_CH`/`kPdoInBytes` → 运行时 `xrx`/`in_bytes`;「撑满」条件改为 `headEnd >= xrx`。`escSeg2WriteLen` @199–216、diff 三区 @258–279:同改。影子数组 `s_escSh2/s_escSh3` **保持按 600 上限分配**,只比较/写 `in_bytes` 内长度。 | | A8 | `GD32E508_DCU_APP/sys/config.h` @152 | `APP_FW_VERSION_PATCH` 33→**34**,追加 1.6.34 注释:SM3 分档跟随主站(GenerateMapping 读回 SM3.Length)。 | | A9 | `GD32E508_DCU_APP/comm/ssc/port/gen_eeprom.py` | **不改** PD_IN_SIZE=600(v1 不刷 EEPROM)。可在文件头加一行注释:「运行期主站可把 SM3 改为 264/432,SII 仍 600」。 | | A10 | `ethercat_diff_stats.cpp` | 若 `kInBytes` 宏绑死 600:改为读 `EthercatPdo::inBytes()` 或仍按 max 快照——**差分仪表在短档下勿拿 600 去比 in_bytes 内容**;实现时优先 `observe` 用当前 in_bytes。 | #### B. 主站 tools/ecat | # | 文件 | 改什么 | |---|------|--------| | B1 | `ecat_native.c` 顶部常量 @70–133 | 新增:`TIER_IN_BYTES[]`、`tier_cap_for_count(n)`、`tier_in_bytes(cap)`、`dense_xrx_off(ilen)`、`dense_diag_off(ilen)`(`cap=(ilen-96)/56`)。**保留** `XRX_DENSE_OFF_*`/`DIAG_OFF_*_WIN` 仅作文档/默认 cap=9;decode 路径改走函数。 | | B2 | `ecat_native.c` 状态 @190–226 附近 | 新增:`g_tier_cap`(默认9)、`g_tier_auto`(默认1)、`g_tier_busy`、`g_tier_need_cap`、`g_tier_since`、`g_tier_remaps`、`g_tier_remap_fail`。 | | B3 | `ecat_native.c` `decode_input` @450–530 | 密排分支:`xch/xda/diag` 用 `dense_*_off(ilen)`。若 `(ilen-96)%56!=0` 或 cap∉{3,6,9}:仍按魔数密排处理但 `off+size>ilen` 则 break(已有),并置 layout 异常标志。段5 读取位置同步改(@559–560 现用 `DIAG_OFF_LOAD_WIN`)。 | | B4 | `ecat_native.c` 新函数 `do_tier_remap(int target_cap, …)` | 放在 `do_op_start`/`do_op_stop` 附近。实现 S2.3 十步;内部用 `ecx_FPWR`/`ec_FPWRw`:SM3 长度字 @ ESC `0x081A`(`ECT_REG_SM0+3*8+2`),整段 SM 重写可抄 `ethercatconfig.c` `ecx_map_sm` 的 FPWR 模式;输入 FMMU:读当前 FMMU 表,只改输入那条的 `LogLength=target_in`,FPWR 回去;`ec_slave[1].Ibytes=Ibits/8`、`ec_group[0].Ibytes` 收缩/回扩;`g_expected_wkc` 重算。成功:`g_tier_cap=target`;失败:`do_op_start` 回落并 `g_tier_cap=9`。 | | B5 | `ecat_native.c` `wedge_worker` @3057–3100 | 在 `need` 楔死分支**之前**增加 tier 滞回评估(仅当 `g_loop_running && g_op && !g_tier_busy && !pm_hold && g_tier_auto`):用最新 `g_head.wcount`;同一 `need_cap≠g_tier_cap` 持续 ≥0.5s → `loop_stop` + `do_tier_remap` + `loop_start`。楔死恢复成功路径:`g_tier_cap=9`、清观察。 | | B6 | `ecat_native.c` `do_op_start` 成功出口 @1178–1207 | `g_tier_cap=9`、清滞回观察。日志 `输入 %dB` 已打真实 Ibytes(@1180)——确认 remap 后再 start 也正确。 | | B7 | `ecat_native.c` `/api/motor/start` @1987–2046 | 成功 `loop_start` 后同样 `g_tier_cap=9`(若走的是未 remap 的 do_op_start)。日志去掉写死「输入 600B」文案 @2039。 | | B8 | `ecat_native.c` `/api/motor/state` @2365–2403 | 快路径楔死恢复成功后 `g_tier_cap=9`。`build_state_json` 增加 `tier_cap`/`tier_in_bytes`/`tier_remaps`(函数体搜 `build_state_json`)。 | | B9 | `ecat_native.c` 新路由 `/api/pdo/tier` | GET:返回 `{ok, auto, tier_cap, tier_in_bytes, remaps, last_ms}`。POST body `{"cap":3\|6\|9}` 或 `{"auto":false\|true}`:钉档时持 `g_bus_lock`、`loop_stop`、`do_tier_remap`、`loop_start`;`auto` 只改标志。 | | B10 | `ecat_native.c` `pdo_worker` @848–852 | 无需改 Ibytes 来源(已读 `ec_slave[1].Ibytes`)。**禁止**在此 remap。可选:每 N 拍把 `g_head.wcount` 时间戳写给 wedge(直接读 `g_head` 已无锁跨线程——与现状一致,保持)。 | | B11 | `build_new.bat` / 工程 | 无新源文件则不用改;若 `do_tier_remap` 拆到新 `.c` 再加入 bat。 | #### C. 前端 tools/ESP32(不阻塞固件) | # | 文件 | 改什么 | |---|------|--------| | C1 | `lab/spi_test.html` | 总线/KV 区:若 API 返回 `tier_in_bytes` 则显示「SM3 264/432/600 · 档」。dense 段5 注释 @1198 改为「偏移随 cap」。CSS/JS 引用若外置需 `?v=`(本页自包含则内联即可)。 | | C2 | `lab/pdo_layout.html` | 统计卡 @243–244、图例 @819/@841、说明 @264:由「600 B SM3」改为分档说明;图可标三档长度。 | | C3 | `ECAT_BEAT_TAIL_REPORT.html` 等历史报告 | **不改**(已归档结论);避免改写历史数字。 | #### D. 明确不改 - `ESP32_COMM/**` - `gen_eeprom.py` 的 PD_IN_SIZE、`DCU_ESI.xml` - `soem-src/**`(只调用,不改库;FMMU/SM 用现成 FPWR) - 输出 SM2、邮箱 SM0/1 #### E. 实现顺序(文件依赖) ``` A1 → A2 → A5/A6(GenerateMapping+InputMapping) → A3/A4/A7(填充与稀疏/差分,依赖 A5 通知 in_bytes) → A8 版本 B1 → B3 decode → B4 remap → B5/B6/B7 滞回与重置 → B8/B9 API C1/C2 可后置 ``` 双端可并行:A 与 B 仅共享数值表;联调以 V3–V8 为准。 --- ## [S3] Out of Scope - ESP32_COMM 固件(不解析 EtherCAT PDO) - 输出 PDO 384B 收缩 - 刷 EEPROM / 冷启动依赖的 SII 多映射 - 电机 10+ 节点 - 用 tier 直接调 PDO 周期(0.25ms 等) - 提交/合并(用户明确不自动 commit) --- ## Tasks - [ ] T1: A1+A2+A5+A6 从站长度宏 + GenerateMapping 跟随 SM3 + InputMapping 按 nPdInputSize —— acceptance: 静态断言 264/432/600;主站手写 SM3=264 后 PREOP→SAFEOP 不报 0x001A/INVALIDSMINCFG;`nPdInputSize` 变 264 (covers: S2.2 S2.7-A) - [ ] T2: A3+A4+A7 从站密排/稀疏/差分偏移 cap 相对 —— acceptance: 264B SM 下段3/段5 不越界,主站能读 load/torn;count≤3 稀疏写两段正确 (covers: S2.2 S2.7-A; depends: T1) - [ ] T3: A8 版本 1.6.34 —— acceptance: PATCH=34 + 注释 (covers: S2.2; depends: T1) - [ ] T4: B1+B3 主站 decode cap 相对 —— acceptance: ilen=264+magic 槽/段3/段5 正确;ilen=600 回归一致 (covers: S2.1 S2.3 S2.7-B) - [ ] T5: B4 do_tier_remap + Ibytes/FMMU/group —— acceptance: 600→264→600 各一次 AL=OP、wkc 正、成功路径日志 <500ms (covers: S2.3; depends: T4) - [ ] T6: B5+B6+B7 滞回 + start/wedge 重置 —— acceptance: 稳定 3 电机 0.5–1.0s 进 264;4 电机进 432;楔死后回 9 再缩回 (covers: S2.3; depends: T5) - [ ] T7: B8+B9 `/api/pdo/tier` 与 state 字段 —— acceptance: POST cap 立即 remap;GET 含 tier_* (covers: S2.3; depends: T5) - [ ] T8: V1–V10 双端编译+台架 —— acceptance: cbuild/build_native 通过;见 Verification (covers: S2.2 S2.3; depends: T1–T7) - [ ] T9: C1+C2 前端分档展示 —— acceptance: 页面显示当前档;不阻塞 T8 (covers: S2.4; depends: T7) --- ## Verification(交付证据) | 步骤 | 命令/操作 | 期望 | |------|-----------|------| | V1 | DCU cbuild(既有 Release+GD32E508VE) | 0 error | | V2 | `build_new.bat` / VS2022 cl 原生主站 | 产出 `ecat_native.new.exe` | | V3 | 冷启动 600 进 OP(3 电机) | 与今日一致,layout=dense | | V4 | 等待 ≥1s | 自动 remap 到 264,日志含耗时 | | V5 | `/api/motor/state` + spi_test | Ibytes/tier=264,count=3,wkc 稳定 | | V6 | rx 线时(粗) | 过程数据字节 648 vs 984,rx 段应下降(允许 ±15µs 噪声) | | V7 | 手动 `/api/pdo/tier` cap=9 | 回到 600 | | V8 | 拔/加第 4 台(或模拟 count) | 滞回后 432 | | V9 | 旧从站 + 新主站 | SAFEOP 失败路径回 600,总线仍可用 | | V10 | 楔死/PM hold | hold 中不 remap;恢复后 tier 策略符合 S2.3 | --- ## Journey log(设计期) 1. 线时收益来自 **SM3 变短**,稀疏写不够——与 1.6.22/1.6.25 结论一致。 2. SSC 长度双源校验 ⇒ 必须主从同数;v1 选「主站写 SM,从站 GenerateMapping 读回」,避免 EEPROM 与多 PDO 映射。 3. 首拍恒 600 是为了兼容 config_map/CoE,用 500ms 滞回消化一次 remap,比改 CoE 映射表风险低。 4. 重映射不能走完整 `do_op_start`(重枚举+长喂帧)——独立轻量路径 + 失败回退完整路径。 5. 段3/段5 偏移写死是主站解码的隐雷;Ibytes 分支(ilen<600→slim)会把 264 误判成瘦身布局——必须先用魔数 0xA5 拦下,再按 cap 排偏移。