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

21 KiB
Raw Blame History

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