Files
DCU_APP/docs/compose/spec/tiered-pdo-mapping.md
T
cbb 6136c2363f feat(ethercat): 添加LAN9252睡眠挂起机制防止HBI访问冲突
添加了s_pmSuspended状态标志来管理LAN9252的电源管理模式,在芯片进入D1-D3
睡眠状态时阻止HBI访问以避免唤醒问题。同步修改了ethercat_sync中断处理程序,
在睡眠挂起期间跳过HBI相关操作确保芯片能够正常维持睡眠状态。

fix(spi): 提升SPI CS EXTI抢占优先级避免帧丢失

将SPI CS EXTI中断优先级从1提升至0,使其能够抢占PDI/SYNC1等关键中断,确
保SPI帧释放处理能够在微秒级完成,避免因延时导致的ESP32 CS间隔超时和帧
丢失问题。

feat(ssc): 实现OP输入新鲜度不变量监控机制

添加了输入映射序列号监控机制,当PDI路径中断时能够检测到输入数据冻结并
自动触发强制刷新,解决结构洞问题导致的输入数据停止更新现象。

chore(pdo): 更新PDO输入字节注释说明布局变更历史
2026-09-13 18:59:57 +08:00

314 lines
21 KiB
Markdown
Raw 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.
---
feature: tiered-pdo-mapping
status: designed
updated: 2026-09-12
branch: master (DCU_APP) + main (tools/ecat) — 无 worktree,沿用工作树
commits: <pending delivery>
---
# 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&lt;600→slim)会把 264 误判成瘦身布局——必须先用魔数 0xA5 拦下,再按 cap 排偏移。