Files
DCU_APP/GD32E508_DCU_APP/comm/ethercat_diff_stats.cpp
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

219 lines
8.0 KiB
C++

/**
* @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 */