/** * @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 #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(__builtin_assume_aligned(s_prev, 4)); const uint8_t *curW = static_cast(__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 */