Compare commits

..
18 Commits
Author SHA1 Message Date
cbb 662dcccbf2 docs(review): 专家评审 2026-10-19 2026-10-20 21:12:10 +08:00
cbb d8b3c9767c feat(ota): 电机 OTA 支持 EtherCAT 模式(CoE 邮箱进料 + 通道独占锁)
进料口从「只有 ESP32 SPI 一条」扩成两条,中继引擎不变:主站经 CoE 0x2511 数据窗
把电机固件明文灌进 GD32 的 48KB SRAM 暂存区,再由 comm/ecat_motor_ota 泵进原有
4KB 环缓;AES 握手、CAN 单发停等、重发、0AAC 运行确认全部沿用 MotorOtaRelay。
纯网线(ESP32 WiFi 关断、/motor/ota 打不通)场合因此也能刷电机。
偏移/幂等判据仍只有一份:feedData 拆出 feedAt,两个进料口共用。

同时把审计报告 VULN-12/13/14 落地(此前只存在于文档,一行未实现):start() 第一条
语句在 PRIMASK 内原子置位 s_channel_lock_mask,EtherCAT 桥接 sendBatch/processOutput
与 SPI 0xC0/0xC7 对锁定通道回新码 0xE7=「被电机 OTA 独占」,不冒充「邮箱满」0xE4。
反向互斥一并补上:电机 OTA 活跃时 requestEnterBoot() 直接拒绝——SPI 0xB0、CoE 0x2502、
CAN 0x320 三条交接入口共用这一个判据,避免复位把刷写腰斩在 erase 之后。

完整性口径按链路分开,不互相冒充:块级 CRC32 + 整镜像 CRC32 都在从站核,且核过才
让电机进 BOOT——失败时电机仍跑旧 APP、擦除没开始,不存在「先擦再验」的变砖路径。
CRC 为 CRC-32/B(poly 0x04C11DB7,初值 0xFFFFFFFF,无反射无末尾异或),与 v2_crc32
同算法(用公开向量 123456789=0x0376E6E7 对过)。

块长取 1024 而不是 BOOT 用的 4096:SSC 的分段下载缓冲是全工程唯一的 malloc 用户,
而 APP 的 __HEAP_SIZE 只有 3KB,4096 会被从站当场回 abort 0x05040005 Out of memory
(BOOT 的堆是 16KB 才撑得住 4096——块长能抄,堆抄不了)。这是跨仓常量,须与
tools/ecat 的 MFO_CHUNK_OBJ 同步。

APP 1.7.4(RAM ZI 97156)。已实测:0x2510 参数写与一块 1016B 数据写均被从站接受
(staged=1016、phase bit4);端到端电机刷写待复测(native 块长修复后未跑完整一轮)。
2026-10-11 02:47:19 +08:00
cbb f3d7fc0c30 feat(canfd): 添加收发器静默模式支持和SPI命令接口
添加了CAN FD收发器静默模式功能,允许通过硬件引脚控制发送使能:
- 新增setTxSilent/isTxSilent接口控制TCAN1057收发器Silent模式
- 静默时驱动器关断但接收机继续工作,支持只听不发场景
- 添加SPI命令0xC8用于ESP32远程控制静默模式切换
- 在总线状态位图中增加bit5表示静默状态
- 新增canBusyWaitUs函数用于ISR安全的微秒延时

fix(esc_lan9252): 改进warm jump后LAN9252探测逻辑

修复warm jump导致的LAN9252初始化问题:
- 识别warm jump后芯片非POR状态(idx0!=0x1234)的情况
- 自动补发硬件复位对齐到POR状态后再进行正常探测
- 改进调试信息输出,区分POR状态和warm jump残值情况
- 修正失败路径的复位标志初始化逻辑

refactor(spi): 整理SPI命令文件结构

调整SPI命令相关文件的组织结构:
- 移动诊断相关文件到更合适的位置
- 重新整理SPI命令处理文件的分组
2026-10-10 02:25:56 +08:00
cbb a777be1cb4 docs(compose): EtherCAT 总线刷写 DCU 加密/握手完整性审计结论 2026-09-24 03:47:37 +08:00
cbb 28d8426052 feat(ethercat): 添加ESP32使能控制功能通过EtherCAT SDO
添加0x2503对象字典项用于通过EtherCAT SDO控制ESP32 WiFi使能信号,
解决WiFi关断后无法重新开启的问题。同时添加sys/esp32_en.h/.cpp文件
提供GPIO控制接口。

SPI诊断命令(0xA2)扩展支持ESP32 EN引脚控制,用于诊断备援。
ESP32掉电重启后自动清理SPI链路状态,防止通信异常。
2026-09-24 03:18:18 +08:00
cbb 5fd986299c feat(comm): 1.6.51 CAN 空通道自动静默 + PDO 诊断 + OTA 布局共用头
变更点:
1. CAN-FD(device/CANFD.cpp、sys/config.h)
   - 发送统一 FD+BRS(DLC≤8 不再回退经典 CAN);OTA 期仍按通道压制 BRS
   - 空通道自动识别:连续 5 个 1Hz 窗口无有效帧且有错误 → 静默上报;有帧即恢复
   - 三路 CAN 默认全开;RX parse budget 48→64
2. EtherCAT 桥 / PDO
   - 段1 增 txtxid/rx00ff;版本应答 0x00FF 粘滞发布,避免被 CONFIG 冲掉
   - STATUS 禁止写入 xrx 快照;processSeq 新鲜度救援
3. OTA / 配置
   - sys/config.h include 仓库内 shared/ota_layout.h(与 BOOT 共用,不再依赖仓外路径)
   - ota_boot_request 对齐配置页 word 布局
4. docs/audit_ecat_spi_wifi_ota_compatibility.md:ECAT/SPI/WiFi/OTA 兼容性审计
5. .gitignore:忽略 .trae/、docs/traework-import、评审过程稿
2026-09-20 22:58:09 +08:00
cbb 63af85e7be feat(ethercat): 1.6.40 起板探针改 BYTE_TEST 放行, 根治 warm jump 后 EtherCAT 降级 2026-09-16 01:54:25 +08:00
cbb 7d9d9c70a1 feat(app): 1.6.39 —— 1.6.29 起未上板业务改动入库,启动链与已提交基线对齐
主体为工作区累积的业务改动(CAN/PDO/桥/SSC/task_manager 等,详目见
config.h 版本注释链),新增 CoE 0x100A 真版本上报与 0x2502 APP→BOOT
总线交接。启动链(startup/SystemInit/initial/systick/hardfault)与
HEAD 逐字节一致;1.6.35~38 的跳转诊断实验(3M 化/BKP 面包屑/
haltWithWatchdog)未提交、已整体回退,补丁存 _revert_backup_20260916/。
真机实测(配对 BOOT 1.1.0.18):刷写 warm jump 与 reset cold jump 均可。
2026-09-16 00:48:34 +08:00
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
cbb 6afaacd03a feat(comm): LAN9252增加csrRead验证分级并实现PDO差分写功能
为提升EtherCAT通信性能,实现以下两项优化:

1. 对EscLan9252::csrRead方法增加verify参数,支持分级验证:
   - verify=true(默认):保持原有的双读比对+重试机制,用于SM/AL/邮箱等
     关键路径防止EXMC毛刺
   - verify=false:单读模式,用于诊断回读/MII busy轮询/init轮询等低风
     险场景,节省一半CSR间接访问时间

2. 实现PDO输入差分写ESC功能:
   - 将输入PDO分为三区:段1桥状态(0-31)、段2电机密排(32-543)、段3+5
     快照诊断(544-599)
   - 采用3-buffer冲刷协议:变化后连续写3拍确保3个缓冲区全部更新
   - 对内容稳定的段跳过写操作,显著减少PRAM字节搬运时间
   - 全段跳过时强制写末2字节完成SM事件应答

同时更新配置版本至1.6.25,添加相关配置选项和文档说明。
2026-09-12 20:10:52 +08:00
cbb 9ca3db26f8 feat(comm): 1.6.22 稀疏写 ESC + 1.6.23~24 pmDrive 电源管理; LAN9252 性能审计报告 2026-09-12 01:59:42 +08:00
cbb 1742d9a759 feat(comm): 1.6.21 段2改电机整轮密排, 删段4历史覆盖窗
- 段2(32..543): count=实际电机数(0..9), 空节点不占位, 槽序按 node_id 升序
- 同号多通道取 seq 最新; magic@35=0xA5 区分密排与旧滑窗
- 差分填充: 节点集合+seq/online 未变时整段复用影子
- 删段4 fillRxWindow; 非STATUS仍走段1/3最近一帧
- 输入PDO总长仍600B, SII无需重刷; 主站需按count密排解码
- 动机: 接3电机却看到5状态帧(空槽占位); win_base滑窗对已识别电机无意义
2026-09-11 01:08:37 +08:00
cbb 959e2aef6e feat(comm): 1.6.14~1.6.20 —— CAN-FD 数据段 5M、批量区改定长覆盖窗口(输入 PDO 496→600)、drop 口径修复、差分写仪表
1.6.14 CAN-FD 数据段 2Mbps→5Mbps:CONFIG_CAN_DATA_PRESCALER 9→3、BS1 3→4TQ
  (90MHz/(3×6TQ),采样点 80%→83.3%),仲裁段仍 1M。TDC filter/offset 维持 4 不做
  等比缩放(TDCMOD=CALC_AND_OFFSET 下硬件已自动测回路延时;放到 12 会让本节点回采
  错开约 6 个数据位,自发热帧被判错误帧)。配套电机 APP/BOOT data_prescaler=2。

1.6.15 段4 批量接收由"环形队列 + 变长紧排排空"改为 5×44B 定长覆盖窗口,输入 PDO
  496→600(75×64bit),段5 诊断区 484→584。动机两条:现场 4000 帧/s 超旧容量 3 帧/拍
  恒丢 1 帧;更关键是排空赤字把队列长期压在饱和态,而 1.6.9~1.6.13 追的错标/拼接竞态
  正是饱和态喂出来的(判满用的 tail 滞后→生产者写进消费者刚读过的槽)。定长消灭
  tail/判满/pending,边界越界写在结构上不可能再犯。每槽带 u32 seqlock:buildInput 在
  PDI_Isr(preempt=0) 抢占主循环写侧,无它则主站会拿到"新 ID + 旧数据"拼出的假帧。
  out+in=984 ≤ 1024 硬上限,DPRAM 留 120B。语义变更:批量区从"不丢的事件流"变"最近
  N 帧窗口",去重与丢帧判据移交主站按 seq 归一。三端同步 size.h/bridge/dc_app/
  gen_eeprom/DCU_ESI.xml,必须重烧固件 + 烧 EEPROM + 断电冷启动,只烧一边 SAFEOP 报
  AL 0x001A/0x0011。

1.6.16 段1/段3 快照 drop 计数口径修复(纯判据,PDO 布局逐字节不动):旧判据
  s_rxValid 置位后永不清零 ⇒ 开机首帧后每帧都算丢弃,drop ≡ rx−1,是零灵敏度假仪表
  且把网页状态栏刷成 ERR。新增 s_rxPublished,判据改 s_rxValid && !s_rxPublished。

1.6.17~19 [DIFF-STATS] 差分写取证仪表,零行为变更,只加 0x2000 sub13..23(11 个只读
  UINT32) ⇒ 不动 PDO 长度/映射,无需重生成 SII。实测:每拍 27.4% 字节真变化(164B)、
  run 峰值 12~13、段2 占变化量 ~53%、段4 ~25%。
  1.6.19 推翻 1.6.18 的自我诊断:比对开销虚报不是 -Oz 把 memcpy 编成调用(手拼移位
  反而 21.3µs→41µs),真根因是 alignas(4) 到 const uint8_t* 形参处丢失,优化器无法
  证明 4 字节对齐。__builtin_assume_aligned + memcpy 后循环体缩成 ldr/ldr/cmp/bne、
  bl 归零。教训:测量工具自身的开销会直接变成结论。

1.6.20 基线对照版(当前提交状态 CONFIG_ECAT_DIFF_STATS=0):同工况(电机在线无指令)
  实测 load 49.1% vs 仪表开的 51.6% ⇒ 仪表自身成本 2.4 个点,差分改造收益 = 0。
  按实测变化量推算 A1(段粒度条件写) 只值约 1 点(段2/段4 每拍都变,必然整块重写),
  仅 A2(8B 块粒度 + run 上限退化为整写) 有空间 —— 推算值,未做 A/B 实测不作为结论。
2026-09-09 03:04:53 +08:00
cbb 0cd3d9bed2 fix(comm): 回退LAN9252批量读写优化并修复多个CAN-EtherCAT桥竞态问题
回退1.6.0引入的LAN9252 PRAM批量读写优化,因其AVAIL_CNT计数器对短读存在语义问题,
导致邮箱短读和探针传输时出现FIFO陈旧数据损坏问题。

同时修复EtherCAT-CAN桥中的多个竞态条件:
- 修复消费者在fillRxBatch中直接访问共享ring buffer导致的数据混淆问题
- 修复生产者-消费者间tail指针发布时机不当造成的槽位覆盖竞态
- 修复WIN窗口基址更新位置错误导致电机状态窗口被锁定的问题
- 修复buildInput中断上下文与主循环间的数据竞争和内存撕裂问题
- 添加STATUS帧过滤机制减少批量区压力并解决队列饱和问题

此外还增加了多个诊断计数器用于监控撕裂、溢出和拼接错误,并调整了
输入PDO段布局和大小参数以支持新的48字节STATUS格式。

这些修改解决了上位机观察到的帧标签错误、电机状态丢失和数据拼接问题。
2026-09-08 00:52:35 +08:00
cbb ace5a1671b chore(config): 固件版本 bump 1.6.1(输入 PDO 瘦身记录, 见 9f1c2d2) 2026-09-06 00:20:51 +08:00
cbb 9f1c2d25dd feat!: 1.6.0 DWT 剖分探针 + 1.6.1 输入 PDO 瘦身 640→496(待重烧验收)
1.6.0 剖分探针(上一轮真机已验证, 随本轮一并入库):
- ecatappl.c: 0x2000 sub5..12 DWT 周期累加器(PDI_Isr/输入映射/640B 写细分)
- ethercat_pdo.cpp: 输出按需全量读(12B 探针判 seq 沿, 省每周期 ~190µs 全量读)
- 段5 诊断区(负载率+seq)随 PDO 上报, 主站 /api/app/load 轮询

1.6.1 输入 PDO 瘦身 640→496(62×64bit; 0..359 与 1.5.61 逐字节兼容):
- 依据: DWT 实测 640B 写恒 296µs/拍且与 SYNC0 相位无关(主站 dc_bench 五形态
  对拍极差 0.04%), PRAM ≈0.463µs/B 线性; 真机 6086 帧 DLC 仅 24/32
- 批量接收条目 8B 头+64B 数据 → 8B+32B(DLC 真值保留, 截断打 flags bit1,
  环形队列仍存全量 64B); 段5 诊断区 632→484
- 预期写侧 −67µs/拍(→~230µs, 占空 29.7%→23%); LRW 帧 1024→880B
- 五处一致: ethercat_pdo_size.h(真源) / dc_app.c(62 条目+映射初始化器) /
  gen_eeprom.py(PD_IN_SIZE, nEntry 自动推导) / DCU_ESI.xml(SM3 496+62 条目) /
  bridge.h(kEcatRxBatchDataMax=32, 断言更新)
- 主站 tools/ecat decode_input 按 Ibytes 双布局自适应, 固件/EEPROM 与主站可
  任意先后升级; 混烧(只烧一侧)会被 SM 长度校验拦下(AL 0x001A)
- AC6 编译 0 警告; 真机验收待: 烧 axf + ecat.bat flash + 断电冷启动
  (流程与复验判据见 tools/ecat/MIGRATION.md 1.6.1 节)
2026-09-06 00:19:18 +08:00
cbb 70daa4b0c6 feat(DCU_APP): 添加CPU负载监控和EtherCAT诊断功能
- 在DCU_APP项目中添加cpu_load.h和cpu_load.cpp模块用于CPU负载率监测
- 修改csolution配置将构建目标从Debug切换为Release
- 在EtherCAT PDO协议栈中新增段5诊断区(632-639字节),包含负载率和序列号
- 在所有关键中断服务程序中添加CpuLoad::isrEnter()/isrExit()钩子用于准确统计中断占用
- 增强EtherCAT同步中断处理,改进AL事件掩码守护机制,修复OP状态下PDI中断门控问题
- 解决SSC源码中指针地址对齐警告和打包成员访问警告问题
- 在主循环中集成CPU负载率采样tick()调用,实现1秒滚动窗口统计

BREAKING CHANGE: 构建配置从Debug改为Release,EtherCAT协议栈增加新的诊断数据区
2026-09-05 20:07:22 +08:00
cbb 2ebd4ff1d2 fix(ssc): 根治邮箱楔死(1.5.59): 几何常量化消灭建立路径, 守护字节序修复
- ecat_def.h: 新增 MBX_SM0/1_START/SIZE 构建期常量(=SII 真值 0x1000/0x1200 各 256B,
  与 gen_eeprom.py/DCU_ESI.xml 三方一致, 改 SII 邮箱尺寸必须同步)
- mailbox.c: MBX_StartMailboxHandler 常量直达缓存, CSR 单次间接读降为交叉校验
  (失配打 [ECAT][MBX] WARN)。根因: INIT→PREOP 单次 CSR 读错值(0x1204/0x1208,
  BUSLAT 余量外的 EXMC 毛刺)被缓存成 u16EscAddrSendMbx → 响应写错址 → 断电才清;
  常量化后错值无进入缓存的路径, 运行期内存踩踏也会在每次 INIT→PREOP 被覆写
- esc_lan9252.cpp: csrRead 双读 CSR_DATA 一致校验+整事务重试(CSR_DATA 为锁存
  寄存器非 FIFO, 连读必一致; 不一致即本拍总线毛刺)。瞬态错读曾致 CheckSmSettings
  以 0x0016/0x001F 打断转换
- dc_app.c: 修复守护/心跳 SWAPWORD 字节序 bug——ESC 寄存器 PDI/CSR 读即主机序
  (与 GetSyncMan/mailbox 裸读一致), 交换使 sm0Len=0x0001<8 恒短路, 1.5.52~1.5.58
  地址失配守护从未生效过; 现降级为运行期内存踩踏兜底并真正可用; ReadAlDiag 同修,
  期间串口日志的 al/evt 均为字节交换错值, 历史日志需交换重读
- task_runner.cpp: 守护注释同步
- 新增 _syntax_check.bat + _syntaxcheck_stubs: 无 AC6 环境用 VS2022 cl /Zs 验 C 文件

真机验收: 140 次 op/start 全成功(40 轮 DC 4ms + 100 轮 DC/free-run 交替),
每轮进 OP、SDO 0x2000:3/4 全成功, 无 0x0016/0x001D/0x001B。
2026-09-05 17:38:35 +08:00
74 changed files with 8178 additions and 1124 deletions
+20
View File
@@ -85,3 +85,23 @@ uv2csolution.log
# legacy intermediate project file
*.cprj
# Verification & Test suite process artifacts
/tests/
/.agents/
TEST_READY.md
TEST_READY.html
convert_md_to_html.py
*.html
# IDE / 多智能体过程产物
.trae/
.worktrees/
.qoder/
.zcode/
docs/traework-import/
# 本机固件 keep / 安全评审过程稿
firmware_keep/
SECURITY_AUDIT_REPORT.*
ORIGINAL_REQUEST.md
+1 -1
View File
@@ -30,7 +30,7 @@ solution:
target-set:
- set:
images:
- project-context: GD32E508_DCU_APP.Debug
- project-context: GD32E508_DCU_APP.Release
debugger:
name: ST-Link@pyOCD
clock: 4000000
@@ -16,6 +16,8 @@ project:
- file: ./sys/task_manager.cpp
- file: ./sys/task_runner.h
- file: ./sys/task_runner.cpp
- file: ./sys/cpu_load.h
- file: ./sys/cpu_load.cpp
- file: ./sys/ota_boot_request.h
- file: ./sys/ota_boot_request.cpp
- file: ./sys/aes128.h
@@ -25,6 +27,8 @@ project:
- file: ./sys/hardfault.cpp
- file: ./sys/cpp_stubs.cpp
- file: ./sys/config.h
- file: ./sys/esp32_en.h
- file: ./sys/esp32_en.cpp
- group: device
files:
- file: ./device/adc.h
@@ -66,6 +70,8 @@ project:
- file: ./device/spi/app_spi_cmd_motor_version.cpp
- file: ./device/spi/app_spi_cmd_motor_ota.cpp
- file: ./device/spi/app_spi_cmd_exmc_diag.cpp
- file: ./device/spi/app_spi_diag.h
- file: ./device/spi/app_spi_cmd_spi_diag.cpp
- group: spi_link
files:
- file: ../../spi_link/spi_link_v2.cpp
@@ -81,8 +87,14 @@ project:
- file: ./comm/ethercat_sync.cpp
- file: ./comm/ethercat_can_bridge.h
- file: ./comm/ethercat_can_bridge.cpp
# 电机 OTA 的 EtherCAT 邮箱进料通路(CoE 0x2510/0x2511 → MotorOtaRelay 环缓)
- file: ./comm/ecat_motor_ota.h
- file: ./comm/ecat_motor_ota.cpp
- file: ./comm/ethercat_pdo.h
- file: ./comm/ethercat_pdo.cpp
# [DIFF-STATS] 临时仪表:差分写取证,删除时连同本两行与 comm/ethercat_diff_stats.* 一并移除
- file: ./comm/ethercat_diff_stats.h
- file: ./comm/ethercat_diff_stats.cpp
- group: comm/ssc
files:
- file: ./comm/ssc/inc/ecatslv.h
@@ -163,6 +175,9 @@ project:
- -DHXTAL_VALUE=8000000U
- -gdwarf-5
- -D__MICROLIB
# 启动文件经 legacy armasm 汇编:消 armclang 驱动"参数未使用"告警 + armasm 弃用告警(A1950W)
- -Wno-unused-command-line-argument
- -Wa,armasm,--diag_suppress=A1950W
Link:
- --library_type=microlib
- --entry=Reset_Handler
+11
View File
@@ -0,0 +1,11 @@
@echo off
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" >nul 2>&1
cd /d "G:\Robot_v2\DCU\DCU_APP\GD32E508_DCU_APP"
set PACKED=/D"STRUCT_PACKED_START=" /D"STRUCT_PACKED_END=" /D"MBX_STRUCT_PACKED_START=" /D"MBX_STRUCT_PACKED_END=" /D"OBJ_STRUCT_PACKED_START=" /D"OBJ_STRUCT_PACKED_END="
set INC=/I. /Icomm\ssc\inc /Icomm\ssc\port /IRTE\_Debug_GD32E508VE /I_syntaxcheck_stubs
echo === mailbox.c ===
cl /nologo /Zs /W0 /TC /std:c11 /utf-8 /Zp1 %PACKED% %INC% comm\ssc\src\mailbox.c
if errorlevel 1 (echo MAILBOX_FAIL) else (echo MAILBOX_OK)
echo === dc_app.c ===
cl /nologo /Zs /W0 /TC /std:c11 /utf-8 /Zp1 %PACKED% %INC% comm\ssc\port\dc_app.c
if errorlevel 1 (echo DC_APP_FAIL) else (echo DC_APP_OK)
@@ -0,0 +1 @@
/* syntax-check stub only: SSC C files never touch device registers directly */
+383
View File
@@ -0,0 +1,383 @@
/**
* @file ecat_motor_ota.cpp
* @brief EtherCAT 邮箱进料通路实现(职责与时序前提见 ecat_motor_ota.h)。
*
* 上下文:三个 SDO 钩子只在 MainLoop(ECAT_Application 调用链)里被调,泵 process()
* 也在主循环,因此 48KB 暂存区没有并发写者。真正的硬保证仍是通道锁:即便主站循环
* 被别的路径(页面自愈 / 守护重连)拉回来,运动帧也进不了正在升级的通道。
*/
#include "comm/ecat_motor_ota.h"
#include "sys/config.h"
#include "sys/motor_ota_relay.h"
#include "sys/ota_boot_request.h"
#include "sys/systick.h"
#include "spi_link_v2.h" /* v2_crc32:CRC32 MPEG-2(poly 0x04C11DB7,初值 0xFFFFFFFF,无反射、无末尾异或) */
#include <cstring>
#include <cstdio> /* LOG_ERR 展开为 printf(sys/config.h) */
namespace {
/*============================================================================
* 尺寸常量与守卫
*==========================================================================*/
constexpr uint32_t kChunkObjBytes = ECAT_MOTA_CHUNK_OBJ_BYTES;
constexpr uint32_t kChunkHdrBytes = ECAT_MOTA_CHUNK_HDR_BYTES;
constexpr uint32_t kImgMaxBytes = ECAT_MOTA_IMG_MAX_BYTES;
constexpr uint8_t kCtlSubs = ECAT_MOTA_CTL_SUBS;
static_assert(kImgMaxBytes == MotorOtaRelay::kMaxFwSize,
"暂存区上限必须与 MotorOtaRelay::kMaxFwSize 一致");
static_assert(kChunkObjBytes * 8U <= 0xFFFFU,
"0x2511 块长超出 SDO 单条目 BitSize(UINT16)上限");
static_assert(kChunkObjBytes > kChunkHdrBytes, "数据窗要留出净载荷");
/** APP 的堆容量(RTE/Device/GD32E508VE/regions_GD32E508VE.h 的 __HEAP_SIZE)。
* SSC 每笔分段下载都要 malloc(块长)(sdoserv.c:751),堆不够就从站直接回
* abort 0x05040005「Out of memory」——2026-10-11 就是拿 4096 撞出来的。
* 这里留 3 倍余量:谁要把块长往上抬,先抬 __HEAP_SIZE 再来改这一行。 */
constexpr uint32_t kHeapBytes = 3072U;
static_assert(kChunkObjBytes * 3U <= kHeapBytes,
"块长受 APP 堆限制:malloc(块长) 必须留足余量,否则从站 OOM 拒收");
/** 单次 process() 最多搬运的字节数:把一圈的占用钳住,别在 MainLoop 里长时间拷 48KB。 */
constexpr uint16_t kPumpSlice = 512U;
/*============================================================================
* 会话状态
*==========================================================================*/
uint32_t s_cmd = 0U; /**< sub1: 最近一次执行的命令 */
uint32_t s_total = 0U; /**< sub2: 声明的镜像总字节数 */
uint32_t s_channel = 0U; /**< sub3: 目标 CAN 通道 */
uint32_t s_node = 0U; /**< sub4: 目标节点 1..9 */
uint32_t s_img_crc = 0U; /**< sub5: 整镜像期望 CRC32 */
uint32_t s_staged = 0U; /**< 已连续暂存字节数(水位,写块时前移) */
uint8_t s_sess_err = ECAT_MOTA_OK;
bool s_armed = false; /**< 已声明总长,可收块 */
bool s_started = false; /**< 已交给 relay(暂存区冻结,只剩泵) */
bool s_crc_ok = false; /**< 整镜像 CRC 已核对通过 */
bool s_last_chunk_ok = false;
bool s_end_logged = false;/**< 终态日志只打一次 */
/* 终态锁存:relay 会话结束后可能很快回到 kIdle(主站下一拍才来读),
* 不做锁存就会把「刷成功了」读成 kIdle,主站据此报的结论就成了猜的。 */
bool s_final_valid = false;
uint8_t s_final_state = 0U;
uint8_t s_final_err = 0U;
uint32_t s_pump_pos = 0U; /**< 泵在暂存区里的读游标 */
/** 48KB 暂存区(.bss:启动零耗时;APP 当前 RAM 用量 35KB/128KB,放得下)。 */
uint8_t s_img[kImgMaxBytes];
inline uint32_t loadU32(const uint8_t *p) {
return static_cast<uint32_t>(p[0])
| (static_cast<uint32_t>(p[1]) << 8)
| (static_cast<uint32_t>(p[2]) << 16)
| (static_cast<uint32_t>(p[3]) << 24);
}
void resetSession()
{
s_armed = false;
s_started = false;
s_crc_ok = false;
s_staged = 0U;
s_pump_pos = 0U;
s_total = 0U;
s_last_chunk_ok = false;
s_end_logged = false;
s_final_valid = false;
s_final_state = 0U;
s_final_err = 0U;
}
/** 只认「已连续暂存水位之内」的偏移:留洞会让泵卡住,而且说明主站发料顺序错了。 */
uint8_t acceptChunk(uint32_t offset, uint32_t crc, const uint8_t *data, uint32_t len)
{
if (!s_armed || s_total == 0U) {
s_sess_err = ECAT_MOTA_E_NOTARMED;
return s_sess_err;
}
if (s_started) {
s_sess_err = ECAT_MOTA_E_STARTED;
return s_sess_err;
}
if (offset > s_staged || offset + len > s_total) {
s_sess_err = ECAT_MOTA_E_GAP;
return s_sess_err;
}
if (len > 0U) {
/* 块级校验:EtherCAT 数据报**头**有 CRC,载荷没有;邮箱分段拼装还是软件路径。
* 这里挡一次,坏块就地重发,不会等到电机回 0x06 才发现(那时 flash 已擦)。 */
if (v2_crc32(data, len) != crc) {
s_sess_err = ECAT_MOTA_E_CHUNKCRC;
s_last_chunk_ok = false;
return s_sess_err;
}
std::memcpy(&s_img[offset], data, len);
if (offset + len > s_staged) {
s_staged = offset + len;
}
}
s_last_chunk_ok = true;
s_sess_err = ECAT_MOTA_OK;
return ECAT_MOTA_OK;
}
/** 整镜像核对 + 交给 relay。所有失败路径都必须在电机进 BOOT **之前**。 */
bool beginTransfer()
{
if (s_total == 0U || s_total > kImgMaxBytes) {
s_sess_err = ECAT_MOTA_E_BADLEN;
return false;
}
if (s_staged != s_total) {
s_sess_err = ECAT_MOTA_E_GAP;
return false;
}
if (s_channel >= 3U || s_node < 1U || s_node > 9U) {
s_sess_err = ECAT_MOTA_E_PARAM;
return false;
}
/* DCU 自己正在回 BOOT(配置页写 + 复位在路上)时绝不能起电机会话:
* 复位会把半程的刷写腰斩在 erase 之后。 */
if (OtaBootRequest::isRebootPending()) {
s_sess_err = ECAT_MOTA_E_BOOTPEND;
return false;
}
/* 整镜像 CRC 先行:这一刻电机还在 APP 正常跑、擦除没开始,校验不过就是零代价失败。
* 反过来「先进 BOOT 再验」才是变砖路径。 */
if (v2_crc32(s_img, s_total) != s_img_crc) {
s_sess_err = ECAT_MOTA_E_IMGCRC;
return false;
}
s_crc_ok = true;
if (!MotorOtaRelay::start(static_cast<uint8_t>(s_channel), s_total,
MotorOtaRelay::FeedSource::kEcat,
static_cast<uint8_t>(s_node))) {
s_sess_err = ECAT_MOTA_E_BUSY;
s_crc_ok = false;
return false;
}
s_started = true;
s_end_logged = false;
s_pump_pos = 0U;
s_final_valid = false;
s_sess_err = ECAT_MOTA_OK;
LOG_ERR("[ECAT-MOTA] start ch=%lu node=%lu size=%lu crc=%08lX\r\n",
(unsigned long)s_channel, (unsigned long)s_node,
(unsigned long)s_total, (unsigned long)s_img_crc);
return true;
}
} /* namespace */
/*============================================================================
* EcatMotorOta(C++ 侧:主循环钩子与查询)
*==========================================================================*/
namespace EcatMotorOta {
void process()
{
if (!s_started) {
return;
}
const MotorOtaRelay::Status st = MotorOtaRelay::getStatus();
/* 电机侧自动重试会清环缓并把 fw_fed 归零:泵必须跟着回退,否则会把
* 「后半段」当成已喂过,喂出一个前段缺失的镜像。 */
if (st.fw_fed < s_pump_pos) {
s_pump_pos = st.fw_fed;
}
/* 泵:按环缓空位切片,搬不动就下一圈再来(不阻塞、不忙等、不碰 CAN)。 */
while (s_pump_pos < s_total) {
const uint16_t freeSpace = MotorOtaRelay::feedFreeSpace();
if (freeSpace == 0U) {
break;
}
const uint32_t remain = s_total - s_pump_pos;
uint16_t n = static_cast<uint16_t>(remain < kPumpSlice ? remain : kPumpSlice);
if (n > freeSpace) {
n = freeSpace;
}
if (!MotorOtaRelay::feedAt(s_pump_pos, &s_img[s_pump_pos], n)) {
break;
}
s_pump_pos += n;
}
if (!MotorOtaRelay::isActive()) {
if (!s_end_logged) {
s_end_logged = true;
s_final_valid = true;
s_final_state = static_cast<uint8_t>(st.state);
s_final_err = st.error_code;
LOG_ERR("[ECAT-MOTA] end state=%u err=0x%02X acked=%u/%u fed=%lu/%lu\r\n",
static_cast<unsigned>(st.state), static_cast<unsigned>(st.error_code),
static_cast<unsigned>(st.packets_acked),
static_cast<unsigned>(st.total_packets),
(unsigned long)st.fw_fed, (unsigned long)st.fw_size);
}
s_started = false;
s_crc_ok = false;
/* 会话已结束:不再收块。主站要开下一轮必须重新写 0x2510:02 声明总长,
* 那一步复位全部会话状态(包括上面的终态锁存)。 */
s_armed = false;
s_last_chunk_ok = false;
}
}
bool sessionActive()
{
return s_started;
}
} /* namespace EcatMotorOta */
/*============================================================================
* SDO 钩子(extern "C":dc_app.c 的对象表直接引用)
*==========================================================================*/
extern "C" {
uint8_t EcatMotorOtaChunkWrite(uint32_t dataSize, const uint8_t *bytes)
{
if (bytes == nullptr) {
return ECAT_MOTA_E_BADLEN;
}
if (dataSize < kChunkHdrBytes) {
s_sess_err = ECAT_MOTA_E_BADLEN;
return s_sess_err;
}
if (dataSize > kChunkObjBytes) {
s_sess_err = ECAT_MOTA_E_BADLEN;
return s_sess_err;
}
const uint32_t offset = loadU32(&bytes[0]);
const uint32_t crc = loadU32(&bytes[4]);
const uint32_t len = dataSize - kChunkHdrBytes;
const uint8_t rc = acceptChunk(offset, crc, &bytes[kChunkHdrBytes], len);
if (rc != ECAT_MOTA_OK) {
/* 一次失败只打一行:主站会重发同一块,串口刷屏会拖慢 MainLoop。 */
LOG_ERR("[ECAT-MOTA] chunk reject off=%lu len=%lu err=0x%02X\r\n",
(unsigned long)offset, (unsigned long)len, (unsigned)rc);
}
return rc;
}
uint8_t EcatMotorOtaCtlWrite(uint8_t subindex, uint32_t dataSize, const uint8_t *bytes)
{
if (bytes == nullptr || dataSize < 4U) {
return ECAT_MOTA_E_BADLEN;
}
const uint32_t v = loadU32(bytes);
switch (subindex) {
case 1U: {
/* sub1 cmd:1 = 校验并启动,3 = 中止会话 */
s_cmd = v;
if (v == 1U) {
if (!beginTransfer()) {
return s_sess_err;
}
} else if (v == 3U) {
if (s_started) {
MotorOtaRelay::cancel();
LOG_ERR("[ECAT-MOTA] cancel by master\r\n");
}
resetSession();
s_sess_err = ECAT_MOTA_OK;
} else {
return ECAT_MOTA_E_PARAM;
}
return ECAT_MOTA_OK;
}
case 2U: {
/* sub2 总长:声明即复位会话(主站重开一轮不需要先 abort) */
if (v == 0U || v > kImgMaxBytes) {
s_sess_err = ECAT_MOTA_E_BADLEN;
return s_sess_err;
}
if (s_started) {
s_sess_err = ECAT_MOTA_E_STARTED;
return s_sess_err;
}
resetSession();
s_total = v;
s_armed = true;
s_sess_err = ECAT_MOTA_OK;
return ECAT_MOTA_OK;
}
case 3U:
if (v >= 3U) {
s_sess_err = ECAT_MOTA_E_PARAM;
return s_sess_err;
}
s_channel = v;
return ECAT_MOTA_OK;
case 4U:
if (v < 1U || v > 9U) {
s_sess_err = ECAT_MOTA_E_PARAM;
return s_sess_err;
}
s_node = v;
return ECAT_MOTA_OK;
case 5U:
s_img_crc = v;
return ECAT_MOTA_OK;
default:
/* sub0 是条目数;sub6..sub11 只读 */
return ECAT_MOTA_E_PARAM;
}
}
uint32_t EcatMotorOtaCtlRead(uint8_t subindex)
{
const MotorOtaRelay::Status st = MotorOtaRelay::getStatus();
/* live = 会话还在跑,或从没跑过(那就照实反映 relay 当前状态);
* 会话刚结束时返回锁存值,避免主站把「成功」读成 kIdle。 */
const bool live = (s_started || !s_final_valid);
switch (subindex) {
case 0U: return kCtlSubs;
case 1U: return s_cmd;
case 2U: return s_total;
case 3U: return s_channel;
case 4U: return s_node;
case 5U: return s_img_crc;
case 6U: return s_staged;
case 7U: return live ? static_cast<uint32_t>(st.state)
: static_cast<uint32_t>(s_final_state);
case 8U: return st.packets_acked;
case 9U: return st.total_packets;
case 10U: return (static_cast<uint32_t>(s_sess_err) << 8)
| (live ? static_cast<uint32_t>(st.error_code)
: static_cast<uint32_t>(s_final_err));
case 11U: {
/* phase 位图:只报能被板子真实触发的事实,不做任何推测性状态。
* bit0 已声明总长 · bit1 暂存齐 · bit2 整镜像 CRC 过 · bit3 会话在跑
* bit4 上一块被接受 · bit5 本会话目标通道当前持锁 · bit6 relay 活跃 */
uint32_t phase = 0U;
if (s_armed) { phase |= 0x0001U; }
if (s_total != 0U && s_staged == s_total) { phase |= 0x0002U; }
if (s_crc_ok) { phase |= 0x0004U; }
if (s_started) { phase |= 0x0008U; }
if (s_last_chunk_ok) { phase |= 0x0010U; }
if (MotorOtaRelay::isChannelLocked(static_cast<uint8_t>(s_channel))) { phase |= 0x0020U; }
if (MotorOtaRelay::isActive()) { phase |= 0x0040U; }
return phase;
}
default: return 0U;
}
}
} /* extern "C" */
+97
View File
@@ -0,0 +1,97 @@
/**
* @file ecat_motor_ota.h
* @brief EtherCAT(CoE 邮箱)电机 OTA 进料通路 —— 主站把电机固件灌进 DCU 本地暂存区,
* 再由本模块泵进 MotorOtaRelay 现有的 4KB 环缓。
*
* 为什么要有它(不是重复造轮子):电机 OTA 的引擎一直是 MotorOtaRelay
* (AES 握手 + CAN 停等 + 单发模式),只有一条进料路:ESP32 经 SPI 0xD1 喂。
* 纯 EtherCAT 场合(只插网线、ESP32 WiFi 关断)那条 HTTP 打不通,
* 于是前端把能力标成 supportsOta=false。本模块只换**进料口**,不换引擎:
* - 分块写入仍走 MotorOtaRelay::feedAt(),offset/幂等/环缓满的判据只有一份;
* - 加密、停等、重发、终态判定全部沿用原状态机;
* - 通道独占锁同样沿用(EtherCAT 自己的 PDO 运动帧也被它挡在 OTA 之外)。
*
* 与 BOOT 的 0x2500/0x2501(刷 DCU 自身)的区别:那边落 flash,这边只落 SRAM 暂存区,
* 全程不碰 fmc_*,因此不需要关中断擦写那套优雅关停。
*
* 对象(APP 字典,表项与钩子包装在 comm/ssc/port/dc_app.c):
* 0x2510 会话控制(REC of UINT32,读写钩子返回实时值)
* 0x2511 数据窗(VAR OCTETSTRING,4096B,载荷 [offset:u32 LE][crc32:u32 LE][data...])
*
* 本头刻意只用 <stdint.h> 类型:dc_app.c 是 C 且靠相对路径取 ecat_def.h,
* 而 comm/ 下的 C++ 拿不到那套 SSC 类型。SDO abort 码的映射留在 dc_app.c 那一层。
*/
#ifndef COMM_ECAT_MOTOR_OTA_H
#define COMM_ECAT_MOTOR_OTA_H
#include <stdint.h>
/**
* ── 尺寸常量(宏:C 与 C++ 两侧共用同一真源) ──
* 0x2511 对象字节数上限,含 8 字节自描述头。
* 单条目 BitSize 是 UINT16 ⇒ 8191 字节是天花板,但**真正的上限是 APP 的堆**:
* SSC 的分段下载缓冲是 `ALLOCMEM(ROUNDUPBYTE2WORD(size))` 即 malloc(size)
* (comm/ssc/src/sdoserv.c:751,池不够就回 abort 0x05040005 Out of memory),
* 而 APP 的 `__HEAP_SIZE` 只有 0xC00=3072B(BOOT 是 0x4000,所以 BOOT 的
* 0x2501 能用 4096 —— 照抄块长不照抄堆就会当场失败,2026-10-11 实测)。
* 取 1024:48KB 镜像 = 47 块,每块 1 initiate + 4 段 ≈ 5 个邮箱帧,
* 总邮箱流量仍在亚秒级,且给 malloc 留出 3 倍余量。
* 改这里必须同步 tools/ecat/ecat_native.c 的 MFO_CHUNK_OBJ(两边各自断言不了对方)。
*/
#define ECAT_MOTA_CHUNK_OBJ_BYTES 1024U
#define ECAT_MOTA_CHUNK_HDR_BYTES 8U
/** 电机固件上限:必须与 MotorOtaRelay::kMaxFwSize(48KB)一致,C++ 侧有 static_assert 守着。 */
#define ECAT_MOTA_IMG_MAX_BYTES 0xC000U
/** 0x2510 的子索引个数(sub1..sub11)。 */
#define ECAT_MOTA_CTL_SUBS 11U
/**
* ── 会话结果码 ──
* 与 MotorOtaRelay 的协议错误码(0x01..0x3x,来自电机 BOOT)刻意分段:
* 0x8x 永远是「DCU 本端把料挡了」,不是电机报错。看到 0x8x 查链路与参数,
* 看到 0x0x/0x1x/0x3x 查电机与 CAN。同一份码既写进 0x2510:10 高字节(主站可读),
* 也作为钩子的返回值(dc_app.c 再换成 SDO abort 码)。
*/
#define ECAT_MOTA_OK 0x00U
#define ECAT_MOTA_E_BADLEN 0x81U /* 声明总长或块长非法 */
#define ECAT_MOTA_E_CHUNKCRC 0x82U /* 块 CRC32 不符:邮箱搬坏了,主站重发该块 */
#define ECAT_MOTA_E_GAP 0x83U /* 块偏移越界/留洞:主站发料顺序错 */
#define ECAT_MOTA_E_IMGCRC 0x84U /* 整镜像 CRC32 不符:拒绝进 BOOT,一个字节都没发 */
#define ECAT_MOTA_E_BUSY 0x85U /* relay 已在跑(另一条链路或上一会话未结束) */
#define ECAT_MOTA_E_NOTARMED 0x86U /* 没声明总长就写数据窗 */
#define ECAT_MOTA_E_STARTED 0x87U /* 已 start,暂存区冻结,不再收块 */
#define ECAT_MOTA_E_PARAM 0x88U /* 通道/节点参数非法 */
#define ECAT_MOTA_E_BOOTPEND 0x89U /* DCU 自己正在交接回 BOOT,此时不许刷电机 */
#ifdef __cplusplus
namespace EcatMotorOta {
/** 主循环每圈调用:把暂存区泵进环缓,并在会话结束时收标志。 */
void process(void);
/** 本模块的会话是否活跃(已 start 且 relay 未进终态)。 */
bool sessionActive(void);
} /* namespace EcatMotorOta */
#define ECAT_MOTA_LINKAGE extern "C"
#else
#define ECAT_MOTA_LINKAGE extern
#endif /* __cplusplus */
/*---------------------------------------------------------------------------
* SDO 钩子(dc_app.c 的对象表引用这三个,实现在 ecat_motor_ota.cpp)
*
* 返回 ECAT_MOTA_OK 或上面的 ECAT_MOTA_E_*;dc_app.c 把非 0 换成 SDO abort 码,
* 主站既拿得到 abort,也能从 0x2510:10 读到具体是哪一条,不用猜。
*---------------------------------------------------------------------------*/
/** @brief 0x2510 写入:sub1=cmd(1=start / 3=abort)、sub2=总长、sub3=通道、sub4=node、sub5=整镜像 CRC32。 */
ECAT_MOTA_LINKAGE uint8_t EcatMotorOtaCtlWrite(uint8_t subindex, uint32_t dataSize,
const uint8_t *bytes);
/** @brief 0x2510 读取:返回该 sub 的实时值(进度不落地缓存,避免读到旧数)。 */
ECAT_MOTA_LINKAGE uint32_t EcatMotorOtaCtlRead(uint8_t subindex);
/** @brief 0x2511 写入:一块镜像(自描述 offset + 块 CRC32),失败即 SDO abort。 */
ECAT_MOTA_LINKAGE uint8_t EcatMotorOtaChunkWrite(uint32_t dataSize, const uint8_t *bytes);
#endif /* COMM_ECAT_MOTOR_OTA_H */
+15 -15
View File
@@ -114,6 +114,10 @@ bool EscExmcBus::init() {
rcu_periph_clock_enable(RCU_GPIOE);
rcu_periph_clock_enable(RCU_AF);
rcu_periph_clock_enable(RCU_EXMC);
/* 先 deinit 再 init。BOOT 跳转前 exmc_norsram_disable + 关时钟,
* SNCTL/SNTCFG 仍留着 BOOT 的配置;APP 若只 enable+init 可能叠在残留位上。
* deinit 把 Region0 打回复位默认,再按下面参数重铺。 */
exmc_norsram_deinit(CONFIG_ESC_EXMC_BANK_NOR_SRAM_REGION0);
/* 配置 16-bit 数据线 D0-D15 为 AF 推挽 */
gpio_init(CONFIG_EXMC_D0_PORT, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ,
CONFIG_EXMC_D0_PIN);
@@ -196,19 +200,15 @@ bool EscExmcBus::init() {
norsram_init.memory_type = EXMC_MEMORY_TYPE_SRAM;
norsram_init.address_data_mux = DISABLE;
/* 读/写共用时序(非 extended mode)
* 2026-08-20 慢档测试(16/16/256 HCLK)无效,已还原:慢档时序未带来任何
* 读数变化,速度因素彻底排除(与 bit-bang 40-80us 仍失败一致)。
* 注:那轮测试的失败根因是访问模型错误(直读核心 CSR 索引空间,1.5.39 修复),
* 与时序无关;时序余量本身仍需按 DS 表 9-4/9-5 独立核算,见下方 BUSLAT。 */
/* 读/写共用时序(非 extended mode)。时序余量按 DS 表 9-4/9-5 独立核算,见下方 BUSLAT。 */
rw_timing.asyn_access_mode = EXMC_ACCESS_MODE_A;
rw_timing.syn_data_latency = EXMC_DATALAT_2_CLK;
rw_timing.syn_clk_division = EXMC_SYN_CLOCK_RATIO_DISABLE;
/* BUSLAT: 1→4 (1.5.57)。DS 表 9-4/9-5 要求 trdrd/twrwr/twrrd ≥ 13ns,
/* BUSLAT: 4。DS 表 9-4/9-5 要求 trdrd/twrwr/twrrd ≥ 13ns,
* 该间隔由 EXMC 在两次访问之间插入的总线空闲期承担 = (1 固有 + BUSLAT) HCLK。
* BUSLAT=1 → 2 HCLK ≈ 11.1ns,低于 13ns 下限,仅靠 AHB 取指间隙偶然兜底:
* 常温刚好可用,温度升高(传播延迟增大)/电压下降(驱动减弱)即越界,表现为
* 偶发读到 0xFFFF 或错值——与实测的温漂偶发读错吻合。
* 偶发读到 0xFFFF 或错值。
* BUSLAT=4 → 5 HCLK ≈ 27.8ns,余量 2.1 倍,覆盖全温区与电压波动。
* 代价:每次 HBI 访问多 3 HCLK(16.7ns)。满载 1024B 过程数据按每 DWORD
* 两次 16-bit 访问计,增加约 256×2×16.7ns ≈ 8.6us,相对原 218us 增幅 3.9%,
@@ -243,7 +243,7 @@ bool EscExmcBus::init() {
ECAT_LOGI("[ECAT][EXMC] init: SNCTL0 fix: before=0x%08lX after=0x%08lX (bit7 reserved cleared)\r\n",
(unsigned long)snctl_before, (unsigned long)snctl_after);
/* 时序寄存器实测回读:确认写入的 BUSLAT 真正落到硬件(1.5.57)。
/* 时序寄存器回读:确认写入的 BUSLAT 真正落到硬件。
* SNTCFG0 位域: [3:0]=ASET-1 [7:4]=AHLD-1 [15:8]=DSET-1 [19:16]=BUSLAT-1。
* 期望 ASET=4/AHLD=2/DSET=20/BUSLAT=4 → 0x0313_0000|... 逐字段解码打印,
* 避免只看整字导致的位域误读;BUSLAT 实际值 <4 说明库或硬件截断,须立即排查。 */
@@ -271,8 +271,8 @@ bool EscExmcBus::init() {
"ESC driver\r\n");
/* ---- 硬件诊断: 转储关键寄存器 + GPIO 手动输出测试 ----
* 1.5.33: 默认关闭。硬件层已确认全通,探针仅换板/补焊排查浮空时经
* CONFIG_ESC_EXMC_DIAG 临时开启(会拆 EXMC 引脚成 GPIO,正常运行不该跑)。 */
* 默认关闭。硬件层已确认全通,探针仅换板/补焊排查浮空时经
* CONFIG_ESC_EXMC_DIAG 临时开启(会拆 EXMC 引脚成 GPIO,正常运行不该跑)。 */
#if CONFIG_ESC_EXMC_DIAG
diagDump();
#endif
@@ -284,8 +284,8 @@ bool EscExmcBus::init() {
/* ========================================================================
* 硬件诊断: 精简版 —— 只保留核心探活 + bit-bang + 位级回显
* 1.5.33: 整段由 CONFIG_ESC_EXMC_DIAG 门控,默认不编译。bbDelay/bbReadData/
* bbDriveData 等位拍辅助函数在上方匿名 namespace 中,continuityDrive 仍复用,保留。
* 整段由 CONFIG_ESC_EXMC_DIAG 门控,默认不编译。bbDelay/bbReadData/
* bbDriveData 等位拍辅助函数在上方匿名 namespace 中,continuityDrive 仍复用,保留。
* ======================================================================== */
#if CONFIG_ESC_EXMC_DIAG
void EscExmcBus::diagDump() {
@@ -301,7 +301,7 @@ void EscExmcBus::diagDump() {
return;
}
printf("[EXMC-DIAG] --- bit-bang probe (bypass EXMC) ---\r\n");
/* NRST 实际电平回读(1.5.26):输入通路常连引脚,输出态亦读真实电压。
/* NRST 实际电平回读:输入通路常连引脚,输出态亦读真实电压。
* 驱动高却 LOW => ECAT_NRST 被外部按低 => LAN9252 一直在复位态。 */
printf("[EXMC-DIAG] NRST(PB10) reads %s\r\n",
(gpio_input_bit_get(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN) == SET)
@@ -459,7 +459,7 @@ static void continuityEnterTestMode() {
s_save_ctl0_e = GPIO_CTL0(GPIOE);
s_save_ctl1_e = GPIO_CTL1(GPIOE);
/* 测试期屏蔽 SYNC0/ESC IRQ/SYNC1:ISR 的 HBI 访问落在已被切成 GPIO 态的
* 总线上只会读回垃圾并错写 IDX,SSC 状态可能被污染(1.5.29)。 */
* 总线上只会读回垃圾并错写 IDX,SSC 状态可能被污染。 */
s_maskPrev = EscExmcBus::maskEnter();
/* 全部数据线 + 控制线设为 OUT_PP */
gpio_init(GPIOD, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
@@ -571,7 +571,7 @@ uint32_t EscExmcBus::maskEnter() {
/* EXTI0=SYNC0(preempt=0) 无法被 BASEPRI 屏蔽,只能关 NVIC;
* EXTI1=ESC IRQ / EXTI2=SYNC1 (均 preempt=1) 的 PDI_Isr/Sync1_Isr 同样做
* HBI 两步事务访问,一并屏蔽——否则主循环事务会被 Sync1_Isr 的
* PDO_InputMapping→HW_EscWriteIsr 打断而错址(1.5.29 修复)。 */
* PDO_InputMapping→HW_EscWriteIsr 打断而错址。 */
if (NVIC_GetEnableIRQ(EXTI0_IRQn) != 0U) {
prev |= 1U;
NVIC_DisableIRQ(EXTI0_IRQn);
+355 -64
View File
@@ -7,19 +7,17 @@
* - A2=1,A1=0 (偏移 0x40000) -> HBI_DATA_0 低 16 位
* - A2=1,A1=1 (偏移 0x60000) -> HBI_DATA_0 高 16 位
*
* 地址线换算(1.5.27 修正,此前 0x20000/0x30000 是按 8-bit 规则算的,DATA 访问全部
* 落在 HBI_IDX_0 高半字,IDX_0=0x1234 探针假通过而 BYTE_TEST 永远失败):
* 地址线换算(16-bit 数据宽度时 EXMC 内部将 HADDR[25:1] 接到 EXMC_A[24:0]):
* 板线:LAN9252 A1(Pin25)→PD11=EXMC_A16,A2(Pin29)→PD12=EXMC_A17。
* GD32E50x 用户手册 p869:16-bit 数据宽度时 EXMC 内部将 HADDR[25:1] 接到
* EXMC_A[24:0],即 EXMC_A16=CPU 偏移 bit17(0x20000)、EXMC_A17=bit18(0x40000)。
* LAN9252 手册 Table 9-3:A2=1 选 HBI_DATA_0,A1 选低/高半字。
*
* 访问模型(1.5.39 按 DS 12.11/表 12-13 修正):
* 访问模型(DS 12.11/表 12-13):
* HBI 索引直接寻址的只有 LAN9252 系统 CSR(BYTE_TEST 0x064 等)与访问端口;
* EtherCAT 核心 CSR(0x0000-0x0FFF)经 ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300)
* 间接访问,过程 RAM(0x1000-0x1FFF)经 PRAM FIFO(0x000-0x03C + 0x308-0x314) 访问。
* 1.5.37 及之前把索引空间当统一寻址,导致全部核心 CSR 读 0/写丢弃——
* 主站恒读 AL=INIT 的真正根因(网络侧寄存器诊断证实 EEPROM/核心一直正常)。
* 核心 CSR 不在 HBI 直接空间,直读恒 0、写丢弃。
*/
#include "comm/esc_lan9252.h"
#include "comm/esc_exmc_bus.h"
@@ -59,7 +57,7 @@ constexpr uint32_t IRQ_CFG_IRQ_TYPE = 1UL << 0; /**< 1=推挽,0=开漏(开漏
constexpr uint32_t INT_EN_ECAT_INT = 1UL << 0; /**< 允许 EtherCAT 事件驱动 IRQ 引脚。 */
/* ESC 过程数据看门狗(DS 12.14.41-43)。SII 不携带看门狗字段,芯片复位默认
* divider=0x09C2(100µs)/WD Time PD=0x03E8(1000) 即 100ms——低于实测可用的
* divider=0x09C2(100µs)/WD Time PD=0x03E8(1000) 即 100ms——等于
* 100ms 手动调试周期,SM2 每 100ms 被判定超时,SSC 以 0x001B 拖回 SAFEOP。 */
constexpr uint16_t ESC_REG_WD_DIVIDER = 0x0400U; /**< 看门狗分频(100µs 基准,复位默认 0x09C2)。 */
constexpr uint16_t ESC_REG_WD_TIME_PD = 0x0420U; /**< 过程数据看门狗步数(复位默认 0x03E8=100ms)。 */
@@ -80,6 +78,28 @@ constexpr uint32_t PRAM_BUSY = 1UL << 31; /**< PRAM_*_CMD:写 1 启动,完成
constexpr uint32_t PRAM_AVAIL = 1UL << 0; /**< PRAM_*_CMD:数据可用(读)/空间可用(写) */
constexpr uint32_t POLL_MAX = 2000UL; /**< busy/avail 轮询上限(~ms 级,防芯片异常时死等) */
/* 电源管理(DS 6.3/表 5-1):芯片级睡眠 D1-D3。
* READY=0 期间仅 HW_CFG/PMT_CTRL/BYTE_TEST/RESET_CTL 四个寄存器可访问(DS 6.2.3),
* 恰好覆盖睡眠进入与唤醒流程所需;PMT_CTRL 可在睡眠态安全读(DS 6.3.5.1 注)。 */
constexpr uint16_t LAN9252_REG_HW_CFG = 0x0074U; /**< 硬件配置:bit27=READY(与 PMT_CTRL bit0 同源,DS 6.3.5.1)。 */
constexpr uint16_t LAN9252_REG_PMT_CTRL = 0x0084U; /**< 功耗管理控制:bit0=READY,bit28=PM_SLEEP_EN,bit31:29=PM_MODE。 */
constexpr uint32_t PMT_CTRL_READY = 1UL << 0;
constexpr uint32_t PMT_CTRL_PM_MODE_MSK = 7UL << 29; /**< 001=D1 010=D2 011=D3。 */
constexpr uint32_t PMT_CTRL_PM_SLEEP_EN = 1UL << 28; /**< 置 1 进入 PM_MODE 指定的睡眠档(须与 PM_MODE 分两次写)。 */
/* ESC 核心 MII 管理(ETG.1000.4 ESC 寄存器 0x0510-0x0514,经 csrRead/csrWrite 间接访问;
* 表 5-1 证实 PHY 寄存器不在 HBI 直接空间)。用于 D3 前置的 PHY General Power Down
* (DS 6.3.4:D3 要求两 PHY 先 General Power Down)与唤醒后恢复。 */
constexpr uint16_t ESC_REG_MII_CTRL = 0x0510U; /**< bit0=CMD(写 1 启动自清) bit1=WRITE bit8=BUSY。 */
constexpr uint16_t ESC_REG_MII_ADDR = 0x0512U; /**< bit4:0=PHY_ADDR bit15:5=REG_ADDR。 */
constexpr uint16_t ESC_REG_MII_DATA = 0x0514U; /**< bit15:0=MII 数据。 */
constexpr uint16_t MII_CTRL_CMD = 1U << 0;
constexpr uint16_t MII_CTRL_WRITE = 1U << 1;
constexpr uint16_t MII_CTRL_BUSY = 1U << 8;
constexpr uint8_t MII_REG_BMCR = 0U; /**< IEEE 802.3 Basic Control 寄存器 0。 */
constexpr uint16_t MII_BMCR_POWER_DOWN = 1U << 11; /**< PHY_PWR_DWN(DS 11.2.8)。 */
constexpr uint8_t MII_PHY_ADDR_B = 1U; /**< PHY B 地址恒 1(DS 11.1.1);A 模式相关 0/2,运行期判定。 */
__attribute__((section(".ramfunc"), noinline))
void setHbiIndex(uint16_t addr)
{
@@ -126,18 +146,93 @@ void writeReg32(uint16_t addr, uint32_t v)
writeDataLow((uint16_t)(v & 0xFFFFU));
writeDataHigh((uint16_t)(v >> 16));
}
/* ---- 电源管理 helper(仅 pmDrive 诊断路径使用) ---- */
/** @brief DWT CYCCNT 忙等微秒。
* @note EXTI ISR 上下文(0xA2 在 AppSpiCommand::onCsRelease 内同步执行)禁止用
* SysTickTimer::delayMs:其基于 SysTick 中断递增 s_ms,中断被 CS EXTI
* 压住时计数冻结会死等。DWT CYCCNT 已由 init() 使能,纯寄存器读,ISR 安全。 */
__attribute__((section(".ramfunc"), noinline))
void pmBusyWaitUs(uint32_t us)
{
const uint32_t start = DWT->CYCCNT;
const uint32_t cycles = us * (SystemCoreClock / 1000000U);
while ((DWT->CYCCNT - start) < cycles) { }
}
/** @brief ESC 核心 MII 管理事务(读/写一个 PHY 寄存器)。
* @note 调用方必须已持 HBI 互斥(pmDrive 的 maskEnter 段内)。MII 事务典型 ~25µs
* (2.5MHz MDIO × 32bit 帧结构),BUSY 轮询上限 POLL_MAX 防芯片异常死等。 */
bool miiXfer(uint8_t phyAddr, uint8_t regAddr, bool write, uint16_t &data)
{
EscLan9252::csrWrite(ESC_REG_MII_ADDR, ((uint32_t)regAddr << 5) | phyAddr, 2U);
if (write) {
EscLan9252::csrWrite(ESC_REG_MII_DATA, data, 2U);
}
EscLan9252::csrWrite(ESC_REG_MII_CTRL,
write ? (MII_CTRL_CMD | MII_CTRL_WRITE) : MII_CTRL_CMD, 2U);
for (uint32_t i = 0U; i < POLL_MAX; i++) {
if ((EscLan9252::csrRead(ESC_REG_MII_CTRL, 2U, false) & MII_CTRL_BUSY) == 0U) {
if (!write) {
data = (uint16_t)EscLan9252::csrRead(ESC_REG_MII_DATA, 2U, false);
}
return true;
}
}
return false;
}
/** @brief 置/清两 PHY 的 BMCR.PowerDown(D3 进入前置要求/唤醒后恢复)。
* @return 全部事务成功 true。PHY A 地址模式相关(DS 11.1.1:0 或 2,随 chip_mode_strap),
* 以 BMCR 读回有效性判定(0x0000/0xFFFF=无响应),结果缓存在静态变量。 */
bool phySetPowerDown(bool down)
{
static uint8_t addrA = 0xFFU; /* 0xFF=未判定 */
if (addrA == 0xFFU) {
addrA = 0U;
uint16_t probe = 0xFFFFU;
if (!miiXfer(0U, MII_REG_BMCR, false, probe) ||
probe == 0x0000U || probe == 0xFFFFU) {
addrA = 2U;
}
}
bool ok = true;
const uint8_t addrs[2] = { addrA, MII_PHY_ADDR_B };
for (uint8_t i = 0U; i < 2U; i++) {
uint16_t bmcr = 0U;
ok = miiXfer(addrs[i], MII_REG_BMCR, false, bmcr) && ok;
if (down) {
bmcr |= MII_BMCR_POWER_DOWN;
} else {
bmcr &= (uint16_t)~MII_BMCR_POWER_DOWN;
}
ok = miiXfer(addrs[i], MII_REG_BMCR, true, bmcr) && ok;
}
return ok;
}
/** @brief 把 PMT_CTRL 值打包成单字节状态:bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE。 */
uint8_t pmPackStatus(uint32_t pmt)
{
return (uint8_t)(((pmt & PMT_CTRL_READY) ? 0x01U : 0U) |
((pmt & PMT_CTRL_PM_SLEEP_EN) ? 0x02U : 0U) |
(((pmt >> 29) & 3U) << 2));
}
} /* anonymous namespace */
bool EscLan9252::s_busReady = false;
bool EscLan9252::s_coreReady = false;
bool EscLan9252::s_resetDone = false;
bool EscLan9252::s_pmPhyDown = false;
bool EscLan9252::s_pmSuspended = false;
void EscLan9252::hardwareReset()
{
/* 受控复位 ECAT_NRST(PB10 → LAN9252 Pin11 RST#,SCH 第1页 Pin47)。
* 2026-08-22(1.5.25) 前固件从未初始化 PB10,RST# 仅靠 R44 10k 上拉自然释放,
* 不满足手册 18.6.3:tpurstd(电源稳定→RST#释放)≥25ms、trstia(低脉宽)≥200µs,
* 且要求释放前时钟输入已稳定。固件执行到此点时上电早已完成、晶振已稳定,
* 手册 18.6.3 要求 tpurstd(电源稳定→RST#释放)≥25ms、trstia(低脉宽)≥200µs,
* 且释放前时钟输入已稳定。固件执行到此点时上电早已完成、晶振已稳定,
* 统一 30ms 低脉宽同时覆盖两个下限。RST# 释放瞬间重锁 CHIP_MODE0/1 strap
* (LED_LINK1/2 经 R35/R36 10k 下拉 → 00b = 2 端口 HBI 模式)。
* 释放后保持推挽输出高:空闲电平确定,并保留运行期软件复位手段;
@@ -148,7 +243,10 @@ void EscLan9252::hardwareReset()
gpio_bit_reset(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN); /* RST# 拉低 */
SysTickTimer::delayMs(CONFIG_ESC_NRST_ASSERT_MS);
gpio_bit_set(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN); /* 释放复位 */
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
/* 释放后等 50ms:warm jump 后芯片不是冷上电,内部状态机/PHY
* 重锁比 POR 慢;等太短时随后的 HBI 探针会读到启动中垃圾值。
* 50ms 仍远小于 300ms 轮询窗,且 init 单次成本可接受。 */
SysTickTimer::delayMs(50U);
/* 回读实际电平:GD32 输入通路常连引脚,输出态也能读到真实电压。
* 驱动高却读回低 => ECAT_NRST 网络被外部短路到地(或引脚损伤),
* LAN9252 被按死在复位态 —— 直接解释 HBI 全浮空 + 晶振照振。 */
@@ -157,6 +255,8 @@ void EscLan9252::hardwareReset()
printf("[ECAT][ESC] hardwareReset: RST# low %ums -> released, pin reads %s\r\n",
(unsigned)CONFIG_ESC_NRST_ASSERT_MS,
pinHigh ? "HIGH" : "*** LOW (net shorted to GND / pin damaged) ***");
/* RST# 复位清掉 PM 睡眠态(芯片回 D0),挂起门控随之解除 */
s_pmSuspended = false;
}
uint8_t EscLan9252::nrstDrive(uint8_t action)
@@ -169,6 +269,8 @@ uint8_t EscLan9252::nrstDrive(uint8_t action)
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
CONFIG_ESC_NRST_PIN);
gpio_bit_set(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN);
/* 复位释放后芯片重走上电流程回 D0,睡眠挂起随之解除 */
s_pmSuspended = false;
break;
case 1: /* 持续拉低(手动保持复位) */
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
@@ -178,6 +280,7 @@ uint8_t EscLan9252::nrstDrive(uint8_t action)
case 3: /* 释放:浮空输入,网络交给 R44 10k 上拉 */
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_IN_FLOATING, 0,
CONFIG_ESC_NRST_PIN);
s_pmSuspended = false;
break;
case 4: /* 只读回,不改驱动 */
break;
@@ -193,71 +296,231 @@ uint8_t EscLan9252::nrstDrive(uint8_t action)
return level ? 1U : 0U;
}
uint8_t EscLan9252::pmDrive(uint8_t op)
{
if (op > 4U) {
return 0xEEU;
}
if (!s_busReady) {
printf("[ECAT][PM] op%u rejected: HBI bus not ready\r\n", op);
return 0xE1U;
}
/* 0xA2 在 CS EXTI ISR 上下文执行(AppSpiCommand::onCsRelease 同步调用)。
* HBI Indexed 是两步事务(写 IDX→读写 DATA),而 SYNC0(EXTI0,preempt=0)是
* 全系统最高优先级、BASEPRI 挡不住,与主循环 HW_* 路径相同必须关 NVIC:
* maskEnter 屏蔽 EXTI0/EXTI1 并返回先前状态,ISR 里调用同样正确(纯 NVIC
* 寄存器保存/恢复,挂起的中断在 maskExit 后补执行)。
* 残余竞态窗口:CS EXTI 优先级更高,可抢占主循环持锁中的 HBI 事务半截——
* 人工低频诊断可接受(continuityDrive 直接切走 EXMC 引脚破坏性更大)。 */
const uint32_t mask = EscExmcBus::maskEnter();
uint8_t status;
if (op == 0U) {
/* 状态查询。注意:HBI 读事务本身就会唤醒睡眠中的芯片,
* 本操作的语义 = "读状态(若在睡眠则主动打破睡眠)",非无侵入观测。 */
const uint32_t pmt = readReg32(LAN9252_REG_PMT_CTRL);
status = pmPackStatus(pmt);
/* 读到 D0(SLEEP_EN=0):芯片不在睡眠,若门控仍挂起(如 op4 唤醒超时后
* 芯片延迟自醒)则自愈解除,恢复主循环/ISR 的 HBI 访问。 */
if ((status & 0x02U) == 0U && s_pmSuspended) {
s_pmSuspended = false;
printf("[ECAT][PM] status: chip in D0, pm suspend gate released\r\n");
}
printf("[ECAT][PM] status: PMT_CTRL=0x%08lX READY=%u SLEEP_EN=%u MODE=D%u\r\n",
(unsigned long)pmt,
(unsigned)((status >> 0) & 1U), (unsigned)((status >> 1) & 1U),
(unsigned)((status >> 2) & 3U));
} else if (op <= 3U) {
/* 进入 D1/D2/D3。D3 最深(晶振/PLL 全关,仅手动唤醒),手册要求先把两个 PHY
* 置 General Power Down,否则 PLL 在 D2 不真关/D3 直接不满足前置。 */
if (op == 3U) {
s_pmPhyDown = phySetPowerDown(true);
printf("[ECAT][PM] D3 pre: both PHY power-down %s\r\n",
s_pmPhyDown ? "OK" : "FAIL (entering anyway)");
}
uint32_t v = readReg32(LAN9252_REG_PMT_CTRL);
v = (v & ~PMT_CTRL_PM_MODE_MSK) | ((uint32_t)op << 29);
/* PM_MODE 与 PM_SLEEP_EN 必须分两次写(DS 6.3.5.1 注:同一写中改 MODE 置 EN 无效)。 */
writeReg32(LAN9252_REG_PMT_CTRL, v);
writeReg32(LAN9252_REG_PMT_CTRL, v | PMT_CTRL_PM_SLEEP_EN);
/* 立即挂起全部 HBI 消费者(task_runner 停 MainLoop/守护/心跳,
* ethercat_sync 三 ISR 清 pending 后直接返回)——本函数运行于 CS EXTI ISR,
* 退出后主循环第一圈 ECAT_Main 读 0x220 就会把刚睡着的芯片叫醒;
* 此前置位窗口内本 ISR 是唯一运行者(maskEnter 已屏 EXTI0/1),无竞态。 */
s_pmSuspended = true;
/* 进入睡眠后任何 HBI 访问(含读!)都会立即触发唤醒——
* 手册 6.3.4.2 只说 HBI "write" 唤醒,但芯片实际按主机 CS 活动判定,
* 写入后的读回验证会把芯片叫回 D0。
* 故此处置信构造返回值,不再读回;睡眠态观测交给"读状态"(读即唤醒,
* 语义=主动打破睡眠),页面轮询须在睡眠态暂停。 */
status = (uint8_t)(0x02U | ((uint32_t)op << 2)); /* READY=0 | SLEEP_EN=1 | MODE=op */
static const char *const kMode[4] = {"D0", "D1", "D2", "D3"};
printf("[ECAT][PM] enter %s: entered, no read-back (HBI read wakes chip)\r\n",
kMode[op]);
printf("[ECAT][PM] WARNING: EtherCAT comms halted, wake with 0xA2 action 34\r\n");
} else {
/* 手动唤醒(DS 6.3.4.2):对 BYTE_TEST 做一次 HBI 写触发,随后轮询
* BYTE_TEST=0x87654321(接口恢复)→ PMT_CTRL.READY=1(回 D0 且 PLL 稳定)。
* 手册标称 ≤2ms,轮询 50µs 步进、上限 10ms——EXTI ISR 内不做长阻塞。 */
writeReg32(LAN9252_REG_BYTE_TEST, LAN9252_BYTE_TEST_VALUE);
bool awake = false;
for (uint32_t i = 0U; i < 200U; i++) {
pmBusyWaitUs(50U);
if (readReg32(LAN9252_REG_BYTE_TEST) == LAN9252_BYTE_TEST_VALUE) {
awake = true;
break;
}
}
if (awake) {
for (uint32_t i = 0U; i < 200U; i++) {
pmBusyWaitUs(50U);
if ((readReg32(LAN9252_REG_PMT_CTRL) & PMT_CTRL_READY) != 0U) {
break;
}
}
awake = (readReg32(LAN9252_REG_PMT_CTRL) & PMT_CTRL_READY) != 0U;
}
if (awake && s_pmPhyDown) {
/* D3 唤醒后恢复 PHY 上电,链路重新自协商(数秒后 LINK 恢复)。 */
const bool restored = phySetPowerDown(false);
s_pmPhyDown = !restored;
printf("[ECAT][PM] wake: PHY power-up %s (autoneg re-negotiating)\r\n",
restored ? "OK" : "FAIL (retry action 34)");
}
if (awake) {
/* 唤醒成功:解除 HBI 挂起,主循环/ISR 恢复运行。
* 超时则保持挂起——芯片大概率仍睡着;若实际已醒,下一次 op0
* 状态查询读到 D0 会自愈解除。 */
s_pmSuspended = false;
}
status = awake ? pmPackStatus(readReg32(LAN9252_REG_PMT_CTRL)) : 0U;
printf("[ECAT][PM] wake: %s -> status=0x%02X (READY=%u SLEEP_EN=%u)\r\n",
awake ? "OK" : "TIMEOUT (10ms, poll action 30 / retry 34)",
status, (unsigned)(status & 1U), (unsigned)((status >> 1) & 1U));
}
EscExmcBus::maskExit(mask);
return status;
}
bool EscLan9252::init()
{
s_busReady = false; /* 1.5.27:s_busReady 语义 = init 完整成功(EXMC+探针+BYTE_TEST) */
s_coreReady = false; /* 1.5.39:s_coreReady 语义 = EEPROM 加载完成、EtherCAT 核心使能 */
s_busReady = false; /* EXMC+BYTE_TEST; warm 先探再定是否 RST# */
s_coreReady = false; /* s_coreReady 语义 = EEPROM 加载完成、EtherCAT 核心使能 */
/* 使能 DWT 周期计数器,供 hw_gd32exmc/SSC 执行时间测量 */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0U;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
/* 受控 RST# 复位(仅首次;重复调用 init 不得二次复位,否则清掉 SSC 配置) */
if (!s_resetDone) {
hardwareReset();
s_resetDone = true;
}
/* 先起 EXMC 再谈复位。
* warm jump:BOOT 刚用过 HBI,芯片活着;此时若先 RST# 再起 EXMC,等于
* 把一条本来能通的总线按进复位重协商,一旦 RST# 释放时序不稳就整段假死。
* cold:POR 后芯片也需要 EXMC 才谈得上探活。
* 顺序固定为:EXMC → 无复位探 BYTE_TEST → 不通才 RST# 重来。 */
if (!EscExmcBus::init()) {
printf("[ECAT][ESC] init: FAIL, EXMC bus not ready (see [ECAT][EXMC] log)\r\n");
return false;
}
/* HBI 存活探针(数据手册 9.5.4.2):先直接读 HBI_IDX_0(offset 0,不写任何东西),
* 其复位默认值 = 0x1234。RST# 释放后 ESC 需完成内部启动与 EEPROM 加载,
* 故按手册建议轮询(此前为单次读,复位后立即读可能仍在启动中)。
* 若超时仍 != 0x1234(通常是 0x0000/0xFFFF 或上次写入的回显),
* 说明 LAN9252 完全没有驱动总线——CS 未拉低、CHIP_MODE strap 非 00(三端口=SPI only)
* 或芯片未出复位/未上电,而不是 EEPROM 问题(HBI 在 EEPROM 空白时也工作)。 */
uint16_t idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
for (uint32_t waited = 0U;
idx0 != 0x1234U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS;
waited += CONFIG_ESC_HBI_POLL_STEP_MS) {
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
}
if (idx0 != 0x1234U) {
printf("[ECAT][ESC] init: FAIL, HBI_IDX_0=0x%04X (expect 0x1234) -> LAN not driving bus\r\n",
uint16_t idx0 = 0U;
uint32_t byteTest = 0U;
/* 第 0 轮:不复位直接探。warm 残值下 BOOT 刚写过 HBI,BYTE_TEST 应立刻可读。
* BYTE_TEST 只证明通路;idx0==0x1234 才证明「芯片处于 POR 默认态」。
* warm jump 后 idx0 常为 0x0054(BOOT 索引事务残值)——与刚上电不同。
* 为了「跳转 = 上电」,残值时补一发 RST# 回到 POR,再走与 cold 相同的探活。 */
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
byteTest = readReg32(LAN9252_REG_BYTE_TEST);
const bool porLike = (byteTest == LAN9252_BYTE_TEST_VALUE) && (idx0 == 0x1234U);
if (porLike) {
printf("[ECAT][ESC] probe: BYTE_TEST ok without RST# (idx0=0x%04X) — POR 态,跳过复位\r\n",
(unsigned)idx0);
printf("[ECAT][ESC] hint: check CS(PD7) pulse, CHIP_MODE0/1 straps=00, RST#/power\r\n");
return false;
s_resetDone = true;
} else if (byteTest == LAN9252_BYTE_TEST_VALUE) {
/* 通路活但不是 POR(典型 warm jump:idx0=0x0054)。补 RST# 对齐上电。 */
printf("[ECAT][ESC] probe: BYTE_TEST ok but idx0=0x%04X != 0x1234 — warm 残值,"
"补 RST# 对齐上电 POR\r\n", (unsigned)idx0);
hardwareReset();
s_resetDone = true;
if (!EscExmcBus::init()) {
printf("[ECAT][ESC] init: FAIL, EXMC re-init after RST# (warm residual)\r\n");
return false;
}
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
byteTest = readReg32(LAN9252_REG_BYTE_TEST);
if (byteTest != LAN9252_BYTE_TEST_VALUE) {
printf("[ECAT][ESC] probe: BYTE_TEST=0x%08lX after warm RST#, fall back to multi-round\r\n",
(unsigned long)byteTest);
} else {
printf("[ECAT][ESC] probe: warm RST# ok (idx0=0x%04X, BYTE_TEST=0x87654321) — 与上电同态\r\n",
(unsigned)idx0);
}
}
/* 再读 BYTE_TEST(0x064)确认接口完全就绪,默认 0x87654321(小端)。
* 该值只有 HBI_DATA_0 偏移正确(EXMC 16-bit 地址左移 1 位)才可能读到。 */
const uint32_t byteTest = readReg32(LAN9252_REG_BYTE_TEST);
if (byteTest != LAN9252_BYTE_TEST_VALUE) {
printf("[ECAT][ESC] init: FAIL, BYTE_TEST=0x%08lX (expect 0x87654321)\r\n",
(unsigned long)byteTest);
printf("[ECAT][ESC] hint: IDX probe passed but DATA access failed -> check HBI_DATA_0 offsets (0x40000/0x60000)\r\n");
return false;
printf("[ECAT][ESC] probe: BYTE_TEST=0x%08lX idx0=0x%04X, 补 RST# 最多 3 轮\r\n",
(unsigned long)byteTest, (unsigned)idx0);
s_resetDone = false; /* 失败路径首轮强制 RST#,与上电后探不通时一致 */
bool alive = false;
for (uint8_t attempt = 0U; attempt < 3U; ++attempt) {
if (!s_resetDone || attempt > 0U) {
hardwareReset();
s_resetDone = true;
}
/* 复位后重配 EXMC:BOOT 关时钟时 PD/PE 停在 AF,MCU 侧寄存器也可能残留 */
if (!EscExmcBus::init()) {
continue;
}
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
/* 仅在「像复位中/未驱动」时轮询 0x1234;warm 残值直接测 BYTE_TEST */
const bool idxLooksIdle = (idx0 == 0x0000U) || (idx0 == 0xFFFFU);
if (idxLooksIdle || idx0 == 0x1234U) {
for (uint32_t waited = 0U;
idx0 != 0x1234U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS;
waited += CONFIG_ESC_HBI_POLL_STEP_MS) {
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
}
}
byteTest = readReg32(LAN9252_REG_BYTE_TEST);
if (byteTest == LAN9252_BYTE_TEST_VALUE) {
alive = true;
printf("[ECAT][ESC] probe#%u: BYTE_TEST ok (idx0=0x%04X)\r\n",
(unsigned)(attempt + 1U), (unsigned)idx0);
break;
}
printf("[ECAT][ESC] probe#%u fail (idx0=0x%04X byte_test=0x%08lX)\r\n",
(unsigned)(attempt + 1U), (unsigned)idx0, (unsigned long)byteTest);
}
if (!alive) {
printf("[ECAT][ESC] init: FAIL, BYTE_TEST=0x%08lX (expect 0x87654321), idx0=0x%04X -> LAN not driving bus\r\n",
(unsigned long)byteTest, (unsigned)idx0);
printf("[ECAT][ESC] hint: check CS(PD7) pulse, CHIP_MODE0/1 straps=00, RST#/power\r\n");
return false;
}
}
printf("[ECAT][ESC] init: OK, HBI alive (IDX=0x1234, BYTE_TEST=0x87654321)\r\n");
if (idx0 != 0x1234U) {
printf("[ECAT][ESC] init: WARN idx0=0x%04X != 0x1234, BYTE_TEST ok -> 复位态未知但通路可用,放行\r\n",
(unsigned)idx0);
}
printf("[ECAT][ESC] init: OK, HBI alive (idx0=0x%04X, BYTE_TEST=0x87654321)\r\n",
(unsigned)idx0);
s_busReady = true;
/* 等待 PDI 就绪、EtherCAT 核心可交互(1.5.37 新增,1.5.39 改经 CSR 间接端口读——
* 此前直读 HBI 索引,DL_STATUS 属 EtherCAT 核心寄存器,根本不在直接地址空间)。
/* 等待 PDI 就绪、EtherCAT 核心可交互。
* DL_STATUS 属 EtherCAT 核心寄存器,不在 HBI 直接空间,必须经 CSR 间接端口读。
* 判据:DL_STATUS(0x0110) bit0 = PDI operational / EEPROM loaded。
* 网络侧诊断已证实本板 EEPROM 加载与核心均正常,此等待应几乎立即通过;
* 超时意味着 PDI 真的未就绪(EEPROM 内容/I2C/strap 问题),此时 SSC 的
* AL Event Mask/邮箱 SM 配置会落空,由 initial.cpp 按 isCoreReady() 门控跳过。 */
uint16_t dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U);
uint16_t dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U, false);
uint32_t waited = 0U;
while ((dlStatus & 0x0001U) == 0U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS) {
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
waited += CONFIG_ESC_HBI_POLL_STEP_MS;
dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U);
dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U, false);
}
if ((dlStatus & 0x0001U) != 0U) {
s_coreReady = true;
@@ -269,7 +532,7 @@ bool EscLan9252::init()
printf("[ECAT][ESC] init: FAIL, PDI not operational after %ums "
"(DL_STATUS=0x%04X EEP_CSR=0x%04X)\r\n",
(unsigned)CONFIG_ESC_HBI_POLL_TIMEOUT_MS, (unsigned)dlStatus,
(unsigned)csrRead(ESC_REG_EEP_CSR, 2U));
(unsigned)csrRead(ESC_REG_EEP_CSR, 2U, false));
printf("[ECAT][ESC] hint: 用 tools/ecat/ecat_esc_regs.py 从网络侧读 0x0502 错误位区分 EEPROM/I2C/内容问题\r\n");
}
return true;
@@ -277,11 +540,10 @@ bool EscLan9252::init()
/**
* @brief 使能 LAN9252 的 IRQ 引脚输出,让 EtherCAT 事件真正到达 GD32 的 PB1。
* @note 1.5.43 新增。IRQ_CFG(0x054)/INT_EN(0x05C) 都是 LAN9252 系统 CSR
* @note IRQ_CFG(0x054)/INT_EN(0x05C) 都是 LAN9252 系统 CSR
* (HBI 索引直接可达,不走 CSR 间接端口),复位默认值均为 0,即
* IRQ_EN=0(引脚永久去断言) 且 ECAT_INT_EN=0(EtherCAT 事件不上报)。
* 此前固件从未配置它们,导致 PDI_Isr 一次都没被调用过:SSC 因此
* 永远等不到 PROCESS_OUTPUT_EVENT、bEcatFirstOutputsReceived 恒 FALSE,
* 不配这两个寄存器,PDI_Isr 不会触发,SSC 永远等不到 PROCESS_OUTPUT_EVENT,
* SAFEOP->OP 被无限挂起(表现为主站 "进 OP 失败 code=0x0000")。
* 极性必须与 ethercat_sync.cpp 的 EXTI 配置一致:那里是上拉输入 +
* 下降沿触发,故此处选推挽(IRQ_TYPE=1) + 低有效(IRQ_POL=0)。
@@ -289,13 +551,13 @@ bool EscLan9252::init()
*/
void EscLan9252::configPdWatchdog()
{
/* 1.5.45:过程数据看门狗 100ms(复位默认)→1s。
/* 过程数据看门狗 100ms(复位默认)→1s。
* 主站侧手动单步/100ms 调试周期会触发 SM2 看门狗:SSC 在 CheckIfEcatError()
* 读 0x0440 发现已过期,调 AL_ControlInd(STATE_SAFEOP, 0x001B) 把 OP 拖回 SAFEOP,
* 表现为"100ms 周期全 WKC 错、10/50ms 正常"。分频保持默认 100µs,只延长步数;
* 正常周期(≤50ms)远小于 1s,行为不变。 */
csrWrite(ESC_REG_WD_TIME_PD, ESC_WD_TIME_PD_STEPS, 2U);
const uint32_t rd = csrRead(ESC_REG_WD_TIME_PD, 2U);
const uint32_t rd = csrRead(ESC_REG_WD_TIME_PD, 2U, false);
if ((rd & 0xFFFFU) == ESC_WD_TIME_PD_STEPS) {
printf("[ECAT][ESC] PD watchdog = 1s (0x0420=%u x 100us, default was 100ms)\r\n",
(unsigned)ESC_WD_TIME_PD_STEPS);
@@ -326,15 +588,39 @@ void EscLan9252::enableEcatIrqOutput()
}
}
uint32_t EscLan9252::csrRead(uint16_t addr, uint8_t size)
uint32_t EscLan9252::csrRead(uint16_t addr, uint8_t size, bool verify)
{
/* DS 12.11.1:写 CSR_CMD(BUSY|R_nW|SIZE|ADDR) 启动,busy 自清后读 CSR_DATA;
* 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。 */
uint32_t n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
writeReg32(ECAT_CSR_CMD_REG, CSR_BUSY | CSR_RNW | ((uint32_t)size << 16) | addr);
n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
* 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。
* verify=true(默认):CSR_DATA 是锁存寄存器(非 FIFO),命令不变时连读两次必一致;
* 不一致即本拍 EXMC 毛刺——错读可能让 GetSyncMan 把错误值缓存成邮箱几何
* (→SSC 邮箱楔死,邮箱几何另有 mailbox.c 构建期常量兜底),或让
* CheckSmSettings 以瞬态值打断状态转换。对错读整事务重跑一次;
* 两轮仍不一致则交出末拍。
* verify=false:单读路径,供诊断回读/MII busy 轮询/init 轮询等低风险
* 调用方使用;SM/AL/邮箱等状态机关键寄存器必须保持默认双读。 */
if (!verify) {
uint32_t n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
writeReg32(ECAT_CSR_CMD_REG, CSR_BUSY | CSR_RNW | ((uint32_t)size << 16) | addr);
n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
return readReg32(ECAT_CSR_DATA_REG);
}
for (uint8_t attempt = 0U; attempt < 2U; ++attempt)
{
uint32_t n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
writeReg32(ECAT_CSR_CMD_REG, CSR_BUSY | CSR_RNW | ((uint32_t)size << 16) | addr);
n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
const uint32_t d1 = readReg32(ECAT_CSR_DATA_REG);
const uint32_t d2 = readReg32(ECAT_CSR_DATA_REG);
if (d1 == d2)
{
return d1;
}
}
return readReg32(ECAT_CSR_DATA_REG);
}
@@ -353,7 +639,11 @@ void EscLan9252::pramRead(uint16_t addr, uint8_t *buf, uint16_t len)
{
/* DS 12.11.3:写 ADDR_LEN 再置 BUSY 启动;引擎按 DWORD 向下对齐填充读 FIFO,
* 首 DWORD 有效字节从 (addr&3) 起(表 12-7),末 DWORD 尾部无效(表 12-8)。
* 别名口 0x00-0x1C 功能相同,轮换寻址即官方突发用法;每读满一个 DWORD 弹一项。 */
* 别名口 0x00-0x1C 功能相同,轮换寻址即官方突发用法;每读满一个 DWORD 弹一项。
*
* 必须逐 DWORD 轮询并弹前查 AVAIL 位。CMD 的 AVAIL_CNT(bits[12:8])批量弹模式
* 对短读不可靠:首 DWORD 新鲜、后续 DWORD 可能取到 FIFO 陈旧项。邮箱短读/探针
* 类小传输只验证过"逐 DWORD 查 AVAIL"模式,不要改回批量弹 DWORD。 */
if (len == 0U) {
return;
}
@@ -381,7 +671,8 @@ void EscLan9252::pramRead(uint16_t addr, uint8_t *buf, uint16_t len)
void EscLan9252::pramWrite(uint16_t addr, const uint8_t *buf, uint16_t len)
{
/* DS 12.11.4:写 ADDR_LEN 再置 BUSY 启动;首 DWORD 有效字节从 (addr&3) 起,
* 引擎只取有效字节;别名口 0x20-0x3C 轮换;写满一个 DWORD 推一项。 */
* 引擎只取有效字节;别名口 0x20-0x3C 轮换;写满一个 DWORD 推一项。
* 与 pramRead 同样逐 DWORD 轮询(推前必查 AVAIL 位),邮箱小 PDU 写同类风险。 */
if (len == 0U) {
return;
}
@@ -417,7 +708,7 @@ uint32_t EscLan9252::readChipId()
uint16_t EscLan9252::initOnlyReadReg(uint16_t addr)
{
/* 1.5.39:ESC 核心 CSR 经间接端口读(此前直读 HBI 索引,核心寄存器不在直接空间,恒 0) */
/* ESC 核心 CSR 经间接端口读(核心寄存器不在 HBI 直接空间,直读恒 0) */
return (uint16_t)csrRead(addr, 2U);
}
+43 -15
View File
@@ -2,7 +2,7 @@
* @file esc_lan9252.h
* @brief LAN9252 EtherCAT 从站控制器(ESC)驱动接口。
* @note 通过 EscExmcBus 16-bit 并行总线,以 HBI Indexed Address Mode 访问 LAN9252。
* 访问模型(1.5.39 按 DS 12.11 修正,此前误以为整个 16 位索引空间统一可寻址):
* 访问模型(DS 12.11):
* - HBI 直接地址空间:仅 LAN9252 系统 CSR(BYTE_TEST 0x064 等)+ 访问端口
* (PRAM FIFO 0x000-0x03C、ECAT_CSR_DATA/CMD 0x300/0x304、PRAM_ADDR_LEN/CMD
* 0x308-0x314)——经 HBI_IDX_0/HBI_DATA_0 直接读写;
@@ -29,7 +29,7 @@
class EscLan9252 {
public:
/** @brief 初始化 EXMC 总线与 ESC 基本寄存器。
* @note 1.5.25 起内部先执行一次 hardwareReset() 受控复位(s_resetDone 门控,
* @note 内部先执行一次 hardwareReset() 受控复位(s_resetDone 门控,
* 重复调用不会二次复位 SSC 已配置好的芯片),再探活 EXMC/HBI。
* @return bool true 表示 EXMC 总线探活通过(可继续读取芯片 ID)。 */
static bool init();
@@ -46,6 +46,18 @@ public:
* 驱动高读回低 => ECAT_NRST 被按死在复位,LAS9252 永在复位态。 */
static uint8_t nrstDrive(uint8_t action);
/** @brief 诊断:LAN9252 芯片级电源管理(休眠/唤醒,SPI 0xA2 action 30+ 转发到此)。
* @param op 0=读状态 1=进 D1 2=进 D2 3=进 D3(先两 PHY General Power Down) 4=手动唤醒
* @return 打包状态字节 bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE;
* 唤醒 op 另有 1=成功 0=超时;0xEE=无效 op;0xE1=HBI 总线未就绪。
* @note DS 6.3:PMT_CTRL(0x084) PM_MODE[31:29] 选档 + PM_SLEEP_EN(bit28) 进入,
* 两字段必须分两次写。D1/D2/D3 下主机接口停摆、EtherCAT 通信中断(主站
* wkc 报错),唤醒后 PM_MODE/PM_SLEEP_EN 自清、寄存器配置保留、READY 回高
* (正常 ≤2ms)。D3 最深(晶振/PLL 全关)且手册要求先置两 PHY BMCR.PowerDown
* (经 ESC 核心 MII 管理 0x0510 间接访问,PHY A 地址模式相关 0/2 运行期判定),
* 唤醒后本函数自动恢复 PHY 上电,链路重新自协商需数秒。 */
static uint8_t pmDrive(uint8_t op);
/**
* @brief 读取 ESC 芯片 ID(INIT-ONLY,供 init 打印诊断用)。
* @return uint32_t 32-bit 芯片 ID(期望值 0x87654321)。
@@ -53,20 +65,23 @@ public:
static uint32_t readChipId();
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SSC 移植层(HW_*)。
* @note 1.5.39 起经 CSR_CMD/CSR_DATA 间接端口读 ESC 核心 CSR(此前直读 HBI 索引,
* 而 EtherCAT 核心寄存器根本不在 HBI 直接地址空间,恒读 0)。 */
* @note 经 CSR_CMD/CSR_DATA 间接端口读 ESC 核心 CSR
* (EtherCAT 核心寄存器不在 HBI 直接地址空间,直读恒 0)。 */
static uint16_t initOnlyReadReg(uint16_t addr);
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用。 */
static void initOnlyWriteReg(uint16_t addr, uint16_t v);
/** @brief ESC 核心 CSR 间接读(ECAT_CSR_CMD/DATA 端口,0x0000-0x0FFF 地址空间)。
* @note 1.5.39:LAN9252 的 HBI/SPI 直接地址空间只有 LAN9252 系统 CSR 与访问端口
* @note LAN9252 的 HBI/SPI 直接地址空间只有 LAN9252 系统 CSR 与访问端口
* (DS 12.11/表 12-13),EtherCAT 核心寄存器(0x0000-0x0FFF)必须经
* CSR_CMD(0x304)/CSR_DATA(0x300) 间接访问;过程 RAM(0x1000-0x1FFF)
* 必须经 PRAM FIFO。size=1/2/4(size2 需偶地址,size4 需 4 字节对齐)。
* 调用方负责 HBI 互斥(主循环 EscExmcBus::maskEnter / 已在 ISR 上下文)。 */
static uint32_t csrRead(uint16_t addr, uint8_t size);
* 调用方负责 HBI 互斥(主循环 EscExmcBus::maskEnter / 已在 ISR 上下文)。
* @param verify true(默认)=双读比对+不一致重试(防 EXMC 毛刺,SM/AL/邮箱
* 等关键路径必须开);false=单读(诊断回读/MII busy 轮询/init 等低风险路径,
* 省约一半 CSR 间接访问)。 */
static uint32_t csrRead(uint16_t addr, uint8_t size, bool verify = true);
/** @brief ESC 核心 CSR 间接写(ECAT_CSR_CMD/DATA 端口)。约束同 csrRead。 */
static void csrWrite(uint16_t addr, uint32_t data, uint8_t size);
@@ -77,31 +92,44 @@ public:
/** @brief 过程 RAM(0x1000-0x1FFF)经 PRAM 写 FIFO 写入 len 字节。 */
static void pramWrite(uint16_t addr, const uint8_t *buf, uint16_t len);
/** @brief EXMC 总线 + HBI_IDX_0 探针 + BYTE_TEST 全部通过(init 成功)后为 true。
* @note 与 init() 返回值一致,可作运行期查询。 */
/** @brief EXMC 总线 + BYTE_TEST 往返探针通过(init 成功)后为 true。
* @note HBI_IDX_0 复位默认值仅作诊断读数,不参与放行
* (它只证明"芯片刚出过复位",证明不了 HBI 通路)。
* 与 init() 返回值一致,可作运行期查询。 */
static bool isBusReady() { return s_busReady; }
/** @brief EEPROM(SII) 加载完成、EtherCAT 核心使能(1.5.39)。
/** @brief EEPROM(SII) 加载完成、EtherCAT 核心使能。
* @note false 时核心寄存器对 PDI 读全 0、写静默丢弃,SSC MainInit 会全部
* 落空(AL Event Mask/邮箱 SM 配置写不进),调用方必须跳过 SSC 初始化。 */
static bool isCoreReady() { return s_coreReady; }
/** @brief ESC 已被 pmDrive 置入 D1-D3 睡眠、所有 HBI 消费者必须静默。
* @note 睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。置位期间
* task_runner 停调 MainLoop()/AL 守护/AL 心跳,ethercat_sync 三个 EXTI
* ISR 清 pending 后直接返回——否则芯片进入睡眠后毫秒级就被主循环的
* ECAT_Main(读 0x220)叫醒,根本睡不住。
* 解除:pmDrive 唤醒/读到 D0、nrstDrive 释放复位、hardwareReset。
* ISR 上下文安全:读一个 bool 静态成员,无锁。 */
static bool pmSuspended() { return s_pmSuspended; }
private:
/** @brief 使能 LAN9252 的 IRQ 引脚输出(IRQ_CFG 0x054 / INT_EN 0x05C)。
* @note 1.5.43 新增,由 init() 在 PDI/核心就绪后调用。两寄存器复位默认全 0,
* @note 由 init() 在 PDI/核心就绪后调用。两寄存器复位默认全 0,
* 不配则 EtherCAT 事件永不到达 PB1、SSC 的 PDI_Isr 从不执行,
* bEcatFirstOutputsReceived 恒 FALSE 使 SAFEOP->OP 被无限挂起。 */
* SAFEOP->OP 被无限挂起。 */
static void enableEcatIrqOutput();
/** @brief 过程数据看门狗 100ms(复位默认)→1s(1.5.45)。
/** @brief 过程数据看门狗 100ms(复位默认)→1s。
* @note 由 init() 在 PDI/核心就绪后调用。写 ESC_REG_WD_TIME_PD(0x0420)=10000
* (100µs 分频保持默认)。不写则主站 100ms 手动调试周期触发 SM2 看门狗,
* SSC 以 0x001B 拖回 SAFEOP。 */
static void configPdWatchdog();
static bool s_busReady; /**< init 完整成功(EXMC+IDX 探针+BYTE_TEST)后置位。 */
static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位(1.5.39)。 */
static bool s_busReady; /**< init 成功(EXMC+BYTE_TEST 往返;IDX 残值不挡)后置位。 */
static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位。 */
static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */
static bool s_pmPhyDown; /**< pmDrive 进 D3 时两 PHY 已被置 PowerDown,唤醒后需恢复。 */
static bool s_pmSuspended; /**< ESC 睡眠挂起:置位期间全固件 HBI 消费者静默(见 pmSuspended)。 */
};
#endif /* COMM_ESC_LAN9252_H */
+281 -170
View File
@@ -1,10 +1,12 @@
/**
* @file ethercat_can_bridge.cpp
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.5.56)。
* @brief EtherCAT PDO ↔ CAN 转发桥实现。
*/
#include "comm/ethercat_can_bridge.h"
#include "device/canfd.h"
#include "device/motor_state.h"
#include "sys/motor_ota_relay.h"
#include "sys/systick.h"
#include "RTE_Components.h"
#include CMSIS_device_header /* __DMB():seqlock 的编译器/总线屏障 */
@@ -13,12 +15,16 @@
volatile uint8_t EthercatCanBridge::s_lastSeq = 0U;
volatile bool EthercatCanBridge::s_seqPrimed = false;
volatile bool EthercatCanBridge::s_coldStartPending = false;
volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U;
volatile uint16_t EthercatCanBridge::s_txCount = 0U;
volatile uint16_t EthercatCanBridge::s_dropCount = 0U;
volatile uint8_t EthercatCanBridge::s_winBase = 0U;
volatile uint32_t EthercatCanBridge::s_lastTxId = 0U;
volatile uint16_t EthercatCanBridge::s_rx00ffCount = 0U;
volatile uint8_t EthercatCanBridge::s_outSeenSeq = 0U;
volatile bool EthercatCanBridge::s_rxValid = false;
volatile bool EthercatCanBridge::s_rxPublished = false;
volatile uint32_t EthercatCanBridge::s_xrxSeq = 0U;
volatile uint8_t EthercatCanBridge::s_rxCh = 0U;
volatile uint8_t EthercatCanBridge::s_rxDlc = 0U;
@@ -27,14 +33,29 @@ volatile uint32_t EthercatCanBridge::s_rxId = 0U;
volatile uint32_t EthercatCanBridge::s_rxCount = 0U;
uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0};
EthercatCanBridge::RxRingItem
EthercatCanBridge::s_rxRing[kEcatRxRingSlots] = {};
volatile uint8_t EthercatCanBridge::s_rxRingHead = 0U;
volatile uint8_t EthercatCanBridge::s_rxRingTail = 0U;
volatile uint8_t EthercatCanBridge::s_rxRingLost = 0U;
volatile bool EthercatCanBridge::s_verValid = false;
volatile uint32_t EthercatCanBridge::s_verHoldUntilMs = 0U;
uint8_t EthercatCanBridge::s_verCh = 0U;
uint8_t EthercatCanBridge::s_verDlc = 0U;
uint32_t EthercatCanBridge::s_verId = 0U;
uint8_t EthercatCanBridge::s_verData[kEcatCanXRxDataMax] = {0};
namespace {
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端,不依赖编译器结构体布局。 */
/** 版本应答在 PDO 段3 的保持时长(ms)。主站 1ms 周期下足够被采到。 */
constexpr uint32_t kVerHoldMs = 300U;
inline uint16_t canSubId(uint32_t id) { return static_cast<uint16_t>(id & 0xFFFFU); }
} // namespace
uint8_t EthercatCanBridge::s_motShadow[kEcatMotRegionBytes] = {0};
uint32_t EthercatCanBridge::s_motPubSeq[kMotorMaxNodes + 1U] = {0};
uint8_t EthercatCanBridge::s_motPubOnline[kMotorMaxNodes + 1U] = {0};
uint16_t EthercatCanBridge::s_motPubMask = 0U;
uint8_t EthercatCanBridge::s_motPubCount = 0U;
bool EthercatCanBridge::s_motShadowValid = false;
namespace {
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端。 */
inline uint32_t rd32le(const uint8_t *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
@@ -49,13 +70,19 @@ inline void wr16le(uint8_t *p, uint16_t v)
{
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
}
/** 段 2 头 + count 槽在影子区中的总长度。 */
inline uint16_t motRegionLen(uint8_t count)
{
return (uint16_t)(kEcatMotHdrSize + (uint16_t)count * kEcatMotSlotSize);
}
} // namespace
uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
{
uint8_t want = out[ECAT_TXB_OFF_COUNT];
if (want == 0U) {
return 0U; /* 未使用批量区:行为与 1.5.55 完全一致 */
return 0U;
}
if (want > kEcatTxBatchMax) {
want = kEcatTxBatchMax;
@@ -66,8 +93,6 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
uint16_t off = ECAT_TXB_OFF_ITEMS;
for (uint8_t i = 0U; i < want; ++i) {
/* 不信任主站给的 count/dlc:每步都验证头与数据都完整落在 PDO 内。
* 越界即停 —— 这是唯一的边界判据,count 只是上限提示。 */
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE) > kEcatPdoOutBytes) {
break;
}
@@ -84,10 +109,13 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
CanFd::CanTxResult r;
if (!CanFd::isChannelEnabled(ch)) {
r = CanFd::CanTxResult::kErrInvalid;
} else if (MotorOtaRelay::isChannelLocked(ch)) {
/* 该通道正被电机 OTA 独占:PDO 运动帧一律不投递。
* OTA 用单发模式(ARD=1),一旦挤进同一个邮箱就会 abort 掉在途的
* 0x101 固件包,或让电机 BOOT 收到非预期长度直接回 0x05/0x06。
* 计数与 lastTxResult 走 0xE7,主站侧能看到「被 OTA 挡了」而不是「邮箱满」。 */
r = CanFd::CanTxResult::kErrOtaLocked;
} else {
/* 与单帧路径同样用 enqueueTx:批量下发必然连续打同一通道,
* transmitFrame 的 allow_abort 会让后帧顶掉前帧(历史上 7 帧
* 只发出 3 帧的根因),队列语义才不静默丢帧。 */
r = CanFd::enqueueTx(ch, rd32le(&f[ECAT_FRM_OFF_ID]),
&f[ECAT_FRM_HDR_SIZE], dlc);
}
@@ -95,6 +123,7 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
if (r == CanFd::CanTxResult::kOk) {
++s_txCount;
++sent;
s_lastTxId = rd32le(&f[ECAT_FRM_OFF_ID]);
} else {
s_lastTxResult = (uint8_t)r;
if (stopOnError) {
@@ -106,43 +135,141 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
return sent;
}
void EthercatCanBridge::fillRxBatch(uint8_t *in)
/**
* 段 2:电机整轮密排。
*
* 1. 扫 MotorState,每个节点号 1..9 在 3 个通道里取 seq 最新的一条;
* 2. 若节点集合与各节点 seq 均与上一拍发布相同 → 整段复用影子区(跳过重读);
* 3. 否则按 node_id 升序密排写入,空节点不占位;seqlock 奇数(写入中)的槽
* 本拍省略,下拍自愈 —— 不发半新半旧的 48B。
*/
uint8_t EthercatCanBridge::fillMotorRound(uint8_t *in)
{
uint8_t count = 0U;
uint16_t off = ECAT_RXB_OFF_ITEMS;
uint8_t tail = s_rxRingTail;
const uint8_t head = s_rxRingHead; /* 快照一次:生产者可能边填边推进 */
const MotorStateTable &mt = MotorState::table();
while (tail != head && count < kEcatRxBatchMax) {
const RxRingItem &it = s_rxRing[tail];
uint8_t dlc = it.dlc;
if (dlc > kEcatCanFdDataMax) {
dlc = kEcatCanFdDataMax;
/* ── 1. 每节点选一条最新槽 ── */
const MotorSlot *picks[kMotorMaxNodes];
uint8_t nodes[kMotorMaxNodes];
uint8_t n = 0U;
for (uint8_t node = 1U; node <= kMotorMaxNodes; ++node) {
const MotorSlot *best = nullptr;
for (uint8_t ch = 0U; ch < kMotorChannels; ++ch) {
const MotorSlot &s =
mt.slots[(uint8_t)((node - 1U) * kMotorChannels + ch)];
if (s.node_id == 0U) {
continue;
}
if (best == nullptr || s.seq >= best->seq) {
best = &s;
}
}
/* 装不下就停,**且不推进 tail** —— 该帧留到下个周期,不丢。 */
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc) > kEcatPdoInBytes) {
break;
if (best != nullptr) {
picks[n] = best;
nodes[n] = node;
++n;
}
uint8_t *p = &in[off];
p[ECAT_FRM_OFF_CH] = it.ch;
p[ECAT_FRM_OFF_DLC] = dlc;
p[ECAT_FRM_OFF_FLAGS] = 0x01U;
wr32le(&p[ECAT_FRM_OFF_ID], it.id);
memcpy(&p[ECAT_FRM_HDR_SIZE], it.data, dlc);
off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc);
tail = (uint8_t)((tail + 1U) % kEcatRxRingSlots);
++count;
}
__DMB(); /* 槽位读取先于 tail 发布落地:否则生产者可能提前覆写还没读完的槽 */
s_rxRingTail = tail; /* 只在这里推进:消费者独占写 tail */
const uint8_t pending = (uint8_t)((head + kEcatRxRingSlots - tail) % kEcatRxRingSlots);
in[ECAT_RXB_OFF_COUNT] = count;
in[ECAT_RXB_OFF_LOST] = s_rxRingLost;
in[ECAT_RXB_OFF_PENDING] = pending;
s_rxRingLost = 0U; /* 回填后清零:lost 是"自上次回填以来"的增量 */
/* ── 2. 判「上一轮已填充且未变」──
* 节点集合一致 + 每节点 seq/online 一致 + 头字段(active/boot)一致 + 写侧
* seqlock 偶数 → 复用影子。online 必须比:checkOffline 超时置 0 时不 bump
* seq,只比 seq 会把「已离线」永远报成在线。 */
bool unchanged = s_motShadowValid && (n == s_motPubCount)
&& mt.active_count == s_motShadow[ECAT_MOTW_OFF_ACTIVE - ECAT_MOTW_OFF_COUNT]
&& mt.boot_active == s_motShadow[ECAT_MOTW_OFF_BOOT - ECAT_MOTW_OFF_COUNT]
&& s_motShadow[ECAT_MOTW_OFF_MAGIC - ECAT_MOTW_OFF_COUNT]
== kEcatMotLayoutMagic;
if (unchanged) {
for (uint8_t i = 0U; i < n; ++i) {
const uint8_t node = nodes[i];
const MotorSlot &s = *picks[i];
if ((s.wr_seq & 1U) != 0U
|| s.seq != s_motPubSeq[node]
|| s.online != s_motPubOnline[node]
|| (s_motPubMask & (uint16_t)(1U << node)) == 0U) {
unchanged = false;
break;
}
}
}
const uint16_t region = motRegionLen(unchanged ? s_motPubCount : n);
/* 集合+seq+online 未变:整段复用影子,避免 8kHz 下每拍重建密排区。
* 头字段 ACTIVE/BOOT/MAGIC/GSEQ 在影子起点 COUNT 之后,一并复用。 */
if (unchanged && s_motShadowValid && s_motPubCount > 0U) {
memcpy(&in[ECAT_MOTW_OFF_COUNT], s_motShadow, region);
return s_motPubCount;
}
/* ── 3. 重建密排区 ── */
in[ECAT_MOTW_OFF_ACTIVE] = mt.active_count;
in[ECAT_MOTW_OFF_BOOT] = mt.boot_active;
in[ECAT_MOTW_OFF_MAGIC] = kEcatMotLayoutMagic;
wr32le(&in[ECAT_MOTW_OFF_GLOBAL_SEQ], mt.global_seq);
uint8_t w = 0U;
uint16_t newMask = 0U;
for (uint8_t i = 0U; i < n; ++i) {
const MotorSlot &s = *picks[i];
if ((s.wr_seq & 1U) != 0U) {
continue; /* 写入中:本拍省略,下拍自愈 */
}
__DMB();
uint8_t *p = &in[ECAT_MOTW_OFF_SLOTS + (uint16_t)w * kEcatMotSlotSize];
p[ECAT_MOT_OFF_NODE] = nodes[i];
p[ECAT_MOT_OFF_CH] = s.channel;
p[ECAT_MOT_OFF_ONLINE] = s.online;
p[ECAT_MOT_OFF_DLC] = s.dlc;
wr32le(&p[ECAT_MOT_OFF_SEQ], s.seq);
memcpy(&p[ECAT_MOT_OFF_STATUS], s.data, kMotorStatusDlc);
s_motPubSeq[nodes[i]] = s.seq;
s_motPubOnline[nodes[i]] = s.online;
newMask |= (uint16_t)(1U << nodes[i]);
++w;
}
in[ECAT_MOTW_OFF_COUNT] = w;
/* 更新影子:下次集合+seq 未变时整段复用 */
memcpy(s_motShadow, &in[ECAT_MOTW_OFF_COUNT], motRegionLen(w));
s_motPubMask = newMask;
s_motPubCount = w;
s_motShadowValid = true;
return w;
}
bool EthercatCanBridge::peekOutput(const uint8_t *head)
{
/* 输出探针:12B 头里 SEQ@0 / FLAGS。seq 无变化沿且无清统计请求时返回 false,
* PDO_OutputMapping 只补读 SM2 末字,跳过 384B 全量读。
* 恒 true 会让 8kHz 每拍都全量读,单拍被 PRAM 打爆(6kHz 即掉速)。 */
if (head == nullptr) {
return false;
}
const uint8_t seq = head[ECAT_CAN_TX_OFF_SEQ];
if (!s_seqPrimed) {
s_seqPrimed = true;
s_lastSeq = seq;
s_coldStartPending = true;
return true; /* 冷启动首包强制全量读,避免漏掉首帧命令 */
}
if (seq != s_lastSeq) {
return true;
}
return (head[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U;
}
void EthercatCanBridge::resetSequenceState()
{
s_seqPrimed = false;
s_lastSeq = 0U;
s_coldStartPending = false;
}
void EthercatCanBridge::processOutput(const uint8_t *out)
@@ -152,60 +279,71 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
}
const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ];
s_outSeenSeq = seq; /* 诊断:processOutput 本拍看到的输出 seq */
/* 电机状态窗口下标每周期都取(与 seq 沿无关):主站靠移动这个值滚动扫表,
* 不该被「有没有新 CAN 帧要发」耦合。越界值钳到 0,避免读到槽数组外。 */
const uint8_t win = out[ECAT_CAN_TX_OFF_WIN];
s_winBase = (win < kMotorSlotCount) ? win : 0U;
/* 上电/重连后第一次看到输出数据时只记录基准,不发送:否则主站残留的
* 非零 seq 会被当成一次新请求,在进 OP 的瞬间凭空发出一帧。 */
if (!s_seqPrimed) {
s_seqPrimed = true;
s_lastSeq = seq;
return;
s_seqPrimed = true;
s_coldStartPending = true;
}
/* flags bit0:清零统计(一次性,不依赖 seq 沿,便于主站随时归零) */
/* 无论 seq 是否变化,优先处理非 seq 驱动的命令(FLAGS bit0 清统计) */
if ((out[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U) {
s_txCount = 0U;
s_dropCount = 0U;
s_rxCount = 0U;
s_txCount = 0U;
s_dropCount = 0U;
s_rxCount = 0U;
s_rx00ffCount = 0U;
s_rxPublished = true;
}
if (seq == s_lastSeq) {
return; /* 没有新的发送请求 */
const uint8_t dlcRaw = out[ECAT_CAN_TX_OFF_DLC];
const uint8_t batchCount = out[ECAT_TXB_OFF_COUNT];
const bool hasPayload = (dlcRaw > 0U) || (batchCount > 0U);
bool executeCommand = false;
if (s_coldStartPending) {
s_coldStartPending = false;
/* 冷启动:有载荷就发(不依赖 s_lastSeq 是否已被探针碰过) */
if (hasPayload) {
executeCommand = true;
} else {
s_lastSeq = seq;
return;
}
} else if (seq != s_lastSeq) {
executeCommand = true;
} else if (hasPayload && seq != s_lastSeq) {
executeCommand = true;
}
if (!executeCommand) {
return;
}
s_lastSeq = seq;
const uint8_t ch = out[ECAT_CAN_TX_OFF_CH];
uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC];
uint8_t dlc = dlcRaw;
if (dlc > kEcatCanTxDataMax) {
dlc = kEcatCanTxDataMax;
}
/* 单帧区先发(保持 1.5.55 语义),再发批量区。
* dlc==0 表示本次只用批量区,不发单帧 —— 否则会凭空发出一帧 0 长度报文。 */
if (dlc > 0U) {
/* isChannelEnabled 内含越界检查,是"通道号合法且可用"的唯一判据 */
if (!CanFd::isChannelEnabled(ch)) {
s_lastTxResult = (uint8_t)CanFd::CanTxResult::kErrInvalid;
} else if (MotorOtaRelay::isChannelLocked(ch)) {
/* 同 sendBatch:OTA 独占通道,周期运动帧不投邮箱(0xE7 上报主站)。 */
s_lastTxResult = (uint8_t)CanFd::CanTxResult::kErrOtaLocked;
} else {
/* 用 enqueueTx 而非 transmitFrame:PDO 是周期性场景,可能连续多帧打到
* 同一通道。transmitFrame 默认 allow_abort=true 会在邮箱满时顶掉已装载
* 的旧帧(历史上 7 帧突发只发出 3 帧的根因);enqueueTx 队列空时直通、
* 满时排队,不静默丢帧。 */
const CanFd::CanTxResult r =
CanFd::enqueueTx(ch, rd32le(&out[ECAT_CAN_TX_OFF_ID]),
&out[ECAT_CAN_TX_OFF_DATA], dlc);
s_lastTxResult = (uint8_t)r;
if (r == CanFd::CanTxResult::kOk) {
++s_txCount;
s_lastTxId = rd32le(&out[ECAT_CAN_TX_OFF_ID]);
}
}
}
/* 批量区:同一个 seq 沿里一次性发 N 帧,供多轴同步下发。 */
(void)sendBatch(out);
}
@@ -214,20 +352,19 @@ void EthercatCanBridge::buildInput(uint8_t *in)
if (in == nullptr) {
return;
}
/* 必须按新的全长清零。旧代码写死 memset(in,0,32),扩容后会把 32 字节以后
* 的槽区留成上一周期的残留数据,主站读到的是"看起来合法但已过期"的电机状态。 */
memset(in, 0, kEcatPdoInBytes);
/* 不整包 memset(600):8kHz 下每拍 Process 都会走到这里,600B 清零
* 与后续覆盖写重复。各段自己写满有效域;未用槽由 fillMotorRound
* 按 count 截断,主站只迭代 count,空洞区可为残值。 */
/* ── 段 1(0..31):桥状态 + 短快照,布局与 1.5.53 逐字节兼容 ── */
/* ── 段 1(0..31):桥状态 + 短快照 ── */
in[ECAT_CAN_RX_OFF_ACK] = s_lastSeq;
in[ECAT_CAN_RX_OFF_TXRES] = s_lastTxResult;
wr16le(&in[ECAT_CAN_RX_OFF_TXCNT], s_txCount);
wr16le(&in[ECAT_CAN_RX_OFF_DROP], s_dropCount);
wr32le(&in[ECAT_CAN_RX_OFF_RXCNT], s_rxCount);
wr32le(&in[ECAT_CAN_RX_OFF_TXTXID], s_lastTxId);
wr16le(&in[ECAT_CAN_RX_OFF_RX00FF], s_rx00ffCount);
/* seqlock 判稳(见 onCanFrame):奇数=生产者正在写这组快照。单核下 ISR
* 执行期间生产者冻结,读一次即可定性;本周期跳过段 1/段 3(开头已清零,
* 语义=无有效快照),下周期自愈。批量区(段 4)走环形队列,不受影响。 */
const bool xrxStable = ((s_xrxSeq & 1U) == 0U);
if (xrxStable && s_rxValid) {
@@ -239,65 +376,47 @@ void EthercatCanBridge::buildInput(uint8_t *in)
in[ECAT_CAN_RX_OFF_DLC] = s_rxDlc;
in[ECAT_CAN_RX_OFF_FLAGS] = flags;
wr32le(&in[ECAT_CAN_RX_OFF_ID], s_rxId);
/* 段 1 只放前 8 字节(兼容),完整 payload 见段 3 */
memcpy(&in[ECAT_CAN_RX_OFF_DATA], s_rxData, kEcatCanRxDataMax);
s_rxPublished = true;
} else {
in[ECAT_CAN_RX_OFF_FLAGS] = 0U;
in[ECAT_CAN_RX_OFF_CH] = 0U;
in[ECAT_CAN_RX_OFF_DLC] = 0U;
}
const uint8_t ch = s_rxValid ? s_rxCh : 0U;
const uint32_t backlog = CanFd::isChannelEnabled(ch) ? CanFd::txQueueBacklog(ch) : 0U;
in[ECAT_CAN_RX_OFF_BACKLOG] = (backlog > 255U) ? 255U : (uint8_t)backlog;
/* ── 段 2(32..319):电机状态窗口 ──
* 直接读 MotorState 聚合表,不再依赖"最近一帧 CAN"。窗口起点由主站通过
* 输出 PDO 的 WIN 字节指定,主站每周期 +7 即可 4 周期扫完 27 个槽。
*
* 上下文安全:buildInput 在主循环(free-run)或 Sync0_Isr 里跑,而状态表由
* 主循环的 taskCanRxParse 写。单字段撕裂最坏是某槽的 seq 与 data 不同步一拍,
* 主站按 seq 变化判新旧、下一周期即自愈,故不加临界区(与本文件既有约定一致)。 */
const MotorStateTable &mt = MotorState::table();
const uint8_t base = (s_winBase < kMotorSlotCount) ? s_winBase : 0U;
/* ── 段 2(32..543):电机整轮密排 ── */
(void)fillMotorRound(in);
in[ECAT_MOTW_OFF_ACTIVE] = mt.active_count;
in[ECAT_MOTW_OFF_BOOT] = mt.boot_active;
in[ECAT_MOTW_OFF_WIN_BASE] = base;
wr32le(&in[ECAT_MOTW_OFF_GLOBAL_SEQ], mt.global_seq);
uint8_t count = 0U;
for (uint8_t i = 0U; i < kEcatMotSlotsPerPdo; ++i) {
const uint8_t idx = (uint8_t)(base + i);
if (idx >= kMotorSlotCount) {
break; /* 窗口尾部越界即停,不回绕:主站据 win_count 判断 */
/* ── 段 3(544..583):长帧快照(通用,不专宠版本) ──
* 语义 =「最近一帧非 STATUS 长帧」:CONFIG/版本/节点ID 谁后到谁上。
* STATUS(0x0100) 由段2 密排承载,不进本槽(否则 48B STATUS 会淹没一切)。
* 例外:仅当刚收到 0x00FF 的短粘滞窗口内,优先摆版本应答,
* 给主站一个稳定采样窗;窗口结束后立刻交还给 CONFIG 等,避免其它长帧失效。 */
if (xrxStable) {
const uint32_t nowMs = SysTickTimer::getMs();
const bool stickyLive = s_verValid && static_cast<int32_t>(nowMs - s_verHoldUntilMs) < 0;
/* 段3 RSVD@547:processOutput 看到的输出 seq(诊断用,不参与解析) */
in[ECAT_XRX_OFF_RSVD] = s_outSeenSeq;
if (stickyLive) {
in[ECAT_XRX_OFF_CH] = s_verCh;
in[ECAT_XRX_OFF_DLC] = s_verDlc;
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
wr32le(&in[ECAT_XRX_OFF_ID], s_verId);
memcpy(&in[ECAT_XRX_OFF_DATA], s_verData, kEcatCanXRxDataMax);
} else if (s_rxValid) {
in[ECAT_XRX_OFF_CH] = s_rxCh;
in[ECAT_XRX_OFF_DLC] = s_rxDlc;
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
wr32le(&in[ECAT_XRX_OFF_ID], s_rxId);
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
}
const MotorSlot &s = mt.slots[idx];
uint8_t *p = &in[ECAT_MOTW_OFF_SLOTS + (uint16_t)i * kEcatMotSlotSize];
p[ECAT_MOT_OFF_NODE] = s.node_id;
p[ECAT_MOT_OFF_CH] = s.channel;
p[ECAT_MOT_OFF_ONLINE] = s.online;
p[ECAT_MOT_OFF_DLC] = s.dlc;
wr32le(&p[ECAT_MOT_OFF_SEQ], s.seq);
memcpy(&p[ECAT_MOT_OFF_STATUS], s.data, kMotorStatusDlc);
++count;
}
in[ECAT_MOTW_OFF_WIN_COUNT] = count;
/* ── 段 3(320..359):最近一帧完整快照 ──
* 版本帧(0x00FF)/CONFIG 帧(0x0101) 都是 DLC 24,段 1 的 8 字节装不下。
* 与段 1 共用 xrxStable 门控:两段必须来自同一帧,否则 ecat.html 的短快照
* 和电机页的长快照会各自解出半新半旧的 ID/数据。 */
if (xrxStable && s_rxValid) {
in[ECAT_XRX_OFF_CH] = s_rxCh;
in[ECAT_XRX_OFF_DLC] = s_rxDlc;
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
wr32le(&in[ECAT_XRX_OFF_ID], s_rxId);
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
}
/* ── 段 4(360..639):CANFD 批量接收区 ──
* 段 1/段 3 只带"最近一帧",高速多帧场景下周期间到达的帧会互相覆盖
* (s_dropCount 就是在数这个)。批量区从环形队列里一次回收多帧,
* 让主站在一个周期内拿到完整的帧序列而不是采样点。 */
fillRxBatch(in);
/* 段 5 由 EthercatPdo::fillDiagTail 写。 */
}
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
@@ -307,34 +426,25 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
return;
}
/* 上一帧还没被 PDO 周期取走就来了新帧:记一次丢弃(覆盖旧帧,
* 保证主站看到的永远是最新状态,而不是卡在一帧陈旧数据上)。 */
if (s_rxValid) {
++s_dropCount;
/* STATUS(0x0100) 由 MotorState 段2 密排承载,不写入 xrx。
* 原注释本就如此约定;实现曾漏过滤,导致高频 STATUS 盖掉 CONFIG/版本。 */
if (canSubId(id) == 0x0100U) {
return;
}
/* 缓存区已扩到 32B(段 3 用),truncated 仍按段 1 的 8B 口径上报,
* 这样 ecat.html 的 CAN 调试页语义不变。 */
uint8_t copyLen = length;
if (copyLen > kEcatCanXRxDataMax) {
copyLen = kEcatCanXRxDataMax;
}
/* ── 快照 seqlock(1.5.58)──
* s_rxData/s_rxCh/s_rxId/s_rxDlc/s_rxTruncated/s_rxValid 是一组,必须整体
* 被 buildInput 读到。DC 模式下 buildInput 跑在 Sync0_Isr(preempt=0),会
* 抢占本函数(主循环 taskCanRxParse)。原注释声称"valid 最后置位保证一致"
* 是错的:valid 一旦置位就永不清零,ISR 落在"新 payload 已写完、id 还没
* 更新"的窗口里,会把新帧数据配旧帧 ID 发出去——STATUS 字节被标成 CONFIG
* (0x000N0101),前端按 CONFIG 布局解码出 -3e26 这类离谱目标值。
* 协议:seq 奇数=正在写。消费者在单核 ISR 里生产者必然冻结,判一次奇偶
* 即可,奇数则本周期跳过(段 1/段 3 已清零=无效),下周期自愈。
* 不关中断:critical.h 的 BASEPRI 方案放行 preempt=0,挡不住 Sync0;
* 而 seqlock 零抖动,与本文件既有无锁约定一致。
* __DMB 防编译器把非 volatile 的 memcpy 挪出两次 seq 自增之间。 */
++s_xrxSeq; /* → 奇数:快照进入"正在写" */
/* ── 快照 seqlock:奇数=正在写,消费者本拍跳过段 1/段 3 ── */
++s_xrxSeq;
__DMB();
if (s_rxValid && !s_rxPublished) {
++s_dropCount;
}
memcpy(s_rxData, data, copyLen);
if (copyLen < kEcatCanXRxDataMax) {
memset(&s_rxData[copyLen], 0, (size_t)(kEcatCanXRxDataMax - copyLen));
@@ -342,37 +452,33 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
s_rxCh = channel;
s_rxId = id;
s_rxDlc = length; /* 报告原始长度,便于主站发现被截断 */
s_rxDlc = length;
s_rxTruncated = (length > kEcatCanRxDataMax);
++s_rxCount;
s_rxValid = true;
s_rxPublished = false;
if (canSubId(id) == 0x00FFU) {
if (s_rx00ffCount < 0xFFFFU) {
++s_rx00ffCount;
}
}
/* 版本应答:只做短粘滞,供主站在窗口内采到;不长期霸占 xrx */
if (canSubId(id) == 0x00FFU) {
s_verCh = channel;
s_verDlc = length;
s_verId = id;
memcpy(s_verData, data, copyLen);
if (copyLen < kEcatCanXRxDataMax) {
memset(&s_verData[copyLen], 0, (size_t)(kEcatCanXRxDataMax - copyLen));
}
s_verHoldUntilMs = SysTickTimer::getMs() + kVerHoldMs;
s_verValid = true;
}
__DMB();
++s_xrxSeq; /* → 偶数:快照整体一致 */
/* ── 推入批量接收环形队列(1.5.56)──
* 与上面的"最近一帧快照"并行维护:快照会被后帧覆盖(这是它的设计语义),
* 队列则保序累积,供 fillRxBatch 一次回收多帧。
* 队列满时丢**新**帧并计数,而不是覆盖旧帧 —— 批量区的价值在于保序完整,
* 覆盖会在序列中间留下空洞,比明确丢尾更难排查。 */
const uint8_t next = (uint8_t)((s_rxRingHead + 1U) % kEcatRxRingSlots);
if (next == s_rxRingTail) {
if (s_rxRingLost < 0xFFU) {
++s_rxRingLost;
}
} else {
RxRingItem &it = s_rxRing[s_rxRingHead];
uint8_t n = length;
if (n > kEcatCanFdDataMax) {
n = kEcatCanFdDataMax;
}
it.id = id;
it.ch = channel;
it.dlc = n;
memcpy(it.data, data, n);
__DMB(); /* 数据写入先于 head 发布落地(volatile 不约束非 volatile 的 memcpy) */
s_rxRingHead = next; /* 最后推进 head:消费者读到时数据必已写完 */
}
++s_xrxSeq;
}
extern "C" {
@@ -387,4 +493,9 @@ void EcatCan_BuildInput(uint8_t *in)
EthercatCanBridge::buildInput(in);
}
void EcatCan_ResetSequenceState(void)
{
EthercatCanBridge::resetSequenceState();
}
} /* extern "C" */
+135 -130
View File
@@ -1,9 +1,9 @@
/**
* @file ethercat_can_bridge.h
* @brief EtherCAT PDO ↔ CAN 转发桥(1.5.55)。
* @brief EtherCAT PDO ↔ CAN 转发桥。
*
* 输出 PDO(0x7000, 主站→从站, 64B) = 待发送的 CAN 帧(数据区 32B)
* 输入 PDO(0x6000, 从站→主站, 384B) = 桥状态 + 电机状态窗口 + 长帧快照
* 输出 PDO(0x7000, 主站→从站, 384B) = 待发送的 CAN 帧(数据区 32B)
* 输入 PDO(0x6000, 从站→主站, 600B) = 桥状态 + 电机整轮状态 + 长帧快照 + 诊断
*
* 发送用**序号沿触发**:仅当 seq 字节变化时才真正发一帧。PDO 是周期性刷新的,
* 若按「内容非空即发」会把同一帧每个周期重发一次;seq 变化沿是 PDO 里表达
@@ -13,38 +13,17 @@
* TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态
* 会随机丢帧。改为由 taskCanRxParse() 的分发链回调 onCanFrame() 投递。
*
* ── 1.5.55 为电机控制台扩容(32B/32B → 64B/384B)──
* 段 2 语义:一帧 = 总线电机整轮状态。
*
* 三个动机:
* 1. TX 数据区 24B→32B。电机 CMD 是 DLC 32,旧 processOutput 里
* `dlc > kEcatCanTxDataMax` 直接钳位到 24,后 8 字节被**静默丢弃**且不报错。
* 2. 输入侧加电机状态窗口。单帧快照对电机控制台不够用:9 节点各 32B STATUS,
* 一周期只带回一帧、新帧还覆盖旧帧,永远取不齐。改为直接从 MotorState
* 状态表(已按 node×channel 聚合,见 device/motor_state.h)取一个窗口,
* 主站移动 win_base 即可 4 个周期扫完全部 27 槽。
* 3. 加长帧快照区。旧 RX 数据区只有 8B,版本帧(DLC24)/CONFIG 帧(DLC24) 会被截断。
* · 每一拍输入 PDO 携带**总线上已识别电机的完整一轮状态**(最多 9 个);
* · **空槽不发送**:密排,count = 本拍实际打包的电机数;
* · 主站按 count 迭代,每槽自带 node_id,自动识别,无需 win_base 滑窗;
* · 版本/CONFIG 走段 1/段 3「最近一帧」快照;
* · 上一轮已打包且 seq 未变的电机,本轮跳过 MotorState 重读与 48B 拷贝,
* 直接复用影子区(s_motShadow)。
*
* **输入 PDO 前 32 字节与 1.5.53 逐字节兼容**,偏移与语义全部不变。这样
* tools/ecat 的 CAN 调试页(ecat.html)与 ecat_server.py 的 _can_decode_input
* 不改一行也照常工作,扩容只是在后面追加新区段。
*
* ── 1.5.56 扩到 DPRAM 硬上限并加 CANFD 批量帧区(64/384 → 384/640)──
*
* 动机:需要一个周期里合并下发/回收**多条 CANFD 帧**。1.5.55 的单帧槽
* (一个 seq 一帧)每周期最多一帧,4ms 周期就是 250 帧/s 上限,
* 而一次多轴同步下发就要 9 帧,靠排队要 36ms 才发完 —— 各轴指令不同拍,
* 失去"同步下发"的意义。
*
* 新增两个批量区,每条帧带 8B 头(ch/dlc/flags/rsvd + id)+ 数据:
* 输出 48..383(336B):批量发送,一个 seq 沿一次性发 N 帧
* 输入 360..639(280B):批量接收,一周期回收多帧而不是只带最近一帧
*
* 三条兼容性铁律(改动只做追加,不动既有偏移):
* 1. 输出 0..47 与 1.5.55 单帧布局完全一致(seq/ch/dlc/flags/id/win/data32);
* 2. 输入 0..359 与 1.5.55 完全一致(段1 桥状态 + 段2 电机窗口 + 段3 长帧);
* 3. 批量区**为空时**(count=0)行为与 1.5.55 完全相同,老主站照旧可用。
*
* 上限推导(为何是 384+640 而不是更大)见 comm/ethercat_pdo_size.h。
* 输入 PDO 总长 600B;主站解码必须与段 2/3 偏移一致
* (tools/ecat 的 decode_input / transport-ecat.js 等)。
*/
#ifndef COMM_ETHERCAT_CAN_BRIDGE_H
#define COMM_ETHERCAT_CAN_BRIDGE_H
@@ -52,20 +31,21 @@
#include <cstdint>
#include "comm/ethercat_pdo_size.h"
#include "device/motor_state.h" /* kMotorMaxNodes / kMotorChannels */
/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。**与 1.5.55 完全兼容**。 */
/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。 */
enum : uint8_t {
ECAT_CAN_TX_OFF_SEQ = 0U, /**< 发送序号:变化沿触发一次发送(单帧+批量一起) */
ECAT_CAN_TX_OFF_CH = 1U, /**< 目标通道 0/1/2 */
ECAT_CAN_TX_OFF_DLC = 2U, /**< 数据长度 0..32;0 表示本次不发单帧(只发批量) */
ECAT_CAN_TX_OFF_FLAGS = 3U, /**< bit0=1 请求清零统计;其余保留 */
ECAT_CAN_TX_OFF_ID = 4U, /**< CAN ID,小端 uint32(29 位扩展帧) */
ECAT_CAN_TX_OFF_WIN = 8U, /**< 请求回传的电机槽窗口起始下标 0..26 */
/* 9..15 保留:让数据区落在 16 字节边界,hexdump 排查时对齐好读 */
ECAT_CAN_TX_OFF_WIN = 8U, /**< 当前忽略(电机整轮密排,无滑窗);保留占位 */
/* 9..15 保留:让数据区落在 16 字节边界 */
ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */
};
/** 输出 PDO 段 2(48..383):CANFD 批量发送区(1.5.56 新增)。 */
/** 输出 PDO 段 2(48..383):CANFD 批量发送区。 */
enum : uint16_t {
ECAT_TXB_OFF_COUNT = 48U, /**< 本次要发的帧数 0..kEcatTxBatchMax;0=不发批量 */
ECAT_TXB_OFF_FLAGS = 49U, /**< bit0=1 遇错即停(默认继续发完并各自记结果) */
@@ -83,7 +63,7 @@ enum : uint8_t {
ECAT_FRM_HDR_SIZE = 8U, /**< 头长度,数据紧跟其后 */
};
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照。**与 1.5.53 完全兼容**。 */
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照 + 收发诊断。 */
enum : uint8_t {
ECAT_CAN_RX_OFF_ACK = 0U, /**< 回显已执行的发送序号,主站据此确认送达 */
ECAT_CAN_RX_OFF_CH = 1U, /**< 该帧来自哪个通道 */
@@ -95,65 +75,86 @@ enum : uint8_t {
ECAT_CAN_RX_OFF_TXRES = 20U, /**< 最近一次发送结果(CanTxResult 原值,0=OK) */
ECAT_CAN_RX_OFF_BACKLOG = 21U, /**< 该通道软件 TX 队列积压 */
ECAT_CAN_RX_OFF_TXCNT = 22U, /**< 累计发送受理帧数,小端 uint16 */
ECAT_CAN_RX_OFF_DROP = 24U, /**< 桥接层丢帧数(投递时上一帧还没被取走),小端 uint16 */
ECAT_CAN_RX_OFF_DROP = 24U, /**< 段 1/段 3 快照「未取先覆」次数(u16 累计回绕) */
ECAT_CAN_RX_OFF_TXTXID = 26U, /**< 最近 enqueueTx 成功的 CAN ID,小端 uint32 */
ECAT_CAN_RX_OFF_RX00FF = 30U, /**< CAN RX 收到 0x00FF 次数,小端 uint16 */
};
/** 输入 PDO 段 2(32..319):电机状态窗口头 + 槽数组。 */
/**
* 输入 PDO 段 2(32..543):电机整轮状态(密排)。
*
* 主站解析:
* count = in[32] // 0..9,本拍实际打包的电机数
* for (i = 0; i < count; i++)
* slot = in[40 + i*56] // node_id/online/dlc/seq/status
*
* 空节点不占位;槽序按 node_id 升序;同号多通道取 seq 最新的一条。
*/
enum : uint16_t {
ECAT_MOTW_OFF_ACTIVE = 32U, /**< 在线电机数 */
ECAT_MOTW_OFF_BOOT = 33U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */
ECAT_MOTW_OFF_WIN_BASE = 34U, /**< 本帧携带的槽窗口起始下标(回显主站请求) */
ECAT_MOTW_OFF_WIN_COUNT = 35U, /**< 本帧携带的有效槽数(≤7) */
ECAT_MOTW_OFF_GLOBAL_SEQ = 36U,/**< 状态表全局刷新序号,小端 uint32 */
ECAT_MOTW_OFF_SLOTS = 40U, /**< 槽数组起点,7×40B */
ECAT_MOTW_OFF_COUNT = 32U, /**< 本拍密排电机数 0..9(主站按此迭代) */
ECAT_MOTW_OFF_ACTIVE = 33U, /**< 在线电机数(与 count 可不同:离线但仍报最后一轮) */
ECAT_MOTW_OFF_BOOT = 34U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */
ECAT_MOTW_OFF_MAGIC = 35U, /**< 恒 kEcatMotLayoutMagic(0xA5):密排布局标识 */
ECAT_MOTW_OFF_GLOBAL_SEQ = 36U, /**< 状态表全局刷新序号,小端 uint32 */
ECAT_MOTW_OFF_SLOTS = 40U, /**< 密排槽数组起点,count×56B */
};
/** 电机状态槽内部布局(40 字节)。 */
/** 电机状态槽内部布局(56 字节)。 */
enum : uint8_t {
ECAT_MOT_OFF_NODE = 0U, /**< 节点号 1..9,0=空槽 */
ECAT_MOT_OFF_NODE = 0U, /**< 节点号 1..9(密排下不会出现 0) */
ECAT_MOT_OFF_CH = 1U, /**< CAN 通道 0/1/2 */
ECAT_MOT_OFF_ONLINE = 2U, /**< 1=在线 */
ECAT_MOT_OFF_DLC = 3U, /**< 最后一帧 STATUS 的 DLC(正常 32) */
ECAT_MOT_OFF_DLC = 3U, /**< 最后一帧 STATUS 的 DLC(正常 48) */
ECAT_MOT_OFF_SEQ = 4U, /**< STATUS 刷新序号,小端 uint32 */
ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,32 字节 */
ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,48 字节 */
};
/** 输入 PDO 段 3(320..359):最近一帧**完整**快照(版本/CONFIG 帧要 24B)。 */
/** 输入 PDO 段 3(544..583):最近一帧完整快照(版本/CONFIG 帧要 24B+)。 */
enum : uint16_t {
ECAT_XRX_OFF_CH = 320U, /**< 通道 */
ECAT_XRX_OFF_DLC = 321U, /**< 原始 DLC(可达 32) */
ECAT_XRX_OFF_FLAGS = 322U, /**< bit0=有效 */
ECAT_XRX_OFF_RSVD = 323U,
ECAT_XRX_OFF_ID = 324U, /**< CAN ID,小端 uint32 */
ECAT_XRX_OFF_DATA = 328U, /**< 数据区,32 字节完整 payload */
ECAT_XRX_OFF_CH = 544U, /**< 通道 */
ECAT_XRX_OFF_DLC = 545U, /**< 原始 DLC(可达 32) */
ECAT_XRX_OFF_FLAGS = 546U, /**< bit0=有效 */
ECAT_XRX_OFF_RSVD = 547U,
ECAT_XRX_OFF_ID = 548U, /**< CAN ID,小端 uint32 */
ECAT_XRX_OFF_DATA = 552U, /**< 数据区,32 字节完整 payload */
};
/** 输入 PDO 段 4(360..639):CANFD 批量接收区(1.5.56 新增)。 */
/** 输入 PDO 段 5(584..595):设备诊断区(位置未变)。 */
enum : uint16_t {
ECAT_RXB_OFF_COUNT = 360U, /**< 本周期回收的帧数 0..kEcatRxBatchMax */
ECAT_RXB_OFF_LOST = 361U, /**< 本周期因批量区放不下而丢弃的帧数 */
ECAT_RXB_OFF_PENDING = 362U, /**< 环形队列中仍待回收的帧数(主站据此提速) */
ECAT_RXB_OFF_RSVD = 363U,
ECAT_RXB_OFF_ITEMS = 364U, /**< 帧数组起点,项布局同 ECAT_FRM_OFF_* */
ECAT_DIAG_OFF_LOAD = 584U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%) */
ECAT_DIAG_OFF_TORN = 586U, /**< CAN 取帧撕裂丢帧数(低 16 位,饱和) */
ECAT_DIAG_OFF_SEQ = 588U, /**< 诊断区序号,小端 uint16,每完成 1s 窗口 +1 */
ECAT_DIAG_OFF_OVERFULL = 590U, /**< CAN RX FIFO 满溢计数 RFO0(u16 饱和) */
ECAT_DIAG_OFF_RELTIMEOUT = 592U, /**< RFD0 释放握手超时计数(u16 饱和) */
ECAT_DIAG_OFF_REJECT = 594U, /**< CAN 内容级拼接帧命中计数(u16 饱和) */
};
/** 单帧数据在输出 PDO / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */
constexpr uint8_t kEcatCanTxDataMax = 32U;
/** 段 1 短快照数据区字节数(保持 1.5.53 兼容,勿改)。 */
/** 段 1 短快照数据区字节数。 */
constexpr uint8_t kEcatCanRxDataMax = 8U;
/** 段 3 长快照数据区字节数。 */
constexpr uint8_t kEcatCanXRxDataMax = 32U;
/** 单个电机状态槽字节数。 */
constexpr uint8_t kEcatMotSlotSize = 40U;
/** 一份输入 PDO 携带的电机状态槽数。 */
constexpr uint8_t kEcatMotSlotsPerPdo = 7U;
/** 单个电机状态槽字节数:8B 头 + 48B STATUS。 */
constexpr uint8_t kEcatMotSlotSize = 56U;
/** 一拍最多打包的电机数 = 节点号 1..9 各一槽(同号多通道取最新)。 */
constexpr uint8_t kEcatMotSlotsPerPdo = kMotorMaxNodes; /* 9 */
/** 段 2 头长度(COUNT..GLOBAL_SEQ,8B)。 */
constexpr uint8_t kEcatMotHdrSize = 8U;
/** 段 2 密排区最大字节:头 8B + 9×56B = 512B,落在 32..543。 */
constexpr uint16_t kEcatMotRegionBytes =
(uint16_t)(kEcatMotHdrSize
+ (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize);
/** 布局魔数,写在 offset 35。主站据此区分密排布局与旧滑窗布局(两者
* Ibytes 同为 600,不能只靠长度分支)。0=非密排布局。 */
constexpr uint8_t kEcatMotLayoutMagic = 0xA5U;
/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h(C/C++ 共用)。 */
constexpr uint16_t kEcatPdoOutBytes = ECAT_PDO_OUT_BYTES; /* 48×64bit = 384B */
constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 80×64bit = 640B */
/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h。 */
constexpr uint16_t kEcatPdoOutBytes = ECAT_PDO_OUT_BYTES; /* 384B */
constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 600B */
/** CANFD 单帧数据上限(DLC 64)—— 批量区按此上限做容量推导。 */
/** CANFD 单帧数据上限。 */
constexpr uint8_t kEcatCanFdDataMax = 64U;
/** 批量发送区可用字节数与最坏情况(全 DLC64)帧数上限。 */
@@ -161,57 +162,52 @@ constexpr uint16_t kEcatTxBatchBytes = (uint16_t)(kEcatPdoOutBytes - ECAT_TXB_OF
constexpr uint8_t kEcatTxBatchMax =
(uint8_t)(kEcatTxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax));
/** 批量接收区可用字节数与最坏情况帧数上限。 */
constexpr uint16_t kEcatRxBatchBytes = (uint16_t)(kEcatPdoInBytes - ECAT_RXB_OFF_ITEMS);
constexpr uint8_t kEcatRxBatchMax =
(uint8_t)(kEcatRxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax));
/** 批量接收环形队列槽数。取 kEcatRxBatchMax 的 4 倍,让主站慢一两拍也不丢帧。
* 注意这是 RAM 成本(槽数 × 72B),不是 PDO 成本。 */
constexpr uint8_t kEcatRxRingSlots = (uint8_t)(kEcatRxBatchMax * 4U);
/* 布局守卫:段 2 的槽数组必须完整落在输入 PDO 内,段 3 也不能越界。
* 改槽数/槽大小时忘了同步 PDO 长度,会在这里编译期失败。 */
static_assert(ECAT_MOTW_OFF_SLOTS + kEcatMotSlotsPerPdo * kEcatMotSlotSize
<= ECAT_XRX_OFF_CH, "电机槽窗口与长帧快照区重叠");
static_assert(ECAT_XRX_OFF_DATA + kEcatCanXRxDataMax <= ECAT_RXB_OFF_COUNT,
"长帧快照区与批量接收区重叠");
/* ── 布局守卫 ── */
static_assert(ECAT_MOTW_OFF_SLOTS
+ (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize
== ECAT_XRX_OFF_CH,
"段2密排区必须与段3首尾相接");
static_assert(ECAT_XRX_OFF_DATA + kEcatCanXRxDataMax == ECAT_DIAG_OFF_LOAD,
"段3必须与段5首尾相接");
static_assert(ECAT_CAN_TX_OFF_DATA + kEcatCanTxDataMax <= ECAT_TXB_OFF_COUNT,
"单帧发送数据区与批量发送区重叠");
static_assert(ECAT_RXB_OFF_ITEMS < kEcatPdoInBytes, "批量接收区起点越界");
static_assert(ECAT_TXB_OFF_ITEMS < kEcatPdoOutBytes, "批量发送区起点越界");
/* 批量区至少要装得下一帧满长 CANFD,否则"批量"没有意义 */
static_assert(kEcatTxBatchMax >= 1U, "批量发送区装不下一帧 DLC64");
static_assert(kEcatRxBatchMax >= 1U, "批量接收区装不下一帧 DLC64");
static_assert(ECAT_DIAG_OFF_REJECT + 2U <= kEcatPdoInBytes, "段5诊断区越界");
static_assert(ECAT_MOTW_OFF_COUNT + kEcatMotRegionBytes <= ECAT_XRX_OFF_CH,
"段2密排区越界");
/**
* @brief EtherCAT ↔ CAN 转发桥。
* @note 全静态;PDO 侧函数在 Sync0_Isr(preempt=0) 或主循环 MainLoop 上下文执行,
* onCanFrame() 在主循环 taskCanRxParse() 上下文执行。共享状态用 volatile +
* 单生产者/单消费者语义,不需要临界区(投递只写、消费只读+清标志)。
* 例外:段 1/段 3 的"最近一帧快照"是多字段一组(id/dlc/data 必须配套),
* 用 s_xrxSeq seqlock 保证消费者不会读到"新数据+旧 ID"的混搭——
* 单生产者/单消费者语义。多字段一组用 seqlock:
* - 段 1/段 3 的「最近一帧快照」→ s_xrxSeq;
* - 电机槽 48B payload → MotorSlot.wr_seq。
* Sync0 是 preempt=0,critical.h 的 BASEPRI 方案挡不住它,故只能无锁协议。
*/
class EthercatCanBridge {
public:
/**
* @brief 处理一份输出 PDO:seq 变化时向 CAN 发一帧。
* @param[in] out kEcatPdoOutBytes 字节输出过程数据。
* @note 由 APPL_Application() 每周期调用。
* @brief 输出探针:seq 变化时请求全量读。
* @note 不再刷新 win_base(电机整轮密排,无滑窗);仍处理 seq 沿
* 与 FLAGS bit0 清统计。
*/
static bool peekOutput(const uint8_t *head);
/** @brief 复位输出 sequence 与冷启动同步状态。 */
static void resetSequenceState();
static void processOutput(const uint8_t *out);
/**
* @brief 填充输入 PDO:桥状态 + 电机状态窗口 + 长帧快照。
* @param[out] in kEcatPdoInBytes 字节输入过程数据(本函数负责全部写满)。
* @brief 填充输入 PDO:桥状态 + 电机整轮密排 + 长帧快照。
* @param[out] in kEcatPdoInBytes 字节输入过程数据。
*/
static void buildInput(uint8_t *in);
/**
* @brief CAN 收帧投递入口。
* @note 由 TaskRunner::taskCanRxParse() 在分发链里调用,不消费帧(不返回
* "已处理"),只做旁路复制,因此不影响 OTA relay / 电机状态。
* @brief CAN 收帧投递入口(旁路复制,不消费帧)。
*/
static void onCanFrame(uint8_t channel, uint32_t id,
const uint8_t *data, uint8_t length);
@@ -221,29 +217,31 @@ public:
private:
/**
* @brief 处理输出 PDO 的批量发送区(seq 沿内调用一次)。
* @return 实际受理的帧数。
* @note 逐帧解析 (8B 头 + dlc),边界不足即停 —— 不信任主站给的 count,
* 越界的 count 只会让循环提前结束,不会读到 PDO 之外。
* @brief 填充段 2 电机整轮密排区(含差分跳过)。
* @return 本拍打包的电机数(写入 count 字段)。
*/
static uint8_t fillMotorRound(uint8_t *in);
static uint8_t sendBatch(const uint8_t *out);
/** @brief 把接收环形队列里的帧尽量装进输入 PDO 批量区。 */
static void fillRxBatch(uint8_t *in);
static volatile uint8_t s_lastSeq; /**< 上一次已执行的发送序号 */
static volatile bool s_seqPrimed; /**< 是否已记录过基准 seq(防上电首帧误发) */
static volatile uint8_t s_lastSeq;
static volatile bool s_seqPrimed;
static volatile bool s_coldStartPending;
static volatile uint8_t s_lastTxResult;
static volatile uint16_t s_txCount;
static volatile uint16_t s_dropCount;
/** 主站请求的电机槽窗口起始下标(输出 PDO 的 WIN 字节,每周期刷新)。 */
static volatile uint8_t s_winBase;
/** 最近一次 enqueueTx 成功的 CAN ID(诊断:ECAT 是否真的发出去) */
static volatile uint32_t s_lastTxId;
/** CAN RX 收到 0x00FF(版本应答)次数(诊断:电机是否回帧) */
static volatile uint16_t s_rx00ffCount;
/** processOutput 本拍读到的输出 PDO seq(诊断:ESC 输出是否到达应用层) */
static volatile uint8_t s_outSeenSeq;
/* 最近一帧接收快照:taskCanRxParse 写、PDO 读。
* s_xrxSeq 为 seqlock:奇数=正在写,buildInput 本周期跳过段 1/段 3。
* 没有它,Sync0_Isr 抢占在"数据已写完、id 未更新"之间会把新帧数据
* 配旧帧 ID 发出去(STATUS 被标成 CONFIG → 前端解出离谱目标值)。 */
/* 最近一帧接收快照:taskCanRxParse 写、PDO 读。s_xrxSeq 为 seqlock。
* STATUS(0x0100) 禁止写入本快照 —— 它由 MotorState 段2 密排承载;
* 否则高频 STATUS 会持续冲掉版本应答(0x00FF)等低频长帧。 */
static volatile bool s_rxValid;
static volatile bool s_rxPublished;
static volatile uint32_t s_xrxSeq;
static volatile uint8_t s_rxCh;
static volatile uint8_t s_rxDlc;
@@ -252,20 +250,25 @@ private:
static volatile uint32_t s_rxCount;
static uint8_t s_rxData[kEcatCanXRxDataMax];
/* ── 批量接收环形队列(1.5.56)──
* 单生产者(taskCanRxParse 的 onCanFrame) / 单消费者(PDO 的 fillRxBatch)。
* head/tail 各由一侧独占写入,故无需临界区:生产者只推进 head、
* 消费者只推进 tail,最坏情况是某周期少读一帧,下周期即取到。 */
struct RxRingItem {
uint32_t id;
uint8_t ch;
uint8_t dlc;
uint8_t data[kEcatCanFdDataMax];
};
static RxRingItem s_rxRing[kEcatRxRingSlots];
static volatile uint8_t s_rxRingHead; /**< 生产者写入位置 */
static volatile uint8_t s_rxRingTail; /**< 消费者读取位置 */
static volatile uint8_t s_rxRingLost; /**< 队列满导致的丢帧数(累计,回填后清零) */
/* 版本应答粘滞快照:0x00FF 到达后在 PDO 段3 保持发布一段时间,
* 避免被随后的 CONFIG 等帧在主站采样前覆盖。 */
static volatile bool s_verValid;
static volatile uint32_t s_verHoldUntilMs;
static uint8_t s_verCh;
static uint8_t s_verDlc;
static uint32_t s_verId;
static uint8_t s_verData[kEcatCanXRxDataMax];
/* ── 段 2 差分填充影子 ──
* 上一拍已发布的密排区副本 + 各节点上次发布的 seq/online + 头字段。
* 节点集合、seq、online、active/boot 全未变时整段复用,跳过 MotorState
* 重读与逐槽 48B 拷贝。掩码必须 u16:节点号 1..9,bit9 超出 uint8_t。 */
static uint8_t s_motShadow[kEcatMotRegionBytes];
static uint32_t s_motPubSeq[kMotorMaxNodes + 1U]; /* 下标=节点号 */
static uint8_t s_motPubOnline[kMotorMaxNodes + 1U]; /* checkOffline 不 bump seq,须单独比 */
static uint16_t s_motPubMask; /**< bit n=1:节点 n 在影子区中 */
static uint8_t s_motPubCount;
static bool s_motShadowValid;
};
extern "C" {
@@ -273,6 +276,8 @@ extern "C" {
void EcatCan_ProcessOutput(const uint8_t *out);
/** @brief 供 dc_app.c 调用的 shim:填充输入 PDO。 */
void EcatCan_BuildInput(uint8_t *in);
/** @brief 供 dc_app.c 调用的 shim:复位输出 sequence 状态机。 */
void EcatCan_ResetSequenceState(void);
}
#endif /* COMM_ETHERCAT_CAN_BRIDGE_H */
@@ -0,0 +1,218 @@
/**
* @file ethercat_diff_stats.cpp
* @brief [DIFF-STATS] 输入镜像差分统计实现(度量定义与存在理由见 ethercat_diff_stats.h)。
*
* 整文件由 CONFIG_ECAT_DIFF_STATS 门控:置 0 时不参与编译,挂接点各自也有 #if,
* 因此关掉之后固件与改造前逐字节等价。
*/
#include "comm/ethercat_diff_stats.h"
#include <cstring>
#include "RTE_Components.h"
#include CMSIS_device_header
#include "comm/ethercat_pdo_size.h"
#include "sys/config.h"
#if CONFIG_ECAT_DIFF_STATS
namespace {
constexpr uint16_t kInBytes = (uint16_t)ECAT_PDO_IN_BYTES;
constexpr uint32_t kInWords = (uint32_t)kInBytes / 4U; /* 600/4=150,无尾巴字节 */
constexpr uint8_t kSegCount = 5U;
/**
* @brief 各段字节上界(开区间),用于把变化量归到布局段落。
* @note 1.6.21 布局:段1(0..31) 段2(32..543) 段3(544..583) 段4 已删 段5(584..599)。
* 段4 上界=段5 起点,区间为空、恒 0,子索引保留以免动对象字典。
*/
constexpr uint16_t kSegEnd[kSegCount] = {32U, 544U, 584U, 584U, kInBytes};
/** @brief 上一份已发布的镜像。alignas(4) 是让优化器能把 4 字节 memcpy 折成单条
* LDR 的前提——丢了它,比对会从 ~150 次字访问退化成 600 次字节访问。 */
alignas(4) uint8_t s_prev[kInBytes];
bool s_havePrev = false;
/* ── 正在累积的窗口 ── */
uint32_t s_curCycles = 0U;
uint32_t s_curChangedMax = 0U;
uint32_t s_curRunsTotal = 0U;
uint32_t s_curRunsMax = 0U;
uint32_t s_curCompareCyc = 0U;
uint32_t s_curSeg[kSegCount] = {0U, 0U, 0U, 0U, 0U};
uint32_t s_winStart = 0U;
/* ── 上一个已完成的窗口(getter 只读这份,保证主站拿到的是稳定值)── */
uint32_t s_winCycles = 0U;
uint32_t s_winChangedMax = 0U;
uint32_t s_winRunsTotal = 0U;
uint32_t s_winRunsMax = 0U;
uint32_t s_winCompareCyc = 0U;
uint32_t s_winSeg[kSegCount] = {0U, 0U, 0U, 0U, 0U};
uint32_t s_windowSeq = 0U;
/** @brief 字节偏移所属段号。只对"确实变了"的字调用,线性 5 次比较足够。 */
uint8_t segOf(uint32_t byteOff)
{
for (uint8_t i = 0U; i < kSegCount; ++i) {
if (byteOff < (uint32_t)kSegEnd[i]) {
return i;
}
}
return (uint8_t)(kSegCount - 1U); /* 理论上到不了:kSegEnd[4] == kInBytes */
}
/** @brief 结算窗口:把累加值搬成"上一个完整窗口",再清零重新计时。 */
void rollWindow(uint32_t now)
{
s_winCycles = s_curCycles;
s_winChangedMax = s_curChangedMax;
s_winRunsTotal = s_curRunsTotal;
s_winRunsMax = s_curRunsMax;
s_winCompareCyc = s_curCompareCyc;
for (uint8_t i = 0U; i < kSegCount; ++i) {
s_winSeg[i] = s_curSeg[i];
s_curSeg[i] = 0U;
}
s_curCycles = 0U;
s_curChangedMax = 0U;
s_curRunsTotal = 0U;
s_curRunsMax = 0U;
s_curCompareCyc = 0U;
s_winStart = now;
s_windowSeq++;
}
} /* namespace */
/**
* @brief 与上一份已发布镜像比对并累计统计。
* @note .ramfunc:每拍在 PDI_Isr 里跑。放 flash 会带来等待态,既污染本仪表
* 自己要测的 compareCyc,也给被测的写侧窗口添无谓的额外占用。
*/
__attribute__((section(".ramfunc"), noinline))
void EthercatDiffStats::observe(const uint8_t *cur) noexcept
{
const uint32_t t0 = DWT->CYCCNT;
if (!s_havePrev) {
/* 首拍只建基准:否则第一拍会把"整 600B 都变了"记成样本,把 changedMax
* 和段计数全钉在无意义的上限上。 */
std::memcpy(s_prev, __builtin_assume_aligned(cur, 4), kInBytes);
s_havePrev = true;
s_winStart = DWT->CYCCNT;
return;
}
uint32_t changed = 0U;
uint32_t runs = 0U;
bool inRun = false;
/* 对齐必须在调用点再声明一次:alignas(4) 只到形参为止,函数签名是
* const uint8_t*,优化器看不见对齐就无从折字访问。实测两版都因此慢到不能用——
* 移位版反汇编出 8 条 ldrb.w/字(41µs/拍),memcpy 版 21µs/拍 —— 而这个数会被
* 直接读成"差分判断很贵",是仪表自己造的假象,不是差分的成本。 */
const uint8_t *prevW = static_cast<const uint8_t *>(__builtin_assume_aligned(s_prev, 4));
const uint8_t *curW = static_cast<const uint8_t *>(__builtin_assume_aligned(cur, 4));
for (uint32_t w = 0U; w < kInWords; ++w) {
const uint32_t o = w * 4U;
uint32_t a;
uint32_t b;
/* assume_aligned + memcpy:不碰严格别名,又能让 -Oz 折成单条 LDR。 */
std::memcpy(&a, prevW + o, 4U);
std::memcpy(&b, curW + o, 4U);
if (a == b) {
inRun = false;
continue;
}
changed += 4U;
if (!inRun) {
runs++;
inRun = true;
}
s_curSeg[segOf(o)] += 4U;
}
s_curCycles++;
s_curRunsTotal += runs;
if (changed > s_curChangedMax) { s_curChangedMax = changed; }
if (runs > s_curRunsMax) { s_curRunsMax = runs; }
std::memcpy(s_prev, curW, kInBytes);
const uint32_t t1 = DWT->CYCCNT;
/* 比对自身开销:不含上面那次 600B 快照搬运(那是差分写真要做时要付的成本之一,
* 单列出来才看得清"光判断"值多少钱)。 */
s_curCompareCyc += (uint32_t)(t1 - t0);
/* SystemCoreClock 个计数 = 1s(与 sys/cpu_load.cpp 同法;DWT 32 位约 23.8s
* 绕回,窗口远短于此,无绕回问题)。 */
if ((uint32_t)(t1 - s_winStart) >= SystemCoreClock) {
rollWindow(t1);
}
}
void EthercatDiffStats::reset() noexcept
{
s_havePrev = false;
}
uint32_t EthercatDiffStats::windowSeq() noexcept { return s_windowSeq; }
uint32_t EthercatDiffStats::cycles() noexcept { return s_winCycles; }
uint32_t EthercatDiffStats::changedMax() noexcept { return s_winChangedMax; }
uint32_t EthercatDiffStats::runsTotal() noexcept { return s_winRunsTotal; }
uint32_t EthercatDiffStats::runsMax() noexcept { return s_winRunsMax; }
uint32_t EthercatDiffStats::seg1Bytes() noexcept { return s_winSeg[0]; }
uint32_t EthercatDiffStats::seg2Bytes() noexcept { return s_winSeg[1]; }
uint32_t EthercatDiffStats::seg3Bytes() noexcept { return s_winSeg[2]; }
uint32_t EthercatDiffStats::seg4Bytes() noexcept { return s_winSeg[3]; }
uint32_t EthercatDiffStats::seg5Bytes() noexcept { return s_winSeg[4]; }
uint32_t EthercatDiffStats::compareCyc() noexcept { return s_winCompareCyc; }
/* ── 供 dc_app.c(C 侧对象字典) 取值:一次拿全 11 个窗口值,避免 11 次跨语言调用 ── */
extern "C" {
/**
* @brief [DIFF-STATS] 读上一个完整 1s 窗口的统计值。
* @param[out] out 依次对应 0x2000 sub13..sub23。
* 形参用 uint32_t 而非 SSC 的 UINT32 别名:本翻译单元不引 SSC 头,
* 两者同为 32 位无符号,C 侧声明仍按 UINT32 写。
*/
void EcatDiffStats_GetWindow(uint32_t *out)
{
if (out == nullptr) {
return;
}
out[0] = EthercatDiffStats::windowSeq();
out[1] = EthercatDiffStats::cycles();
out[2] = EthercatDiffStats::changedMax();
out[3] = EthercatDiffStats::runsTotal();
out[4] = EthercatDiffStats::runsMax();
out[5] = EthercatDiffStats::seg1Bytes();
out[6] = EthercatDiffStats::seg2Bytes();
out[7] = EthercatDiffStats::seg3Bytes();
out[8] = EthercatDiffStats::seg4Bytes();
out[9] = EthercatDiffStats::seg5Bytes();
out[10] = EthercatDiffStats::compareCyc();
}
} /* extern "C" */
#else /* !CONFIG_ECAT_DIFF_STATS */
/* 关掉时仍导出同名取值函数并填 0:这样 dc_app.c 的挂接点不需要再套一层
* #if,子索引数量也保持与开启版一致(主站读到的恒 0,脚本按 winseq==0 判'未启用')。
* 留一个空函数体反而会漏掉符号——1.6.20 首编就是栽在这里(L6218E)。 */
extern "C" void EcatDiffStats_GetWindow(uint32_t *out)
{
if (out == nullptr) {
return;
}
for (uint32_t i = 0U; i < 11U; ++i) {
out[i] = 0U;
}
}
#endif /* CONFIG_ECAT_DIFF_STATS */
@@ -0,0 +1,60 @@
/**
* @file ethercat_diff_stats.h
* @brief [DIFF-STATS] 临时仪表:量化"若把输入 PDO 改成差分写,每拍真正需要写多少字节、
* 变化点碎成几个连续段(run)"。只测量,不改变任何现有写行为。
*
* 存在的理由:差分写(方案 A)的收益完全取决于"每拍有多少字节真的变了",而 run 碎片
* 数量决定它会不会把一次 600B 整块写退化成几百次带 AVAIL 轮询的小写。这两个数今天
* 都没有实测依据,所以先只上仪表,拿分布再决定 A1(语义分区条件写) 值不值得做、
* A2(块粒度差分) 的碎片上限 N 该取几。
*
* 顺带把"比较本身要多久"也测出来(sub23 累计 DWT 周期)——这是差分方案最容易被
* 质疑的成本项,用实测代替拍脑袋。
*
* @note 结果按 1s 窗口滚动(与 sys/cpu_load.cpp 同法,用 DWT 周期判窗口边界),
* getter 返回**上一个已完成的完整窗口**,因此主站任何时刻 SDO 读到的都是
* 稳定值,不会拿到半累积的窗口。窗口未满时 windowSeq()==0,读侧应显示
* "等待首个完整窗口"而不是当 0 解读。
*
* @note 删除方法:删本文件与 ethercat_diff_stats.cpp + 删全工程 [DIFF-STATS] 标记行
* + 把 sys/config.h 的 CONFIG_ECAT_DIFF_STATS 置 0。对象字典 0x2000 的
* sub13..23 字段建议保留(恒 0,不影响任何既有子索引)。
*/
#ifndef COMM_ETHERCAT_DIFF_STATS_H
#define COMM_ETHERCAT_DIFF_STATS_H
#include <cstdint>
/**
* @brief 输入镜像差分统计。
* @note 全静态;observe() 在 PDI_Isr 上下文每拍调用一次,故内部状态无需临界区
* (唯一写者),getter 由 APPL_InputMapping 同上下文读取。
*/
class EthercatDiffStats {
public:
/**
* @brief 与上一份已发布的输入镜像比对并累计统计。
* @param[in] cur 刚发布完成的完整输入 PDO 镜像(kEcatPdoInBytes 字节)。
* @note 内部自持上一份副本,调用方无需额外维护缓冲、也不必自己 memcpy。
* 首次调用只建立基准,不产生样本(否则会把"全 600B 都变了"记成第一拍)。
*/
static void observe(const uint8_t *cur) noexcept;
/** @brief 丢弃基准,下一次调用重新起算(AL 状态跳变、布局变更时用)。 */
static void reset() noexcept;
/* ── 对象字典 0x2000 sub13..23 的取值接口,均为"上一个完整 1s 窗口" ── */
static uint32_t windowSeq() noexcept; /**< sub13 已完成窗口数,0=还没有完整窗口 */
static uint32_t cycles() noexcept; /**< sub14 窗口内参与比对的拍数 */
static uint32_t changedMax() noexcept; /**< sub15 单拍变化字节峰值 */
static uint32_t runsTotal() noexcept; /**< sub16 窗口内 run 总数(配 cycles 求均值) */
static uint32_t runsMax() noexcept; /**< sub17 单拍 run 数峰值,A2 的 N 由它定 */
static uint32_t seg1Bytes() noexcept; /**< sub18 段1(0..31) 桥状态+短快照 */
static uint32_t seg2Bytes() noexcept; /**< sub19 段2(32..543) 电机整轮密排 */
static uint32_t seg3Bytes() noexcept; /**< sub20 段3(544..583) 长帧快照 */
static uint32_t seg4Bytes() noexcept; /**< sub21 已删(1.6.21 恒 0) */
static uint32_t seg5Bytes() noexcept; /**< sub22 段5(584..599) 诊断区+保留 */
static uint32_t compareCyc() noexcept; /**< sub23 比对自身累计 DWT 周期(本征开销) */
};
#endif /* COMM_ETHERCAT_DIFF_STATS_H */
+382 -6
View File
@@ -1,17 +1,60 @@
/**
* @file ethercat_pdo.cpp
* @brief EtherCAT 过程数据(PDO)收发模块实现(1.5.55)。
* @brief EtherCAT 过程数据(PDO)收发模块实现。
*/
#include "comm/ethercat_pdo.h"
#include "comm/ethercat_can_bridge.h"
#include "comm/esc_exmc_bus.h"
#include "device/canfd.h"
#include "sys/config.h"
#include "sys/cpu_load.h"
#include "RTE_Components.h"
#include CMSIS_device_header /* __DMB() */
/* [DIFF-STATS] 临时仪表挂接(删除:去掉本块与 process() 内的 observe 块)。
* 上一行的 sys/config.h 必须在此之前:宏未定义时 #if 会静默按 0 求值,
* 开关就会被悄悄当成"关掉"。 */
#if CONFIG_ECAT_DIFF_STATS
#include "comm/ethercat_diff_stats.h"
#endif
#include <cstring>
uint8_t EthercatPdo::s_out[EthercatPdo::kPdoOutBytes] = {0};
uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0};
extern "C" {
/* SSC 输出过程数据:SM2 地址与长度(ecatslv.c,类型为 SSC UINT16=uint16) */
extern uint16_t nEscAddrOutputData;
extern uint16_t nPdOutputSize;
void HW_EscRead(void *pData, uint16_t Address, uint16_t Len);
}
/* alignas(4) 必须在类内声明与出类定义上同时标注:AC6(Clang) 否则报
* "'alignas' must be specified on definition"。用途见 ethercat_pdo.h。 */
alignas(4) uint8_t EthercatPdo::s_out[EthercatPdo::kPdoOutBytes] = {0};
alignas(4) uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0};
/* 输入 PDO 的工作缓冲:buildInput 填这里,填完整块发布到 s_in(见 process())。
* 读侧 writeInputs 只读 s_in,因此永远看不到"填到一半"的镜像。 */
alignas(4) uint8_t EthercatPdo::s_build[EthercatPdo::kPdoInBytes] = {0};
namespace {
/* 待交回主站的输入数据双缓冲(ping-pong,配合 __DMB() 保证原子发布与 tear-free 读写) */
alignas(4) uint8_t s_inBuf[2][EthercatPdo::kPdoInBytes] = {{0}, {0}};
volatile uint8_t s_inActiveIdx = 0U;
}
alignas(4) uint8_t EthercatPdo::s_escSh1[32] = {0};
alignas(4) uint8_t EthercatPdo::s_escSh2[ECAT_XRX_OFF_CH - 32U] = {0};
alignas(4) uint8_t EthercatPdo::s_escSh3[kEcatPdoInBytes - ECAT_XRX_OFF_CH] = {0};
uint8_t EthercatPdo::s_escFlush1 = 0U;
uint8_t EthercatPdo::s_escFlush2 = 0U;
uint8_t EthercatPdo::s_escFlush3 = 0U;
bool EthercatPdo::s_escSh1Valid = false;
bool EthercatPdo::s_escSh2Valid = false;
bool EthercatPdo::s_escSh3Valid = false;
volatile uint32_t EthercatPdo::s_outputCycles = 0U;
volatile uint32_t EthercatPdo::s_inputCycles = 0U;
volatile uint32_t EthercatPdo::s_reentryCount = 0U;
/** OutputMapping 本拍是否已刷新 s_out(process 兜底读的新鲜度门) */
static volatile bool s_outMappedThisCycle = false;
volatile uint32_t EthercatPdo::s_processSeq = 0U;
void EthercatPdo::readOutputs(const uint8_t *esc)
{
@@ -22,16 +65,144 @@ void EthercatPdo::readOutputs(const uint8_t *esc)
++s_outputCycles;
}
bool EthercatPdo::outputProbe(const uint8_t *head)
{
return EthercatCanBridge::peekOutput(head);
}
void EthercatPdo::process()
{
/* 重入防护:SSC 的 ECAT_Application()→本函数 在**四个上下文**都会被
* 调用 —— PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop(见 ssc/src/ecatappl.c
* 388、499、527、641 行)。而 MainLoop 那条路只用 DISABLE_ESC_INT() 关了
* EXTI1(ESC IRQ),**挡不住 EXTI0(Sync0,preempt=0) 和 EXTI2(Sync1,preempt=1)**。
*
* 竞态:MainLoop 把 s_in 填到一半时被 Sync0/Sync1
* 抢占 → 内层重入本函数,memset 清空后重填整个输入 PDO,随后 PDO_InputMapping()
* 把这一份发给 ESC;外层回来又按自己那轮的进度接着往下写。于是发出去的镜像里
* 批量区相邻两条来自**不同轮次**。
*
* 处理:已在填充中就直接返回。输入 PDO 是周期量,跳过一次下一拍即补,
* 远好过发出一份跨轮次拼接的镜像。 */
static volatile bool s_building = false;
if (s_building) {
++s_reentryCount;
return;
}
s_building = true;
/* PDO ↔ CAN 转发:输出 PDO 的 seq 字节变化沿触发一次 CAN 发送,
* 输入 PDO 回填桥状态 + 电机状态窗口(布局见 ethercat_can_bridge.h)。
*
* 顺序必须是先 processOutput 后 buildInput:buildInput 会把本周期刚执行的
* seq 回显成 ack,主站因此能在下一周期立刻确认送达;反过来则 ack 恒滞后一拍。
* processOutput 还负责取走主站请求的状态窗口下标,也必须在 buildInput 之前。 */
* processOutput 还负责取走主站请求的状态窗口下标,也必须在 buildInput 之前。
*
* 填的是**工作缓冲** s_build,填完整体发布到 s_in:只有重入门还不够 ——
* 内层被门挡住后,它随后的 PDO_InputMapping() 仍会去读 s_in,若 buildInput
* 直接写 s_in,读到的就是外层写到一半的半成品。填在别处、整块发布之后,
* 读侧要么拿到上一份完整镜像、要么拿到这一份完整镜像,不存在中间态。 */
/* 双缓冲发布:填入非活跃槽 nextIdx,完成后通过 __DMB() 与 s_inActiveIdx 切换
* 实现原子发布,彻底避免中断读侧(PDI_Isr / Sync0_Isr)与主循环之间的读写撕裂。 */
const uint8_t nextIdx = 1U - s_inActiveIdx;
uint8_t *build = s_inBuf[nextIdx];
/* 输出兜底全量读:仅当 OutputMapping 本拍未刷新 s_out,且
* (s_out 看起来是死的 或 距上次强制读已跑过一段 process) 才读。
* 每拍 process() 都 HW_EscRead 384B 会与 PDO_OutputMapping 叠成双份 HBI,
* PDO 2kHz 即把 ISR 成本打满(历史探针版 4kHz load≈80%)。 */
static uint32_t s_lastForceReadSeq = 0U;
if (nPdOutputSize > 0U && nEscAddrOutputData != 0U && !s_outMappedThisCycle) {
const bool outDead = (s_out[0] == 0U && s_out[1] == 0U && s_out[8] == 0U);
const uint32_t seqNow = s_processSeq;
/* 仅 s_out 疑似死数据时强制全量读;探针已处理 seq 沿。
* 按拍数兜底间隔拉到 256,避免 8kHz 下周期性 384B HBI。 */
if (outDead || (uint32_t)(seqNow - s_lastForceReadSeq) >= 256U) {
const uint16_t nread = (nPdOutputSize > kPdoOutBytes)
? kPdoOutBytes
: nPdOutputSize;
uint8_t escOut[kEcatPdoOutBytes];
const uint16_t prefix = (nread < 52U) ? nread : 52U;
HW_EscRead(escOut, nEscAddrOutputData, prefix);
const uint8_t batchCount = (prefix > 48U) ? escOut[48] : 0U;
if (batchCount > 0U && nread > prefix) {
HW_EscRead(&escOut[prefix],
(uint16_t)(nEscAddrOutputData + prefix),
(uint16_t)(nread - prefix));
}
readOutputs(escOut);
s_lastForceReadSeq = seqNow;
}
}
s_outMappedThisCycle = false;
EthercatCanBridge::processOutput(s_out);
EthercatCanBridge::buildInput(s_in);
EthercatCanBridge::buildInput(build);
fillDiagTail(build);
__DMB();
s_inActiveIdx = nextIdx;
/* 不再 memcpy 到 s_in:writeInputs/稀疏写/差分写都读 s_inBuf[activeIdx],
* 每拍多拷 600B 是纯浪费(8kHz 下不可接受)。 */
++s_processSeq;
#if CONFIG_ECAT_DIFF_STATS
/* [DIFF-STATS] 只测量:与上一份已发布镜像比对并累计到 1s 窗口,经 0x2000
* sub13..23 上报。不改变上面的整块写行为。仪表自持上一份副本,这里不需要额外缓冲。 */
EthercatDiffStats::observe(s_inBuf[nextIdx]);
#endif
s_building = false;
}
/**
* @brief 填充输入 PDO 段 5 设备诊断区(632..639)。
* @param[out] in 全长输入过程数据缓冲(buildInput 刚清零+填完段 1~4)。
* @note 负载率 1s 滚动一个窗口,seq 随窗口递增——主站只认"seq 在变"的负载值。
* 每周期重写 6 字节比"值变化才写"的分支判断更便宜,也不引入隐藏状态。
*/
void EthercatPdo::fillDiagTail(uint8_t *in)
{
static uint16_t s_diagTick = 0U;
const uint16_t load = CpuLoad::loadPermille();
in[ECAT_DIAG_OFF_LOAD] = (uint8_t)(load & 0xFFU);
in[ECAT_DIAG_OFF_LOAD + 1] = (uint8_t)(load >> 8);
/* CAN 取帧撕裂丢帧数。只有 16 位,饱和不回绕 —— 主站看的是
* "有没有涨、涨多快",回绕会被误读成计数清零。 */
const CanFd::Diag *cd = CanFd::diag();
const uint32_t torn = (cd != nullptr) ? cd->rx_torn_count : 0U;
const uint16_t torn16 = (torn > 0xFFFFU) ? 0xFFFFU : (uint16_t)torn;
in[ECAT_DIAG_OFF_TORN] = (uint8_t)(torn16 & 0xFFU);
in[ECAT_DIAG_OFF_TORN + 1] = (uint8_t)(torn16 >> 8);
/* seq 写「本函数被调用次数」而不是 windowSeq。
* windowSeq 依赖 CpuLoad::tick() 滚窗;Process 若在 tick 前停摆,
* 主站会长期读到 0 且分不清「没跑」和「窗口未满」。自由计数每拍+1,
* 冻结看门狗与 app/load 的「seq 在涨」语义都更干净。 */
++s_diagTick;
in[ECAT_DIAG_OFF_SEQ] = (uint8_t)(s_diagTick & 0xFFU);
in[ECAT_DIAG_OFF_SEQ + 1] = (uint8_t)(s_diagTick >> 8);
/* CAN RX FIFO 满溢计数 RFO0:与内容级 reject 计数是否同涨,
* 是"残余拼接是否仍源自溢出"的观测指标。 */
const uint32_t of = (cd != nullptr) ? cd->rx_fifo_overfull_count : 0U;
const uint16_t of16 = (of > 0xFFFFU) ? 0xFFFFU : (uint16_t)of;
in[ECAT_DIAG_OFF_OVERFULL] = (uint8_t)(of16 & 0xFFU);
in[ECAT_DIAG_OFF_OVERFULL + 1] = (uint8_t)(of16 >> 8);
/* RFD0 释放握手超时计数。dequeue 握手没完成 = 窗口/读指针失同步前兆。 */
const uint32_t rt = (cd != nullptr) ? cd->rx_release_timeout_count : 0U;
const uint16_t rt16 = (rt > 0xFFFFU) ? 0xFFFFU : (uint16_t)rt;
in[ECAT_DIAG_OFF_RELTIMEOUT] = (uint8_t)(rt16 & 0xFFU);
in[ECAT_DIAG_OFF_RELTIMEOUT + 1] = (uint8_t)(rt16 >> 8);
/* 内容级拼接帧拦截次数:MI/MP 校验挡不住的指针错位拼接由入队前
* 结构自洽校验兜底,语义同上 —— 主站看"涨不涨"判断拼接是否仍在发生。 */
const uint32_t rj = (cd != nullptr) ? cd->rx_content_reject_count : 0U;
const uint16_t rj16 = (rj > 0xFFFFU) ? 0xFFFFU : (uint16_t)rj;
in[ECAT_DIAG_OFF_REJECT] = (uint8_t)(rj16 & 0xFFU);
in[ECAT_DIAG_OFF_REJECT + 1] = (uint8_t)(rj16 >> 8);
}
void EthercatPdo::writeInputs(uint8_t *esc)
@@ -39,10 +210,149 @@ void EthercatPdo::writeInputs(uint8_t *esc)
if (esc == nullptr) {
return;
}
memcpy(esc, s_in, kPdoInBytes);
const uint8_t activeIdx = s_inActiveIdx;
__DMB();
memcpy(esc, s_inBuf[activeIdx], kPdoInBytes);
++s_inputCycles;
}
bool EthercatPdo::sparseEscWritePlan(uint16_t *off0, uint16_t *len0,
uint16_t *off1, uint16_t *len1)
{
if (off0 == nullptr || len0 == nullptr || off1 == nullptr || len1 == nullptr) {
return false;
}
#if !CONFIG_ECAT_SPARSE_ESC_WRITE
return false;
#else
/* 仅密排布局:count 有效且两段之间确有空洞才分段。
* magic 不对 / count 越界 / count 撑满到与段3相接 → 整包,避免陈旧空洞被误读。 */
const uint8_t activeIdx = s_inActiveIdx;
__DMB();
const uint8_t *cur = s_inBuf[activeIdx];
if (cur[ECAT_MOTW_OFF_MAGIC] != kEcatMotLayoutMagic) {
return false;
}
const uint8_t count = cur[ECAT_MOTW_OFF_COUNT];
if (count > kEcatMotSlotsPerPdo) {
return false;
}
const uint16_t headEnd =
(uint16_t)(ECAT_MOTW_OFF_SLOTS + (uint16_t)count * kEcatMotSlotSize);
if (headEnd >= ECAT_XRX_OFF_CH) {
return false; /* 9 电机满配:A+B 无缝,整包更省一次 EXMC 事务 */
}
*off0 = 0U;
*len0 = headEnd;
*off1 = ECAT_XRX_OFF_CH;
*len1 = (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH);
return true;
#endif
}
namespace {
/** 段2 有效长度(稀疏:到 count*56 为止;否则整段)。 */
uint16_t escSeg2WriteLen(const uint8_t *in)
{
#if CONFIG_ECAT_SPARSE_ESC_WRITE
if (in[ECAT_MOTW_OFF_MAGIC] == kEcatMotLayoutMagic) {
const uint8_t count = in[ECAT_MOTW_OFF_COUNT];
if (count <= kEcatMotSlotsPerPdo) {
const uint16_t headEnd =
(uint16_t)(ECAT_MOTW_OFF_SLOTS + (uint16_t)count * kEcatMotSlotSize);
if (headEnd < ECAT_XRX_OFF_CH) {
return headEnd;
}
}
}
#else
(void)in;
#endif
return ECAT_XRX_OFF_CH;
}
/** 判定是否需要写某区:无影子 / 冲刷中 / 内容变化。变化时刷新影子并重置冲刷=2。 */
bool diffNeedWrite(const uint8_t *cur, uint16_t off, uint16_t cmpLen,
uint8_t *shadow, bool &valid, uint8_t &flush)
{
const bool changed = !valid || memcmp(cur + off, shadow, cmpLen) != 0;
if (changed) {
memcpy(shadow, cur + off, cmpLen);
valid = true;
flush = 2U; /* 本拍 + 再 2 拍,冲完 3-buffer */
return true;
}
if (flush > 0U) {
--flush;
return true;
}
return false;
}
} /* anonymous namespace */
uint8_t EthercatPdo::escWritePlan(uint16_t *offs, uint16_t *lens)
{
if (offs == nullptr || lens == nullptr) {
return 0U;
}
#if !CONFIG_ECAT_DIFF_ESC_WRITE
/* 回退:稀疏两段或整包 */
uint8_t n = 0U;
uint16_t o0, l0, o1, l1;
if (sparseEscWritePlan(&o0, &l0, &o1, &l1)) {
offs[n] = o0; lens[n] = l0; ++n;
offs[n] = o1; lens[n] = l1; ++n;
} else {
offs[0] = 0U; lens[0] = kPdoInBytes; n = 1U;
}
return n;
#else
/* 三区差分:段1 桥状态 / 段2 电机密排(稀疏写有效长) / 段3+5 快照诊断。
* 段1 计数器几乎每拍变,段2 稳定时省最大头,段3+5 多数拍稳定。
* 无论各段是否变化,必须保证每周期必定写入 SM3 结束地址 (kPdoInBytes - 1),
* 以触发 LAN9252 3-buffer 提交换冲,否则主站将读到冻结的旧缓冲。 */
const uint8_t activeIdx = s_inActiveIdx;
__DMB();
const uint8_t *cur = s_inBuf[activeIdx];
uint8_t n = 0U;
if (diffNeedWrite(cur, 0U, 32U, s_escSh1, s_escSh1Valid, s_escFlush1)) {
offs[n] = 0U;
lens[n] = 32U;
++n;
}
if (diffNeedWrite(cur, 32U, (uint16_t)(ECAT_XRX_OFF_CH - 32U),
s_escSh2, s_escSh2Valid, s_escFlush2)) {
offs[n] = 32U;
/* 写长:稀疏时只写到 count*56,但影子比对的是完整 32..543(含清零空洞)。
* 空洞不写入 ESC 无妨:主站只按 count 迭代。 */
const uint16_t wlen = escSeg2WriteLen(cur);
lens[n] = (wlen > 32U) ? (uint16_t)(wlen - 32U) : 0U;
if (lens[n] == 0U) {
/* count=0:段2 头 8 字节仍要写(count/active/magic) */
lens[n] = 8U;
}
++n;
}
bool seg3Included = false;
if (diffNeedWrite(cur, ECAT_XRX_OFF_CH, (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH),
s_escSh3, s_escSh3Valid, s_escFlush3)) {
offs[n] = ECAT_XRX_OFF_CH;
lens[n] = (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH);
++n;
seg3Included = true;
}
if (!seg3Included) {
/* 段 3 未包含时,补写末 2 字节以保证触碰 SM3 结束地址触发硬件提交 */
offs[n] = (uint16_t)(kPdoInBytes - 2U);
lens[n] = 2U;
++n;
}
return n;
#endif
}
void EthercatPdo::snapshotOutput(uint8_t *dst)
{
if (dst == nullptr) {
@@ -53,11 +363,23 @@ void EthercatPdo::snapshotOutput(uint8_t *dst)
extern "C" {
/* 输出探针,SSC PDI_Isr 的 PDO_OutputMapping 每周期调用(C 链接)。
* 返回 0/1;SSC 侧声明为 UINT8,小返回值零扩展,调用约定兼容。 */
uint8_t EcatPdo_OutputProbe(const uint8_t *head)
{
return EthercatPdo::outputProbe(head) ? 1U : 0U;
}
void EcatPdo_ReadOutputs(const uint8_t *esc)
{
EthercatPdo::readOutputs(esc);
}
void EcatPdo_NoteOutputMapped(void)
{
s_outMappedThisCycle = true;
}
void EcatPdo_Process(void)
{
EthercatPdo::process();
@@ -68,4 +390,58 @@ void EcatPdo_WriteInputs(uint8_t *esc)
EthercatPdo::writeInputs(esc);
}
/* 稀疏写计划:SSC PDO_InputMapping 据此决定 HW_EscWriteIsr 写几段。
* 返回 1=两段有效;0=回退整包。 */
uint8_t EcatPdo_SparseEscPlan(uint16_t *off0, uint16_t *len0,
uint16_t *off1, uint16_t *len1)
{
return EthercatPdo::sparseEscWritePlan(off0, len0, off1, len1) ? 1U : 0U;
}
/* 差分写计划:SSC PDO_InputMapping 逐段 HW_EscWriteIsr。 */
uint8_t EcatPdo_EscWritePlan(uint16_t *offs, uint16_t *lens, uint8_t maxSegs)
{
if (offs == nullptr || lens == nullptr || maxSegs == 0U
|| maxSegs > EthercatPdo::kEscPlanMaxSegs) {
return 0U;
}
uint16_t o[EthercatPdo::kEscPlanMaxSegs];
uint16_t l[EthercatPdo::kEscPlanMaxSegs];
const uint8_t n = EthercatPdo::escWritePlan(o, l);
if (n == 0U || n > maxSegs) {
return 0U;
}
for (uint8_t i = 0U; i < n; ++i) {
offs[i] = o[i];
lens[i] = l[i];
}
return n;
}
/* HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
uint32_t EscExmcBus_MaskEnter(void)
{
return EscExmcBus::maskEnter();
}
void EscExmcBus_MaskExit(uint32_t prev)
{
EscExmcBus::maskExit(prev);
}
uint32_t HW_EscExmcMaskEnter(void)
{
return EscExmcBus::maskEnter();
}
void HW_EscExmcMaskExit(uint32_t prev)
{
EscExmcBus::maskExit(prev);
}
uint32_t EcatPdo_ProcessSeq(void)
{
return EthercatPdo::processSeq();
}
} /* extern "C" */
+82 -7
View File
@@ -1,6 +1,6 @@
/**
* @file ethercat_pdo.h
* @brief EtherCAT 过程数据(PDO)收发模块(1.5.54 由 dc_app.c 的散装 C 代码类化)。
* @brief EtherCAT 过程数据(PDO)收发模块。
*
* 一个 EtherCAT 周期在 SSC 里被拆成三个回调,本类按这三步各提供一个入口:
* 1. APPL_OutputMapping() → readOutputs() 从 ESC 取走主站下发的输出数据
@@ -11,7 +11,7 @@
* 是 SDO 存储,必须留在 C 侧)与三行 extern "C" 转发。这样"收发"这件事只有一个
* 归属:想加新的过程数据语义只改本类,不必再动 SSC 移植层。
*
* @note 1.5.55 起输出/输入**长度不再相等**(64B / 384B),为电机控制台腾出
* @note 输出/输入**长度不相等**(64B / 384B),为电机控制台腾出
* 状态窗口空间。长度常量统一由 ethercat_can_bridge.h 提供,避免三处各写
* 一个魔数后失配。
*
@@ -48,6 +48,16 @@ public:
*/
static void readOutputs(const uint8_t *esc);
/**
* @brief 输出探针(输出按需全量读,配合 SSC PDO_OutputMapping)。
* @param[in] head 输出 PDO 头 12B(覆盖 SEQ@0 / WIN@8 / FLAGS@3)。
* @return true=需要全量读(seq 沿或清统计请求);false=本周期跳过全量读。
* @note 每周期由 PDI_Isr 在读 ESC 前调用:做 seq 基准 priming 与沿判定;
* s_lastSeq 的推进仍留在全量路径(processOutput)。不再
* 维护 win_base(段 2 电机整轮密排,无滑窗)。
*/
static bool outputProbe(const uint8_t *head);
/**
* @brief 跑一个应用周期:消费输出、生成输入。
* @note 由 APPL_Application() 在主循环调用。
@@ -57,15 +67,48 @@ public:
/**
* @brief 交回输入过程数据(从站→主站)。
* @param[out] esc 指向 ESC 输入缓冲的 kPdoInBytes 字节。
* @note 由 APPL_InputMapping() 每周期调用。
* @note 由 APPL_InputMapping() 每周期调用。只填 RAM/对象存储;
* 真正写 ESC 的字节量由 PDO_InputMapping 经 sparseEscWritePlan 决定。
*/
static void writeInputs(uint8_t *esc);
/**
* @brief 稀疏写 ESC 的两段计划:段1+段2有效区、段3+段5;跳过空槽区。
* @param[out] off0/len0 第一段相对 PDO 起点的偏移与长度
* @param[out] off1/len1 第二段
* @return true=可按两段写;false=应回退整包 600B(开关关/非密排/count 越界/两段相接)
* @note 必须在 writeInputs() 之后调用,读的是已发布的 s_in。
*/
static bool sparseEscWritePlan(uint16_t *off0, uint16_t *len0,
uint16_t *off1, uint16_t *len1);
/**
* @brief 输入 PDO 写 ESC 的多段差分计划(未启用时回退稀疏/整包)。
* @param[out] offs 段偏移数组,至少 kEscPlanMaxSegs 项
* @param[out] lens 段长度数组,至少 kEscPlanMaxSegs 项
* @return 段数 1..kEscPlanMaxSegs;调用方逐段 HW_EscWriteIsr。
* @note 必须在 writeInputs()/s_in 发布之后、每拍一次调用。按三区差分:
* 段1(0..31 桥状态)、段2(32..543 电机密排,写时用稀疏有效长)、
* 段3+5(544..599)。变化后连续写 3 拍冲完 3-buffer 才允许跳过。
* 全段跳过时返回「末 2 字节」一段以完成 SM 事件。有副作用。
*/
static uint8_t escWritePlan(uint16_t *offs, uint16_t *lens);
/** escWritePlan 最多段数。 */
static constexpr uint8_t kEscPlanMaxSegs = 4U;
/** @brief 累计取走输出的周期数(主站在发过程数据的证据)。 */
static uint32_t outputCycles() { return s_outputCycles; }
/** @brief 累计交回输入的周期数。 */
static uint32_t inputCycles() { return s_inputCycles; }
/** @brief process() 完整填完一拍输入的序号。新鲜度救援看这个,而不是
* InputMapping 的 s_inMapSeq——后者只证明「在拷陈旧 s_in」。 */
static uint32_t processSeq() { return s_processSeq; }
/** @brief process() 被重入拦下的次数。持续增长 = 跨上下文抢占确实在发生
* (PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 互相打断),也就证明了
* 批量区跨轮次拼接的成因;恒为 0 则说明该机制推断有误。 */
static uint32_t reentryCount() { return s_reentryCount; }
/** @brief 是否已开始过程数据交换(收到过至少一个输出周期)。 */
static bool isExchanging() { return s_outputCycles != 0U; }
@@ -77,13 +120,41 @@ public:
static void snapshotOutput(uint8_t *dst);
private:
/** @brief 最近一份主站下发的输出数据。 */
static uint8_t s_out[kPdoOutBytes];
/** @brief 待交回主站的输入数据。 */
static uint8_t s_in[kPdoInBytes];
/**
* @brief 填充输入 PDO 段 5 设备诊断区(尾部 8 字节)。
* @param[out] in 全长输入过程数据缓冲。
* @note 布局常量 ECAT_DIAG_OFF_* 见 ethercat_can_bridge.h;当前携带
* CPU 负载率(0.1% 单位) + 窗口序号,取自 sys/cpu_load.h。
*/
static void fillDiagTail(uint8_t *in);
/** @brief 最近一份主站下发的输出数据。
* @note alignas(4):让整块 memcpy(writeInputs / [DIFF-STATS] 的快照拷贝)按
* 字宽走而非逐字节。比对主循环本身不依赖它——已改成手拼 4 字节移位,
* 因为 -Oz 下 memcpy(&a,p,4) 会编成函数调用,见 ethercat_diff_stats.cpp。 */
alignas(4) static uint8_t s_out[kPdoOutBytes];
/** @brief 待交回主站的输入数据(已发布的完整镜像,writeInputs 只读它)。 */
alignas(4) static uint8_t s_in[kPdoInBytes];
/** @brief 输入 PDO 工作缓冲:buildInput 填这里,填完整块发布到 s_in。
* ECAT_Application() 会在 PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 四个
* 上下文被调用并互相抢占,直接写 s_in 会让读侧取到跨轮次的半成品。 */
alignas(4) static uint8_t s_build[kPdoInBytes];
/** @brief 写 ESC 差分影子:段1 / 段2 / 段3+5 三区。 */
alignas(4) static uint8_t s_escSh1[32];
alignas(4) static uint8_t s_escSh2[ECAT_XRX_OFF_CH - 32U];
alignas(4) static uint8_t s_escSh3[kEcatPdoInBytes - ECAT_XRX_OFF_CH];
static uint8_t s_escFlush1;
static uint8_t s_escFlush2;
static uint8_t s_escFlush3;
static bool s_escSh1Valid;
static bool s_escSh2Valid;
static bool s_escSh3Valid;
/** @brief process() 被重入拦下的次数(诊断用,见 reentryCount())。 */
static volatile uint32_t s_reentryCount;
static volatile uint32_t s_outputCycles;
static volatile uint32_t s_inputCycles;
static volatile uint32_t s_processSeq;
};
extern "C" {
@@ -93,6 +164,10 @@ void EcatPdo_ReadOutputs(const uint8_t *esc);
void EcatPdo_Process(void);
/** @brief 供 dc_app.c 调用:交回输入过程数据。 */
void EcatPdo_WriteInputs(uint8_t *esc);
/** @brief 供 dc_app.c 新鲜度救援:process() 是否还在推进。 */
uint32_t EcatPdo_ProcessSeq(void);
/** @brief PDO_OutputMapping 探针命中并完成全量读后调用(本拍 s_out 已新鲜)。 */
void EcatPdo_NoteOutputMapped(void);
}
#endif /* COMM_ETHERCAT_PDO_H */
+9 -2
View File
@@ -50,8 +50,15 @@
/** 输出过程数据字节数(主站→从站,SM2):48×64bit。 */
#define ECAT_PDO_OUT_BYTES 384
/** 输入过程数据字节数(从站→主站,SM3):80×64bit。 */
#define ECAT_PDO_IN_BYTES 640
/**
* 输入过程数据字节数(从站→主站,SM3):75×64bit = 600B。
* 布局(1.6.21 密排):段1..2 0..543 · 段3 544..583 · 段5 584..595。
* 改长度须同步 bridge/dc_app/gen_eeprom/ESI/主站 decode,并烧 EEPROM 冷启动。
*
* 注意:DWT sub10 量的是从站 PDI 写 ESC 耗时,**不是**主站 rx 段。
* 主站 rx 主要由线速往返决定(约 1KB @100Mbit ≈ 80µs 量级),两边 µs 不要对拍。
*/
#define ECAT_PDO_IN_BYTES 600
/** DPRAM 中过程数据区起址与 SM 缓冲数 —— 上限推导的依据,供静态断言使用。 */
#define ECAT_PD_AREA_START 0x1400
+7 -14
View File
@@ -64,13 +64,12 @@ void EthercatSlave::init()
* (如 SM Watchdog=0x1B、FMMU 配置错、邮箱协议错),需主站介入。 */
/* ============================================================
* bring-up 诊断(1.5.35 引入,1.5.39 重新定性):经 CSR 间接端口读三个关键寄存器
* bring-up 诊断:经 CSR 间接端口读三个关键寄存器
* + 一次写回环,确认 EtherCAT 核心侧状态。
* 注意主站读身份/AL 走 ESC 以太网侧 + EEPROM(SII),完全不经过 GD32 的 PDI,
* 所以"主站能读到身份"不能证明下面任何一项成立。
* 1.5.39 起访问已改经 ECAT_CSR_CMD/CSR_DATA(EtherCAT 核心寄存器不在 HBI 直接
* 地址空间,DS 12.11);此前直读索引使本段恒读全 0,曾被误判为"EEPROM 加载竞态"
* 与"HBI 写路径损坏"两个错误结论。现在读到全 0 才真正指向 EEPROM/PDI 故障,
* 访问必须经 ECAT_CSR_CMD/CSR_DATA(EtherCAT 核心寄存器不在 HBI 直接
* 地址空间,DS 12.11);直读索引恒读全 0。
* 交叉验证用 tools/ecat/ecat_esc_regs.py 从网络侧读同样的寄存器。
* 本段在 EthercatSync::init() 使能 EXTI 之前执行,满足 initOnly* 的 INIT-ONLY 约束。
* ============================================================ */
@@ -87,9 +86,7 @@ void EthercatSlave::init()
/* (3) EEPROM 接口状态 0x0502-0x0503(16 位字)。位定义按 ETG.1000.4 表 49
* 与 SSC esc.h:bit11=Checksum Error、bit12=EEPROM loading status(0=OK)、
* bit13=Missing EEPROM Acknowledge、bit14=Write Error、bit15=Busy。
* 注意 1.5.40 及之前本行把 bit11 打成 "loaded",语义还正好反了
* (bit11=1 才是校验错),于是正常板子恒显示 "loaded=0",字面像"EEPROM 未加载",
* 正是历史上 1.5.37 那个已撤回的错误结论的诱因;现按位分别命名,0 全部代表正常。
* 0 全部代表正常。
* 真正的"EEPROM 已加载/PDI 可用"判据是 DL_STATUS(0x0110) bit0,见上方 (1)。 */
const uint16_t eepCsr = EscLan9252::initOnlyReadReg(0x0502U);
LOG_ERR("[ECAT][DIAG] DL_STATUS(0x110)=0x%04X pdi_oper=%u | "
@@ -109,10 +106,7 @@ void EthercatSlave::init()
* 就是 PDI 侧可写(PDI 用它选择哪些 AL Event 产生 IRQ),不像 0x0012 Station
* Alias 在部分 ESC 上对 PDI 只读(会造成假 FAIL);本段在 MainInit/HW_Init
* 之前执行,还原后不影响 SSC 后续建立的 mask 影子。
* 1.5.39 起本回环经 ECAT_CSR_CMD/CSR_DATA 间接端口执行。历史注意:1.5.35
* 该回环报 FAIL 曾被误判为"HBI 写路径损坏",真因是当时直读直写 HBI 索引——
* EtherCAT 核心寄存器不在 HBI 直接地址空间(DS 12.11),读恒 0、写被丢弃。
* 网络侧诊断(tools/ecat/ecat_esc_regs.py)已证实 ESC 本体与 EEPROM 一直正常。 */
* 回环必须经 ECAT_CSR_CMD/CSR_DATA 间接端口执行。 */
const uint16_t maskSaved = EscLan9252::initOnlyReadReg(0x0204U);
EscLan9252::initOnlyWriteReg(0x0204U, 0x0A50U);
const uint16_t maskRead = EscLan9252::initOnlyReadReg(0x0204U);
@@ -129,7 +123,7 @@ void EthercatSlave::init()
/* 是否有主站在线:DL_STATUS(0x0110) 的 Physical Link 位 bit4/5/6 对应 Port0/1/2
* (DS 12.14.19)。三个端口都无 link = 网线未插/无主站,此时 AL 状态机根本没人驱动
* (AL 迁移由主站写 AL Control 0x0120 触发,从站不自跳),停在 INIT 是唯一正确行为。
* 参考:接主站时实测 DL_STATUS=0x5611(Port0 有 link 有 comm);
* 参考:接主站时 DL_STATUS 通常为 0x5611(Port0 有 link 有 comm);
* 空载时为 0x5503(三口 loop closed、link/comm 全 0)。 */
const bool anyPhyLink = chipIdOk && ((dlStatus & 0x0070U) != 0U);
@@ -137,8 +131,7 @@ void EthercatSlave::init()
LOG_ERR("[ECAT][SLAVE] ESC NOT ready, sync ISR disabled (check EXMC wiring/timing)\r\n");
} else if (s_alState < AL_STATE_SAFEOP) {
/* 区分"等主站"与"真故障":两者 AL 都 < SAFEOP,但只有后者需要排查。
* 1.5.40 及之前一律打 FAIL(partial),而冷启动无主站时必然走到这里,
* 与紧随其后的 "SSC MainInit OK" 自相矛盾,极易被误读为初始化失败。 */
* 冷启动无主站时必然走到这里,报 WAIT_MASTER 而非 FAIL。 */
if (!anyPhyLink) {
LOG_ERR("[ECAT][SLAVE] WAIT_MASTER: no PHY link (DL_STATUS=0x%04X), "
"AL=0x%02X(%s) 由主站驱动,空载停在 INIT 属预期\r\n",
+1 -2
View File
@@ -1,8 +1,7 @@
/**
* @file ethercat_slave.h
* @brief EtherCAT 从站 init 探针与运行状态查询接口。
* @note 1.5.27:原先的 periodicIsr()/三缓冲 PDO 桥是死代码(无任何调用点),
* 运行期 PDO 交换实际由 SSC 协议栈完成 —— 周期路径为 Sync0_Isr →
* @note 运行期 PDO 交换由 SSC 协议栈完成 —— 周期路径为 Sync0_Isr →
* ECAT_Application → APPL_InputMapping/OutputMapping(comm/ssc/port/dc_app.c),
* 主循环路径为 MainLoop → ECAT_Main。本类仅保留 init 探针与诊断查询。
*/
+40 -7
View File
@@ -7,8 +7,10 @@
* SYNC1 (EXTI2, preempt=1) → Sync1_Isr() : 预留
*/
#include "comm/ethercat_sync.h"
#include "comm/esc_lan9252.h" /* pmSuspended:ESC 睡眠挂起期 ISR 拒碰 HBI */
#include "sys/config.h"
#include "main.h" /* g_app_uptime_sec */
#include "sys/cpu_load.h"
#include <cstdio>
extern "C" {
@@ -113,6 +115,12 @@ void EthercatSync::onSync0Exti()
/* 直接寄存器访问,绕过 GD32 库函数以消除可变抖动 */
if ((EXTI_PD & (uint32_t)CONFIG_ESC_SYNC0_EXTI) != RESET) {
EXTI_PD = (uint32_t)CONFIG_ESC_SYNC0_EXTI;
/* ESC 睡眠挂起期,清了 pending 就走——Sync0_Isr 内的 PRAM/CSR
* 访问全是 HBI,读即唤醒。睡眠中 SYNC0 引脚本应无沿,此分支只兜
* 进入睡眠瞬间的残留 pending。 */
if (EscLan9252::pmSuspended()) {
return;
}
++s_sync0Count;
Sync0_Isr();
}
@@ -122,6 +130,10 @@ void EthercatSync::onEscIrqExti()
{
if (exti_interrupt_flag_get(CONFIG_ESC_IRQ_EXTI) != RESET) {
exti_interrupt_flag_clear(CONFIG_ESC_IRQ_EXTI);
/* ESC 睡眠挂起期直接返回——PDI_Isr 会读 0x220/PRAM(HBI),读即唤醒 */
if (EscLan9252::pmSuspended()) {
return;
}
++s_escIrqCount;
/* PDI_Isr(preempt=1) 可被 Sync0_Isr(preempt=0) 抢占;HBI Indexed 是两步
* 事务(IDX 写 + DATA 读),被抢占即错址。此处屏蔽 EXTI0 实现互斥:
@@ -140,6 +152,10 @@ void EthercatSync::onSync1Exti()
{
if (exti_interrupt_flag_get(CONFIG_ESC_SYNC1_EXTI) != RESET) {
exti_interrupt_flag_clear(CONFIG_ESC_SYNC1_EXTI);
/* ESC 睡眠挂起期直接返回——Sync1_Isr 内为 HBI 访问 */
if (EscLan9252::pmSuspended()) {
return;
}
++s_sync1Count;
/* 同 onEscIrqExti:Sync1_Isr(preempt=1) 内的 HBI 访问需与 SYNC0 互斥 */
const uint32_t prevSync0 = NVIC_GetEnableIRQ(CONFIG_ESC_SYNC0_IRQn);
@@ -156,7 +172,10 @@ void EthercatSync::onTimerIrq()
#if (CONFIG_ETHERCAT_SYNC_MODE == 1)
if (timer_interrupt_flag_get(TIMER0, TIMER_INT_FLAG_UP) != RESET) {
timer_interrupt_flag_clear(TIMER0, TIMER_INT_FLAG_UP);
Sync0_Isr();
/* ESC 睡眠挂起期跳过——Sync0_Isr 内为 HBI 访问 */
if (!EscLan9252::pmSuspended()) {
Sync0_Isr();
}
}
#else
/* EXTI 模式下 TIMER0 不使用,本函数为占位;CONFIG_ETHERCAT_SYNC_MODE=1 时激活。 */
@@ -168,13 +187,23 @@ extern "C" {
#if (CONFIG_ETHERCAT_SYNC_MODE == 0)
__attribute__((section(".ramfunc"), noinline))
void EXTI0_IRQHandler(void) { EthercatSync::onSync0Exti(); }
void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); }
void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); }
void EXTI0_IRQHandler(void)
{
CpuLoad::isrEnter();
EthercatSync::onSync0Exti();
CpuLoad::isrExit();
}
void EXTI1_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onEscIrqExti(); CpuLoad::isrExit(); }
void EXTI2_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onSync1Exti(); CpuLoad::isrExit(); }
#else
void TIMER0_UP_TIMER9_IRQHandler(void) { EthercatSync::onTimerIrq(); }
void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); }
void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); }
void TIMER0_UP_TIMER9_IRQHandler(void)
{
CpuLoad::isrEnter();
EthercatSync::onTimerIrq();
CpuLoad::isrExit();
}
void EXTI1_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onEscIrqExti(); CpuLoad::isrExit(); }
void EXTI2_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onSync1Exti(); CpuLoad::isrExit(); }
#endif
/* ---- SSC 诊断对象 0x2000 的 C 侧访问 (dc_app.c APPL_InputMapping 每周期拉取) ---- */
@@ -182,5 +211,9 @@ uint32_t EcatDbg_GetSync0(void) { return EthercatSync::getSync0Count(); }
uint32_t EcatDbg_GetSync1(void) { return EthercatSync::getSync1Count(); }
uint32_t EcatDbg_GetEscIrq(void) { return EthercatSync::getEscIrqCount(); }
uint32_t EcatDbg_GetUptimeSec(void){ return g_app_uptime_sec; }
uint32_t EcatDbg_GetTick1ms(void) { return g_scheduler.tick_1ms; }
/* 剖分埋点:供 ecatappl.c (C) 读取 DWT 周期计数 */
uint32_t EcatDbg_Dwt(void) { return DWT->CYCCNT; }
} /* extern "C" */
+6 -38
View File
@@ -25,7 +25,7 @@
<!-- SM2: Process Data Output (RxPDO, Master -> Slave) -->
<Sm DefaultSize="384" StartAddress="#x1400" ControlByte="#x64" Enable="1">Outputs</Sm>
<!-- SM3: Process Data Input (TxPDO, Slave -> Master) -->
<Sm DefaultSize="640" StartAddress="#x1880" ControlByte="#x20" Enable="1">Inputs</Sm>
<Sm DefaultSize="600" StartAddress="#x1880" ControlByte="#x20" Enable="1">Inputs</Sm>
<!-- RxPDO (Output, Master -> Slave): 索引 0x1600-0x17FF,映射 0x7000:01..48
1.5.56 扩到 48 条目(384B):兼容单帧区 48B + CANFD 批量发送区 336B。
与 dc_app.c 的 PDO_OUT_ENTRIES / gen_eeprom.py 的 PD_OUT_SIZE 必须一致,
@@ -370,10 +370,13 @@
<DataType>ULINT</DataType>
</Entry>
</RxPdo>
<!-- TxPDO (Input, Slave -> Master): 索引 0x1A00-0x1BFF,映射 0x6000:01..80
<!-- TxPDO (Input, Slave -> Master): 索引 0x1A00-0x1BFF,映射 0x6000:01..75
1.5.56 扩到 80 条目(640B):电机状态窗口 + CANFD 批量接收区 280B。
SM3 起址随之挪到 #x1880 —— SM2 是 3-buffer 模式,384B×3 占到 #x187F。
(384+640)×3 = 3072 = 过程数据区 0x1400-0x1FFF 全长,恰好用满。
1.6.1 输入瘦身 640→496: 批量接收条目数据 64→32B, 条目 80→62。
1.6.15 496→600: 批量接收改定长覆盖窗口 5 槽(每槽 12B 头含 u32 seqlock
+ 32B 数据), 条目 62→75, SM3 实占 3×600=1800B (0x1880-0x1F67),
out+in=984 <= 1024 硬上限。推导见 comm/ethercat_pdo_size.h。
撞址会被 SSC ecatslv.c 重叠检查拦下,报 INVALIDSMOUTCFG。 -->
<TxPdo Fixed="1" Mandatory="1" Sm="3">
<Index>#x1A00</Index>
@@ -903,41 +906,6 @@
<Name>Status Data 75</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>76</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 76</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>77</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 77</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>78</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 78</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>79</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 79</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>80</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 80</Name>
<DataType>ULINT</DataType>
</Entry>
</TxPdo>
<Mailbox DataLinkLayer="true">
<CoE SdoInfo="true" CompleteAccess="true" SegmentedSdo="true"/>
+534 -79
View File
@@ -11,10 +11,7 @@
* 0x6000 Input Data (从站→主站, 384 bytes)
* 0x2000 SYNC Diag (只读: sync0/sync1/escirq/uptime, 主站 SDO 轮询看 DC 通路)
*
* 1.5.55:为电机控制台把过程数据从 32B/32B 扩到 64B/384B。
* 0x7000/0x6000/0x1600/0x1A00 由 OBJCODE_REC 改为 OBJCODE_ARR —— 关键原因见
* 下方 Entry Descriptions 注释:REC 会逐下标索引 pEntryDesc[Subindex],48 个
* 条目就得写 49 项描述符;ARR 由 OBJ_GetEntryDesc 复用 pEntryDesc[1],只需 2 项。
* 0x7000/0x6000/0x1600/0x1A00 为 OBJCODE_ARR(原因见下方 Entry Descriptions 注释)。
*/
#include "ecat_def.h"
@@ -24,10 +21,15 @@
#include "objdef.h"
#include "dc_app.h"
#include "../../ethercat_pdo_size.h" /* ECAT_PDO_OUT_BYTES / ECAT_PDO_IN_BYTES */
#include "../../ecat_motor_ota.h" /* 0x2510/0x2511:电机 OTA 邮箱进料的尺寸、会话码与钩子 */
#include <string.h>
#include <stddef.h> /* offsetof:下方布局守卫断言使用 */
#include <stdio.h> /* printf:0x204 守护 WARN 日志(1.5.48) */
#include <stdio.h> /* printf:0x204 守护 WARN 日志 */
/* CPU 负载挂钩(sys/cpu_load.cpp,与 /api/app/load 同口径) */
extern void CpuLoad_Enter(void);
extern void CpuLoad_Leave(void);
/*---------------------------------------------------------------------------
* el9800appl.h 桩 include (SSC 内部需要)
@@ -39,14 +41,13 @@
/*---------------------------------------------------------------------------
* PDO 数据缓冲
*
* 1.5.56 扩到 DPRAM 硬上限:输出 48×64bit=384B,输入 80×64bit=640B(非对称)。
* 长度的唯一真源是 comm/ethercat_pdo_size.h;本文件用 _Static_assert 与之绑定,
* 改一处漏一处会编译失败。为何硬上限是 out+in≤1024B 见该头文件的推导。
*---------------------------------------------------------------------------*/
/** 输出/输入 PDO 的 64bit 条目数。 */
#define PDO_OUT_ENTRIES 48u /* 48×8 = 384 B */
#define PDO_IN_ENTRIES 80u /* 80×8 = 640 B */
#define PDO_IN_ENTRIES 75u /* 75×8 = 600 B (覆盖窗口) */
/** 输出数据对象 0x7000 (主站→从站, RxPDO) */
typedef struct OBJ_STRUCT_PACKED_START {
@@ -65,13 +66,13 @@ TOBJ6000;
static TOBJ7000 s_outputObj = {PDO_OUT_ENTRIES, {0}};
static TOBJ6000 s_inputObj = {PDO_IN_ENTRIES, {0}};
/* 布局守卫(1.5.42):SSC 的 SDO 单条目访问按"si0 占 16bit,其后按 BitLength 紧排"
/* 布局守卫:SSC 的 SDO 单条目访问按"si0 占 16bit,其后按 BitLength 紧排"
* 计算偏移(OBJ_GetEntryOffset),故 UINT64 数组必须紧跟在 UINT16 之后落在字节 2。
* OBJ_STRUCT_PACKED_START 曾被留空导致其落在字节 8、SDO 读写错位,加断言防回归。 */
_Static_assert(offsetof(TOBJ7000, u64Data) == 2, "TOBJ7000 未紧排:检查 OBJ_STRUCT_PACKED_START");
_Static_assert(offsetof(TOBJ6000, u64Data) == 2, "TOBJ6000 未紧排:检查 OBJ_STRUCT_PACKED_START");
_Static_assert(sizeof(TOBJ7000) == 2 + 8 * PDO_OUT_ENTRIES, "TOBJ7000 尺寸应为 2+384");
_Static_assert(sizeof(TOBJ6000) == 2 + 8 * PDO_IN_ENTRIES, "TOBJ6000 尺寸应为 2+640");
_Static_assert(sizeof(TOBJ6000) == 2 + 8 * PDO_IN_ENTRIES, "TOBJ6000 尺寸应为 2+600");
/* 与 C++ 侧桥接层的长度约定绑定:任一处改了尺寸而忘了另一处,这里直接编译失败,
* 而不是等到运行期 SAFEOP 报 AL 0x001A/0x0011 再回头查。 */
@@ -94,10 +95,35 @@ typedef struct OBJ_STRUCT_PACKED_START {
UINT32 u32Sync1; /* sub2 */
UINT32 u32EscIrq; /* sub3 */
UINT32 u32UptimeS; /* sub4 */
/* 剖分埋点(sub5..12,DWT 周期/计数累加,主站 1s 差分) */
UINT32 u32PdiCyc; /* sub5: PDI_Isr 全函数累计 */
UINT32 u32OutCyc; /* sub6: PDO_OutputMapping(输出探针/全量读) */
UINT32 u32FreeRunCyc; /* sub7: MainLoop free-run 块(SM 同步下恒 0) */
UINT32 u32PdiCnt; /* sub8: PDI_Isr 触发次数 */
UINT32 u32InCyc; /* sub9: PDO_InputMapping 全程(APPL+输入PDO写) */
UINT32 u32InWriteCyc; /* sub10: 其中 HW_EscWriteIsr 输入PDO 过程数据写 */
UINT32 u32AlCyc; /* sub11: PDI_Isr 内 AL 事件读×2+超限应答 */
UINT32 u32AppCyc; /* sub12: ECAT_Application(桥 processOutput) */
/* [DIFF-STATS] 差分写取证仪表(sub13..23):上一个完整 1s 窗口的值,
* 取自 comm/ethercat_diff_stats.cpp。CONFIG_ECAT_DIFF_STATS 置 0 时恒 0,
* 字段保留(不影响任何既有子索引,也不动 PDO/映射,故无需重刷 SII)。 */
UINT32 u32DsWinSeq; /* sub13: 已完成窗口数,0=还没有完整窗口(勿把 0 当"无变化") */
UINT32 u32DsCycles; /* sub14: 窗口内参与比对的拍数 */
UINT32 u32DsChangedMax; /* sub15: 单拍变化字节峰值(差分写的理论上限=600×拍数) */
UINT32 u32DsRunsTotal; /* sub16: 窗口内 run(连续变化段)总数,配 sub14 求均值 */
UINT32 u32DsRunsMax; /* sub17: 单拍 run 数峰值 —— 块粒度差分的碎片上限由它定 */
UINT32 u32DsSeg1; /* sub18: 段1(0..31) 桥状态+短快照 变化字节合计 */
UINT32 u32DsSeg2; /* sub19: 段2(32..543) 电机整轮密排 */
UINT32 u32DsSeg3; /* sub20: 段3(544..583) 长帧快照 */
UINT32 u32DsSeg4; /* sub21: 已删(恒 0) */
UINT32 u32DsSeg5; /* sub22: 段5(584..599) 诊断区+保留 */
UINT32 u32DsCompareCyc; /* sub23: 比对自身累计 DWT 周期(u32 粒度,不含快照拷贝) */
UINT32 u32OpFreshRescues; /* sub24: OP 输入新鲜度救援次数(主站网页看,不打串口) */
} OBJ_STRUCT_PACKED_END
TOBJ2000;
static TOBJ2000 s_syncDiag = {4, 0, 0, 0, 0};
static TOBJ2000 s_syncDiag = {24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
/* 同上:UINT32 首条目必须落在字节 2(未紧排时为 4)。 */
_Static_assert(offsetof(TOBJ2000, u32Sync0) == 2, "TOBJ2000 未紧排:检查 OBJ_STRUCT_PACKED_START");
@@ -107,14 +133,45 @@ extern UINT32 EcatDbg_GetSync0(void);
extern UINT32 EcatDbg_GetSync1(void);
extern UINT32 EcatDbg_GetEscIrq(void);
extern UINT32 EcatDbg_GetUptimeSec(void);
/* 剖分埋点:ecatappl.c 累加器 + ethercat_sync.cpp 的 DWT 读 */
extern UINT32 EcatDbg_Dwt(void);
extern volatile UINT32 g_dbg_pdiCyc;
extern volatile UINT32 g_dbg_outCyc;
extern volatile UINT32 g_dbg_freerunCyc;
extern volatile UINT32 g_dbg_pdiCnt;
extern volatile UINT32 g_dbg_inCyc;
extern volatile UINT32 g_dbg_inWriteCyc;
extern volatile UINT32 g_dbg_alCyc;
extern volatile UINT32 g_dbg_appCyc;
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim, 1.5.54)。
* 1.5.53 时这里直接调 EcatCan_* 转发桥;1.5.54 把"一个 EtherCAT 周期怎么收发"
* 收拢进 EthercatPdo 类,本文件退回纯 SSC 移植层:只留对象字典 + 三行转发。 */
/* [DIFF-STATS] 差分写统计窗口值(comm/ethercat_diff_stats.cpp),out[0..10] 对应 sub13..23 */
void EcatDiffStats_GetWindow(UINT32 *out);
/* OP 输入新鲜度 — APPL_InputMapping 每次成功刷新就自增 */
static volatile UINT32 s_inMapSeq = 0U;
static UINT32 s_inMapSeqSeen = 0U;
static UINT32 s_inStaleBaseCyc = 0U;
static UINT32 s_opFreshRescues = 0U;
#include <stdint.h>
/* DFP 的 SystemCoreClock 是 uint32_t(=unsigned int);SSC 的 UINT32 是 unsigned long,
* 不能用 UINT32 重复声明。 */
extern uint32_t SystemCoreClock;
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim)。
* 本文件为纯 SSC 移植层:只留对象字典 + 三行转发。 */
extern void EcatPdo_ReadOutputs(const UINT8 *esc);
extern void EcatPdo_Process(void);
extern uint32_t EcatPdo_ProcessSeq(void);
extern void EcatPdo_WriteInputs(UINT8 *esc);
/* 0x2502 用:进 BOOT 的异步请求(sys/ota_boot_request.cpp 的 extern "C" shim)。
* 只置 pending 标志,真正的配置页写入 + 复位在主循环下一圈,见 ObjBootReqWrite 注。 */
extern void EcatEnterBootRequestAsync(void);
/* 0x2503 用:ESP32 EN(PC9) 控制(sys/esp32_en.cpp 的 extern "C" shim)。
* 经 EtherCAT SDO 下发,**不走 ESP32 WiFi**——WiFi 关断后主站仍能拉回使能。 */
extern UINT8 EcatEsp32EnDrive(UINT8 action);
/*---------------------------------------------------------------------------
* PDO Assign 对象 (0x1C12 / 0x1C13)
*---------------------------------------------------------------------------*/
@@ -161,12 +218,12 @@ static TOBJ_RXPDO_MAPPING sRxPDOMap = {PDO_OUT_ENTRIES, {
PDO_MAP_8(0x7000, 24), PDO_MAP_8(0x7000, 32), PDO_MAP_8(0x7000, 40)
}};
/* 0x1A00: 映射 0x6000:01..80, 每个 64 bits */
/* 0x1A00: 映射 0x6000:01..75, 每个 64 bits(9×8 + 3) */
static TOBJ_TXPDO_MAPPING sTxPDOMap = {PDO_IN_ENTRIES, {
PDO_MAP_8(0x6000, 0), PDO_MAP_8(0x6000, 8), PDO_MAP_8(0x6000, 16),
PDO_MAP_8(0x6000, 24), PDO_MAP_8(0x6000, 32), PDO_MAP_8(0x6000, 40),
PDO_MAP_8(0x6000, 48), PDO_MAP_8(0x6000, 56), PDO_MAP_8(0x6000, 64),
PDO_MAP_8(0x6000, 72)
PDO_MAP_ENTRY(0x6000, 73), PDO_MAP_ENTRY(0x6000, 74), PDO_MAP_ENTRY(0x6000, 75)
}};
_Static_assert(sizeof(sRxPDOMap.aEntries) / 4 == PDO_OUT_ENTRIES, "0x1600 条目数不符");
@@ -176,16 +233,11 @@ _Static_assert(sizeof(sTxPDOMap.aEntries) / 4 == PDO_IN_ENTRIES, "0x1A00 条目
/*---------------------------------------------------------------------------
* Entry Descriptions
*
* 重要(1.5.42 修正): OBJ_GetEntryDesc()(objdef.c:554) 只对 OBJCODE_ARR 复用
* OBJ_GetEntryDesc()(objdef.c:554) 只对 OBJCODE_ARR 复用
* pEntryDesc[1] 描述所有子索引; 对 OBJCODE_REC 是逐下标索引 pEntryDesc[Subindex]。
* 因此 REC 对象的描述数组必须给满(Sub0..SubN 共 N+1 项), 否则 SDO Info 越界读到
* 相邻常量数组的垃圾, 主站 config_map 报 "Unsupported access to an object" /
* "Attempt to read to a write only object"(实测 0x1600/0x1A00 的 sub0/3/4 报错
* 而 sub1/2 侥幸通过, 正是越界特征)。
*
* 1.5.55: 输入 PDO 扩到 48 条目后, 继续用 REC 就得写 49 项描述符(且 0x1600/0x1A00
* 与 0x7000/0x6000 条目数不同, 不能再共用一个数组)。这些条目类型完全一致, 正是
* ARR 的语义, 故统一改为 OBJCODE_ARR —— 描述数组固定 2 项, 由 SSC 复用 [1]。
* 相邻常量数组的垃圾。
* 条目类型完全一致时用 OBJCODE_ARR —— 描述数组固定 2 项, 由 SSC 复用 [1]。
* 注意: 可被 PDO 映射的数据子条目必须带 OBJACCESS_RxPDOMAPPING/TxPDOMAPPING,
* 否则主站校验 0x1600/0x1A00 的映射目标时会拒绝(官方 sampleappl.h 同此)。
*---------------------------------------------------------------------------*/
@@ -215,15 +267,266 @@ OBJCONST TSDOINFOENTRYDESC OBJMEM asInputEntryDesc[] = {
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}
};
/* SYNC 诊断 0x2000: REC, Sub0..Sub4 共 5 项, 纯只读不参与 PDO 映射。 */
/* SYNC 诊断 0x2000: REC, sub0..sub24 共 25 项, 纯只读不参与 PDO 映射,
* 改子索引数无需重刷 SII/EEPROM。sub5..12 为剖分埋点(DWT 周期/计数累加);sub13..23 为 [DIFF-STATS] 差分写取证仪表;
* sub24 = OP 输入新鲜度救援次数(不打串口,网页 /api/sync_diag 读)。
* 行数必须与 TOBJ2000 字段一一对应:少一行不会编译报错,只会让 SDO 从错误的
* 偏移取值 —— 故逐行标号便于核对。 */
OBJCONST TSDOINFOENTRYDESC OBJMEM asSyncDiagEntryDesc[] = {
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* sub0 : 子索引个数 = 24 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub1 : SYNC0 计数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub2 : SYNC1 计数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub3 : ESC IRQ 计数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub4 : uptime 秒 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub5 : PDI_Isr 累计周期 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub6 : PDO_OutputMapping */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub7 : MainLoop free-run 块 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub8 : PDI_Isr 次数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub9 : PDO_InputMapping 全程 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub10 : 其中输入 PDO 过程数据写 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub11 : AL 事件读×2+超限应答 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub12 : ECAT_Application */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub13 : [DIFF-STATS] 已完成窗口数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub14 : [DIFF-STATS] 窗口内拍数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub15 : [DIFF-STATS] 单拍变化字节峰值 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub16 : [DIFF-STATS] run 总数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub17 : [DIFF-STATS] 单拍 run 峰值 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub18 : [DIFF-STATS] 段1 变化字节 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub19 : [DIFF-STATS] 段2 变化字节 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub20 : [DIFF-STATS] 段3 变化字节 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub21 : [DIFF-STATS] 段4 变化字节 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub22 : [DIFF-STATS] 段5 变化字节 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub23 : [DIFF-STATS] 比对自身 DWT 周期 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub24 : OP 输入新鲜度救援次数 */
};
/*---------------------------------------------------------------------------
* 0x2502 引导程序交接请求(主站 → APP:「让你回 BOOT」)
*
* 为什么要有它:EtherCAT 直灌的会话对象在 **BOOT** 里(0x2500/0x2501),APP 的字典
* 根本没有这两个对象 —— 实测对着 APP 写 0x2500 回 abort=0x06020000「对象不存在」。
* 所以一台正在跑 APP 的机器必须先交接,才能走总线刷写。量产那条交接只有
* SPI 0xB0 和 CAN 0x320+"OTA!",没有总线内的入口;常驻刷写必须补上。
*
* 语义:写 1 = 请求进 BOOT;写 0 是无操作(不做"取消",避免半路误清)。
* 动作复用现成的 OtaBootRequest 异步链:这里**只置 pending 标志**,配置页三字的
* Flash 写入 + NVIC_SystemReset 由主循环 OtaBootRequest::process() 下一圈执行
* (sys/initial.cpp 每圈调;与 SPI 0xB0 在 ISR 里只置标志同一个形状),
* 所以 SDO 响应先发出去、复位后到,不会把 SM1 半包留在总线上。
*
* 交接后板子会稳定停在 BOOT:APP 写的是 EN=NOT_AVAL,而 BOOT 的救砖门
* ecatArmed() = !(CHECK==AVAL && EN==AVAL) ⇒ 顺手就把 0x2500 直灌解锁了,
* 不需要 word8 令牌(也别依赖它:APP 的 otaConfigSet 只保全 8 个字,一擦页就没了)。
*---------------------------------------------------------------------------*/
OBJCONST TSDOINFOENTRYDESC OBJMEM sEntryDesc0x2502[] = {
/* VAR 只有 :00 一个条目,且必须带写权限位(SSC 的通用写权限门在 objdef.c:1325,
* 早于 1352 的回调分发 —— BOOT 的 0x2500 就是栽在只给 ACCESS_READ 上) */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE},
};
static OBJCONST UCHAR aName0x2502[] = "GD32 boot request";
static UINT32 s_bootReq = 0U; /**< 最近一次写入值,读回用于主站自证 */
static UINT8 ObjBootReqWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
{
const UINT8 *p8 = (const UINT8 *)pData;
UINT32 v;
(void)index;
(void)bCompleteAccess;
if (subindex != 0U) {
return ABORTIDX_SUBINDEX_NOT_EXISTING;
}
if (pData == NULL || dataSize < 4U) {
return ABORTIDX_PARAM_LENGTH_TOO_SHORT;
}
v = (UINT32)p8[0] | ((UINT32)p8[1] << 8) | ((UINT32)p8[2] << 16) | ((UINT32)p8[3] << 24);
s_bootReq = v;
if (v == 1U) {
EcatEnterBootRequestAsync();
}
return 0U;
}
/*---------------------------------------------------------------------------
* 0x2503 ESP32 WiFi EN(PC9 / WLESS_EN)控制(主站 → APP)
*
* 为什么走 EtherCAT 而不是 SPI 0xA2 / WiFi HTTP:WiFi 使能被拉低后 ESP32
* 当场掉线,经 /spi/send 的控制路径跟被控对象同生共死——关得掉、开不回来。
* EtherCAT 主站在 PC 侧,与 ESP32 完全解耦,关断/开启都能下发。
*
* 语义(UINT32 bit[7:0]=action,与 sys/esp32_en.h Esp32En::drive 对齐):
* 0=拉高(开) 1=拉低保持(关) 2=拉高 3=浮空 4=只读
* 读回:bit[7:0] = 实际引脚电平 0/1(0xEE=无效 action)。
*---------------------------------------------------------------------------*/
OBJCONST TSDOINFOENTRYDESC OBJMEM sEntryDesc0x2503[] = {
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE},
};
static OBJCONST UCHAR aName0x2503[] = "ESP32 WiFi EN";
static UINT32 s_esp32EnVal = 1U; /**< 读回缓存:默认 1=上电已释放 EN */
static UINT8 ObjEsp32EnWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
{
const UINT8 *p8 = (const UINT8 *)pData;
UINT32 v;
UINT8 action, st;
(void)index;
(void)bCompleteAccess;
if (subindex != 0U) {
return ABORTIDX_SUBINDEX_NOT_EXISTING;
}
if (pData == NULL || dataSize < 4U) {
return ABORTIDX_PARAM_LENGTH_TOO_SHORT;
}
v = (UINT32)p8[0] | ((UINT32)p8[1] << 8) | ((UINT32)p8[2] << 16) | ((UINT32)p8[3] << 24);
action = (UINT8)(v & 0xFFU);
st = EcatEsp32EnDrive(action);
s_esp32EnVal = (UINT32)st;
return 0U;
}
/*---------------------------------------------------------------------------
* 0x2510 / 0x2511 电机 OTA 邮箱进料通路(主站 → APP)
*
* 为什么要有它:电机 OTA 的引擎在 GD32(MotorOtaRelay:AES 握手 + CAN 停等),
* 但固件明文只有一条进料路 —— ESP32 经 SPI 0xD1 喂。只插网线的场合那条 HTTP
* 打不通,于是前端把电机 OTA 标成「本链路不支持」。这两个对象把进料口换成
* EtherCAT 邮箱,引擎一行没动:数据窗的写回调最终调的还是 MotorOtaRelay::feedAt()。
*
* 形状完全照 BOOT 已验证的 0x2500/0x2501(comm/ecat/boot_ecat_app.cpp):
* - REC 的条目统一 UINT32,偏移才和 SSC「si0 占 2 字节 + 按 BitLength 紧排」一致;
* - 数据窗建成 VAR(不是 ARR):VAR 没有 sub0 计数字段,主站按 `:00` 整块写;
* VAR 的 maxSubindex 恒为 0 ⇒ 写 `:1` 会直接 ABORTIDX_SUBINDEX_NOT_EXISTING;
* complete access 对 VAR 一律 UNSUPPORTED ⇒ 主站必须传 CA=FALSE;
* - 条目描述必须带写权限位,objdef.c:1325 的通用写权限门早于 1352 的回调分发,
* 少了它回调一次都不会进(BOOT 的 0x2500 当初就这么静默躺平过)。
* 业务逻辑(暂存、CRC、泵、终态锁存)在 comm/ecat_motor_ota.cpp,本层只做
* SDO 字节 ↔ 纯整型参数的翻译。
*---------------------------------------------------------------------------*/
typedef struct OBJ_STRUCT_PACKED_START {
UINT16 bSubIndex; /**< sub0:条目数,SSC 按 UINT16 读 */
UINT32 u32Cmd; /**< sub1:W 1=start 3=abort;R 最近一次命令 */
UINT32 u32Total; /**< sub2:镜像总字节数 */
UINT32 u32Channel; /**< sub3:目标 CAN 通道 0..2 */
UINT32 u32Node; /**< sub4:目标节点 1..9 */
UINT32 u32ImgCrc; /**< sub5:整镜像 CRC32(MPEG-2,与 SPI 链路同款) */
UINT32 u32Staged; /**< sub6:已连续暂存字节数(只读) */
UINT32 u32RelayState; /**< sub7:MotorOtaRelay 状态(只读) */
UINT32 u32PkAcked; /**< sub8:已确认包数(只读) */
UINT32 u32PkTotal; /**< sub9:总包数(只读) */
UINT32 u32Err; /**< sub10:高字节=本模块会话码 0x8x,低字节=电机协议错误码 */
UINT32 u32Phase; /**< sub11:位图,见 ecat_motor_ota.cpp */
}
OBJ_STRUCT_PACKED_END
TOBJ2510;
_Static_assert(offsetof(TOBJ2510, u32Cmd) == 2, "TOBJ2510 未紧排:检查 OBJ_STRUCT_PACKED_START");
_Static_assert(sizeof(TOBJ2510) == 2 + 4 * 11, "TOBJ2510 尺寸应为 2+44");
static TOBJ2510 s_motorOtaCtl = {ECAT_MOTA_CTL_SUBS, 0U, 0U, 0U, 0U, 0U,
0U, 0U, 0U, 0U, 0U, 0U};
OBJCONST TSDOINFOENTRYDESC OBJMEM asMotorOtaCtlEntryDesc[] = {
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* sub0 : 子索引个数 = 11 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub1 : cmd(必须带写位,否则回调进不来) */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub2 : 镜像总长 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub3 : CAN 通道 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub4 : 节点号 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE}, /* sub5 : 整镜像 CRC32 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub6 : 已暂存字节 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub7 : relay 状态 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub8 : 已 ACK 包数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub9 : 总包数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub10 : 会话码<<8 | 协议错误码 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub11 : phase 位图 */
};
static OBJCONST UCHAR aName0x2510[] = "Motor OTA control";
OBJCONST TSDOINFOENTRYDESC OBJMEM asMotorOtaChunkEntryDesc[] = {
/* 唯一条目 = 一整块 [offset:u32][crc32:u32][data...]。
* 写回调在 objdef.c:1352 早分发,SSC 的默认拷贝路径不会执行。 */
{DEFTYPE_OCTETSTRING, (UINT16)(ECAT_MOTA_CHUNK_OBJ_BYTES * 8U), ACCESS_READWRITE},
};
static OBJCONST UCHAR aName0x2511[] = "Motor firmware chunk";
/** 数据窗的落地缓冲:写路径不依赖它(回调自己取字节),保留是给默认读路径的落点。 */
static UINT8 s_motorOtaChunk[ECAT_MOTA_CHUNK_OBJ_BYTES];
static UINT8 ObjMotorOtaCtlWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
{
const UINT8 rc = EcatMotorOtaCtlWrite(subindex, dataSize, (const UINT8 *)pData);
(void)index;
(void)bCompleteAccess;
if (subindex == 0U) {
return ABORTIDX_SUBINDEX_NOT_EXISTING; /* sub0 是条目数,不可写 */
}
if (subindex >= 2U + ECAT_MOTA_CTL_SUBS) {
return ABORTIDX_SUBINDEX_NOT_EXISTING;
}
if (rc != ECAT_MOTA_OK) {
/* 具体原因不靠 abort 码分类:主站读 0x2510:10 的高字节拿真码。 */
return ABORTIDX_DATA_CANNOT_BE_READ_OR_STORED;
}
return 0U;
}
/** 0x2510 读取:逐 subindex 从 ecat_motor_ota 取**实时**值,不读 s_motorOtaCtl 的镜像。 */
static UINT8 ObjMotorOtaCtlRead(UINT16 index, UINT8 subindex, UINT32 objSize,
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
{
const UINT32 v = EcatMotorOtaCtlRead(subindex);
UINT8 *p8 = (UINT8 *)pData;
(void)index;
(void)objSize;
(void)bCompleteAccess;
if (pData == NULL) {
return ABORTIDX_PARAM_LENGTH_TOO_SHORT;
}
if (subindex == 0U) {
/* sub0 是 UINT8(条目数),只占载荷一个字节 */
p8[0] = (UINT8)v;
} else {
p8[0] = (UINT8)(v & 0xFFU);
p8[1] = (UINT8)((v >> 8) & 0xFFU);
p8[2] = (UINT8)((v >> 16) & 0xFFU);
p8[3] = (UINT8)((v >> 24) & 0xFFU);
}
return 0U;
}
static UINT8 ObjMotorOtaChunkWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
{
const UINT8 rc = EcatMotorOtaChunkWrite(dataSize, (const UINT8 *)pData);
(void)index;
(void)bCompleteAccess;
if (subindex != 0U) {
return ABORTIDX_SUBINDEX_NOT_EXISTING; /* VAR 只有 :00 */
}
if (rc == ECAT_MOTA_E_GAP || rc == ECAT_MOTA_E_PARAM || rc == ECAT_MOTA_E_BADLEN) {
return ABORTIDX_VALUE_EXCEEDED;
}
if (rc != ECAT_MOTA_OK) {
return ABORTIDX_DATA_CANNOT_BE_READ_OR_STORED;
}
return 0U;
}
/*---------------------------------------------------------------------------
* Object Names
*---------------------------------------------------------------------------*/
@@ -266,9 +569,27 @@ TOBJECT OBJMEM ApplicationObjDicList[] = {
asInputEntryDesc, aName0x6000, &s_inputObj, NULL, NULL, 0x0000},
/* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路) */
{NULL, NULL, 0x2000, {DEFTYPE_RECORD, 4 | (OBJCODE_REC << 8)},
{NULL, NULL, 0x2000, {DEFTYPE_RECORD, 24 | (OBJCODE_REC << 8)},
asSyncDiagEntryDesc, aName0x2000, &s_syncDiag, NULL, NULL, 0x0000},
/* 0x2502: 引导程序交接请求(写 1 = 回 BOOT 等灌写;见 ObjBootReqWrite 注) */
{NULL, NULL, 0x2502, {DEFTYPE_UNSIGNED32, 0 | (OBJCODE_VAR << 8)},
sEntryDesc0x2502, aName0x2502, &s_bootReq, NULL, ObjBootReqWrite, 0x0000},
/* 0x2503: ESP32 WiFi EN(PC9) —— 经 EtherCAT 控制,WiFi 关断后仍可拉回 */
{NULL, NULL, 0x2503, {DEFTYPE_UNSIGNED32, 0 | (OBJCODE_VAR << 8)},
sEntryDesc0x2503, aName0x2503, &s_esp32EnVal, NULL, ObjEsp32EnWrite, 0x0000},
/* 0x2510: 电机 OTA 会话控制(写 sub2 声明总长 → 反复写 0x2511 灌块 → 写 sub1=1 启动) */
{NULL, NULL, 0x2510, {DEFTYPE_RECORD, ECAT_MOTA_CTL_SUBS | (OBJCODE_REC << 8)},
asMotorOtaCtlEntryDesc, aName0x2510, &s_motorOtaCtl,
ObjMotorOtaCtlRead, ObjMotorOtaCtlWrite, 0x0000},
/* 0x2511: 电机固件数据窗(VAR OCTETSTRING,主站分段下载攒完才回调一次) */
{NULL, NULL, 0x2511, {DEFTYPE_OCTETSTRING, 0 | (OBJCODE_VAR << 8)},
asMotorOtaChunkEntryDesc, aName0x2511, &s_motorOtaChunk,
NULL, ObjMotorOtaChunkWrite, 0x0000},
/* 终止条目 */
{NULL, NULL, 0xFFFF, {0, 0}, NULL, NULL, NULL, NULL, NULL, 0x000}
};
@@ -280,7 +601,7 @@ TOBJECT OBJMEM * VARMEM ApplicationObjDic = ApplicationObjDicList;
*---------------------------------------------------------------------------*/
UINT16 APPL_GenerateMapping(UINT16 *pInputSize, UINT16 *pOutputSize)
{
*pInputSize = ECAT_PDO_IN_BYTES; /* 80×UINT64 = 640 bytes */
*pInputSize = ECAT_PDO_IN_BYTES; /* 75×UINT64 = 600 bytes */
*pOutputSize = ECAT_PDO_OUT_BYTES; /* 48×UINT64 = 384 bytes */
return 0;
}
@@ -296,12 +617,12 @@ UINT16 APPL_StopMailboxHandler(void) { return 0; }
*---------------------------------------------------------------------------*/
UINT16 APPL_StartInputHandler(UINT16 *pIntMask)
{
/* 1.5.44:此处**不能**覆写 *pIntMask。
/* 此处**不能**覆写 *pIntMask。
*
* pIntMask 指向 SSC 全局 u16ALEventMask,StartInputHandler() 已按同步模式算好:
* free-run + 有输出 PDO 时它是 PROCESS_OUTPUT_EVENT(0x0400)(ecatslv.c:959/1090),
* 即 SM2 收到输出这一事件才是 SM-Synchron 模式的同步节拍。
* 旧代码写死 0x0800(PROCESS_INPUT_EVENT) 把 0x0400 顶掉了,而随后的
* 若写死 0x0800(PROCESS_INPUT_EVENT) 会把 0x0400 顶掉,而随后的
* SetALEventMask() 只做“或”,于是寄存器 0x204 永远缺 SM2 位:
* 输出到达不再产生 IRQ,PDI_Isr 只能被主站“读输入”事件带起来,
* 输入刷新因此永远落在主站读之后 —— SM3 是 3-buffer 缓冲模式,
@@ -317,8 +638,17 @@ UINT16 APPL_StopInputHandler(void) { return 0; }
/*---------------------------------------------------------------------------
* Output Handler (SAFEOP→OP)
*---------------------------------------------------------------------------*/
UINT16 APPL_StartOutputHandler(void) { return 0; }
UINT16 APPL_StopOutputHandler(void) { return 0; }
UINT16 APPL_StartOutputHandler(void)
{
EcatCan_ResetSequenceState();
return 0;
}
UINT16 APPL_StopOutputHandler(void)
{
EcatCan_ResetSequenceState();
return 0;
}
/*---------------------------------------------------------------------------
* APPL_AckErrorInd
@@ -340,12 +670,43 @@ void APPL_InputMapping(UINT16 *pInputData)
s_syncDiag.u32Sync1 = EcatDbg_GetSync1();
s_syncDiag.u32EscIrq = EcatDbg_GetEscIrq();
s_syncDiag.u32UptimeS = EcatDbg_GetUptimeSec();
/* 剖分埋点快照(sub5..12) */
s_syncDiag.u32PdiCyc = g_dbg_pdiCyc;
s_syncDiag.u32OutCyc = g_dbg_outCyc;
s_syncDiag.u32FreeRunCyc = g_dbg_freerunCyc;
s_syncDiag.u32PdiCnt = g_dbg_pdiCnt;
s_syncDiag.u32InCyc = g_dbg_inCyc;
s_syncDiag.u32InWriteCyc = g_dbg_inWriteCyc;
s_syncDiag.u32AlCyc = g_dbg_alCyc;
s_syncDiag.u32AppCyc = g_dbg_appCyc;
s_syncDiag.u32OpFreshRescues = s_opFreshRescues;
/* 1.5.54:先让 EthercatPdo 把本周期的输入数据写进 0x6000 的对象存储,
/* [DIFF-STATS] 差分写统计快照(sub13..23):一次取全 11 个窗口值,
* 取的都是"上一个已完成的 1s 窗口",故 SDO 读不会拿到半累积值。 */
{
UINT32 ds[11];
EcatDiffStats_GetWindow(ds);
s_syncDiag.u32DsWinSeq = ds[0];
s_syncDiag.u32DsCycles = ds[1];
s_syncDiag.u32DsChangedMax = ds[2];
s_syncDiag.u32DsRunsTotal = ds[3];
s_syncDiag.u32DsRunsMax = ds[4];
s_syncDiag.u32DsSeg1 = ds[5];
s_syncDiag.u32DsSeg2 = ds[6];
s_syncDiag.u32DsSeg3 = ds[7];
s_syncDiag.u32DsSeg4 = ds[8];
s_syncDiag.u32DsSeg5 = ds[9];
s_syncDiag.u32DsCompareCyc = ds[10];
}
/* 先让 EthercatPdo 把本周期的输入数据写进 0x6000 的对象存储,
* 再交给 ESC。经由对象存储中转(而非直接写 pInputData)是为了让主站
* SDO 读 0x6000 时看到与过程数据一致的内容。 */
EcatPdo_WriteInputs((UINT8 *)&s_inputObj.u64Data[0]);
memcpy(pInputData, &s_inputObj.u64Data[0], ECAT_PDO_IN_BYTES);
/* OP 输入新鲜度不变量的活性计数。每次真正走到输入映射就自增,
* 供 EcatDbg_OpInputFreshness 判断 PDI 路径是否还活着。 */
++s_inMapSeq;
}
/*---------------------------------------------------------------------------
@@ -362,11 +723,9 @@ void APPL_OutputMapping(UINT16 *pOutputData)
*---------------------------------------------------------------------------*/
void APPL_Application(void)
{
/* 1.5.54:应用逻辑全部搬进 EthercatPdo::process()(当前实现是 PDO↔CAN 转发)。
*
* 原 Phase 2 的 32B 回环已被取代:回环的验证价值由输入 PDO 的 seq 回显
* (ECAT_CAN_RX_OFF_ACK)承担——主站写 seq=N 后应在下一周期读回 ack=N,
* 同样能证明 输出→应用→输入 整条过程数据链路通。 */
/* 应用逻辑在 EthercatPdo::process()(当前实现是 PDO↔CAN 转发)。
* 不 CpuLoad 包裹:SM/DC 下本函数在 PDI/Sync0 ISR 内已被 isrEnter/Exit
* 计入;free-run 下若主循环每圈调用,再包会抬高 load_perm。 */
EcatPdo_Process();
}
@@ -374,35 +733,42 @@ void APPL_Application(void)
* AL 诊断快照 — task_runner 5s 心跳用 (C 链接, C++ 侧 extern "C" 声明)
* [19:16]=AL 状态(0x0130 低 4 位) [31:24]=AL 错误码(0x0130 高 8 位)
* [15:0] =AL 事件寄存器(0x0220, bit0=主站 AL 控制请求待处理)
*
* ESC 寄存器经 PDI/CSR 读出即主机序(LAN9252 CSR 引擎按
* 16-bit 主机总线给出寄存器"值";线上大端只存在于网络侧与 PRAM 原始帧数据),
* 与 SSC 自身 GetSyncMan/mailbox 的裸读约定一致,勿加 SWAPWORD。
*---------------------------------------------------------------------------*/
UINT32 EcatDbg_ReadAlDiag(void)
{
UINT16 alStatus = 0U;
HW_EscReadWord(alStatus, ESC_AL_STATUS_OFFSET);
alStatus = SWAPWORD(alStatus);
return ((UINT32)alStatus << 16) | (UINT32)SWAPWORD(HW_GetALEventRegister());
return ((UINT32)alStatus << 16) | (UINT32)HW_GetALEventRegister();
}
/*---------------------------------------------------------------------------
* AL Event Mask (0x204) 守护 + SM2 事件楔死自愈 — 1.5.48 新增, 1.5.49 扩展
* (C 链接, task_runner 1s 节流调用)
*
* 背景:实测"停止回 INIT 后重进 OP"概率性失败——主站 LRW WKC=3/3(输出
* 确实写进 SM2),但从站 9s ESM 超时报 0x001B:bEcatFirstOutputsReceived 一直
* 没被置位。该标志只在 PDI_Isr 收到 SM2 事件(AL Event 0x220 bit10)时置位。
* 01:03 串口日志钉死现场:evt=0x0400 在 0x220 挂起 10s 无人处理(PDI_Isr 的
* SM2 分支从未执行),escirq 2.6/s 证明 EXTI 只在 AL Control 事件时跳动——
* 即 PDI_Isr 在跑但 SM2 分支被某个内部旗标挡住(bEscIntEnabled 门控)。
* AL Event Mask (0x204) 守护 + SM2 事件楔死自愈
* (C 链接, task_runner 250ms 节流调用)
*
* 守护(1)0x204 缺位自愈:从站 AL>=SAFEOP 且有输出 PDO 时,SSC 不变式是
* "0x204 ⊇ u16ALEventMask"。读实际值比对,缺位则补写并打 WARN。
* 先补期望值再比对——下行转换路径(ecatslv.c AL_ControlInd 尾部)同时清
* 0x204 的 PDO 事件位、bEscIntEnabled、bEcatFirstOutputsReceived;若期望值
* 本身缺 SM2 位,旧判定恒通过、缺位永远补不回。SM-Synchron+有输出 PDO 的
* 不变式是 u16ALEventMask ⊇ PROCESS_OUTPUT_EVENT(ecatslv.c:959/1086)。
*
* 自愈(2)SM2 事件楔死(1.5.49):SAFEOP 下 0x220 bit10 挂起 且 标志未置位
* 自愈(2)SM2 事件楔死:SAFEOP 下 0x220 bit10 挂起 且 标志未置位
* = PDI_Isr 通路楔死。从主循环直接完成 PDI_Isr 该做的事:置位
* bEcatFirstOutputsReceived + 读 SM2 首/末字应答事件(ecatappl.c:317-320 同款)。
* 协议等价:bit10 挂起 = 主站已写过 SM2 = "收到输出"成立;AL_ControlRes 下一个
* MainLoop 周期即完成 SAFEOP→OP。WARN 附带全部内部旗标(bEscInt/bDcSync/
* 期望与实际 mask/看门狗状态),下次复现一行日志即可定位楔死路径。
* 协议等价:bit10 挂起 = 主站已写过 SM2 = "收到输出"成立。
* WARN 附带全部内部旗标(bEscInt/bDcSync/期望与实际 mask/看门狗状态),
* 下次复现一行日志即可定位楔死路径。
*
* 自愈(3)OP 态 PDI_Isr 门控:AL==OP 且 bEscIntEnabled==FALSE 不可
* 合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改 AL 状态;
* 守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即重入路径漏了恢复段。
* 此时 PDI_Isr 整体被门控(ecatappl.c:282),PDO_OutputMapping 停调、输入冻结。
* 修法:重开门 + 应答挂起的 SM2 事件(掩码补位由守护(1)在同次调用完成),
* 下一帧 LRW 的 SM2 中断即恢复整条过程数据链路。
*---------------------------------------------------------------------------*/
UINT32 EcatDbg_GuardAleventMask(void)
{
@@ -420,30 +786,17 @@ UINT32 EcatDbg_GuardAleventMask(void)
return 0xFFFFU; /* 状态不适用:返回哨兵值,调用方不打印 */
}
/* (0) 邮箱 SM 地址/长度失配自愈(2026-09-03 03:00 根治,1.5.52):
*
* 主站侧探针(楔死态)实锤的证据链:
* - ESC/SSC 邮箱处理全程活着(注入合法 CoE 请求后 200ms 内 SM1 出响应);
* - 但响应帧头出现在 SM1 start+8 而非 start(且逐会话累积:+4→+8,断电才清零);
* - 网络侧(FPRD)读 SM0/SM1 寄存器 start 恒正确(0x1000/0x1200);
* => SSC 的 u16EscAddrSendMbx(Init→PreOP 时经 GetSyncMan/CSR 间接读)拿到了错值,
* 主站按协议从 0x1200 读 → 帧头永远错位 → SOEM 报 WkcError/MailboxError,
* 且预清理重枚举(重跑 StartMailboxHandler)重读仍错 → 死锁,只有断电恢复。
* 自愈:AL>=PREOP 时用寄存器真值(0x0800/0x0808)校验 SSC 缓存的 start/len,
* 失配即写回 + WARN(旧值/新值一并打印,下次串口复现可确凿定位 CSR 读错还是
* 变量污染——两种成因此写回均有效,写回后下一个邮箱事务即恢复)。
* 变量仅在 Stop/StartMailboxHandler(INIT<->PREOP 迁移,主循环上下文)被写,
* 守护同样在主循环——无并发风险。 */
/* (0) 邮箱 SM 地址/长度失配兜底:
* 邮箱几何构建期常量化(mailbox.c 直达 ecat_def.h:MBX_SM0/1_START/SIZE),
* CSR 错值无进入缓存的路径;本守护为运行期内存踩踏的最后兜底:
* AL>=PREOP 时用寄存器真值校验 SSC 缓存的 start/len,失配即写回 + WARN。
* ESC 寄存器经 PDI/CSR 读出即主机序,勿加 SWAPWORD。 */
if (al >= 2U && bMbxRunning) {
UINT16 sm0Start = 0U, sm0Len = 0U, sm1Start = 0U, sm1Len = 0U;
HW_EscReadWord(sm0Start, ESC_SYNCMAN_REG_OFFSET);
HW_EscReadWord(sm0Len, ESC_SYNCMAN_REG_OFFSET + 2U);
HW_EscReadWord(sm1Start, ESC_SYNCMAN_REG_OFFSET + SIZEOF_SM_REGISTER);
HW_EscReadWord(sm1Len, ESC_SYNCMAN_REG_OFFSET + SIZEOF_SM_REGISTER + 2U);
sm0Start = SWAPWORD(sm0Start);
sm0Len = SWAPWORD(sm0Len);
sm1Start = SWAPWORD(sm1Start);
sm1Len = SWAPWORD(sm1Len);
if (sm0Len >= 8U && sm1Len >= 8U
&& (u16EscAddrReceiveMbx != sm0Start || u16EscAddrSendMbx != sm1Start
|| u16ReceiveMbxSize != sm0Len || u16SendMbxSize != sm1Len)) {
@@ -464,16 +817,20 @@ UINT32 EcatDbg_GuardAleventMask(void)
return 0xFFFFU; /* 以下仅 SAFEOP+ 适用 */
}
evt = SWAPWORD(HW_GetALEventRegister());
/* PDI 寄存器读即主机序,勿加 SWAPWORD。 */
evt = HW_GetALEventRegister();
HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET);
actual = SWAPWORD(actual);
/* (1) 0x204 缺位自愈 */
/* (1) 0x204 缺位自愈:先补期望值,再比对补实际值 */
if ((u16ALEventMask & PROCESS_OUTPUT_EVENT) == 0U) {
u16ALEventMask |= PROCESS_OUTPUT_EVENT;
printf("[ECAT][AEM] WARN: expected AL mask lacked SM2 bit -> 0x%04X\r\n",
(unsigned)u16ALEventMask);
}
if ((UINT16)(actual & u16ALEventMask) != u16ALEventMask) {
const UINT16 before = actual;
HW_SetALEventMask(u16ALEventMask);
HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET);
actual = SWAPWORD(actual);
printf("[ECAT][AEM] WARN: AL Event Mask lost bits, actual=0x%04X expect=0x%04X -> restored 0x%04X\r\n",
(unsigned)before, (unsigned)u16ALEventMask, (unsigned)actual);
}
@@ -482,7 +839,6 @@ UINT32 EcatDbg_GuardAleventMask(void)
* (OP 态下 LRW 每周期置位/清除 bit10 属正常,不能误报) */
if (al == 4U /* SAFEOP */ && (evt & 0x0400U) != 0U && bEcatFirstOutputsReceived == FALSE) {
HW_EscReadWord(wdState, ESC_PD_WD_STATE);
wdState = SWAPWORD(wdState);
bEcatFirstOutputsReceived = TRUE;
HW_EscReadWord(dummy, nEscAddrOutputData);
HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U));
@@ -494,5 +850,104 @@ UINT32 EcatDbg_GuardAleventMask(void)
(unsigned)(bDcSyncActive ? 1U : 0U),
(unsigned)u16ALEventMask, (unsigned)actual, (unsigned)wdState);
}
/* (3) OP 态 PDI_Isr 门控自愈:OP 下 bEscIntEnabled==FALSE
* 不可合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改
* AL 状态;守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即 SAFEOP
* 重入路径漏了恢复段。此时 PDI_Isr 整体被门控(ecatappl.c:282),
* PDO_OutputMapping 停调、输入冻结。
* 修法:重开门 + 应答挂起的 SM2 事件(ISR 非 OP 分支同款读首/末字);
* 0x204 掩码补位已由上面(1)在同次调用完成,下一帧 LRW 即恢复。 */
if (al == 8U /* OP */ && nPdOutputSize > 0U && bEscIntEnabled == FALSE) {
HW_EscReadWord(wdState, ESC_PD_WD_STATE);
bEscIntEnabled = TRUE;
HW_EscReadWord(dummy, nEscAddrOutputData);
HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U));
printf("[ECAT][AEM] WARN: OP with PDI_Isr gated, evt=0x%04X "
"(bDcSync=%u mask_exp=0x%04X mask_act=0x%04X wd=0x%04X) "
"-> gate reopened + event acked\r\n",
(unsigned)evt,
(unsigned)(bDcSyncActive ? 1U : 0U),
(unsigned)u16ALEventMask, (unsigned)actual, (unsigned)wdState);
}
return actual;
}
/*---------------------------------------------------------------------------
* OP 输入新鲜度不变量
*
* SM-Synchron 稳态下 MainLoop 的 free-run 分支被
* (!bEscIntEnabled || !bEcatFirstOutputsReceived) && !bDcSyncActive
* 整段跳过,输入 PDO **只**由 PDI_Isr 的 PDO_InputMapping 刷新。
* PDI 通路一停(门控/掩码/EXTI),ESC 仍答 WKC、AL 仍 OP,主站读到的是
* 冻住的旧缓冲。
*
* 不变量:「OP ⇒ 输入必须在 T ms 内被重写」。
* - 正常路径:Process 每拍自增 seq
* - 异常路径:seq 停涨超过阈值 → 主循环强制 PDO_InputMapping
* (屏中断防双写)+ 重开门/应答 SM2,主站侧 gseq/rx 立刻恢复前进
* 不依赖主站重拉、不依赖人工复位。MainLoop 若本身死锁则本函数也停 ——
* 那是另一类故障(心跳会停),不在本不变量覆盖范围。
*---------------------------------------------------------------------------*/
void EcatDbg_OpInputFreshness(void)
{
const UINT8 al = (UINT8)(nAlStatus & 0x0FU);
const UINT32 now = EcatDbg_Dwt();
/* 活性看 processSeq(Process 真填过输入),不能看 s_inMapSeq。
* InputMapping 可在 Process 停摆时继续拷陈旧 s_in,seq 照涨 → 救援永不触发。 */
const UINT32 seq = EcatPdo_ProcessSeq();
if (al != 8U || nPdInputSize == 0U) {
s_inMapSeqSeen = seq;
s_inStaleBaseCyc = now;
return;
}
if (seq != s_inMapSeqSeen) {
s_inMapSeqSeen = seq;
s_inStaleBaseCyc = now;
return;
}
/* seq 未推进:等满阈值再介入,避开两拍之间的正常空隙。
* 5ms @1ms 周期 = 连丢 ≥5 拍;@4ms 周期 = 连丢 1~2 拍,仍足够判死。 */
{
const UINT32 thresh = (SystemCoreClock / 1000U) * 5U;
if ((UINT32)(now - s_inStaleBaseCyc) < thresh) {
return;
}
}
/* 强制刷新:先重开门/首输出标志(门关着时 PDI 永远不会回来),
* 再在 EXMC 互斥窗口(仅屏蔽 EXTI0/1/2)内做一次输入映射,保证不关全局中断(Preempt 0 CAN RX 不受影响)。
* 必须先 EcatPdo_Process() 再 PDO_InputMapping()。
* Process 先填好 buildInput+fillDiagTail,InputMapping 再发布,救援才有内容可写。 */
{
UINT16 dummy = 0U;
const UINT16 evt = HW_GetALEventRegister();
if (!bEscIntEnabled) {
bEscIntEnabled = TRUE;
}
if (!bEcatFirstOutputsReceived) {
bEcatFirstOutputsReceived = TRUE;
HW_EscReadWord(dummy, nEscAddrOutputData);
HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U));
}
/* 救援路径计入 busy(低频;与 ISR/任务派发同一口径)。
* 不要把 MainLoop 整段包进 CpuLoad——见 task_runner / cpu_load.h。 */
CpuLoad_Enter();
const uint32_t prevMask = EscExmcBus_MaskEnter();
EcatPdo_Process();
PDO_InputMapping();
EscExmcBus_MaskExit(prevMask);
CpuLoad_Leave();
++s_opFreshRescues;
s_inMapSeqSeen = EcatPdo_ProcessSeq();
s_inStaleBaseCyc = EcatDbg_Dwt();
/* 不打串口:计数经 0x2000:24 / /api/sync_diag 上到网页,避免 5ms 刷屏。 */
}
}
UINT32 EcatDbg_OpFreshRescues(void)
{
return s_opFreshRescues;
}
+14 -2
View File
@@ -3,8 +3,6 @@
* @brief DCU EtherCAT 应用层接口声明
*
* 提供 SSC 所需的 APPL_ 回调函数声明。
* Phase 1: 桩实现,仅保证编译通过。
* Phase 3: 接入实际 PDO 映射(DCU 传感器输入 / 控制输出)。
*/
#ifndef COMM_SSC_PORT_DC_APP_H
#define COMM_SSC_PORT_DC_APP_H
@@ -12,6 +10,7 @@
#include "ecat_def.h"
#include "esc.h"
#include "ecatslv.h"
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
@@ -32,6 +31,19 @@ void APPL_InputMapping(UINT16 *pInputData);
void APPL_OutputMapping(UINT16 *pOutputData);
void APPL_Application(void);
/* OP 输入新鲜度不变量(task_runner 每圈调用;内部廉价,仅冻结时强制重写) */
void EcatDbg_OpInputFreshness(void);
UINT32 EcatDbg_OpFreshRescues(void);
/* HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
uint32_t EscExmcBus_MaskEnter(void);
void EscExmcBus_MaskExit(uint32_t prev);
uint32_t HW_EscExmcMaskEnter(void);
void HW_EscExmcMaskExit(uint32_t prev);
/* 复位输出 sequence 状态机 */
void EcatCan_ResetSequenceState(void);
#ifdef __cplusplus
}
#endif
+40 -27
View File
@@ -144,22 +144,17 @@
#endif
/** 看门狗由定时器中断检查。
* 1.5.47:1→0。ECAT_TIMER_INT=1 要求端口提供调用 ECAT_CheckTimer() 的 1ms 硬件
* 定时器 ISR——本工程从未实现,导致 ECAT_CheckTimer 在整个固件里零调用者:
* (a) EsmTimeoutCounter 永不递减 → 任何 pending 状态迁移(SAFEOP→OP 等)永不
* 超时,从站会无限期忽略一切 AL Control(实测"进 PREOP 失败: AL=SAFEOP
* code=0x0000"的卡死就是它);
* (b) DC_CheckWatchdog/PLL 计数器(i16WaitForPllRunningCnt)不前进 → 若 ESC
* 残留 DC 激活位(0x0981,上个 DC 会话遗留、跨重连不清),从站按 DC 同步
* 模式进 SAFEOP→OP,OP 等待 PLL 序列永远不满足 → SAFEOP+0x0000 永久挂起
* (00:27 复测日志: 等待期轨迹恒 SAFEOP+0x0000,LRW 每 7ms 也救不回)。
* 改 0 后 MainLoop 按 DWT CYCCNT 轮询(hw_gd32exmc.cpp 的 HW_GetTimerValue/
* HW_ClearTimerValue 本就是为此写的死代码),每 1ms 调一次 ECAT_CheckTimer。 */
* ECAT_TIMER_INT=0:MainLoop 按 DWT CYCCNT 轮询(hw_gd32exmc.cpp 的
* HW_GetTimerValue/HW_ClearTimerValue),每 1ms 调一次 ECAT_CheckTimer。
* 若置 1 则要求端口提供调用 ECAT_CheckTimer() 的 1ms 硬件定时器 ISR——
* 本工程无该 ISR,ECAT_CheckTimer 零调用者会导致 EsmTimeoutCounter 永不递减
* (状态迁移永不超时)及 DC PLL 计数器不前进(残留 DC 激活位时 SAFEOP+0x0000
* 永久挂起)。 */
#ifndef ECAT_TIMER_INT
#define ECAT_TIMER_INT 0
#endif
/** 1.5.47:DWT CYCCNT @180MHz 的每毫秒 tick 数(ECAT_TIMER_INT=0 时 MainLoop 用)。 */
/** DWT CYCCNT @180MHz 的每毫秒 tick 数(ECAT_TIMER_INT=0 时 MainLoop 用)。 */
#ifndef ECAT_TIMER_INC_P_MS
#define ECAT_TIMER_INC_P_MS 180000U
#endif
@@ -535,13 +530,10 @@
------
------ Struct Packing
------
------ 1.5.42 修正:此前三对宏全部留空,注释误以为"AC6 不需要"。实际这些宏就是 SSC
------ 用来紧排结构体的唯一手段——SSC 的 SDO 读写按"si0 占 16bit,其后各条目按
------ BitLength 紧排"计算字节偏移(OBJ_GetEntryOffset, objdef.c:609),留空则结构体
------ 按编译器自然对齐,与该假设不符。armclang 实测:{UINT16; UINT64[4]} 的 UINT64
------ 落在字节 8 而非 2(编译期断言验证),{UINT16; UINT32 x4} 落在字节 4 而非 2,
------ 于是 0x7000/0x6000/0x2000 的 SDO 单条目访问全部读写错位内存;SSC 内部
------ 0x1C3x(TSYNCMANPAR)/0x10F1 等同样受影响。
------ 这些宏是 SSC 紧排结构体的唯一手段——SSC 的 SDO 读写按"si0 占 16bit,
------ 其后各条目按 BitLength 紧排"计算字节偏移(OBJ_GetEntryOffset, objdef.c:609),
------ 留空则结构体按编译器自然对齐,与该假设不符,导致 SDO 单条目访问
------ 全部读写错位内存;SSC 内部 0x1C3x/0x10F1 等同样受影响。
------ AC6/armclang 支持 GCC 风格 __attribute__((packed)),置于 struct 关键字之后
------ (即 START 位置)对整个结构体生效。
-----------------------------------------------------------------------------------------*/
@@ -590,11 +582,8 @@
------
------ PDO Size (需与 DCU_ESI.xml / gen_eeprom.py / dc_app.c 三处一致)
------
------ 1.5.56 扩到 DPRAM 硬上限:64B/384B → 384B/640B。
------ 输出 384B = 48×64bit:兼容单帧区 48B + CANFD 批量发送区 336B
------ 输入 640B = 80×64bit:桥状态 32B + 电机窗口 288B + 长帧 40B + 批量接收区 280B
------ 上限依据:邮箱占 0x1000-0x13FF,过程数据区仅 0x1400-0x1FFF=3072B,
------ SM2/SM3 均 3-buffer 实占 3×size ⇒ out+in ≤ 1024B。384+640 恰好用满。
------ 上限依据:邮箱占 0x1000-0x13FF,过程数据区仅 0x1400-0x1FFF=3072B,
------ SM2/SM3 均 3-buffer 实占 3×size ⇒ out+in ≤ 1024B。
------ 推导与实测(SII 余量/EXMC 耗时)见 comm/ethercat_pdo_size.h。
------
------ 这两个宏是**编译期硬门限**,必须先于其它改动生效:
@@ -604,12 +593,12 @@
------ 忘改这里会在进 SAFEOP 时报 AL 错误,而不是静默截断。
-----------------------------------------------------------------------------------------*/
/** 最大输出 PDO 字节数(RxPDO,主站→从站)。1.5.56: 0x40 → 0x180 = 384B */
/** 最大输出 PDO 字节数(RxPDO,主站→从站)。0x180 = 384B */
#ifndef MAX_PD_OUTPUT_SIZE
#define MAX_PD_OUTPUT_SIZE 0x180
#endif
/** 最大输入 PDO 字节数(TxPDO,从站→主站)。1.5.56: 0x140 → 0x280 = 640B */
/** 最大输入 PDO 字节数(TxPDO,从站→主站)。0x280 = 640B */
#ifndef MAX_PD_INPUT_SIZE
#define MAX_PD_INPUT_SIZE 0x280
#endif
@@ -666,8 +655,10 @@
#define DEVICE_SW_VERSION "1.0"
#endif
/* 0x100A 的位长。SSC 默认 3 字符只装得下占位 "1.0";这里抬到 16,真实值由
* sys/initial.cpp 起板时写进 acSoftwareversion(与 BOOT 侧同款改动)。 */
#ifndef DEVICE_SW_VERSION_LEN
#define DEVICE_SW_VERSION_LEN 0x3
#define DEVICE_SW_VERSION_LEN 0x10
#endif
/*-----------------------------------------------------------------------------------------
@@ -708,6 +699,28 @@
#define MAX_MBX_READ_ADDRESS 0x1FFF
#endif
/* 本板邮箱几何构建期常量。
* 取值 = gen_eeprom.py 的 MBX_RX_OFFSET/SIZE(0x1000/256) 与 MBX_TX_OFFSET/SIZE
* (0x1200/256),即 DCU_ESI.xml <Sm> 与 ESC 上电自 SII 加载的寄存器真值;主站
* config_init 也只是照抄同一份 SII。MBX_StartMailboxHandler 以此直达缓存,
* 不信任单次 CSR 间接读,CSR 读仅作交叉校验(mailbox.c 打 WARN)。
* ★ 改 SII 邮箱几何必须同步改这里(gen_eeprom.py 顶部注释同样有约束)。★ */
#ifndef MBX_SM0_START
#define MBX_SM0_START 0x1000
#endif
#ifndef MBX_SM0_SIZE
#define MBX_SM0_SIZE 0x0100
#endif
#ifndef MBX_SM1_START
#define MBX_SM1_START 0x1200
#endif
#ifndef MBX_SM1_SIZE
#define MBX_SM1_SIZE 0x0100
#endif
/*-----------------------------------------------------------------------------------------
------
------ PDO Timing (DC sync parameters, unit: ns)
+13 -9
View File
@@ -4,8 +4,7 @@ Generate LAN9252 EEPROM binary image (ESI/SII) for GD32E508 DCU.
Outputs a C header file with the EEPROM content as a byte array.
Layout per ETG.1000.6 section 5.4 "SII coding" (Table 16/17, verified against the
local spec copy ETG1000_6_V1i0i3; 1.5.27 rewrite of the previous generator whose
layout was shifted by 2 words from word 4 onwards and used XOR instead of CRC-8):
local spec copy ETG1000_6_V1i0i3):
Word 0x00 PDI Control (byte0=PDI type 0x8D HBI 16-bit indexed,
byte1=ESC Configuration 0x00)
@@ -62,7 +61,6 @@ MBX_TX_SIZE = 256
# Process Data configuration
#
# 1.5.56 扩到 DPRAM 硬上限 64/384 -> 384/640(需要一周期合并多条 CANFD 帧)。
# 必须与 comm/ethercat_pdo_size.h 的 ECAT_PDO_OUT_BYTES / ECAT_PDO_IN_BYTES 一致
# ——固件侧有 _Static_assert 守着,但 EEPROM 这一侧无法编译期校验,只能靠这里对齐。
#
@@ -70,8 +68,8 @@ MBX_TX_SIZE = 256
# 邮箱 SM0/SM1 各 256B 固定占 0x1000-0x13FF,过程数据区只剩 0x1400-0x1FFF=3072B。
# SM2/SM3 都是 3-buffer 模式(control 0x64 / 0x20,bit1=0),实际占用 3×size:
# SM2 384B -> 0x1400-0x187F (384×3 = 1152B)
# SM3 640B -> 0x1880-0x1FFF (640×3 = 1920B) ← 恰好用满最后一字节
# (384+640)×3 = 3072 = 过程数据区全长,一字节不剩也一字节不越界。
# SM3 600B -> 0x1880-0x1F67 (600×3 = 1800B), DPRAM 留 120B 余量
# (384+600)×3 = 2952 ≤ 3072 = 过程数据区全长。
# SSC 在 ecatslv.c:708-721 按 (start + size*buffer) 做重叠检查,撞了会在
# PREOP->SAFEOP 返回 ALSTATUSCODE_INVALIDSMOUTCFG,不是静默出错。
# 要再更大只能压邮箱(拖慢 SDO/FoE)或改 1-buffer(失去读写并发保护),见
@@ -79,7 +77,13 @@ MBX_TX_SIZE = 256
PD_OUT_OFFSET = 0x1400 # SM2: RxPDO (Master -> Slave outputs)
PD_OUT_SIZE = 384 # 48×64bit
PD_IN_OFFSET = 0x1880 # SM3: TxPDO (Slave -> Master inputs)
PD_IN_SIZE = 640 # 80×64bit
PD_IN_SIZE = 600 # 75×64bit
# 批量接收为**定长覆盖窗口** 5 槽(每槽 12B 头含 u32 seqlock
# + 32B 数据 = 44B,共 220B,落在 364..583),段5诊断区 584。
# 5 槽 = 5000 帧/s@1ms。
# SM3 实占 3×600=1800B (0x1880-0x1F67),out+in=984 ≤ 1024 硬上限,留 120B 余量;
# 依据见 comm/ethercat_pdo_size.h。
# 改后必须重烧 EEPROM 并断电冷启动。
# 3-buffer 占用与 DPRAM 上限自检:改尺寸/偏移时立刻在生成阶段拦住,
# 而不是等烧进 EEPROM、冷启动后再看 AL 错误码。
@@ -97,7 +101,7 @@ assert PD_OUT_SIZE % 8 == 0 and PD_IN_SIZE % 8 == 0, \
# 0x8D=HBI 16-bit). 0x05 is a reserved value on LAN9252 and disables the HBI PDI.
PDI_CONTROL = 0x8D
# Word 0 high byte = ESC Configuration register (0x0141): all zero
# (the old 0x0C hit "NOT set by EEPROM" DC bits - removed in 1.5.27).
# (0x0C would hit "NOT set by EEPROM" DC bits).
ESC_CONFIG = 0x00
# Word 1 low byte = PDI Configuration register (0x0150) HBI-mode bits: ALE
# qual / RD_WR mode / CS / RD / WR / ALE polarities. 0x00 = normal active-low
@@ -109,7 +113,7 @@ PDI_CONFIG = 0x00
# SYNC1 Output=1, SYNC0 Map=1, SYNC0 Output=1, both Driver/Polarity=00
# (push-pull ACTIVE LOW, DS p247). Active-low pulses pair with the firmware's
# falling-edge EXTI; with 0x00 the SYNC0/1 pins stay LATCH inputs and a DC
# SYNC0 event would never reach PB0 -> process data dead in DC mode (1.5.28).
# SYNC0 event would never reach PB0 -> process data dead in DC mode.
# Same value as the official LAN9252-EVB-HBIXML.xml ConfigData 890E80CC...
SYNC_LATCH_CFG = 0xCC
# Word 2 = Pulse Length of SyncSignals register (0x0982), units of 10ns
@@ -343,7 +347,7 @@ for row in range(0, EEPROM_SIZE, 16):
chunk = eeprom[row:row+16]
hex_str = ", ".join(f"0x{b:02X}" for b in chunk)
if all(b == 0xFF for b in chunk):
# 1.5.28: emit the real 0xFF bytes. A comment-only padding row leaves
# Emit the real 0xFF bytes. A comment-only padding row leaves
# those array elements zero-initialized (C static init) instead of
# the erased-0xFF state, and the firmware burns the whole array.
if row % 256 == 0:
@@ -3,13 +3,13 @@
* @brief SSC V5.11 硬件 port 层 — GD32E508 + LAN9252 (EXMC 16-bit HBI Indexed Mode)
*
* 实现 el9800hw.h 中声明的全部 HW_* 函数,将 SSC 的 ESC 访问请求
* 转发到 EscLan9252 的间接访问端口(1.5.39 按 DS 12.11 修正访问模型):
* 转发到 EscLan9252 的间接访问端口:
* - EtherCAT 核心 CSR(0x0000-0x0FFF:AL Status/Event、SyncManager、LED 等)
* → ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300) 间接端口;
* - 过程 RAM(0x1000-0x1FFF:邮箱 0x1000+、PDO 0x1400+)
* → PRAM FIFO(0x000-0x03C 数据口 + 0x308-0x314 地址/命令)。
* 1.5.37 及之前直读/直写 HBI 索引(误以为索引空间统一可寻址),核心 CSR 全部
* 读 0/写丢弃——SSC 对主站 AL Control 完全失聪,主站恒读 AL=INIT。
* 核心 CSR 不在 HBI 直接空间,直读/直写会使核心 CSR 全部
* 读 0/写丢弃,SSC 对主站 AL Control 完全失聪。
*
* 并发保护(不变):
* - HW_EscRead/HW_EscWrite 等主循环函数:内部经 EscExmcBus::maskEnter/Exit
@@ -229,8 +229,7 @@ void HW_EscWriteIsr(MEM_ADDR *pData, UINT16 Address, UINT16 Len)
/**
* @brief 禁用指定 SM 通道 (清 SM Activate 寄存器 0x0806+8n bit0)。
* @param channel SM 通道号 (0-15)
* @note 1.5.39:偏移由 +4 改为 +6——+4 是 SM Control(操作模式字段),
* Activate(bit0=SM Enable)在 +6;旧值从未真正生效过(访问层全空)。
* @note SM Activate 在偏移 +6(bit0=SM Enable);+4 是 SM Control(操作模式字段)。
*/
void HW_DisableSyncManChannel(UINT8 channel)
{
+20 -1
View File
@@ -209,8 +209,27 @@ OBJCONST UCHAR OBJMEM aName0x1009[] = "Hardware version";
-----------------------------------------------*/
/**
* \brief 0x100A (Software version) variable to handle the object data
*
* 必须显式给到 DEVICE_SW_VERSION_LEN+1:条目描述符按 LEN 声明位长,SSC 会照长度
* 整块回给主站;用 =DEVICE_SW_VERSION 推出来只有 4 字节,读到的是越界内容。
* 真实版本号由 sys/initial.cpp 起板时调 AppSetSoftwareVersion() 覆盖。
*/
CHAR acSoftwareversion[] = DEVICE_SW_VERSION;
CHAR acSoftwareversion[DEVICE_SW_VERSION_LEN + 1] = DEVICE_SW_VERSION;
/** @brief 覆盖 0x100A 上报的软件版本串(容量只有本文件知道,故 setter 放这儿)。 */
void AppSetSoftwareVersion(const char *version)
{
unsigned int i = 0U;
if (version == NULL) {
return;
}
/* 本文件没 include stdio,手写有界拷贝:超长就截断,末尾必带 NUL。 */
for (; i + 1U < sizeof(acSoftwareversion) && version[i] != '\0'; i++) {
acSoftwareversion[i] = version[i];
}
acSoftwareversion[i] = '\0';
}
/**
* \brief 0x1009 (Hardware version) entry description
+139 -101
View File
@@ -11,94 +11,6 @@
This file contains the Process Data interface
\version 5.11
<br>Changes to version V5.10.1:<br>
V5.11 COE3: change 0x10F3.2 (Sync Error limit) from UINT32 to UINT16 (according to the ETG.1020)<br>
V5.11 ECAT1: update EEPROM access reset operation<br>
V5.11 ECAT10: change PROTO handling to prevent compiler errors<br>
V5.11 ECAT11: create application interface function pointer, add eeprom emulation interface functions<br>
V5.11 ECAT2: update EEPROM access retry cycle (add 10ms delay between two retry cycles)<br>
V5.11 ECAT3: handle bus cycle calculation for input/output only devices and create warning diag message only if calculation failed<br>
V5.11 ECAT4: enhance SM/Sync monitoring for input/output only slaves<br>
V5.11 ECAT6: add function to calculate bus cycle time<br>
V5.11 ECAT8: call PDO_InputMapping only once if DC is enabled and COE is not supported<br>
V5.11 EEPROM1: fix compiler error during pEEPROM pointer initialization<br>
V5.11 EEPROM2: write Station alias value to EEPROM data register on EEPROM reload command<br>
V5.11 EEPROM3: clear EEPROM error bits<br>
V5.11 EEPROM4: prevent the variable in the EEPROM busy loop to be removed by the compiler<br>
V5.11 ESM7: "add Sync define for 0x22 (""SYNCTYPE_SM2_SYNCHRON""), support value 0x22 for 0x1C33.1 (SM2 sync)"<br>
<br>Changes to version V5.01:<br>
V5.10 COE1: Define one entry description for all 0x1C3x objects and change data type of SI11,12,13 to UINT16 (according ETG.1020)<br>
V5.10 ECAT1: Correct calculation of blinking and flashing sequence<br>
V5.10 ECAT13: Update Synchronisation handling (FreeRun,SM Sync, Sync0, Sync1)<br>
Compare DC UINT configuration (by ESC Config data) vs. DC activation register settings<br>
Update 0x1C3x entries<br>
V5.10 ECAT2: Prevent EEPROM data null pointer access (if the pointer is null an command error is set)<br>
EEPROM emulation return command error if unknown command was received<br>
V5.10 ECAT4: Update alignment marco for 8 to 15 bit alignments (16 and 32 Bit controllers)<br>
Bugfix calculate LED blink frequency<br>
V5.10 ECAT7: Add "bInitFinished" to indicate if the initialization is complete<br>
V5.10 HW2: Change HW_GetTimer return value to UINT32<br>
<br>Changes to version V5.0:<br>
V5.01 APPL3: Include library demo application<br>
V5.01 ESC1: Change ESC access function (if EEPROM Emulation is active)<br>
V5.01 ESC2: Add missed value swapping<br>
<br>Changes to version V4.40:<br>
V5.0 TEST1: Add test application. See Application Note ET9300 for more details.<br>
V5.0 ECAT2: Application specific functions are moved to application files.<br>
V5.0 ECAT3: Global dummy variables used for dummy ESC operations.<br>
V5.0 ESC1: ESC 32Bit Access added.<br>
V5.0 ESC3: Add EEPROM emulation support.<br>
V5.0 ESM3: Handling pending ESM transitions.<br>
V5.0 ESC5: Enhance EEPROM access handling.<br>
V5.0 PDO1: AL Event flags are not rechecked in PDO_OutputMappping(). (Already checked before call function)<br>
V5.0 SYNC1: Add missed SM event indication (0x1C32/0x1C33 SI11).<br>
<br>Changes to version V4.30:<br>
V4.40 DIAG1: add diagnosis message support<br>
V4.40 PDO1: merge content of HW_InputMapping (spihw.c/mcihw.c) to PDO_InputMapping. merge content of HW_OutputMapping (spihw.c/mcihw.c) to PDO_OutputMapping.<br>
V4.40 PDO2: Generic process data length calculation<br>
V4.40 ECAT2: call cyclic CheckIfLocalError() to check the local flags<br>
V4.40 HW0: Generic hardware access functions. Add Function (PDI_Isr()), content merged from spihw.c and mcihw.c.<br>
V4.40 WD1: define (ESC_SM_WD_SUPPORTED) to choose ESC SyncManager watchdog or local watchdog<br>
V4.40 ESM2: Change state transition behaviour from SafeOP to OP<br>
V4.40 TIMER1: Change bus cycle time calculation and trigger of ECAT_CheckTimer() if ECAT_TIMER_INT is reset<br>
V4.40 HW1: Add support for fc1100 hardware<br>
<br>Changes to version V4.20:<br>
V4.30 EL9800: EL9800_x cyclic application is moved to el9800.c<br>
V4.30 OBJ 3: add object dictionary initialization<br>
V4.30 SYNC: add CalcSMCycleTime() (calculation of bus cycle time); change synchronisation control functions<br>
V4.30 PDO: include PDO specific functions (moved from coeappl.c).<br>
xxx_InputMapping(); xxx_OutputMapping(); xxx_ReadInputs(); xxx_ResetOutputs(); xxx_Application()<br>
V4.30 CiA402: Add CiA402_StateMachine() and CiA402_Application() call<br>
V4.20 DC 1: Add DC pending Statemachine handling<br>
V4.20 PIC24: Add EL9800_4 (PIC24) required source code<br>
V4.20 LED 1: Modified LED Handling<br>
V4.11 APPL 1: The checkWatchdog() function should not called in checkTimer() if this function is triggered by an Interrupt<br>
<br>Changes to version V4.08:<br>
V4.10 LED 1: The handling of the EtherCAT-Error-LED was added<br>
V4.10 AOE 3: The AoE fragment size has to be initialized during the state transition<br>
from INIT to PREOP<br>
<br>Changes to version V4.07:<br>
V4.08 LED 1: The handling of the EtherCAT-LED can be (de-)selected by the switch LEDS_SUPPORTED<br>
because the ET1100 and ET1200 have an output port which could be connected directly.<br>
<br>Changes to version V4.01:<br>
V4.02 ECAT 1: The watchdog timer variables shall be initialized.<br>
<br>Changes to version V4.00:<br>
V4.01 APPL 1: If the application is running in synchron mode and no SM event<br>
is received, the application should be called from the main loop<br>
V4.01 APPL 2: In FreeRun mode the output should only be copied if the slave is in OP<br>
<br>Changes to version V3.20:<br>
V4.00 APPL 1: The watchdog checking should be done by a microcontroller<br>
timer because the watchdog trigger of the ESC will be reset too<br>
if only a part of the sync manager data is written<br>
V4.00 APPL 2: The setting of EtherCAT state LEDs were included<br>
V4.00 APPL 3: The outputs should be reset to a safe state,<br>
when the state OP is left<br>
V4.00 APPL 4: An example for the EEPROM access through the ESC is shown in<br>
the function APPL_StartMailboxHandler<br>
V4.00 APPL 5: The inputs should be read once when the state transition<br>
from PREOP to SAFEOP is made<br>
V4.00 APPL 6: The main function was split in MainInit and MainLoop
*/
@@ -157,6 +69,33 @@ BOOL bCycleTimeMeasurementStarted; // indicates if the bus cycle measurement is
UINT16 aPdOutputData[(MAX_PD_OUTPUT_SIZE>>1)];
UINT16 aPdInputData[(MAX_PD_INPUT_SIZE>>1)];
/*ECAT_DBG_START(剖分埋点)*/
/* 负载归因埋点:DWT 周期累加器,经 0x2000 sub5..8 上报,主站 1s 差分出速率。
* EcatDbg_Dwt() 定义在 comm/ethercat_sync.cpp (extern "C")。 */
extern UINT32 EcatDbg_Dwt(void);
#define ecatdbg_dwt() EcatDbg_Dwt()
/* CPU 负载挂钩(sys/cpu_load.cpp) */
extern void CpuLoad_Enter(void);
extern void CpuLoad_Leave(void);
/* 1ms 调度节拍(ethercat_sync.cpp extern "C") */
extern UINT32 EcatDbg_GetTick1ms(void);
/* 输出探针(实现见 comm/ethercat_pdo.cpp,C 链接) */
extern UINT8 EcatPdo_OutputProbe(UINT8 const *head);
extern void EcatPdo_NoteOutputMapped(void);
/* 稀疏写 ESC 计划(密排 count 只写有效段; 实现见 ethercat_pdo.cpp) */
extern UINT8 EcatPdo_SparseEscPlan(UINT16 *off0, UINT16 *len0, UINT16 *off1, UINT16 *len1);
/* 差分写 ESC 多段计划(段头/段尾 + 3-buffer 冲刷; 实现见 ethercat_pdo.cpp) */
extern UINT8 EcatPdo_EscWritePlan(UINT16 *offs, UINT16 *lens, UINT8 maxSegs);
volatile UINT32 g_dbg_pdiCyc = 0; /* sub5: PDI_Isr 全函数累计周期 */
volatile UINT32 g_dbg_outCyc = 0; /* sub6: PDO_OutputMapping(输出探针/全量读) */
volatile UINT32 g_dbg_freerunCyc = 0; /* sub7: MainLoop free-run 分支(SM 同步下恒 0) */
volatile UINT32 g_dbg_pdiCnt = 0; /* sub8: PDI_Isr 触发次数(=每周期 1 次) */
volatile UINT32 g_dbg_inCyc = 0; /* sub9: PDO_InputMapping 全程(APPL+过程数据写) */
volatile UINT32 g_dbg_inWriteCyc = 0; /* sub10: 其中 HW_EscWriteIsr 640B 过程数据写 */
volatile UINT32 g_dbg_alCyc = 0; /* sub11: PDI_Isr 内 AL 事件读×2+超限应答 */
volatile UINT32 g_dbg_appCyc = 0; /* sub12: ECAT_Application(桥 processOutput) */
/*ECAT_DBG_END*/
/*variables are declared in ecatslv.c*/
extern VARVOLATILE UINT16 u16dummy;
BOOL bInitFinished = FALSE; /** < \brief indicates if the initialization is finished*/
@@ -178,21 +117,76 @@ BOOL bInitFinished = FALSE; /** < \brief indicates if the initialization is fini
*////////////////////////////////////////////////////////////////////////////////////////
void PDO_InputMapping(void)
{
/*ECAT_DBG(剖分:输入映射全程 + 过程数据写细分)*/
/* 稀疏写:APPL 仍整填 aPdInputData(对齐 SDO 0x6000),但 HW_EscWriteIsr
* 只写密排有效段+段3/5,跳过空槽区 —— 省的是 PRAM 字节时间,不是 RAM 拷贝。
* 差分写:在稀疏之上再跳过「与影子一致且 3-buffer 冲刷完成」的段。 */
const UINT32 dbgT0 = ecatdbg_dwt();
APPL_InputMapping((UINT16*)aPdInputData);
HW_EscWriteIsr(((MEM_ADDR *) aPdInputData), nEscAddrInputData, nPdInputSize );
const UINT32 dbgT1 = ecatdbg_dwt();
{
UINT16 offs[4], lens[4];
const UINT8 nseg = EcatPdo_EscWritePlan(offs, lens, 4U);
if (nseg == 0U) {
HW_EscWriteIsr(((MEM_ADDR *) aPdInputData), nEscAddrInputData, nPdInputSize );
} else {
for (UINT8 i = 0U; i < nseg; ++i) {
HW_EscWriteIsr(((MEM_ADDR *)((UINT8 *)aPdInputData + offs[i])),
(UINT16)(nEscAddrInputData + offs[i]), lens[i]);
}
}
}
g_dbg_inWriteCyc += ecatdbg_dwt() - dbgT1;
g_dbg_inCyc += ecatdbg_dwt() - dbgT0;
}
/////////////////////////////////////////////////////////////////////////////////////////
/**
\brief This function will copies the outputs from the ESC memory to the local memory
to the hardware. This function is only called in case of an SM2
to the hardware. This function is only called in case of an SM2
(output process data) event.
输出按需全量读:每周期先读输出 PDO 头 12B 探针(覆盖 SEQ@0
与 WIN@8),经 EcatPdo_OutputProbe 判定——seq 无变化沿且无清统计请求时,
跳过全量读(只补读 SM2 末字完成事件应答)。主站滚动窗口扫槽依赖的
WIN 字节在探针内每周期刷新,窗口扫描不受影响。
SM2 应答语义不变:首字(探针内)+末字(两分支各自覆盖)。
*////////////////////////////////////////////////////////////////////////////////////////
void PDO_OutputMapping(void)
{
/*ECAT_DBG(剖分:输出映射全程)*/
const UINT32 dbgT0 = ecatdbg_dwt();
HW_EscReadIsr(((MEM_ADDR *)aPdOutputData), nEscAddrOutputData, nPdOutputSize );
/* 输出映射:12B 探针判定是否有新输出。
* 「每拍始终全量读 384B」会在 PDO 提速后把 PDI 成本抬高一个数量级
* (历史:探针版 4kHz load≈80%;全量+process 双读时 2kHz 即饱和)。
* 探针命中才全量读;否则只补读 SM2 末字。s_out 长期不更新由
* EthercatPdo::process() 按新鲜度门兜底全量读(不是每拍)。 */
UINT8 head[12];
HW_EscReadIsr(((MEM_ADDR *) head), nEscAddrOutputData, 12U );
APPL_OutputMapping((UINT16*) aPdOutputData);
if ( EcatPdo_OutputProbe(head) )
{
/* 有新 seq 时不必每次拉满 384B:先读命令前缀(含批量 count@48)。
* 8kHz 主站每拍都递增 seq 时,若 count==0 仍全量读会每拍吃满 PRAM,
* 把 CAN 解析饿死(gseq/s 从 ~3750 掉到数百)。 */
const UINT16 prefix = (nPdOutputSize < 52U) ? nPdOutputSize : 52U;
HW_EscReadIsr(((MEM_ADDR *)aPdOutputData), nEscAddrOutputData, prefix);
const UINT8 batchCount = (prefix > 48U) ? ((const UINT8 *)aPdOutputData)[48] : 0U;
if (batchCount > 0U && nPdOutputSize > prefix) {
HW_EscReadIsr(((MEM_ADDR *)((UINT8 *)aPdOutputData + prefix)),
(UINT16)(nEscAddrOutputData + prefix),
(UINT16)(nPdOutputSize - prefix));
}
APPL_OutputMapping((UINT16*) aPdOutputData);
EcatPdo_NoteOutputMapped();
}
else
{
if (nPdOutputSize >= 2U) {
HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2));
}
}
g_dbg_outCyc += ecatdbg_dwt() - dbgT0;
}
/////////////////////////////////////////////////////////////////////////////////////////
@@ -279,10 +273,14 @@ void HandleBusCycleCalculation(void)
void PDI_Isr(void)
{
/*ECAT_DBG(剖分:ISR 全程)*/
const UINT32 dbgT0 = ecatdbg_dwt();
if(bEscIntEnabled)
{
/* get the AL event register */
const UINT32 dbgTa = ecatdbg_dwt();
UINT16 ALEvent = HW_GetALEventRegister_Isr();
g_dbg_alCyc += ecatdbg_dwt() - dbgTa;
ALEvent = SWAPWORD(ALEvent);
if ( ALEvent & PROCESS_OUTPUT_EVENT )
@@ -330,29 +328,48 @@ void PDI_Isr(void)
/*ECATCHANGE_END(V5.11) ECAT4*/
/*
Call ECAT_Application() in SM Sync mode
Call ECAT_Application() in SM Sync mode (non-DC only).
DC:应用节拍已由 Sync0_Isr 拥有(bDcSyncActive),PDI 再跑一次
会把 Process/buildInput 算两遍 load_perm。
非 DC:仍只在 SM/SM2 类型时在 PDI 内跑应用。
**不要**「凡有 AL 过程事件就跑」——8kHz 时 PDI 每拍都带事件,
Process+InputMapping 热路径会被放大,单拍超过 ~120µs 墙,
实测 6kHz 即掉出(曾可 8kHz)。
负载随 PDO 上升由 ISR 包装 + free-run 1ms 节流路径体现,
见 sys/cpu_load.h;输出侧保持 12B 探针,避免每拍全量读。
*/
if (sSyncManOutPar.u16SyncType == SYNCTYPE_SM_SYNCHRON)
if (!bDcSyncActive
&& (sSyncManOutPar.u16SyncType == SYNCTYPE_SM_SYNCHRON
|| sSyncManOutPar.u16SyncType == SYNCTYPE_SM2_SYNCHRON
|| sSyncManInPar.u16SyncType == SYNCTYPE_SM_SYNCHRON
|| sSyncManInPar.u16SyncType == SYNCTYPE_SM2_SYNCHRON))
{
/* The Application is synchronized to process data Sync Manager event*/
const UINT32 dbgTb = ecatdbg_dwt();
ECAT_Application();
g_dbg_appCyc += ecatdbg_dwt() - dbgTb;
}
if ( bEcatInputUpdateRunning
if ( bEcatInputUpdateRunning
/*ECATCHANGE_START(V5.11) ESM7*/
&& ((sSyncManInPar.u16SyncType == SYNCTYPE_SM_SYNCHRON) || (sSyncManInPar.u16SyncType == SYNCTYPE_SM2_SYNCHRON))
/*ECATCHANGE_END(V5.11) ESM7*/
)
{
/* EtherCAT slave is at least in SAFE-OPERATIONAL, update inputs */
const UINT32 dbgTc = ecatdbg_dwt();
PDO_InputMapping();
g_dbg_inCyc += ecatdbg_dwt() - dbgTc;
}
/*
Check if cycle exceed
*/
/*if next SM event was triggered during runtime increment cycle exceed counter*/
const UINT32 dbgTd = ecatdbg_dwt();
ALEvent = HW_GetALEventRegister_Isr();
g_dbg_alCyc += ecatdbg_dwt() - dbgTd;
ALEvent = SWAPWORD(ALEvent);
if ( ALEvent & PROCESS_OUTPUT_EVENT )
@@ -365,6 +382,9 @@ void PDI_Isr(void)
HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2));
}
} //if(bEscIntEnabled)
/*ECAT_DBG(剖分:ISR 全程 收尾)*/
g_dbg_pdiCyc += ecatdbg_dwt() - dbgT0;
g_dbg_pdiCnt++;
}
void Sync0_Isr(void)
@@ -577,12 +597,30 @@ void MainLoop(void)
}
DISABLE_ESC_INT();
ECAT_Application();
if ( bEcatInputUpdateRunning )
/*ECAT_DBG(剖分:free-run 应用+输入映射块)*/
{
/* EtherCAT slave is at least in SAFE-OPERATIONAL, update inputs */
PDO_InputMapping();
const UINT32 dbgT1 = ecatdbg_dwt();
/*
* free-run 主循环:**严格 1ms 节流**再跑应用并计入 busy。
* 不要「AL 有过程事件就跑」——事件位可能连续置位,退化成
* 每圈 MainLoop 都 Process,热路径被抬爆(8kHz 掉到 6kHz)。
* SM 模式(bEscIntEnabled)本分支本就不进;PDO 高速走 PDI ISR,
* 负载由 EXTI1 挂钩计入。
*/
static UINT32 s_freeRunAppMs = 0xFFFFFFFFu;
const UINT32 nowMs = EcatDbg_GetTick1ms();
if (nowMs != s_freeRunAppMs) {
s_freeRunAppMs = nowMs;
CpuLoad_Enter();
ECAT_Application();
if ( bEcatInputUpdateRunning )
{
PDO_InputMapping();
}
CpuLoad_Leave();
}
g_dbg_freerunCyc += ecatdbg_dwt() - dbgT1;
}
ENABLE_ESC_INT();
}
+24 -47
View File
@@ -67,48 +67,6 @@ When the repeated mailbox service was sent (call of MBX_MailboxReadInd), psReadM
and psStoreMbx will be sent (in MBX_CopyToSendMailbox) and stored in psReadMbx, psStoreMbx will be set to 0.
\version 5.11
<br>Changes to version V5.10:<br>
V5.11 ECAT10: change PROTO handling to prevent compiler errors<br>
V5.11 ECAT7: add missing big endian swapping<br>
V5.11 HW1: "move hardware independent functions ""HW_DisableSyncManChannel()"", ""HW_EnableSyncManChannel()"", ""HW_GetSyncMan()"", ""HW_ResetALEventMask()"", ""HW_SetALEventMask()"" to ecatalv.c"<br>
V5.11 MBX2: "check in IP if enough dynamic memory is available to handle mailbox communication, if it is not the case the mbx error ""no memory"" is returned on any mbx request"<br>
V5.11 MBX3: set application triggered emergency and EoE data to pending if no mailbox queue is supported and another mailbox request is currently handled, Handle only one mailbox request at a time (in case that MAILBPX_QUEUE is disabled)<br>
V5.11 TEST4: add new mailbox test behaviour (the master mailbox cnt shall be incremented by 1 and the slave mailbox cnt is alternating)<br>
<br>Changes to version V5.01:<br>
V5.10 EOE3: Change local send frame pending indication variable to a global variable (it need to be resetted if the mailbox is stopped and a frame is pending)<br>
V5.10 ESC6: Update SyncManager register size (only for 8Bit ESC access)<br>
V5.10 MBX2: Allocate only memory for current configured mailbox size (max supported size was allocated before)<br>
<br>Changes to version V5.0:<br>
V5.01 ESC2: Add missed value swapping<br>
V5.01 MBX2: Return an error if length specified in mailbnox header is too large.<br>
<br>Changes to version V4.40:<br>
V5.0 ECAT3: Global dummy variables used for dummy ESC operations.<br>
V5.0 ESC1: ESC 32Bit Access added.<br>
V5.0 MBX2: Prevent double swapping (MBX repeat service) of MBX length field.<br>
V5.0 MBX4: Unlock SM buffer based on SM state.<br>
<br>Changes to version V4.11:<br>
V4.40 MBX8: Handle pending mailbox data even if mailbox queue is enabled<br>
V4.40 MBX7: Reset SyncManger 1 Repeat Acknowledge bit if local variable is reset<br>
V4.40 EMCY2: Send queued emergency messages<br>
V4.40 BOOT2: Support only FoE in Bootstrap state<br>
V4.40 MBX5: Prevent multiple free buffer<br>
V4.40 MBX6: change return value if no mailbox buffer is available<br>
<br>Changes to version V4.08:<br>
V4.11 MBX 1: free some dynamic memory to avoid memory leaks in the mailbox queue<br>
<br>Changes to version V4.07:<br>
V4.10 MBX 1: check Mailbox message buffer and print emergency messages<br>
during state change Init -> PreOP<br>
<br>Changes to version V4.07:<br>
V4.08 MBX 1: The mailbox queue was added (switch MAILBOX_QUEUE), if the<br>
mailbox should be handled in the ESC-Interrupt Service Routine<br>
the defines ENTER_MBX_CRITICAL and LEAVE_MBX_CRITICAL could be used<br>
to disable the interrupts or change the priority<br>
V4.08 AOE 1: AoE was added<br>
<br>Changes to version V4.06:<br>
V4.07 ECAT 1: The sources for SPI and MCI were merged (in ecat_def.h<br>
set the switch MCI_HW to 1 when using the MCI,<br>
set the switch SPI_HW to 1 when using the SPI
*/
@@ -133,6 +91,8 @@ V4.07 ECAT 1: The sources for SPI and MCI were merged (in ecat_def.h<br>
#include "ecatcoe.h"
#include <stdio.h> /* printf: SM 几何交叉校验 WARN(见 MBX_StartMailboxHandler) */
/*--------------------------------------------------------------------------------------
------
------ internal Types and Defines
@@ -282,24 +242,41 @@ void MBX_Init(void)
UINT16 MBX_StartMailboxHandler(void)
{
UINT16 result = 0;
/* get address of the receive mailbox sync manager (SM0) */
/* 邮箱几何构建期常量(ecat_def.h:MBX_SM0/1_START/SIZE)直达缓存,
* 不信任 GetSyncMan/CSR 单次间接读——CSR 读降级为交叉校验:
* 失配打 WARN(说明 ESC 寄存器被非常规改写,需查)但缓存仍取常量。
* 运行期内存踩踏也会在每次 INIT→PREOP 被常量覆写。
* 注意:ESC 寄存器经 PDI/CSR 读出即主机序(与 GetSyncMan 既有路径一致,勿加
* SWAPWORD;PRAM 帧数据才是线上大端,那边交换是对的)。 */
/*ECATCHANGE_START(V5.11) HW1*/
TSYNCMAN ESCMEM * pSyncMan = (TSYNCMAN ESCMEM *)GetSyncMan(MAILBOX_WRITE);
/*ECATCHANGE_END(V5.11) HW1*/
if (pSyncMan->PhysicalStartAddress != MBX_SM0_START || pSyncMan->Length != MBX_SM0_SIZE)
{
printf("[ECAT][MBX] WARN: SM0 reg 0x%04X/%u != build-time 0x%04X/%u -> keep build-time\r\n",
(unsigned)pSyncMan->PhysicalStartAddress, (unsigned)pSyncMan->Length,
(unsigned)MBX_SM0_START, (unsigned)MBX_SM0_SIZE);
}
/* store size of the receive mailbox */
u16ReceiveMbxSize = pSyncMan->Length;
u16ReceiveMbxSize = MBX_SM0_SIZE;
/* store the address of the receive mailbox */
u16EscAddrReceiveMbx = pSyncMan->PhysicalStartAddress;
u16EscAddrReceiveMbx = MBX_SM0_START;
/* get address of the send mailbox sync manager (SM1) */
/*ECATCHANGE_START(V5.11) HW1*/
pSyncMan =(TSYNCMAN ESCMEM *) GetSyncMan(MAILBOX_READ);
/*ECATCHANGE_END(V5.11) HW1*/
if (pSyncMan->PhysicalStartAddress != MBX_SM1_START || pSyncMan->Length != MBX_SM1_SIZE)
{
printf("[ECAT][MBX] WARN: SM1 reg 0x%04X/%u != build-time 0x%04X/%u -> keep build-time\r\n",
(unsigned)pSyncMan->PhysicalStartAddress, (unsigned)pSyncMan->Length,
(unsigned)MBX_SM1_START, (unsigned)MBX_SM1_SIZE);
}
/* store the size of the send mailbox */
u16SendMbxSize = pSyncMan->Length;
u16SendMbxSize = MBX_SM1_SIZE;
/* store the address of the send mailbox */
u16EscAddrSendMbx = pSyncMan->PhysicalStartAddress;
u16EscAddrSendMbx = MBX_SM1_START;
// HBu 02.05.06: it should be checked if there are overlaps in the sync manager areas
if ((u16EscAddrReceiveMbx + u16ReceiveMbxSize) > u16EscAddrSendMbx && (u16EscAddrReceiveMbx < (u16EscAddrSendMbx + u16SendMbxSize)))
+3 -2
View File
@@ -887,14 +887,15 @@ UINT8 OBJ_Read( UINT16 index, UINT8 subindex, UINT32 objSize, OBJCONST TOBJECT O
p = (CHAR **) pVarPtr;
pVarPtr = (UINT16 MBXMEM *)p[subindex-1];
if((((UINT16)pVarPtr) & 0x1) == 0x1)
if((((UINT32)pVarPtr) & 0x1) == 0x1)
{
/*enum is stored at an odd address*/
UINT16 cnt = 0;
//get last even WORD address
/*ECATCHANGE_START(V5.11) SDO3*/
pVarPtr = (UINT16 MBXMEM *)(((MEM_ADDR)pVarPtr)& ~(MEM_ADDR)0x1);
/*MEM_ADDR=UINT16 会把 32 位指针截成 16 位,必须用完整位宽清 bit0*/
pVarPtr = (UINT16 MBXMEM *)(((UINT32)pVarPtr) & ~(UINT32)0x1);
/*ECATCHANGE_END(V5.11) SDO3*/
for(cnt = 0; cnt < (size / 2);cnt++)
+4 -3
View File
@@ -1025,7 +1025,8 @@ UINT8 SDOS_SdoInfoInd( TSDOINFORMATION MBXMEM *pSdoInfoInd )
UINT16 n = OBJ_GetNoOfObjects(i);
/* copy the number of objects of the list type in the SDO Information response */
((UINT16 MBXMEM *) &pSdoInfoInd->CoeHeader)[(SIZEOF_SDOINFOLISTSTRUCT>>1)+i] = SWAPWORD(n);
/*(UINT32) 中转:对 packed 成员直接取址强转 UINT16 指针会报 -Waddress-of-packed-member(邮箱缓冲实际偶对齐)*/
((UINT16 MBXMEM *)(UINT32)&pSdoInfoInd->CoeHeader)[(SIZEOF_SDOINFOLISTSTRUCT>>1)+i] = SWAPWORD(n);
}
/* size of the mailbox service response */
@@ -1045,7 +1046,7 @@ UINT8 SDOS_SdoInfoInd( TSDOINFORMATION MBXMEM *pSdoInfoInd )
size = u16SendMbxSize - SIZEOF_SDOINFO - MBX_HEADER_SIZE;
}
/* initialize pData with the pointer where the fragment has to be copied */
pData = &((UINT16 MBXMEM *) &pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFO>>1];
pData = &((UINT16 MBXMEM *)(UINT32)&pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFO>>1];
/* initialize index with the next index to be sent */
index = nSdoInfoIndex;
/* decrement the number of fragments to be sent */
@@ -1063,7 +1064,7 @@ UINT8 SDOS_SdoInfoInd( TSDOINFORMATION MBXMEM *pSdoInfoInd )
size = u16SendMbxSize - SIZEOF_SDOINFOLISTSTRUCT - MBX_HEADER_SIZE;
}
/* initialize pData with the pointer where the fragment has to be copied */
pData = &((UINT16 MBXMEM *) &pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFOLISTSTRUCT>>1];
pData = &((UINT16 MBXMEM *)(UINT32)&pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFOLISTSTRUCT>>1];
/*Check if List need to be send in fragments*/
if( (n<<1) > size)
{
+473 -70
View File
@@ -7,6 +7,7 @@
#include "device/canfd.h"
#include "sys/config.h"
#include "sys/critical.h"
#include "sys/cpu_load.h"
#include "sys/systick.h"
#include "sys/task_manager.h"
#include <cstring>
@@ -18,6 +19,125 @@ static inline void spinDelay(uint32_t loops) {
for (volatile uint32_t d = 0; d < loops; ++d) { }
}
/** @brief DWT CYCCNT 忙等微秒(ISR 安全)。
* @note setTxSilent 退 Silent 时要等收发器 tMODE=20µs;本函数可能在 SPI CS
* EXTI ISR 内被 0xC8 handler 调用,而 SysTickTimer::delayMs 依赖 SysTick
* 中断计数,在该上下文会被冻死(同 esc_lan9252.cpp 的 pmBusyWaitUs)。
* **不得复位 CYCCNT**:cpu_load / ethercat_diff_stats / SSC 软件定时器都拿它
* 当时间基,清零会让它们的窗口计算瞬时报错。只在未使能时补使能位。 */
static inline void canBusyWaitUs(uint32_t us)
{
if ((DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk) == 0U) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
const uint32_t start = DWT->CYCCNT;
const uint32_t cycles = us * (SystemCoreClock / 1000000U);
/* 有界自旋:DWT 因故没在跑时不能变成无限等(最多多转 ~1ms 的圈数) */
const uint32_t spin_cap = cycles + (SystemCoreClock / 1000U);
for (uint32_t i = 0U; i < spin_cap; ++i) {
if ((DWT->CYCCNT - start) >= cycles) break;
}
}
/* DLENC>8 的长度映射,与厂商库 g_can_fdlength_table 一致 */
constexpr uint8_t kFdLenTable[7] = { 12U, 16U, 20U, 24U, 32U, 48U, 64U };
/** 释放 FIFO0 邮箱并等待 RFD0 由硬件自动清 0(手册 26.3.4 第 3 步)。
*
* 两处与厂商库 can_message_receive() 的差别,都是它漏掉的:
* 1) 用**直接赋值**而不是 `|=`。CAN_RFIFO0 里 RFF0(bit3)/RFO0(bit4) 是 rc_w1
* (写 1 清除),读-改-写会把这期间硬件刚置起来的"FIFO 满/溢出"标志顺手
* 清掉 —— 溢出证据就是这么丢的。写 0 对 rc_w1 位无副作用。
* 2) 置位后**等它自清**。手册原文:"将 RFD 置 1 释放邮箱,并且等待其由硬件
* 自动清 0"。不等就接着读下一条,会撞上 dequeue 尚未完成的邮箱窗口。
* @return true=已确认释放完成;false=超时(异常,计数后照常继续,不死等)。 */
static inline bool releaseFifo0(uint32_t canPeriph)
{
CAN_RFIFO0(canPeriph) = CAN_RFIFO0_RFD0;
for (uint32_t n = 0U; n < 2000U; ++n) {
if ((CAN_RFIFO0(canPeriph) & CAN_RFIFO0_RFD0) == 0U) {
/* 节点指纹实验表明拼接是 FIFO 缓冲指针与帧边界失同步
* (STATUS 帧头 24B = 完整 CONFIG,错位恰为 CONFIG 帧长 6 字),
* 不是延迟能挡的;结构矛盾的帧由 handleRxInterrupt 的内容校验计数
* (只计不丢 —— 管道必须诚实;源头修复见 CONFIG_IRQ_PREEMPT_CAN_RX)。 */
return true;
}
}
return false;
}
/** 取一帧 FIFO0 报文,并校验取数期间硬件没有换帧。
*
* 为什么自己实现(不用厂商库 can_message_receive):一条 CANFD 报文要对同一个
* 地址 CAN_RFIFOMDATA0 连读 dlen/4 次才取完(48B = 12 次),加上头部两次,一条
* 帧就是十几次有先后依赖的总线访问,整个过程**不是原子的**。若这期间硬件把下
* 一帧搬进邮箱窗口,就会读出"前几个字属于上一帧、后几个字属于这一帧"的拼接
* 体,而 id/dlc 取自最开始那次读 —— 表现为 STATUS 帧的 ID 配上 CONFIG 帧
* 的前 12~24 字节,且混合边界 100% 落在 4 字节边界上。
*
* 判据用 MP 寄存器:TS[31:16] 是硬件接收时间戳、DLENC[3:0] 是长度,同一帧内两
* 者恒定。取数前后各读一次,只要有一个变了就说明窗口内容已不是原来那帧,整帧
* 作废(计入 rx_torn_count)—— 宁可丢一帧,也不把拼接数据递上去。
*
* @return true=取到一帧完整报文;false=本帧判为撕裂、应丢弃(邮箱仍需释放)。 */
static bool receiveFrameChecked(uint32_t canPeriph, can_receive_message_struct *msg)
{
/* 整段取数放进 BASEPRI 临界区:屏蔽 preempt>=1,把 EtherCAT 的 ESC(1)/SYNC1(1)
* 挡在外面。取数是十几次有先后依赖的寄存器读,被抢占就会读出跨帧拼接体。
* 本 ISR 自身是 preempt=0(CONFIG_IRQ_PREEMPT_CAN_RX),与 SYNC0
* 同级互不抢占,取数真正原子;MI/MP 回读校验保留,作为对硬件自发换帧
* (dequeue 异步完成等)的兜底观测 —— rx_torn 计数即"该病理是否还在发生"。 */
const uint32_t basepri = RT_CRITICAL_SAVE();
const uint32_t mi0 = CAN_RFIFOMI(canPeriph, CAN_FIFO0);
const uint32_t mp0 = CAN_RFIFOMP(canPeriph, CAN_FIFO0);
msg->rx_ff = static_cast<uint8_t>(CAN_RFIFOMI_FF & mi0);
if (CAN_FF_STANDARD == msg->rx_ff) {
msg->rx_sfid = static_cast<uint32_t>(GET_RFIFOMI_SFID(mi0));
} else {
msg->rx_efid = static_cast<uint32_t>(GET_RFIFOMI_EFID(mi0));
}
msg->rx_ft = static_cast<uint8_t>(CAN_RFIFOMI_FT & mi0);
msg->rx_fi = static_cast<uint8_t>(GET_RFIFOMP_FI(mp0));
msg->fd_flag = static_cast<uint8_t>((CAN_RFIFOMP_FDF & mp0) >> 7);
msg->fd_brs = static_cast<uint8_t>((mp0 & CAN_RFIFOMP_BRS) >> 5);
msg->fd_esi = static_cast<uint8_t>((mp0 & CAN_RFIFOMP_ESI) >> 4);
const uint8_t dlenc = static_cast<uint8_t>(GET_RFIFOMP_DLENC(mp0));
const uint8_t dlen = (dlenc <= 8U) ? dlenc
: ((dlenc <= 15U) ? kFdLenTable[dlenc - 9U] : 64U);
msg->rx_dlen = dlen;
/* 按字取数:与厂商库同样是对同一地址重复读(硬件读指针自动前进)。
* 5~7 字节要多读一个字才取全,与厂商库口径一致。 */
uint32_t words = static_cast<uint32_t>(dlen) / 4U;
if ((1U == words) && (4U != dlen)) {
words++;
}
if (0U == words) {
words = 1U; /* dlen<4 也要读走一个字 */
}
for (uint32_t i = 0U; i < words; ++i) {
const uint32_t w = CAN_RFIFOMDATA0(canPeriph, CAN_FIFO0);
std::memcpy(&msg->rx_data[i * 4U], &w, sizeof(w));
}
/* 取数后回读 MI+MP:任一变化 ⇒ 邮箱窗口在取数期间被换帧,这一帧是拼接体。
* 必须连 MI(完整 ID)一起比 —— 只比 MP 的 TS/DLENC 会漏:TS 是 16 位计数器,
* 相邻两帧可能同值,DLENC 也常常相同(都是 48),于是"ID 报 STATUS、数据却是
* CONFIG"这种错配能从校验里溜过去。ID 变了就一定是
* 换了帧,这条判据没有巧合空间。 */
const uint32_t keep = CAN_RFIFOMP_TS | CAN_RFIFOMP_DLENC;
const uint32_t mp1 = CAN_RFIFOMP(canPeriph, CAN_FIFO0);
const uint32_t mi1 = CAN_RFIFOMI(canPeriph, CAN_FIFO0);
const bool intact = ((mp1 & keep) == (mp0 & keep)) && (mi1 == mi0);
RT_CRITICAL_RESTORE(basepri);
return intact;
}
struct CanChannelConfig {
uint32_t canPeriph;
rcu_periph_enum canRcu;
@@ -153,6 +273,49 @@ static void hardResetCan(uint32_t canPeriph, rcu_periph_enum canRcu)
}
}
/**
* @brief CAN TX 并发保护 RAII 守卫。
*
* 屏蔽 EXTI10_15_IRQn(SPI CS Assert/Release, Preempt 0)与该通道的 txIrq(Preempt 2),
* 防止 EtherCAT PDI_Isr(Preempt 1)与 SPI CS EXTI ISR(Preempt 0)在操作 s_txQueues
* 或硬件发送邮箱寄存器时产生并发撕裂与重入竞争。
* 具备重入安全性:若某中断当前未使能(例如已被外层守卫屏蔽),退出时不会误使能。
*/
class CanTxConcurrencyGuard {
public:
explicit CanTxConcurrencyGuard(uint8_t channelIndex)
: m_txIrq(kChannels[channelIndex].txIrq)
{
m_exti15_enabled = (NVIC_GetEnableIRQ(EXTI10_15_IRQn) != 0U);
m_tx_enabled = (NVIC_GetEnableIRQ(m_txIrq) != 0U);
if (m_exti15_enabled) {
NVIC_DisableIRQ(EXTI10_15_IRQn);
}
if (m_tx_enabled) {
NVIC_DisableIRQ(m_txIrq);
}
__DSB();
__ISB();
}
~CanTxConcurrencyGuard() {
if (m_tx_enabled) {
NVIC_EnableIRQ(m_txIrq);
}
if (m_exti15_enabled) {
NVIC_EnableIRQ(EXTI10_15_IRQn);
}
__DSB();
__ISB();
}
private:
IRQn_Type m_txIrq;
bool m_exti15_enabled;
bool m_tx_enabled;
};
} // namespace
CanFd::Diag CanFd::s_diag{};
@@ -165,6 +328,8 @@ CanFd::ChannelFeedback CanFd::s_feedback[3]{};
uint32_t CanFd::s_feedbackSeq = 0U;
CanFd::IdSnapshotTable CanFd::s_idSnapshot[3]{};
bool CanFd::s_channelHwOk[3]{};
bool CanFd::s_brsSuppress[3]{};
volatile uint8_t CanFd::s_txSilent[3]{};
/* TX 错误事件标志(ARD=1 单发丢帧通知):TX 完成 ISR 置位,主循环
* consumeTxError() 读清;单写单读,无锁。 */
@@ -273,7 +438,14 @@ bool CanFd::initNominalTiming(uint32_t canPeriph)
can_parameter.auto_bus_off_recovery = ENABLE;
can_parameter.auto_wake_up = DISABLE;
can_parameter.auto_retrans = ENABLE; /* 默认自动重发兜底偶发 ACK 丢失;OTA 停等协议期间由 setChannelAutoRetrans(ch,false) 临时关闭,避免重发打穿电机单缓冲 RX */
can_parameter.rec_fifo_overwrite = ENABLE;
/* rec_fifo_overwrite = DISABLE:FIFO 满时丢新帧而非覆盖最旧邮箱。
* 覆盖最旧会伤及 CPU 正在读取的输出窗口。buildInput 在 PDI_Isr(preempt1)
* 占 ~357µs 会压住 CAN ISR(preempt2),发指令时电机 CONFIG 翻倍到 2kHz/节点、
* 总线 ~8kHz,阻塞窗口内到达 ~3 帧恰好打满 FIFO 深度 → 覆盖进行到一半被读走
* = 跨节点/跨类型拼接帧。DISABLE 后溢出丢新帧并置 RFO0
* (rx_fifo_overfull_count 已在统计):STATUS/CONFIG 均周期量,丢一拍下拍
* 即补;覆盖递上来的才是网页乱码。与本文件软件队列"满丢新不覆旧"同则。 */
can_parameter.rec_fifo_overwrite = DISABLE;
can_parameter.trans_fifo_order = ENABLE;
can_parameter.working_mode = CAN_NORMAL_MODE;
can_parameter.resync_jump_width = CONFIG_CAN_NOMINAL_SJW;
@@ -288,7 +460,7 @@ bool CanFd::initNominalTiming(uint32_t canPeriph)
* @param[in] canPeriph 目标 CAN 外设实例。
* @param[out] 无。
* @retval 无返回值。
* @note 时钟源为 APB1=90MHz;按 CONFIG_CAN_DATA_* 宏配置,目标 2.0 Mbps。
* @note 时钟源为 APB1=90MHz;按 CONFIG_CAN_DATA_* 宏配置,目标 5.0 Mbps。
* 实际数据段波特率 = 90MHz / (CONFIG_CAN_DATA_PRESCALER × (1+BS1+BS2))。
* BRS 受 CONFIG_CAN_FD_BRS_ENABLE 控制。
*/
@@ -302,6 +474,9 @@ bool CanFd::initFdMode(uint32_t canPeriph)
can_fd_parameter.fd_frame = ENABLE;
can_fd_parameter.excp_event_detect = ENABLE;
can_fd_parameter.delay_compensation = ENABLE;
/* TDCF/TDCO 不随数据波特率等比缩放:TDCMOD=CALC_AND_OFFSET 下硬件已自动测得
* 回路延时(TDCV),这里只是微调,经验取 TDCO ≈ 数据段 BS1 的 tq 数(4TQ→4)。
* 放到 12 会让本节点回采时错开约 6 个数据位,自己发出的 FD+BRS 帧被判成错误帧。 */
can_fd_tdc_parameter.tdc_filter = 0x04U;
can_fd_tdc_parameter.tdc_mode = CAN_TDCMOD_CALC_AND_OFFSET;
can_fd_tdc_parameter.tdc_offset = 0x04U;
@@ -339,7 +514,8 @@ void CanFd::initChannelHardware(uint8_t i)
}
s_channelHwOk[i] = false;
rcu_periph_clock_enable(ch.canRcu);
gpio_bit_reset(ch.slpPort, ch.slpPin); /* STB=0 -> Normal */
gpio_bit_reset(ch.slpPort, ch.slpPin); /* S=0 → Normal(静默不跨重启,上电一律放开发送) */
s_txSilent[i] = 0U;
const uint8_t rxLevelBefore = (gpio_input_bit_get(ch.rxPort, ch.rxPin) != RESET) ? 1U : 0U;
const uint8_t stbLevel = (gpio_output_bit_get(ch.slpPort, ch.slpPin) != RESET) ? 1U : 0U;
@@ -421,11 +597,11 @@ void CanFd::initChannelHardware(uint8_t i)
CAN_TSTAT(ch.canPeriph) = CAN_TSTAT_MTF0 | CAN_TSTAT_MTF1 | CAN_TSTAT_MTF2;
can_interrupt_enable(ch.canPeriph, CAN_INT_TME | CAN_INT_RFNE0 | CAN_INT_RFF0 | CAN_INT_RFO0);
nvic_irq_enable(ch.txIrq, CONFIG_IRQ_PREEMPT_CAN, static_cast<uint8_t>(i));
nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN, static_cast<uint8_t>(i));
/* RX 提到 preempt=0 —— 见 sys/config.h CONFIG_IRQ_PREEMPT_CAN_RX 注释。 */
nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN_RX, static_cast<uint8_t>(i));
s_rxQueues[i].head = 0U;
s_rxQueues[i].tail = 0U;
s_rxQueues[i].count = 0U;
s_txQueues[i].head = 0U;
s_txQueues[i].tail = 0U;
s_txQueues[i].count = 0U;
@@ -788,7 +964,7 @@ void CanFd::init()
/**
* @brief L0 轻量自检:无总线发帧,仅校验使能通道控制器自身状态(GD32 侧故障)。
* @note 1.5.35:不再检查 BUS_OFF/TEC/REC——未接从站的通道 RX 浮空噪声会累积 REC,
* @note 不检查 BUS_OFF/TEC/REC——未接从站的通道 RX 浮空噪声会累积 REC,
* 「没接设备」不等于错误;总线侧健康由 0xC1 反馈帧运行期上报。
*/
bool CanFd::selfTestL0(uint8_t *fault_code, uint8_t *fault_channel)
@@ -866,23 +1042,20 @@ CanFd::Mode CanFd::mode()
* @param[in] 无。
* @param[out] 无。
* @retval bool true 表示至少一条接收队列非空,false 表示当前三路均无待消费报文。
* @note 无锁检查 head != tail,免关中断。
*/
bool CanFd::hasRxFrame()
{
bool found = false;
uint32_t save = RT_CRITICAL_SAVE();
for(size_t i = 0; i < kChannelCount; ++i) {
if(s_rxQueues[i].count != 0U) {
found = true;
break;
if(s_rxQueues[i].head != s_rxQueues[i].tail) {
return true;
}
}
RT_CRITICAL_RESTORE(save);
return found;
return false;
}
/**
* @brief 轮转读取并消费一帧接收报文。
* @brief 轮转读取并消费一帧接收报文(Lock-Free SPSC 消费者端)。
* @param[in] 无。
* @param[out] id 输出报文标识符;允许为空。
* @param[out] data 输出报文数据缓存;允许为空。
@@ -890,42 +1063,37 @@ bool CanFd::hasRxFrame()
* @param[out] channel 输出报文来源通道号;允许为空。
* @retval bool true 表示成功消费一帧报文,false 表示当前三路接收队列均为空。
* @note 消费顺序采用从 `s_nextConsumeChannel` 开始的轮转扫描,避免长期偏向固定通道。
* 基于 SPSC 无锁环形缓冲区,消费端读取 payload 并执行 __DMB() 后推进 head,
* 无需关中断或依赖 BASEPRI,彻底消除与 Preempt 0 RX ISR 的并发撕裂。
*/
bool CanFd::consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel)
{
RxFrame localFrame{};
bool found = false;
uint32_t save = RT_CRITICAL_SAVE();
for(size_t n = 0; n < kChannelCount; ++n) {
const uint8_t idx = static_cast<uint8_t>((s_nextConsumeChannel + n) % kChannelCount);
auto &queue = s_rxQueues[idx];
if(queue.count == 0U) {
const uint8_t head = queue.head;
const uint8_t tail = queue.tail;
if(head == tail) {
continue;
}
localFrame = queue.frames[queue.head];
queue.head = static_cast<uint8_t>((queue.head + 1U) % kRxQueueDepth);
queue.count--;
s_nextConsumeChannel = static_cast<uint8_t>((idx + 1U) % kChannelCount);
found = true;
break;
}
RT_CRITICAL_RESTORE(save);
if(!found) {
return false;
}
if(channel) *channel = localFrame.channel;
if(id) *id = localFrame.id;
if(length) *length = localFrame.length;
if(data) {
for(uint8_t i = 0; i < localFrame.length; ++i) {
data[i] = localFrame.data[i];
const auto &src = queue.frames[head];
if(channel) *channel = src.channel;
if(id) *id = src.id;
if(length) *length = src.length;
if(data && src.length > 0U) {
for(uint8_t i = 0; i < src.length; ++i) {
data[i] = src.data[i];
}
}
__DMB();
queue.head = static_cast<uint8_t>((head + 1U) & kRxQueueMask);
s_nextConsumeChannel = static_cast<uint8_t>((idx + 1U) % kChannelCount);
return true;
}
return true;
return false;
}
/**
@@ -951,6 +1119,15 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
if (dlc > 0U && data == nullptr) {
return CanTxResult::kErrInvalid;
}
/* 收发器 Silent(只听不发):本端主动配置的发送禁能,不是总线状态。
* 与下面「BUS_OFF/TEC 不做软件拦截」的约定不冲突——那两条是粘性总线侧标志会误判,
* 这条是 s_txSilent 单一真源,且驱动器已关断:硬发只会让控制器白涨 TEC。
* 拒绝路径不碰任何计数/诊断,静默期间的拒发不是错误。 */
if (s_txSilent[channelIndex] != 0U) {
return CanTxResult::kErrSilent;
}
CanTxConcurrencyGuard guard(channelIndex);
/* 诊断:捕获任何通过 transmitFrame 发 0x100 DLC=16 的帧(排查幽灵帧来源) */
if (ext_id == 0x00000100U && dlc == 16U) {
@@ -984,9 +1161,13 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
tx_msg.tx_ft = CAN_FT_DATA;
tx_msg.tx_dlen = dlc;
#ifdef GD32E508
tx_msg.fd_flag = (dlc > 8U) ? ENABLE : DISABLE;
/* 只要通道硬件已启用 CAN-FD(3路均由 initFdMode 开启),统一发 FD 帧;
* 避免 DLC<=8 时回退经典 CAN 导致对端电机 DMA / 报文解析异常。 */
tx_msg.fd_flag = ENABLE;
#if (CONFIG_CAN_FD_BRS_ENABLE != 0)
tx_msg.fd_brs = (dlc > 8U) ? ENABLE : DISABLE;
/* 电机 BOOT 收不了 BRS 数据段,OTA 期间 relay 对该通道压制(setChannelBrsSuppress),
* 压制中回退仲裁段速率(FD-no-BRS,OTA 长期验证过的组合)。日常发送统一启用 BRS 高速段。 */
tx_msg.fd_brs = (!s_brsSuppress[channelIndex]) ? ENABLE : DISABLE;
#else
tx_msg.fd_brs = DISABLE;
#endif
@@ -1060,9 +1241,19 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id,
if (dlc > 0U && data == nullptr) {
return CanTxResult::kErrInvalid;
}
/* 静默通道在入队口就拒掉:否则帧会进软件队列,等 TX 完成中断 drainTxQueueIsr
* 调 transmitFrame 时才被拒,那一轮会把 head/count 推进并静默丢帧,
* 表面上"已受理"实则必丢。 */
if (s_txSilent[channelIndex] != 0U) {
return CanTxResult::kErrSilent;
}
TxQueue &q = s_txQueues[channelIndex];
/* 关中断并发保护:屏蔽 EXTI10_15_IRQn(SPI burst, Preempt 0)与 txIrq(Preempt 2),
* 解决 PDI_Isr(Preempt 1)与 SPI CS release(Preempt 0)并发访问 s_txQueues 的竞态。 */
CanTxConcurrencyGuard guard(channelIndex);
/* 快速路径:队列空 → 直通硬件邮箱。装得下立即返回,不用等待 TX 完成中断。
* ★ allow_abort=false:邮箱满时禁止 abort 旧帧顶替(那会把同通道已装载的
* 旧帧静默丢弃,>3 帧同通道突发只剩最后 3 帧上线),一律落入软件队列,
@@ -1075,16 +1266,9 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id,
/* 邮箱满:落入下面的入队路径 */
}
/* 关中段:EXTI15(SPI burst) 与 CANx TX ISR 并发,保护 head/tail/count。
* 用 CMSIS NVIC 接口(SPL 的 nvic_irq_enable 是 3 参版)。
* 关的是该通道自身的 txIrq,避免关全局影响其他通道的实时性。 */
const IRQn_Type irqn = kChannels[channelIndex].txIrq;
NVIC_DisableIRQ(irqn);
if (q.count >= kTxQueueDepth) {
/* 队列也满了:彻底装不下。此时不能再返回 kOk,
* 否则前端会误判成功;只能丢弃并返回 kErrMailbox。 */
NVIC_EnableIRQ(irqn);
s_diag.error_count++;
s_channelStats[channelIndex].error_count++;
return CanTxResult::kErrMailbox;
@@ -1099,14 +1283,13 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id,
q.tail = static_cast<uint8_t>((q.tail + 1U) % kTxQueueDepth);
q.count++;
NVIC_EnableIRQ(irqn);
return CanTxResult::kOk;
}
/**
* @brief CAN TX 完成中断末尾调用:用硬件邮箱空闲出的位置,把队列里的帧装回去。
* @note 只在 ISR 内调用;关中断保护已在 transmitFrame 返回值判定外层完成,
* 本函数本身不关中(邮箱空位是"单调减少再单调增加",多读无害)。
* @note 由 CAN TX 完成 ISR 调用;使用 CanTxConcurrencyGuard 屏蔽 EXTI10_15 与 txIrq,
* 防止出队期间被高优先级的 SPI CS release 抢占导致 head/count 撕裂。
*/
void CanFd::drainTxQueueIsr(uint8_t channelIndex)
{
@@ -1114,6 +1297,8 @@ void CanFd::drainTxQueueIsr(uint8_t channelIndex)
return;
}
TxQueue &q = s_txQueues[channelIndex];
CanTxConcurrencyGuard guard(channelIndex);
while (q.count > 0U) {
TxQueuedFrame &f = q.frames[q.head];
/* 复用 transmitFrame 的装载逻辑;allow_abort=false:这里邮箱刚空出来,
@@ -1143,6 +1328,16 @@ uint32_t CanFd::txQueueBacklog(uint8_t channelIndex)
return s_txQueues[channelIndex].count;
}
uint32_t CanFd::rxQueueCount(uint8_t channelIndex)
{
if (channelIndex >= kChannelCount) {
return 0U;
}
const uint8_t head = s_rxQueues[channelIndex].head;
const uint8_t tail = s_rxQueues[channelIndex].tail;
return static_cast<uint32_t>((tail - head) & kRxQueueMask);
}
void CanFd::abortPendingTx(uint8_t channelIndex)
{
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
@@ -1181,6 +1376,60 @@ void CanFd::setChannelAutoRetrans(uint8_t channelIndex, bool enable)
(CAN_CTL(canPeriph) & CAN_CTL_ARD) ? 1U : 0U);
}
void CanFd::setChannelBrsSuppress(uint8_t channelIndex, bool suppress)
{
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
return;
}
/* 仅置运行时标志,打点在 transmitFrame:BRS 是逐帧属性(fd_brs 位),无需动寄存器。
* 写方(relay enterState 钩子)与读方(主循环/CS 中断的 transmitFrame)间 bool 单写单读,原子。 */
s_brsSuppress[channelIndex] = suppress;
CAN_LOGI("[CAN] CH%u set brs_suppress=%u\r\n", (unsigned)channelIndex, (unsigned)suppress);
}
bool CanFd::setTxSilent(uint8_t channelIndex, bool silent)
{
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
return false;
}
const auto &ch = kChannels[channelIndex];
if (silent) {
/* 先立旗再动脚:并发路径(主循环/其它 ISR)调 transmitFrame 立刻被拒,
* 不会在关驱动器的当口又塞一帧进邮箱。 */
s_txSilent[channelIndex] = 1U;
/* 驱动器即将关断:残留 PENDING 邮箱发不出也等不到 ACK,只会白涨 TEC,
* 进静默前先把它们停掉。 */
abortPendingTx(channelIndex);
gpio_bit_set(ch.slpPort, ch.slpPin); /* S=1 → TCAN1057 Silent(驱动关断,接收照常) */
} else {
gpio_bit_reset(ch.slpPort, ch.slpPin); /* S=0 → Normal */
/* 收发器模式建立时间 tMODE=20µs(手册 6.9):等够再放开发送门,
* 否则紧随其后的第一帧会被驱动器忽略。 */
canBusyWaitUs(20U);
s_txSilent[channelIndex] = 0U;
}
const uint8_t pinHigh = (gpio_output_bit_get(ch.slpPort, ch.slpPin) != RESET) ? 1U : 0U;
CAN_LOGI("[CAN] CH%u tx_silent=%u (S pin=%u)\r\n", (unsigned)channelIndex,
(unsigned)silent, (unsigned)pinHigh);
/* 回读确认脚电平与意图一致;不一致就把标志回滚成原状态再报错,
* 否则 isTxSilent()(发送门控 + 0xC1 bit5)会与物理脚电平各说一套。 */
if (pinHigh != (silent ? 1U : 0U)) {
s_txSilent[channelIndex] = silent ? 0U : 1U;
return false;
}
return true;
}
bool CanFd::isTxSilent(uint8_t channelIndex)
{
if (channelIndex >= kChannelCount) {
return false;
}
return s_txSilent[channelIndex] != 0U;
}
uint32_t CanFd::readChannelErrorReg(uint8_t channelIndex)
{
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
@@ -1291,7 +1540,104 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
can_receive_message_struct rx_message{};
can_struct_para_init(CAN_RX_MESSAGE_STRUCT, &rx_message);
can_message_receive(ch.canPeriph, CAN_FIFO0, &rx_message);
/* 自己取帧并校验(见 receiveFrameChecked):厂商库不校验也不等 RFD 自清,
* 会把"半条上一帧 + 半条本帧"的拼接体当正常帧交上来。
*
* **校验失败只丢不重取**。RFIFOMDATA0 是"重复读同一地址、硬件读
* 指针自动前进",取数失败时读指针已随取数前进若干字,
* 重取会从错位指针开读 —— MI/MP 回读比对的是"窗口身份",
* 管不住"指针相对窗口起点的错位",于是校验通过、内容却是"上一帧的尾巴
* 拼下一帧的开头"。
* 丢帧无害:STATUS/CONFIG 都是 1kHz 周期量,下拍即补;递上去才会变乱码。 */
const bool intact = receiveFrameChecked(ch.canPeriph, &rx_message);
if (!releaseFifo0(ch.canPeriph)) {
s_diag.rx_release_timeout_count++;
}
if (!intact) {
/* 校验判为拼接:整帧作废,不计 rx_count、不入队、不转发。 */
s_diag.rx_torn_count++;
continue;
}
/* 本帧的 ID 用局部量,不要读 s_diag.last_rx_id ——三路通道共用那个全局,
* 嵌套/交错时会串出"A 通道的 id 配 B 通道的数据"。 */
const uint32_t rxId = (rx_message.rx_ff == CAN_FF_STANDARD)
? rx_message.rx_sfid : rx_message.rx_efid;
/* ── 内容级拼接帧拦截 ──
* MI/MP 回读校验管不住 FIFO 缓冲指针与帧边界的失同步
* (指针病理,软件无法从取数时序上根除)。
* 在入队前做结构自洽校验:拼接帧的 (id, data) 必矛盾,而真帧必自洽。
* STATUS(0x000N0100, dlc48):CONFIG 头灌入必然把 MCU/Motor 温度推出
* [-40,125]℃ 物理量程(CONFIG B0-1 = 03+限压低字节 → 123~250℃ 或
* 负值),母线电压同步失序;
* CONFIG(0x000N0101, dlc24):DBC v0.74 规定 B15-23 未使用恒 0,
* STATUS 体灌入几乎必非 0;模式字节限 0..7(电机模式表最大 7)。
* 命中即计数(不丢帧——管道必须诚实,计数仅做"源头是否
* 还在产拼接帧"的测量):源头修复生效后本计数应恒 0,非 0 即失效告警。
* 计数经段5@494 上报。
* 残余风险:限压字节恰使温度落回量程内的理论组合
* (24V 台架实测限压 ≥15V → 对应"温度"≥153℃,仍在量程外),可接受。 */
{
const uint16_t idLo = (uint16_t)(rxId & 0xFFFFU);
bool reject = false;
/* 上一帧已接受的温度/电压(按节点):拼接帧的 CONFIG 字节可能恰好落进
* 静态量程,但绝不可能在 1ms 帧间隔里物理地跳过去 —— 斜率判据
* 是静态量程补不上的洞。阈值留 3 个数量级余量:温度 20℃/帧(实际
* ≤0.005℃),电压 5V/帧(实际 ≤0.05V)。 */
static int16_t s_lastMcu[16];
static int16_t s_lastMot[16];
static uint16_t s_lastPwr[16];
static uint8_t s_lastOk[16];
if (idLo == 0x0100U && rx_message.rx_dlen == 48U) {
const int16_t mcu = (int16_t)((uint16_t)rx_message.rx_data[0] |
((uint16_t)rx_message.rx_data[1] << 8));
const int16_t mot = (int16_t)((uint16_t)rx_message.rx_data[2] |
((uint16_t)rx_message.rx_data[3] << 8));
const uint16_t pwr = (uint16_t)rx_message.rx_data[11] |
((uint16_t)rx_message.rx_data[12] << 8);
if (mcu < -4000 || mcu > 12500 || mot < -4000 || mot > 12500 ||
pwr < 8000 || pwr > 60000) {
reject = true;
}
const uint8_t nd = (uint8_t)((rxId >> 16) & 0x0FU);
if (!reject && nd >= 1U && nd < 16U && (s_lastOk[nd] & 1U)) {
int16_t dmcu = (int16_t)(mcu - s_lastMcu[nd]);
int16_t dmot = (int16_t)(mot - s_lastMot[nd]);
if (dmcu < 0) { dmcu = (int16_t)-dmcu; }
if (dmot < 0) { dmot = (int16_t)-dmot; }
/* 只比温度斜率:拼接帧的 CONFIG 头必然把 mcu/mot 推出 20℃/帧
* (实际温度 ≤0.005℃/帧,4 个数量级余量)。
* 电压不比斜率 —— 正弦换向/堵转时母线真实跌落可超 5V/帧,
* 误杀会让显示冻结;拼接的电压特征(≈48V)已由量程+温度斜率兜住。 */
if (dmcu > 2000 || dmot > 2000) {
reject = true;
}
}
if (!reject && nd >= 1U && nd < 16U) {
s_lastMcu[nd] = mcu;
s_lastMot[nd] = mot;
s_lastPwr[nd] = pwr;
s_lastOk[nd] = 1U;
}
} else if (idLo == 0x0101U && rx_message.rx_dlen == 24U) {
if (rx_message.rx_data[0] > 7U) {
reject = true;
}
for (uint8_t i = 15U; !reject && i < 24U; ++i) {
if (rx_message.rx_data[i] != 0U) {
reject = true;
}
}
}
if (reject) {
/* 只计不丢 —— 数据管道必须诚实。
* 网页/ESP32/主站看到的就是源头产出的原始字节,
* 本计数退化为纯测量仪表:源头修好 = 恒 0;还在涨 = 源头问题仍在,
* 且网页如实显示着,不存在"看着干净其实坏了"。 */
s_diag.rx_content_reject_count++;
}
}
s_diag.rx_count++;
s_diag.last_rx_channel = channelIndex;
@@ -1299,14 +1645,14 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
s_diag.last_rx_fd_flag = rx_message.fd_flag;
s_diag.last_rx_brs = rx_message.fd_brs;
s_diag.last_rx_esi = rx_message.fd_esi;
s_diag.last_rx_id = (rx_message.rx_ff == CAN_FF_STANDARD) ? rx_message.rx_sfid : rx_message.rx_efid;
s_diag.last_rx_id = rxId;
/* CAN0 桥接模式:将刚收到的原帧透传到 CAN1 与 CAN2,保留 11/29 位、FD、BRS、ESI。 */
if (channelIndex == 0U && s_mode == Mode::kModeBridge0To12) {
for (uint8_t bridgeCh = 1U; bridgeCh <= 2U; ++bridgeCh) {
const uint32_t bridgePeriph = kChannels[bridgeCh].canPeriph;
const bool ok = bridgeTransmitRaw(bridgePeriph, rx_message);
if (ok) {
s_diag.last_tx_id = s_diag.last_rx_id;
s_diag.last_tx_id = rxId;
s_diag.last_tx_channel = bridgeCh;
s_diag.last_payload_length = rx_message.rx_dlen;
} else {
@@ -1319,7 +1665,7 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
/* 维护每通道独立计数与最近一帧快照,用于 1Hz 反馈给 ESP32。 */
auto &stats = s_channelStats[channelIndex];
stats.rx_count++;
stats.last_rx_id = s_diag.last_rx_id;
stats.last_rx_id = rxId;
stats.last_rx_dlc = rx_message.rx_dlen;
stats.last_rx_fd_flag = rx_message.fd_flag;
stats.last_rx_brs = rx_message.fd_brs;
@@ -1335,7 +1681,6 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
/* 维护每通道不重复 ID 快照表,用于 1Hz 分页拉取给 ESP32。 */
{
auto &table = s_idSnapshot[channelIndex];
const uint32_t rxId = s_diag.last_rx_id;
/* 查找已有条目 */
uint8_t found = CANFD_ID_SNAPSHOT_MAX;
for (uint8_t si = 0U; si < table.count; ++si) {
@@ -1378,22 +1723,26 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
}
auto &queue = s_rxQueues[channelIndex];
if(queue.count >= kRxQueueDepth) {
const uint8_t head = queue.head;
const uint8_t tail = queue.tail;
const uint8_t next_tail = static_cast<uint8_t>((tail + 1U) & kRxQueueMask);
if(next_tail == head) {
s_diag.rx_drop_count++;
stats.rx_drop_count++;
continue;
}
auto &frame = queue.frames[queue.tail];
auto &frame = queue.frames[tail];
frame.channel = channelIndex;
frame.id = s_diag.last_rx_id;
frame.id = rxId;
frame.length = rx_message.rx_dlen;
for(uint8_t i = 0; i < rx_message.rx_dlen; ++i) {
frame.data[i] = rx_message.rx_data[i];
}
queue.tail = static_cast<uint8_t>((queue.tail + 1U) % kRxQueueDepth);
queue.count++;
__DMB();
queue.tail = next_tail;
}
if(fifoFull) {
@@ -1455,6 +1804,15 @@ void CanFd::handleTxInterrupt(uint8_t channelIndex)
drainTxQueueIsr(channelIndex);
}
/* ── 空通道自动识别状态(2026-09-18)──
* 无对端的通道 RX 浮空 → 持续 STUFF、REC 冲 ~130 停在 ERR_PASSIVE,每 5s 刷一行
* [CAN-HW-ERR](config.h 老注释"CH1/CH2 未接设备,关闭避免持续 CAN 错误 + 占 SPI
* 带宽")。改为运行时自动判定:连续 CONFIG_CAN_QUIET_WINDOW 个窗口无有效帧增量
* 且有错误 → 静默上报;出现帧增量立刻恢复。只抑制上报,硬件保持 NORMAL 接收。 */
static uint8_t s_chanQuiet[3] = {0U, 0U, 0U}; /* 1=该通道已自动静默上报 */
static uint32_t s_rxCountPrev[3] = {0U, 0U, 0U}; /* 上一拍末尾的 rx_count */
static uint8_t s_quietMiss[3] = {0U, 0U, 0U}; /* 连续"无帧且有错"的窗口数 */
/**
* @brief 刷新三路 CAN 通道的 1Hz 反馈快照。
* @note 在临界区内拷贝实时计数与最近一帧信息到 s_feedback;保证 SPI 命令读到一致快照。
@@ -1484,6 +1842,37 @@ void CanFd::refreshFeedbackSnapshot()
}
s_feedbackSeq++;
/* 空通道自动识别:本拍无有效帧增量且有错误 → 记一次 miss,连续达到窗口长度才静默
* (加滞回,避免偶发丢拍/慢流量误判);一旦有帧增量立刻恢复上报。
* 时间基 = s_feedbackSeq(本函数 1Hz,见下面 %5 的 5s 打印节拍)。 */
for (size_t i = 0; i < kChannelCount; ++i) {
const uint32_t rxNow = s_channelStats[i].rx_count;
const uint32_t rxDelta = rxNow - s_rxCountPrev[i];
s_rxCountPrev[i] = rxNow;
const bool hasErr = (hw_tec[i] != 0U || hw_rec[i] != 0U
|| (hw_err[i] & (CAN_ERR_WERR | CAN_ERR_PERR | CAN_ERR_BOERR)) != 0U);
if (rxDelta > 0U) {
s_quietMiss[i] = 0U;
if (s_chanQuiet[i] != 0U) {
s_chanQuiet[i] = 0U;
CAN_LOGI("[CAN] CH%u 恢复上报:检测到有效帧 (rx=%lu)\r\n",
static_cast<unsigned>(i), static_cast<unsigned long>(rxNow));
}
} else if (hasErr) {
if (s_quietMiss[i] < 0xFFU) s_quietMiss[i]++;
if (s_chanQuiet[i] == 0U
&& s_feedbackSeq >= CONFIG_CAN_QUIET_AFTER_SEC
&& s_quietMiss[i] >= CONFIG_CAN_QUIET_WINDOW_SEC) {
s_chanQuiet[i] = 1U;
LOG_ERR("[CAN] CH%u 无有效帧且错误累积 → 自动静默上报(本通道无设备/无对端;"
"硬件保持接收,插上设备有帧即自动恢复;TEC=%lu REC=%lu)\r\n",
static_cast<unsigned>(i),
static_cast<unsigned long>(hw_tec[i]), static_cast<unsigned long>(hw_rec[i]));
}
} else {
s_quietMiss[i] = 0U;
}
}
/* 每 5 秒采样硬件错误态;正常心跳不打印(统计走 SPI 0xC1)。
* 仅当 TEC/REC 或告警位非零时输出 [CAN-HW-ERR];全量 seq 心跳见 CONFIG_UART_DIAG_LEVEL。 */
if (s_feedbackSeq % 5U == 0U) {
@@ -1499,6 +1888,7 @@ void CanFd::refreshFeedbackSnapshot()
&& (hw_err[i] & (CAN_ERR_WERR | CAN_ERR_PERR | CAN_ERR_BOERR)) == 0U) {
continue;
}
if (s_chanQuiet[i] != 0U) continue; /* 已自动静默的空通道:不再刷错误 */
LOG_ERR("[CAN-HW-ERR] CH%u ERR=0x%08lX ERRN=%lu(%s) TEC=%lu REC=%lu TSTAT=0x%08lX\r\n",
static_cast<unsigned>(i),
static_cast<unsigned long>(hw_err[i]),
@@ -1515,14 +1905,17 @@ void CanFd::refreshFeedbackSnapshot()
/* 写入硬件层错误状态供 SPI 反馈使用 */
s_feedback[i].stats.tec = static_cast<uint8_t>(hw_tec[i]);
s_feedback[i].stats.rec = static_cast<uint8_t>(hw_rec[i]);
/* 总线状态位图:bit0=init, bit1=sleep, bit2=error_passive, bit3=bus_off, bit4=error_warning
* init/sleep 来自 CAN_STAT,error_passive/bus_off/error_warning 来自 CAN_ERR */
/* 总线状态位图:bit0=init, bit1=控制器sleep, bit2=error_passive, bit3=bus_off,
* bit4=error_warning, bit5=本端收发器 Silent(只听不发)
* bit0~1 来自 CAN_STAT,bit2~4 来自 CAN_ERR;bit5 来自 s_txSilent(唯一真源,
* 与发送门控同一个变量),别拿 bit1 当收发器状态——两者不是一回事。 */
uint8_t busState = 0U;
if (hw_stat[i] & CAN_STAT_IWS) busState |= (1U << 0); /* 初始化模式 */
if (hw_stat[i] & CAN_STAT_SLPWS) busState |= (1U << 1); /* 睡眠模式 */
if (hw_stat[i] & CAN_STAT_SLPWS) busState |= (1U << 1); /* 控制器睡眠模式 */
if (hw_err[i] & CAN_ERR_PERR) busState |= (1U << 2); /* 错误被动 */
if (hw_err[i] & CAN_ERR_BOERR) busState |= (1U << 3); /* 总线关闭 */
if (hw_err[i] & CAN_ERR_WERR) busState |= (1U << 4); /* 错误警告 */
if (s_txSilent[i] != 0U) busState |= (1U << 5); /* 收发器 Silent */
s_feedback[i].stats.bus_state = busState;
s_idSnapshot[i].seq = s_feedbackSeq;
}
@@ -1532,6 +1925,10 @@ void CanFd::refreshFeedbackSnapshot()
for (size_t i = 0; i < kChannelCount; ++i) {
uint8_t cur = s_feedback[i].stats.bus_state;
if (cur != s_prevBusState[i]) {
if (s_chanQuiet[i] != 0U) { /* 静默通道的状态变化只记账不打印 */
s_prevBusState[i] = cur;
continue;
}
const char *stateStr = "NORMAL";
if (cur & (1U << 0)) stateStr = "INIT_MODE";
else if (cur & (1U << 3)) stateStr = "BUS_OFF";
@@ -1593,10 +1990,12 @@ bool CanFd::copyFeedbackSnapshot(uint8_t channelIndex, ChannelFeedback *out)
* @retval 无返回值。
* @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(0U)`。
*/
extern "C" void CAN0_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(0U); }
extern "C" void CAN0_RX0_IRQHandler(void)
{ CpuLoad::isrEnter(); CanFd::handleRxInterrupt(0U); CpuLoad::isrExit(); }
/** @brief CAN0 TX 完成中断向量入口。 */
extern "C" void CAN0_TX_IRQHandler(void) { CanFd::handleTxInterrupt(0U); }
extern "C" void CAN0_TX_IRQHandler(void)
{ CpuLoad::isrEnter(); CanFd::handleTxInterrupt(0U); CpuLoad::isrExit(); }
/**
* @brief CAN1 RX0 中断向量入口。
@@ -1605,10 +2004,12 @@ extern "C" void CAN0_TX_IRQHandler(void) { CanFd::handleTxInterrupt(0U); }
* @retval 无返回值。
* @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(1U)`。
*/
extern "C" void CAN1_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(1U); }
extern "C" void CAN1_RX0_IRQHandler(void)
{ CpuLoad::isrEnter(); CanFd::handleRxInterrupt(1U); CpuLoad::isrExit(); }
/** @brief CAN1 TX 完成中断向量入口。 */
extern "C" void CAN1_TX_IRQHandler(void) { CanFd::handleTxInterrupt(1U); }
extern "C" void CAN1_TX_IRQHandler(void)
{ CpuLoad::isrEnter(); CanFd::handleTxInterrupt(1U); CpuLoad::isrExit(); }
/**
* @brief CAN2 RX0 中断向量入口。
@@ -1617,7 +2018,9 @@ extern "C" void CAN1_TX_IRQHandler(void) { CanFd::handleTxInterrupt(1U); }
* @retval 无返回值。
* @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(2U)`。
*/
extern "C" void CAN2_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(2U); }
extern "C" void CAN2_RX0_IRQHandler(void)
{ CpuLoad::isrEnter(); CanFd::handleRxInterrupt(2U); CpuLoad::isrExit(); }
/** @brief CAN2 TX 完成中断向量入口。 */
extern "C" void CAN2_TX_IRQHandler(void) { CanFd::handleTxInterrupt(2U); }
extern "C" void CAN2_TX_IRQHandler(void)
{ CpuLoad::isrEnter(); CanFd::handleTxInterrupt(2U); CpuLoad::isrExit(); }
+7 -1
View File
@@ -7,6 +7,7 @@
#include "device/timer.h"
#include "sys/task_manager.h"
#include "sys/systick.h"
#include "sys/cpu_load.h"
#include "sys/config.h"
/**
@@ -105,4 +106,9 @@ void SchedulerTimer::onInterrupt()
* @retval 无返回值。
* @note C 向量包装函数,内部转调 `SchedulerTimer::onInterrupt()`。
*/
extern "C" void TIMER2_IRQHandler(void) { SchedulerTimer::onInterrupt(); }
extern "C" void TIMER2_IRQHandler(void)
{
CpuLoad::isrEnter();
SchedulerTimer::onInterrupt();
CpuLoad::isrExit();
}
+53 -5
View File
@@ -33,7 +33,9 @@ public:
/**
* @brief CAN 发送结果枚举,枚举值即 SPI 错误响应码,便于 handler 直接透传。
* @note 0xE0/0xE1 保留给 handler 入口校验(payload 过短/通道非法),
* 不经 transmitFrame 返回;0xE2~0xE5 由 transmitFrame 区分四种失败原因。
* 不经 transmitFrame 返回;0xE2~0xE5 由 transmitFrame 区分四种失败原因;
* 0xE6=通道处于收发器 Silent(只听不发),在入口即拒绝,不计任何错误计数。
* 0xE7=通道被电机 OTA 独占锁定(EtherCAT/SPI 跨总线排他),同样不计错误计数。
*/
enum class CanTxResult : uint8_t {
kOk = 0x00U, /**< 发送成功,已进入硬件邮箱。 */
@@ -41,6 +43,8 @@ public:
kErrTecHigh = 0xE3U, /**< TEC > 96,暂停发送保护通道。 */
kErrMailbox = 0xE4U, /**< 3 个硬件邮箱全 PENDING,abort+重投仍失败。 */
kErrInvalid = 0xE5U, /**< 入口参数无效(channelIndex/dlc/data 等)。 */
kErrSilent = 0xE6U, /**< 通道收发器处于 Silent(驱动器关断),拒绝发送。 */
kErrOtaLocked = 0xE7U, /**< 通道被电机 OTA 独占(跨总线排他),拒绝发送,不计错误计数。 */
};
/**
@@ -53,6 +57,15 @@ public:
uint32_t rx_drop_count;
uint32_t rx_fifo_full_count;
uint32_t rx_fifo_overfull_count;
/** 取帧期间被硬件换帧、整帧丢弃的次数(1.6.6,TS/DLC 前后不一致即判定)。
* 这是"读到半条帧"的唯一可观测量,稳态下应当只在高负载时缓慢增长。 */
uint32_t rx_torn_count;
/** 释放邮箱后等待 RFD 硬件自清超时的次数(手册 26.3.4 第 3 步要求等待)。 */
uint32_t rx_release_timeout_count;
/** 内容级拼接帧拦截次数(1.6.11):(id, data) 结构自相矛盾(STATUS 的
* 温度/电压出量程、CONFIG 的未使用区非 0)⇒ 整帧作废。与 rx_torn 互补:
* torn 挡"取数期间窗口被换",这个挡"窗口没换但 FIFO 缓冲指针错位"。 */
uint32_t rx_content_reject_count;
uint32_t tx_abort_count;
uint32_t last_rx_id;
uint32_t last_tx_id;
@@ -80,7 +93,7 @@ public:
uint8_t last_rx_data[CANFD_FEEDBACK_LAST_DATA_MAX];
uint8_t tec; /**< 最近一次采样时硬件发送错误计数器 (0~255)。 */
uint8_t rec; /**< 最近一次采样时硬件接收错误计数器 (0~255)。 */
uint8_t bus_state; /**< 总线状态位图:bit0=init_mode, bit1=sleep, bit2=error_passive, bit3=bus_off, bit4=error_warning。 */
uint8_t bus_state; /**< 总线状态位图:bit0=init_mode, bit1=sleep, bit2=error_passive, bit3=bus_off, bit4=error_warning, bit5=tx_silent(收发器 Silent,只听不发)。bit0~4 是控制器/总线侧状态,bit5 是本端 S 脚状态。 */
};
/**
@@ -158,7 +171,7 @@ public:
/**
* @brief 直接向指定 CAN 通道发送一帧(绕过软件队列)。
* @param allow_abort 邮箱全占时是否允许 abort 一个 PENDING 邮箱并重投一次。
* - true(0xC0 单帧路径默认):非阻塞顶替,适合"最新命令优先"的单帧发送;
* - true(底层非阻塞顶替):适合最新单发覆盖场景;高总线负载下慎用(可能 abort 掉正在等仲裁的帧);
* - false(突发/队列路径):邮箱满直接返回 kErrMailbox,交由 enqueueTx
* 的软件队列缓存,由 TX 完成中断回灌——避免 abort 把同通道已装载的
* 旧帧静默丢弃(>3 帧同通道突发只剩最后 3 帧上线的根因)。
@@ -173,6 +186,28 @@ public:
/** @brief 运行时切换指定通道 CAN 自动重发(ARD 位,需 init-mode);停等式 OTA 期间关闭、结束后恢复。 */
static void setChannelAutoRetrans(uint8_t channelIndex, bool enable);
/** @brief 运行时压制指定通道 TX 帧的 BRS:压制中 FD 帧数据段回退仲裁段速率。
* 电机 BOOT 无法正确接收 BRS 高速段(见 sys/config.h CONFIG_CAN_FD_BRS_ENABLE 注释),
* 电机 OTA relay 经 enterState 钩子与 setChannelAutoRetrans 同点切换,OTA 结束必恢复。 */
static void setChannelBrsSuppress(uint8_t channelIndex, bool suppress);
/** @brief 切换指定通道收发器(TCAN1057AV)的 Silent 模式:只听不发。
* S 脚(CAN0=PA8/CAN1=PB14/CAN2=PB9)置高 → 驱动器关断、**接收机照常工作**
* (手册 Table 8-4:RXD 仍镜像总线),所以静默期间接收与上报完全不受影响;
* 置低 → 回 Normal。这是硬件级禁止发送,S 脚内部上拉,MCU 复位/浮空即为 Silent。
* @note 进 Silent 前先 abort 本通道 PENDING 邮箱:驱动器已关,残留邮箱只会让
* 控制器因收不到 ACK 而白涨 TEC。
* 退 Silent 时忙等收发器模式建立时间 tMODE=20µs 再返回,否则紧跟的第一帧
* 驱动不上线。用 DWT CYCCNT 忙等,可在 CS EXTI ISR 上下文调用
* (SysTickTimer::delayMs 在该上下文会死等)。
* 与 canfd.cpp 内 s_chanQuiet(空通道上报节流)无关,也别混为一谈。
* 静默不持久化:上电 init 一律回 Normal。
* @retval true 通道合法且已生效;false 通道非法。 */
static bool setTxSilent(uint8_t channelIndex, bool silent);
/** @brief 查询指定通道是否处于收发器 Silent(唯一真源 s_txSilent,0xC1 bus_state bit5 用它)。 */
static bool isTxSilent(uint8_t channelIndex);
/** @brief 读取指定通道 CAN_ERR 寄存器原始值(事件时刻只读快照,不依赖 1Hz 采样)。
* 解码:TEC=bit16-23,REC=bit24-31,ERRN=bit4-6;无效通道返回 0。仅供诊断日志,禁用于发送门控。 */
static uint32_t readChannelErrorReg(uint8_t channelIndex);
@@ -232,8 +267,15 @@ public:
/** @brief 软件 TX 队列当前积压帧数(统计/诊断用)。 */
static uint32_t txQueueBacklog(uint8_t channelIndex);
/** @brief 接收队列容量深度(2 的幂;8kHz PDO 下主循环间隙短,加深缓冲防 STATUS 溢出丢帧)。 */
static constexpr uint8_t rxQueueDepth() { return kRxQueueDepth; }
/** @brief 查询指定通道软件接收队列当前积压帧数(无锁 SPSC,由 tail 与 head 计算)。 */
static uint32_t rxQueueCount(uint8_t channelIndex);
private:
static constexpr uint8_t kRxQueueDepth = 16U;
static constexpr uint8_t kRxQueueDepth = 64U; /* 16→64:1kHz STATUS×3 在 PDI 风暴下不再轻易打满 */
static constexpr uint8_t kRxQueueMask = kRxQueueDepth - 1U;
/** 每通道软件突发队列深度:覆盖整块突发(CAN_BURST_HARD_MAX=16 帧)中同一通道的
* 全部超载帧(3 邮箱之外最多 13 帧 = 16-3),保证任意 DLC 的整块突发不因队列满丢帧。 */
static constexpr uint8_t kTxQueueDepth = 13U;
@@ -245,11 +287,15 @@ private:
uint8_t length;
};
/**
* @brief Lock-Free 单生产者单消费者 (SPSC) 环形接收队列。
* @note 生产者为 CAN RX ISR (Preempt 0) 写入 tail,消费者为 consumeRxFrame (Thread) 推进 head。
* 移除共享 count 变量,消除 BASEPRI=0x10 临界区对 Preempt 0 无效导致的并发撕裂与计数下溢风险。
*/
struct RxQueue {
RxFrame frames[kRxQueueDepth];
volatile uint8_t head;
volatile uint8_t tail;
volatile uint8_t count;
};
/** 排队等待硬件邮箱空位的 CAN 帧(仅 SDO 突发用,payload ≤ 64B)。 */
@@ -294,6 +340,8 @@ private:
static uint8_t s_nextConsumeChannel;
static volatile Mode s_mode;
static ChannelStats s_channelStats[3];
static bool s_brsSuppress[3]; /**< TX BRS 压制(电机 OTA 期间该通道 DLC>8 帧禁 BRS) */
static volatile uint8_t s_txSilent[3]; /**< 收发器 Silent(只听不发):1=S 脚已置高,TX 入口拒绝 */
static ChannelFeedback s_feedback[3];
/** 各通道硬件 init(时序+退出 init mode)是否成功 */
static bool s_channelHwOk[3];
+20 -6
View File
@@ -2,8 +2,10 @@
* @file motor_state.cpp
* @brief 多电机状态表实现 — CAN STATUS 帧识别、内存缓存、SPI 推送序列化。
*
* 所有函数仅在主循环上下文调用(taskCanRxParse / taskHeartbeat / pushStep),
* 无 ISR 竞态,不需要临界区保护。
* 本模块的函数只在主循环上下文调用(taskCanRxParse / taskHeartbeat / pushStep),
* 但消费者不止主循环:EtherCAT 的 buildInput 跑在 PDI_Isr,会抢占 STATUS 写侧,
* 故 STATUS 的 seq/data 由 MotorSlot.wr_seq(seqlock)保护,见 onCanFrame。
* SPI 侧的 buildPayload 与写侧同上下文,天然无此问题。
*/
#include "device/motor_state.h"
@@ -36,7 +38,7 @@ void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
const uint32_t masked = id & kMotorStatusIdMask;
/* STATUS 帧 0x000N0100 (DLC 32, 实时值) */
/* STATUS 帧 0x000N0100 (DLC 48, 实时值,字节 32~35 = InitStatus) */
if (masked == kMotorStatusIdMatch) {
const uint8_t node = static_cast<uint8_t>((id >> 16) & 0x0FU);
if (node < 1U || node > kMotorMaxNodes || channel >= kMotorChannels) {
@@ -45,6 +47,14 @@ void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
MotorSlot &slot = s_motorTable.slots[(node - 1U) * kMotorChannels + channel];
/* seqlock 写侧(1.6.5):奇数=本槽 seq/data 正在改。EtherCAT 的 buildInput
* 跑在 PDI_Isr,会抢占这里;抢占落在下面那次 memcpy 中途就会发布
* "新 seq + 半新半旧 48B"——两帧字节拼成一个 STATUS,边界若落在 float 的
* 符号/指数字节上就是网页上的离谱值。缺的不是顺序而是让消费者能认出
* "这一段没写完"的标记。 */
++slot.wr_seq; /* → 奇数 */
__DMB(); /* 普通写不得重排到奇/偶两次自增之外 */
slot.node_id = node;
slot.channel = channel;
slot.online = 1U;
@@ -57,6 +67,9 @@ void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
if (slot.dlc < kMotorStatusDlc) {
std::memset(&slot.data[slot.dlc], 0, kMotorStatusDlc - slot.dlc);
}
__DMB();
++slot.wr_seq; /* → 偶数:本槽整体一致 */
return;
}
@@ -159,9 +172,10 @@ uint16_t buildPayload(uint8_t page, uint8_t *buf, uint16_t maxLen)
writeU32Le(&buf[offset + 4U], slot.rx_count);
writeU32Le(&buf[offset + 8U], slot.seq);
std::memcpy(&buf[offset + 12U], slot.data, kMotorStatusDlc);
writeU32Le(&buf[offset + 44U], slot.config_rx_count);
writeU32Le(&buf[offset + 48U], slot.config_seq);
std::memcpy(&buf[offset + 52U], slot.config_data, kMotorConfigDlc);
/* STATUS DLC 48 后,CONFIG 字段整体后移 16B */
writeU32Le(&buf[offset + 60U], slot.config_rx_count);
writeU32Le(&buf[offset + 64U], slot.config_seq);
std::memcpy(&buf[offset + 68U], slot.config_data, kMotorConfigDlc);
offset += kMotorEntrySize;
count++;
+10 -5
View File
@@ -32,7 +32,7 @@ static constexpr uint8_t kMotorMaxNodes = CONFIG_MOTOR_MAX_NODES;
* 状态表按 node×channel 双键存储,各通道独立槽位互不覆盖。 */
static constexpr uint8_t kMotorChannels = 3U;
static constexpr uint8_t kMotorSlotCount = kMotorMaxNodes * kMotorChannels; /* 9×3 = 27 */
static constexpr uint8_t kMotorStatusDlc = 32U;
static constexpr uint8_t kMotorStatusDlc = 48U; /* v0.74:STATUS 帧 DLC 48(字节 32~35 = InitStatus) */
static constexpr uint8_t kMotorConfigDlc = 24U; /* CONFIG 帧 DLC */
static constexpr uint16_t kMotorOfflineSec = CONFIG_MOTOR_OFFLINE_MS / 1000U;
@@ -50,18 +50,18 @@ static constexpr uint32_t kBootHeartbeatTimeoutSec = 3U; /* >3s 无 0x103 →
static constexpr uint8_t kMotorActiveBootBit = 0x80U; /* 0xC5 page0 header active 字节 bit7 = boot_active */
/* ── 序列化常量 (0xC5 payload) ── */
static constexpr uint8_t kMotorEntrySize = 76U; /* 1+1+1+1+4+4+32+4+4+24 (STATUS+CONFIG) */
static constexpr uint8_t kMotorEntrySize = 92U; /* 1+1+1+1+4+4+48+4+4+24 (STATUS+CONFIG) */
static constexpr uint8_t kMotorPageHeaderLen = 4U; /* page+total+count+active */
static constexpr uint8_t kMotorEntriesPerPage = 3U; /* (236-4)/76 = 3.05 → 3 */
static constexpr uint8_t kMotorEntriesPerPage = 2U; /* (236-4)/92 = 2.52 → 2 */
static constexpr uint8_t kMotorTotalPages =
(kMotorSlotCount + kMotorEntriesPerPage - 1U) / kMotorEntriesPerPage; /* ceil(27/3) = 9 */
(kMotorSlotCount + kMotorEntriesPerPage - 1U) / kMotorEntriesPerPage; /* ceil(27/2) = 14 */
/// 单电机状态槽
struct MotorSlot {
uint8_t node_id; ///< 1-9, 0=空槽
uint8_t channel; ///< 最后收到帧的 CAN 通道 (0/1/2)
uint8_t online; ///< 1=在线, 0=离线
uint8_t dlc; ///< 最后帧 DLC (正常=32)
uint8_t dlc; ///< 最后帧 DLC (正常=48)
uint32_t rx_count; ///< 累计收到 STATUS 帧数
uint32_t last_rx_sec; ///< 最后收到帧时的 g_app_uptime_sec
uint32_t seq; ///< 更新序号 (每次刷新 +1)
@@ -69,6 +69,11 @@ struct MotorSlot {
uint32_t config_rx_count; ///< 累计收到 CONFIG 帧数
uint32_t config_seq; ///< CONFIG 更新序号
uint8_t config_data[kMotorConfigDlc]; ///< 原始 CONFIG payload (目标值回报)
/* STATUS 写侧 seqlock(1.6.5):奇数=正在写本槽的 seq/data。
* 只有主循环(onCanFrame 的 STATUS 分支)自增,ISR 只读 —— 单核下消费者是
* ISR,它执行期间生产者被冻结,故"读到偶数 ⇒ 生产者不在这段里"成立。
* 放结构体末尾,已有字段偏移全不变(SPI 序列化/PDO 布局照旧)。 */
volatile uint8_t wr_seq;
};
/// 全局电机状态表
@@ -3,6 +3,7 @@
#include "device/spi/app_spi_command_defs.h"
#include "device/canfd.h"
#include "sys/config.h"
#include "sys/motor_ota_relay.h"
#include <cstdio>
namespace {
@@ -76,7 +77,22 @@ public:
return;
}
const CanFd::CanTxResult result = CanFd::transmitFrame(can_ch, can_id, can_data, can_dlc);
/* 通道正被电机 OTA 独占(WiFi 与 EtherCAT 两条链路可能同时在跑,锁是共用的):
* 这一帧挤进同一通道会 abort 掉在途的 0x101 固件包,单发模式下永不重传。
* 回 0xE7,上位机据此知道「是被 OTA 挡了」而不是「邮箱满」。 */
if (MotorOtaRelay::isChannelLocked(can_ch)) {
postCanSendLog(bus, "LOCKED(motor-OTA)", can_ch, can_id, can_data, can_dlc);
const uint8_t err = static_cast<uint8_t>(CanFd::CanTxResult::kErrOtaLocked);
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U);
return;
}
/* 0xC0 单帧发送改走 enqueueTx:
* 1) allow_abort=false 杜绝硬件邮箱被后续发送主动 abort 抹杀;
* 2) 电机高频上报抢占仲裁导致邮箱全占时,帧自动进入 13 深度软件队列,
* 由 TX 中断 drainTxQueueIsr 持续回灌空闲邮箱发送,彻底消除丢帧。 */
const CanFd::CanTxResult result = CanFd::enqueueTx(can_ch, can_id, can_data, can_dlc);
if (result == CanFd::CanTxResult::kOk) {
postCanSendLog(bus, "OK", can_ch, can_id, can_data, can_dlc);
const uint8_t ack = 0x00U;
@@ -3,6 +3,7 @@
#include "device/spi/app_spi_command_defs.h"
#include "device/canfd.h"
#include "sys/config.h"
#include "sys/motor_ota_relay.h"
namespace {
@@ -81,6 +82,11 @@ public:
resp[1U + i] = 0xE1U; /* 通道非法:跳过该帧,其余照常发送 */
continue;
}
if (MotorOtaRelay::isChannelLocked(ch)) {
/* 电机 OTA 独占该通道:整帧拒绝,不挤邮箱(0xE7)。 */
resp[1U + i] = static_cast<uint8_t>(CanFd::CanTxResult::kErrOtaLocked);
continue;
}
const CanFd::CanTxResult r =
CanFd::enqueueTx(ch, id, (dlc > 0U) ? &p[1] : nullptr, dlc);
resp[1U + i] = static_cast<uint8_t>(r);
@@ -21,7 +21,7 @@
* [21..24] error_count (LE)
* [25] tec 硬件发送错误计数器 (0~255)
* [26] rec 硬件接收错误计数器 (0~255)
* [27] bus_state 总线状态位图:bit0=init,bit1=sleep,bit2=error_passive,bit3=bus_off,bit4=error_warning
* [27] bus_state 总线状态位图:bit0=init,bit1=控制器sleep,bit2=error_passive,bit3=bus_off,bit4=error_warning,bit5=tx_silent(收发器Silent)
* [28..] 条目逐条拼接:
* id(4LE) + dlc(1) + flags(1) + reserved(2) + data(dlc)
* 每条 = 8 + dlc 字节
@@ -31,6 +31,16 @@
* [0] mode 0=全部接收, 1=按 ID 过滤;其它值返回 kRspError。
* 应答 payload:
* [0] 实际生效的 mode。
*
* 3) kTypeCanSetSilent (0xC8):ESP32 切换指定通道收发器 Silent(只听不发)。
* 请求 payload:
* [0] channel 0/1/2
* [1] silent 1=进 Silent(S 脚置高:驱动关断、接收机照常), 0=回 Normal
* 应答 payload:
* [0] channel [1] 实际生效的 silent(与发送门控同一个值) [2] 该通道使能位
* ERROR:0xE0=payload 过短, 0xE1=通道非法/未使能, 0xE2=S 脚回读与意图不符(标志已回滚)。
* 生效期间该通道所有发送入口(0xC0/0xC7/桥接/relay)回 0xE6=kErrSilent,
* 不计错误计数;静默不持久化,上电 init 一律回 Normal。
*/
#include "device/spi/spi_command_handler.h"
@@ -138,18 +148,19 @@ static uint16_t buildFeedbackPayload(uint8_t channel, uint8_t page,
namespace {
/** @brief SPI 0xC1 反馈查询 + 0xC2 模式切换处理器。 */
/** @brief SPI 0xC1 反馈查询 + 0xC2 模式切换 + 0xC8 收发器 Silent 开关处理器。 */
class SpiCanFeedbackHandler final : public ISpiCommandHandler {
public:
/** @brief 是否处理 0xC1 / 0xC2。 */
/** @brief 是否处理 0xC1 / 0xC2 / 0xC8。 */
bool canHandle(uint8_t frame_type) const override
{
return (frame_type == SpiDefs::kTypeCanFeedback)
|| (frame_type == SpiDefs::kTypeCanSetMode);
|| (frame_type == SpiDefs::kTypeCanSetMode)
|| (frame_type == SpiDefs::kTypeCanSetSilent);
}
/**
* @brief 分发到 handleFeedback / handleSetMode。
* @brief 分发到 handleFeedback / handleSetMode / handleSetSilent。
* @param bus 命令总线
* @param ctx 请求上下文
*/
@@ -157,6 +168,8 @@ public:
{
if (ctx.frame_type == SpiDefs::kTypeCanFeedback) {
handleFeedback(bus, ctx);
} else if (ctx.frame_type == SpiDefs::kTypeCanSetSilent) {
handleSetSilent(bus, ctx);
} else {
handleSetMode(bus, ctx);
}
@@ -237,6 +250,58 @@ private:
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspAck, &actual, 1U);
}
/**
* @brief 0xC8:切换指定通道收发器 Silent(只听不发,S 脚控制)。
* @param bus 命令总线
* @param ctx 请求上下文(payload[0]=ch, [1]=silent 1/0)
* @note 运行在 SPI CS EXTI ISR 上下文:只做 GPIO + 标志位;退 Silent 的 20µs
* 收发器建立时间由 CanFd::setTxSilent 内部用 DWT 忙等,禁止 SysTick delayMs。
*/
void handleSetSilent(AppSpiCommand &bus, const SpiCommandContext &ctx)
{
if (ctx.payload_len < 2U) {
uint8_t err = 0xE0U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U);
return;
}
const uint8_t channel = ctx.payload[0];
const bool silent = (ctx.payload[1] != 0U);
if (!CanFd::isChannelEnabled(channel)) {
uint8_t err = 0xE1U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U);
return;
}
const bool ok = CanFd::setTxSilent(channel, silent);
const uint8_t actual = CanFd::isTxSilent(channel) ? 1U : 0U;
if (!ok) {
/* S 脚回读与意图不符(被别处抢改 / GPIO 配置未生效):如实报错,不报成功 */
uint8_t err = 0xE2U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspError, &err, 1U);
return;
}
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
char log_msg[AppSpiCommand::kLogBufSize] = {};
(void)snprintf(log_msg, sizeof(log_msg),
"CAN_SILENT ch=%u set=%u -> %u",
static_cast<unsigned>(channel),
static_cast<unsigned>(silent ? 1U : 0U),
static_cast<unsigned>(actual));
bus.postLog(log_msg);
#endif
const uint8_t rsp[3] = { channel, actual, 1U };
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspAck, rsp, 3U);
}
};
static SpiCanFeedbackHandler s_handler;
@@ -1,18 +1,23 @@
/**
* @file app_spi_cmd_exmc_diag.cpp
* @brief SPI 0xA2 EXMC/HBI 诊断命令处理器:驱动 LAN9252 通断测试 pattern / NRST 手动诊断。
* @brief SPI 0xA2 EXMC/HBI 诊断命令处理器:驱动 LAN9252 通断测试 pattern / NRST 手动诊断 / 电源管理 / ESP32 EN。
*
* 网页按钮 → ESP32 /spi/send → SPI 0xA2 → 本 handler → 分流:
* action 0~10 → EscExmcBus::continuityDrive() (EXMC 数据/控制线 pattern)
* action 20~24 → EscLan9252::nrstDrive() (ECAT_NRST/PB10 拉低/拉高/浮空/回读)
* action 30~34 → EscLan9252::pmDrive() (电源管理:读状态/进D1/D2/D3/手动唤醒)
* action 40~44 → Esp32En::drive() (ESP32 EN/PC9 诊断备援;开关请走 EtherCAT 0x2503)
* 立即返回 ACK,不阻塞 SPI 总线。电平保持到下次调用或 action=0(stop)。
* NRST action 返回的 status 字节 = 实际引脚电平(0/1),非 OK 码。
* NRST/ESP32_EN action 返回的 status 字节 = 实际引脚电平(0/1),非 OK 码。
* PM action 返回的 status 字节 = bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE;
* 唤醒(34)另有 1=成功 0=超时;0xEE=无效 action,0xE1=HBI 未就绪。
*/
#include "device/spi/spi_command_handler.h"
#include "device/spi/app_spi_command.h"
#include "device/spi/app_spi_command_defs.h"
#include "comm/esc_exmc_bus.h"
#include "comm/esc_lan9252.h"
#include "sys/esp32_en.h"
namespace {
@@ -29,7 +34,11 @@ public:
uint8_t status = 0xEE;
if (ctx.payload_len >= 1) {
const uint8_t action = ctx.payload[0];
if (action >= 20U) {
if (action >= 40U) {
status = Esp32En::drive(action - 40U);
} else if (action >= 30U) {
status = EscLan9252::pmDrive(action - 30U);
} else if (action >= 20U) {
status = EscLan9252::nrstDrive(action - 20U);
} else {
status = EscExmcBus::continuityDrive(action);
@@ -0,0 +1,118 @@
/**
* @file app_spi_cmd_spi_diag.cpp
* @brief SPI 0xA4 链路全量诊断命令处理器。
*
* 供主机 (ESP32 / 调试脚本) 查询 GD32 侧 SPI0 从机实时运行诊断指标与异常现场快照:
* payload[0]=sub_cmd:
* 0 (或空载荷) -> 读取 ~88 字节全量诊断快照 (ACK)
* 1 -> 复位清零所有诊断计数器 (ACK 1B: 0x00)
* 2 -> 立即输出完整 ASCII 格式化报告到 GD32 USART0 串口 (ACK 1B: 0x00)
*/
#include "device/spi/spi_command_handler.h"
#include "device/spi/app_spi_command.h"
#include "device/spi/app_spi_command_defs.h"
#include "device/spi/app_spi_diag.h"
#include "gd32e50x_gpio.h"
#include "sys/config.h"
#include <cstring>
#if (CONFIG_SPI_DIAG_ENABLE != 0)
namespace {
class SpiDiagHandler final : public ISpiCommandHandler {
public:
bool canHandle(uint8_t frame_type) const override
{
return frame_type == SpiDefs::kTypeSpiDiag;
}
void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override
{
uint8_t sub_cmd = 0U;
if (ctx.payload_len >= 1U && ctx.payload != nullptr) {
sub_cmd = ctx.payload[0];
}
if (sub_cmd == 1U) {
// 复位清零诊断计数器
resetSpiDiagStats();
uint8_t status = 0x00U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspAck, &status, 1U);
return;
}
if (sub_cmd == 2U) {
// 立即向串口输出全量 ASCII 诊断报告
dumpSpiDiag();
uint8_t status = 0x00U;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspAck, &status, 1U);
return;
}
// 默认 / sub_cmd == 0: 返回二进制全量指标快照
const SpiDiagStats &st = getSpiDiagStats();
uint8_t buf[88] = {};
buf[0] = 0x00U; // status OK
buf[1] = bus.isV2Active() ? 1U : 0U;
writeU32Le(&buf[2], bus.getSessionId());
buf[6] = bus.getLocalCredit();
buf[7] = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U;
buf[8] = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U;
buf[9] = bus.isCsActive() ? 1U : 0U;
buf[10] = bus.isRingPending() ? 1U : 0U;
writeU16Le(&buf[11], st.last_spi_stat);
writeU32Le(&buf[13], st.total_cs_assert);
writeU32Le(&buf[17], st.total_cs_release);
writeU32Le(&buf[21], st.valid_v2_frames);
writeU32Le(&buf[25], st.parse_fail_count);
writeU32Le(&buf[29], st.err_crc_mismatch);
writeU16Le(&buf[33], (uint16_t)st.err_no_sof);
writeU16Le(&buf[35], (uint16_t)st.err_sof_drift);
writeU16Le(&buf[37], (uint16_t)st.err_bad_len);
writeU16Le(&buf[39], (uint16_t)st.rx_overrun_count);
writeU16Le(&buf[41], (uint16_t)st.short_rx_len_count);
writeU16Le(&buf[43], (uint16_t)st.zero_rx_len_count);
writeU16Le(&buf[45], (uint16_t)st.all_ff_count);
writeU16Le(&buf[47], (uint16_t)st.all_00_count);
writeU16Le(&buf[49], (uint16_t)st.session_mismatch);
writeU16Le(&buf[51], (uint16_t)st.seq_jump_count);
writeU16Le(&buf[53], (uint16_t)st.ring_timeout_count);
writeU16Le(&buf[55], (uint16_t)st.unknown_type_count);
writeU16Le(&buf[57], (uint16_t)st.handler_empty_count);
buf[59] = st.last_rx_type;
buf[60] = st.last_tx_type;
buf[61] = st.last_error_code;
buf[62] = st.last_error_reason;
writeU16Le(&buf[63], (uint16_t)st.last_rx_len);
writeU16Le(&buf[65], (uint16_t)st.last_error_rx_len);
memcpy(&buf[67], st.last_error_bytes, 8);
writeU32Le(&buf[75], st.last_error_calc_crc);
writeU32Le(&buf[79], st.last_error_expected_crc);
writeU32Le(&buf[83], st.last_error_uptime_ms);
buf[87] = st.last_error_sof_offset;
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
SpiDefs::kRspAck, buf, sizeof(buf));
}
};
static SpiDiagHandler s_handler;
} // namespace
void registerSpiDiagCommand(AppSpiCommand &bus)
{
bus.registerHandler(&s_handler);
}
#else /* CONFIG_SPI_DIAG_ENABLE == 0 */
void registerSpiDiagCommand(AppSpiCommand & /*bus*/) {}
#endif
+568 -80
View File
@@ -23,6 +23,7 @@
#include "device/spi/app_spi_command.h"
#include "device/spi/spi_command_handler.h"
#include "device/spi/app_spi_command_defs.h"
#include "device/spi/app_spi_diag.h"
#include "spi_link_v2.h"
#include "sys/ota_boot_request.h"
@@ -37,6 +38,8 @@ extern "C" {
#include "sys/config.h"
#include "sys/critical.h"
#include "sys/systick.h"
#include "sys/cpu_load.h"
#include "sys/esp32_en.h"
#include <cstdio>
#include <cstring>
@@ -166,7 +169,113 @@ static uint16_t s_lastRespLen = 0;
static bool s_hasCachedResp = false;
/* ============================================================
* v2 帧构建 / 帧解析(委托 spi_link_v2.h,三工程共用)
* SPI 诊断统计与现场快照
* ============================================================ */
#if (CONFIG_SPI_DIAG_ENABLE != 0)
#define SPI_DIAG_RECORD(...) do { __VA_ARGS__; } while (0)
static SpiDiagStats s_diag = {};
const SpiDiagStats& getSpiDiagStats()
{
s_diag.current_local_credit = (uint8_t)g_localCredit;
s_diag.expected_rx_seq = g_expectedRxSeq;
return s_diag;
}
void resetSpiDiagStats()
{
memset(&s_diag, 0, sizeof(s_diag));
}
const char* spiParseResultToString(SpiParseResult res)
{
switch (res) {
case SPI_PARSE_OK: return "OK";
case SPI_PARSE_ERR_TOO_SHORT: return "TOO_SHORT";
case SPI_PARSE_ERR_ALL_FF: return "ALL_FF";
case SPI_PARSE_ERR_ALL_00: return "ALL_00";
case SPI_PARSE_ERR_NO_SOF: return "NO_SOF";
case SPI_PARSE_ERR_BAD_VERSION: return "BAD_VERSION";
case SPI_PARSE_ERR_BAD_LEN: return "BAD_LEN";
case SPI_PARSE_ERR_CRC_MISMATCH: return "CRC_MISMATCH";
default: return "UNKNOWN";
}
}
#else
#define SPI_DIAG_RECORD(...) ((void)0)
#endif
bool AppSpiCommand::isV2Active() const { return g_v2Active; }
uint32_t AppSpiCommand::getSessionId() const { return g_sessionId; }
uint8_t AppSpiCommand::getLocalCredit() const { return (uint8_t)g_localCredit; }
void AppSpiCommand::dumpDiag() const { dumpSpiDiag(); }
#if (CONFIG_SPI_DIAG_ENABLE != 0)
void dumpSpiDiag()
{
const SpiDiagStats &st = getSpiDiagStats();
const uint32_t now = SysTickTimer::getMs();
const uint8_t cs_pin = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U;
const uint8_t rdy_pin = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U;
const uint16_t stat = (uint16_t)SPI_STAT(SPI0);
printf("\r\n==================== [GD32 SPI0 LINK DIAGNOSTICS] ====================\r\n");
printf("Status : v2_active=%d session_id=0x%08lX local_credit=%u uptime=%lums\r\n",
AppSpiCommandBus.isV2Active() ? 1 : 0, (unsigned long)AppSpiCommandBus.getSessionId(),
(unsigned)AppSpiCommandBus.getLocalCredit(), (unsigned long)now);
printf("Pins & HW: CS(PA15)=%u DATA_READY(PB6)=%u ring_pending=%d cs_active=%d\r\n",
cs_pin, rdy_pin, AppSpiCommandBus.isRingPending() ? 1 : 0, AppSpiCommandBus.isCsActive() ? 1 : 0);
printf("SPI_STAT : 0x%04X (TRANS:%d TBE:%d RBNE:%d OVR:%d FERR:%d)\r\n",
stat, (stat & SPI_FLAG_TRANS) ? 1 : 0, (stat & SPI_FLAG_TBE) ? 1 : 0,
(stat & SPI_FLAG_RBNE) ? 1 : 0, (stat & SPI_FLAG_RXORERR) ? 1 : 0,
(stat & SPI_FLAG_FERR) ? 1 : 0);
printf("CS Stats : assert=%lu release=%lu missed=%lu short(<20B)=%lu zero_len=%lu\r\n",
(unsigned long)st.total_cs_assert, (unsigned long)st.total_cs_release,
(unsigned long)st.missed_cs_assert, (unsigned long)st.short_rx_len_count,
(unsigned long)st.zero_rx_len_count);
printf("Frames : valid_v2=%lu parse_fail=%lu all_FF=%lu all_00=%lu\r\n",
(unsigned long)st.valid_v2_frames, (unsigned long)st.parse_fail_count,
(unsigned long)st.all_ff_count, (unsigned long)st.all_00_count);
printf("ParseErr : CRC_err=%lu no_SOF=%lu SOF_drift=%lu bad_ver=%lu bad_len=%lu\r\n",
(unsigned long)st.err_crc_mismatch, (unsigned long)st.err_no_sof,
(unsigned long)st.err_sof_drift, (unsigned long)st.err_bad_version,
(unsigned long)st.err_bad_len);
printf("Hw Errors: RXORERR(ovr)=%lu FERR(fmt)=%lu\r\n",
(unsigned long)st.rx_overrun_count, (unsigned long)st.format_error_count);
printf("Session : HELLO_cnt=%lu mismatch=%lu seq_ok=%lu seq_jump=%lu seq_dup=%lu\r\n",
(unsigned long)st.hello_count, (unsigned long)st.session_mismatch,
(unsigned long)st.seq_ok_count, (unsigned long)st.seq_jump_count,
(unsigned long)st.seq_dup_count);
printf("Sequences: last_rx_seq=0x%02X exp_rx_seq=0x%02X last_tx_seq=0x%02X\r\n",
st.last_rx_seq, st.expected_rx_seq, st.last_tx_seq);
printf("Commands : last_rx_type=0x%02X last_tx_type=0x%02X error_rsp(0x82)=%lu (last_code=0x%02X)\r\n",
st.last_rx_type, st.last_tx_type, (unsigned long)st.error_rsp_count, st.last_error_code);
printf("Routing : unknown_type=%lu (last=0x%02X) handler_empty=%lu (last=0x%02X)\r\n",
(unsigned long)st.unknown_type_count, st.last_unknown_type,
(unsigned long)st.handler_empty_count, st.last_empty_type);
printf("Ring Push: req=%lu timeout=%lu collision=%lu\r\n",
(unsigned long)st.ring_request_count, (unsigned long)st.ring_timeout_count,
(unsigned long)st.ring_collision_count);
if (st.last_error_uptime_ms > 0U) {
printf("LastErr : at %lums, rxLen=%u, reason=%s(0x%02X), SOF_off=%u\r\n",
(unsigned long)st.last_error_uptime_ms, (unsigned)st.last_error_rx_len,
spiParseResultToString((SpiParseResult)st.last_error_reason),
st.last_error_code, (unsigned)st.last_error_sof_offset);
printf("LastBytes: [%02X %02X %02X %02X %02X %02X %02X %02X]\r\n",
st.last_error_bytes[0], st.last_error_bytes[1], st.last_error_bytes[2], st.last_error_bytes[3],
st.last_error_bytes[4], st.last_error_bytes[5], st.last_error_bytes[6], st.last_error_bytes[7]);
if (st.last_error_reason == SPI_PARSE_ERR_CRC_MISMATCH) {
printf("LastCRC : calc=0x%08lX exp=0x%08lX\r\n",
(unsigned long)st.last_error_calc_crc, (unsigned long)st.last_error_expected_crc);
}
}
printf("======================================================================\r\n\r\n");
}
#endif
/* ============================================================
* v2 帧构建 / 诊断帧解析(委托 spi_link_v2.h,三工程共用)
* ============================================================ */
/* 构建响应帧;上层接口(type/payload)保持不变,handler 无需改动。
@@ -190,36 +299,120 @@ static bool spi_build_frame(uint8_t type, const uint8_t *payload, uint16_t paylo
return true;
}
/* 解析 v2 帧;委托 v2_parse_frame,扫描 SOF 并校验 VERSION/CRC32。
* 仅 v2:v2_parse_frame 扫 SOF0/SOF1 + CRC32;禁止再加 v1 CRC8 分支。 */
static bool spi_try_parse_frame(const uint8_t *frame, uint16_t frame_len,
uint8_t *type, const uint8_t **payload, uint16_t *payload_len,
v2_header_t *out_hdr = nullptr)
/* 细粒度诊断帧解析:逐项分析失败原因(无SOF/相位漂移/版本不符/长度截断/CRC位错)。 */
static bool spi_diagnose_and_parse_frame(
const uint8_t *buf, size_t buf_len,
uint8_t *out_type, const uint8_t **out_payload, uint16_t *out_payload_len,
v2_header_t *out_hdr,
SpiParseResult *out_res,
uint32_t *out_calc_crc, uint32_t *out_exp_crc,
uint8_t *out_sof_offset)
{
if (!frame || !type || !payload || !payload_len) { return false; }
if (!buf || buf_len == 0U) {
if (out_res) *out_res = SPI_PARSE_ERR_TOO_SHORT;
if (out_sof_offset) *out_sof_offset = 0xFFU;
return false;
}
v2_header_t hdr;
const uint8_t *pload = nullptr;
const bool ok = v2_parse_frame(frame, frame_len, &hdr, &pload);
if (!ok) { return false; }
if (isAllBytesValue(buf, (uint16_t)buf_len, 0xFFU)) {
if (out_res) *out_res = SPI_PARSE_ERR_ALL_FF;
if (out_sof_offset) *out_sof_offset = 0xFFU;
return false;
}
*type = hdr.type;
*payload = pload;
*payload_len = hdr.payload_len;
if (out_hdr) { *out_hdr = hdr; }
return true;
if (isAllBytesValue(buf, (uint16_t)buf_len, 0x00U)) {
if (out_res) *out_res = SPI_PARSE_ERR_ALL_00;
if (out_sof_offset) *out_sof_offset = 0xFFU;
return false;
}
if (buf_len < V2_OVERHEAD) {
if (out_res) *out_res = SPI_PARSE_ERR_TOO_SHORT;
if (out_sof_offset) *out_sof_offset = 0xFFU;
return false;
}
bool found_sof = false;
size_t first_sof_offset = 0xFFU;
SpiParseResult best_err = SPI_PARSE_ERR_NO_SOF;
for (size_t i = 0; i + V2_OVERHEAD <= buf_len; ++i) {
if (buf[i + V2_OFF_SOF0] != V2_SOF0 || buf[i + V2_OFF_SOF1] != V2_SOF1) {
continue;
}
if (!found_sof) {
found_sof = true;
first_sof_offset = i;
}
const uint8_t version = buf[i + V2_OFF_VERSION];
if (version != V2_VERSION) {
best_err = SPI_PARSE_ERR_BAD_VERSION;
continue;
}
const uint16_t plen = v2_get_u16le(&buf[i + V2_OFF_LEN]);
if (plen > V2_MAX_PAYLOAD) {
best_err = SPI_PARSE_ERR_BAD_LEN;
continue;
}
const size_t total = V2_OVERHEAD + plen;
if (i + total > buf_len) {
best_err = SPI_PARSE_ERR_BAD_LEN;
continue;
}
const uint32_t expect = v2_get_u32le(&buf[i + V2_HEADER_LEN + plen]);
const uint32_t calc = v2_crc32(&buf[i], V2_HEADER_LEN + plen);
if (expect != calc) {
best_err = SPI_PARSE_ERR_CRC_MISMATCH;
if (out_calc_crc) *out_calc_crc = calc;
if (out_exp_crc) *out_exp_crc = expect;
continue;
}
if (out_hdr) {
out_hdr->version = version;
out_hdr->flags = buf[i + V2_OFF_FLAGS];
out_hdr->session_id = v2_get_u32le(&buf[i + V2_OFF_SESSION]);
out_hdr->tx_seq = buf[i + V2_OFF_TX_SEQ];
out_hdr->ack_seq = buf[i + V2_OFF_ACK_SEQ];
out_hdr->credit = buf[i + V2_OFF_CREDIT];
out_hdr->type = buf[i + V2_OFF_TYPE];
out_hdr->payload_len = plen;
out_hdr->frame_offset = i;
}
if (out_type) *out_type = buf[i + V2_OFF_TYPE];
if (out_payload) *out_payload = (plen > 0U) ? &buf[i + V2_HEADER_LEN] : nullptr;
if (out_payload_len) *out_payload_len = plen;
if (out_res) *out_res = SPI_PARSE_OK;
if (out_sof_offset) *out_sof_offset = (uint8_t)i;
if (out_calc_crc) *out_calc_crc = calc;
if (out_exp_crc) *out_exp_crc = expect;
return true;
}
if (out_res) *out_res = best_err;
if (out_sof_offset) *out_sof_offset = (uint8_t)first_sof_offset;
return false;
}
/* ============================================================
* 调试日志辅助
* 调试日志辅助(环形队列,避免事件并发时丢日志)
* ============================================================ */
void AppSpiCommand::postLog(const char *msg)
{
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
if (!msg) return;
snprintf(m_logBuf, kLogBufSize, "[SPI-APP] %s", msg);
m_logPending = true;
uint8_t next = (uint8_t)((m_logHead + 1U) % kLogSlotCount);
if (next != m_logTail) {
strncpy(m_logQueue[m_logHead], msg, kLogSlotLen - 1U);
m_logQueue[m_logHead][kLogSlotLen - 1U] = '\0';
m_logHead = next;
}
#else
(void)msg;
#endif
@@ -229,14 +422,29 @@ void AppSpiCommand::postLogFmt(const char *fmt, uint8_t val)
{
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
if (!fmt) return;
snprintf(m_logBuf, kLogBufSize, fmt, val);
m_logPending = true;
char tmp[kLogSlotLen];
snprintf(tmp, sizeof(tmp), fmt, val);
postLog(tmp);
#else
(void)fmt;
(void)val;
#endif
}
void AppSpiCommand::postLogFmt32(const char *fmt, uint32_t v1, uint32_t v2)
{
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
if (!fmt) return;
char tmp[kLogSlotLen];
snprintf(tmp, sizeof(tmp), fmt, (unsigned long)v1, (unsigned long)v2);
postLog(tmp);
#else
(void)fmt;
(void)v1;
(void)v2;
#endif
}
void AppSpiCommand::buildResponse(uint8_t *tx_buf, uint16_t tx_buf_size,
uint16_t *tx_len, uint8_t rsp_type,
const uint8_t *payload, uint16_t payload_len)
@@ -369,9 +577,18 @@ void AppSpiCommand::configureExti()
{
gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15);
exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH);
exti_interrupt_enable(EXTI_15);
/* 先清 STAT 再开 IMR。warm jump 下 BOOT 关中断跳转前 ESP32 持续拉 CS,
* EXTI15 STAT 大概率残留;若先 enable IMR,残留位会立即回放成一次假 CS
* 沿进 NVIC。 */
exti_interrupt_flag_clear(EXTI_15);
nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U);
exti_interrupt_enable(EXTI_15);
/* SPI CS EXTI 抢占级 0。抢占级 1 与 PDI/SYNC1/SCHED 同级不抢占
* (PDI 窗口可达数百 µs),且被 RT_CRITICAL(BASEPRI≥1) 完全屏蔽;
* release ISR 一旦拖过 ESP32 的 CS 间隔(1ms),re-arm 落空,
* 下一笔查询帧头部丢失。提到 0 后仅与 SYNC0/CAN-RX
* (µs 级)同级排队,release 处理确定性 µs 级完成。代价:0xA2 pmDrive 的
* 忙等(≤10ms,手动 PM 诊断才用)会阻塞 SYNC0——诊断窗口,可接受。 */
nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U);
}
@@ -391,18 +608,20 @@ extern void registerSpiAdcFeedbackCommand(AppSpiCommand &bus);
extern void registerSpiMotorStateCommand(AppSpiCommand &bus);
extern void registerSpiMotorOtaCommand(AppSpiCommand &bus);
extern void registerSpiExmcDiagCommand(AppSpiCommand &bus);
extern void registerSpiDiagCommand(AppSpiCommand &bus);
AppSpiCommand AppSpiCommandBus;
void AppSpiCommand::init()
{
m_csActive = false;
m_rxLen = 0U;
m_txLen = 0U;
m_logPending = false;
m_logHead = 0U;
m_logTail = 0U;
m_commandCount = 0U;
m_ringPending = false;
m_ringDeadline = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
memset(m_logBuf, 0, sizeof(m_logBuf));
memset(m_logQueue, 0, sizeof(m_logQueue));
memset(m_commands, 0, sizeof(m_commands));
registerSpiBootRequestCommand(AppSpiCommandBus);
@@ -417,6 +636,7 @@ void AppSpiCommand::init()
registerSpiMotorStateCommand(AppSpiCommandBus); /* CONFIG 关时为空注册 */
registerSpiMotorOtaCommand(AppSpiCommandBus);
registerSpiExmcDiagCommand(AppSpiCommandBus);
registerSpiDiagCommand(AppSpiCommandBus);
configureGpio();
configureDma();
@@ -519,34 +739,122 @@ void AppSpiCommand::processCommand()
const uint8_t *payload = nullptr;
uint16_t payload_len = 0U;
v2_header_t hdr;
SpiParseResult parse_res = SPI_PARSE_OK;
uint32_t calc_crc = 0U;
uint32_t exp_crc = 0U;
uint8_t sof_offset = 0xFFU;
if (!spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr)) {
postLogFmt("[SPI-APP] 非v2帧 0x%02X -> ERROR", m_rxBuf[0]);
const bool parse_ok = spi_diagnose_and_parse_frame(
m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr,
&parse_res, &calc_crc, &exp_crc, &sof_offset);
if (!parse_ok) {
SPI_DIAG_RECORD(
s_diag.parse_fail_count++;
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
s_diag.last_error_rx_len = m_rxLen;
s_diag.last_error_calc_crc = calc_crc;
s_diag.last_error_expected_crc = exp_crc;
s_diag.last_error_sof_offset = sof_offset;
s_diag.last_error_reason = (uint8_t)parse_res;
s_diag.last_error_code = 0xE8U;
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
);
#if (CONFIG_SPI_APP_LOG_ENABLE != 0 && CONFIG_SPI_DIAG_ENABLE != 0)
switch (parse_res) {
case SPI_PARSE_ERR_CRC_MISMATCH: {
SPI_DIAG_RECORD(s_diag.err_crc_mismatch++);
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] CRC mismatch: calc=0x%08lX exp=0x%08lX rxLen=%u",
(unsigned long)calc_crc, (unsigned long)exp_crc, (unsigned)m_rxLen);
postLog(log_msg);
break;
}
case SPI_PARSE_ERR_NO_SOF: {
SPI_DIAG_RECORD(s_diag.err_no_sof++);
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] No SOF: rxLen=%u raw=[%02X %02X %02X %02X]",
(unsigned)m_rxLen, m_rxBuf[0], m_rxBuf[1], m_rxBuf[2], m_rxBuf[3]);
postLog(log_msg);
break;
}
case SPI_PARSE_ERR_BAD_VERSION: {
SPI_DIAG_RECORD(s_diag.err_bad_version++);
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] Bad ver=0x%02X (exp 0x02) SOF_off=%u rxLen=%u",
(sof_offset < m_rxLen - 2) ? m_rxBuf[sof_offset + 2] : 0xFFU,
(unsigned)sof_offset, (unsigned)m_rxLen);
postLog(log_msg);
break;
}
case SPI_PARSE_ERR_BAD_LEN: {
SPI_DIAG_RECORD(s_diag.err_bad_len++);
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] Bad len at SOF_off=%u rxLen=%u",
(unsigned)sof_offset, (unsigned)m_rxLen);
postLog(log_msg);
break;
}
default: {
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] Parse fail res=%d rxLen=%u",
(int)parse_res, (unsigned)m_rxLen);
postLog(log_msg);
break;
}
}
#endif
if (sof_offset > 0U && sof_offset != 0xFFU) {
SPI_DIAG_RECORD(s_diag.err_sof_drift++);
}
/* 0xE8 payload 携带接收诊断——[0]=0xE8, [1]=rxLen, [2..3]=rxBuf 前 2 字节, [4]=parse_res, [5]=sof_offset */
uint16_t tx_len = 0U;
uint8_t err = 0xEEU;
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
uint8_t diag[8];
diag[0] = 0xE8U;
diag[1] = (m_rxLen > 255U) ? 255U : (uint8_t)m_rxLen;
diag[2] = (m_rxLen > 0U) ? m_rxBuf[0] : 0xFFU;
diag[3] = (m_rxLen > 1U) ? m_rxBuf[1] : 0xFFU;
diag[4] = (uint8_t)parse_res;
diag[5] = sof_offset;
diag[6] = 0x5AU;
diag[7] = 0xA5U;
SPI_DIAG_RECORD(s_diag.error_rsp_count++);
spi_build_frame(kRspError, diag, sizeof(diag), m_txBuf, &tx_len);
m_txLen = tx_len;
finishResponseAndReady();
return;
}
if (CONFIG_SPI_APP_LOG_ENABLE != 0 && !isFlashRelatedFrame(frame_type)) {
postLogFmt("[SPI-APP] 收到非刷写 SPI type=0x%02X", frame_type);
// 解析合法 v2 帧成功
SPI_DIAG_RECORD(
s_diag.valid_v2_frames++;
s_diag.last_rx_type = frame_type;
s_diag.last_rx_seq = hdr.tx_seq;
);
if (sof_offset > 0U && sof_offset != 0xFFU) {
SPI_DIAG_RECORD(s_diag.err_sof_drift++);
}
g_pendingAckSeq = hdr.tx_seq;
if (frame_type == V2_TYPE_LINK_HELLO) {
SPI_DIAG_RECORD(s_diag.hello_count++);
Esp32En::cancelRelinkFromSpi();
g_sessionId = hdr.session_id;
g_txSeq = 0;
g_expectedRxSeq = 0;
g_v2Active = true;
s_hasCachedResp = false;
/* 与 requestEsp32Read() 一致(L5 8bc01dd):建帧前 credit=1,
* 避免 TX 占用时 g_localCredit=0 写入 HELLO_ACK,导致主机 remoteCredit=0 拒发 PING。 */
g_localCredit = 1U;
/* 建连 + 开机身份一次完成:HELLO_ACK = 6B 链路 + 16B 身份(版本/开关) */
uint8_t identity[V2_APP_IDENTITY_LEN];
fillGd32AppIdentityPayload(identity);
size_t built_len = 0U;
@@ -559,6 +867,10 @@ void AppSpiCommand::processCommand()
memcpy(s_lastRespBuf, m_txBuf, m_txLen);
s_lastRespLen = m_txLen;
s_hasCachedResp = true;
SPI_DIAG_RECORD(
s_diag.last_tx_type = V2_TYPE_LINK_HELLO;
s_diag.last_tx_seq = 0;
);
finishResponseAndReady();
return;
@@ -566,42 +878,77 @@ void AppSpiCommand::processCommand()
if (g_v2Active) {
if (hdr.session_id != g_sessionId) {
postLogFmt("[SPI-APP] session 不匹配 0x%02X -> LINK_ERROR",
(uint8_t)(hdr.session_id & 0xFFU));
SPI_DIAG_RECORD(
s_diag.session_mismatch++;
s_diag.last_error_code = V2_ERR_SESSION;
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
s_diag.error_rsp_count++;
);
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] Session mismatch: rx=0x%08lX != cur=0x%08lX",
(unsigned long)hdr.session_id, (unsigned long)g_sessionId);
postLog(log_msg);
#endif
size_t built_len = 0U;
v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq,
(uint8_t)g_localCredit, V2_ERR_SESSION,
(uint8_t)g_expectedRxSeq, m_txBuf, &built_len);
m_txLen = (uint16_t)built_len;
SPI_DIAG_RECORD(
s_diag.last_tx_type = V2_TYPE_LINK_ERROR;
s_diag.last_tx_seq = g_txSeq;
);
finishResponseAndReady();
return;
}
const uint8_t dist = v2_seq_distance(hdr.tx_seq, g_expectedRxSeq);
if (dist == 0U) {
SPI_DIAG_RECORD(s_diag.seq_ok_count++);
g_expectedRxSeq = (uint8_t)(g_expectedRxSeq + 1U);
} else if (dist >= 128U) {
SPI_DIAG_RECORD(s_diag.seq_dup_count++);
if (s_hasCachedResp && hdr.tx_seq == s_lastReqSeq) {
postLogFmt("[SPI-APP] 重复请求 seq=0x%02X -> 重发缓存", hdr.tx_seq);
postLogFmt("[SPI-DIAG][INFO] 重复请求 seq=0x%02X -> 重发缓存", hdr.tx_seq);
memcpy(m_txBuf, s_lastRespBuf, s_lastRespLen);
m_txLen = s_lastRespLen;
finishResponseAndReady();
return;
}
} else {
postLogFmt("[SPI-APP] SEQ jump rx=0x%02X", hdr.tx_seq);
SPI_DIAG_RECORD(
s_diag.seq_jump_count++;
s_diag.last_error_code = V2_ERR_SEQ_JUMP;
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
);
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][WARN] SEQ jump: rx=0x%02X exp=0x%02X (gap=%u)",
hdr.tx_seq, g_expectedRxSeq, (unsigned)dist);
postLog(log_msg);
#endif
g_expectedRxSeq = (uint8_t)(hdr.tx_seq + 1U);
if (frame_type != kTypeBootReq) {
SPI_DIAG_RECORD(s_diag.error_rsp_count++);
size_t built_len = 0U;
v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq,
(uint8_t)g_localCredit, V2_ERR_SEQ_JUMP,
(uint8_t)g_expectedRxSeq, m_txBuf, &built_len);
m_txLen = (uint16_t)built_len;
SPI_DIAG_RECORD(
s_diag.last_tx_type = V2_TYPE_LINK_ERROR;
s_diag.last_tx_seq = g_txSeq;
);
finishResponseAndReady();
return;
}
postLog("SEQ jump but BOOT_REQ still handled");
postLog("[SPI-DIAG] SEQ jump but BOOT_REQ still handled");
}
}
@@ -626,17 +973,46 @@ void AppSpiCommand::processCommand()
}
if (!handled) {
postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type);
uint8_t err = 0xEEU;
SPI_DIAG_RECORD(
s_diag.unknown_type_count++;
s_diag.last_unknown_type = frame_type;
s_diag.last_error_code = 0xE9U;
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
s_diag.error_rsp_count++;
);
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] 未知 type=0x%02X -> ERROR 0xE9", frame_type);
postLog(log_msg);
#endif
uint8_t err = 0xE9U; /* E9=无 handler 认领 */
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
}
if (tx_len == 0U) {
uint8_t err = 0xEEU;
SPI_DIAG_RECORD(
s_diag.handler_empty_count++;
s_diag.last_empty_type = frame_type;
s_diag.last_error_code = 0xEAU;
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
s_diag.error_rsp_count++;
);
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
char log_msg[128];
snprintf(log_msg, sizeof(log_msg),
"[SPI-DIAG][ERR] handler t=0x%02X tx_len=0 -> ERROR 0xEA", frame_type);
postLog(log_msg);
#endif
uint8_t err = 0xEAU; /* EA=handler 命中但未产出响应 */
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
}
m_txLen = tx_len;
SPI_DIAG_RECORD(
s_diag.last_tx_type = m_txBuf[V2_OFF_TYPE];
s_diag.last_tx_seq = g_txSeq;
);
if (g_v2Active) {
s_lastReqSeq = hdr.tx_seq;
@@ -670,8 +1046,47 @@ void AppSpiCommand::cancelRing()
updateDataReady();
}
void AppSpiCommand::resetEsp32Link()
{
/* ESP32 被 PC9 掉电可能死在半笔 CS 里,SPI0/DMA/EXTI 都可能卡死。
* 做一次完整外设重初始化(对齐 init 的硬件序列),而不是只清软件状态。
* 仅在 PC9 翻转、对端确定不在发帧时调用。 */
nvic_irq_disable(EXTI10_15_IRQn);
exti_interrupt_disable(EXTI_15);
g_v2Active = false;
g_sessionId = 0U;
g_txSeq = 0U;
g_expectedRxSeq = 0U;
g_pendingAckSeq = 0U;
g_localCredit = 1U;
s_lastReqSeq = 0xFFU;
s_lastRespLen = 0U;
s_hasCachedResp = false;
m_csActive = false;
m_rxLen = 0U;
m_ringPending = false;
m_ringDeadline = 0U;
m_txLen = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
memset(m_rxBuf, 0x00, sizeof(m_rxBuf));
/* 硬件重初始化:SPI0 + DMA + CS EXTI(与 init() 同序列) */
configureDma();
configureSpi0();
armSpiDmasForNextCs();
updateDataReady();
configureExti();
#if (CONFIG_SPI_APP_LOG_ENABLE != 0)
printf("[SPI-APP] resetEsp32Link: SPI0/DMA/EXTI re-init done\r\n");
#endif
}
bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len)
{
SPI_DIAG_RECORD(s_diag.ring_request_count++);
/* 若上一次按铃尚未被 ESP32 读走,拒绝重复按铃 */
if (m_ringPending) {
return false;
@@ -704,39 +1119,61 @@ bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload,
}
/* ============================================================
* 主循环轮询:打印延迟日志 + 按铃超时看护
* 主循环轮询:打印延迟日志 + 按铃超时看护 + 周期/异常诊断报告
* ============================================================ */
void AppSpiCommand::poll()
{
/* 打印 ISR 推迟的日志 */
char log_copy[kLogBufSize] = {};
bool has_log = false;
uint32_t save = RT_CRITICAL_SAVE();
if (m_logPending) {
memcpy(log_copy, m_logBuf, sizeof(log_copy));
m_logPending = false;
has_log = true;
}
RT_CRITICAL_RESTORE(save);
if (has_log) {
printf("%s\r\n", log_copy);
/* 1. 打印 ISR 推迟的日志队列(环形缓冲,避免丢弃) */
while (m_logTail != m_logHead) {
printf("%s\r\n", m_logQueue[m_logTail]);
m_logTail = (uint8_t)((m_logTail + 1U) % kLogSlotCount);
}
/* 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。
/* 2. 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。
* 防止 ESP32 不响应时永久占用 SPIready 线导致双方死锁。 */
if (m_ringPending) {
uint32_t save2 = RT_CRITICAL_SAVE();
if (m_ringPending) {
if ((int32_t)(SysTickTimer::getMs() - m_ringDeadline) >= 0) {
printf("[SPI-APP] 按铃超时取消 (ESP32 未响应, 释放 SPIready 重试)\r\n");
SPI_DIAG_RECORD(s_diag.ring_timeout_count++);
#if (CONFIG_SPI_DIAG_PERIODIC_PRINT != 0)
printf("[SPI-DIAG][WARN] 按铃超时取消 (ESP32 未在 %lums 内响应, 释放 SPIready 重试)\r\n",
(unsigned long)kRingTimeoutMs);
#endif
cancelRing();
}
}
RT_CRITICAL_RESTORE(save2);
}
#if (CONFIG_SPI_DIAG_ENABLE != 0 && CONFIG_SPI_DIAG_PERIODIC_PRINT != 0)
/* 3. 定期 / 增量打印 SPI 诊断摘要 */
static uint32_t s_lastDiagPrintMs = 0;
static uint32_t s_lastDiagErrCount = 0;
const uint32_t now = SysTickTimer::getMs();
const uint32_t totalErrs = s_diag.parse_fail_count + s_diag.rx_overrun_count +
s_diag.short_rx_len_count + s_diag.session_mismatch +
s_diag.seq_jump_count + s_diag.ring_timeout_count +
s_diag.unknown_type_count + s_diag.handler_empty_count;
const bool hasNewError = (totalErrs > s_lastDiagErrCount && (now - s_lastDiagPrintMs >= 2000U));
const bool periodicCheck = (s_diag.total_cs_assert > 0U && (now - s_lastDiagPrintMs >= 5000U));
if (hasNewError || periodicCheck) {
s_lastDiagPrintMs = now;
s_lastDiagErrCount = totalErrs;
const uint8_t cs_pin = (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == SET) ? 1U : 0U;
const uint8_t rdy_pin = (gpio_input_bit_get(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN) == SET) ? 1U : 0U;
printf("[SPI-DIAG] CS:assert=%lu rls=%lu ok_v2=%lu | err[CRC:%lu noSOF:%lu short:%lu ovr:%lu sessMis:%lu jmp:%lu tmo:%lu] pins[CS:%d RDY:%d STAT:0x%02X]\r\n",
(unsigned long)s_diag.total_cs_assert, (unsigned long)s_diag.total_cs_release,
(unsigned long)s_diag.valid_v2_frames,
(unsigned long)s_diag.err_crc_mismatch, (unsigned long)s_diag.err_no_sof,
(unsigned long)s_diag.short_rx_len_count, (unsigned long)s_diag.rx_overrun_count,
(unsigned long)s_diag.session_mismatch, (unsigned long)s_diag.seq_jump_count,
(unsigned long)s_diag.ring_timeout_count, cs_pin, rdy_pin, (unsigned)(s_diag.last_spi_stat & 0xFFU));
}
#endif
}
/* ============================================================
@@ -745,6 +1182,7 @@ void AppSpiCommand::poll()
void AppSpiCommand::onCsAssert()
{
SPI_DIAG_RECORD(s_diag.total_cs_assert++);
if (m_csActive) {
return;
}
@@ -756,9 +1194,16 @@ void AppSpiCommand::onCsAssert()
void AppSpiCommand::onCsRelease()
{
SPI_DIAG_RECORD(s_diag.total_cs_release++);
if (!m_csActive) {
/* EXTI 丢失下降沿时:仍尝试收尾并 re-arm,避免永久失步(对齐 BOOT) */
m_rxLen = stopSpiDmas(kRxBufSize);
SPI_DIAG_RECORD(
s_diag.missed_cs_assert++;
s_diag.last_rx_len = m_rxLen;
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
);
postLogFmt("[SPI-DIAG][WARN] CS release without assert (got %u B)", (uint8_t)m_rxLen);
drainSpiRxHw();
if (m_txLen > 0U) {
m_txLen = 0U;
@@ -766,6 +1211,9 @@ void AppSpiCommand::onCsRelease()
}
if (m_rxLen >= V2_OVERHEAD) {
processCommand();
} else {
/* 残包:SPI 可能已乱,重配外设 */
configureSpi0();
}
armSpiDmasForNextCs();
updateDataReady();
@@ -775,18 +1223,15 @@ void AppSpiCommand::onCsRelease()
/* 停 DMA 并取本笔 RX 长度(先读 CHCNT 再 disable,否则计数器被清) */
m_rxLen = stopSpiDmas(kRxBufSize);
SPI_DIAG_RECORD(
s_diag.last_rx_len = m_rxLen;
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
);
drainSpiRxHw();
/* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧,
* 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。
*
* 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 v2 协议帧(如 PING/OTA/0xB0)。
* 若 GD32 正在按铃时 ESP32 发协议帧,会因 m_ringPending=true 被误吞,
* 导致命令无法处理。检测到 v2 SOF 时取消按铃并走正常命令处理。 */
* 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。 */
if (m_ringPending) {
/* 判断本次收到的数据是否为 v2 协议帧(扫描 SOF0 SOF1)。
* 取代旧 containsSyncSof,直接用 V2_SOF0/V2_SOF1。
* 0xB0 进 BOOT 现已走标准 v2 帧 handler,不再单独检测裸字节。 */
bool is_v2_frame = false;
if (m_rxLen >= 2U) {
const uint16_t last = (m_rxLen < 16U) ? (uint16_t)(m_rxLen - 1U) : 16U;
@@ -798,9 +1243,8 @@ void AppSpiCommand::onCsRelease()
}
}
if (is_v2_frame) {
/* ESP32 发的是 v2 协议帧(如 PING/OTA/0xB0 等),不是按铃响应。
* 取消按铃,清空 TX 让 processCommand() 重新装载对应应答。
* processCommand() 末尾由 updateDataReady() 依据新 m_txLen 驱动 PB6。 */
SPI_DIAG_RECORD(s_diag.ring_collision_count++);
postLog("[SPI-DIAG][INFO] Ring collision: host sent v2 frame while ring pending");
m_ringPending = false;
m_txLen = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
@@ -819,8 +1263,46 @@ void AppSpiCommand::onCsRelease()
}
if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) {
if (m_rxLen == 0U) {
SPI_DIAG_RECORD(
s_diag.zero_rx_len_count++;
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_TOO_SHORT;
s_diag.last_error_rx_len = 0U;
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
);
postLog("[SPI-DIAG][WARN] Ignored 0-byte transaction (SCK did not toggle)");
} else if (isAllBytesValue(m_rxBuf, m_rxLen, 0xFFU)) {
SPI_DIAG_RECORD(
s_diag.all_ff_count++;
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_ALL_FF;
s_diag.last_error_rx_len = m_rxLen;
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
);
postLogFmt("[SPI-DIAG][WARN] Ignored all-0xFF transaction (%u bytes)", (uint8_t)m_rxLen);
} else if (isAllBytesValue(m_rxBuf, m_rxLen, 0x00U)) {
SPI_DIAG_RECORD(
s_diag.all_00_count++;
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_ALL_00;
s_diag.last_error_rx_len = m_rxLen;
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
);
postLogFmt("[SPI-DIAG][WARN] Ignored all-0x00 transaction (%u bytes)", (uint8_t)m_rxLen);
} else {
SPI_DIAG_RECORD(
s_diag.short_rx_len_count++;
s_diag.last_error_reason = (uint8_t)SPI_PARSE_ERR_TOO_SHORT;
s_diag.last_error_rx_len = m_rxLen;
memcpy(s_diag.last_error_bytes, m_rxBuf, (m_rxLen < 8U) ? m_rxLen : 8U);
s_diag.last_error_uptime_ms = SysTickTimer::getMs();
);
postLogFmt("[SPI-DIAG][WARN] Ignored short transaction (%u bytes < 20B)", (uint8_t)m_rxLen);
}
m_rxLen = 0U;
/* v2 DATA_READY: 无待处理帧时 updateDataReady() 释放 PB6。 */
/* 起机毛刺/相位漂移后仅 re-arm DMA 不够:SPI0 可能已 OVR 卡死。
* 顺手重配 SPI0+DMA(不动 EXTI/session),下一笔 HELLO 才能收进来。 */
configureSpi0();
armSpiDmasForNextCs();
updateDataReady();
return;
@@ -828,12 +1310,6 @@ void AppSpiCommand::onCsRelease()
m_txLen = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
/* ★ 关键:在 processCommand 之前同步 g_localCredit。
* 此时 m_txLen=0, m_ringPending=false → g_localCredit=1。
* 若不做,buildResponse 会读到上一次 pushStatus/requestEsp32Read 留下的
* g_localCredit=0 → 响应帧携带 credit=0 → ESP32 m_remoteCredit=0 → 后续发送死锁。
* 这是电机 OTA 吐量只有 1 chunk/2s 的根因。 */
updateDataReady();
/* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式),
@@ -854,12 +1330,20 @@ void AppSpiCommand::onInterrupt()
/* RX 已改走 DMA0_CH1:RBNE 逐字节中断已禁用,本 ISR 只保留错误标志清理。
* CS 边沿处理全部在 onCsExtiInterrupt 的 EXTI15 ISR 里完成。 */
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) {
/* RXORERR 理论上不应再出现(RX 走 DMA),但兜底清理避免标志卡死。
* 读 SPI_DATA + SPI_STAT 清除 OVR 标志。 */
SPI_DIAG_RECORD(
s_diag.rx_overrun_count++;
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
);
postLog("[SPI-DIAG][ERR] SPI0 RXORERR (RX Overrun Error)!");
(void)spi_i2s_data_receive(SPI0);
(void)SPI_STAT(SPI0);
}
if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) {
SPI_DIAG_RECORD(
s_diag.format_error_count++;
s_diag.last_spi_stat = (uint16_t)SPI_STAT(SPI0);
);
postLog("[SPI-DIAG][ERR] SPI0 FERR (Frame Format Error)!");
spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR);
}
}
@@ -878,10 +1362,14 @@ void AppSpiCommand::onCsExtiInterrupt()
extern "C" void SPI0_IRQHandler(void)
{
CpuLoad::isrEnter();
AppSpiCommandBus.onInterrupt();
CpuLoad::isrExit();
}
extern "C" void EXTI10_15_IRQHandler(void)
{
CpuLoad::isrEnter();
AppSpiCommandBus.onCsExtiInterrupt();
CpuLoad::isrExit();
}
+34 -1
View File
@@ -3,6 +3,7 @@
#include <cstdint>
#include "sys/config.h"
#include "device/spi/app_spi_diag.h"
class ISpiCommandHandler;
@@ -51,6 +52,9 @@ public:
/** @brief 记录一条带 1 字节参数的待输出日志。 */
void postLogFmt(const char *fmt, uint8_t val);
/** @brief 记录一条带双 32 位参数的待输出日志。 */
void postLogFmt32(const char *fmt, uint32_t v1, uint32_t v2);
/**
* @brief 将应答帧构建到指定发送缓冲区。
* @param[out] tx_buf 发送缓冲区。
@@ -85,10 +89,34 @@ public:
/** @brief 查询当前是否有未读按铃(ESP32 尚未来读)。 */
bool isRingPending() const { return m_ringPending; }
/** @brief 查询 v2 会话是否已建立。 */
bool isV2Active() const;
/** @brief 查询当前 v2 会话 ID。 */
uint32_t getSessionId() const;
/** @brief 查询当前 local credit。 */
uint8_t getLocalCredit() const;
/** @brief 查询 CS 是否当前处于激活(低电平)状态。 */
bool isCsActive() const { return m_csActive; }
/** @brief 打印完整 ASCII SPI 链路诊断报告。 */
void dumpDiag() const;
/** @brief 取消未完成的按铃(释放 SPIready、清 m_ringPending)。
* 0xB0 进 BOOT 等路径需公开调用,避免 0xC1 残留盖住 ACK。 */
void cancelRing();
/**
* @brief ESP32 掉电/重上电后复位 SPI 链路(PC9 翻转时调用)。
*
* ESP32 被 EN 拉低时可能死在半笔 CS 事务里:session/序号/按铃/DMA 都会脏。
* 重上电 HELLO 前必须清干净,否则首帧对不齐 → 永远建不起 v2 会话。
* 清 session(HELLO 会重建)、取消按铃、re-arm DMA、释放 DATA_READY。
*/
void resetEsp32Link();
static constexpr uint16_t kRxBufSize = CONFIG_SPI_RX_BUF_SIZE;
static constexpr uint16_t kTxBufSize = CONFIG_SPI_TX_BUF_SIZE;
static constexpr uint16_t kLogBufSize = CONFIG_SPI_LOG_BUF_SIZE;
@@ -102,7 +130,12 @@ private:
/** 本笔 CS 实际收到的字节数(CS 上升沿时由 RX DMA CHCNT 反算得到)。 */
volatile uint16_t m_rxLen = 0U;
volatile uint16_t m_txLen = 0U;
volatile bool m_logPending = false;
/** 日志环形缓冲区:避免高频/多事件并发时单缓冲覆盖丢日志。 */
static constexpr uint8_t kLogSlotCount = 8U;
static constexpr uint8_t kLogSlotLen = 128U;
char m_logQueue[kLogSlotCount][kLogSlotLen] = {};
volatile uint8_t m_logHead = 0U;
volatile uint8_t m_logTail = 0U;
/** RX DMA 接收缓冲(替代旧 RBNE 逐字节累积的 m_rxBuf)。
* CS 上升沿时 m_rxLen 为本笔实际收到的字节数,本缓冲前 m_rxLen 字节有效。 */
uint8_t m_rxBuf[kRxBufSize] = {};
@@ -21,7 +21,10 @@ static constexpr uint8_t kTypeOtaCancel = 0x03U;
static constexpr uint8_t kTypeAppPing = 0xA1U;
/** @brief EXMC/HBI 诊断命令(ESP32→GD32):payload[0]=action。
* 0=stop(恢复AF_PP), 1-4=数据pattern(0xFFFF/0x0000/0xAAAA/0x5555),
* 5=CS低, 6=RD低, 7=WR低, 8=A1高, 9=A2高。用于 LAN9252 通断测试。 */
* 5=CS低, 6=RD低, 7=WR低, 8=A1高, 9=A2高。用于 LAN9252 通断测试。
* 20~24=ECAT_NRST(PB10) 30~34=LAN9252 电源管理
* 40~44=ESP32 EN(PC9):40/42=拉高 41=拉低保持 43=浮空 44=回读
* (运行期开关请走 EtherCAT SDO 0x2503,本组仅 SPI 诊断备援) */
static constexpr uint8_t kTypeExmcDiag = 0xA2U;
/** @brief APP 初始化状态查询(ESP32→GD32,1.5.34 新增):无 payload。
* 响应 ACK 25B(1.5.35 起;1.5.34 仅前 12B):
@@ -32,6 +35,10 @@ static constexpr uint8_t kTypeExmcDiag = 0xA2U;
* [12..24] fault_codes[13](每模块 1B 故障码,0=成功;编码见 sys/initial.h,
* canfd = 原因码|(首个失败通道<<4))。位定义与 sys/initial.h 一致。 */
static constexpr uint8_t kTypeInitStatus = 0xA3U;
/** @brief SPI 链路全量诊断查询与控制(ESP32→GD32):
* payload[0]=sub_cmd: 0=读取全量诊断快照 (ACK ~88B), 1=复位诊断计数器, 2=打印完整报告到 GD32 串口。
* 若 payload 为空,默认执行 sub_cmd=0。 */
static constexpr uint8_t kTypeSpiDiag = 0xA4U;
static constexpr uint8_t kTypeBootReq = 0xB0U;
static constexpr uint8_t kTypeCanSend = 0xC0U;
static constexpr uint8_t kTypeCanFeedback = 0xC1U; /**< ESP32 -> GD32 轮询 CAN 反馈快照 */
@@ -55,6 +62,14 @@ static constexpr uint8_t kTypeMotorVersion = 0xC6U;
* 响应 ACK: count(1) + 每条目 1 字节状态(CanTxResult 码)。 */
static constexpr uint8_t kTypeCanBurstSend = 0xC7U;
/** @brief CAN 通道收发器 Silent 开关(ESP32→GD32):只听不发(驱动关断、接收照常)。
* 请求 payload: [0] channel 0/1/2, [1] silent 1=进 Silent / 0=回 Normal。
* 应答 ACK: [0] channel, [1] 实际生效的 silent(与发送门控同一个值),[2] 该通道是否使能。
* ERROR: 0xE0=payload 过短, 0xE1=通道未使能/非法, 0xE2=S 脚回读与意图不符(标志已回滚)。
* 生效期间该通道所有发送入口(0xC0/0xC7/桥接/relay)返回 0xE6=kErrSilent,
* 不计错误计数;静默不持久化,上电 init 一律回 Normal。 */
static constexpr uint8_t kTypeCanSetSilent = 0xC8U;
/** @brief 电机 OTA relay 命令(ESP32→GD32):start / cancel / query。
* payload[0]=cmd(0=start,1=cancel,2=query), start 时 payload[1]=ch, payload[2..5]=fw_size LE */
static constexpr uint8_t kTypeMotorOtaCmd = 0xD0U;
+122
View File
@@ -0,0 +1,122 @@
#ifndef DEVICE_APP_SPI_DIAG_H
#define DEVICE_APP_SPI_DIAG_H
#include <cstdint>
#include <cstddef>
#include "sys/config.h"
/**
* @file app_spi_diag.h
* @brief SPI 链路全量硬件与协议诊断模块声明。
*
* 用于快速量化与定位 SPI 通信故障根因:
* - 物理/时序层:CS 边沿跳变、丢失下降沿、0 字节事务(SCK未走/DMA落后)、短包(<20B)、全FF/全00
* - 硬件状态:SPI0 RXORERR (接收FIFO溢出)、FERR (格式错误)、SPI_STAT 寄存器值
* - 协议/帧层:SOF丢失、相位偏移(SOF drift)、版本错误、超长/截断、CRC32位跳变(附带期望值与计算值)
* - 状态机/会话:Session ID脱节、序号跳变(丢包)、序号重复(重发)、未处理命令TYPE、空响应
* - 握手/流控:PB6 DATA_READY 引脚电平、PA15 CS 引脚电平、主动按铃超时、按铃竞争碰撞
* - 现场快照:记录最近一次异常发生时刻、实际接收长度、前导原始字节及 CRC 对比
*/
/**
* @brief SPI 帧解析失败原因细分
*/
enum SpiParseResult : uint8_t {
SPI_PARSE_OK = 0U, ///< 成功解析合法 v2 帧
SPI_PARSE_ERR_TOO_SHORT = 1U, ///< 事务长度小于 v2 最小帧头 (rxLen < 20)
SPI_PARSE_ERR_ALL_FF = 2U, ///< 接收数据全为 0xFF (从机浮空/DMA未就绪/MISO未连接)
SPI_PARSE_ERR_ALL_00 = 3U, ///< 接收数据全为 0x00 (地电平短路或总线死锁)
SPI_PARSE_ERR_NO_SOF = 4U, ///< 未扫描到 SOF 同步字 (0xA5 0x5A)
SPI_PARSE_ERR_BAD_VERSION = 5U, ///< 找到 SOF,但 VERSION != 0x02
SPI_PARSE_ERR_BAD_LEN = 6U, ///< 长度异常 (payload_len > 236 或帧总长超出实际收到的 rxLen)
SPI_PARSE_ERR_CRC_MISMATCH = 7U, ///< CRC32 校验失败 (物理信号干扰/位跳变)
};
/**
* @brief SPI 链路全量诊断计数器与状态快照
*/
struct SpiDiagStats {
/* 1. CS 边沿与事务统计 */
uint32_t total_cs_assert; ///< CS 下降沿次数
uint32_t total_cs_release; ///< CS 上升沿次数
uint32_t missed_cs_assert; ///< 丢失 CS 下降沿异常次数
uint32_t zero_rx_len_count; ///< RX 收到 0 字节 (SCK 未时钟化或 DMA 启动严重落后)
uint32_t short_rx_len_count; ///< RX 收到 1~19 字节 (<20B 残包)
uint32_t all_ff_count; ///< 全 0xFF 事务数
uint32_t all_00_count; ///< 全 0x00 事务数
/* 2. 硬件外设与状态 */
uint32_t rx_overrun_count; ///< SPI0 RXORERR (接收 FIFO 溢出)
uint32_t format_error_count; ///< SPI0 FERR (帧格式错误)
uint16_t last_spi_stat; ///< 最近一笔事务结束时的 SPI_STAT 寄存器值
/* 3. 帧解析统计 */
uint32_t valid_v2_frames; ///< 成功解析的合法 v2 帧总数
uint32_t parse_fail_count; ///< 帧解析失败总数
uint32_t err_no_sof; ///< 未找到 SOF0(0xA5)+SOF1(0x5A)
uint32_t err_sof_drift; ///< SOF 偏移不在索引 0 (前导毛刺或字节时序偏移)
uint32_t err_bad_version; ///< 版本不匹配
uint32_t err_bad_len; ///< 长度字段错误或事务截断
uint32_t err_crc_mismatch; ///< CRC32 校验失败
/* 4. 会话与序号流控 */
uint32_t hello_count; ///< 收到 LINK_HELLO 次数
uint32_t session_mismatch; ///< Session ID 不匹配 (对端重置或会话脱节)
uint32_t seq_ok_count; ///< 序号严格递增正常次数
uint32_t seq_jump_count; ///< 序号跳变 (丢帧)
uint32_t seq_dup_count; ///< 序号重复 (重发包)
uint16_t last_rx_len; ///< 最近收到的 rx_len (字节数)
uint8_t last_rx_seq; ///< 最近收到的 rx_seq
uint8_t expected_rx_seq; ///< 当前期望的 rx_seq
uint8_t last_tx_seq; ///< 最近发送的 tx_seq
/* 5. 命令路由与处理 */
uint8_t last_rx_type; ///< 最近收到的命令 TYPE
uint8_t last_tx_type; ///< 最近构建的响应 TYPE
uint32_t unknown_type_count; ///< 未知命令 TYPE (无 handler 注册)
uint8_t last_unknown_type; ///< 最近未知的 TYPE 值
uint32_t handler_empty_count; ///< Handler 执行但返回 tx_len == 0
uint8_t last_empty_type; ///< 最近产出 0 字节的 TYPE 值
uint32_t error_rsp_count; ///< GD32 主动返回 0x82 ERROR 响应次数
uint8_t last_error_code; ///< 最近一次返回 0x82 的错误码 (0xE8/0xE9/0xEA/V2_ERR_*)
/* 6. 握手引脚与流控 */
uint32_t ring_request_count; ///< 主动按铃请求次数
uint32_t ring_timeout_count; ///< 按铃超时 (ESP32 未在期限内读走)
uint32_t ring_collision_count; ///< 按铃与 ESP32 主动发帧竞争碰撞次数
uint8_t current_local_credit; ///< 当前 local credit (0 或 1)
/* 7. 最近异常快照 (用于定位具体错误现场) */
uint32_t last_error_uptime_ms; ///< 异常发生时刻 SysTick 毫秒数
uint16_t last_error_rx_len; ///< 异常事务收到的字节数
uint8_t last_error_bytes[8]; ///< 异常事务前 8 字节原始数据
uint32_t last_error_calc_crc; ///< CRC 错误时: 计算出的 CRC
uint32_t last_error_expected_crc;///< CRC 错误时: 帧内携带的 CRC
uint8_t last_error_sof_offset; ///< SOF 所在的偏移位置 (255 表示未找到)
uint8_t last_error_reason; ///< SpiParseResult 错误原因枚举
};
#if (CONFIG_SPI_DIAG_ENABLE != 0)
/** @brief 获取当前全局 SPI 诊断统计快照引用 */
const SpiDiagStats& getSpiDiagStats();
/** @brief 复位清零所有 SPI 诊断统计计数器 */
void resetSpiDiagStats();
/** @brief 将解析结果枚举转换为可读字符串 */
const char* spiParseResultToString(SpiParseResult res);
/** @brief 打印完整 SPI 链路诊断报告到串口 (ASCII 表格) */
void dumpSpiDiag();
#else
inline const SpiDiagStats& getSpiDiagStats()
{
static const SpiDiagStats s_empty{};
return s_empty;
}
inline void resetSpiDiagStats() {}
inline const char* spiParseResultToString(SpiParseResult /*res*/) { return "DISABLED"; }
inline void dumpSpiDiag() {}
#endif
#endif /* DEVICE_APP_SPI_DIAG_H */
+7 -1
View File
@@ -7,6 +7,7 @@
#include "device/usart0.h"
#include "sys/config.h"
#include "sys/critical.h"
#include "sys/cpu_load.h"
uint8_t Usart0::s_txBuf[Usart0::kTxBufSize];
volatile uint16_t Usart0::s_txHead = 0U;
@@ -182,4 +183,9 @@ extern "C" int fputc(int ch, FILE *f) { return Usart0::putChar(ch, f); }
* @retval 无返回值。
* @note 该 C 包装函数内部转调 `Usart0::onInterrupt()`。
*/
extern "C" void USART0_IRQHandler(void) { Usart0::onInterrupt(); }
extern "C" void USART0_IRQHandler(void)
{
CpuLoad::isrEnter();
Usart0::onInterrupt();
CpuLoad::isrExit();
}
+2 -3
View File
@@ -15,8 +15,7 @@ uint8_t Watchdog::init()
{
/* FWDGT 的 PSC/RLD 写入经 LSI(IRC40K) 时钟域同步(PUD/RUD 由硬件清零)。
* BOOT 只检测 FWDGT 复位标志、从不启动看门狗,冷启动时 LSI 未开——
* 直接配置会让 PUD/RUD 永远等不到清零(库超时返回 ERROR,1.5.34 起暴露为 init 失败;
* 此前静默:FWDGT 实际按复位默认值 DIV4/0xFFF 运行,超时就与配置不符)。
* 直接配置会让 PUD/RUD 永远等不到清零(库超时返回 ERROR)。
* 必须先手动开 LSI 并等就绪,再配置。 */
rcu_osci_on(RCU_IRC40K);
if (rcu_osci_stab_wait(RCU_IRC40K) != SUCCESS) {
@@ -24,7 +23,7 @@ uint8_t Watchdog::init()
}
/* PSC/RLD 配置函数自带 ErrStatus(等待 PUD/RUD 清零带超时);
* 此前返回值被忽略,写入失败会无声变成"看门狗实际未按预期运行"。 */
* 必须检查返回值,否则写入失败会让看门狗实际未按预期运行。 */
fwdgt_write_enable();
uint8_t fault = kFaultOk;
if (fwdgt_prescaler_value_config((uint16_t)CONFIG_FWDGT_PSC) != SUCCESS) {
+8
View File
@@ -5,6 +5,7 @@
#include "main.h"
#include "sys/initial.h"
#include "sys/cpu_load.h"
#include "sys/task_manager.h"
#include "sys/task_runner.h"
#include "sys/config.h"
@@ -45,8 +46,15 @@ int App::run()
APP_LOGI("[APP] >>> TaskRunner ready, entering main loop\r\n");
printBanner();
CpuLoad::init();
while(1) {
/* 负载率测量(1.5.63 定义收窄):runPending 内部按"非延迟工作"分段挂钩
* (ISR 全量 + 任务派发 + 电机 OTA relay,见 sys/cpu_load.h);
* 常开轮询(SSC MainLoop / 各 poll)与 NOP 空转不计入。
* tick() 每圈无条件做 1s 窗口滚动检查——滚动不依赖本圈是否有挂钩段。 */
TaskRunner::runPending();
CpuLoad::tick();
#if (CONFIG_MAIN_LOOP_WFI_ENABLE != 0)
uint32_t save = RT_CRITICAL_SAVE();
if(TaskManager::state().task_flags == 0U) {
+59
View File
@@ -0,0 +1,59 @@
/**
* @file ota_layout.h
* @brief APP/BOOT 共用 OTA Flash 布局(唯一真源)。
* @note 两侧 sys/config.h 都必须 include 本头,禁止再抄地址常量。
* 相对路径:BOOT/APP 的 sys/config.h → ../../../shared/ota_layout.h
*
* 约定:0x00000000=AVAL(有效),0xFFFFFFFF=NOT_AVAL(擦除态/无效)。
*/
#ifndef SHARED_OTA_LAYOUT_H
#define SHARED_OTA_LAYOUT_H
#include <stdint.h>
/* ---- 整片 Flash 区段(GD32E508VE 512KB) ---- */
#define OTA_LAYOUT_BOOT_START 0x08000000U
#define OTA_LAYOUT_BOOT_SIZE 0x00012000U /* 72KB */
#define OTA_LAYOUT_APP_BACK_START 0x08012000U /* 216KB 备份区 */
#define OTA_LAYOUT_APP_MAIN_START 0x08048000U /* 216KB 主应用区 = APP VTOR */
#define OTA_LAYOUT_APP_MAX_SIZE 0x00035FFCU /* 216KB-4B,末尾 CRC32 */
#define OTA_LAYOUT_CONFIG_PAGE 0x0807E000U /* OTA 状态配置页 2KB */
#define OTA_LAYOUT_SLEEP_PAGE 0x0807E800U /* 睡眠配置页 2KB */
#define OTA_LAYOUT_PAGE_SIZE 0x00000800U /* FMC 单页 2KB */
/* ---- OTA 配置页 word 布局(word N = PAGE + 4*N) ---- */
#define OTA_LAYOUT_WORD_LATEST_AVAL 0U
#define OTA_LAYOUT_WORD_BACKUP_AVAL 1U
#define OTA_LAYOUT_WORD_LATEST_EN 2U
#define OTA_LAYOUT_WORD_BACKUP_EN 3U
#define OTA_LAYOUT_WORD_LATEST_CHECK 4U
#define OTA_LAYOUT_WORD_BACKUP_CHECK 5U
#define OTA_LAYOUT_WORD_BOOT_VER0 6U /* major|minor */
#define OTA_LAYOUT_WORD_BOOT_VER1 7U /* patch|build */
#define OTA_LAYOUT_WORD_ECAT_ARM 8U /* 0xACA70001 才允许 ECAT SDO 刷 APP */
#define OTA_LAYOUT_WORD_CRASH_COUNT 9U /* 连续 FWDGT 崩溃次数;APP 起板成功清 0 */
#define OTA_LAYOUT_ADDR_LATEST_AVAL (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_LATEST_AVAL)
#define OTA_LAYOUT_ADDR_BACKUP_AVAL (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BACKUP_AVAL)
#define OTA_LAYOUT_ADDR_LATEST_EN (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_LATEST_EN)
#define OTA_LAYOUT_ADDR_BACKUP_EN (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BACKUP_EN)
#define OTA_LAYOUT_ADDR_LATEST_CHECK (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_LATEST_CHECK)
#define OTA_LAYOUT_ADDR_BACKUP_CHECK (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BACKUP_CHECK)
#define OTA_LAYOUT_ADDR_BOOT_VER0 (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BOOT_VER0)
#define OTA_LAYOUT_ADDR_BOOT_VER1 (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BOOT_VER1)
#define OTA_LAYOUT_ADDR_ECAT_ARM (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_ECAT_ARM)
#define OTA_LAYOUT_ADDR_CRASH_COUNT (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_CRASH_COUNT)
/** 配置页有效 word 数(擦页回写必须覆盖到此);页 2KB,余量充足。 */
#define OTA_LAYOUT_WORD_COUNT 10U
/** ECAT 下载 arm 令牌。避开擦除态与 AVAL。 */
#define OTA_LAYOUT_ECAT_ARM_KEY 0xACA70001U
/** 连续 FWDGT 达到该次数后 BOOT 停止自动跳 APP,只接受外部灌写。 */
#define OTA_LAYOUT_CRASH_STAY_MAX 3U
#define OTA_LAYOUT_AVAL 0x00000000U
#define OTA_LAYOUT_NOT_AVAL 0xFFFFFFFFU
#endif /* SHARED_OTA_LAYOUT_H */
File diff suppressed because one or more lines are too long
+122
View File
@@ -0,0 +1,122 @@
/**
* @file cpu_load.cpp
* @brief CPU 负载率测量实现(1.5.61,度量定义与并发协议见 cpu_load.h)。
*
* 热路径(beginWork/endWork/isrEnter/isrExit/tick)放进 .ramfunc——它们会被包进
* EXTI0(Sync0, preempt=0) 等延迟敏感的 ISR 包装,与 EthercatSync::onSync0Exti
* 同款 section 属性,避免 flash 等待态给 Sync0 入口添抖动。1s 一次的窗口
* 结算(含 64 位除法)留在普通 flash 函数里。
*/
#include "sys/cpu_load.h"
#include "RTE_Components.h"
#include CMSIS_device_header
volatile uint32_t CpuLoad::s_depth = 0U;
volatile uint32_t CpuLoad::s_stamp = 0U;
volatile uint32_t CpuLoad::s_busy = 0U;
volatile uint32_t CpuLoad::s_winStart = 0U;
volatile uint16_t CpuLoad::s_loadPerm = 0U;
volatile uint32_t CpuLoad::s_seq = 0U;
/** @brief 区间并集的共享进入点。0→1 者负责盖时间戳;STREX 失败方重试时深度
* 已被抢占者修正,不会再重复盖戳。 */
__attribute__((always_inline)) static inline void cpuLoadEnter()
{
uint32_t d;
do {
d = __LDREXW(&CpuLoad::s_depth);
if (d == 0U) {
CpuLoad::s_stamp = DWT->CYCCNT;
__DMB();
}
} while (__STREXW(d + 1U, &CpuLoad::s_depth) != 0U);
__DMB();
}
/** @brief 区间并集的共享退出点。1→0 者把区间累进 s_busy(同样走独占访问,
* 防"ISR 恰好插在主循环退出的读-改-写之间"丢累加)。 */
__attribute__((always_inline)) static inline void cpuLoadLeave()
{
const uint32_t end = DWT->CYCCNT;
const uint32_t start = CpuLoad::s_stamp;
uint32_t d;
do {
d = __LDREXW(&CpuLoad::s_depth);
} while (__STREXW(d - 1U, &CpuLoad::s_depth) != 0U);
if (d == 1U) {
const uint32_t delta = end - start;
uint32_t b;
do {
b = __LDREXW(&CpuLoad::s_busy);
} while (__STREXW(b + delta, &CpuLoad::s_busy) != 0U);
}
}
void CpuLoad::init()
{
/* DWT 已由 EscLan9252::init 使能;这里幂等兜底(万一 EtherCAT 初始化被裁剪)。 */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
s_depth = 0U;
s_busy = 0U;
s_winStart = DWT->CYCCNT;
s_loadPerm = 0U;
s_seq = 0U;
}
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::beginWork()
{
cpuLoadEnter();
}
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::isrEnter()
{
cpuLoadEnter();
}
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::isrExit()
{
cpuLoadLeave();
}
/**
* @brief 窗口结算(普通 flash 函数,1s 才进一次)。
* @note s_busy 用独占访问换出:窗口边界上 ISR 的在途累加要么算进本窗口、
* 要么算进下窗口,不丢。
*/
static void cpuLoadRollWindow(uint32_t now)
{
const uint32_t elapsed = now - CpuLoad::s_winStart; /* 调用方已保证 >= 1s */
uint32_t busy;
do {
busy = __LDREXW(&CpuLoad::s_busy);
} while (__STREXW(0U, &CpuLoad::s_busy) != 0U);
CpuLoad::s_winStart = now;
uint32_t perm = (uint32_t)(((uint64_t)busy * 1000ULL) / (uint64_t)elapsed);
if (perm > 1000U) {
perm = 1000U;
}
CpuLoad::s_loadPerm = (uint16_t)perm;
CpuLoad::s_seq++;
}
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::endWork()
{
cpuLoadLeave();
}
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::tick()
{
const uint32_t now = DWT->CYCCNT;
if ((uint32_t)(now - s_winStart) >= SystemCoreClock) {
cpuLoadRollWindow(now);
}
}
/* C 侧挂钩:供 ecatappl.c / dc_app.c 计入 EtherCAT 过程数据路径。
* 与 beginWork/endWork 共用深度计数器,嵌套不重不漏。 */
extern "C" void CpuLoad_Enter(void) { CpuLoad::beginWork(); }
extern "C" void CpuLoad_Leave(void) { CpuLoad::endWork(); }
+110
View File
@@ -0,0 +1,110 @@
/**
* @file cpu_load.h
* @brief CPU 负载率测量(1.5.63 定义收窄,机制沿用 1.5.61)。
*
* 度量定义:**中断 + 非延迟处理段(任务派发 / OTA 中继等请求驱动工作)占墙钟
* 时间的比值**,1s 滚动窗口,分辨率 0.1%(permille)。
*
* 为什么不计入主循环常开轮询:本架构在 EtherCAT 实时模式(CONFIG_MAIN_LOOP_WFI_ENABLE=0)
* 下主循环空闲时是 NOP 忙等,且 runPending() 每圈无条件跑 SSC MainLoop 轮询与各
* poll() 检查——把这部分计入"busy"的话负载率恒 ≈99%,失去参考价值(1.5.61 实机
* 实测 99.4%,其中绝大部分是自由运转的轮询而非在处理事件)。收窄后的定义回答
* 的问题变成"CPU 有多少时间在响应事件",随真实负载(CAN 流量、OTA 中继、
* DC 模式 Sync0 过程数据搬运)增长;常开轮询成本与空转时间不计入。
*
* 计入项(全走同一深度计数器求**区间并集**,嵌套不重不漏):
* - 全部挂钩 ISR:TIMER2(100µs 调度)、SysTick、EXTI0/1/2(EtherCAT Sync0/PDI/
* Sync1)、TIMER0(DC 定时档)、CAN0-2 RX+TX、SPI0+EXTI10_15、USART0。
* DC 模式下 Sync0_Isr 的 EXMC 过程数据搬运由此计入——这是大头,不能漏。
* - 主循环非延迟段:TaskManager 派发任务、BOOT、OTA relay、新鲜度救援、
* free-run 下 **1ms 节流** 的过程数据应用(有 CpuLoad 挂钩)。
* - PDI/Sync0 ISR 全程(含 SM 类型下每拍 Process/映射)——负载随 PDO Hz 升。
* 不计入:MainLoop/SSC/poll 空转。
* 性能约束:PDI 热路径不得「凡 AL 事件就 Process/InputMapping」——8kHz 时
* 单拍超 ~120µs 墙会掉速;输出用 12B 探针,process 兜底读带新鲜度门。
*
* 并发协议(无关中断、无临界区):enter/exit 用 LDREX/STREX 原子改深度,
* 任一优先级可抢占任意上下文而不破坏计数——STREX 失败方重试时会看到已修正的
* 深度并补上时间戳。已知残余误差:exit 在读时间戳与递减深度之间被高优先级
* 中断插入时,该次中断时长漏计(几 cycle 窗口,对 0.1% 分辨率可忽略)。
*
* @note DWT 已由 EscLan9252::init 使能;init() 幂等兜底,在主循环启动前调用。
*/
#ifndef SYS_CPU_LOAD_H
#define SYS_CPU_LOAD_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/** C 侧挂钩(ecatappl.c / dc_app.c)。与 beginWork/endWork 同一深度计数器。 */
void CpuLoad_Enter(void);
void CpuLoad_Leave(void);
#ifdef __cplusplus
}
#endif
#ifdef __cplusplus
/**
* @brief CPU 负载率测量。
* @note 全静态类,与 SysTickTimer 同风格。
*/
class CpuLoad {
public:
/**
* @brief 使能 DWT CYCCNT 并复位统计状态。
* @note 幂等;须在主循环启动前调用一次。
*/
static void init();
/** @brief 主循环圈起点:进入"busy"区间。与 endWork() 成对,每圈各一次。 */
static void beginWork();
/**
* @brief 主循环圈终点:退出"busy"区间。
* @note 与 beginWork() 成对;窗口滚动不在这里(begin/end 可能整圈不执行),
* 见 tick()。
*/
static void endWork();
/**
* @brief 主循环每圈无条件调用:检查 1s 窗口是否滚动。
* @note 只做一次 CYCCNT 读 + 比较(命中才进 flash 侧结算函数),滚动不再
* 依赖"本圈恰好有挂钩段执行",空闲系统 seq 也会按秒推进。
*/
static void tick();
/** @brief 中断入口挂钩:与 isrExit() 成对,包住 ISR 主体。 */
static void isrEnter();
/** @brief 中断出口挂钩。 */
static void isrExit();
/**
* @brief 最近一个完整窗口的负载率。
* @return 0..1000,单位 0.1%(1000=满载);窗口未滚动过时为 0。
*/
static uint16_t loadPermille() { return s_loadPerm; }
/**
* @brief 已完成的统计窗口数(自 init 起,单调递增回绕)。
* @note 随负载率一起进 EtherCAT 输入 PDO 段 5,主站据此判断数据是否仍在刷新。
*/
static uint32_t windowSeq() { return s_seq; }
public:
/* 静态成员开成 public:cpu_load.cpp 里 .ramfunc 的文件级内联热路径
* (cpuLoadEnter/cpuLoadLeave/cpuLoadRollWindow) 直接触碰它们,
* 经成员函数绕一道反而多一层调用。勿在模块外写这些状态。 */
static volatile uint32_t s_depth; /**< busy 区间嵌套深度 */
static volatile uint32_t s_stamp; /**< 深度 0→1 时刻的 CYCCNT 快照 */
static volatile uint32_t s_busy; /**< 当前窗口内累计 busy 周期数 */
static volatile uint32_t s_winStart; /**< 当前窗口起点 CYCCNT */
static volatile uint16_t s_loadPerm; /**< 最近一个完整窗口的负载率(0.1%) */
static volatile uint32_t s_seq; /**< 完成滚动的窗口计数 */
};
#endif /* __cplusplus */
#endif /* SYS_CPU_LOAD_H */
+15
View File
@@ -59,4 +59,19 @@
*/
#define RT_CRITICAL_RESTORE(val) __set_BASEPRI(val)
/**
* @brief Preempt-0 专用临界区:PRIMASK 全局屏蔽,用于与 preempt=0 中断共享的
* 单字标志(读-改-写)。
* @note 严格限定 O(1) 状态操作(置位/清位/单字赋值)。**严禁**在其中调用
* printf/LOG_*:串口入队实测 36us,TX FIFO 满时可达 123us,会把 EtherCAT
* SYNC0 抖动顶出 DC 容差。日志一律放临界区外。
*/
#define RT_CRITICAL_PREEMPT0_SAVE() \
({ \
uint32_t __primask_save = __get_PRIMASK(); \
__disable_irq(); \
__primask_save; \
})
#define RT_CRITICAL_PREEMPT0_RESTORE(val) __set_PRIMASK(val)
#endif /* SYS_CRITICAL_H */
+109
View File
@@ -0,0 +1,109 @@
/**
* @file esp32_en.cpp
* @brief ESP32 EN (PC9) 驱动。PC9 拉高后延时 1.2s 复位 SPI,避开起机毛刺。
*/
#include "sys/esp32_en.h"
#include "sys/config.h"
#include "sys/task_manager.h"
#include "device/spi/app_spi_command.h"
#include <stdio.h>
namespace {
/** @brief PC9 拉高后延时多久再清 SPI(等 ESP32 ROM/APP 引脚初始化毛刺结束)。 */
constexpr uint32_t kPostBootRelinkDelayMs = 1200U;
/** @brief 延时复位排期;0=无。 */
volatile uint32_t s_relinkAtMs = 0U;
void drivePinLow()
{
gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
CONFIG_ESP32_RESET_PIN);
gpio_bit_reset(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN);
}
void drivePinHigh()
{
gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
CONFIG_ESP32_RESET_PIN);
gpio_bit_set(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN);
}
void floatPin()
{
gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_IN_FLOATING, 0,
CONFIG_ESP32_RESET_PIN);
}
bool readPin()
{
return gpio_input_bit_get(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN) == SET;
}
} // namespace
uint8_t Esp32En::drive(uint8_t action)
{
switch (action) {
case 0: /* 拉高(开 WiFi) */
case 2:
/* ESP32 仍死着:先清一次 SPI;起机毛刺会在 1.2s 后再清一次 */
s_relinkAtMs = 0U;
AppSpiCommandBus.resetEsp32Link();
drivePinHigh();
s_relinkAtMs = TaskManager::state().tick_1ms + kPostBootRelinkDelayMs;
break;
case 1: /* 拉低保持(关 WiFi) */
s_relinkAtMs = 0U;
drivePinLow();
AppSpiCommandBus.resetEsp32Link();
break;
case 3: /* 浮空 */
s_relinkAtMs = 0U;
floatPin();
AppSpiCommandBus.resetEsp32Link();
break;
case 4: /* 只读 */
break;
case 5: /* 仅复位 SPI(诊断;ESP32 空闲时调用,再让它 HELLO) */
s_relinkAtMs = 0U;
AppSpiCommandBus.resetEsp32Link();
break;
default:
return 0xEEU;
}
const bool level = readPin();
static const char *const kName[6] = {
"restore HIGH", "drive LOW (hold)", "drive HIGH", "float", "read-only", "spi-relink"};
printf("[APP][ESP32_EN] %s -> pin reads %s\r\n", kName[action],
level ? "HIGH" : "LOW");
return level ? 1U : 0U;
}
void Esp32En::poll()
{
if (s_relinkAtMs == 0U) {
return;
}
const uint32_t now = TaskManager::state().tick_1ms;
if ((int32_t)(now - s_relinkAtMs) < 0) {
return;
}
s_relinkAtMs = 0U;
AppSpiCommandBus.resetEsp32Link();
printf("[APP][ESP32_EN] post-boot SPI relink (glitch window over)\r\n");
}
void Esp32En::cancelRelinkFromSpi()
{
/* HELLO/业务帧已进来:不必再延时复位(会打断建连) */
s_relinkAtMs = 0U;
}
/* EtherCAT SDO 0x2503 回调入口(dc_app.c 为 C 侧)。 */
extern "C" uint8_t EcatEsp32EnDrive(uint8_t action)
{
return Esp32En::drive(action);
}
+42
View File
@@ -0,0 +1,42 @@
/**
* @file esp32_en.h
* @brief ESP32 EN/RESET (PC9 / WLESS_EN) 手动控制 —— 网页开关经 EtherCAT SDO 0x2503 下发。
*
* 硬件语义(见 sys/config.h CONFIG_ESP32_RESET_*):
* - PC9 推挽高 = 释放 ESP_CHIP_EN,ESP32 运行、WiFi 可用
* - PC9 推挽低 = 按死 EN,ESP32 硬复位/掉使能,WiFi 与 HTTP 全断
* - PC9 浮空 = 外部 C41/U11 把 ESP_CHIP_EN 拉低,同样关断(与复位后一致)
*
* 控制必须走 EtherCAT(与 ESP32 解耦):WiFi 关断后 ESP32 已死,
* 经 /spi/send 的路径关得掉、开不回来。
* 恢复:再次 SDO 写 action 0/2 拉高;或 GD32 复位/重新上电(initial.cpp 无条件拉高)。
*
* SPI 重建:ESP32 冷启动的 GPIO 毛刺会再次弄脏 SPI 链路,故拉高 PC9 后
* 在起机窗口内多次 resetEsp32Link(),赶在 HELLO 前把 session/DMA 清干净。
*/
#ifndef SYS_ESP32_EN_H
#define SYS_ESP32_EN_H
#include <stdint.h>
class Esp32En {
public:
/**
* @brief 驱动/回读 ESP32 EN(PC9)。EtherCAT SDO 0x2503 转发到此。
* @param action 0=拉高(开) 1=拉低保持(关) 2=拉高 3=浮空 4=只读 5=仅复位 SPI
* @return 实际引脚电平 0/1(与 nrstDrive 同口径);0xEE=无效 action。
*/
static uint8_t drive(uint8_t action);
/**
* @brief 起机窗口内补清 SPI 链路(TaskRunner::runPending 每圈调用)。
* @note 无排期时立即返回。PC9 拉高后早期少量补清;一旦收到 CS 事务即停,
* 避免打断正在建连的 HELLO。
*/
static void poll();
/** @brief SPI 收到任意 CS 事务时取消剩余补清(由 processCommand 调用)。 */
static void cancelRelinkFromSpi();
};
#endif /* SYS_ESP32_EN_H */
+22 -9
View File
@@ -7,7 +7,7 @@
* ADC → CANFD → OtaBootRequest → TaskManager → Scheduler →
* EtherCAT(+Sync 可选) → Watchdog → report
*
* 1.5.34:各模块 init 从"盲配置"改为带检查(寄存器回读 / 节拍验证 / 校准超时),
* 各模块 init 带检查(寄存器回读 / 节拍验证 / 校准超时),
* 失败不置位;复位源/模块位图/critical 失败位图经 getter 导出供 SPI 0xA3 上报。
*/
@@ -37,6 +37,10 @@ uint8_t AppInitializer::s_initState = AppInitializer::kInitOk;
uint8_t AppInitializer::s_resetCause = 0U;
uint8_t AppInitializer::s_faultCodes[AppInitializer::kModuleCount] = {0U};
/* CoE 0x100A「Software version」写入口,实现在 comm/ssc/src/coeappl.c(C 链接)。
* 主站总线读此对象可自证「新 APP 真起来了」。 */
extern "C" void AppSetSoftwareVersion(const char *version);
namespace {
struct InitEntry {
@@ -195,9 +199,8 @@ void AppInitializer::report()
fail_list[0] != '\0' ? fail_list : "?");
}
/* 日志完整性自证:串口 TX 缓冲溢出会静默吞字节,1.5.40 及之前实测使 [ECAT] 诊断
* 出现 "bus eaabled"/"pdi_type=0x8Dexect" 等掉字,并两次导致 LAN9252 故障误定性。
* 1.5.41 起缓冲满时自旋腾位,仅 USART 外设异常才丢弃;非 0 即代表上面的日志不可全信。 */
/* 日志完整性自证:串口 TX 缓冲溢出会静默吞字节。
* 缓冲满时自旋腾位,仅 USART 外设异常才丢弃;非 0 即代表上面的日志不可全信。 */
const uint32_t dropped = Usart0::droppedBytes();
if (dropped != 0U) {
printf("[APP] WARN: uart dropped %u bytes, 以上日志可能不完整\r\n",
@@ -279,6 +282,16 @@ void AppInitializer::init()
OtaBootRequest::init();
mark(kOtaBootReq);
/* 版本号写进 CoE 0x100A:唯一真源是 sys/config.h 的 APP_FW_VERSION_*,
* 不在别处再抄一遍字符串。 */
{
char ver[20];
snprintf(ver, sizeof ver, "%u.%u.%u",
(unsigned)APP_FW_VERSION_MAJOR, (unsigned)APP_FW_VERSION_MINOR,
(unsigned)APP_FW_VERSION_PATCH);
AppSetSoftwareVersion(ver);
}
TaskManager::init();
mark(kTaskManager);
@@ -293,13 +306,11 @@ void AppInitializer::init()
}
/* ---- EtherCAT: EXMC 总线探活 → SSC 协议栈初始化 → SYNC0/IRQ 中断使能 ----
* 1.5.27:改经 EthercatSlave::init()(内部调用 EscLan9252::init 并记录
* chipId/AL 状态诊断,此前该函数无调用点、s_escReady 恒为 false)。
* 门控用 EscLan9252::isBusReady()(BYTE_TEST 通过)而非 isEscReady():
* 冷启动时 AL=INIT,SSC 必须照常启动并等待主站配置。
* 1.5.39:追加 isCoreReady() 门控——EscLan9252::init() 已等待 EEPROM(SII)
* 另需 isCoreReady()——EscLan9252::init() 已等待 EEPROM(SII)
* 加载完成(DL_STATUS bit0);未加载时 EtherCAT 核心寄存器写静默丢弃,
* SSC 的 AL Event Mask/邮箱 SM 配置会全部落空(主站恒读 AL=INIT 的根因),
* SSC 的 AL Event Mask/邮箱 SM 配置会全部落空(主站恒读 AL=INIT),
* 此时跳过 SSC 并以独立故障码上报。
* EEPROM 校验/烧录延迟到主循环执行,避免阻塞 init(烧录需 ~20s)。 */
EthercatSlave::init();
@@ -322,7 +333,9 @@ void AppInitializer::init()
printf("[ECAT] EXMC bus not ready, SSC skipped\r\n");
}
/* 看门狗放最后:PSC/RLD 写入经硬件确认才置位 */
/* 看门狗放最后:PSC/RLD 写入经硬件确认才置位。
* 崩溃计数不在这里清:须喂狗稳定满 CONFIG_CRASH_CLEAR_AFTER_SEC
* (task_runner 每圈 maybeClearCrashCount),否则主循环崩溃攒不到阈值。 */
const uint8_t wdgFault = Watchdog::init();
if (wdgFault == Watchdog::kFaultOk) {
mark(kWatchdog);
+153 -73
View File
@@ -5,6 +5,7 @@
#include "sys/motor_ota_relay.h"
#include "sys/aes128.h"
#include "sys/config.h"
#include "sys/critical.h"
#include "sys/systick.h"
#include "sys/task_manager.h"
#include "main.h"
@@ -29,11 +30,23 @@ static const uint8_t K_MASTER[16] = {
* 提到 4KB≈85 包(~420ms)给 WiFi/HTTP 抖动更大缓冲。 */
static constexpr uint16_t kBufSize = 4096U;
/* ═══════════════════ 通道独占锁 ═══════════════════ */
/* 位 0..2 = CAN 通道 0..2 正被 OTA 独占。置位于 start() 第一条语句(早于任何
* 状态清零与串口日志),释位于 enterState() 进终态时。
* EtherCAT 的 PDO→CAN 在 Sync0_Isr(preempt 0) 里发,OTA 的 START 可能来自主循环
* 也可能来自 EXTI10_15(preempt 0),因此置位/判忙必须原子完成——否则两者之间
* 的窗口(含 LOG 的百微秒级耗时)足够让一拍运动帧灌进正在升级的通道。
* 同一时刻只跑一个会话,故写入用整字赋值/在 PRIMASK 里做单次 RMW,不做 RMW 于中断。 */
static volatile uint32_t s_channel_lock_mask = 0U;
/* 本次会话的进料来源:环缓只认一路,交叉写入会拼出「一半 WiFi 一半 ECAT」的镜像。 */
static FeedSource s_feed_src = FeedSource::kSpi;
/* ═══════════════════ 内部状态 ═══════════════════ */
static State s_state = State::kIdle;
static volatile State s_state = State::kIdle;
static uint8_t s_error = 0;
static uint8_t s_channel = 0;
static volatile uint8_t s_channel = 0;
static uint32_t s_fw_size = 0;
static uint32_t s_fw_fed = 0; /* 已接受的固件字节数(绝对流位置,防 0xD1 重传重复追加) */
static uint16_t s_total_packets = 0;
@@ -214,6 +227,12 @@ static void enterState(State st) {
s_last_send_ms = 0;
s_crc_resend_pending = false;
/* 通道锁释放:进终态(含 kIdle/kFinished/kError)即解锁,让 EtherCAT/SPI 恢复
* 对该通道发帧。整字清零——同一时刻只可能有一个会话,无需 RMW。 */
if (st == State::kIdle || st == State::kFinished || st == State::kError) {
s_channel_lock_mask = 0U;
}
/* auto_retrans 切换:OTA 是应用层停等协议,硬件自动重发会打穿电机单缓冲 RX
* (重发的前一帧被当成新帧处理 → receive_packet_num 错位 → CRC 错)。
* 进入 kHandshakeReq(start() 唯一入口)即关 ARD 切单发;
@@ -229,8 +248,12 @@ static void enterState(State st) {
|| prev == State::kFinishOta);
if (otaActive && !wasActive) {
CanFd::setChannelAutoRetrans(s_channel, false); /* OTA 开始:单发 */
CanFd::setChannelBrsSuppress(s_channel, true); /* OTA 开始:压 BRS——电机 BOOT 收不了
BRS 数据段,nonce/confirm/64B 数据包
(DLC>8) 会全部静默丢失 */
} else if (!otaActive && wasActive) {
CanFd::setChannelAutoRetrans(s_channel, true); /* OTA 结束:恢复重发 */
CanFd::setChannelBrsSuppress(s_channel, false); /* OTA 结束:恢复 BRS(EtherCAT 桥接吞吐) */
}
}
@@ -1063,18 +1086,33 @@ bool onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
return true; /* 已消费,不再传递给其他 handler */
}
bool start(uint8_t can_channel, uint32_t fw_size) {
if (s_state != State::kIdle && s_state != State::kFinished && s_state != State::kError) {
return false; /* 已在进行中 */
}
bool start(uint8_t can_channel, uint32_t fw_size, FeedSource src, uint8_t node_id) {
if (fw_size == 0 || fw_size > kMaxFwSize) return false;
if (can_channel >= 3) return false;
s_channel = can_channel;
s_fw_size = fw_size;
s_fw_fed = 0U;
s_total_packets = static_cast<uint16_t>((fw_size + kPacketPayload - 1U) / kPacketPayload);
s_packets_acked = 0;
/* ★ 第一条触碰共享状态的语句就完成「判忙 + 上锁」,且两者原子。
* 下面的二十余项清零和 START 日志(UART 格式化,百微秒级)都在锁之后;
* EtherCAT 的 PDO→CAN 由 preempt 0 的 Sync0_Isr 驱动,随时能插进这些空隙,
* 只要漏一拍运动帧进这条通道,单发模式(ARD=1)的 0x101 固件包就被硬件 abort,
* 电机会回 0x05/0x06 并在擦除后半路变砖。 */
{
const uint32_t save = RT_CRITICAL_PREEMPT0_SAVE();
const bool busy = (s_channel_lock_mask != 0U) ||
(s_state != State::kIdle && s_state != State::kFinished &&
s_state != State::kError);
if (!busy) {
s_channel_lock_mask = (1U << can_channel);
}
RT_CRITICAL_PREEMPT0_RESTORE(save);
if (busy) return false; /* 已有会话在跑(WiFi 或 EtherCAT,两者共用这一道门) */
}
s_channel = can_channel;
s_feed_src = src;
s_fw_size = fw_size;
s_fw_fed = 0U;
s_total_packets = static_cast<uint16_t>((fw_size + kPacketPayload - 1U) / kPacketPayload);
s_packets_acked = 0;
s_error = 0;
s_session_num = 0; /* 由 processNonceAck 根据 s_nonce 前4字节设置 */
s_buf_head = 0;
@@ -1089,81 +1127,102 @@ s_channel = can_channel;
s_ack_resend_total = 0;
s_fast_resend_total = 0;
s_last_pad_valid = 0xFFFFU;
s_last_feed_ms = SysTickTimer::getMs(); /* 从 start 起算 SPI 喂数超时 */
s_last_feed_ms = SysTickTimer::getMs(); /* 从 start 起算喂数超时 */
s_auto_retry = 0;
/* 不在 SPI 中断里同步发握手帧(电机跳 BOOT 期间 CAN 可能短暂断开)。
* 先进 kPreCancel 等待电机 0x103 心跳确认已进 BOOT,再由 process() 异步推进到握手。 */
LOG_ERR("[MOTOR-OTA] START ch=%u fw=%lu total=%u\r\n",
(unsigned)can_channel, (unsigned long)fw_size, (unsigned)s_total_packets);
LOG_ERR("[MOTOR-OTA] START ch=%u fw=%lu total=%u src=%s\r\n",
(unsigned)can_channel, (unsigned long)fw_size, (unsigned)s_total_packets,
(src == FeedSource::kEcat) ? "ECAT" : "SPI");
s_boot_heartbeat_seen = false;
enterState(State::kPreCancel);
/* 进 BOOT 帧(0x000N0000, DLC=0)由本机在**已加锁**的通道上发:EtherCAT 链路必须
* 走这里——那条链路的运动帧就是同一批 CAN 邮箱发出去的,先解锁再让上位机发帧
* 等于把窗口留给运动帧。WiFi 链路保持 node_id=0(上位机先发帧的既有顺序),
* 不动那条已验证的通路。 */
if (node_id >= 1U && node_id <= 9U) {
const uint32_t bootId = static_cast<uint32_t>(node_id) << 16;
const bool sent = canSend(bootId, nullptr, 0U);
LOG_ERR("[MOTOR-OTA] ENTER-BOOT id=0x%08lX ch=%u %s\r\n",
(unsigned long)bootId, (unsigned)can_channel, sent ? "queued" : "retry-by-state");
}
return true;
}
bool feedData(const uint8_t *data, uint16_t len) {
/* 接受任何活跃状态的数据(包括握手期间)——让 ESP32 在 kPreCancel/握手
* 期间就能预填充缓冲,进入 kTransfer 后立即有数据可发。
*
* payload: [offset:u32 LE][chunk...]。offset 为固件绝对字节偏移。
* ★ BLF 根因:无 offset 纯流时,SPI 超时重传同一 chunk,GD32 再次追加,
* CAN 上出现「前 236B 循环 185 次」→ CRC 全过但固件内容全错。 */
if (s_state == State::kIdle || s_state == State::kError ||
s_state == State::kFinished) {
return false;
}
if (data == nullptr || len < 4U) {
return false;
}
/* payload: [offset:u32 LE][chunk...]。offset 为固件绝对字节偏移。
* ★ BLF 根因:无 offset 纯流时,SPI 超时重传同一 chunk,GD32 再次追加,
* CAN 上出现「前 236B 循环 185 次」→ CRC 全过但固件内容全错。 */
if (s_feed_src != FeedSource::kSpi) {
/* 本次会话由 ECAT 暂存泵进料:SPI 侧(ESP32 还在线时)的喂数一律拒收,
* 两路交叉写环缓会拼出「半份 WiFi、半份 EtherCAT」的镜像。 */
return false;
}
if (data == nullptr || len < 4U) {
return false;
}
const uint32_t offset = static_cast<uint32_t>(data[0])
| (static_cast<uint32_t>(data[1]) << 8)
| (static_cast<uint32_t>(data[2]) << 16)
| (static_cast<uint32_t>(data[3]) << 24);
const uint8_t *chunk = data + 4U;
uint16_t chunk_len = static_cast<uint16_t>(len - 4U);
if (chunk_len == 0U) {
return true;
}
if (s_fw_size == 0U) {
return false;
}
if (offset >= s_fw_size) {
return true; /* 越界尾:幂等 ACK */
}
/* 截到固件末尾 */
if (offset + chunk_len > s_fw_size) {
chunk_len = static_cast<uint16_t>(s_fw_size - offset);
}
const uint32_t offset = static_cast<uint32_t>(data[0])
| (static_cast<uint32_t>(data[1]) << 8)
| (static_cast<uint32_t>(data[2]) << 16)
| (static_cast<uint32_t>(data[3]) << 24);
return feedAt(offset, data + 4U, static_cast<uint16_t>(len - 4U));
}
if (offset + chunk_len <= s_fw_fed) {
/* 完全已收过:幂等 ACK,不写环缓 */
return true;
}
if (offset > s_fw_fed) {
/* 缺口:拒收,ESP32 按 s_fw_offset 重发正确位置 */
static uint32_t s_last_gap_ms = 0;
const uint32_t now = SysTickTimer::getMs();
if (now - s_last_gap_ms >= 200U) {
LOG_ERR("[MOTOR-OTA] feed gap off=%lu fed=%lu len=%u\r\n",
(unsigned long)offset, (unsigned long)s_fw_fed,
(unsigned)chunk_len);
s_last_gap_ms = now;
}
return false;
}
bool feedAt(uint32_t offset, const uint8_t *chunk, uint16_t chunk_len) {
/* 接受任何活跃状态的数据(包括握手期间)——让上位机在 kPreCancel/握手
* 期间就能预填充缓冲,进入 kTransfer 后立即有数据可发。 */
if (s_state == State::kIdle || s_state == State::kError ||
s_state == State::kFinished) {
return false;
}
if (chunk == nullptr) {
return false;
}
if (chunk_len == 0U) {
return true;
}
if (s_fw_size == 0U) {
return false;
}
if (offset >= s_fw_size) {
return true; /* 越界尾:幂等 ACK */
}
/* 截到固件末尾 */
if (offset + chunk_len > s_fw_size) {
chunk_len = static_cast<uint16_t>(s_fw_size - offset);
}
/* 部分重叠:只追加 [s_fw_fed, offset+chunk_len) */
const uint32_t skip = s_fw_fed - offset;
const uint16_t add_len = static_cast<uint16_t>(chunk_len - skip);
if (bufFree() < add_len) {
return false; /* 环缓满 → SPI WAIT */
}
bufPush(chunk + skip, add_len);
s_fw_fed += add_len;
s_last_feed_ms = SysTickTimer::getMs(); /* SPI 断流检测:记录有效喂数时刻 */
return true;
}
if (offset + chunk_len <= s_fw_fed) {
/* 完全已收过:幂等 ACK,不写环缓 */
return true;
}
if (offset > s_fw_fed) {
/* 缺口:拒收,上位机按 s_fw_fed 重发正确位置 */
static uint32_t s_last_gap_ms = 0;
const uint32_t now = SysTickTimer::getMs();
if (now - s_last_gap_ms >= 200U) {
LOG_ERR("[MOTOR-OTA] feed gap off=%lu fed=%lu len=%u\r\n",
(unsigned long)offset, (unsigned long)s_fw_fed,
(unsigned)chunk_len);
s_last_gap_ms = now;
}
return false;
}
/* 部分重叠:只追加 [s_fw_fed, offset+chunk_len) */
const uint32_t skip = s_fw_fed - offset;
const uint16_t add_len = static_cast<uint16_t>(chunk_len - skip);
if (bufFree() < add_len) {
return false; /* 环缓满 → SPI WAIT / ECAT 下拍再泵 */
}
bufPush(chunk + skip, add_len);
s_fw_fed += add_len;
s_last_feed_ms = SysTickTimer::getMs(); /* 喂数断流检测:记录有效喂数时刻 */
return true;
}
void cancel() {
if (s_state == State::kIdle) return;
@@ -1194,11 +1253,32 @@ Status getStatus() {
st.packets_acked = s_packets_acked;
st.total_packets = s_total_packets;
st.fw_size = s_fw_size;
st.fw_fed = s_fw_fed;
return st;
}
uint16_t feedFreeSpace() {
return bufFree();
}
bool isActive() {
return (s_state != State::kIdle && s_state != State::kFinished && s_state != State::kError);
}
bool isChannelLocked(uint8_t channel) {
if (channel >= 3U) return false;
/* 单次 32 位读:位与本身就是原子的,不需要临界区。
* 判据只看锁位,不看状态机——状态机那几个变量是在锁之后逐步赋的,
* 拿它当门会让出几十微秒的空窗(审计报告里的 88.53% 漏包就是这么来的)。 */
return ((s_channel_lock_mask & (1U << channel)) != 0U);
}
int8_t lockedChannel() {
const uint32_t mask = s_channel_lock_mask;
for (uint8_t ch = 0U; ch < 3U; ++ch) {
if ((mask & (1U << ch)) != 0U) return static_cast<int8_t>(ch);
}
return -1;
}
} /* namespace MotorOtaRelay */
+43 -1
View File
@@ -51,6 +51,16 @@ struct Status {
uint16_t packets_acked;
uint16_t total_packets;
uint32_t fw_size;
/** 已被环缓接受的明文字节数(流位置)。ECAT 暂存泵用它对账:
* 电机侧自动重试会把环缓与这个位置一起清零,泵必须跟着回退。
* 注意:只是内部 C++ 结构,SPI 0xD0 QUERY 的线格式没有改动。 */
uint32_t fw_fed;
};
/** 固件明文进料来源。同一时刻只允许一个来源,环缓不接受两路交叉写入。 */
enum class FeedSource : uint8_t {
kSpi = 0U, /* ESP32 经 SPI 0xD1 喂(WiFi/BLE 链路的既有通路) */
kEcat = 1U, /* EtherCAT 主站经 CoE 0x2511 灌进暂存区,再由 ecat_motor_ota 泵入环缓 */
};
/* 初始化(注册 CAN RX 回调等) */
@@ -67,19 +77,51 @@ void init();
bool onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len);
/* SPI 命令接口 */
bool start(uint8_t can_channel, uint32_t fw_size);
/**
* @brief 启动一次电机 OTA。
* @param can_channel 目标 CAN 通道 0..2。
* @param fw_size 固件明文字节数。
* @param src 进料来源(SPI / ECAT 暂存泵)。
* @param node_id 1..9:由 GD32 自己在**已加锁**的通道上发 0x000N0000 进 BOOT 帧;
* 0:不进 BOOT(WiFi 链路沿用上位机先发帧的老顺序)。
* @note 入口第一条语句即置通道锁,早于任何状态清零与串口日志。
*/
bool start(uint8_t can_channel, uint32_t fw_size,
FeedSource src = FeedSource::kSpi, uint8_t node_id = 0U);
/**
* @brief 喂入固件明文。payload 布局:[offset:u32 LE][data...]
* offset 为固件绝对字节偏移;重复/重叠区间幂等 ACK,缺口拒绝(→SPI WAIT)。
* 无 offset 的旧纯流会在 SPI 超时重传时把同一 chunk 追加进环缓(BLF 见前 236B 循环)。
*/
bool feedData(const uint8_t *data, uint16_t len);
/**
* @brief 已解析过的进料写入(feedData 的核心,偏移/幂等/环缓满判据只有一份)。
* @note 供 ECAT 暂存泵调用:那边已按 offset 寻址,无需再拼 4 字节头,
* 也避免在仅 512B 的主栈上放临时头缓冲。
*/
bool feedAt(uint32_t offset, const uint8_t *chunk, uint16_t chunk_len);
/**
* @brief 环缓当前还能接受多少明文字节。
* @note 给进料方按空位切片用(否则一次投过大永远排不进队首)。不改变任何判据:
* feedAt 内部仍会自己再查一次空位。
*/
uint16_t feedFreeSpace(void);
void cancel();
/* 状态查询 */
Status getStatus();
bool isActive();
/**
* @brief 指定 CAN 通道当前是否被电机 OTA 独占。
* @note 单条 32 位位与,可在 preempt=0(EXTI0/SYNC0、EXTI10_15)里直接调用。
* EtherCAT 桥接与 SPI 桥接/突发在发送前必须查它,否则周期运动帧会
* abort 掉 OTA 的 0x101 包(单发模式 ARD=1 不重传),电机会回 0x05/0x06。
*/
bool isChannelLocked(uint8_t channel);
/** @brief 当前被锁定的通道号;-1 表示无锁。 */
int8_t lockedChannel();
} /* namespace MotorOtaRelay */
#endif /* SYS_MOTOR_OTA_RELAY_H */
+60 -13
View File
@@ -7,21 +7,23 @@
#include "gd32e50x.h"
#include "sys/config.h"
#include "sys/motor_ota_relay.h"
#include "sys/systick.h"
#include <cstdio> /* LOG_ERR 展开为 printf(sys/config.h),本文件原先没有 stdio */
namespace {
/** Bootloader OTA 配置页地址,与 BOOT 工程 sys/config.h 保持一致。 */
constexpr uint32_t kOtaConfigPageAddr = 0x0807E000U;
constexpr uint32_t kAppLatestAvalAddr = kOtaConfigPageAddr + 0x00U;
constexpr uint32_t kAppLatestEnAddr = kOtaConfigPageAddr + 0x08U;
constexpr uint32_t kAppLatestCheckAddr = kOtaConfigPageAddr + 0x10U;
/** BOOT 版本号在 OTA 配置页中的存储地址:word6=major|minor, word7=patch|build。 */
constexpr uint32_t kBootVersionAddr0 = kOtaConfigPageAddr + 0x18U;
constexpr uint32_t kBootVersionAddr1 = kOtaConfigPageAddr + 0x1CU;
constexpr uint32_t kOtaAval = 0x00000000U;
constexpr uint32_t kOtaNotAval = 0xFFFFFFFFU;
constexpr uint32_t kFmcPageSize = 0x00000800U;
constexpr uint32_t kRebootDelayMs = 20U;
/** Bootloader OTA 配置页地址与 word 布局:唯一真源 shared/ota_layout.h。 */
constexpr uint32_t kOtaConfigPageAddr = OTA_LAYOUT_CONFIG_PAGE;
constexpr uint32_t kAppLatestAvalAddr = OTA_LAYOUT_ADDR_LATEST_AVAL;
constexpr uint32_t kAppLatestEnAddr = OTA_LAYOUT_ADDR_LATEST_EN;
constexpr uint32_t kAppLatestCheckAddr = OTA_LAYOUT_ADDR_LATEST_CHECK;
constexpr uint32_t kBootVersionAddr0 = OTA_LAYOUT_ADDR_BOOT_VER0;
constexpr uint32_t kBootVersionAddr1 = OTA_LAYOUT_ADDR_BOOT_VER1;
constexpr uint32_t kBootCrashCountAddr = OTA_LAYOUT_ADDR_CRASH_COUNT;
constexpr uint32_t kOtaAval = OTA_LAYOUT_AVAL;
constexpr uint32_t kOtaNotAval = OTA_LAYOUT_NOT_AVAL;
constexpr uint32_t kFmcPageSize = OTA_LAYOUT_PAGE_SIZE;
constexpr uint32_t kRebootDelayMs = 20U;
static bool writeWord(uint32_t addr, uint32_t value)
{
@@ -122,12 +124,45 @@ BootVersion OtaBootRequest::readBootVersion()
};
}
/**
* @brief 清零连续 FWDGT 崩溃计数。
* @note 与 BOOT OTA_CRASH_STAY_MAX 门配套。写 0(AVAL)可对 1→0 直接编程。
*/
bool OtaBootRequest::clearCrashCount()
{
return otaConfigSet(kBootCrashCountAddr, kOtaAval);
}
/**
* @brief 喂狗运行满 CONFIG_CRASH_CLEAR_AFTER_SEC 后清崩溃计数(每上电至多一次)。
*/
void OtaBootRequest::maybeClearCrashCount(uint32_t uptime_sec)
{
static bool s_cleared = false;
if (s_cleared || uptime_sec < CONFIG_CRASH_CLEAR_AFTER_SEC) {
return;
}
if (clearCrashCount()) {
s_cleared = true;
}
}
/**
* @brief 写入 Bootloader OTA 等待标志,并安排稍后软复位。
* @return true 表示标志写入成功;false 表示 Flash 配置页写入失败。
*/
bool OtaBootRequest::requestEnterBoot()
{
/* 电机 OTA 正在跑(WiFi 与 EtherCAT 两条进料路都算)时绝不交回 BOOT:
* 复位会把电机的刷写腰斩在 erase 之后——旧 APP 已抹、新 APP 没收完,直接变砖。
* 判据只放这一个执行点:SPI 0xB0、EtherCAT CoE 0x2502、CAN 0x320 三条入口
* 最终都要过这里,不在调用方各抄一份。 */
if (MotorOtaRelay::isActive()) {
LOG_ERR("[OTA-BOOT] enter-boot refused: motor OTA active ch=%d\r\n",
(int)MotorOtaRelay::lockedChannel());
return false;
}
bool ok = true;
ok = otaConfigSet(kAppLatestCheckAddr, kOtaAval) && ok;
ok = otaConfigSet(kAppLatestAvalAddr, kOtaAval) && ok;
@@ -197,7 +232,7 @@ void OtaBootRequest::process()
if(!ok) {
s_spiFrozenForBoot = false;
nvic_irq_enable(SPI0_IRQn, 2U, 0U);
nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U);
nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U); /* 与 configureExti 一致:抢占级 0 */
/* requestEnterBoot 失败时 s_rebootPending 仍为 false */
return;
}
@@ -223,3 +258,15 @@ void OtaBootRequest::process()
while(1) {
}
}
/*--------------------------------------------------------------------------
* EtherCAT CoE 0x2502 的入口(comm/ssc/port/dc_app.c 是 C 文件,调不到类静态方法,
* 与 EcatPdo_* 同一形状)。写 1 走的是和 SPI 0xB0 完全相同的那条异步链:
* 这里只置标志,配置页三字的 Flash 写入 + NVIC_SystemReset 在主循环
* OtaBootRequest::process() 下一圈做 —— SDO 响应先发、复位后到,
* 不会把 SM1 半包留在总线上。
*-------------------------------------------------------------------------*/
extern "C" void EcatEnterBootRequestAsync(void)
{
OtaBootRequest::requestEnterBootAsync();
}
+13
View File
@@ -40,6 +40,19 @@ public:
*/
static BootVersion readBootVersion();
/**
* @brief 清零 BOOT 侧连续 FWDGT 崩溃计数(配置页 word9)。
* @note 不要在 init 里立刻调用;见 maybeClearCrashCount()。
*/
static bool clearCrashCount();
/**
* @brief APP 持续喂狗满 CONFIG_CRASH_CLEAR_AFTER_SEC 后清一次崩溃计数。
* @param[in] uptime_sec 当前 APP 运行秒数(g_app_uptime_sec)。
* @note 挂在主循环;init 未走完或 30s 内崩溃时计数保留,BOOT 才能攒到阈值。
*/
static void maybeClearCrashCount(uint32_t uptime_sec);
/**
* @brief 尝试处理一帧 CAN 接收报文。
* @param[in] channel 报文来源 CAN 通道。
+7 -1
View File
@@ -5,6 +5,7 @@
#include "main.h"
#include "sys/systick.h"
#include "sys/cpu_load.h"
volatile uint32_t SysTickTimer::s_ms = 0U;
@@ -93,4 +94,9 @@ bool SysTickTimer::initMs()
* @retval 无返回值。
* @note C 向量包装函数,内部转调 `SysTickTimer::onInterrupt()`。
*/
extern "C" void SysTick_Handler(void) { SysTickTimer::onInterrupt(); }
extern "C" void SysTick_Handler(void)
{
CpuLoad::isrEnter();
SysTickTimer::onInterrupt();
CpuLoad::isrExit();
}
+11 -9
View File
@@ -40,13 +40,15 @@ void TaskManager::init()
* @param[in] taskMask 待置位的任务位图掩码。
* @param[out] 无。
* @retval 无返回值。
* @note 该操作不清除其他已存在的任务位。
* @note 该操作不清除其他已存在的任务位;通过 __LDREXW/__STREXW 独占访问循环实现无锁原子置位。
*/
void TaskManager::set(uint32_t taskMask)
{
uint32_t save = RT_CRITICAL_SAVE();
g_scheduler.task_flags |= taskMask;
RT_CRITICAL_RESTORE(save);
uint32_t val;
do {
val = __LDREXW(&g_scheduler.task_flags);
} while (__STREXW(val | taskMask, &g_scheduler.task_flags) != 0U);
__DMB();
}
/**
@@ -54,15 +56,15 @@ void TaskManager::set(uint32_t taskMask)
* @param[in] 无。
* @param[out] 无。
* @retval uint32_t 返回当前待执行任务位图。
* @note 内部通过关中断实现对 `task_flags` 的原子交换。
* @note 通过 __LDREXW/__STREXW 独占访问循环实现对 `task_flags` 的原子读取与清零,彻底避免被 Preempt 0 ISR 抢占丢失标志。
*/
uint32_t TaskManager::takePending()
{
uint32_t pending;
uint32_t save = RT_CRITICAL_SAVE();
pending = g_scheduler.task_flags;
g_scheduler.task_flags &= ~pending;
RT_CRITICAL_RESTORE(save);
do {
pending = __LDREXW(&g_scheduler.task_flags);
} while (__STREXW(0U, &g_scheduler.task_flags) != 0U);
__DMB();
return pending;
}
+2 -2
View File
@@ -33,7 +33,7 @@ public:
* @param[in] 无。
* @param[out] 无。
* @retval uint32_t 返回当前待执行任务位图。
* @note 内部通过关中断保证原子性,供 TaskRunner 消费。
* @note 内部通过 Cortex-M 独占访问循环(__LDREXW/__STREXW)保证原子性,供 TaskRunner 消费。
*/
static uint32_t takePending();
@@ -42,7 +42,7 @@ public:
* @param[in] taskMask 待置位的任务位图掩码。
* @param[out] 无。
* @retval 无返回值。
* @note 内部通过 BASEPRI 临界区(RT_CRITICAL_SAVE/RESTORE)保证可由主循环与 ISR 共享调用。
* @note 内部通过 Cortex-M 独占访问循环(__LDREXW/__STREXW)保证可由主循环与任意抢占优先级 ISR 共享调用。
*/
static void set(uint32_t taskMask);
+83 -29
View File
@@ -11,14 +11,19 @@
#include "device/motor_state.h"
#include "device/motor_version.h"
#include "sys/config.h"
#include "sys/initial.h"
#include "sys/ota_boot_request.h"
#include "sys/motor_ota_relay.h"
#include "sys/task_manager.h"
#include "sys/cpu_load.h"
#include "sys/esp32_en.h"
#include "sys/task_runner.h"
#include "comm/ethercat_slave.h"
#include "comm/ethercat_sync.h"
#include "comm/ethercat_can_bridge.h"
#include "comm/ecat_motor_ota.h"
#include "comm/esc_exmc_bus.h"
#include "comm/esc_lan9252.h"
extern "C" {
void MainLoop(void);
@@ -167,6 +172,9 @@ void TaskRunner::taskCanTxSend()
if (!CanFd::isChannelEnabled(ch)) {
continue;
}
if (MotorOtaRelay::isChannelLocked(ch)) {
continue; /* 电机 OTA 独占:诊断帧也不许挤这条通道 */
}
uint8_t data[64];
data[0] = 0xC0U;
@@ -234,7 +242,7 @@ void TaskRunner::taskCanRxParse()
s_runtime.last_rx_data,
s_runtime.last_rx_len);
#endif
/* 1.5.53:旁路投递给 EtherCAT PDO 桥。放在分发链末尾且**不消费**帧
/* 旁路投递给 EtherCAT PDO 桥。放在分发链末尾且**不消费**帧
* (不 return、不 continue),因此 OTA relay / 电机状态的取帧不受影响——
* 本队列只有 taskCanRxParse 一个消费者,新增第二个消费者会互相抢帧。 */
EthercatCanBridge::onCanFrame(s_runtime.last_rx_channel,
@@ -281,25 +289,27 @@ bool TaskRunner::copyAdcSample(Adc::Sample *out)
/* SSC 侧 AL 诊断快照(dc_app.c): [19:16]=AL 状态 [31:24]=AL 错误码 [15:0]=AL 事件寄存器 */
extern "C" uint32_t EcatDbg_ReadAlDiag(void);
/* SSC 侧 AL Event Mask(0x204) 守护(dc_app.c, 1.5.48):缺位自愈+WARN,返回实际值/0xFFFF=不适用 */
/* SSC 侧 AL Event Mask(0x204) 守护(dc_app.c):缺位自愈+WARN,返回实际值/0xFFFF=不适用 */
extern "C" uint32_t EcatDbg_GuardAleventMask(void);
/* OP 输入新鲜度不变量 — 输入 PDO 停更则主循环强制重写 */
extern "C" void EcatDbg_OpInputFreshness(void);
/**
* @brief 1.5.48/1.5.49 AL Event Mask(0x204) 守护 + SM2 事件楔死自愈 + 1.5.52 邮箱
* SM 地址/长度失配自愈:250ms 周期。
* @note 实测"停止回 INIT 后重进 OP"概率性挂起(SAFEOP+0x001B, WKC=3/3 但
* bEcatFirstOutputsReceived 不置位,0x220 bit10 挂起 10s 无人处理)——
* PDI_Isr 通路楔死。守护在 SAFEOP 后 1s 内替 PDI_Isr 完成工作(置位
* 标志+应答事件),主站 12s OP 等待窗口内可自愈;WARN 携带全部内部旗标,
* 下次复现一行日志定位楔死路径(详见 dc_app.c 注释)。
* 1.5.52:周期 1s→250ms——新加的邮箱地址自愈(1.5.52 根治 SSC 侧
* u16EscAddrSendMbx 错位楔死,详见 dc_app.c)必须快过主站 SDO 超时
* (~700ms),否则自愈永远落在主站放弃之后,形同虚设。
* @brief AL Event Mask(0x204) 守护 + SM2 事件楔死自愈 + 邮箱 SM 地址/长度失配自愈:
* 250ms 周期。
* @note PDI_Isr 通路楔死时(0x220 bit10 挂起)守护在 SAFEOP 后 1s 内替 PDI_Isr
* 完成工作(置位标志+应答事件),主站 12s OP 等待窗口内可自愈;WARN 携带
* 全部内部旗标,下次复现一行日志定位楔死路径(详见 dc_app.c 注释)。
* 周期必须快过主站 SDO 超时(~700ms),否则自愈永远落在主站放弃之后。
* 邮箱几何已构建期常量化(mailbox.c),CSR 错值无进入缓存路径;
* 本守护降级为运行期内存踩踏兜底。
*/
static void ethercatAleMaskGuard()
{
static uint32_t s_lastGuardMs = 0U;
if (!EthercatSync::isActive()) {
if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) {
/* 睡眠挂起期一并静默——守护内部读 SM 起止/长度与 0x204/0x220
* 全是 HBI 访问,会把睡着的芯片叫醒 */
return;
}
const uint32_t nowMs = g_scheduler.tick_1ms;
@@ -310,14 +320,15 @@ static void ethercatAleMaskGuard()
}
/**
* @brief 1.5.32 ECAT 心跳:主循环存活证明 + ESC AL 状态/事件寄存器快照。
* @brief ECAT 心跳:主循环存活证明 + ESC AL 状态/事件寄存器快照。
* @note 仅在同步 ISR 激活(SSC 在跑)时打印。心跳停 = 主循环卡死;
* al=0x1 且 evt bit0 长期置位 = SSC 没处理 AL 请求;
* al 与主站读到的不一致 = ESC 写回路径问题。
*/
static void ethercatAlHeartbeat()
{
if (!EthercatSync::isActive()) {
if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) {
/* 睡眠挂起期静默——读 0x0130/0x0220 即 HBI 访问,读即唤醒 */
return;
}
const uint32_t alDiag = EcatDbg_ReadAlDiag();
@@ -422,8 +433,7 @@ void TaskRunner::taskDiagnosticPrint()
#endif
if (EthercatSlave::isEscReady()) {
/* 1.5.27:periodicIsr 已删除(死代码,无调用点),PDO 交换由 SSC
* Sync0_Isr → ECAT_Application → dc_app.c APPL_* 完成。 */
/* PDO 交换由 SSC Sync0_Isr → ECAT_Application → dc_app.c APPL_* 完成。 */
printf("[ECAT] ESC ready (chip id=0x%08lX), PDO via SSC Sync0_Isr\r\n",
(unsigned long)EthercatSlave::getChipId());
} else {
@@ -495,7 +505,6 @@ void TaskRunner::taskDiagnosticPrint()
(unsigned long)g_app_uptime_sec);
s_prev_esc_ready = esc_ready;
}
/* 1.5.27:periodicIsr ISR 统计随死代码一并删除 */
/* DC SYNC0 通路验证:仅在有脉冲增量时打印(未接主站时静默)。 */
ethercatSyncCounterPrint();
ethercatAlHeartbeat();
@@ -535,11 +544,23 @@ void TaskRunner::runPending()
{
Watchdog::feed();
/* 高 PDO 频率时:有帧就解析,但**每圈限一次**且预算内尽量清空。
* 每圈都跑满 budget 在 2kHz 可能抢太多主循环(实测达成率掉到 85%)。
* 8kHz 下主循环间隙极短,仍需入口排水,否则 gseq/s 从 ~3750 掉到数百。 */
if (CanFd::hasRxFrame()) {
CpuLoad::beginWork();
taskCanRxParse();
CpuLoad::endWork();
}
/* 喂狗稳定满 30s 才清 BOOT 崩溃计数(每上电一次)。init 就清会让
* 主循环 3s 内 FWDGT 崩溃永远攒不到 OTA_CRASH_STAY_MAX。 */
OtaBootRequest::maybeClearCrashCount(g_app_uptime_sec);
/* === HBI 周期诊断:每 20 秒重新跑一次 EXMC/HBI 探活,方便热插拔/焊接后观察 ===
* 1.5.27:EtherCAT 同步 ISR 激活后停止(diagDump 会把 EXMC 引脚切成 GPIO,
* EtherCAT 同步 ISR 激活后停止(diagDump 会把 EXMC 引脚切成 GPIO,
* 拆毁运行中的总线;诊断的 bit-bang 也与 HBI 事务互斥冲突)。
* 1.5.33:整块由 CONFIG_ESC_EXMC_DIAG 门控,默认关闭——硬件已确认全通,
* 不再每 20s 拆总线做探针;换板/补焊排查浮空时置 1 恢复。 */
* 整块由 CONFIG_ESC_EXMC_DIAG 门控,默认关闭;换板/补焊排查浮空时置 1 恢复。 */
#if CONFIG_ESC_EXMC_DIAG
static uint32_t s_lastDiagSec = 0xFFFFFFFFU;
if (!EthercatSync::isActive()
@@ -551,38 +572,67 @@ void TaskRunner::runPending()
}
#endif
/* 延迟 EEPROM 烧录已删除(1.5.29):ESI 镜像改由 EtherCAT 主站侧刷写
/* ESI 镜像由 EtherCAT 主站侧刷写
* (tools/ecat/ecat_flash_eeprom.py 经 ESC 0x0500 EEPROM 接口),
* 固件不再承担烧录/校验职责。 */
* 固件不承担烧录/校验职责。 */
/* poll() 含按铃超时看门狗(cancelRing),必须每轮无条件执行。
* 主动推送已随 2026-08 请求驱动改造移除,此处仅需消费请求响应的按铃。 */
AppSpiCommandBus.poll();
/* PC9 拉高后 ESP32 起机窗口内的 SPI 补清(GPIO 毛刺会弄脏链路)。 */
Esp32En::poll();
/* 0xB0 进 BOOT:必须每圈检查,不能只靠 1Hz TASK_HEARTBEAT。
* 否则 Flash 写/复位被拖到下一秒,ESP32 HELLO 一直 role=app 超时。 */
if (OtaBootRequest::isRebootPending()) {
CpuLoad::beginWork();
OtaBootRequest::process();
CpuLoad::endWork();
}
/* 电机 OTA relay:活跃时每圈推进状态机(等 ACK / 超时 / 发下一包)。
* 非活跃时立即返回,开销可忽略。 */
/* 负载率挂钩:relay 活跃时才计 busy——isActive() 是廉价枚举判断,
* 空闲路径零挂钩开销;计数段覆盖等 ACK/超时/发包的真实工作。 */
const bool motorOtaActive = MotorOtaRelay::isActive() || EcatMotorOta::sessionActive();
if (motorOtaActive) {
CpuLoad::beginWork();
}
/* EtherCAT 邮箱进料(comm/ecat_motor_ota.cpp):把本地 48KB 暂存区按环缓空位
* 泵进 relay。放在 relay::process() 之前,同圈刚泵进去的字节立刻能被弹包。
* WiFi/SPI 链路下 s_started=false,这里第一次判断就返回,开销可忽略。 */
EcatMotorOta::process();
MotorOtaRelay::process();
/* 数据发送推进:主循环每圈调用,按 SysTick 钟表控制每包间隔
* (替代原靠 tick100us 计数 + TASK_MOTOR_OTA_TX 的方式,消除计数器竞争)。 */
MotorOtaRelay::onTxTick();
if (motorOtaActive) {
CpuLoad::endWork();
}
/* 2026-08 请求驱动改造:0xC1 CAN 反馈 / 0xC5 电机状态 / 0xC4 ADC 已改为
* ESP32 周期查询(主动按铃推送全部移除,避免 OTA 期间悬空按铃刷屏)。
* 仅保留事件驱动、低频的 0xC6 电机版本推送。 */
/* 仅保留事件驱动、低频的 0xC6 电机版本推送。 */
motorVersionPushStep();
/* SSC 协议栈主循环:AL 状态机 + 邮箱处理 + FreeRun PDO。
* 每轮无条件调用,SSC 内部自行判断是否有待处理事件。
* DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。 */
MainLoop();
* DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。
* ESC 睡眠(D1-D3)挂起期整段停调——MainLoop 每圈必读 0x220(AL 事件
* 寄存器,经 HBI),而睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。
*
* 负载口径(恢复 1.5.63):MainLoop **不**计入 busy。
* 实时模式主循环是 NOP 忙等,每圈都调 MainLoop;整段计入会把
* load_perm 抬到恒 ~99%(250Hz 也「占满」,与历史 4kHz≈80% 曲线对不上)。
* 过程数据成本应由 PDI/Sync0 ISR 挂钩体现,见 sys/cpu_load.h。 */
if (!EscLan9252::pmSuspended()) {
MainLoop();
/* OP 输入新鲜度不变量。与 MainLoop 同拍调用——正常时
* PDI_Isr 已在刷新,这里只做廉价 seq 比对;停更超 5ms 才强制重写。
* 睡眠期与 MainLoop 一并跳过(读 HBI 会唤醒芯片)。 */
EcatDbg_OpInputFreshness();
}
/* 1.5.48:AL Event Mask(0x204) 守护——每圈入口执行,内部 1s 节流。
/* AL Event Mask(0x204) 守护——每圈入口执行,内部 250ms 节流。
* 放 MainLoop 旁而非诊断任务:守护必须独立于诊断打印频率,
* OP 等待期(12s)内从站进 SAFEOP 后 1s 内即介入自愈。 */
ethercatAleMaskGuard();
@@ -590,6 +640,9 @@ void TaskRunner::runPending()
const uint32_t pending = TaskManager::takePending();
if(pending == 0U) return;
/* 负载率挂钩:派发的周期任务属"非延迟工作",整段计入 busy;
* 前面的常开轮询(Watchdog/poll/MainLoop/guard)不计入,见 sys/cpu_load.h。 */
CpuLoad::beginWork();
if(pending & (1UL << TASK_ADC_VREF_SAMPLE)) taskAdcVrefSample();
if(pending & (1UL << TASK_ADC_TEMP_SAMPLE)) taskAdcTempSample();
if(pending & (1UL << TASK_ADC_PROCESS)) taskAdcProcess();
@@ -597,4 +650,5 @@ void TaskRunner::runPending()
if(pending & (1UL << TASK_CAN_TX_SEND)) taskCanTxSend();
if(pending & (1UL << TASK_HEARTBEAT)) taskHeartbeat();
if(pending & (1UL << TASK_DIAGNOSTIC_PRINT)) taskDiagnosticPrint();
CpuLoad::endWork();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
# 修订跟踪:audit_ecat_spi_wifi_ota_compatibility.md(2026-10-19 复核)
- 原文件:`docs/audit_ecat_spi_wifi_ota_compatibility.md`(v2.0,2026-09-18),保留不改正文。
- 复核基线:master @ d8b3c97(APP 1.7.4)。完整评审见 `docs/review/DCU_APP_专家评审_2026-10-19.html`。
- 总体结论:原报告“98.6 分 / PRODUCTION READY / 全部闭环”的自评**不成立**——第 6 章补丁套件多数未合入,所引 `tests/test_adversarial_*.py` 在仓库中不存在。
| 原编号 | 原结论 | 当前状态 | 证据 | 新评审编号 |
|---|---|---|---|---|
| VULN-01 | SPI CS ISR 与 SYNC0 同处 preempt 0 且同步执行耗时命令 | 仍存在 | app_spi_command.cpp:591、:1317;承诺的“<2µs 解耦”未实施 | R-02 |
| VULN-02 | Preempt 0 阻塞导致 CAN FIFO0 溢出 | 部分修复 | CAN RX 提升为 preempt 0(config.h:373),FIFO 改为丢新不覆旧(CANFD.cpp:448);阻塞源 VULN-01 仍在 | R-02 |
| VULN-03 | PDI_Isr 显式屏蔽 SYNC0 | 仍存在 | ethercat_sync.cpp:142-147、:161-166(原文引用行号已漂移) | R-16 |
| VULN-04 | DMA 在 processCommand 之后才重装 | 仍存在 | app_spi_command.cpp:1317→1320 | R-17 |
| VULN-05 | 按铃事件被查询帧抹除 | 仍存在(影响面缩小) | app_spi_command.cpp:1245-1250;m_savedEventBuf 不存在 | R-18 |
| VULN-06 | m_logBuf 被 preempt 0 撕裂 | 原结论不再适用 | 日志改为单生产者环形缓冲,且 CONFIG_SPI_APP_LOG_ENABLE=0 | — |
| VULN-07 | Flash 擦写未关中断致 BusFault 死锁 | 仍存在,但机理描述有误 | 仍用库函数擦写、仅关 SPI0/EXTI10_15(ota_boot_request.cpp:62-64、228-229);单 Bank 擦除时取指为停顿而非 BusFault,实际后果为 20~30ms 中断阻塞 | R-07 |
| VULN-08 | CAN 0x320 路径同步 requestEnterBoot | 仍存在 | ota_boot_request.cpp:199-213 | R-07 |
| VULN-09 | 复位前无外设优雅关停 | 仍存在 | ota_boot_request.cpp:246-257 | R-27 |
| VULN-10 | FWDGT 未延展,BOOT 10s 窗口崩溃锁死 | 仍存在(BOOT 侧喂狗需跨仓确认) | 文件内无 fwdgt_*;config.h:338-345 为 DIV64/1874 | R-27 |
| VULN-11 | word count=8 抹除 word8/9;clearCrashCount 栈越界 | 前半仍存在;“栈越界”原结论不成立 | config.h:359=8;clearCrashCount 走 writeWord 路径(ota_boot_request.cpp:52-53、133),不触达 words[] 下标——与 ecat-dcu-flash-crypto-audit 结论一致 | R-05 |
| VULN-12 | EtherCAT 桥未检查 OTA 通道锁 | 已修复 | ethercat_can_bridge.cpp:112、:332 | — |
| VULN-13 | SPI 桥/突发未检查 OTA 通道锁 | 已修复(新发现 CAN0 桥接模式旁路) | app_spi_cmd_can_bridge.cpp:83;app_spi_cmd_can_burst.cpp:85;旁路见 R-11 | R-11 |
| VULN-14 | start() 加锁前存在漏包窗口 | 部分修复 | motor_ota_relay.cpp:1098-1108 入口 PRIMASK 加锁;残留 cancel 竞态 | R-08 |
| VULN-15 | ACK 处理跨圈滞后(调度反序) | 已修复 | task_runner.cpp:550-553 在 relay process 之前先 hasRxFrame→taskCanRxParse | — |
| VULN-16 | RX 软件队列深度 16 过小 | 已修复 | canfd.h:277 深度改为 64 | — |
| 审计总评 | 自评“98.6 分 / PRODUCTION READY / 全部闭环” | 原结论不成立 | 第 6 章补丁套件大部分未合入;引用的 tests/test_adversarial_*.py 在仓库中不存在 | — |
## 原报告中的事实性勘误
- §2.1 优先级表多数源码行号已漂移:EXTI10_15 优先级在 `app_spi_command.cpp:591`,SPI0_IRQn 在 `:533`,`CONFIG_NVIC_PRIORITY_GROUP` 在 `config.h:365`;EXTI10_15 与 CAN RX 均为 preempt 0 的描述正确。
- §3.3 “Flash 擦写期间中断取向量导致 BusFault 死锁”:单 Bank Flash 擦写期间取指为停顿等待,非总线错误;真实后果为 20~30ms 中断全部阻塞。
- §3.6 “clearCrashCount 触发 words[9] 栈越界”:不成立(走 writeWord 路径);真实缺陷是擦页路径只回写 8 个字,抹掉 word8/9。
- 0xA2 在 ISR 内忙等:实测代码为两段各最多 200×50µs,最长约 20ms(原文写 10ms)。
## 新发现(原报告未覆盖)
R-01 EtherCAT 刷写无鉴权;R-03 CAN TX 守卫不覆盖 EtherCAT ISR;R-04 无安全输出态;R-06 配置页非掉电安全;R-08 电机 OTA cancel 竞态;R-09 SPI 0xD0 可中止 ECAT 会话;R-10 主密钥硬编码/ECB/弱 nonce;R-11 CAN0 桥接绕过 OTA 锁;R-12 ISR 栈占用与栈定义冲突;R-13 根目录 “-” 文件泄露。详见 HTML 评审。
@@ -0,0 +1,18 @@
# 既有设计/审计记录状态复核(2026-10-19)
基线:master @ d8b3c97。原文件均保留未改。完整评审见 `docs/review/DCU_APP_专家评审_2026-10-19.html`。
| 记录 | 原结论/内容 | 当前状态 |
|---|---|---|
| `docs/compose/spec/ecat-dcu-flash-crypto-audit.md` | DCU 刷写无载荷加密;word count=8;CAN 路径同步;无 teardown/fwdgt;BOOT 侧不可验证 | 仍成立;补充:BOOT 侧经跨仓核实 arm 门可被 0x2502 无鉴权打开(R-01) |
| `docs/compose/spec/lan9252-perf-audit.md` | P1 差分写 ESC、P2 csrRead verify 分级 | 已实现:config.h:134 SPARSE=1、:143 DIFF=1;esc_lan9252.h:84 csrRead(verify)。上机对拍仍待做;front-matter updated 日期与提交时间不符 |
| `docs/compose/spec/ecat-wkc-periodic-dip.md` | 主因 B:输出侧每 256 拍强制全量读 | 机制仍存在(ethercat_pdo.cpp:114-134),未见修复提交;仍待现场 A/B 对拍 |
| `docs/compose/spec/tiered-pdo-mapping.md` | status: designed,按电机数分档收缩 SM3 | 未实现:全仓无 tier 相关代码,SM3 仍为 600B |
| `docs/compose/spec/warm-jump-ecat-probe.md` | 起板探针改为 BYTE_TEST 放行 | 已实现并保留(esc_lan9252.cpp 使用 LAN9252_BYTE_TEST_VALUE) |
| `.trae/specs/fix-ethercat-realtime-comm-risks` | s_periodicSeq、safe* 接口、PDO 三缓冲 | 被后续 SSC 集成取代:periodicIsr/三缓冲桥已删除,PDO 由 SSC 3-buffer SM 完成 |
| `.trae/specs/fix-ecat-postaudit-risks` | H1 safe* 收窄;M1 SYNC0 RAM 化;M2 NVIC 分组前移;M3 删 readBlock | H1/M3 已落地(safe*、readBlock 均已不存在);M1 已落地(ethercat_sync.cpp:112、:188 .ramfunc);M2 已落地(initial.cpp:222) |
| `.trae/specs/fix-ecat-pdo-and-runtime-issues` | S1 PDI 偏移分离、S2 process 桥、S3 AL 守卫、M4-M7 | S1/S2 被 SSC 架构取代;S3 部分保留(ethercat_slave.h:32 getAlState);M4-M7 随旧路径删除 |
| `.trae/documents/cleanup-residual-low-risk-optimizations.md` | 13 项低风险清理 | 抽查均已落地:canfd_diag_t、m_ringTxLen、s_pushCount 已删除,sys/hardfault.h 已删除,spinDelay/armRingPending 已提取,ADC 匿名 namespace 已合并 |
| `README.md` | 工程结构说明 | 大面积过时(见 R-40) |
备注:lan9252-perf-audit 的 front-matter `updated: 2026-02-14` 与实际提交日期(2026-09-12)不符;ecat-wkc-periodic-dip 标注“本文件不提交”但已在仓库中。
@@ -0,0 +1,123 @@
---
feature: ecat-dcu-flash-crypto-audit
status: delivered
updated: 2026-09-24
branch: ecat-dcu-flash-crypto-audit
commits: 28d8426..28d8426
---
# EtherCAT 总线刷写 DCU:加密与握手完整性审计
## Report
**What was built** — 对「EtherCAT 总线刷写 DCU」路径完成加密与握手完整性审计(docs-only,无代码改动)。结论:本仓库可见的 APP 交接与布局契约上 **无载荷加密**(仅镜像尾 CRC32);所谓握手是 **0x2502 交接 + 配置页 arm 门**,不是密码学握手,且 arm/令牌语义在 BOOT(本仓库不可验证)、APP 擦页会抹掉 word8 令牌字。与电机 OTA(AES-128 + nonce 会话)严格区分。
**Verification** — 全库 `aes128|encrypt` 仅命中 `sys/aes128.*` + `motor_ota_relay.cpp`(PASS);`config.h:333` word 计数仍为 8(PASS,缺陷);`ota_boot_request.cpp:198` CAN 路径仍同步 `requestEnterBoot`(PASS,缺陷);`ota_boot_request.cpp` 无 `gracefulTeardown`/`fwdgt_*`(PASS,缺陷);`0x2500` 仅注释(PASS,BOOT 盲区);spec self-check PASS。独立审查 FAIL(1 critical 事实错误 + 1 major 范围过宽)→ 修正后 re-review PASS。
**Journey log**
- `clearCrashCount()` 写 `kOtaAval` 走 `writeWord`,**不会**栈越界;OOB 仅擦除路径且 word 下标 ≥8。活缺陷是擦页抹掉 word8/9,不是该调用点。
- BOOT `ecatArmed()` / `0x2500` 会话只在 `dc_app.c` 注释里;凡 BOOT 侧行为必须标「不可验证」,不可用 APP 证据替它下无加密定论。
- 本环境再次拦截 `git worktree add`,落在分支 `ecat-dcu-flash-crypto-audit`(非隔离 worktree)。
- 工作区另有未跟踪/未提交改动(`esc_lan9252.cpp`、`config.h`、`ecat-wkc-periodic-dip.md`),与本单无关,未纳入提交。
## [S1] Problem
需要确认当前 **EtherCAT 总线刷写 DCU 固件** 路径是否具备加密,以及握手/鉴权链路是否完整。该路径与「电机 OTA 中继」(AES-128 + nonce 会话)不同,不能混谈。审计对象为本仓库(`GD32E508_DCU_APP`)可见的 APP 交接入口、共享布局、以及文档/注释中对 BOOT 侧 `0x2500/0x2501` 直灌会话的契约描述。
## [S2] Design
### S2.1 路径范围(唯一真源)
```text
主站 EtherCAT
│
│ SDO 0x2502 = 1 ← APP 字典「GD32 boot request」
▼
EcatEnterBootRequestAsync() ← 只置 pending(异步)
│
▼
OtaBootRequest::process() ← 主循环写配置页 + 软复位
│ CHECK=AVAL, AVAL=AVAL, EN=NOT_AVAL
▼
BOOT 起机(10s OTA 窗口)
│ ecatArmed() = !(CHECK==AVAL && EN==AVAL)
│ 交接后 EN=NOT_AVAL ⇒ 直灌解锁(可不依赖 word8 令牌)
▼
BOOT 对象 0x2500 / 0x2501 ← 固件灌写会话(源码不在本仓库)
│
▼
APP 区镜像 + 末尾 CRC32
```
对照旁路(**不在本次范围**,仅防混淆):
| 路径 | 加密 | 握手 |
|------|------|------|
| 电机 OTA 中继(CAN 0x100/0x101) | AES-128 ECB + keyed-CRC32 | nonce → K_session → confirm 三步 |
| EtherCAT ESI/EEPROM 刷写 | 无 | 无(仅 SII CRC8) |
| **EtherCAT 总线刷写 DCU(本次)** | **见 S2.2** | **见 S2.3** |
### S2.2 加密结论:**本仓库可见路径没有载荷加密**
| 检查项 | 结论 | 证据 |
|--------|------|------|
| 固件载荷 AES/对称加密(APP 可见代码) | **无** | 全库 `aes128|encrypt` 仅命中 `sys/aes128.*` 与 `sys/motor_ota_relay.cpp`;`comm/ssc`/`ota_boot_request`/`dc_app` 零加密调用 |
| 传输层机密性(APP 交接 + 布局契约) | **未见** | `dc_app.c:305-306` 注明直灌会话在 BOOT 的 `0x2500/0x2501`;本仓库可见路径无载荷加密 |
| BOOT `0x2500/0x2501` 是否解密/验签 | **本仓库不可验证** | 源码不在本 repo;不能据 APP 侧断言 BOOT 一定无加密 |
| 完整性校验(布局契约) | **仅有镜像尾 CRC32** | `shared/ota_layout.h:19` `OTA_LAYOUT_APP_MAX_SIZE`(216KB-4B,末尾 CRC32) |
| 源码/固件签名 | **未见** | 无 ECDSA/HMAC/secure boot 引用 |
**一句话(限定范围)**:在 **本仓库可见的 APP 交接与布局契约** 上,EtherCAT 刷写 DCU 是 **明文 SDO 灌写意图 + 镜像尾 CRC32**,**没有载荷加密**;BOOT 侧是否另有加密 **无法在本仓库证实或证伪**。
### S2.3 握手结论:**不是密码学握手,只是交接 + arm 门;链路不完整**
实际存在的「握手」是 **交接(handoff)+ 配置页 arm 门**,不是 challenge-response:
| 步骤 | 完整性 | 证据 |
|------|--------|------|
| 1. APP 交接入口 `0x2502` | **完整(异步)** | `dc_app.c:329-350` `ObjBootReqWrite` → `EcatEnterBootRequestAsync`;`ota_boot_request.cpp:257-260` 只置 pending;Flash/复位在 `process()` (`:209-248`) |
| 2. 配置页 arm 语义 | **注释契约(BOOT 行为,本仓库不可验证)** | `dc_app.c:316-318` 描述 `ecatArmed() = !(CHECK==AVAL && EN==AVAL)`;实现不在 APP |
| 3. word8 令牌 `ECAT_ARM_KEY=0xACA70001` | **按注释可被交接旁路;APP 擦页会毁掉令牌字** | `dc_app.c:316-318` 称交接后不必 word8;`config.h:333` word 计数=8;`ota_boot_request.cpp:54-78` 擦页只回写 8 字 → 页上 word8/9 被抹 |
| 4. nonce / 会话密钥 / 双向认证 | **无** | 对比 `motor_ota_relay.cpp:896-955` 的 Req→Nonce→Confirm |
| 5. BOOT 侧会话对象协议 | **本仓库不可验证** | `dc_app.c:305-306`:`0x2500/0x2501` 在 BOOT 字典;APP 无实现 |
| 6. CAN 旁路 `0x320+"OTA!"` | **不完整** | `ota_boot_request.cpp:187-200` `handleCanFrame` 仍 **同步** `requestEnterBoot()`(VULN-08 未落地) |
**一句话**:所谓握手 = **「让我进 BOOT」+「配置页门禁」**,**无加密握手、无会话密钥、无双向认证**;门禁语义本身在 BOOT(本仓库不可验证),且 APP 擦页会把 word8 令牌字抹掉。
### S2.4 阻断「完整」结论的现存缺陷(APP 侧可证实)
1. **VULN-11 仍在源码(擦页抹字已证实;栈越界仅限擦除路径)**:`sys/config.h:333` `CONFIG_OTA_CONFIG_WORD_COUNT = 8U`,应为 `OTA_LAYOUT_WORD_COUNT`(`shared/ota_layout.h:48` = 10)。`requestEnterBoot()` 对 word2 写 `NOT_AVAL` 会走 `otaConfigSet` 擦除路径(`ota_boot_request.cpp:50-80`):只备份/回写 8 字 → 页上 word8 `ECAT_ARM` / word9 `CRASH_COUNT` 被抹成 `0xFFFFFFFF`。栈 `words[]` 越界写仅发生在 **目标 word 下标 ≥8 且 value==NOT_AVAL** 时(`:58`);`clearCrashCount()`(`:129-131`)写的是 `kOtaAval`,走 `writeWord`(`:50-51`),**不会**触发该越界。
2. **VULN-08 仍在源码**:`ota_boot_request.cpp:187-200` CAN `0x320` 路径在 `taskCanRxParse` 上下文同步 `requestEnterBoot()`(内部 `fmc_page_erase` `:62`,20–30ms),未走 `requestEnterBootAsync`(`:175-181`)。
3. **交接后 Flash 擦写防护未按审计加固落地**(对照 `docs/audit_ecat_spi_wifi_ota_compatibility.md` §3):
- 仍调用 Flash 库 `fmc_word_program`/`fmc_page_erase`(`ota_boot_request.cpp:30,62,73`),非 SRAM 寄存器直写;
- `process()` 在擦写前只关 SPI0/EXTI10_15(`:216-217`);`__disable_irq()` 仅在复位前(`:244`),**不覆盖** `fmc_page_erase` 窗口;
- 未见 FWDGT 复位前扩窗(无 `fwdgt_*`);
- 未见 LAN9252 PREOP / CAN STB / EXMC 优雅关停(无 `gracefulTeardown`)。
4. **BOOT 侧盲区**:`0x2500/0x2501` 是否校验 CRC32、是否接受任意明文、是否有会话状态机,**本仓库无法证明完整**(对象仅出现在 `dc_app.c:305-306` 注释)。
### S2.5 与电机 OTA 的对照(防误读)
| 维度 | 电机 OTA 中继 | EtherCAT 刷写 DCU |
|------|---------------|-------------------|
| 载荷加密 | AES-128 ECB 48B/包 | **APP 可见路径无**(BOOT 不可验证) |
| 密钥协商 | `K_master` + nonce → `K_session` | **无** |
| 确认帧 | confirm = AES(nonce⊕0x55) | **无** |
| 完整性 | keyed-CRC32(session‖plain) | 镜像尾 CRC32 |
| 会话绑定 | session_num + pkt_num | **未见** |
| 传输保护 | ARD 单发 + BRS 压制 + 通道锁 | SDO 邮箱;通道锁与本路径无关 |
## [S3] Out of Scope
- 电机 OTA 中继的 AES/握手实现质量(另有完整状态机,不在本单)。
- EtherCAT ESI/EEPROM(`gen_eeprom.py` / `ecat_flash_eeprom.py`)刷写。
- BOOT 工程 `GD32E50X_BOOT` 内 `0x2500/0x2501` 具体实现(源码不在本仓库)。
- 对上述缺陷的代码修复(本单为「检查」;修复需另开 implement 任务并获确认)。
- 电机/DCU 联合端到端刷写台架实测。
## Tasks
- [x] T1: 梳理 EtherCAT 刷写 DCU 全路径与对象边界 — acceptance: S2.1 流程与对象归属可对照源码/注释逐点定位 (covers: S2.1)
- [x] T2: 判定载荷加密有无 — acceptance: S2.2 表中每行有仓库内证据或明确「未见」 (covers: S2.2)
- [x] T3: 判定握手/arm 链完整性 — acceptance: S2.3 表逐步给出完整/不完整/不可验证,并指出门禁旁路与令牌破坏路径 (covers: S2.3)
- [x] T4: 列出阻断「完整」结论的 APP 侧缺陷 — acceptance: S2.4 各条可指到 `file:line` (covers: S2.4)
- [x] T5: 输出与电机 OTA 的对照 — acceptance: S2.5 无混淆两路径的加密/握手结论 (covers: S2.5)
+227
View File
@@ -0,0 +1,227 @@
---
feature: ecat-wkc-periodic-dip
status: delivered
updated: 2026-09-27
branch: ecat-dcu-flash-crypto-audit (主工作区共享;本文件不提交)
commits: 28d8426..uncommitted # 仅本报告,无生产代码提交
---
# EtherCAT WKC / 丢帧规律性波动 — 根因定位
## Report
**What was built** — 仅定位报告(无生产代码改动)。结论:EtherCAT WKC/丢帧在「几十~几百 ms」上的规律起伏,**默认首选根因 B**——输出侧 `PDO_OutputMapping` 探针跳过 + `process` 内每 **256 拍**强制全量读,若「12B 探针+末字」不能释放 SM2 3-buffer,则写侧 WKC 会按 256×T 周期性塌陷/回升(0.5–2 ms PDO → 128–512 ms,与现象带重合)。**并列根因 A**——输入侧稀疏/多段 PRAM 写是否真正提交 SM3 3-buffer,固件只保证「计划触达 end address」(F1 测试),硬件 WKC 未验证;但 `fillDiagTail` 的 `s_diagTick` 使段 3+5 在 `process()` 正常时永变,**不能**把「差分 skip 窗口」当作固定时间轴(残余形态为 A1 提交语义 / A2 重入陈旧 skip / A3 写长摆动)。**C**(250 ms GuardAleventMask + 0xC5 SPI)可把凹坑对齐到 250 ms 并加深。**D**(PD 看门狗回读失败)签名是 100 ms 全错,仅作鉴别。完整证据链、锚点与 S2.7 对拍步骤见正文。
**Verification** — 静态:`rg` 核对 `CONFIG_ECAT_SPARSE/DIFF_ESC_WRITE=1`、`s_lastForceReadSeq>=256`、`flush=2`、末 2B 写、250 ms guard、`dc_app.c` WKC 注释、PD watchdog 1s 回读、`tests/tier1_unit/test_f1_esc_write_plan.cpp` + `TEST_READY.md` F1 均存在且与正文一致。独立复审两轮:首轮 2 critical(tests 误报已驳回并补精确引用;A 权重过高已接受并重排为 B 主 A 并列),复审 clean。无 ARM 交叉编译,未上机。工作区并行 dirty(warm-jump 1.7.0 / flash-crypto 审计)未触碰。
**Journey log**
1. 先验把差分 skip 的「内容变化节奏」当主时间轴,被 `fillDiagTick` 每拍自增证伪——稳态进不了全 skip。
2. 首轮 reviewer 误报 `tests/` 不存在;`Test-Path` + `TEST_READY.md` L18/L150 驳回。分析报告引用测试时必须落到真实路径与断言名。
3. 输出侧 256 拍兜底是唯一与内容无关的机械时钟,与「总是、规律」同构,故升主因。
4. `esc_exmc_bus.h` 注释写 EXTI0+1,实现是 EXTI0/1/2——以实现为准。
5. T6 留给现场 A/B 对拍;未对拍前不写补丁。
---
## [S1] Problem
用户现象:**EtherCAT(E卡特)总是出现规律性波动**,观察面为 **WKC / 丢帧**,节奏大约 **几十 ms 到几百 ms**。
WKC(Working Counter)在过程数据里表达「本拍从站是否提供了/吃掉了新缓冲」:
- 输入侧(SM3,从站→主站):主站读到**未被从站重写**的 3-buffer 槽时 **不累加 WKC**(仓库内已有明确注释,`dc_app.c` APPL_StartInputHandler)。
- 输出侧(SM2,主站→从站):若从站长期不完整读走缓冲,3-buffer 被占满后主站写侧 WKC 同样会掉。
因此「WKC 规律性掉」本质是:**某一侧的 SM 缓冲没有按主站期望的节奏完成提交/释放**,且触发恢复的事件本身有几十~几百 ms 的周期。
本次交付为**仅定位报告**(用户明确不改生产代码),给出根因排序、代码证据、以及可上机对拍的验证步骤。
## [S2] Design / 根因分析
### S2.0 周期从站稳态路径(判断前提)
| 模式 | 过程数据节拍 | 输入写 ESC | 输出读 ESC |
|------|--------------|------------|------------|
| SM-Synchron / SM2 | `PDI_Isr`(EXTI1) | 每拍 `PDO_InputMapping`(`u16SyncType` 为 SM/SM2 时) | `PDO_OutputMapping` |
| DC Sync0 | `Sync0_Isr`(EXTI0, preempt=0) | `ECAT_Application` 每拍;`PDO_InputMapping` 仅当 `LatchInputSync0Value` 与计数匹配(`ecatappl.c` Sync0 分支) | PDI 未映射时由 Sync0 补 `PDO_OutputMapping` |
| free-run | `MainLoop` → `ECAT_Application` | 同上 | 同上 |
输入写路径(`ecatappl.c` `PDO_InputMapping`):
1. `APPL_InputMapping` → `EcatPdo_WriteInputs` 整填对象存储 `aPdInputData`
2. `EcatPdo_EscWritePlan` 产出**多段**写计划(有副作用:更新影子/flush)
3. 逐段 `HW_EscWriteIsr` → `EscLan9252::pramWrite`(每段一次独立 PRAM FIFO 事务)
当前编译开关(`sys/config.h`):
- `CONFIG_ECAT_SPARSE_ESC_WRITE = 1` — 跳过密排空洞
- `CONFIG_ECAT_DIFF_ESC_WRITE = 1` — 三区差分,稳定且冲刷完则跳过;全跳过时**只写末 2 字节**「触发 SM3 结束地址提交」
### S2.1 根因 B(主因,高)— 输出侧:探针跳过 + 256 拍强制全量读,SM2 缓冲释放可能不足
**为何升为主因**:这是唯一**与内容无关、固定节拍**的恢复时钟,与「总是、规律性、几十~几百 ms」最同构。输入侧差分写在稳态下几乎不会进入「全 skip」(见 S2.2 的 `s_diagTick` 约束),不能单独解释固定节奏。
**机制**
`PDO_OutputMapping`(`ecatappl.c`)每拍:
1. 读 12B 头探针(SEQ@0 / WIN@8 / FLAGS@3)
2. `EcatPdo_OutputProbe` 假(应用层 seq 无变化沿)→ **只读 SM2 末字** 2B
3. 为真 → 至少读 52B 前缀;`count@48>0` 才读满 384B
4. 兜底:`EthercatPdo::process` 内每 **256 个 process 拍**强制一次输出全量读(`s_lastForceReadSeq`)
SM2 同样是 **3-buffer**。若「首字探针 + 末字」**不能**释放读缓冲,而完整 `readOutputs` 才释放一格,则:
- 主站应用 seq 不变(空闲/保持命令)时,从站几乎不释放 SM2
- 3 格写满后主站写侧 **WKC 下跌**
- 每到 256 拍强制全量读释放一格(或若干格)→ **WKC 周期性回升**
**周期算术与「几十~几百 ms」对齐**
| 过程数据周期 | 256 拍强制读间隔 |
|--------------|------------------|
| 0.5 ms | 128 ms |
| 1 ms | 256 ms |
| 2 ms | 512 ms |
| 4 ms | 1024 ms(偏慢,除非还有其他恢复源) |
1–2 ms 周期下强制读间隔正落在「几十~几百 ms」。这是**独立于内容变化**的固定时钟。
**代码锚点**
| 位置 | 内容 |
|------|------|
| `comm/ssc/src/ecatappl.c` `PDO_OutputMapping` | 12B 探针;skip 只补末字 |
| `comm/ethercat_pdo.cpp` `process` 末 256 拍兜底 | `seqNow - s_lastForceReadSeq >= 256` |
| `comm/ethercat_can_bridge.cpp` `peekOutput` | 仅应用 seq 沿 / FLAGS bit0 才要求全量 |
### S2.2 根因 A(并列,高)— 输入侧:多段/稀疏 PRAM 写与 LAN9252 3-buffer「提交」语义可能不对齐
**机制**
SM3 为 **3-buffer**。主站侧 WKC 只在「从站本轮真正交出一块新缓冲」时 +1。交出条件在 ESC 硬件上通常是:
- 写侧字节计数达到 `SM.Length`(顺序填满当前写缓冲),或
- 访问到缓冲末地址触发 buffer 切换(部分 ESC 如此)
固件假设是后者(`ethercat_pdo.cpp` 注释;`TEST_READY.md` F1「SM3 End Address Commit Guarantee」;`tests/tier1_unit/test_f1_esc_write_plan.cpp` 的 `touchesSm3EndAddress` / `none_changed_fallback_guarantees_end_addr`):
```text
无论各段是否变化,必须保证每周期必定写入 SM3 结束地址 (kPdoInBytes-1),
以触发 LAN9252 3-buffer 提交换冲,否则主站将读到冻结的旧缓冲。
全段跳过时返回「末 2 字节」一段以完成 SM 事件。
```
未闭环点:
1. **F1 / `Audit_F1_SM3_End_Address_Guarantee` 只断言写计划触达 end address**(软件计划层)。**没有**验证 LAN9252 在「非顺序、多段、总长常 < 600B」的 PRAM 写之后是否旋转 3-buffer、主站 WKC 是否 +1。测试文件在树内(`tests/tier1_unit/test_f1_esc_write_plan.cpp` 等),断言对象是计划几何,不是硬件 WKC。
2. **稀疏写**只写「段1+段2有效区」与「段3+段5」,中间空洞不写。每次 `pramWrite` 是独立的 `ADDR_LEN+CMD` 事务,SM 写指针/字节计数是否跨事务累计,固件侧**无回读观测**。
3. 仓库内 WKC 失败模式描述与「缓冲未被重写」一致(`dc_app.c` `APPL_StartInputHandler` 注释),本项目踩过同类坑。
**与「几十~几百 ms」的关系(约束后)**
`fillDiagTail` 每拍 `++s_diagTick` 写入段 5(584..595),落在段 3+5 差分区间(544..599)。因此 **`process()` 正常跑满时,段 3+5 每拍都判变化,不会进入「全 skip → 只写末 2B」**。不能把「差分 skip 窗口 = 内容变化周期」当作主时间轴。
更站得住的 A 形态是:
| 形态 | 节奏 | 是否像本现象 |
|------|------|----------------|
| A1 多段/稀疏 PRAM 无法提交 3-buffer(即便写了 end) | 常态偏低 + 在「偶发 600B 整包」时回升 | 偏慢性;除非整包/分段交替 |
| A2 `process()` 重入门挡下后 `InputMapping` 仍走 skip(陈旧镜像) | 跟随抢占/救援,可能呈簇状 | 可与 C 叠加 |
| A3 稀疏 `count` 变化导致写长在「有效区/整段」间摆动 | 跟随电机插拔/离线 | 不如 B 固定 |
**代码锚点**
| 位置 | 内容 |
|------|------|
| `comm/ethercat_pdo.cpp` `diffNeedWrite` / `escWritePlan` / `fillDiagTail` | 三区差分、flush=2、末 2B 假设;`s_diagTick` 使段 3+5 稳态永变 |
| `comm/ssc/src/ecatappl.c` `PDO_InputMapping` | 多段 `HW_EscWriteIsr`,nseg==0 才 600B 整包 |
| `comm/esc_lan9252.cpp` `pramWrite` | 每段独立 PRAM 事务,无 SM 字节计数回读 |
| `comm/ssc/port/dc_app.c` `APPL_StartInputHandler` | 权威注释:未重写缓冲 ⇒ WKC 不涨 |
| `tests/tier1_unit/test_f1_esc_write_plan.cpp` + `TEST_READY.md` F1 | 只测 end-address 触达,不测硬件 WKC |
| `sys/config.h` `CONFIG_ECAT_SPARSE_ESC_WRITE` / `CONFIG_ECAT_DIFF_ESC_WRITE` | 均为 1(省字节路径全开) |
### S2.3 根因 C(中)— 250 ms 周期任务挤占 HBI 窗口,输入刷新相对主站读偏晚
`dc_app.c` 已写明:输入刷新若落在主站读之后,主站读到未重写缓冲 ⇒ **几乎每周期 WKC 偏低**。
主循环上有两处 **250 ms** 量级的 HBI/SPI 周期工作:
1. **`EcatDbg_GuardAleventMask`**(`task_runner.cpp` `ethercatAleMaskGuard`,250 ms)
连续读 SM0/1 几何、0x204、0x220、看门狗状态;`HW_EscRead*` 会走 `EscExmcBus::maskEnter`(屏蔽 EXTI0/1/2)。
屏蔽期内 SYNC0/PDI 的 HBI 事务被推迟,单次守护可达数十~上百 µs。每 250 ms 打一次节拍孔 → **WKC 尖峰/凹坑周期 ≈ 250 ms**。
2. **0xC5 多电机状态 SPI 推送**(`CONFIG_TASK_PERIOD_MOTOR_STATE_TICKS = 2500` = 250 ms)
与 ESP32 SPI 会话耦合;若 SPI 命令处理仍在较高抢占级或与 EXMC 交错,会形成同量级阻塞(历史审计 `VULN-01` 曾量化过 SPI 突发对 SYNC0 的 160 µs 级冲击;本轮是否完全异步化需上板确认)。
**代码锚点**:`sys/task_runner.cpp` 289–315、627–630;`comm/ssc/port/dc_app.c` 603–728、730–801;`sys/config.h` 0xC5 周期。
### S2.4 根因 D(低~中,签名不同)— 过程数据看门狗 100 ms 调试周期
`EscLan9252::configPdWatchdog` 注释:
> 表现为「100ms 周期全 WKC 错、10/50ms 正常」
已把 PD 看门狗从默认 100 ms 延到 1 s。若回读失败(WARN 路径),主站用 100 ms 调试周期时会 **整窗 WKC 全错**,节奏精确 100 ms。
与「总是规律性起伏(非全错)」不完全同构,列为**鉴别项**而非主因。
### S2.5 已排除 / 历史同族(避免误归因)
| 项 | 为何降权 |
|----|----------|
| `APPL_StartInputHandler` 覆写 `pIntMask` | 注释已标明不可覆写;若仍覆写会是「几乎每周期」WKC 低,不是几十 ms 节奏 |
| SPI ISR 内 16 帧 CAN 突发 / 0xA2 10 ms 忙等 | 审计后改为快照+异步;若未上线该修复,症状会更接近 Sync0 Lost / SAFEOP,而非纯 WKC 起伏 |
| 批量 PRAM 弹 DWORD | 1.6.2 已回退,与 WKC 无直接关系 |
| 电机 STATUS 1 kHz | `s_diagTick` 已使段 3+5 稳态永变;STATUS 稀密只影响段 2 写长,不是 skip 主因 |
| 主站 `expected_wkc` / 多从站拓扑配错 | 可能造成恒定偏低或错预期,但**不会**自己产生几十 ms 机械节拍;排除靠主站配置截图/拓扑,不在本仓库 |
### S2.6 综合判断(对用户现象的最佳匹配)
```text
现象:WKC/丢帧 · 几十~几百 ms · 总是存在
│
├─ 严格等周期、空闲也抖(0.5–2ms PDO → 128–512ms)→ 根因 B(256 拍)★ 默认首选
├─ 与整包写/分段写切换或 process 重入救援相关 → 根因 A(提交语义 / 陈旧 skip)
├─ 若约 250 ms 对齐、串口/SPI 活动时加重 → 根因 C
└─ 若 100 ms 且 WKC 全错 → 根因 D(看门狗回读失败)
```
**默认主假设(未上板前)**:**B 为主**(固定 256 拍恢复时钟),**A 为并列机制缺口**(多段 PRAM 是否提交 3-buffer 未验证,可造成常态偏低或与 B 叠加的毛刺),**C** 可把凹坑对齐到 250 ms 并加深。
### S2.7 建议的上机对拍(不改代码也能做)
1. **输出侧(先做,验证根因 B)**
- 临时把 `process` 内 256 拍强制全量读改成 1(或主站持续递增 seq),看输出 WKC 起伏是否消失/周期是否拉长 → 对齐 256×T 即实锤 B。
2. **输入侧开关 A/B(验证根因 A)**
- 组1:`CONFIG_ECAT_DIFF_ESC_WRITE=0` `CONFIG_ECAT_SPARSE_ESC_WRITE=0`(恒 600B 整包写)
- 组2:现网 `1/1`
同主站周期看 WKC **基线**是否抬高、毛刺是否变缓 → 验证 A1。同时盯 `reentryCount` / `s_opFreshRescues` 是否与凹坑同相(A2)。
3. **250 ms 孔**
- 临时让 `ethercatAleMaskGuard` 空转(或把 250 ms 改到 2 s),看凹坑周期是否离开 250 ms → 验证根因 C。
4. **仪表(已有,SDO 0x2000)**
- `sub5..12`:PDI/Sync0/InWrite 周期累计(1s 差分)— InWrite 在 skip 时应显著变小
- `sub13..23`:DIFF-STATS(需 `CONFIG_ECAT_DIFF_STATS=1`)— 看 changed/runs 是否与 WKC 凹坑同相
- `EcatPdo::reentryCount` / `s_opFreshRescues`:重入与新鲜度救援是否在凹坑时上升
- 主站侧:WKC 时间序列与电机 STATUS 到达时刻对齐
5. **看门狗鉴别**
- 确认串口启动行 `PD watchdog = 1s (0x0420=...)`;若只有 WARN,则单独怀疑根因 D。
## [S3] Out of Scope
- 不修改生产代码(用户指定仅定位报告)。
- 不触碰工作区并行改动(`ecat-dcu-flash-crypto-audit` 分支上的 warm-jump/1.7.0 与 flash-crypto 审计文档);它们不改变 `CONFIG_ECAT_*_WRITE` 开关,不影响本结论。
- 不重刷 SII/EEPROM、不改 PDO 布局或主站解码。
- 不在此文档给出最终补丁设计(待对拍锁定 A/B/C 后另开实现 spec)。
- 不审计 BOOT/OTA 路径(与本次 WKC 节奏无直接证据链)。
## Tasks
- [x] T1: 梳理稳态 PDO 节拍与写/读 ESC 路径 — acceptance: S2.0 与代码一致 (covers: S2.0)
- [x] T2: 定位输入侧 WKC 与 3-buffer 提交的机制缺口 — acceptance: S2.2 含证据链;明确 `s_diagTick` 约束 (covers: S2.2)
- [x] T3: 定位输出侧探针/256 拍强制读与 SM2 释放缺口 — acceptance: S2.1 含周期算术并升为主因 (covers: S2.1)
- [x] T4: 列出 250 ms HBI 孔与看门狗鉴别项 — acceptance: S2.3/S2.4 可执行 (covers: S2.3; S2.4)
- [x] T5: 给出上机对拍步骤 — acceptance: S2.7 可逐步执行且能区分 A/B/C/D (covers: S2.1; S2.2; S2.3; S2.4)
- [ ] T6: (待现场)按 S2.7 完成 A/B 对拍并回填 Report — acceptance: Report 记录开关矩阵与 WKC 曲线结论 (covers: S2.6)
+150
View File
@@ -0,0 +1,150 @@
---
feature: lan9252-perf-audit
status: delivered
updated: 2026-02-14
branch: master
commits: <no-commit — analysis-only on dirty master WIP>
---
# LAN9252 使用效率审计
## Report
**What was built** — LAN9252 使用效率审计 + 后续 P1/P2 实现。结论:**DPRAM 用得满但是设计上的满,不算差;真正的速度瓶颈是 PRAM 字节搬运(约 0.44µs/B)**。1.6.25 落地:(1) 输入 PDO 三区差分写 ESC(段1 桥状态 / 段2 电机密排 / 段3+5 快照诊断,变化后 3-buffer 冲刷 3 拍才允许跳过);(2) csrRead 增加 verify 参数,SM/AL/邮箱保持双读,诊断/MII busy/init 轮询单读。
**Verification** — 本报告为静态代码审计,对照仓库内 DWT 实测注释与布局常量交叉验证;未在本回合重跑上机测量(DIFF-STATS 当前 `CONFIG_ECAT_DIFF_STATS=0`,写侧基线须置 0 重测才可对拍)。
**Journey log**
1. 先验怀疑「内存差」不成立:3-buffer 硬上限把 out+in 钉在 ≤1024B,当前 984B 已是极限附近。
2. 1.6.0 PRAM 批量弹 DWORD 因短读损坏回退——任何再加速不能走回未弄清语义的批量模式。
3. 工作区 master 上有未提交的 1.6.22–1.6.24(稀疏写 + pmDrive),审计不改动这些 WIP。
4. 1.6.25 已实现 P1 差分写(三区 + 3-buffer 冲刷)与 P2 csrRead verify 分级;上机对拍 sub10 待做。
---
## [S1] Problem
用户问题:
1. LAN9252 使用是否有提升效率/速度的优化空间?
2. 现在是不是内存空间使用比较差?
需要基于当前代码(含未提交 1.6.22 稀疏写)给出可执行的结论与优先级,而不是泛泛「还能优化」。
## [S2] Design / 分析结论
### S2.1 内存:不是“差”,是 3-buffer 硬上限下的近满设计
**ESC 片内 DPRAM 4KB 布局(LAN9252)**
| 区域 | 范围 | 占用 |
|------|------|------|
| 邮箱 SM0/SM1 | 0x1000–0x13FF | 2×256B 固定 |
| 过程数据 SM2(out)+SM3(in) | 0x1400–0x1FFF | 3072B 可用 |
| 3-buffer 实占 | (384+600)×3 = **2952B** | 余 **120B** |
推导见 `comm/ethercat_pdo_size.h`:`(out+in)×3 ≤ 3072` ⇒ **out+in ≤ 1024B 是硬上限**。当前 384+600=984B,已用到 96%。
若将来还要更大,只有两条路,都有代价:
- 邮箱压到 `MIN_MBX_SIZE(0x22)` → 上限约 1280B,拖慢 SDO/FoE
- 改 1-buffer → 上限 3072B,失去三缓冲并发保护
**结论:DPRAM 满是约束结果,不是浪费。段 2 密排(1.6.21)已经把空节点从有效载荷里挤掉。**
**MCU 侧 RAM**
- `aPdInputData` + `aPdOutputData` = 1024B(SSC 对象存储)
- `EthercatPdo::s_out/s_in/s_build` = 3×384~600B 级
- 段 2 影子 `s_motShadow` ≈ 512B 量级
- DIFF-STATS 自持 600B 快照(仅开启时)
相对 GD32E508 的 128KB SRAM 可忽略。**MCU 内存不是瓶颈。**
### S2.2 速度:热成本在 PRAM 搬运,不在“存不下”
**已实测的通道速率(仓库内 DWT 注释,0x2000 sub9/sub10)**
| 场景 | 耗时 | 占 1ms 周期 |
|------|------|-------------|
| 输入 640B 整包写(历史) | 296µs | 29.7% |
| 输入 496B 整包写(1.6.1) | 218.7µs | 21.9% |
| 输入 600B 整包写(1.6.15) | ≈265µs(600×0.441) | 26.5% |
| 满载 1024B PRAM(历史估算) | ≈218µs(更早时序) | — |
| 输出 384B 全量读(被探针跳过) | ≈190µs | 已优化 |
通道近似线性:**≈0.44–0.463 µs/B**。写多少、读多少,时间就按字节缩放。
**已落地的减法(正确方向)**
1. **输出按需读**(1.6.0,`ecatappl.c`):每拍先读 12B 头探针;seq 无变化则跳过 384B 全量读,只补读末字应答。
2. **段 2 差分填充**(1.6.21,`ethercat_can_bridge.cpp::fillMotorRound`):节点集合与各 seq 未变时整段复用影子,跳过 MotorState 重读与逐槽拷贝——省的是 MCU 侧填充,不直接省 PRAM 字节。
3. **稀疏写 ESC**(1.6.22,工作区未提交):`CONFIG_ECAT_SPARSE_ESC_WRITE=1` 时,密排 count 有效则只写「段1+段2有效区」与「段3+段5」,跳过空槽区。count=3 时约省 (9−3)×56 ≈ 336B PRAM 写 ≈ 150µs/拍。magic 不对/count 撑满自动回退整包。
4. **输入 PDO 瘦身**(1.6.1,历史):640→496 已证明“减字节=减时间”;1.6.15 为批量窗扩回 600 是容量权衡,不是速度回退。
**未做 / 被否决的加速**
| 尝试 | 结果 | 依据 |
|------|------|------|
| PRAM 批量弹 DWORD(用 AVAIL_CNT) | **已回退**(1.6.2) | 短读首 DWORD 新鲜、后续取陈旧项;窗口冻结 |
| EXMC 慢档时序试速 | 无效 | 当年失败根因是访问模型错误,与时序无关 |
| 压缩总线时序 | **不建议** | BUSLAT 1→4 是为温漂可靠(+3HCLK≈16.7ns/访问) |
### S2.3 仍有空间的优化点(按 ROI 排序)
#### P1 — 输入 PDO 真正的差分写 ESC
- **现状**:填充侧有段 2 影子;**写侧**仍是整包 600B 或稀疏两段,段 1/3/5 每拍全写。
- **前提**:开 `CONFIG_ECAT_DIFF_STATS=1` 采 0x2000 sub13..23(changedMax / runsMax / 各段变化字节),拿分布再定 A1(语义分区条件写)或 A2(块粒度差分,N 由 runsMax 定)。
- **预期**:若稳定工况下每拍变化字节数远小于 600,写侧可再省数十至上百 µs。
- **风险**:run 碎片会把一次大写退化成多次带 AVAIL 轮询的小写;必须设碎片上限,超限回退整段/整包。
- **验收**:`CONFIG_ECAT_DIFF_STATS=0` 对拍 sub10 与 load_perm;布局/SII/主站解码不变。
#### P2 — csrRead 双读校验分级
- **现状**:`EscLan9252::csrRead` 对每次事务读两遍 CSR_DATA 并比较(1.5.59),不一致整事务重跑一次。对 EXMC 毛刺防护有效,但 **CSR 路径访问次数近似翻倍**。
- **热路径调用方**:AL Event(每拍 1–2 次)、邮箱长度/SM 状态、看门狗状态、LED、诊断。
- **可行做法**:提供 `csrReadOnce`(或按寄存器关键级别开关);**几何/状态关键寄存器(SM、AL、邮箱)保持双读**;LED/诊断/低频路径可单读。
- **风险**:去掉防护的路径可能偶发读错;须限定白名单,不可一刀切。
- **预期**:CSR 间接访问(多步 HBI)减少约一半;相对 600B PRAM 写占比更小,属次优先。
#### P3 — 稀疏写收益标定(已实现,待验收)
- 工作区已实现 `sparseEscWritePlan` + `CONFIG_ECAT_SPARSE_ESC_WRITE=1`。
- **验收动作**:同工况开关 0/1 各测一轮 sub10;count 越小收益越大。
- 空洞区为 SM 缓冲上次残留,主站必须只按 count 迭代(1.6.21 已如此)。
#### 不建议再碰
- **EXMC 时序收紧**:BUSLAT=4 / DSET=20 为全温区可靠性的已知代价;已有“慢档无效、访问模型才是当年根因”的记录。除非板级时序重测给出明确裕量,否则不动。
- **恢复 PRAM 批量弹 DWORD**:短读语义未弄清前禁止。
- **改 1-buffer 扩 DPRAM**:失去三缓冲,并发读写会拿到半新半旧过程数据。
### S2.4 访问模型备忘(审计时反复踩过的坑)
```
主站帧
→ LAN9252 DPRAM (4KB)
邮箱 0x1000+ / PDO 0x1400+
← PDI (HBI Indexed)
HBI_IDX_0 / HBI_DATA_0 ← 仅系统 CSR + 访问端口
ECAT_CSR_CMD/DATA 0x304/0x300 ← 核心 CSR 0x0000–0x0FFF
PRAM FIFO 0x000–0x03C + 0x308–0x314 ← 过程 RAM 0x1000–0x1FFF
→ EXMC NE0 @ 0x60000000, 16-bit
```
核心 CSR **不能**直读 HBI 索引(1.5.37 之前全 0 的根因)。过程数据 **必须**走 PRAM FIFO。
## [S3] Out of Scope
- 不重生成 SII、不烧录、不改主站解码。
- 不审计主站侧解码或 CAN 桥竞态(另见 1.6.9–1.6.21 修复记录)。
- 不重新设计 PDO 布局或 EEPROM。
- 上机 A/B 对拍(T3/T6)待现场执行。
## Tasks
- [x] T1: 审计 DPRAM/MCU 内存占用与 PRAM 热路径 — acceptance: 本报告 S2.1/S2.2 有依据与数字 (covers: S2)
- [x] T2: 给出优化优先级与不做清单 — acceptance: S2.3 含 ROI/风险/验收 (covers: S2)
- [x] T4: (1.6.25)实现输入 PDO 差分写 — acceptance: 三区影子+3-buffer 冲刷;全跳过写末 2 字节;SII 不变;CONFIG_ECAT_DIFF_ESC_WRITE 门控 (covers: S2.3)
- [x] T5: (1.6.25)csrRead 分级单读 — acceptance: verify 参数;SM/AL 默认双读;MII busy/init/诊断回读单读 (covers: S2.3)
- [ ] T3: (后续)开 DIFF-STATS + 上机对拍 sub10 — acceptance: 记录写侧耗时 A/B(差分开/关)(covers: S2.3)
- [ ] T6: (遗留)验收工作区稀疏写 A/B — acceptance: CONFIG_ECAT_SPARSE_ESC_WRITE 0/1 同工况 sub10 对比入档 (covers: S2.3)
+313
View File
@@ -0,0 +1,313 @@
---
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 排偏移。
+81
View File
@@ -0,0 +1,81 @@
---
feature: warm-jump-ecat-probe
status: delivered
updated: 2026-09-16
branch: master
commits: 7d9d9c7..uncommitted
---
# Warm Jump 后 EtherCAT 起板探针修复
## Report
**What was built** — APP 1.6.40:`EscLan9252::init` 起板探针放行门从
`HBI_IDX_0==0x1234` 改为 `BYTE_TEST==0x87654321`。warm jump 残留的
`HBI_IDX_0=0x0054` 只打 WARN 并放行;BYTE_TEST 不过才补一发 RST# 重探一轮。
cold 路径行为不变(仍等芯片出复位到 0x1234 再测 BYTE_TEST)。
**Verification** — 无 ARM 交叉编译环境;静态审查通过(spec 合规 / 正确性 /
代码风格),reviewer 无 critical。上板验收:ECAT 刷完 warm jump 后串口应见
`[ECAT][ESC] init: OK`(可带 `WARN idx0=0x0054`),`[APP] init` 不再报
`skipped/failed: ethercat`,主站可建连;reset 路径回归不破坏。
**Journey log**
- BOOT 1.1.0.18 短路 `escResetForJump` 是刻意实验;APP 侧旧探针把「复位默认值」
当成通路判据,在 warm 残值上必然假阴性。
- 主工作区曾有一份未完成的 1.6.40 草稿;本任务收口并跳过 warm 残值的 300ms 空等。
- 用户选了隔离 worktree,但本环境拦截 `git worktree add`,改动落在主工作区未提交 diff。
## [S1] Problem
刷完固件后 BOOT(1.1.0.18,跳转前 `escResetForJump()` 已短路)warm jump 进 APP:
APP banner 正常,但 EtherCAT 不可用(自检降级 / 主站无法建连)。按复位键走 cold
jump 后 EtherCAT 恢复。同一镜像、同一 APP 启动链,差异只在进入 APP 前 ESC 的
HBI 索引寄存器状态。
## [S2] Design
### 根因
1. BOOT `bootEcatDeinit()` 最后一条 `writeReg32(IRQ_CFG)` 把 LAN9252 的
`HBI_IDX_0` 留在 **0x0054**(索引模式残值,DS 9.5.4.2 复位默认才是 0x1234)。
2. 跳转前硬复位在 BOOT 1.1.0.18 被刻意短路(1.1.0.13/14 实测那发复位会打死
warm jump),warm 路径芯片 **不会** 回到 POR 默认态。
3. APP `EscLan9252::init()` 旧放行门是 `HBI_IDX_0==0x1234`。该值只证明
「芯片刚出过复位」,证明不了 HBI 通路。warm 路径上探针必然假阴性 →
`s_busReady=false` → SSC 跳过 → 降级 ethercat。
4. cold 路径芯片自带 POR,`IDX_0` 天然 0x1234,与 PB10 那发复位无关 ——
这正是「reset 能好、刷完不行」的分界。
### 修复契约
- **BYTE_TEST(0x064==0x87654321)是唯一物理通路门**。`readReg32` 每次访问前
都 `setHbiIndex(addr)`,完整「索引写 + 数据低/高字读」往返,与复位态无关。
- `HBI_IDX_0` 降级为诊断读数:异常时打 WARN 并放行,不再导致 `return false`。
- 任一判据不过 → 补一发 `hardwareReset()` 再探一轮(此刻 SSC 未 MainInit,
二次复位零配置代价)。两轮仍不通 = 总线物理不通,维持降级。
- `s_busReady` 语义同步:EXMC + BYTE_TEST 通过即 true(与 `initial.cpp` 的
`isBusReady()&&isCoreReady()` 门控一致)。
### 不做的事
- 不改 BOOT(1.1.0.18 保持 RST# 短路);APP 侧已能兜住 warm 残值。
- 不把 `HBI_IDX_0==0x1234` 恢复为硬门。
- 不动 PDO/SII/主站解码。
## [S3] Out of Scope
- BOOT `bootEcatDeinit` 的 PD/PE 浮空注释错误、`escResetForJump` 时序重设计。
- PHY 链路重协商窗口与主站 probe 重试(另见 `ecat_boot_handoff_link_issue.md`)。
- 1.6.35~38 启动链实验回退项(已在 1.6.39 完成)。
## Tasks
- [x] T1: 改 `EscLan9252::init` 探针 — 放行门改为 BYTE_TEST,IDX 残值 WARN 放行,失败补一发 RST# 重探 — acceptance: 代码与 [S2] 契约一致 (covers: S2)
- [x] T2: 同步 `esc_lan9252.h` 的 `isBusReady` 文档语义 — acceptance: 注释与实现一致 (covers: S2; depends: T1)
- [x] T3: `config.h` 版本 bump 1.6.40 + 来由注释 — acceptance: PATCH=40 且注释写明 warm-jump 根因 (covers: S2)
- [x] T4: 逻辑核对探针与调用方 — acceptance: 无 ARM 交叉编译环境,完成静态审查;`initial.cpp` 仍用 `isBusReady()&&isCoreReady()` 门控,语义兼容 (covers: S2; depends: T1)
### 工作区说明
用户选择隔离 worktree,但本环境拦截了 `git worktree add`(共享 ref store 保护)。改动落在主工作区未提交 diff,分支意图为 `fix/warm-jump-ecat-probe`。验收通过后需用户确认是否提交到 master。
File diff suppressed because one or more lines are too long