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
+76 -32
View File
@@ -1,6 +1,6 @@
/**
* @file ethercat_pdo.cpp
* @brief EtherCAT 过程数据(PDO)收发模块实现(1.5.55)。
* @brief EtherCAT 过程数据(PDO)收发模块实现。
*/
#include "comm/ethercat_pdo.h"
#include "comm/ethercat_can_bridge.h"
@@ -19,6 +19,13 @@
#include <cstring>
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};
@@ -45,6 +52,9 @@ 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)
{
@@ -62,22 +72,15 @@ bool EthercatPdo::outputProbe(const uint8_t *head)
void EthercatPdo::process()
{
/* 重入防护(1.6.7):SSC 的 ECAT_Application()→本函数 在**四个上下文**都会被
/* 重入防护: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 填到一半(如刚写完批量区第 1 条)时被 Sync0/Sync1
* 竞态:MainLoop 把 s_in 填到一半时被 Sync0/Sync1
* 抢占 → 内层重入本函数,memset 清空后重填整个输入 PDO,随后 PDO_InputMapping()
* 把这一份发给 ESC;外层回来又按自己那轮的进度接着往下写。于是发出去的镜像里
* 批量区相邻两条来自**不同轮次**,而两轮之间环形队列 tail 已推进 —— 第 2 条
* 就带上了另一条帧的内容。实测特征与此吻合:批量区第 1 条几乎不坏(0.06%)、
* 第 2 条 30.75% 坏且内容是一条完整 CONFIG 帧;段 1/段 3 干净,因为它们每轮
* 内容相同,被覆盖也看不出来。
*
* 1.6.15 批量区改成定长覆盖窗口后,跨轮次混填不再产生"半新半旧"的假帧(每槽
* 自带 seqlock,且槽位固定、语义本就是"最近 N 帧"),但本防护仍然保留:省掉
* 一次无谓的整块重填,并让镜像保持同一轮次的一致性。
* 批量区相邻两条来自**不同轮次**。
*
* 处理:已在填充中就直接返回。输入 PDO 是周期量,跳过一次下一拍即补,
* 远好过发出一份跨轮次拼接的镜像。 */
@@ -104,18 +107,48 @@ void EthercatPdo::process()
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(build);
fillDiagTail(build);
__DMB();
s_inActiveIdx = nextIdx;
memcpy(s_in, build, kPdoInBytes);
/* 不再 memcpy 到 s_in:writeInputs/稀疏写/差分写都读 s_inBuf[activeIdx],
* 每拍多拷 600B 是纯浪费(8kHz 下不可接受)。 */
++s_processSeq;
#if CONFIG_ECAT_DIFF_STATS
/* [DIFF-STATS] 只测量:与上一份已发布镜像比对并累计到 1s 窗口,经 0x2000
* sub13..23 上报。不改变上面的整块写行为,因此本版本固件的输出与 1.6.16
* 逐字节一致。仪表自持上一份副本,这里不需要额外缓冲。 */
* sub13..23 上报。不改变上面的整块写行为。仪表自持上一份副本,这里不需要额外缓冲。 */
EthercatDiffStats::observe(s_inBuf[nextIdx]);
#endif
@@ -123,18 +156,19 @@ void EthercatPdo::process()
}
/**
* @brief 填充输入 PDO 段 5 设备诊断区(632..639,1.5.61)。
* @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 取帧撕裂丢帧数(1.6.6)。只有 16 位,饱和不回绕 —— 主站看的是
/* CAN 取帧撕裂丢帧数。只有 16 位,饱和不回绕 —— 主站看的是
* "有没有涨、涨多快",回绕会被误读成计数清零。 */
const CanFd::Diag *cd = CanFd::diag();
const uint32_t torn = (cd != nullptr) ? cd->rx_torn_count : 0U;
@@ -142,28 +176,28 @@ void EthercatPdo::fillDiagTail(uint8_t *in)
in[ECAT_DIAG_OFF_TORN] = (uint8_t)(torn16 & 0xFFU);
in[ECAT_DIAG_OFF_TORN + 1] = (uint8_t)(torn16 >> 8);
const uint32_t seq = CpuLoad::windowSeq();
const uint16_t seq16 = (uint16_t)seq; /* 1.6.12:u32 拆成 u16,高 2 字节让给
overfull 计数。主站只判"非 0 且在涨",
按 u32 读到 seq|overfull<<16 同样成立。 */
in[ECAT_DIAG_OFF_SEQ] = (uint8_t)(seq16 & 0xFFU);
in[ECAT_DIAG_OFF_SEQ + 1] = (uint8_t)(seq16 >> 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(1.6.12 测量):1.6.11 改"满则丢新"后拼接未消失,
* 本计数与 reject@494 是否同涨,是"残余拼接是否仍源自溢出"的判决性观测。 */
/* 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 释放握手超时计数(1.6.12,占用原 1.6.7 重入计数位 492——该计数引入
* 至今恒 0,转为纯内部)。dequeue 握手没完成 = 窗口/读指针失同步前兆。 */
/* 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);
/* 内容级拼接帧拦截次数(1.6.11):MI/MP 校验挡不住的指针错位拼接由入队前
/* 内容级拼接帧拦截次数:MI/MP 校验挡不住的指针错位拼接由入队前
* 结构自洽校验兜底,语义同上 —— 主站看"涨不涨"判断拼接是否仍在发生。 */
const uint32_t rj = (cd != nullptr) ? cd->rx_content_reject_count : 0U;
const uint16_t rj16 = (rj > 0xFFFFU) ? 0xFFFFU : (uint16_t)rj;
@@ -191,7 +225,7 @@ bool EthercatPdo::sparseEscWritePlan(uint16_t *off0, uint16_t *len0,
#if !CONFIG_ECAT_SPARSE_ESC_WRITE
return false;
#else
/* 仅 1.6.21 密排布局:count 有效且两段之间确有空洞才分段。
/* 仅密排布局:count 有效且两段之间确有空洞才分段。
* magic 不对 / count 越界 / count 撑满到与段3相接 → 整包,避免陈旧空洞被误读。 */
const uint8_t activeIdx = s_inActiveIdx;
__DMB();
@@ -329,7 +363,7 @@ void EthercatPdo::snapshotOutput(uint8_t *dst)
extern "C" {
/* 1.6.0:输出探针,SSC PDI_Isr 的 PDO_OutputMapping 每周期调用(C 链接)。
/* 输出探针,SSC PDI_Isr 的 PDO_OutputMapping 每周期调用(C 链接)。
* 返回 0/1;SSC 侧声明为 UINT8,小返回值零扩展,调用约定兼容。 */
uint8_t EcatPdo_OutputProbe(const uint8_t *head)
{
@@ -341,6 +375,11 @@ void EcatPdo_ReadOutputs(const uint8_t *esc)
EthercatPdo::readOutputs(esc);
}
void EcatPdo_NoteOutputMapped(void)
{
s_outMappedThisCycle = true;
}
void EcatPdo_Process(void)
{
EthercatPdo::process();
@@ -351,7 +390,7 @@ void EcatPdo_WriteInputs(uint8_t *esc)
EthercatPdo::writeInputs(esc);
}
/* 1.6.22 稀疏写计划:SSC PDO_InputMapping 据此决定 HW_EscWriteIsr 写几段。
/* 稀疏写计划:SSC PDO_InputMapping 据此决定 HW_EscWriteIsr 写几段。
* 返回 1=两段有效;0=回退整包。 */
uint8_t EcatPdo_SparseEscPlan(uint16_t *off0, uint16_t *len0,
uint16_t *off1, uint16_t *len1)
@@ -359,7 +398,7 @@ uint8_t EcatPdo_SparseEscPlan(uint16_t *off0, uint16_t *len0,
return EthercatPdo::sparseEscWritePlan(off0, len0, off1, len1) ? 1U : 0U;
}
/* 1.6.25 差分写计划:SSC PDO_InputMapping 逐段 HW_EscWriteIsr。 */
/* 差分写计划: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
@@ -379,7 +418,7 @@ uint8_t EcatPdo_EscWritePlan(uint16_t *offs, uint16_t *lens, uint8_t maxSegs)
return n;
}
/* 1.6.34: HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
/* HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
uint32_t EscExmcBus_MaskEnter(void)
{
return EscExmcBus::maskEnter();
@@ -400,4 +439,9 @@ void HW_EscExmcMaskExit(uint32_t prev)
EscExmcBus::maskExit(prev);
}
uint32_t EcatPdo_ProcessSeq(void)
{
return EthercatPdo::processSeq();
}
} /* extern "C" */