1.6.14 CAN-FD 数据段 2Mbps→5Mbps:CONFIG_CAN_DATA_PRESCALER 9→3、BS1 3→4TQ (90MHz/(3×6TQ),采样点 80%→83.3%),仲裁段仍 1M。TDC filter/offset 维持 4 不做 等比缩放(TDCMOD=CALC_AND_OFFSET 下硬件已自动测回路延时;放到 12 会让本节点回采 错开约 6 个数据位,自发热帧被判错误帧)。配套电机 APP/BOOT data_prescaler=2。 1.6.15 段4 批量接收由"环形队列 + 变长紧排排空"改为 5×44B 定长覆盖窗口,输入 PDO 496→600(75×64bit),段5 诊断区 484→584。动机两条:现场 4000 帧/s 超旧容量 3 帧/拍 恒丢 1 帧;更关键是排空赤字把队列长期压在饱和态,而 1.6.9~1.6.13 追的错标/拼接竞态 正是饱和态喂出来的(判满用的 tail 滞后→生产者写进消费者刚读过的槽)。定长消灭 tail/判满/pending,边界越界写在结构上不可能再犯。每槽带 u32 seqlock:buildInput 在 PDI_Isr(preempt=0) 抢占主循环写侧,无它则主站会拿到"新 ID + 旧数据"拼出的假帧。 out+in=984 ≤ 1024 硬上限,DPRAM 留 120B。语义变更:批量区从"不丢的事件流"变"最近 N 帧窗口",去重与丢帧判据移交主站按 seq 归一。三端同步 size.h/bridge/dc_app/ gen_eeprom/DCU_ESI.xml,必须重烧固件 + 烧 EEPROM + 断电冷启动,只烧一边 SAFEOP 报 AL 0x001A/0x0011。 1.6.16 段1/段3 快照 drop 计数口径修复(纯判据,PDO 布局逐字节不动):旧判据 s_rxValid 置位后永不清零 ⇒ 开机首帧后每帧都算丢弃,drop ≡ rx−1,是零灵敏度假仪表 且把网页状态栏刷成 ERR。新增 s_rxPublished,判据改 s_rxValid && !s_rxPublished。 1.6.17~19 [DIFF-STATS] 差分写取证仪表,零行为变更,只加 0x2000 sub13..23(11 个只读 UINT32) ⇒ 不动 PDO 长度/映射,无需重生成 SII。实测:每拍 27.4% 字节真变化(164B)、 run 峰值 12~13、段2 占变化量 ~53%、段4 ~25%。 1.6.19 推翻 1.6.18 的自我诊断:比对开销虚报不是 -Oz 把 memcpy 编成调用(手拼移位 反而 21.3µs→41µs),真根因是 alignas(4) 到 const uint8_t* 形参处丢失,优化器无法 证明 4 字节对齐。__builtin_assume_aligned + memcpy 后循环体缩成 ldr/ldr/cmp/bne、 bl 归零。教训:测量工具自身的开销会直接变成结论。 1.6.20 基线对照版(当前提交状态 CONFIG_ECAT_DIFF_STATS=0):同工况(电机在线无指令) 实测 load 49.1% vs 仪表开的 51.6% ⇒ 仪表自身成本 2.4 个点,差分改造收益 = 0。 按实测变化量推算 A1(段粒度条件写) 只值约 1 点(段2/段4 每拍都变,必然整块重写), 仅 A2(8B 块粒度 + run 上限退化为整写) 有空间 —— 推算值,未做 A/B 实测不作为结论。
190 lines
8.4 KiB
C++
190 lines
8.4 KiB
C++
/**
|
||
* @file ethercat_pdo.cpp
|
||
* @brief EtherCAT 过程数据(PDO)收发模块实现(1.5.55)。
|
||
*/
|
||
#include "comm/ethercat_pdo.h"
|
||
#include "comm/ethercat_can_bridge.h"
|
||
#include "device/canfd.h"
|
||
#include "sys/config.h"
|
||
#include "sys/cpu_load.h"
|
||
/* [DIFF-STATS] 临时仪表挂接(删除:去掉本块与 process() 内的 observe 块)。
|
||
* 上一行的 sys/config.h 必须在此之前:宏未定义时 #if 会静默按 0 求值,
|
||
* 开关就会被悄悄当成"关掉"。 */
|
||
#if CONFIG_ECAT_DIFF_STATS
|
||
#include "comm/ethercat_diff_stats.h"
|
||
#endif
|
||
|
||
#include <cstring>
|
||
|
||
/* alignas(4) 必须在类内声明与出类定义上同时标注:AC6(Clang) 否则报
|
||
* "'alignas' must be specified on definition"。用途见 ethercat_pdo.h。 */
|
||
alignas(4) uint8_t EthercatPdo::s_out[EthercatPdo::kPdoOutBytes] = {0};
|
||
alignas(4) uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0};
|
||
/* 输入 PDO 的工作缓冲:buildInput 填这里,填完整块发布到 s_in(见 process())。
|
||
* 读侧 writeInputs 只读 s_in,因此永远看不到"填到一半"的镜像。 */
|
||
alignas(4) uint8_t EthercatPdo::s_build[EthercatPdo::kPdoInBytes] = {0};
|
||
|
||
volatile uint32_t EthercatPdo::s_outputCycles = 0U;
|
||
volatile uint32_t EthercatPdo::s_inputCycles = 0U;
|
||
volatile uint32_t EthercatPdo::s_reentryCount = 0U;
|
||
|
||
void EthercatPdo::readOutputs(const uint8_t *esc)
|
||
{
|
||
if (esc == nullptr) {
|
||
return;
|
||
}
|
||
memcpy(s_out, esc, kPdoOutBytes);
|
||
++s_outputCycles;
|
||
}
|
||
|
||
bool EthercatPdo::outputProbe(const uint8_t *head)
|
||
{
|
||
return EthercatCanBridge::peekOutput(head);
|
||
}
|
||
|
||
void EthercatPdo::process()
|
||
{
|
||
/* 重入防护(1.6.7):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
|
||
* 抢占 → 内层重入本函数,memset 清空后重填整个输入 PDO,随后 PDO_InputMapping()
|
||
* 把这一份发给 ESC;外层回来又按自己那轮的进度接着往下写。于是发出去的镜像里
|
||
* 批量区相邻两条来自**不同轮次**,而两轮之间环形队列 tail 已推进 —— 第 2 条
|
||
* 就带上了另一条帧的内容。实测特征与此吻合:批量区第 1 条几乎不坏(0.06%)、
|
||
* 第 2 条 30.75% 坏且内容是一条完整 CONFIG 帧;段 1/段 3 干净,因为它们每轮
|
||
* 内容相同,被覆盖也看不出来。
|
||
*
|
||
* 1.6.15 批量区改成定长覆盖窗口后,跨轮次混填不再产生"半新半旧"的假帧(每槽
|
||
* 自带 seqlock,且槽位固定、语义本就是"最近 N 帧"),但本防护仍然保留:省掉
|
||
* 一次无谓的整块重填,并让镜像保持同一轮次的一致性。
|
||
*
|
||
* 处理:已在填充中就直接返回。输入 PDO 是周期量,跳过一次下一拍即补,
|
||
* 远好过发出一份跨轮次拼接的镜像。 */
|
||
static volatile bool s_building = false;
|
||
if (s_building) {
|
||
++s_reentryCount;
|
||
return;
|
||
}
|
||
s_building = true;
|
||
|
||
/* PDO ↔ CAN 转发:输出 PDO 的 seq 字节变化沿触发一次 CAN 发送,
|
||
* 输入 PDO 回填桥状态 + 电机状态窗口(布局见 ethercat_can_bridge.h)。
|
||
*
|
||
* 顺序必须是先 processOutput 后 buildInput:buildInput 会把本周期刚执行的
|
||
* seq 回显成 ack,主站因此能在下一周期立刻确认送达;反过来则 ack 恒滞后一拍。
|
||
* processOutput 还负责取走主站请求的状态窗口下标,也必须在 buildInput 之前。
|
||
*
|
||
* 填的是**工作缓冲** s_build,填完整体发布到 s_in:只有重入门还不够 ——
|
||
* 内层被门挡住后,它随后的 PDO_InputMapping() 仍会去读 s_in,若 buildInput
|
||
* 直接写 s_in,读到的就是外层写到一半的半成品。填在别处、整块发布之后,
|
||
* 读侧要么拿到上一份完整镜像、要么拿到这一份完整镜像,不存在中间态。 */
|
||
EthercatCanBridge::processOutput(s_out);
|
||
EthercatCanBridge::buildInput(s_build);
|
||
fillDiagTail(s_build);
|
||
memcpy(s_in, s_build, kPdoInBytes);
|
||
|
||
#if CONFIG_ECAT_DIFF_STATS
|
||
/* [DIFF-STATS] 只测量:与上一份已发布镜像比对并累计到 1s 窗口,经 0x2000
|
||
* sub13..23 上报。不改变上面的整块写行为,因此本版本固件的输出与 1.6.16
|
||
* 逐字节一致。仪表自持上一份副本,这里不需要额外缓冲。 */
|
||
EthercatDiffStats::observe(s_in);
|
||
#endif
|
||
|
||
s_building = false;
|
||
}
|
||
|
||
/**
|
||
* @brief 填充输入 PDO 段 5 设备诊断区(632..639,1.5.61)。
|
||
* @param[out] in 全长输入过程数据缓冲(buildInput 刚清零+填完段 1~4)。
|
||
* @note 负载率 1s 滚动一个窗口,seq 随窗口递增——主站只认"seq 在变"的负载值。
|
||
* 每周期重写 6 字节比"值变化才写"的分支判断更便宜,也不引入隐藏状态。
|
||
*/
|
||
void EthercatPdo::fillDiagTail(uint8_t *in)
|
||
{
|
||
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 位,饱和不回绕 —— 主站看的是
|
||
* "有没有涨、涨多快",回绕会被误读成计数清零。 */
|
||
const CanFd::Diag *cd = CanFd::diag();
|
||
const uint32_t torn = (cd != nullptr) ? cd->rx_torn_count : 0U;
|
||
const uint16_t torn16 = (torn > 0xFFFFU) ? 0xFFFFU : (uint16_t)torn;
|
||
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);
|
||
|
||
/* CAN RX FIFO 满溢计数 RFO0(1.6.12 测量):1.6.11 改"满则丢新"后拼接未消失,
|
||
* 本计数与 reject@494 是否同涨,是"残余拼接是否仍源自溢出"的判决性观测。 */
|
||
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 握手没完成 = 窗口/读指针失同步前兆。 */
|
||
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 校验挡不住的指针错位拼接由入队前
|
||
* 结构自洽校验兜底,语义同上 —— 主站看"涨不涨"判断拼接是否仍在发生。 */
|
||
const uint32_t rj = (cd != nullptr) ? cd->rx_content_reject_count : 0U;
|
||
const uint16_t rj16 = (rj > 0xFFFFU) ? 0xFFFFU : (uint16_t)rj;
|
||
in[ECAT_DIAG_OFF_REJECT] = (uint8_t)(rj16 & 0xFFU);
|
||
in[ECAT_DIAG_OFF_REJECT + 1] = (uint8_t)(rj16 >> 8);
|
||
}
|
||
|
||
void EthercatPdo::writeInputs(uint8_t *esc)
|
||
{
|
||
if (esc == nullptr) {
|
||
return;
|
||
}
|
||
memcpy(esc, s_in, kPdoInBytes);
|
||
++s_inputCycles;
|
||
}
|
||
|
||
void EthercatPdo::snapshotOutput(uint8_t *dst)
|
||
{
|
||
if (dst == nullptr) {
|
||
return;
|
||
}
|
||
memcpy(dst, s_out, kPdoOutBytes);
|
||
}
|
||
|
||
extern "C" {
|
||
|
||
/* 1.6.0:输出探针,SSC PDI_Isr 的 PDO_OutputMapping 每周期调用(C 链接)。
|
||
* 返回 0/1;SSC 侧声明为 UINT8,小返回值零扩展,调用约定兼容。 */
|
||
uint8_t EcatPdo_OutputProbe(const uint8_t *head)
|
||
{
|
||
return EthercatPdo::outputProbe(head) ? 1U : 0U;
|
||
}
|
||
|
||
void EcatPdo_ReadOutputs(const uint8_t *esc)
|
||
{
|
||
EthercatPdo::readOutputs(esc);
|
||
}
|
||
|
||
void EcatPdo_Process(void)
|
||
{
|
||
EthercatPdo::process();
|
||
}
|
||
|
||
void EcatPdo_WriteInputs(uint8_t *esc)
|
||
{
|
||
EthercatPdo::writeInputs(esc);
|
||
}
|
||
|
||
} /* extern "C" */
|