feat(comm): LAN9252增加csrRead验证分级并实现PDO差分写功能

为提升EtherCAT通信性能,实现以下两项优化:

1. 对EscLan9252::csrRead方法增加verify参数,支持分级验证:
   - verify=true(默认):保持原有的双读比对+重试机制,用于SM/AL/邮箱等
     关键路径防止EXMC毛刺
   - verify=false:单读模式,用于诊断回读/MII busy轮询/init轮询等低风
     险场景,节省一半CSR间接访问时间

2. 实现PDO输入差分写ESC功能:
   - 将输入PDO分为三区:段1桥状态(0-31)、段2电机密排(32-543)、段3+5
     快照诊断(544-599)
   - 采用3-buffer冲刷协议:变化后连续写3拍确保3个缓冲区全部更新
   - 对内容稳定的段跳过写操作,显著减少PRAM字节搬运时间
   - 全段跳过时强制写末2字节完成SM事件应答

同时更新配置版本至1.6.25,添加相关配置选项和文档说明。
This commit is contained in:
cbb
2026-09-12 20:10:52 +08:00
parent 9ca3db26f8
commit 6afaacd03a
7 changed files with 202 additions and 26 deletions
+19 -9
View File
@@ -175,9 +175,9 @@ bool miiXfer(uint8_t phyAddr, uint8_t regAddr, bool write, uint16_t &data)
EscLan9252::csrWrite(ESC_REG_MII_CTRL,
write ? (MII_CTRL_CMD | MII_CTRL_WRITE) : MII_CTRL_CMD, 2U);
for (uint32_t i = 0U; i < POLL_MAX; i++) {
if ((EscLan9252::csrRead(ESC_REG_MII_CTRL, 2U) & MII_CTRL_BUSY) == 0U) {
if ((EscLan9252::csrRead(ESC_REG_MII_CTRL, 2U, false) & MII_CTRL_BUSY) == 0U) {
if (!write) {
data = (uint16_t)EscLan9252::csrRead(ESC_REG_MII_DATA, 2U);
data = (uint16_t)EscLan9252::csrRead(ESC_REG_MII_DATA, 2U, false);
}
return true;
}
@@ -441,12 +441,12 @@ bool EscLan9252::init()
* 网络侧诊断已证实本板 EEPROM 加载与核心均正常,此等待应几乎立即通过;
* 超时意味着 PDI 真的未就绪(EEPROM 内容/I2C/strap 问题),此时 SSC 的
* AL Event Mask/邮箱 SM 配置会落空,由 initial.cpp 按 isCoreReady() 门控跳过。 */
uint16_t dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U);
uint16_t dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U, false);
uint32_t waited = 0U;
while ((dlStatus & 0x0001U) == 0U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS) {
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
waited += CONFIG_ESC_HBI_POLL_STEP_MS;
dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U);
dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U, false);
}
if ((dlStatus & 0x0001U) != 0U) {
s_coreReady = true;
@@ -458,7 +458,7 @@ bool EscLan9252::init()
printf("[ECAT][ESC] init: FAIL, PDI not operational after %ums "
"(DL_STATUS=0x%04X EEP_CSR=0x%04X)\r\n",
(unsigned)CONFIG_ESC_HBI_POLL_TIMEOUT_MS, (unsigned)dlStatus,
(unsigned)csrRead(ESC_REG_EEP_CSR, 2U));
(unsigned)csrRead(ESC_REG_EEP_CSR, 2U, false));
printf("[ECAT][ESC] hint: 用 tools/ecat/ecat_esc_regs.py 从网络侧读 0x0502 错误位区分 EEPROM/I2C/内容问题\r\n");
}
return true;
@@ -484,7 +484,7 @@ void EscLan9252::configPdWatchdog()
* 表现为"100ms 周期全 WKC 错、10/50ms 正常"。分频保持默认 100µs,只延长步数;
* 正常周期(≤50ms)远小于 1s,行为不变。 */
csrWrite(ESC_REG_WD_TIME_PD, ESC_WD_TIME_PD_STEPS, 2U);
const uint32_t rd = csrRead(ESC_REG_WD_TIME_PD, 2U);
const uint32_t rd = csrRead(ESC_REG_WD_TIME_PD, 2U, false);
if ((rd & 0xFFFFU) == ESC_WD_TIME_PD_STEPS) {
printf("[ECAT][ESC] PD watchdog = 1s (0x0420=%u x 100us, default was 100ms)\r\n",
(unsigned)ESC_WD_TIME_PD_STEPS);
@@ -515,15 +515,25 @@ void EscLan9252::enableEcatIrqOutput()
}
}
uint32_t EscLan9252::csrRead(uint16_t addr, uint8_t size)
uint32_t EscLan9252::csrRead(uint16_t addr, uint8_t size, bool verify)
{
/* DS 12.11.1:写 CSR_CMD(BUSY|R_nW|SIZE|ADDR) 启动,busy 自清后读 CSR_DATA;
* 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。
* 1.5.59 加固:CSR_DATA 是锁存寄存器(非 FIFO),命令不变时连读两次必一致;
* 1.5.59 加固(verify=true):CSR_DATA 是锁存寄存器(非 FIFO),命令不变时连读两次必一致;
* 不一致即本拍 EXMC 毛刺——历史上这类错读曾让 GetSyncMan 把 0x1204/0x1208
* 缓存成邮箱几何(→SSC 邮箱楔死,根治见 mailbox.c 构建期常量),或让
* CheckSmSettings 以 0x0016/0x001F 瞬态打断状态转换。对错读整事务重跑一次;
* 两轮仍不一致则交出末拍(与旧行为一致的兜底,邮箱几何另有常量兜底)。 */
* 两轮仍不一致则交出末拍(与旧行为一致的兜底,邮箱几何另有常量兜底)。
* 1.6.25 verify=false:单读路径,供诊断回读/MII busy 轮询/init 轮询等低风险
* 调用方使用;SM/AL/邮箱等状态机关键寄存器必须保持默认双读。 */
if (!verify) {
uint32_t n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
writeReg32(ECAT_CSR_CMD_REG, CSR_BUSY | CSR_RNW | ((uint32_t)size << 16) | addr);
n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
return readReg32(ECAT_CSR_DATA_REG);
}
for (uint8_t attempt = 0U; attempt < 2U; ++attempt)
{
uint32_t n = POLL_MAX;
+5 -2
View File
@@ -77,8 +77,11 @@ public:
* (DS 12.11/表 12-13),EtherCAT 核心寄存器(0x0000-0x0FFF)必须经
* CSR_CMD(0x304)/CSR_DATA(0x300) 间接访问;过程 RAM(0x1000-0x1FFF)
* 必须经 PRAM FIFO。size=1/2/4(size2 需偶地址,size4 需 4 字节对齐)。
* 调用方负责 HBI 互斥(主循环 EscExmcBus::maskEnter / 已在 ISR 上下文)。 */
static uint32_t csrRead(uint16_t addr, uint8_t size);
* 调用方负责 HBI 互斥(主循环 EscExmcBus::maskEnter / 已在 ISR 上下文)。
* @param verify true(默认)=双读比对+不一致重试(1.5.59,防 EXMC 毛刺,SM/AL/邮箱
* 等关键路径必须开);false=单读(诊断回读/MII busy 轮询/init 等低风险路径,
* 省约一半 CSR 间接访问)。 */
static uint32_t csrRead(uint16_t addr, uint8_t size, bool verify = true);
/** @brief ESC 核心 CSR 间接写(ECAT_CSR_CMD/DATA 端口)。约束同 csrRead。 */
static void csrWrite(uint16_t addr, uint32_t data, uint8_t size);
+123
View File
@@ -23,6 +23,15 @@ 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};
alignas(4) uint8_t EthercatPdo::s_escSh1[32] = {0};
alignas(4) uint8_t EthercatPdo::s_escSh2[ECAT_XRX_OFF_CH - 32U] = {0};
alignas(4) uint8_t EthercatPdo::s_escSh3[kEcatPdoInBytes - ECAT_XRX_OFF_CH] = {0};
uint8_t EthercatPdo::s_escFlush1 = 0U;
uint8_t EthercatPdo::s_escFlush2 = 0U;
uint8_t EthercatPdo::s_escFlush3 = 0U;
bool EthercatPdo::s_escSh1Valid = false;
bool EthercatPdo::s_escSh2Valid = false;
bool EthercatPdo::s_escSh3Valid = false;
volatile uint32_t EthercatPdo::s_outputCycles = 0U;
volatile uint32_t EthercatPdo::s_inputCycles = 0U;
@@ -185,6 +194,100 @@ bool EthercatPdo::sparseEscWritePlan(uint16_t *off0, uint16_t *len0,
#endif
}
namespace {
/** 段2 有效长度(稀疏:到 count*56 为止;否则整段)。 */
uint16_t escSeg2WriteLen(const uint8_t *in)
{
#if CONFIG_ECAT_SPARSE_ESC_WRITE
if (in[ECAT_MOTW_OFF_MAGIC] == kEcatMotLayoutMagic) {
const uint8_t count = in[ECAT_MOTW_OFF_COUNT];
if (count <= kEcatMotSlotsPerPdo) {
const uint16_t headEnd =
(uint16_t)(ECAT_MOTW_OFF_SLOTS + (uint16_t)count * kEcatMotSlotSize);
if (headEnd < ECAT_XRX_OFF_CH) {
return headEnd;
}
}
}
#else
(void)in;
#endif
return ECAT_XRX_OFF_CH;
}
/** 判定是否需要写某区:无影子 / 冲刷中 / 内容变化。变化时刷新影子并重置冲刷=2。 */
bool diffNeedWrite(const uint8_t *cur, uint16_t off, uint16_t cmpLen,
uint8_t *shadow, bool &valid, uint8_t &flush)
{
const bool changed = !valid || memcmp(cur + off, shadow, cmpLen) != 0;
if (changed) {
memcpy(shadow, cur + off, cmpLen);
valid = true;
flush = 2U; /* 本拍 + 再 2 拍,冲完 3-buffer */
return true;
}
if (flush > 0U) {
--flush;
return true;
}
return false;
}
} /* anonymous namespace */
uint8_t EthercatPdo::escWritePlan(uint16_t *offs, uint16_t *lens)
{
if (offs == nullptr || lens == nullptr) {
return 0U;
}
#if !CONFIG_ECAT_DIFF_ESC_WRITE
/* 回退:稀疏两段或整包 */
uint8_t n = 0U;
uint16_t o0, l0, o1, l1;
if (sparseEscWritePlan(&o0, &l0, &o1, &l1)) {
offs[n] = o0; lens[n] = l0; ++n;
offs[n] = o1; lens[n] = l1; ++n;
} else {
offs[0] = 0U; lens[0] = kPdoInBytes; n = 1U;
}
return n;
#else
/* 三区差分:段1 桥状态 / 段2 电机密排(稀疏写有效长) / 段3+5 快照诊断。
* 段1 计数器几乎每拍变,段2 稳定时省最大头,段3+5 多数拍稳定。 */
uint8_t n = 0U;
if (diffNeedWrite(s_in, 0U, 32U, s_escSh1, s_escSh1Valid, s_escFlush1)) {
offs[n] = 0U;
lens[n] = 32U;
++n;
}
if (diffNeedWrite(s_in, 32U, (uint16_t)(ECAT_XRX_OFF_CH - 32U),
s_escSh2, s_escSh2Valid, s_escFlush2)) {
offs[n] = 32U;
/* 写长:稀疏时只写到 count*56,但影子比对的是完整 32..543(含清零空洞)。
* 空洞不写入 ESC 无妨:主站只按 count 迭代。 */
const uint16_t wlen = escSeg2WriteLen(s_in);
lens[n] = (wlen > 32U) ? (uint16_t)(wlen - 32U) : 0U;
if (lens[n] == 0U) {
/* count=0:段2 头 8 字节仍要写(count/active/magic) */
lens[n] = 8U;
}
++n;
}
if (diffNeedWrite(s_in, ECAT_XRX_OFF_CH, (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH),
s_escSh3, s_escSh3Valid, s_escFlush3)) {
offs[n] = ECAT_XRX_OFF_CH;
lens[n] = (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH);
++n;
}
if (n == 0U) {
offs[0] = (uint16_t)(kPdoInBytes - 2U);
lens[0] = 2U;
n = 1U;
}
return n;
#endif
}
void EthercatPdo::snapshotOutput(uint8_t *dst)
{
if (dst == nullptr) {
@@ -225,4 +328,24 @@ 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。 */
uint8_t EcatPdo_EscWritePlan(uint16_t *offs, uint16_t *lens, uint8_t maxSegs)
{
if (offs == nullptr || lens == nullptr || maxSegs == 0U
|| maxSegs > EthercatPdo::kEscPlanMaxSegs) {
return 0U;
}
uint16_t o[EthercatPdo::kEscPlanMaxSegs];
uint16_t l[EthercatPdo::kEscPlanMaxSegs];
const uint8_t n = EthercatPdo::escWritePlan(o, l);
if (n == 0U || n > maxSegs) {
return 0U;
}
for (uint8_t i = 0U; i < n; ++i) {
offs[i] = o[i];
lens[i] = l[i];
}
return n;
}
} /* extern "C" */
+25
View File
@@ -82,6 +82,21 @@ public:
static bool sparseEscWritePlan(uint16_t *off0, uint16_t *len0,
uint16_t *off1, uint16_t *len1);
/**
* @brief 输入 PDO 写 ESC 的多段计划(1.6.25 差分写;未启用时回退稀疏/整包)。
* @param[out] offs 段偏移数组,至少 kEscPlanMaxSegs 项
* @param[out] lens 段长度数组,至少 kEscPlanMaxSegs 项
* @return 段数 1..kEscPlanMaxSegs;调用方逐段 HW_EscWriteIsr。
* @note 必须在 writeInputs()/s_in 发布之后、每拍一次调用。按三区差分:
* 段1(0..31 桥状态)、段2(32..543 电机密排,写时用稀疏有效长)、
* 段3+5(544..599)。变化后连续写 3 拍冲完 3-buffer 才允许跳过。
* 全段跳过时返回「末 2 字节」一段以完成 SM 事件。有副作用。
*/
static uint8_t escWritePlan(uint16_t *offs, uint16_t *lens);
/** escWritePlan 最多段数。 */
static constexpr uint8_t kEscPlanMaxSegs = 4U;
/** @brief 累计取走输出的周期数(主站在发过程数据的证据)。 */
static uint32_t outputCycles() { return s_outputCycles; }
@@ -121,6 +136,16 @@ private:
* ECAT_Application() 会在 PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 四个
* 上下文被调用并互相抢占,直接写 s_in 会让读侧取到跨轮次的半成品。 */
alignas(4) static uint8_t s_build[kPdoInBytes];
/** @brief 写 ESC 差分影子(1.6.25):段1 / 段2 / 段3+5 三区。 */
alignas(4) static uint8_t s_escSh1[32];
alignas(4) static uint8_t s_escSh2[ECAT_XRX_OFF_CH - 32U];
alignas(4) static uint8_t s_escSh3[kEcatPdoInBytes - ECAT_XRX_OFF_CH];
static uint8_t s_escFlush1;
static uint8_t s_escFlush2;
static uint8_t s_escFlush3;
static bool s_escSh1Valid;
static bool s_escSh2Valid;
static bool s_escSh3Valid;
/** @brief process() 被重入拦下的次数(诊断用,见 reentryCount())。 */
static volatile uint32_t s_reentryCount;
+12 -8
View File
@@ -167,6 +167,8 @@ extern UINT32 EcatDbg_Dwt(void);
extern UINT8 EcatPdo_OutputProbe(UINT8 const *head);
/* 1.6.22:稀疏写 ESC 计划(密排 count 只写有效段; 实现见 ethercat_pdo.cpp) */
extern UINT8 EcatPdo_SparseEscPlan(UINT16 *off0, UINT16 *len0, UINT16 *off1, UINT16 *len1);
/* 1.6.25:差分写 ESC 多段计划(段头/段尾 + 3-buffer 冲刷; 实现见 ethercat_pdo.cpp) */
extern UINT8 EcatPdo_EscWritePlan(UINT16 *offs, UINT16 *lens, UINT8 maxSegs);
volatile UINT32 g_dbg_pdiCyc = 0; /* sub5: PDI_Isr 全函数累计周期 */
volatile UINT32 g_dbg_outCyc = 0; /* sub6: PDO_OutputMapping(输出探针/全量读) */
volatile UINT32 g_dbg_freerunCyc = 0; /* sub7: MainLoop free-run 分支(SM 同步下恒 0) */
@@ -200,19 +202,21 @@ void PDO_InputMapping(void)
{
/*ECAT_DBG(1.6.0 剖分:输入映射全程 + 640B 写细分)*/
/* 1.6.22 稀疏写:APPL 仍整填 aPdInputData(对齐 SDO 0x6000),但 HW_EscWriteIsr
* 只写密排有效段+段3/5,跳过空槽区 —— 省的是 PRAM 字节时间,不是 RAM 拷贝。 */
* 只写密排有效段+段3/5,跳过空槽区 —— 省的是 PRAM 字节时间,不是 RAM 拷贝。
* 1.6.25 差分写:在稀疏之上再跳过「与影子一致且 3-buffer 冲刷完成」的段。 */
const UINT32 dbgT0 = ecatdbg_dwt();
APPL_InputMapping((UINT16*)aPdInputData);
const UINT32 dbgT1 = ecatdbg_dwt();
{
UINT16 o0, l0, o1, l1;
if (EcatPdo_SparseEscPlan(&o0, &l0, &o1, &l1)) {
HW_EscWriteIsr(((MEM_ADDR *)((UINT8 *)aPdInputData + o0)),
(UINT16)(nEscAddrInputData + o0), l0);
HW_EscWriteIsr(((MEM_ADDR *)((UINT8 *)aPdInputData + o1)),
(UINT16)(nEscAddrInputData + o1), l1);
} else {
UINT16 offs[4], lens[4];
const UINT8 nseg = EcatPdo_EscWritePlan(offs, lens, 4U);
if (nseg == 0U) {
HW_EscWriteIsr(((MEM_ADDR *) aPdInputData), nEscAddrInputData, nPdInputSize );
} else {
for (UINT8 i = 0U; i < nseg; ++i) {
HW_EscWriteIsr(((MEM_ADDR *)((UINT8 *)aPdInputData + offs[i])),
(UINT16)(nEscAddrInputData + offs[i]), lens[i]);
}
}
}
g_dbg_inWriteCyc += ecatdbg_dwt() - dbgT1;
+11 -1
View File
@@ -102,6 +102,16 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
* 验收看 0x2000 sub10(EscWrite 周期)与 load_perm,同工况开关各测一轮。
*/
#define CONFIG_ECAT_SPARSE_ESC_WRITE 1
/**
* @brief 输入 PDO 差分写 ESC(1.6.25 P1):在稀疏写之上,按「段头(0..543)+段尾(544..599)」
* 与上次写入影子比对;内容稳定且冲刷完成则整段跳过,省 PRAM 字节。
* @note 3-buffer 安全:SM3 三缓冲轮转,不能只对「上一拍发布镜像」比——目标缓冲
* 里是 2 拍前的内容。采用「变化后连续写 3 拍冲刷」协议:变化当拍写并置
* flush=2,随后 2 拍强制写完 3 个缓冲,稳定后才允许跳过。全段跳过时强制
* 写末 2 字节完成 SM 事件应答。0=关闭,退回稀疏写/整包。SII/主站解码不变。
* 验收:CONFIG_ECAT_DIFF_STATS 取数后对拍 sub10;或与 1.6.22 基线 A/B。
*/
#define CONFIG_ECAT_DIFF_ESC_WRITE 1
/** @brief 启动横幅与 main 进度打印。 */
#define CONFIG_APP_BANNER_LOG_ENABLE 1
@@ -137,7 +147,7 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
* ============================================================ */
constexpr uint32_t APP_FW_VERSION_MAJOR = 1U;
constexpr uint32_t APP_FW_VERSION_MINOR = 6U;
constexpr uint32_t APP_FW_VERSION_PATCH = 24U; /* 1.6.24:pmDrive 进 D1/D2/D3 移除读回验证。板级实测:进入睡眠后任何 HBI 访问(含读,手册 6.3.4.2 只说写唤醒,芯片实际按主机 CS 活动判定)都会 <50ms 触发唤醒回 D0——读回验证自证失败,改为置信构造返回值(READY=0|SLEEP_EN=1|MODE=op);「读状态」语义改为"若在睡眠则主动打破睡眠"。 */ /* 1.6.23:LAN9252 芯片级电源管理诊断路径。EscLan9252::pmDrive()(0xA2 action 30~34:30 读状态/31 进 D1/32 进 D2/33 进 D3/34 手动唤醒),按 DS 6.3 写 PMT_CTRL(0x084) PM_MODE+PM_SLEEP_EN(分两次写);D3 前置两 PHY General Power Down(经 ESC 核心 MII 管理 0x0510-0x0514,PHY A 地址 0/2 运行期判定),唤醒=HBI 写 BYTE_TEST 后轮询 BYTE_TEST/READY(DWT CYCCNT 忙等,50µs 步进、上限 10ms——0xA2 在 CS EXTI ISR 上下文执行,禁用 SysTickTimer::delayMs 会死等;HBI 段 maskEnter 屏 EXTI0/1 防 SYNC0 突入)。唤醒后自动恢复 PHY 上电(自协商需数秒)。休眠期间 EtherCAT 通信中断属预期,主站 wkc 报错。 */ /* 1.6.22:P1 稀疏写 ESC。PDO_InputMapping 在密排布局下按 count 只写「段1+段2有效区(0..40+count*56)」与「段3+段5(544..599)」,跳过空槽区,省 HW_EscWriteIsr 的 PRAM 字节。开关 CONFIG_ECAT_SPARSE_ESC_WRITE=1;非密排/count 撑满自动回退整包。总长 600B/SII/主站解码不变。A/B:置 0 重编即 1.6.21 写侧。 */ /* 1.6.21:段 2 改「一帧=总线电机整轮状态」。用户裁定旧需求不对:不要 win_base 滑窗扫 27 槽,也不要段 4 历史帧覆盖窗反复重发。实测病理:接 3 个电机网页却看到 5 个状态帧——固定 5 槽里空槽 node_id=0 也占位,主站不滤就多报。本版:(1) 段 2 密排,count=本拍实际电机数(0..9),空节点不占位,槽序按 node_id 升序,同号多通道取 seq 最新;主站 `for i in 0..count` 按槽内 node_id 自动识别,无需滑窗。(2) 删段 4 覆盖窗口(fillRxWindow/RxWinSlot 整套),非 STATUS 仍走段 1/3 最近一帧。(3) 差分填充:节点集合与各 seq 未变时整段复用影子,跳过 MotorState 重读与逐槽 48B 拷贝。(4) 输入 PDO 总长仍 600B,SII 无需重刷;段 2 扩到 32..543、段 3 前移到 544、段 5 仍 584。**主站解码必须同步**(decode_input/transport-ecat.js:count 密排 + 段 3 偏移 320→544)。 */
constexpr uint32_t APP_FW_VERSION_PATCH = 27U; /* A/B 结束: 恢复 1.6.25 稀疏+差分写(用户裁定, 主站侧已有后台恢复兜底) */ /* A/B C组: SPARSE=0+DIFF=0, 回 1.6.21 恒全量 600B 写 ESC(查开机楔死) */ /* 1.6.25:P1 输入 PDO 差分写 ESC(三区:段1 桥状态/段2 电机密排/段3+5 快照诊断;变化后 3-buffer 冲刷协议,稳定段跳过;全跳过写末 2 字节完成 SM)。P2 csrRead 分级:verify 参数,SM/AL/邮箱默认双读,诊断/MII busy/init 轮询单读。开关 CONFIG_ECAT_DIFF_ESC_WRITE。 */ /* 1.6.24:pmDrive 进 D1/D2/D3 移除读回验证。板级实测:进入睡眠后任何 HBI 访问(含读,手册 6.3.4.2 只说写唤醒,芯片实际按主机 CS 活动判定)都会 <50ms 触发唤醒回 D0——读回验证自证失败,改为置信构造返回值(READY=0|SLEEP_EN=1|MODE=op);「读状态」语义改为"若在睡眠则主动打破睡眠"。 */ /* 1.6.23:LAN9252 芯片级电源管理诊断路径。EscLan9252::pmDrive()(0xA2 action 30~34:30 读状态/31 进 D1/32 进 D2/33 进 D3/34 手动唤醒),按 DS 6.3 写 PMT_CTRL(0x084) PM_MODE+PM_SLEEP_EN(分两次写);D3 前置两 PHY General Power Down(经 ESC 核心 MII 管理 0x0510-0x0514,PHY A 地址 0/2 运行期判定),唤醒=HBI 写 BYTE_TEST 后轮询 BYTE_TEST/READY(DWT CYCCNT 忙等,50µs 步进、上限 10ms——0xA2 在 CS EXTI ISR 上下文执行,禁用 SysTickTimer::delayMs 会死等;HBI 段 maskEnter 屏 EXTI0/1 防 SYNC0 突入)。唤醒后自动恢复 PHY 上电(自协商需数秒)。休眠期间 EtherCAT 通信中断属预期,主站 wkc 报错。 */ /* 1.6.22:P1 稀疏写 ESC。PDO_InputMapping 在密排布局下按 count 只写「段1+段2有效区(0..40+count*56)」与「段3+段5(544..599)」,跳过空槽区,省 HW_EscWriteIsr 的 PRAM 字节。开关 CONFIG_ECAT_SPARSE_ESC_WRITE=1;非密排/count 撑满自动回退整包。总长 600B/SII/主站解码不变。A/B:置 0 重编即 1.6.21 写侧。 */ /* 1.6.21:段 2 改「一帧=总线电机整轮状态」。用户裁定旧需求不对:不要 win_base 滑窗扫 27 槽,也不要段 4 历史帧覆盖窗反复重发。实测病理:接 3 个电机网页却看到 5 个状态帧——固定 5 槽里空槽 node_id=0 也占位,主站不滤就多报。本版:(1) 段 2 密排,count=本拍实际电机数(0..9),空节点不占位,槽序按 node_id 升序,同号多通道取 seq 最新;主站 `for i in 0..count` 按槽内 node_id 自动识别,无需滑窗。(2) 删段 4 覆盖窗口(fillRxWindow/RxWinSlot 整套),非 STATUS 仍走段 1/3 最近一帧。(3) 差分填充:节点集合与各 seq 未变时整段复用影子,跳过 MotorState 重读与逐槽 48B 拷贝。(4) 输入 PDO 总长仍 600B,SII 无需重刷;段 2 扩到 32..543、段 3 前移到 544、段 5 仍 584。**主站解码必须同步**(decode_input/transport-ecat.js:count 密排 + 段 3 偏移 320→544)。 */
/* 1.6.19:[DIFF-STATS] 仪表成本终于测对了,并推翻 1.6.18 自己的判断。1.6.18 认定"是 -Oz 把 memcpy(&a,p,4) 编成函数调用",改成手拼 4 字节移位,结果反汇编出**每字 8 条 ldrb.w**、比开销从 21.3µs/拍涨到 41µs/拍 —— 方向完全反了。真根因不在这里:alignas(4) 标在数组声明上只到调用点为止,而 observe(const uint8_t*) 的形参签名不带对齐信息,优化器无从证明指针 4 字节对齐,两版都只能逐字节走。修法是对两个入参各做一次 __builtin_assume_aligned(p,4) 再走 memcpy —— 反汇编确认循环体已缩成 ldr.w/ldr.w/cmp/bne 每字 4 条 (150 字 ≈ 750 cycle ≈ 4.2µs/拍,与最初估算同量级),且整个函数 bl 归零(600B 快照拷贝也被折成字搬运)。教训:**测量工具的开销会直接变成结论**,"差分判断很贵"这句话差一点就被我自己造的假象坐实。被测量(变化字节数 25%、run 峰值 12)不受此影响,两版读数一致,仍然有效。 */
/* 1.6.18:修 [DIFF-STATS] 仪表自己的假象 —— 比对循环里 `memcpy(&a,p,4)` 在 -Oz 下被编成**函数调用**而非单条 LDR,首轮实测 compare 开销虚报成 21.3µs/拍(≈300 次调用×995 拍),若拿它当"差分判断很贵"的证据就结论反了。改成小端 4 字节移位拼装:合法、不碰严格别名、且是 Clang 认得折 LDR 的模式。顺带把 alignas(4) 的理由改成"服务整块 memcpy",比对本身不再依赖它。Code 52716→52768(+52B),代价是体积不是速度。 */
/* 1.6.17:新增 [DIFF-STATS] 临时仪表,为"输入 PDO 改差分写"取证,**零行为变更**。动机:差分写的收益完全取决于每拍真正变了多少字节、变化点碎成几个 run,而 run 碎片数决定它会不会把一次 600B 整块写退化成几百次带 PRAM_AVAIL 轮询的小写——这两个数今天没有实测依据,所以先只上仪表再决定做不做。实现:comm/ethercat_diff_stats.{h,cpp},每拍在 PDI_Isr 里与上一份已发布镜像做 u32 粒度比对,按 1s 窗口(DWT 周期,同 cpu_load 手法)滚动,getter 只回报"上一个完整窗口"以免主站读到半累积值;量为 cycles / changedMax / runsTotal / runsMax / 段1..段5 各自变化字节 / 比对自身 DWT 开销。经对象字典 0x2000 sub13..23 上报(新增 11 个只读 UINT32)。**只加 SDO 子索引、PDO 长度与映射逐字节不动 ⇒ 无需重生成 SII、无需断电冷启动**,可与主站独立升级。整模块由 CONFIG_ECAT_DIFF_STATS 门控,置 0 后与 1.6.16 等价;挂接点统一 [DIFF-STATS] 标记,删除=删 2 文件+删标记行。已知副作用:开启时 sub9/sub10 写侧耗时含比对开销,不能直接与历史 296µs/265µs 对拍,真实写侧基准须置 0 重测。 */
+7 -6
View File
@@ -10,7 +10,7 @@ commits: <no-commit — analysis-only on dirty master WIP>
## Report
**What was built** — 一次只读审计:梳理 LAN9252(EXMC 16-bit HBI Indexed)在当前固件中的内存占用与热路径耗时,回答「内存是否用得差」与「还有哪些提速空间」。结论:**DPRAM 用得满但是设计上的满,不算差;真正的速度瓶颈是 PRAM 字节搬运(约 0.44µs/B)**。已落地的优化(输出 12B 探针、段 2 影子复用、稀疏写)方向正确;下一步若继续提速,优先做输入 PDO 差分写,其次评估 csrRead 双读校验的分级。
**What was built** — LAN9252 使用效率审计 + 后续 P1/P2 实现。结论:**DPRAM 用得满但是设计上的满,不算差;真正的速度瓶颈是 PRAM 字节搬运(约 0.44µs/B)**。1.6.25 落地:(1) 输入 PDO 三区差分写 ESC(段1 桥状态 / 段2 电机密排 / 段3+5 快照诊断,变化后 3-buffer 冲刷 3 拍才允许跳过);(2) csrRead 增加 verify 参数,SM/AL/邮箱保持双读,诊断/MII busy/init 轮询单读。
**Verification** — 本报告为静态代码审计,对照仓库内 DWT 实测注释与布局常量交叉验证;未在本回合重跑上机测量(DIFF-STATS 当前 `CONFIG_ECAT_DIFF_STATS=0`,写侧基线须置 0 重测才可对拍)。
@@ -18,6 +18,7 @@ commits: <no-commit — analysis-only on dirty master WIP>
1. 先验怀疑「内存差」不成立:3-buffer 硬上限把 out+in 钉在 ≤1024B,当前 984B 已是极限附近。
2. 1.6.0 PRAM 批量弹 DWORD 因短读损坏回退——任何再加速不能走回未弄清语义的批量模式。
3. 工作区 master 上有未提交的 1.6.22–1.6.24(稀疏写 + pmDrive),审计不改动这些 WIP。
4. 1.6.25 已实现 P1 差分写(三区 + 3-buffer 冲刷)与 P2 csrRead verify 分级;上机对拍 sub10 待做。
---
@@ -134,16 +135,16 @@ commits: <no-commit — analysis-only on dirty master WIP>
## [S3] Out of Scope
- 本次不改代码、不烧录、不重生成 SII。
- 不重生成 SII、不烧录、不改主站解码。
- 不审计主站侧解码或 CAN 桥竞态(另见 1.6.9–1.6.21 修复记录)。
- 不处理工作区未提交的 pmDrive/稀疏写合入决策(见 Tasks 遗留项)。
- 不重新设计 PDO 布局或 EEPROM。
- 上机 A/B 对拍(T3/T6)待现场执行。
## Tasks
- [x] T1: 审计 DPRAM/MCU 内存占用与 PRAM 热路径 — acceptance: 本报告 S2.1/S2.2 有依据与数字 (covers: S2)
- [x] T2: 给出优化优先级与不做清单 — acceptance: S2.3 含 ROI/风险/验收 (covers: S2)
- [ ] T3: (可选后续)开 DIFF-STATS 采一轮工况数据 — acceptance: 记录 sub15 changedMax、sub17 runsMax、各段字节,再决定是否做 P1 (covers: S2.3)
- [ ] T4: (可选后续)实现输入 PDO 差分写 — acceptance: 变化少时 sub10 下降;碎片超限回退;SII 不变 (covers: S2.3; depends: T3)
- [ ] T5: (可选后续)csrRead 分级单读 — acceptance: 白名单路径单读,默认路径保持双读;无新增偶发错读 (covers: S2.3)
- [x] T4: (1.6.25)实现输入 PDO 差分写 — acceptance: 三区影子+3-buffer 冲刷;全跳过写末 2 字节;SII 不变;CONFIG_ECAT_DIFF_ESC_WRITE 门控 (covers: S2.3)
- [x] T5: (1.6.25)csrRead 分级单读 — acceptance: verify 参数;SM/AL 默认双读;MII busy/init/诊断回读单读 (covers: S2.3)
- [ ] T3: (后续)开 DIFF-STATS + 上机对拍 sub10 — acceptance: 记录写侧耗时 A/B(差分开/关)(covers: S2.3)
- [ ] T6: (遗留)验收工作区稀疏写 A/B — acceptance: CONFIG_ECAT_SPARSE_ESC_WRITE 0/1 同工况 sub10 对比入档 (covers: S2.3)