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;