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、评审过程稿
This commit is contained in:
cbb
2026-09-20 22:58:09 +08:00
parent 63af85e7be
commit 5fd986299c
32 changed files with 2592 additions and 781 deletions
+44 -37
View File
@@ -237,7 +237,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,
@@ -284,31 +284,26 @@ 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);
/* 1.6.33:OP 输入新鲜度不变量 — 输入 PDO 停更则主循环强制重写(根治假连接) */
/* 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),否则自愈永远落在主站放弃之后,形同虚设。
* 1.5.59:邮箱楔死已根治(mailbox.c 几何常量化,CSR 错值无进入缓存路径),
* 本守护降级为运行期内存踩踏兜底;且修复了 1.5.52~1.5.58 的 SWAPWORD
* 字节序 bug——那段时期本守护的失配判定恒短路,实际从未生效过。
* @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() || EscLan9252::pmSuspended()) {
/* 1.6.27:睡眠挂起期一并静默——守护内部读 SM 起止/长度与 0x204/0x220
/* 睡眠挂起期一并静默——守护内部读 SM 起止/长度与 0x204/0x220
* 全是 HBI 访问,会把睡着的芯片叫醒 */
return;
}
@@ -320,7 +315,7 @@ 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 写回路径问题。
@@ -328,7 +323,7 @@ static void ethercatAleMaskGuard()
static void ethercatAlHeartbeat()
{
if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) {
/* 1.6.27:睡眠挂起期静默——读 0x0130/0x0220 即 HBI 访问,读即唤醒 */
/* 睡眠挂起期静默——读 0x0130/0x0220 即 HBI 访问,读即唤醒 */
return;
}
const uint32_t alDiag = EcatDbg_ReadAlDiag();
@@ -433,8 +428,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 {
@@ -506,7 +500,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();
@@ -546,11 +539,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()
@@ -562,9 +567,9 @@ 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 请求驱动改造移除,此处仅需消费请求响应的按铃。 */
@@ -580,7 +585,7 @@ void TaskRunner::runPending()
/* 电机 OTA relay:活跃时每圈推进状态机(等 ACK / 超时 / 发下一包)。
* 非活跃时立即返回,开销可忽略。 */
/* 负载率挂钩(1.5.63):relay 活跃时才计 busy——isActive() 是廉价枚举判断,
/* 负载率挂钩:relay 活跃时才计 busy——isActive() 是廉价枚举判断,
* 空闲路径零挂钩开销;计数段覆盖等 ACK/超时/发包的真实工作。 */
const bool motorOtaActive = MotorOtaRelay::isActive();
if (motorOtaActive) {
@@ -594,26 +599,28 @@ void TaskRunner::runPending()
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) 完成,此处仅处理低优先级逻辑。
* 1.6.27:ESC 睡眠(D1-D3)挂起期整段停调——MainLoop 每圈必读 0x220(AL 事件
* 寄存器,经 HBI),而睡眠中任何 HBI 访问(含读)都会把芯片打回 D0,
* 2026-09-12 实测休眠后 ~20s 即被"复活"。 */
* 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();
/* 1.6.33:OP 输入新鲜度不变量。与 MainLoop 同拍调用——正常时
/* OP 输入新鲜度不变量。与 MainLoop 同拍调用——正常时
* PDI_Isr 已在刷新,这里只做廉价 seq 比对;停更超 5ms 才强制重写。
* 睡眠期与 MainLoop 一并跳过(读 HBI 会唤醒芯片)。 */
EcatDbg_OpInputFreshness();
}
/* 1.5.48:AL Event Mask(0x204) 守护——每圈入口执行,内部 250ms 节流。
/* AL Event Mask(0x204) 守护——每圈入口执行,内部 250ms 节流。
* 放 MainLoop 旁而非诊断任务:守护必须独立于诊断打印频率,
* OP 等待期(12s)内从站进 SAFEOP 后 1s 内即介入自愈。 */
ethercatAleMaskGuard();
@@ -621,7 +628,7 @@ void TaskRunner::runPending()
const uint32_t pending = TaskManager::takePending();
if(pending == 0U) return;
/* 负载率挂钩(1.5.63):派发的周期任务属"非延迟工作",整段计入 busy;
/* 负载率挂钩:派发的周期任务属"非延迟工作",整段计入 busy;
* 前面的常开轮询(Watchdog/poll/MainLoop/guard)不计入,见 sys/cpu_load.h。 */
CpuLoad::beginWork();
if(pending & (1UL << TASK_ADC_VREF_SAMPLE)) taskAdcVrefSample();