Files
DCU_APP/GD32E508_DCU_APP/sys/config.h
T
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

423 lines
68 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file config.h
* @brief 应用级集中配置项定义。
* @note 用于统一管理当前项目中可调的外设、时序、通信与算法常量。
*/
#ifndef SYS_CONFIG_H
#define SYS_CONFIG_H
#include <stdint.h>
#include <stddef.h>
#include "gd32e50x.h"
/* ============================================================
* 功能开关(调试/特性总闸 — 改这里即可开关,改完重编)
* 0/false=关,1/true=开。需要 #if 的保留 #define。
* ============================================================ */
/**
* @brief 5s 诊断任务是否输出全量 UART 摘要(task_runner)。
* @note 任务本身始终 5s 调度:0=默认仅状态变化/错误增量;1=周期打印 ADC/CAN/ECAT 全量行。
* 现场抓吞吐/温度时改回 1 后重编即可。
*/
#define CONFIG_UART_TEMP_PERIODIC_PRINT 0
/**
* @brief 全量摘要详细级别(仅 PERIODIC_PRINT=1 时生效;0=ADC 精简 1=含 CAN 统计)。
* @note 默认 0;需要 can_tx/rx/drop 等列时与 PERIODIC_PRINT 一并打开。
*/
#define CONFIG_UART_DIAG_LEVEL 0
/**
* @brief SPI 调试反馈 0xC4:GD32 ADC 温度/VDDA/VIN 1Hz 主动推送。
* @note 调试用;0=不注册 handler、不推送。与 ESP32 GD32_ADC_FEEDBACK_ENABLE 配对。
*/
#define CONFIG_SPI_ADC_FEEDBACK_ENABLE 1
/**
* @brief 多电机状态表 0xC5:CAN STATUS 帧缓存 + 250ms 主动推送。
* @note 0=不注册 handler、不推送、不解析 STATUS 帧。与 ESP32 端 motor_state_task 配对。
*/
#define CONFIG_MOTOR_STATE_ENABLE 1
/**
* @brief 多电机版本 0xC6:CAN 版本应答帧(0x00FF)缓存 + 事件驱动推送。
* @note 0=不识别版本帧、不推送。与 ESP32 端 motor_version 缓存配对。
* 版本为按需查询的低频数据,独立于 0xC5 周期推送。
*/
#define CONFIG_MOTOR_VERSION_ENABLE 1
/** @brief 最大电机节点数 (node ID 1-9)。 */
constexpr uint8_t CONFIG_MOTOR_MAX_NODES = 9U;
/** @brief 电机离线判定超时 (ms)。超过此时间未收到 STATUS 帧则标记 offline。 */
constexpr uint16_t CONFIG_MOTOR_OFFLINE_MS = 3000U;
/** @brief SPI 命令逐帧日志(#if)。 */
#define CONFIG_SPI_APP_LOG_ENABLE 0
/** @brief CAN_SEND 成功帧详细日志(#if)。 */
#define CONFIG_SPI_CAN_SEND_VERBOSE_LOG 0
/** @brief CAN-FD BRS 使能(canfd.cpp #if)。
* 关闭 BRS: 数据段用仲裁段同速率,牢牢性更高。
* 电机 BOOT 无法正确接收 BRS 高速段→nonce(16B FD+BRS)被拒绝→握手失败。 */
#define CONFIG_CAN_FD_BRS_ENABLE 1U
/** @brief 启动时总线探测帧;正常固件关。 */
#define CONFIG_CAN_STARTUP_BUS_TEST_ENABLE 0
/** @brief 启动时内部回环自检;正常固件关。 */
#define CONFIG_CAN_STARTUP_LOOPBACK_TEST_ENABLE 0
/** @brief CAN 启动详细诊断 dump;默认关。 */
#define CONFIG_CAN_STARTUP_DIAG_VERBOSE 0
/**
* @brief 周期测试发送任务(taskCanTxSend,发 0x18FF5000 64B FD 帧)。
* @note 调试用:该 PGN 无业务接收节点 ACK。auto_retrans=ENABLE 时无 ACK 被
* 无限重发吞进 TEC(TEC 一直涨);DISABLE 时单发失败即 MTEn→error_count++。
* 电机拓扑下无对端应答,默认关以消除持续错误帧。需要回放测试波形时打开。
*/
#define CONFIG_CAN_PERIODIC_TEST_TX_ENABLE 0
/** @brief CAN0/1/2 通道使能。CH1/CH2 未接设备,关闭避免持续 CAN 错误 + 占 SPI 带宽。 */
constexpr bool CONFIG_CAN_CH0_ENABLE = true;
constexpr bool CONFIG_CAN_CH1_ENABLE = true;
constexpr bool CONFIG_CAN_CH2_ENABLE = true;
/** @brief 主循环 WFI(main.cpp #if)。 */
#define CONFIG_MAIN_LOOP_WFI_ENABLE 0
/** @brief EtherCAT SYNC0 同步模式(#if)。 */
#define CONFIG_ETHERCAT_SYNC_MODE 0
/** @brief MOTOR-OTA INFO 级日志(握手/进度/流控 chatter);错误与关键事件恒保留。 */
#define CONFIG_OTA_RELAY_LOG_ENABLE 1
/** @brief CAN 初始化/自检 INFO 级日志;初始化失败与硬件错误恒保留。 */
#define CONFIG_CAN_INIT_LOG_ENABLE 1
/** @brief EtherCAT/EXMC 初始化 INFO 级日志;FAIL 恒保留。 */
#define CONFIG_ECAT_INIT_LOG_ENABLE 1
/** @brief HBI bring-up 诊断探针(bit-bang 探活 + predrive/回显 + 20s 周期复跑)。
* @note 硬件层已确认全通(IDX_0=0x1234、BYTE_TEST=0x87654321),默认关闭以清理上电日志,
* 并避免每次上电/每 20s 把 EXMC 引脚切成 GPIO 手动拉一遍。换板/补焊需重新排查浮空时置 1。
* 网页万用表通断测试 continuityDrive() 不受此开关影响,始终可用。 */
#define CONFIG_ESC_EXMC_DIAG 0
/** @brief 启动横幅与 main 进度打印。 */
#define CONFIG_APP_BANNER_LOG_ENABLE 1
/* ============================================================
* 日志分级宏:*_LOGI=INFO 级,由上方模块开关编译期裁剪,
* 格式串随之被链接器回收;LOG_ERR=错误/关键事件,恒保留。
* 使用文件需自行可见 printf 声明(<cstdio>)。
* ============================================================ */
#define LOG_ERR(...) printf(__VA_ARGS__)
#if CONFIG_OTA_RELAY_LOG_ENABLE
#define OTA_LOGI(...) printf(__VA_ARGS__)
#else
#define OTA_LOGI(...) ((void)0)
#endif
#if CONFIG_CAN_INIT_LOG_ENABLE
#define CAN_LOGI(...) printf(__VA_ARGS__)
#else
#define CAN_LOGI(...) ((void)0)
#endif
#if CONFIG_ECAT_INIT_LOG_ENABLE
#define ECAT_LOGI(...) printf(__VA_ARGS__)
#else
#define ECAT_LOGI(...) ((void)0)
#endif
#if CONFIG_APP_BANNER_LOG_ENABLE
#define APP_LOGI(...) printf(__VA_ARGS__)
#else
#define APP_LOGI(...) ((void)0)
#endif
/* ============================================================
* APP 固件版本号
* ============================================================ */
constexpr uint32_t APP_FW_VERSION_MAJOR = 1U;
constexpr uint32_t APP_FW_VERSION_MINOR = 5U;
constexpr uint32_t APP_FW_VERSION_PATCH = 63U; /* 1.5.63:负载率定义收窄——1.5.61 实机实测恒 99.4%,根因是 runPending 每圈无条件跑 SSC MainLoop 轮询/各 poll() 检查,把常开轮询计入 busy 后指标饱和失去参考价值。改为只计「中断 + 非延迟处理段」占墙钟比:ISR 挂钩不变(TIMER2/SysTick/EXTI0/1/2/TIMER0/CAN0-2 RX+TX/SPI0+EXTI10_15/USART0,DC 模式 Sync0 的 EXMC 过程数据搬运仍计入),主循环侧撤销整圈 begin/end,改为 task_runner.cpp 三段(0xB0 进 BOOT 处理、MotorOtaRelay isActive() 门控的 relay 推进、takePending 任务派发段)挂 begin/end,常开轮询(Watchdog/AppSpiCommandBus.poll/MainLoop/ALE mask guard)与 NOP 空转不计入;1s 窗口滚动从 endWork 挪到主循环每圈无条件调用的 CpuLoad::tick(),空闲系统 seq 也按秒推进。PDO 段 5 布局/上报链路不变。 1.5.62:编译警告清理(固件行为零改动,生成代码与清理前一致)。(1) config.h 历史注释里嵌套的 1.5.47~1.5.51 块注释符使几乎每个编译单元报 -Wcomment,改用【1.5.4x】标注;(2) SSC sdoserv.c 3 处对 packed 成员 CoeHeader 取址强转 UINT16 指针报 -Waddress-of-packed-member,经 UINT32 整型中转消除(邮箱缓冲实际偶对齐,指令不变);(3) SSC objdef.c 2 处指针截断报 -Wpointer-to-int-cast——其中 MEM_ADDR 强转因 MEM_ADDR=UINT16(ecat_def.h:390) 会把 32 位指针截成 16 位,是 ENUM 奇地址路径的潜在 bug(本项目对象字典不含 ENUM 实际不执行),改用 UINT32 保真并顺带修正;(4) 启动文件 startup_gd32e50x_cl.s 走 legacy armasm:cproject.yml 的 ASM misc 增 -Wno-unused-command-line-argument(消 armclang 驱动 4 条参数未使用告警)与 -Wa,armasm,--diag_suppress=A1950W(消 armasm 弃用告警),已用真实工具链实测通过、对象文件仅内嵌命令行字符串有差异。 1.5.61:DCU_APP CPU 负载率经 EtherCAT 上报。新增 sys/cpu_load.{h,cpp}:DWT CYCCNT 计时、主循环任务与挂钩中断(TIMER2/SysTick/EXTI0/1/2/TIMER0/CAN0-2 RX+TX/SPI0/EXTI10_15/USART0)计入同一 busy 区间求并集(单核内任意优先级嵌套不重不漏,LDREX/STREX 原子协议,热路径 .ramfunc 与 onSync0Exti 同款,不给 Sync0 入口添 flash 抖动),1s 滚动窗口出 0.1% 分辨率负载率。数值写入输入 PDO 段 5 诊断区(632..639,批量接收区最坏占用 580 之后永久空闲的尾部,只追加不动既有偏移,PDO 长度/SII/ESI 均不变无需重刷 EEPROM):ECAT_DIAG_OFF_LOAD u16 LE(单位 0.1%) + ECAT_DIAG_OFF_SEQ u32 LE(窗口序号)。主站 tools/ecat ecat_native.c decode_input 解码后经 /api/app/load 暴露,tools/ESP32 OTA 页「运行指标」卡经 /ecat/api 代理 1Hz 轮询显示「GD32 负载」。1.5.60:SM2 事件路径自愈补全——SM2 事件路径自愈补全——2026-09-05 真机 250µs(4kHz) 档主站楔死看门狗每 20~35s 重拉一次(wr 两小时 +38)、从站槽位更新 4200/s 塌到 28/s 的根因加固。(a) dc_app.c 守护(1) 加固:期望值 u16ALEventMask 本身可能缺 PROCESS_OUTPUT_EVENT——下行转换(ecatslv.c AL_ControlInd 尾部)清 0x204/bEscIntEnabled/bEcatFirstOutputsReceived 后,SAFEOP 重入(SetALEventMask @:1506)没走全时 0x204 丢位,而旧判定 actual⊇期望 在错的是期望值时恒通过、缺位永远补不回;现先补期望再比对。(b) dc_app.c 新增自愈(3):OP 态 bEscIntEnabled==FALSE 不可合法出现(下行尾 :1273 清,重入漏恢复段即 OP 带门关)→ PDI_Isr 整体被门控(ecatappl.c:282)、PDO_OutputMapping 停调、输入冻结——正是主站楔死看门狗盯的窗口回显冻结;守护重开门 + 应答挂起 SM2 事件,250ms 节拍内自愈,主站 2s 阈值从此摸不到触发条件。本版首次携带 1.5.59 的 mailbox 构建期常量 + PDI 字节序修复出固件(该轮改动此前只落在 dc_app.c/mailbox.c,config.h 未记档)。 1.5.58:修复 EtherCAT 链路 CONFIG 回显目标值偶发离谱浮点(如 -3.2e26°)。根因:桥段 1/段 3"最近一帧快照"是多字段一组(id/dlc/data),生产者 onCanFrame(主循环 taskCanRxParse) 先写数据后写 id,而 s_rxValid 置位后永不清零,原"最后置位保证一致"注释不成立;DC 模式下 buildInput 在 Sync0_Isr(preempt=0,critical.h 的 BASEPRI 方案挡不住它)执行,ISR 抢占在数据 memcpy 与 id 赋值之间时,PDO 带出"新 STATUS 数据+旧 CONFIG ID",前端 dispatchFrame 按 CONFIG 布局解 STATUS 字节(电压 B11-12+角度低字节 B13-14 被当成 Target_Angle)。误标窗口只在 CONFIG 回显后的下一帧 STATUS 更新瞬间存在,故"调用时偶发";STATUS 走段 2 窗口按槽 seq 判活不受影响,与症状吻合。修法:快照组加 seqlock(s_xrxSeq 奇=正在写,前后 __DMB 防编译器把非 volatile 的 memcpy 挪出窗口),消费者判一次奇偶(单核 ISR 期间生产者必然冻结),奇数则本周期跳过段 1/段 3(开头已清零=无效),下周期自愈;零关中断、零 Sync0 抖动。配套:批量环 head/tail 发布前补 __DMB——同类编译器重排隐患,volatile 自序不约束相邻非 volatile memcpy。 1.5.57:EXMC 时序余量修复——rw/wr_timing.bus_latency 1→4。LAN9252 DS 表 9-4/9-5 要求 trdrd/twrwr/twrrd ≥13ns,该间隔由 EXMC 在两次访问间插入的总线空闲期承担 =(1 固有 + BUSLAT) HCLK:原 BUSLAT=1 只有 2 HCLK≈11.1ns,**低于 13ns 下限**,常温靠 AHB 取指间隙偶然兜底刚好可用,温度升高(传播延迟增大)/电压下降(驱动减弱)即越界——正是实测到的"温漂/压降时偶发读到 0xFFFF 或错值"。BUSLAT=4 → 5 HCLK≈27.8ns,余量 2.1 倍覆盖全温区。注意 2026-08-20 那轮"慢档 16/16/256 无效故排除速度因素"的结论只对当时的症状成立:那次读全 0/FFFF 的真因是访问模型错误(直读核心 CSR 索引空间,1.5.39 修复),时序余量需按 DS 独立核算,不能沿用该结论认为时序无关。代价:每次 HBI 访问多 3 HCLK(16.7ns),满载 1024B 过程数据按每 DWORD 两次 16-bit 访问计增加约 8.6us(相对原 218us 增幅 3.9%),4ms 周期占用 5.4%→5.7%,无实质影响。配套:SNTCFG0 按位域解码回读打印(ASET/AHLD/DSET/BUSLAT),BUSLAT<4 时打 WARN,避免位域写错或库截断静默失效——烧录后串口应见 BUSLAT=4。 1.5.56:扩到 DPRAM 硬上限:输出 48×64bit=384B,输入 80×64bit=640B(非对称)。 1.5.54:EtherCAT 收发模块类化——新增 comm/ethercat_pdo.h/.cpp 的 EthercatPdo 类,与 EthercatSlave / EthercatSync 同风格的全静态类。SSC 把一个 EtherCAT 周期拆成三个回调,类按这三步各给一个入口:APPL_OutputMapping→readOutputs()(取走主站下发的 32B)、APPL_Application→process()(跑应用逻辑,当前是 PDO↔CAN 转发)、APPL_InputMapping→writeInputs()(交回 32B)。过程数据缓冲区(s_out/s_in)与周期计数(outputCycles/inputCycles,可作"主站确实在发过程数据"的证据)归本类所有;dc_app.c 退回纯 SSC 移植层,只保留对象字典(0x7000/0x6000 的 packed 结构体是 SDO 存储必须留在 C 侧)与三行 extern "C" 转发(EcatPdo_ReadOutputs/Process/WriteInputs)。process() 内先 processOutput 后 buildInput,顺序不能反:buildInput 要把本周期刚执行的 seq 回显成 ack,反过来 ack 会恒滞后一拍。输入数据仍经 0x6000 对象存储中转再交给 ESC,保证主站 SDO 读 0x6000 与过程数据内容一致。不加临界区:DC 模式下 readOutputs/writeInputs 在 Sync0_Isr、process() 在主循环,但两者都是单写者整块读写,32B memcpy 撕裂最坏让某周期读到上/下半帧混合,而 seq 是单字节要么旧要么新不会产生第三个值,沿触发语义无害——在 ISR 里关中断反而拖累抖动。 1.5.53:新增 EtherCAT PDO ↔ CAN 转发桥(comm/ethercat_can_bridge.h/.cpp),主站可直接用过程数据收发 CAN 帧。布局:输出 PDO(0x7000,32B)=[0]seq [1]ch [2]dlc [3]flags [4..7]id(LE) [8..31]data(24B);输入 PDO(0x6000,32B)=[0]ack(回显 seq) [1]ch [2]dlc [3]flags(bit0=有效,bit1=截断) [4..7]id [8..15]data(8B) [16..19]rx计数 [20]最近发送结果 [21]TX队列积压 [22..23]tx计数 [24..25]丢帧数。关键设计:(1)**seq 变化沿触发发送**——PDO 是周期性刷新的,若按“内容非空即发”会把同一帧每周期重发;且上电/重连后首次只记基准 seq 不发送(s_seqPrimed),防主站残留的非零 seq 在进 OP 瞬间凭空发一帧。(2)**发送用 enqueueTx 而非 transmitFrame**:后者默认 allow_abort=true 会在邮箱满时abort 掉已装载的旧帧(历史上 7 帧突发只发出 3 帧的根因),PDO 周期性多帧正是该场景。(3)**RX 不自己 consumeRxFrame**:该队列已有唯一消费者 taskCanRxParse(),再加一个会与OTA relay/电机状态互抢帧;改为在 task_runner.cpp 分发链末尾**旁路**调 onCanFrame(),不 return 不 continue,不影响原有消费者。(4)dc_app.c 是 C 文件而 CanFd 是 C++ 类,经 extern "C" shim 调用。原 Phase 2 的 32B 回环被取代:回环的验证价值改由输入 PDO 的 seq 回显承担(写 seq=N 下周期应读回 ack=N,同样证明 输出→应用→输入 整条链路通)。配套:tools/ecat 新增 /api/can/send|/api/can/state 与网页 CAN 面板。 1.5.52:根治"反复启停后从站邮箱楔死"(主站侧任何手段都救不回、断电才恢复的硬楔死)——从站固件守护自愈。
03:00 主站侧探针钉死完整证据链(楔死态、未断电):
(1) ESC/SSC 邮箱处理全程活着:手动注入合法 CoE 请求(写满 SM0 256B)后 200ms 内 SM1 出响应;
(2) 但响应帧头出现在 SM1 start+8 而非 start(逐会话累积:第一轮 +4、后续 +8),前 8 字节全零;
(3) 网络侧(FPRD)读 SM0/SM1 寄存器恒正确(start=0x1000/0x1200,ctl=0x26/0x22,act=01);
=> SSC 的 u16EscAddrSendMbx(Init→PreOP 时经 GetSyncMan→CSR 间接读)拿到了错值,响应写到错位地址,
主站按协议从 0x1200 读永远对不上帧头 → SOEM 报 WkcError/MailboxError;且预清理重枚举
(重跑 StartMailboxHandler 重读)仍错 → 死锁,断电才清零。错位逐会话 +4(DWORD 粒度)指向
HBI 访问层状态残留(CSR 间接读或 PRAM 引擎,成因待串口日志确凿——两种成因下述自愈均有效)。
修复:EcatDbg_GuardAleventMask 扩展自愈(0):AL>=PREOP 且 bMbxRunning 时,读 SM 寄存器真值
(0x0800/0x0808 的 start/len)与 SSC 缓存(u16EscAddrReceiveMbx/SendMbx/Receive/SendMbxSize)比对,
失配即写回+WARN(旧值/寄存器值一并打印——下次串口复现一行即区分"CSR 读错"还是"变量污染")。
变量仅在 Stop/StartMailboxHandler(主循环上下文)被写,守护同上下文无并发。
守护周期 1s→250ms(task_runner ethercatAleMaskGuard,节流源 g_app_uptime_sec→g_scheduler.tick_1ms):
自愈必须快过主站 SDO 超时(~700ms),否则永远落在主站放弃之后形同虚设。
邮箱自愈+主站侧预清理(ecat_server.py 1.5.51)+自动重试三层叠加:错位发生 → 250ms 内写回 →
主站重试成功,彻底消除"断电才恢复"。
主站工具同步(tools/ecat/ecat_server.py):config_map 失败转储扩展为全部 4 个 SM 寄存器
(0x0800..0x081F;此前 0x0810/0x0818 曾误标为 SM0/SM1,实为 SM2/SM3——已修正),
并注明"SM0/SM1 配置正常却持续失败=SSC 邮箱地址错位楔死"的判别法。 【1.5.51】主站工具修复"反复启停后卡住→闪退"(tools/ecat/ecat_server.py),GD32 功能代码零改动(无需重新烧录;bump 仅为变更记录与编译纪律)。
02:05 复现脚本(faulthandler)钉死闪退根因:同一连接内反复 op/start↔op/stop 不重枚举时,从站邮箱逐轮退化
(第 2 轮起 OP 态 SDO 读 0x2000 全 WkcError——即用户日志"报个读取失败"),第 4 轮 config_map() 内 SOEM CoE
分段读(ethercatcoe.c ecx_SDOread 的 memcpy 无边界检查,上游缺陷)吃到楔死邮箱的错位帧 → 原生访问违例
(0xC0000005) → 服务子进程闪退、GUI 连带退出——与用户 01:40 日志逐行吻合(01:40:30"进入 OP"后
无任何输出=进程死在 config_map,响应未及回传,"卡住"即 SOEM 分段循环死循环)。
修复:_require_op_ready 入口无条件"回 INIT|ACK + config_init 重枚举"预清理(等效实测恒有效的
"断开重连",单从站成本 ~100ms),api_op_start 自动重试简化为直接重跑;另修 ecat_server.py 第 99 行
上一轮编辑粘连("g_op_active = Falseg_httpd = None" 被 Python 当链式赋值,g_httpd 全局缺失、
g_op_active 初值错成 None,全靠 falsy 侥幸工作);config_map 失败分支新增邮箱 SM 寄存器现场转储
(0x0810/0x0818 各 8B,start/len/ctl/status/activate/pdi)。
02:12 探针进一步定性(楔死态、未断电):AL 状态机活着(PREOP/SAFEOP 都 ack)但 SSC 完全不处理邮箱——
SM0/SM1 activate/StartAddr 异常(INIT 态读回全零、PREOP 态读回 SII 值 ctl=0x64/0x20 act=0)、
config_map 跑完会重写全部 SM(SOI 使能)但 SDO 依然全败、close+open 也救不回、
SOEM 读到的"错误帧" Detail=0x1300/0x2300/0x3300 随会话递增(=邮箱计数器字节模式,读回的是错位帧)。
结论:从站侧楔死为固件层问题(疑似 PDI 通路/AL Event Mask 0x204 实测=0x03F3 或 SSC 邮箱处理挂起),
主站任何手段(含断开重连)都救不回,必须断电重启——从站根因待串口抓取(下次复现时串口
看主循环 5s 心跳 [ECAT][HB] 是否停跳 + AEM WARN 是否出现)。
30 轮启停压测:修复前第 4 轮必崩(0xC0000005);修复后 30 轮零崩溃零卡死
(第 16 轮起从站进入上述硬楔死、主站侧全部为可恢复的干净报错,直至压测结束)。 【1.5.50】主站工具诊断盲区修复(tools/ecat 两文件),GD32 功能代码零改动(无需重新烧录,板上 1.5.49 即最新;bump 仅为变更记录与编译纪律)。01:31 复测确认 1.5.48 的自动重试机制工作正常:首败("进 PREOP 失败: AL=INIT code=0x0000")后自动恢复,OP 真实建立、UI 周期正确增长。首败根因定位:停止回 INIT 时 SSC MBX_StopMailboxHandler 禁用了邮箱 SM0/SM1(mailbox.c:359-361),而 CheckSmSettings 在 INIT→PREOP 要求邮箱 SM 已使能(ecatslv.c:416-418),拒绝迁移且从站自身无重新使能路径(死锁闭环)——自动重试里 config_init() 重写 SM0/SM1(ethercatconfig.c:279-280)正是解药,与"断开重连有效"同机理。修复:PREOP/SAFEOP 失败分支改用 read_state() 取真实 AL Status Code(旧报文的 code=0x0000 是 SOEM 广播 state_check 不刷新 al_status 的陈旧缓存,与 OP 循环同款盲区),下次首败将显示真实错误码(预期 0x0016/0x001D 类 INVALIDMBXCFGINPREOP)确凿验证根因。 【1.5.49】SM2 事件楔死自愈——01:03 串口日志钉死现场(evt=0x0400 在 0x220 挂起 10s 无人处理、escirq 仅 2.6/s 只随 AL Control 事件跳动、9s ESM 超时报 0x001B、[ECAT][AEM] 一次未打印=其前置 bEscIntEnabled==FALSE 未满足):PDI_Isr 被调用了 13 次却从未执行 SM2 分支(ecatappl.c:287 的 ALEvent&PROCESS_OUTPUT_EVENT 从未命中),bEcatFirstOutputsReceived 永不置位,SAFEOP→OP 挂到 ESM 超时。dc_app.c EcatDbg_GuardAleventMask() 扩展自愈(2):SAFEOP 下检测"0x220 bit10 挂起 + 标志未置位"即从主循环直接替 PDI_Isr 完成工作——置位 bEcatFirstOutputsReceived + 读 SM2 首/末字应答事件(ecatappl.c:317-320 同款);协议等价:bit10 挂起=主站已写过 SM2=WKC=3/3 佐证的"收到输出"成立,AL_ControlRes 下一个 MainLoop 周期即完成 OP。守护(1) 0x204 缺位自愈保留并解除 bEscIntEnabled前置(改判 AL>=SAFEOP+nPdOutputSize>0)。WARN 携带全部内部旗标(bEscInt/bDcSync/mask_exp/mask_act/wd 看门狗状态),下次复现一行日志即可定位楔死路径(嫌疑:bEscIntEnabled 被某路径意外清零、或 PDI_Isr 读 0x220 的 CSR 间接端口结果不含 bit10)。自愈仅在 SAFEOP+标志未置位时触发,OP 态下 LRW 每周期置位/清除 bit10 属正常不误报。1.5.48:修复"停止回 INIT 后重进 OP"概率性挂起(SAFEOP+0x001B, WKC=3/3 但 9s ESM 超时拒绝)。00:58 复测证据链定案:主站 LRW WKC=3/3(输出确实写进 SM2)且看门狗 1000ms(不会被周期触发)——唯一解释是 bEcatFirstOutputsReceived 未被置位,该标志只在 PDI_Isr 收到 SM2 事件时置位,而 IRQ 上报由 AL Event Mask(0x204) bit10 控制:第二次会话中 0x204 意外丢 SM2 位 → PDI_Isr 永不触发 → SAFEOP→OP 挂到 ESM 超时报 0x001B。第一次连接成功、停止重连失败 = 状态残留特征(残留的确切来源由新增的 [ECAT][AEM] WARN 日志在下次复现时捕获)。修复(1)固件守护:dc_app.c 新增 EcatDbg_GuardAleventMask()(task_runner 每 1s 经 ethercatAleMaskGuard 调用,runPending 主路径挂载不依赖诊断任务频率)——从站 AL>=SAFEOP 且 bEscIntEnabled 且 nPdOutputSize>0 时读 0x204 实际值,缺 u16ALEventMask 任一位即 HW_SetALEventMask 补写并打 WARN(actual/expect/restore 三值),PREOP/INIT 不干预(SSC 本来就不置 SM2/SM3 位)。修复(2)主站工具(tools/ecat ecat_server.py):(a) OP 等待循环每 1s SDO 读从站 0x2000:3(escirq 计数),停滞即直接报"PDI_Isr 未触发→0x204 丢 SM2 位"判别性证据;(b) api_op_start 失败后自动重试一次:带 ACK 完整回 INIT(state_check 确认)+config_init 重枚举(重写 SM/FMMU,等效"断开重连"的核心步骤,实测该操作能恢复)+完整序列重跑,用户无需手动断开重连。 【1.5.48】修复"停止回 INIT 后重进 OP"概率性挂起(SAFEOP+0x001B, WKC=3/3 但 9s ESM 超时拒绝)。00:58 复测证据链定案:主站 LRW WKC=3/3(输出确实写进 SM2)且看门狗 1000ms(不会被周期触发)——唯一解释是 bEcatFirstOutputsReceived 未被置位,该标志只在 PDI_Isr 收到 SM2 事件时置位,而 IRQ 上报由 AL Event Mask(0x204) bit10 控制:第二次会话中 0x204 意外丢 SM2 位 → PDI_Isr 永不触发 → SAFEOP→OP 挂到 ESM 超时报 0x001B。第一次连接成功、停止重连失败 = 状态残留特征(残留的确切来源由新增的 [ECAT][AEM] WARN 日志在下次复现时捕获)。修复(1)固件守护:dc_app.c 新增 EcatDbg_GuardAleventMask()(task_runner 每 1s 经 ethercatAleMaskGuard 调用,runPending 主路径挂载不依赖诊断任务频率)——从站 AL>=SAFEOP 且 bEscIntEnabled 且 nPdOutputSize>0 时读 0x204 实际值,缺 u16ALEventMask 任一位即 HW_SetALEventMask 补写并打 WARN(actual/expect/restore 三值),PREOP/INIT 不干预(SSC 本来就不置 SM2/SM3 位)。修复(2)主站工具(tools/ecat ecat_server.py):(a) OP 等待循环每 1s SDO 读从站 0x2000:3(escirq 计数),停滞即直接报"PDI_Isr 未触发→0x204 丢 SM2 位"判别性证据;(b) api_op_start 失败后自动重试一次:带 ACK 完整回 INIT(state_check 确认)+config_init 重枚举(重写 SM/FMMU,等效"断开重连"的核心步骤,实测该操作能恢复)+完整序列重跑,用户无需手动断开重连。 【1.5.47】修复 free-run 连接概率性卡死在"SAFEOP+0x0000 等待 OP"的根因——两个叠加的固件缺陷:(A) ECAT_CheckTimer() 在整个固件里没有任何调用者!ecat_def.h 曾设 ECAT_TIMER_INT=1(要求端口提供 1ms 定时器 ISR 调 ECAT_CheckTimer),但本工程从未实现该 ISR,而 MainLoop 的轮询调用路径被 #if !ECAT_TIMER_INT 编译排除;后果是 EsmTimeoutCounter 永不递减→任何 pending 状态迁移(SAFEOP→OP)永不超时→从站无限期忽略一切后续 AL Control(实测第二次点击报"进 PREOP 失败: AL=SAFEOP"即此),DC_CheckWatchdog/PLL 计数器也全部死掉。修复:ECAT_TIMER_INT 1→0 + 新增 ECAT_TIMER_INC_P_MS=180000(DWT CYCCNT@180MHz,hw_gd32exmc.cpp 的 HW_GetTimerValue/HW_ClearTimerValue 本就是为此写好的死代码),MainLoop 每 1ms 轮询调 ECAT_CheckTimer,ESM 超时(9s)/DC 看门狗/总线周期计算全部复活。(B) ESC 残留 DC 激活位导致从站误入 DC 同步模式:之前跑过 DC 模式(dc_sync(True,...))在 ESC 0x0981 留下激活位,跨"断开重连"不清(仅断电/主站主动清);PREOP→SAFEOP 的 UpdateSyncManagerParameter(ecatslv.c:754)读到 DC unit active 就按 DCSYNC0/1 推导 SyncType→bDcSyncActive=TRUE→SAFEOP→OP 改等 PLL 序列,而 free-run 主站不发 SYNC0、PLL 计数器又在死掉的 DC_CheckWatchdog 里——双重死锁,LRW 每 7ms 也救不回(00:27 复测:轨迹恒 SAFEOP+0x0000)。修复(主站侧,tools/ecat 两文件):free-run 连接时在 config_map 之前 dc_sync(False,0,0) 主动清残留激活;OP 等待 3s→12s 覆盖"ESM 超时(9s)+ACK 清错重试"完整序列;等待期记录 LRW WKC 采样+失败时报真实 AL code/轨迹/看门狗实际值(get_watchdog)作证据链。另:1.5.46 引入的 state_check→read_state 已修正 code=0x0000 假读(SOEM ecx_statecheck 广播模式不更新 ALstatuscode)。1.5.46:主站工具(tools/ecat ecat_server.py/ecat_tool.py)修复 50ms 周期重启后"进 OP 失败/进 PREOP 失败 code=0x0000"仍复现的问题,GD32 侧代码零改动(1.5.45 的 0x0420=1s 看门狗修复保持不变)——bump 版本号并重编译纯粹是为了让烧录生效可验证(串口 init 报告/版本号能对上)。22:15 复测日志钉死了旧修复无效的三处原因:(a) 1.5.45 注释"同态写即 ack"是错误前提——SSC AL_ControlInd(ecatslv.c:1372-1387)仅在 AL Control 带 ACK 位(bit4=0x10)时清 AL Status 错误位并继续执行迁移,错误位置位时普通同态写同样落入"先清错误位后直接 return"的忽略分支;上一轮看门狗 0x001B 拖回 SAFEOP 残留的错误位若不先带 ACK 清掉,重写 SAFEOP/OP 全部石沉大海,这就是第二次点击"进入 OP"报"进 PREOP 失败: AL=SAFEOP code=0x0000"的机制(错误位在 state_check 期间被某次写清掉后 code 显示 0,但状态从未迁移)。现 _require_op_ready/enter_op 所有 AL Control 写统一带 STATE_ACK(pysoem 已导出,=EC_STATE_ACK=0x10),与 SOEM 官方 ecatcheck 的恢复套路一致(写 当前态|ACK 清错再写目标态);无错误时 ACK 位被从站无害消费。(b) OP 等待循环里 state_check(OP, 200ms) 使 LRW 帧间隔 ~205ms,稳超固件<1.5.45 的 100ms SM 看门狗——CheckIfEcatError()(ecatslv.c:2137-2151) 在从站未到 OP 时(bEcatOutputUpdateRunning=FALSE)每 100ms 把 bEcatFirstOutputsReceived 清回 FALSE,而 SAFEOP→OP 的 AL_ControlRes 完成条件恰恰依赖该标志(ecatslv.c:1993),于是迁移挂起直到 ESM 超时回落;轮询收紧到 20ms(帧间隔 ~25ms)。注意:固件≥1.5.45(看门狗 1s)后 (b) 不再致命,但 20ms 轮询对任何看门狗配置都安全。(c) 回落判断须先 0x0F 掩掉错误位:PREOP+错误=0x12>0x04,旧代码 `state < SAFEOP_STATE` 漏判跌落,直接重写 OP 又被忽略,空转到 3s 超时——修正为 (state&0x0F)<SAFEOP。>100ms 全 WKC 错的根治仍在从站固件 1.5.45 的 configPdWatchdog(0x0420=10000×100µs=1s),网页周期下拉最大 200ms 远小于 1s;若烧录 1.5.46 后 100/200ms 仍报 WKC 错误,抓 GD32 串口确认上电有无 "[ECAT][ESC] PD watchdog = 1s"(WARN=0x0420 写失败需查 HBI)。1.5.45:修复主站 100ms 周期全 WKC 错 + 重启有概率卡死。硬件实测(ecat_esc_regs.py RawBus 裸帧)钉死:0x0400=0x09C2(分频 100µs)/0x0420=0x03E8(1000),即 SM 看门狗正好 100ms,且 SII 不携带看门狗字段、这就是芯片复位默认值;探针实测 100ms 周期 wkc 19/20 错(wkc=1,espected=3)、10/50ms 全对、重启 5 轮 1 轮卡 PREOP code=0x0000,与用户现象完全一致。机制:主站写 SM2 后若 100ms 内没有新的写访问,ESC 看门狗到期(0x0440 bit0 清零),SSC CheckIfEcatError()(ecatslv.c:2133)读到后调 AL_ControlInd(STATE_SAFEOP, 0x001B) 拖回 SAFEOP,同时 DisableSyncManChannel(SM2)(ecatslv.c:1740-1746)——SM2 一旦被禁用,主站 LRW 不再写进 SM2、WKC 不累加,表现为 wkc=1 持续报错,直到重连走完整 INIT→PREOP→SAFEOP 才由 StartInputHandler 重新使能 SM2,这就是"WKC错误=周期数-1、断开重连后 WKC=0"的完整链条。修复(1):EscLan9252::configPdWatchdog() 在 init() 的 PDI 就绪分支里把 0x0420 写成 10000(1s),分频保持默认 100µs;正常 10/50ms 周期行为不变,100ms 手动调试周期不再被拖回。修复(2),主站工具(tools/ecat 两文件):重启卡死的根因是 SSC 对带 STATE_CHANGE 错误位的新 AL 请求会忽略(AL_ControlInd ecatslv.c:1376-1384 先清错误位后直接 return)——上一轮看门狗 0x001B/断开时留下的错误位没被 ack,直接 write_state(OP) 无人应答,断开重连之所以能恢复是因为 config_init 的状态轮询恰好完成了 ack。现在 OP 等待循环里若从站跌回 PREOP/INIT,先重写 SAFEOP(同态写即 ack)立起状态再重请 OP。【注:修复(2) 中"同态写即 ack"的前提已被 1.5.46 推翻并修正为带 ACK 位写,见上】 1.5.44:修复 OP 下输入 PDO 几乎每周期 WKC 不计数(主站实测已进 OP、输出 32B/输入 32B正常,但 89 个周期里 87 次 wkc < expected_wkc)——根因是 dc_app.c 的 APPL_StartInputHandler() 写死 *pIntMask = 0x0800,把 SSC 算好的 AL Event Mask 顶掉了。pIntMask 指向 SSC 全局 u16ALEventMask,StartInputHandler() 已根据同步模式推导它:本工程 free-run + 32B 输出 PDO 时,ecatslv.c:959 置 u16ALEventMask = PROCESS_OUTPUT_EVENT(0x0400),:1090 再次确保该位;而 ecatslv.c:1506 的 SetALEventMask() 只做“或”不做赋值,因此旧代码令寄存器 0x204 永远缺 SM2 位、只剩 SM3(0x0800)。后果:主站写输出到 SM2 不再产生 IRQ,PDI_Isr 只能被“主站读 SM3”事件带起来,于是 PDO_InputMapping() 永远发生在主站读取**之后**;SM3 是 3-buffer 模式,主站读到从未被从站重写的缓冲就不累加 WKC,恰好解释“能进 OP、长度对、但几乎每周期 WKC 少”。官方 sampleappl.c/el9800appl.c 的 APPL_StartInputHandler 均只 return、不碰 pIntMask,现修为 (void)pIntMask 并注明原因。同时撤销上一轮在主站工具里加的 0x1C32:01/0x1C33:01 SyncType SDO 写(tools/ecat 两个文件)——它既错也无效:实测返回 SdoError 0x08000022(当前状态不可写),因为 0x1C3x:01 的权限是 ACCESS_WRITE_PREOP 而 pysoem 的 config_map() 已把从站推到 SAFEOP;且本来就不需要写,SSC 在 PREOP→SAFEOP 的 StartInputHandler() 会自动按 DC 寄存器与 PDO 长度推导出 0x1C32=SM_SYNCHRON/0x1C33=SM2_SYNCHRON(ecatslv.c:849-880),coeappl.c:466/513 的 SYNCTYPE_FREERUN 仅是上电初值,bSyncSetByUser 也默认 FALSE。 1.5.43:修复 SAFEOP->OP 永远进不去(主站报 "进 OP 失败: AL=PREOP code=0x0000",且已边发 processdata 边等 3s 仍无效)——根因是 LAN9252 的 IRQ 引脚从未被使能,PDI_Isr 一次都没执行过。IRQ_CFG(0x054) 与 INT_EN(0x05C) 是 LAN9252 系统 CSR(HBI 索引直接可达,不走 CSR 间接端口),复位默认值均为 0,即 IRQ_EN=0(引脚永久去断言)+ ECAT_INT_EN=0(EtherCAT 事件不汇入 IRQ,DS 8.2.6/8.3.1,ECAT_INT 为 INT_STS/INT_EN 的 bit0);固件此前只配了 EXTI/GPIO/NVIC 侧(PB1 上拉输入+下降沿+preempt=1)与 EEPROM 的 SYNC0/1(0x0151=0xCC),从来没打开芯片侧的中断输出总闸。后果链:PDI_Isr 从不触发 → SSC 收不到 PROCESS_OUTPUT_EVENT → bEcatFirstOutputsReceived 恒 FALSE(该标志只在 PDI_Isr(ecatappl.c:305) 或 free-run 的 MainLoop 轮询分支(ecatappl.c:551, 条件 !bEscIntEnabled)里置位)→ AL_ControlInd 的 SAFEOP_2_OP 分支(ecatslv.c:1993 `nPdOutputSize == 0 || bEcatFirstOutputsReceived`) 因本工程有 32B 输出而永不成立 → 迁移被挂起(bApplEsmPending)后回落,AL Status Code 保持 0x0000(不是被拒绝,故不给 0x001B/0x001A)。注意本工程 SyncType 使 UpdateSyncManagerParameter 置 bEscIntEnabled=TRUE(ecatslv.c:951),因此 MainLoop 的 free-run 轮询兜底分支不执行,中断是唯一通路。EscLan9252::init() 在 DL_STATUS bit0 确认 PDI 就绪后新增 enableEcatIrqOutput():先写 INT_STS 清 ECAT_INT 悬留状态(R/WC),再置 INT_EN.ECAT_INT_EN,最后开 IRQ_CFG 总闸并回读校验,打印 [ECAT][ESC] IRQ output enabled(失败则 WARN 并指明 SAFEOP->OP 会卡)。极性与 ethercat_sync.cpp 保持一致:推挽 IRQ_TYPE=1 + 低有效 IRQ_POL=0,配对上拉输入的下降沿 EXTI;INT_DEAS 保持 0=禁用去断言定时器。AL Event Mask(0x204) 仍由 SSC 在状态迁移时自行维护,不在此处干预。配套修主站工具(tools/ecat):(a) _require_op_ready/enter_op 请求 OP 后改为边 send_processdata/receive_processdata 边 state_check 循环至多 3s(SSC 要求先收到首帧输出才 ack OP),中途回落则重发 OP 请求,失败信息按 AL code 分流排查方向;(b) 修正 state_check 超时单位——pysoem docstring 明确 "Timeout value in us",旧代码各处传 2000 实际只等 2ms(SOEM 默认 EC_TIMEOUTSTATE=2000000=2s),PREOP/SAFEOP 能过纯属从站 ack 快的侥幸,现统一用 EC_TIMEOUT_STATE_US=2_000_000 常量。1.5.42:修复主站 config_map 失败(SdoError "Unsupported access to an object" / "Attempt to read to a write only object",实测 0x1600/0x1A00 的 sub0/3/4 报错而 sub1/2 通过)——两个独立的对象字典 bug,均在从站侧,与主站工具无关。(1) REC 对象的 entry descriptor 数组给少了:OBJ_GetEntryDesc()(objdef.c:554) 与 OBJ_GetEntryOffset()(objdef.c:609) 只对 OBJCODE_ARR 复用 pEntryDesc[1] 描述所有子索引,对 OBJCODE_REC 是逐下标索引 pEntryDesc[Subindex];而 dc_app.c 把 0x1600/0x1A00/0x7000/0x6000/0x2000 全声明为 REC(4|OBJCODE_REC<<8) 却只提供 2 元素描述数组,sub3/sub4 越界读到相邻常量数组的垃圾 DataType/ObjAccess,正好解释"sub0/3/4 报错、sub1/2 侥幸通过"的错误模式(sub1/2 恰好落在合法项上)。现为 0x1600/0x1A00 补满 Sub0..Sub4 共 5 项(asPDOMappingEntryDesc),并把原共用的 asDataEntryDesc 拆成 asOutputEntryDesc/asInputEntryDesc(各 5 项)、asSyncDiagEntryDesc 补到 5 项;同时给数据子条目补 OBJACCESS_RXPDOMAPPING/TXPDOMAPPING(官方 sampleappl.h 的 0x7010/0x6000 同此,缺这两个标志主站校验 0x1600/0x1A00 的映射目标时会拒绝),输入侧 0x6000 由 READWRITE 收紧为 ACCESS_READ。0x1C12/0x1C13 是 ARR 保持 2 项不变。(2) 结构体从未被紧排:ecat_def.h 的 STRUCT_PACKED_START/MBX_STRUCT_PACKED_START/OBJ_STRUCT_PACKED_START 三对宏全部留空,注释还误写"AC6 不需要"——实际这些宏就是 SSC 紧排结构体的唯一手段,SSC 的 SDO 读写按"si0 占 16bit、其后各条目按 BitLength 紧排"算字节偏移(OBJ_GetEntryOffset),留空则按编译器自然对齐。armclang 编译期断言实测:{UINT16;UINT64[4]} 的 UINT64 落在字节 8 而非 2,{UINT16;UINT32 x4} 落在字节 4 而非 2,故 0x7000/0x6000/0x2000 的 SDO 单条目访问全部读写错位内存,SSC 内部 0x1C3x(TSYNCMANPAR)/0x10F1 等同样受影响(PDO 路径走 memcpy 未受影响,故此前回环测试无法暴露)。三对宏改为 __attribute__((packed)),并在 dc_app.c 加 _Static_assert 守卫 offsetof==2 与 sizeof==34 防回归。1.5.41:修正三处会误导诊断的日志缺陷(1.5.39 的 LAN9252 访问模型修复经实测已端到端成立:IDX=0x1234、BYTE_TEST=0x87654321、DL_STATUS bit0=1 且 waited 0ms、PDI_CTRL=0x8D、ESC_CFG bit0=0 Device Emulation 关闭、EEP_CSR 无错误位、CSR 写回环 0x0204 写 0x0A50 读回 PASS、SSC MainInit OK;waited 0ms 同时反证 1.5.37「EEPROM 加载竞态」确不存在,读全 0 纯因走错通路)。(1) [ECAT][DIAG] 的 EEP_CSR 位标签错且语义反:0x0502 的 bit11 是 Checksum Error 而非 loaded(真正的加载状态在 bit12,且 0=OK;见 ETG.1000.4 表 49 与 SSC esc.h ESC_EEPROM_ERROR_CRC/LOAD),旧版打印 "loaded=%u" 取 bit11,正常板子恒显示 loaded=0,字面像「EEPROM 未加载」,正是 1.5.37 那个已撤回结论的诱因;现改为 crc_err/load_err/ack_err/wr_err/busy 分位打印并注明全 0=正常。(2) [ECAT][SLAVE] init 终判在无主站时是假警报:s_escReady 要求 AL>=SAFEOP,而 AL 迁移只由主站写 AL Control(0x0120) 驱动、从站不自跳,故冷启动必然打 FAIL(partial),与紧随其后的 SSC MainInit OK 自相矛盾;现按 DL_STATUS 的 Physical Link 位 bit4/5/6(DS 12.14.19) 判定有无主站,空载报 WAIT_MASTER(实测空载 0x5503 三口 link/comm 全 0,接主站 0x5611),link up 却卡 AL 才报真问题并提示逐级 write_state+配 DC。(3) 串口日志丢字节:Usart0::putCharNonBlocking 在 1024B 环形缓冲满时静默丢弃且 putChar 忽略返回值,3M 波特率下 init 阶段诊断突发写入必溢出,实测掉字产生 "bus eaabled"/"pdi_type=0x8Dexect"/"[EAT][SLAVE]" 等伪影,直接污染判断依据;现 kTxBufSize 1024→4096,缓冲满时在临界区内轮询 TBE 就地发出最老字节腾位(drainOneByteLocked,走轮询不等中断,故 ISR 内 printf 亦不死锁;USART0 为 preempt=3 已被 BASEPRI 屏蔽,独占 s_txTail 无争用),仅 USART 外设不发送时才丢弃并累加 s_txDropped,新增 droppedBytes() 并在 init 报告尾部非 0 时打 WARN 自证日志完整性。1.5.40:清理上电 CAN 启动噪声——[CAN-DIAG] CH%u init_nom/init_fd/exit_ok 三行改为仅在本端控制器初始化失败(can_init/can_fd_init 返回失败,或退出 init mode 后 STAT.IWS 仍置位)时以 LOG_ERR 打印;正常上电(三通道 init_nom=1 init_fd=1 exit_ok=1)完全静默。判据只看 GD32 控制器自身,不含 TEC/REC——总线侧计数在未接从站时会因 RX 浮空噪声累积,与 1.5.36 移除 L0 自检 BUS_OFF/TEC/REC 检查同一原则。1.5.39:修复 LAN9252 访问模型根本性错误——EtherCAT 核心寄存器一直读 0/写丢弃(主站恒读 AL=INIT code=0 的真正根因,1.5.35/1.5.37 两次误判已撤回)。网络侧诊断(新增 tools/ecat/ecat_esc_regs.py,经 Npcap 裸 APRD 帧读 ESC + pysoem 读 SII)实测:EEPROM 加载正常(SII CRC=0x9B 相符)、DL_STATUS=0x5611 pdi_oper=1、ESC_CFG pdi_type=0x8D 已生效、AL Status=0x0001,即 ESC 侧一切正常,故障纯在 GD32 的 HBI 访问层。按 DS 12.11/表 12-13:HBI 索引直接寻址的只有 LAN9252 系统 CSR(BYTE_TEST 0x064 等)与访问端口,EtherCAT 核心 CSR(0x0000-0x0FFF:AL Status/Event、SyncManager、EEPROM 接口)必须经 ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300) 间接访问,过程 RAM(0x1000-0x1FFF:邮箱/PDO)必须经 PRAM FIFO(数据口 0x000-0x03C + 地址长度/命令 0x308-0x314);旧代码把 16 位索引当统一地址空间直读直写,只有 BYTE_TEST 这类系统 CSR 恰好可达,所以「探活全过、业务全死」,且写路径从未被端到端验证过。EscLan9252 新增 csrRead/csrWrite(BUSY 轮询+size 1/2/4)与 pramRead/pramWrite(ADDR_LEN→BUSY→按 avail 轮询读写 FIFO,首/末 DWORD 按 addr&3 取有效字节,别名口轮换),initOnlyReadReg/WriteReg 改走 CSR 端口;hw_gd32exmc.cpp 的 escRead/escWrite 按地址自动路由 CSR/PRAM,AL Event(0x220)/AL Event Mask(0x204)/LED override/HW_GetSyncMan 全部改经 CSR 端口。附带修复:HW_Enable/DisableSyncManChannel 的 Activate 偏移 +4→+6(+4 是 SM Control,Activate 在 0x0806+8n)。1.5.38:修复看门狗 init 失败(SPI 0xA3 实测 crit_fail=0x1000)——FWDGT 的 PSC/RLD 写入经 LSI(IRC40K) 时钟域同步(PUD/RUD 由硬件清零),而 BOOT 只检测 FWDGT 复位标志、从不启动看门狗,冷启动时 LSI 未开,fwdgt_prescaler/reload_value_config 等 PUD/RUD 永远等不到清零、库超时返回 ERROR;该失败自始存在(旧代码忽略返回值时 FWDGT 实际按复位默认值 DIV4/0xFFF≈0.41s 运行,与配置 3.0s 不符,主循环喂狗快才未暴露)。Watchdog::init() 改为先 rcu_osci_on(RCU_IRC40K)+rcu_osci_stab_wait 再配置,新增故障码 kFaultLsiNotReady=4。1.5.37:修复 EEPROM 加载竞态致 EtherCAT 永不就绪(主站恒读 AL=INIT code=0 的根因)——LAN9252 RST# 释放后内部 EEPROM Loader 经 I2C 读 SII 约 30ms,加载完成前 EtherCAT 核心寄存器对 PDI(HBI) 读全 0、写静默丢弃;1.5.35 的 [ECAT][DIAG] 实测 RST# 释放 +3ms 时 DL_STATUS/ESC_CFG/EEP_CSR 全 0、写回环 FAIL,而 SSC 在 +4ms 就已完成 MainInit,AL Event Mask/邮箱 SM 配置全部落空。EscLan9252::init() 在 BYTE_TEST 通过后新增轮询 DL_STATUS(0x0110) bit0(PDI operational/EEPROM loaded,超时沿用 CONFIG_ESC_HBI_POLL_TIMEOUT_MS=300ms;成功打印等待时长,失败打印 DL_STATUS/EEP_CSR 原始值与排查提示)并新增 isCoreReady();initial.cpp 仅在 busReady&&coreReady 时放行 MainInit,总线通但核心未就绪报新故障码 kFaultEthercatCoreNotReady(=3,SPI 0xA3 fault_codes 同步可见)。订正 1.5.35 条目:写回环实际用 AL Event Mask(0x0204) 写 0x0A50 后还原,非 Station Alias(0x012)——后者在部分 ESC 上对 PDI 只读会造成假 FAIL。1.5.36:模块级故障码(fault_codes)——每模块 1B 原因码(0=成功),SPI 0xA3 INIT_STATUS 响应从 12B 扩到 25B([12..24] 按 ModuleBit 位序):canfd=原因码|(首个失败通道<<4)(1=控制器init失败 2=卡init mode 3=STB配置未生效)、ADC(1=RSTCLB超时 2=CLB超时 3=VDDA无效)、Scheduler(1=CEN 2=PSC 3=CAR 4=节拍未前进)、Watchdog(1=PSC 2=RLD 3=挂起)、EtherCAT(1=EXMC总线未就绪 2=SSC MainInit失败)、nvic/esp32_hold/usart0 各单码;init 报告失败模块名带 #XX 故障码后缀,码值定义集中在 sys/initial.h 注释。重要修正:CAN L0 自检移除 BUS_OFF 与 TEC/REC 检查——总线侧状态在通道未接从站(RX 浮空噪声累积 REC)时误报,「没接从站≠错误」;L0 现仅校验 GD32 控制器自身(init失败/卡init mode/STB输出配置),总线健康仍经 0xC1 反馈帧运行期上报(网页 CAN 页可见);CAN L0 按通道打 LOG_ERR(含 STAT/ERR 寄存器);无使能 CAN 通道(全关)属合法配置不再算失败。1.5.35:AL 恒卡 INIT 的判死诊断。主站侧根因已先修:tools/ecat/ecat_server.py 的 _require_op_ready 旧版只 state_check(SAFEOP) 而从不 write_state、且全程未请求过 PREOP——pysoem 的 state_check 只轮询等待、不发 AL Control,故从站从未被要求迁移(现象=AL INIT/code=0x0000 且无错误码);已改为 PREOP→config_map→[DC]→SAFEOP→OP 每步先 write_state 再 state_check。修完后主站确认真的发出 PREOP 请求,但从站仍恒 INIT/code=0x0000,故在 EthercatSlave::init() 增 [ECAT][DIAG] 一次性判死三点:DL_STATUS(0x110) bit0 PDI operational(为 0 则 ESC 静默丢弃 PDI 侧写入的 AL Status)、ESC_CFG(0x140) PDI 类型是否被 EEPROM 加载为 0x8D(HBI 16-bit)、EEP_CSR(0x502) EEPROM loaded/校验错(SII 校验失败会拒绝离开 INIT 且常不给 code);外加 HBI 写路径回环(0x012 Configured Station Alias 写 0xA55A 读回后还原)——此前所有上电探针验证的都是"读"(bit-bang IDX_0 / BYTE_TEST),而 esc_exmc_bus 的 EXMC 写回显测试只在 idx0_raw!=0x1234 的失败分支才跑、因 IDX 正确恒被跳过,故 HBI 写路径从未被端到端验证;若写坏,ECAT_Main 能读到 AL Control 事件却写不回 AL Status,与实测现象完全一致。注意主站读身份/AL 走 ESC 以太网侧+EEPROM(SII),不经过 GD32 的 HBI/PDI,"主站能读到身份"不能证明 PDI 写通。1.5.34:初始化状态检查补全覆盖——原"盲初始化"模块全部改为带验证:SysTick initMs 检查 SysTick_Config 返回值并验证 1ms 节拍真实前进(失败=FATAL 裸串口报告+停机,与 HXTAL FAIL 同级)、ADC 校准改带超时轮询(库函数 adc_calibration_enable 内部死等,硬件异常会永久挂住)并验证 VREFINT→VDDA 采样链、SchedulerTimer 回读 CEN/PSC/CAR 并验证 100us 中断节拍前进、USART0 回读 CTL0.UEN、Watchdog 检查 fwdgt PSC/RLD 的 ErrStatus 返回值+PUD/RUD 无挂起、NVIC 回读 AIRCR.PRIGROUP、ESP32_EN 回读门控置位;新增复位源采集(RCU_RSTSCK→ResetCauseBit 位图,init 报告打印);AppInitializer 导出 moduleMask/criticalFailMask/initState/resetCause getter;新增 SPI 0xA3 INIT_STATUS 命令(12B:模块位图+critical失败位图+复位源+init_state),身份块[15] 原 reserved 字节改填 init_state(旧 ESP32 忽略、旧 GD32 恒 0 与 OK 同义,不 bump feat_map_ver 前后兼容);CONFIG_SPI_MAX_COMMANDS 12→14。附带修复:system_gd32e50x.c 五处 HXTAL FAIL 裸串口诊断段寄存器 bug(CTL0 误写 STAT0 偏移 0x40013800 且漏开 APB2EN bit14 USART0EN,该诊断从未真正发出)——正确布局 CTL0 @ USART0+0x0C。1.5.33:HBI bring-up 诊断探针 EscExmcBus::diagDump 改由 CONFIG_ESC_EXMC_DIAG 门控(默认 0)——硬件层已确认全通(IDX_0=0x1234、BYTE_TEST=0x87654321),清理上电 [EXMC-DIAG] 噪声并停止 task_runner 的 20s 周期复跑,不再每次上电把 EXMC 引脚切成 GPIO 手动拉一遍;换板/补焊排查浮空时置 1 即恢复。网页万用表通断测试 continuityDrive() 及其复用的 bit-bang 辅助函数不受影响,始终可用。1.5.32:AL 通路诊断打印(排查主站连接后 AL 卡 INIT)——ECAT_Main 收到主站 AL 控制请求打 [ECAT][AL] evt/req、SetALStatus 写回打 [ECAT][AL] -> 状态+错误码、task_runner 5s 心跳 [ECAT][HB] al/code/evt(仅同步 ISR 激活时,心跳停=主循环卡死);dc_app.c 导出 EcatDbg_ReadAlDiag(0x0130+0x0220 快照)。1.5.31:DC 通路观测同步到网页——SSC OD 新增只读记录 0x2000 SYNC Diag(sub1=sync0/sub2=sync1/sub3=escirq/sub4=uptime 秒,UINT32),APPL_InputMapping 每 PDO 周期从 EthercatSync 拉取快照(ethercat_sync.cpp 导出 EcatDbg_Get* extern C);配套 tools/ecat:/api/sync_diag 经 SDO 读 0x2000 并以主站时间戳算 sync0 速率,ecat.html 运行期 2s 轮询显示,串口 5s 低频打印保留为旁证。1.5.30:DC SYNC0 通路可观测性——EthercatSync 增加 s_sync0Count/s_sync1Count/s_escIrqCount 三个 volatile 计数器(ISR 内自增,SYNC0 路径实测仅 LDR/ADDS/STR 三条指令,仍在 .ramfunc);task_runner 新增 ethercatSyncCounterPrint(),5s 诊断按增量打印 sync0 速率(主站设 1ms 周期时应 ≈1000/s),无脉冲时静默。配套 tools/ecat 支持分布时钟:ecat_tool.py send/monitor 新增 --dc-cycle-us/--dc-shift-us(0=free-run 默认),enter_op() 在 config_map 后、进 SAFEOP 前调 config_dc()+dc_sync();ecat_server.py /api/op/start 接受 dc_cycle_us/dc_shift_us;ecat.html 加 DC SYNC0 周期下拉。此前工具从不调用 config_dc,PDO 恒为 free-run,SYNC0 引脚无脉冲、Sync0_Isr 从未触发,1.5.28 配的 word1=0xCC/word2=5000 一直未被实测覆盖。1.5.29:删除 GD32 侧 EEPROM 烧录路径(EscEeprom/esc_eeprom.h/.cpp、eeprom_image.h 及 task_runner 主循环烧录块)——ESI 镜像改由 EtherCAT 主站侧刷写(tools/ecat/ecat_flash_eeprom.py 经 ESC 0x0500 EEPROM 接口,2026-08-31 实测 162 字全校验通过),固件不再负责烧录;gen_eeprom.py 生成器保留。HBI 复查修复:maskEnter/Exit 补屏蔽 EXTI2(SYNC1)——Sync1_Isr 的 PDO_InputMapping→HW_EscWriteIsr 两步事务(preempt=1)可打断主循环 HW_EscRead/Write 致错址;通断测试模式进入/退出改经 maskEnter/Exit 屏蔽三类 EXTI,防测试期 ISR 访问 GPIO 态总线读垃圾。1.5.28:对照 LAN9252 数据手册/AN1907/官方 EVB XML 修正——EEPROM word1 高字节 0x00→0xCC(Sync/Latch 0151h:SYNC0/1 Map+Output、推挽低有效配 EXTI 下降沿;原 0x00 时 SYNC0 引脚留在 LATCH 输入态,DC 模式过程数据死);word2 0→5000(Sync 脉宽 0982h,x10ns=50us,0=确认模式不可用);eeprom_image.h 尾部补真实 0xFF 字节(原注释行致 C 零填充,烧写全阵列);EXMC ADDSET 2→4 HCLK(DS 表9-4 trdrd/twrwr/twrrd≥13ns);XML ConfigData 扩到 14B 并加 CycleTimeSync1。1.5.27:LAN9252 使用修正——EXMC 16-bit 地址左移 1 位(HBI DATA 偏移 0x20000/0x30000→0x40000/0x60000);gen_eeprom.py 按 ETG.1000.6 重写(CRC-8/word7、身份字 word8-15、CoE=0x0004、分类区 byte128);DCU_ESI.xml ConfigData PDI 0x05→0x8D、RxPdo/TxPdo 互换并改 4×64bit 对齐 dc_app.c;HBI 两步事务互斥守卫(主循环 mask EXTI0/1 + PDI/Sync1 ISR 屏蔽 EXTI0);删除死代码 EthercatSlave::periodicIsr 三缓冲;20s HBI 诊断在同步 ISR 激活后自动停跑 */
constexpr uint32_t APP_FW_VERSION_BUILD = 0U;
/** @brief USART0 默认波特率。 */
constexpr uint32_t CONFIG_USART0_BAUDRATE = 3000000U; /* 3M:诊断打印高速输出,printf 阻塞极小、不拖慢 relay */
/** @brief ADC 外设实例。 */
constexpr uint32_t CONFIG_ADC_INSTANCE = ADC0;
/** @brief ADC 外部 VIN 检测通道号(原理图:PA6 -> VIN_ADC)。 */
constexpr uint32_t CONFIG_ADC_VIN_CHANNEL = ADC_CHANNEL_6;
/** @brief ADC 内部温度通道号。 */
constexpr uint32_t CONFIG_ADC_TEMP_CHANNEL = ADC_CHANNEL_16;
/** @brief ADC 内部参考电压通道号。 */
constexpr uint32_t CONFIG_ADC_VREFINT_CHANNEL = ADC_CHANNEL_17;
/** @brief ADC 满量程计数值。 */
constexpr float CONFIG_ADC_MAX_VALUE = 4095.0f;
/** @brief 内部参考电压典型值,单位 V。 */
constexpr float CONFIG_ADC_VREFINT_TYPICAL_VOLTAGE = 1.20f;
/** @brief 温度传感器 25℃ 对应电压,单位 V。 */
constexpr float CONFIG_ADC_TEMP_V25 = 1.43f;
/** @brief 温度传感器平均斜率,单位 V/℃。 */
constexpr float CONFIG_ADC_TEMP_AVG_SLOPE = 0.0043f;
/** @brief ADC 上电后稳定等待时间,单位 ms。 */
constexpr uint32_t CONFIG_ADC_STABILIZE_DELAY_MS = 2U;
/** @brief ADC 中值滤波窗口长度(最终样本数为该值 + 1)。 */
constexpr uint32_t CONFIG_ADC_FILTER_SIZE = 6U;
/** @brief CAN 状态报文标准帧 ID。 */
constexpr uint32_t CONFIG_CAN_STATUS_ID = 0x321U;
/** @brief ESP32 请求 DCU_APP 进入 Bootloader OTA 窗口的扩展帧 ID(备用 CAN 入口)。 */
constexpr uint32_t CONFIG_CAN_OTA_BOOT_REQ_ID = 0x00000320U;
/** @brief 进入 Bootloader OTA 请求魔术字节 0~3。 */
constexpr uint8_t CONFIG_CAN_OTA_BOOT_MAGIC0 = 0x4FU;
constexpr uint8_t CONFIG_CAN_OTA_BOOT_MAGIC1 = 0x54U;
constexpr uint8_t CONFIG_CAN_OTA_BOOT_MAGIC2 = 0x41U;
constexpr uint8_t CONFIG_CAN_OTA_BOOT_MAGIC3 = 0x21U;
/** @brief ESP32 在 APP 运行期通过 SPI 发送的进入 Bootloader OTA 命令。 */
constexpr uint8_t CONFIG_SPI_APP_OTA_BOOT_CMD = 0xB0U;
/** @brief SPI 进入 Bootloader OTA 命令确认状态:成功。 */
constexpr uint8_t CONFIG_SPI_APP_OTA_BOOT_ACK_OK = 0xB5U;
/** @brief SPI DATA_READY 输出端口(PB6 → ESP32;v2:低=有待读,高=空闲)。 */
constexpr uint32_t CONFIG_SPI_READY_PORT = GPIOB;
/** @brief SPI DATA_READY 输出引脚。 */
constexpr uint32_t CONFIG_SPI_READY_PIN = GPIO_PIN_6;
/** @brief APP SPI DMA/软件缓冲(字节)。业务 payload 上限 = 本值 − V2_OVERHEAD(20)。 */
constexpr uint16_t CONFIG_SPI_TX_BUF_SIZE = 256U;
constexpr uint16_t CONFIG_SPI_RX_BUF_SIZE = 256U;
constexpr uint16_t CONFIG_SPI_LOG_BUF_SIZE = 256U;
/** @brief 主动按铃超时:ESP32 未在该时间内来读则 cancelRing(ms)。 */
constexpr uint32_t CONFIG_SPI_RING_TIMEOUT_MS = 50U;
/** @brief SPI 命令 handler 注册表容量。
*
* 当前 init 注册 12 个:boot_request / ota_wait / ping / init_status / can_bridge /
* can_burst / can_feedback / id_snapshot / adc_feedback / motor_state / motor_ota / exmc_diag。
* registerHandler 表满时静默丢弃,motor_ota 曾因容量=8 被丢弃(0xD0 回 0xEE)。
* 取 14 留余量;新增 handler 时须确认本容量足够。 */
constexpr uint8_t CONFIG_SPI_MAX_COMMANDS = 14U;
/** @brief 每通道不重复 ID 快照表最大条目数(与 ESP32 CANFD_ID_SNAPSHOT_MAX 对齐)。 */
constexpr uint8_t CONFIG_CANFD_ID_SNAPSHOT_MAX = 16U;
/** @brief 快照条目最大数据字节数(CAN FD DLC 上限)。 */
constexpr uint8_t CONFIG_CANFD_ID_SNAPSHOT_DATA_MAX = 64U;
/** @brief ESP32 硬件复位控制端口(PC9 接 ESP32 EN/RESET,对应原理图 WLESS_EN)。
* @note 复位后 PC9 浮空,ESP_CHIP_EN 会被 C41/U11 拉低;BOOT 也必须主动拉高。 */
constexpr uint32_t CONFIG_ESP32_RESET_PORT = GPIOC;
/** @brief ESP32 硬件复位控制引脚。 */
constexpr uint32_t CONFIG_ESP32_RESET_PIN = GPIO_PIN_9;
/* ============================================================
* CAN-FD 位时序配置(三路 CAN0/CAN1/CAN2 共用)
* 时钟链: SYSCLK=180MHz → AHB=/1=180MHz(RCU_MODIFY_UP 强制)
* → APB1=/2=90MHz (CAN_CLK)
*
* 仲裁段目标: 1.0 Mbps = 90MHz / (9 × 10 TQ)
* 数据段目标: 2.0 Mbps = 90MHz / (9 × 5 TQ)
*
* @note RCU_MODIFY_UP 宏在 system_clock_180m_hxtal() 末尾将 AHB 从 /4
* 临时切到 /1,这是 GigaDevice 官方行为,不可删除。
* 最终 AHB=180MHz, APB1=90MHz, APB2=180MHz, TIMER2_CLK=180MHz。
* ============================================================ */
/** @brief 仲裁段预分频值。 */
constexpr uint32_t CONFIG_CAN_NOMINAL_PRESCALER = 9U;
/** @brief 仲裁段再同步跳跃宽度。
* 1TQ→2TQ:总线空闲期即持续 STUFF 接收错误(REC 冲 120+),OTA 数据帧被错误帧
* 击落触发应用层重发;放宽接收再同步容差(不改波特率/采样点,上限受 BS2=2TQ 限制)。 */
constexpr uint32_t CONFIG_CAN_NOMINAL_SJW = CAN_BT_SJW_2TQ;
/** @brief 仲裁段时间段 1(PHSEG1)。 */
constexpr uint32_t CONFIG_CAN_NOMINAL_BS1 = CAN_BT_BS1_7TQ;
/** @brief 仲裁段时间段 2(PHSEG2)。 */
constexpr uint32_t CONFIG_CAN_NOMINAL_BS2 = CAN_BT_BS2_2TQ;
/** @brief 数据段预分频值。 */
constexpr uint32_t CONFIG_CAN_DATA_PRESCALER = 9U;
/** @brief 数据段再同步跳跃宽度。 */
constexpr uint32_t CONFIG_CAN_DATA_SJW = CAN_BT_SJW_1TQ;
/** @brief 数据段时间段 1。 */
constexpr uint32_t CONFIG_CAN_DATA_BS1 = CAN_BT_BS1_3TQ;
/** @brief 数据段时间段 2。 */
constexpr uint32_t CONFIG_CAN_DATA_BS2 = CAN_BT_BS2_1TQ;
/* BRS / 启动探测 / 通道使能:见文件顶部「功能开关」 */
/** @brief CAN 接收过滤器目标标准帧 ID。 */
constexpr uint32_t CONFIG_CAN_RX_FILTER_ID = CONFIG_CAN_STATUS_ID;
/** @brief CAN 接收过滤器标准帧掩码;0x7FF 表示精确匹配 11-bit ID。 */
constexpr uint32_t CONFIG_CAN_RX_FILTER_MASK = 0x7FFU;
/** @brief 单次 CAN 接收解析任务最多消费的软件队列帧数。 */
constexpr uint32_t CONFIG_CAN_RX_PARSE_BUDGET = 48U;
/** @brief 用户调度定时器实例。 */
constexpr uint32_t CONFIG_SCHED_TIMER_INSTANCE = TIMER2;
/** @brief 用户调度定时器中断号。 */
constexpr IRQn_Type CONFIG_SCHED_TIMER_IRQn = TIMER2_IRQn;
/** @brief 用户调度定时器时钟使能宏参数。 */
constexpr rcu_periph_enum CONFIG_SCHED_TIMER_RCU = RCU_TIMER2;
/** @brief 用户调度定时器预分频值。
* 时钟链: SYSCLK=180MHz → AHB=/1=180MHz(RCU_MODIFY_UP 强制) → APB1=/2=90MHz → TIMER2_CLK=APB1×2=180MHz。
* 保持 100us tick: (PSC+1)×(PER+1)=18000 → 180×100 → PSC=179, PER=99。
*/
constexpr uint32_t CONFIG_SCHED_TIMER_PRESCALER = 179U;
/** @brief 用户调度定时器周期重装值。 */
constexpr uint32_t CONFIG_SCHED_TIMER_PERIOD = 99U;
/** @brief 用户调度定时器 NVIC 抢占优先级。 */
constexpr uint32_t CONFIG_SCHED_TIMER_IRQ_PREEMPT_PRIO = 1U;
/** @brief 用户调度定时器 NVIC 子优先级。 */
constexpr uint32_t CONFIG_SCHED_TIMER_IRQ_SUB_PRIO = 0U;
/** @brief 调度基准节拍:ADC VREF 采样周期,单位 100us tick。 */
constexpr uint32_t CONFIG_TASK_PERIOD_VREF_SAMPLE_TICKS = 5U;
/** @brief 调度基准节拍:ADC 温度采样/处理周期,单位 100us tick。 */
constexpr uint32_t CONFIG_TASK_PERIOD_TEMP_SAMPLE_TICKS = 10U;
/** @brief 调度基准节拍:10ms 计数周期,单位 100us tick。 */
constexpr uint32_t CONFIG_TASK_PERIOD_CAN_RX_TICKS = 100U;
/** @brief 调度基准节拍:CAN 周期发送任务周期,100 tick = 10ms。
* @note 原 1ms/帧过密,改为 10ms/帧以降低 CAN 总线负载与 GD32 邮箱压力。 */
constexpr uint32_t CONFIG_TASK_PERIOD_CAN_TX_TICKS = 100U;
/** @brief 调度基准节拍:心跳周期,单位 100us tick。 */
constexpr uint32_t CONFIG_TASK_PERIOD_HEARTBEAT_TICKS = 10000U;
/** @brief 调度基准节拍:多电机状态 0xC5 主动推送启动周期,2500 tick = 250ms。
* @note 独立于 1Hz heartbeat,避免连带加速 OTA Boot 轮询 / CAN 反馈快照刷新 /
* ADC 推送 / checkOffline 离线判定(3s 阈值)。前端 age 应稳定在 ~120ms。 */
constexpr uint32_t CONFIG_TASK_PERIOD_MOTOR_STATE_TICKS = 2500U;
/** @brief 调度基准节拍:APP 状态/诊断打印周期,50000 tick = 5s。 */
constexpr uint32_t CONFIG_TASK_PERIOD_DIAG_PRINT_TICKS = 50000U;
/* UART/SPI 调试开关:见文件顶部「功能开关」 */
/** @brief FWDGT 独立看门狗预分频值。 */
constexpr uint32_t CONFIG_FWDGT_PSC = FWDGT_PSC_DIV64;
/** @brief FWDGT 独立看门狗重载值。
* 超时 = (64 / 40000) * (RLD + 1) = 1.6ms * (RLD+1)
* RLD=1874 → 1.6ms * 1875 ≈ 3.0s
*/
constexpr uint32_t CONFIG_FWDGT_RLD = 1874U;
/** @brief FWDGT 超时近似值(毫秒),仅供文档参考。 */
constexpr uint32_t CONFIG_FWDGT_TIMEOUT_MS = 3000U;
/* ============================================================
* BOOT 版本在 OTA 配置页中的 Flash 存储地址
* 与 BOOT 工程 sys/config.h 保持一致
* ============================================================ */
constexpr uint32_t CONFIG_OTA_CONFIG_PAGE_ADDR = 0x0807E000U;
constexpr uint32_t CONFIG_BOOT_VERSION_ADDR0 = CONFIG_OTA_CONFIG_PAGE_ADDR + 0x18U;
constexpr uint32_t CONFIG_BOOT_VERSION_ADDR1 = CONFIG_OTA_CONFIG_PAGE_ADDR + 0x1CU;
constexpr uint32_t CONFIG_OTA_CONFIG_WORD_COUNT = 8U;
/* ============================================================
* NVIC 优先级分组与中断优先级分层
* (EtherCAT 实时架构改造:PRE4_SUB0,4 位抢占优先级)
* ============================================================ */
constexpr uint32_t CONFIG_NVIC_PRIORITY_GROUP = NVIC_PRIGROUP_PRE4_SUB0;
constexpr uint32_t CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC = 0U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_SCHED = 1U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_CAN = 2U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_ETHERCAT_ESC = 1U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC1 = 1U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_SPI_USART = 3U;
/* ============================================================
* EtherCAT LAN9252 配置(开关见顶部 CONFIG_MAIN_LOOP_WFI / ETHERCAT_SYNC_MODE)
* ============================================================ */
/* 1.5.27:CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD / CONFIG_ESC_PDI_INPUT_OFFSET /
* CONFIG_ESC_PDI_OUTPUT_OFFSET / CONFIG_ETHERCAT_PDO_SIZE 随死代码 EthercatSlave::
* periodicIsr 三缓冲一并删除;PDO 交换由 SSC(dc_app.c APPL_*, 4×64bit=32B) 完成。 */
/* ---------- EXMC 引脚配置(GD32E508 EXMC 标准映射) ---------- */
constexpr uint32_t CONFIG_ESC_EXMC_BASE_ADDR = 0x60000000U;
/* Region0 = NE0 @ 0x60000000(GD32E50x 用户手册 Rev2.2 图25-3: Bank0 四 Region 各 64MB,
* HADDR[27:26]=00 选中 NE0)。LAN9252 CS 接 PD7=EXMC_NE0(数据手册 GD32E508xx
* Datasheet Rev1.8 LQFP100 引脚表: PD7 Alternate2 = EXMC_NE0/EXMC_NCE1)。
* 注意: GD32E508Vx (LQFP100) 封装只引出 NE0 一个 NOR/SRAM 片选引脚,
* NE1~NE3 仅在 LQFP144 (Zx) 上存在;因此必须使用 Region0(SNCTL0),不能用 Region1。 */
#ifndef EXMC_BANK0_NORSRAM_REGION0
#define EXMC_BANK0_NORSRAM_REGION0 ((uint32_t)0x00000000U)
#endif
#define CONFIG_ESC_EXMC_BANK_NOR_SRAM_REGION0 EXMC_BANK0_NORSRAM_REGION0
/* ---------- ESC 中断引脚(PB0/PB1/PB2,三个独立 EXTI 向量) ---------- */
/** @note 保留 PA0 给未来 STANDBY 模式唤醒(WKUP_PIN0)。 */
constexpr uint32_t CONFIG_ESC_SYNC0_PORT = GPIOB;
constexpr uint32_t CONFIG_ESC_SYNC0_PIN = GPIO_PIN_0;
constexpr exti_line_enum CONFIG_ESC_SYNC0_EXTI = EXTI_0;
constexpr IRQn_Type CONFIG_ESC_SYNC0_IRQn = EXTI0_IRQn;
constexpr uint32_t CONFIG_ESC_SYNC0_EXTI_SOURCE_PORT = GPIO_PORT_SOURCE_GPIOB;
constexpr uint32_t CONFIG_ESC_SYNC0_EXTI_SOURCE_PIN = GPIO_PIN_SOURCE_0;
constexpr uint32_t CONFIG_ESC_IRQ_PORT = GPIOB;
constexpr uint32_t CONFIG_ESC_IRQ_PIN = GPIO_PIN_1;
constexpr exti_line_enum CONFIG_ESC_IRQ_EXTI = EXTI_1;
constexpr IRQn_Type CONFIG_ESC_IRQ_IRQn = EXTI1_IRQn;
constexpr uint32_t CONFIG_ESC_IRQ_EXTI_SOURCE_PORT = GPIO_PORT_SOURCE_GPIOB;
constexpr uint32_t CONFIG_ESC_IRQ_EXTI_SOURCE_PIN = GPIO_PIN_SOURCE_1;
constexpr uint32_t CONFIG_ESC_SYNC1_PORT = GPIOB;
constexpr uint32_t CONFIG_ESC_SYNC1_PIN = GPIO_PIN_2;
constexpr exti_line_enum CONFIG_ESC_SYNC1_EXTI = EXTI_2;
constexpr IRQn_Type CONFIG_ESC_SYNC1_IRQn = EXTI2_IRQn;
constexpr uint32_t CONFIG_ESC_SYNC1_EXTI_SOURCE_PORT = GPIO_PORT_SOURCE_GPIOB;
constexpr uint32_t CONFIG_ESC_SYNC1_EXTI_SOURCE_PIN = GPIO_PIN_SOURCE_2;
/* ---------- ESC 硬件复位(PB10 → LAN9252 Pin11 RST#,SCH 第1页 Pin47) ---------- */
/** @note 2026-08-22(1.5.25) 前固件从未初始化 PB10,RST# 仅靠 R44 10k 上拉自然释放,
* 不满足 LAN9252 手册 18.6.3 tpurstd(电源稳定→RST#释放)≥25ms、trstia(低脉宽)≥200µs。
* 由 EscLan9252::hardwareReset() 执行受控复位序列;释放后保持推挽输出高,
* GD32 自身复位期间引脚回浮空,由 R44 上拉维持高,不会误复位 ESC。 */
constexpr uint32_t CONFIG_ESC_NRST_PORT = GPIOB;
constexpr uint32_t CONFIG_ESC_NRST_PIN = GPIO_PIN_10;
constexpr uint32_t CONFIG_ESC_NRST_ASSERT_MS = 30U; /* 低脉宽,同时覆盖 tpurstd 25ms 与 trstia 200µs */
constexpr uint32_t CONFIG_ESC_HBI_POLL_TIMEOUT_MS = 300U; /* RST# 释放后等待 ESC 启动/EEPROM 加载 */
constexpr uint32_t CONFIG_ESC_HBI_POLL_STEP_MS = 5U;
/* ---------- EXMC 数据/控制线引脚(GD32E508 标准映射) ---------- */
constexpr uint32_t CONFIG_EXMC_D0_PORT = GPIOD; constexpr uint32_t CONFIG_EXMC_D0_PIN = GPIO_PIN_14;
constexpr uint32_t CONFIG_EXMC_D1_PORT = GPIOD; constexpr uint32_t CONFIG_EXMC_D1_PIN = GPIO_PIN_15;
constexpr uint32_t CONFIG_EXMC_D2_PORT = GPIOD; constexpr uint32_t CONFIG_EXMC_D2_PIN = GPIO_PIN_0;
constexpr uint32_t CONFIG_EXMC_D3_PORT = GPIOD; constexpr uint32_t CONFIG_EXMC_D3_PIN = GPIO_PIN_1;
constexpr uint32_t CONFIG_EXMC_D4_PORT = GPIOE; constexpr uint32_t CONFIG_EXMC_D4_PIN = GPIO_PIN_7;
constexpr uint32_t CONFIG_EXMC_D5_PORT = GPIOE; constexpr uint32_t CONFIG_EXMC_D5_PIN = GPIO_PIN_8;
constexpr uint32_t CONFIG_EXMC_D6_PORT = GPIOE; constexpr uint32_t CONFIG_EXMC_D6_PIN = GPIO_PIN_9;
constexpr uint32_t CONFIG_EXMC_D7_PORT = GPIOE; constexpr uint32_t CONFIG_EXMC_D7_PIN = GPIO_PIN_10;
constexpr uint32_t CONFIG_EXMC_D8_PORT = GPIOE; constexpr uint32_t CONFIG_EXMC_D8_PIN = GPIO_PIN_11;
constexpr uint32_t CONFIG_EXMC_D9_PORT = GPIOE; constexpr uint32_t CONFIG_EXMC_D9_PIN = GPIO_PIN_12;
constexpr uint32_t CONFIG_EXMC_D10_PORT = GPIOE; constexpr uint32_t CONFIG_EXMC_D10_PIN = GPIO_PIN_13;
constexpr uint32_t CONFIG_EXMC_D11_PORT = GPIOE; constexpr uint32_t CONFIG_EXMC_D11_PIN = GPIO_PIN_14;
constexpr uint32_t CONFIG_EXMC_D12_PORT = GPIOE; constexpr uint32_t CONFIG_EXMC_D12_PIN = GPIO_PIN_15;
constexpr uint32_t CONFIG_EXMC_D13_PORT = GPIOD; constexpr uint32_t CONFIG_EXMC_D13_PIN = GPIO_PIN_8;
constexpr uint32_t CONFIG_EXMC_D14_PORT = GPIOD; constexpr uint32_t CONFIG_EXMC_D14_PIN = GPIO_PIN_9;
constexpr uint32_t CONFIG_EXMC_D15_PORT = GPIOD; constexpr uint32_t CONFIG_EXMC_D15_PIN = GPIO_PIN_10;
constexpr uint32_t CONFIG_EXMC_A16_PORT = GPIOD; constexpr uint32_t CONFIG_EXMC_A16_PIN = GPIO_PIN_11;
/* A17 (PD12) 用于 HBI Indexed Mode 访问 HBI_DATA_0 寄存器,已将 LAN9252 A2 (Pin 29) 接到 PD12。
* 注意 LAN9252 引脚顺序:Pin25=A1, Pin26=A3, Pin27=A4, Pin28=CS, Pin29=A2。
* A2 是 IDX/DATA 选择位,必须接到 PD12 才能访问 DATA 寄存器,EEPROM 烧录功能可用。
* 注意 EXMC 16-bit 模式地址左移 1 位(用户手册 p869):A16(PD11)=CPU 偏移 0x20000、
* A17(PD12)=CPU 偏移 0x40000,HBI_DATA_0 访问偏移见 esc_lan9252.cpp。 */
constexpr uint32_t CONFIG_EXMC_A17_PORT = GPIOD; constexpr uint32_t CONFIG_EXMC_A17_PIN = GPIO_PIN_12;
constexpr uint32_t CONFIG_EXMC_NOE_PORT = GPIOD; constexpr uint32_t CONFIG_EXMC_NOE_PIN = GPIO_PIN_4;
constexpr uint32_t CONFIG_EXMC_NWE_PORT = GPIOD; constexpr uint32_t CONFIG_EXMC_NWE_PIN = GPIO_PIN_5;
/* LQFP100 只有 NE0(PD7) 一个 NOR/SRAM 片选,归 Region0(0x60000000);命名沿用硬件真值 NE0。 */
constexpr uint32_t CONFIG_EXMC_NE0_PORT = GPIOD; constexpr uint32_t CONFIG_EXMC_NE0_PIN = GPIO_PIN_7;
#endif /* SYS_CONFIG_H */