feat(comm): 1.6.22 稀疏写 ESC + 1.6.23~24 pmDrive 电源管理; LAN9252 性能审计报告

This commit is contained in:
cbb
2026-09-12 01:59:42 +08:00
parent 1742d9a759
commit 9ca3db26f8
8 changed files with 434 additions and 5 deletions
+189
View File
@@ -80,6 +80,28 @@ constexpr uint32_t PRAM_BUSY = 1UL << 31; /**< PRAM_*_CMD:写 1 启动,完成
constexpr uint32_t PRAM_AVAIL = 1UL << 0; /**< PRAM_*_CMD:数据可用(读)/空间可用(写) */
constexpr uint32_t POLL_MAX = 2000UL; /**< busy/avail 轮询上限(~ms 级,防芯片异常时死等) */
/* 电源管理(DS 6.3/表 5-1):芯片级睡眠 D1-D3。
* READY=0 期间仅 HW_CFG/PMT_CTRL/BYTE_TEST/RESET_CTL 四个寄存器可访问(DS 6.2.3),
* 恰好覆盖睡眠进入与唤醒流程所需;PMT_CTRL 可在睡眠态安全读(DS 6.3.5.1 注)。 */
constexpr uint16_t LAN9252_REG_HW_CFG = 0x0074U; /**< 硬件配置:bit27=READY(与 PMT_CTRL bit0 同源,DS 6.3.5.1)。 */
constexpr uint16_t LAN9252_REG_PMT_CTRL = 0x0084U; /**< 功耗管理控制:bit0=READY,bit28=PM_SLEEP_EN,bit31:29=PM_MODE。 */
constexpr uint32_t PMT_CTRL_READY = 1UL << 0;
constexpr uint32_t PMT_CTRL_PM_MODE_MSK = 7UL << 29; /**< 001=D1 010=D2 011=D3。 */
constexpr uint32_t PMT_CTRL_PM_SLEEP_EN = 1UL << 28; /**< 置 1 进入 PM_MODE 指定的睡眠档(须与 PM_MODE 分两次写)。 */
/* ESC 核心 MII 管理(ETG.1000.4 ESC 寄存器 0x0510-0x0514,经 csrRead/csrWrite 间接访问;
* 表 5-1 证实 PHY 寄存器不在 HBI 直接空间)。用于 D3 前置的 PHY General Power Down
* (DS 6.3.4:D3 要求两 PHY 先 General Power Down)与唤醒后恢复。 */
constexpr uint16_t ESC_REG_MII_CTRL = 0x0510U; /**< bit0=CMD(写 1 启动自清) bit1=WRITE bit8=BUSY。 */
constexpr uint16_t ESC_REG_MII_ADDR = 0x0512U; /**< bit4:0=PHY_ADDR bit15:5=REG_ADDR。 */
constexpr uint16_t ESC_REG_MII_DATA = 0x0514U; /**< bit15:0=MII 数据。 */
constexpr uint16_t MII_CTRL_CMD = 1U << 0;
constexpr uint16_t MII_CTRL_WRITE = 1U << 1;
constexpr uint16_t MII_CTRL_BUSY = 1U << 8;
constexpr uint8_t MII_REG_BMCR = 0U; /**< IEEE 802.3 Basic Control 寄存器 0。 */
constexpr uint16_t MII_BMCR_POWER_DOWN = 1U << 11; /**< PHY_PWR_DWN(DS 11.2.8)。 */
constexpr uint8_t MII_PHY_ADDR_B = 1U; /**< PHY B 地址恒 1(DS 11.1.1);A 模式相关 0/2,运行期判定。 */
__attribute__((section(".ramfunc"), noinline))
void setHbiIndex(uint16_t addr)
{
@@ -126,11 +148,86 @@ void writeReg32(uint16_t addr, uint32_t v)
writeDataLow((uint16_t)(v & 0xFFFFU));
writeDataHigh((uint16_t)(v >> 16));
}
/* ---- 电源管理 helper(仅 pmDrive 诊断路径使用) ---- */
/** @brief DWT CYCCNT 忙等微秒。
* @note EXTI ISR 上下文(0xA2 在 AppSpiCommand::onCsRelease 内同步执行)禁止用
* SysTickTimer::delayMs:其基于 SysTick 中断递增 s_ms,中断被 CS EXTI
* 压住时计数冻结会死等。DWT CYCCNT 已由 init() 使能,纯寄存器读,ISR 安全。 */
__attribute__((section(".ramfunc"), noinline))
void pmBusyWaitUs(uint32_t us)
{
const uint32_t start = DWT->CYCCNT;
const uint32_t cycles = us * (SystemCoreClock / 1000000U);
while ((DWT->CYCCNT - start) < cycles) { }
}
/** @brief ESC 核心 MII 管理事务(读/写一个 PHY 寄存器)。
* @note 调用方必须已持 HBI 互斥(pmDrive 的 maskEnter 段内)。MII 事务典型 ~25µs
* (2.5MHz MDIO × 32bit 帧结构),BUSY 轮询上限 POLL_MAX 防芯片异常死等。 */
bool miiXfer(uint8_t phyAddr, uint8_t regAddr, bool write, uint16_t &data)
{
EscLan9252::csrWrite(ESC_REG_MII_ADDR, ((uint32_t)regAddr << 5) | phyAddr, 2U);
if (write) {
EscLan9252::csrWrite(ESC_REG_MII_DATA, data, 2U);
}
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 (!write) {
data = (uint16_t)EscLan9252::csrRead(ESC_REG_MII_DATA, 2U);
}
return true;
}
}
return false;
}
/** @brief 置/清两 PHY 的 BMCR.PowerDown(D3 进入前置要求/唤醒后恢复)。
* @return 全部事务成功 true。PHY A 地址模式相关(DS 11.1.1:0 或 2,随 chip_mode_strap),
* 以 BMCR 读回有效性判定(0x0000/0xFFFF=无响应),结果缓存在静态变量。 */
bool phySetPowerDown(bool down)
{
static uint8_t addrA = 0xFFU; /* 0xFF=未判定 */
if (addrA == 0xFFU) {
addrA = 0U;
uint16_t probe = 0xFFFFU;
if (!miiXfer(0U, MII_REG_BMCR, false, probe) ||
probe == 0x0000U || probe == 0xFFFFU) {
addrA = 2U;
}
}
bool ok = true;
const uint8_t addrs[2] = { addrA, MII_PHY_ADDR_B };
for (uint8_t i = 0U; i < 2U; i++) {
uint16_t bmcr = 0U;
ok = miiXfer(addrs[i], MII_REG_BMCR, false, bmcr) && ok;
if (down) {
bmcr |= MII_BMCR_POWER_DOWN;
} else {
bmcr &= (uint16_t)~MII_BMCR_POWER_DOWN;
}
ok = miiXfer(addrs[i], MII_REG_BMCR, true, bmcr) && ok;
}
return ok;
}
/** @brief 把 PMT_CTRL 值打包成单字节状态:bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE。 */
uint8_t pmPackStatus(uint32_t pmt)
{
return (uint8_t)(((pmt & PMT_CTRL_READY) ? 0x01U : 0U) |
((pmt & PMT_CTRL_PM_SLEEP_EN) ? 0x02U : 0U) |
(((pmt >> 29) & 3U) << 2));
}
} /* anonymous namespace */
bool EscLan9252::s_busReady = false;
bool EscLan9252::s_coreReady = false;
bool EscLan9252::s_resetDone = false;
bool EscLan9252::s_pmPhyDown = false;
void EscLan9252::hardwareReset()
{
@@ -193,6 +290,98 @@ uint8_t EscLan9252::nrstDrive(uint8_t action)
return level ? 1U : 0U;
}
uint8_t EscLan9252::pmDrive(uint8_t op)
{
if (op > 4U) {
return 0xEEU;
}
if (!s_busReady) {
printf("[ECAT][PM] op%u rejected: HBI bus not ready\r\n", op);
return 0xE1U;
}
/* 0xA2 在 CS EXTI ISR 上下文执行(AppSpiCommand::onCsRelease 同步调用)。
* HBI Indexed 是两步事务(写 IDX→读写 DATA),而 SYNC0(EXTI0,preempt=0)是
* 全系统最高优先级、BASEPRI 挡不住,与主循环 HW_* 路径相同必须关 NVIC:
* maskEnter 屏蔽 EXTI0/EXTI1 并返回先前状态,ISR 里调用同样正确(纯 NVIC
* 寄存器保存/恢复,挂起的中断在 maskExit 后补执行)。
* 残余竞态窗口:CS EXTI 优先级更高,可抢占主循环持锁中的 HBI 事务半截——
* 人工低频诊断可接受(continuityDrive 直接切走 EXMC 引脚破坏性更大)。 */
const uint32_t mask = EscExmcBus::maskEnter();
uint8_t status;
if (op == 0U) {
/* 状态查询。注意:1.6.24 实测 HBI 读事务本身就会唤醒睡眠中的芯片,
* 本操作的语义 = "读状态(若在睡眠则主动打破睡眠)",非无侵入观测。 */
const uint32_t pmt = readReg32(LAN9252_REG_PMT_CTRL);
status = pmPackStatus(pmt);
printf("[ECAT][PM] status: PMT_CTRL=0x%08lX READY=%u SLEEP_EN=%u MODE=D%u\r\n",
(unsigned long)pmt,
(unsigned)((status >> 0) & 1U), (unsigned)((status >> 1) & 1U),
(unsigned)((status >> 2) & 3U));
} else if (op <= 3U) {
/* 进入 D1/D2/D3。D3 最深(晶振/PLL 全关,仅手动唤醒),手册要求先把两个 PHY
* 置 General Power Down,否则 PLL 在 D2 不真关/D3 直接不满足前置。 */
if (op == 3U) {
s_pmPhyDown = phySetPowerDown(true);
printf("[ECAT][PM] D3 pre: both PHY power-down %s\r\n",
s_pmPhyDown ? "OK" : "FAIL (entering anyway)");
}
uint32_t v = readReg32(LAN9252_REG_PMT_CTRL);
v = (v & ~PMT_CTRL_PM_MODE_MSK) | ((uint32_t)op << 29);
/* PM_MODE 与 PM_SLEEP_EN 必须分两次写(DS 6.3.5.1 注:同一写中改 MODE 置 EN 无效)。 */
writeReg32(LAN9252_REG_PMT_CTRL, v);
writeReg32(LAN9252_REG_PMT_CTRL, v | PMT_CTRL_PM_SLEEP_EN);
/* 1.6.24 实测:进入睡眠后任何 HBI 访问(含读!)都会立即触发唤醒——
* 手册 6.3.4.2 只说 HBI "write" 唤醒,但芯片实际按主机 CS 活动判定,
* 写入后的读回验证会在 <50ms 内把芯片叫回 D0(板级复现:进 D1 后
* 3s/6s 静默仍自动回 D0,唯读回验证为已知访问;去掉后待验证)。
* 故此处置信构造返回值,不再读回;睡眠态观测交给"读状态"(读即唤醒,
* 语义=主动打破睡眠),页面轮询须在睡眠态暂停。 */
status = (uint8_t)(0x02U | ((uint32_t)op << 2)); /* READY=0 | SLEEP_EN=1 | MODE=op */
static const char *const kMode[4] = {"D0", "D1", "D2", "D3"};
printf("[ECAT][PM] enter %s: entered, no read-back (HBI read wakes chip)\r\n",
kMode[op]);
printf("[ECAT][PM] WARNING: EtherCAT comms halted, wake with 0xA2 action 34\r\n");
} else {
/* 手动唤醒(DS 6.3.4.2):对 BYTE_TEST 做一次 HBI 写触发,随后轮询
* BYTE_TEST=0x87654321(接口恢复)→ PMT_CTRL.READY=1(回 D0 且 PLL 稳定)。
* 手册标称 ≤2ms,轮询 50µs 步进、上限 10ms——EXTI ISR 内不做长阻塞。 */
writeReg32(LAN9252_REG_BYTE_TEST, LAN9252_BYTE_TEST_VALUE);
bool awake = false;
for (uint32_t i = 0U; i < 200U; i++) {
pmBusyWaitUs(50U);
if (readReg32(LAN9252_REG_BYTE_TEST) == LAN9252_BYTE_TEST_VALUE) {
awake = true;
break;
}
}
if (awake) {
for (uint32_t i = 0U; i < 200U; i++) {
pmBusyWaitUs(50U);
if ((readReg32(LAN9252_REG_PMT_CTRL) & PMT_CTRL_READY) != 0U) {
break;
}
}
awake = (readReg32(LAN9252_REG_PMT_CTRL) & PMT_CTRL_READY) != 0U;
}
if (awake && s_pmPhyDown) {
/* D3 唤醒后恢复 PHY 上电,链路重新自协商(数秒后 LINK 恢复)。 */
const bool restored = phySetPowerDown(false);
s_pmPhyDown = !restored;
printf("[ECAT][PM] wake: PHY power-up %s (autoneg re-negotiating)\r\n",
restored ? "OK" : "FAIL (retry action 34)");
}
status = awake ? pmPackStatus(readReg32(LAN9252_REG_PMT_CTRL)) : 0U;
printf("[ECAT][PM] wake: %s -> status=0x%02X (READY=%u SLEEP_EN=%u)\r\n",
awake ? "OK" : "TIMEOUT (10ms, poll action 30 / retry 34)",
status, (unsigned)(status & 1U), (unsigned)((status >> 1) & 1U));
}
EscExmcBus::maskExit(mask);
return status;
}
bool EscLan9252::init()
{
s_busReady = false; /* 1.5.27:s_busReady 语义 = init 完整成功(EXMC+探针+BYTE_TEST) */
+13
View File
@@ -46,6 +46,18 @@ public:
* 驱动高读回低 => ECAT_NRST 被按死在复位,LAS9252 永在复位态。 */
static uint8_t nrstDrive(uint8_t action);
/** @brief 诊断:LAN9252 芯片级电源管理(休眠/唤醒,SPI 0xA2 action 30+ 转发到此)。
* @param op 0=读状态 1=进 D1 2=进 D2 3=进 D3(先两 PHY General Power Down) 4=手动唤醒
* @return 打包状态字节 bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE;
* 唤醒 op 另有 1=成功 0=超时;0xEE=无效 op;0xE1=HBI 总线未就绪。
* @note DS 6.3:PMT_CTRL(0x084) PM_MODE[31:29] 选档 + PM_SLEEP_EN(bit28) 进入,
* 两字段必须分两次写。D1/D2/D3 下主机接口停摆、EtherCAT 通信中断(主站
* wkc 报错),唤醒后 PM_MODE/PM_SLEEP_EN 自清、寄存器配置保留、READY 回高
* (正常 ≤2ms)。D3 最深(晶振/PLL 全关)且手册要求先置两 PHY BMCR.PowerDown
* (经 ESC 核心 MII 管理 0x0510 间接访问,PHY A 地址模式相关 0/2 运行期判定),
* 唤醒后本函数自动恢复 PHY 上电,链路重新自协商需数秒。 */
static uint8_t pmDrive(uint8_t op);
/**
* @brief 读取 ESC 芯片 ID(INIT-ONLY,供 init 打印诊断用)。
* @return uint32_t 32-bit 芯片 ID(期望值 0x87654321)。
@@ -102,6 +114,7 @@ private:
static bool s_busReady; /**< init 完整成功(EXMC+IDX 探针+BYTE_TEST)后置位。 */
static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位(1.5.39)。 */
static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */
static bool s_pmPhyDown; /**< pmDrive 进 D3 时两 PHY 已被置 PowerDown,唤醒后需恢复。 */
};
#endif /* COMM_ESC_LAN9252_H */
+39
View File
@@ -154,6 +154,37 @@ void EthercatPdo::writeInputs(uint8_t *esc)
++s_inputCycles;
}
bool EthercatPdo::sparseEscWritePlan(uint16_t *off0, uint16_t *len0,
uint16_t *off1, uint16_t *len1)
{
if (off0 == nullptr || len0 == nullptr || off1 == nullptr || len1 == nullptr) {
return false;
}
#if !CONFIG_ECAT_SPARSE_ESC_WRITE
return false;
#else
/* 仅 1.6.21 密排布局:count 有效且两段之间确有空洞才分段。
* magic 不对 / count 越界 / count 撑满到与段3相接 → 整包,避免陈旧空洞被误读。 */
if (s_in[ECAT_MOTW_OFF_MAGIC] != kEcatMotLayoutMagic) {
return false;
}
const uint8_t count = s_in[ECAT_MOTW_OFF_COUNT];
if (count > kEcatMotSlotsPerPdo) {
return false;
}
const uint16_t headEnd =
(uint16_t)(ECAT_MOTW_OFF_SLOTS + (uint16_t)count * kEcatMotSlotSize);
if (headEnd >= ECAT_XRX_OFF_CH) {
return false; /* 9 电机满配:A+B 无缝,整包更省一次 EXMC 事务 */
}
*off0 = 0U;
*len0 = headEnd;
*off1 = ECAT_XRX_OFF_CH;
*len1 = (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH);
return true;
#endif
}
void EthercatPdo::snapshotOutput(uint8_t *dst)
{
if (dst == nullptr) {
@@ -186,4 +217,12 @@ void EcatPdo_WriteInputs(uint8_t *esc)
EthercatPdo::writeInputs(esc);
}
/* 1.6.22 稀疏写计划:SSC PDO_InputMapping 据此决定 HW_EscWriteIsr 写几段。
* 返回 1=两段有效;0=回退整包。 */
uint8_t EcatPdo_SparseEscPlan(uint16_t *off0, uint16_t *len0,
uint16_t *off1, uint16_t *len1)
{
return EthercatPdo::sparseEscWritePlan(off0, len0, off1, len1) ? 1U : 0U;
}
} /* extern "C" */
+12 -1
View File
@@ -67,10 +67,21 @@ public:
/**
* @brief 交回输入过程数据(从站→主站)。
* @param[out] esc 指向 ESC 输入缓冲的 kPdoInBytes 字节。
* @note 由 APPL_InputMapping() 每周期调用。
* @note 由 APPL_InputMapping() 每周期调用。只填 RAM/对象存储;
* 真正写 ESC 的字节量由 PDO_InputMapping 经 sparseEscWritePlan 决定。
*/
static void writeInputs(uint8_t *esc);
/**
* @brief 稀疏写 ESC 的两段计划(1.6.22):段1+段2有效区、段3+段5;跳过空槽区。
* @param[out] off0/len0 第一段相对 PDO 起点的偏移与长度
* @param[out] off1/len1 第二段
* @return true=可按两段写;false=应回退整包 600B(开关关/非密排/count 越界/两段相接)
* @note 必须在 writeInputs() 之后调用,读的是已发布的 s_in。
*/
static bool sparseEscWritePlan(uint16_t *off0, uint16_t *len0,
uint16_t *off1, uint16_t *len1);
/** @brief 累计取走输出的周期数(主站在发过程数据的证据)。 */
static uint32_t outputCycles() { return s_outputCycles; }
+15 -1
View File
@@ -165,6 +165,8 @@ extern UINT32 EcatDbg_Dwt(void);
#define ecatdbg_dwt() EcatDbg_Dwt()
/* 1.6.0:输出探针(实现见 comm/ethercat_pdo.cpp,C 链接) */
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);
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) */
@@ -197,10 +199,22 @@ BOOL bInitFinished = FALSE; /** < \brief indicates if the initialization is fini
void PDO_InputMapping(void)
{
/*ECAT_DBG(1.6.0 剖分:输入映射全程 + 640B 写细分)*/
/* 1.6.22 稀疏写:APPL 仍整填 aPdInputData(对齐 SDO 0x6000),但 HW_EscWriteIsr
* 只写密排有效段+段3/5,跳过空槽区 —— 省的是 PRAM 字节时间,不是 RAM 拷贝。 */
const UINT32 dbgT0 = ecatdbg_dwt();
APPL_InputMapping((UINT16*)aPdInputData);
const UINT32 dbgT1 = ecatdbg_dwt();
HW_EscWriteIsr(((MEM_ADDR *) aPdInputData), nEscAddrInputData, nPdInputSize );
{
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 {
HW_EscWriteIsr(((MEM_ADDR *) aPdInputData), nEscAddrInputData, nPdInputSize );
}
}
g_dbg_inWriteCyc += ecatdbg_dwt() - dbgT1;
g_dbg_inCyc += ecatdbg_dwt() - dbgT0;
}
@@ -1,12 +1,15 @@
/**
* @file app_spi_cmd_exmc_diag.cpp
* @brief SPI 0xA2 EXMC/HBI 诊断命令处理器:驱动 LAN9252 通断测试 pattern / NRST 手动诊断。
* @brief SPI 0xA2 EXMC/HBI 诊断命令处理器:驱动 LAN9252 通断测试 pattern / NRST 手动诊断 / 电源管理。
*
* 网页按钮 → ESP32 /spi/send → SPI 0xA2 → 本 handler → 分流:
* action 0~10 → EscExmcBus::continuityDrive() (EXMC 数据/控制线 pattern)
* action 20~24 → EscLan9252::nrstDrive() (ECAT_NRST/PB10 拉低/拉高/浮空/回读)
* action 30~34 → EscLan9252::pmDrive() (电源管理:读状态/进D1/D2/D3/手动唤醒)
* 立即返回 ACK,不阻塞 SPI 总线。电平保持到下次调用或 action=0(stop)。
* NRST action 返回的 status 字节 = 实际引脚电平(0/1),非 OK 码。
* PM action 返回的 status 字节 = bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE;
* 唤醒(34)另有 1=成功 0=超时;0xEE=无效 action,0xE1=HBI 未就绪。
*/
#include "device/spi/spi_command_handler.h"
#include "device/spi/app_spi_command.h"
@@ -29,7 +32,9 @@ public:
uint8_t status = 0xEE;
if (ctx.payload_len >= 1) {
const uint8_t action = ctx.payload[0];
if (action >= 20U) {
if (action >= 30U) {
status = EscLan9252::pmDrive(action - 30U);
} else if (action >= 20U) {
status = EscLan9252::nrstDrive(action - 20U);
} else {
status = EscExmcBus::continuityDrive(action);
+10 -1
View File
@@ -93,6 +93,15 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
* 实测见 sub23),因此 **sub9/sub10 的写侧耗时会包含这部分**,不能直接与历史
* 296µs/265µs 对拍——要看差分潜力就只看 sub13..22,要看真实写侧耗时请置 0 重测。 */
#define CONFIG_ECAT_DIFF_STATS 0
/**
* @brief 输入 PDO 稀疏写 ESC(1.6.22 P1):按密排 count 只写「段1+段2有效区」与「段3+段5」,
* 跳过未用到的空槽区(40+count*56 .. 543),省 HW_EscWriteIsr 的 PRAM 字节数。
* @note 0=恒 600B 整包写(A/B 基线);1=分段写。非 1.6.21 密排布局(magic≠0xA5)或
* count 撑满时自动回退整包。**总长/SII/主站解码均不变**;空洞区在 ESC 中为该
* SM 缓冲上次残留,主站必须继续只按 count 迭代(1.6.21 已如此)。
* 验收看 0x2000 sub10(EscWrite 周期)与 load_perm,同工况开关各测一轮。
*/
#define CONFIG_ECAT_SPARSE_ESC_WRITE 1
/** @brief 启动横幅与 main 进度打印。 */
#define CONFIG_APP_BANNER_LOG_ENABLE 1
@@ -128,7 +137,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 = 21U; /* 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 = 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)。 */
/* 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 重测。 */
+149
View File
@@ -0,0 +1,149 @@
---
feature: lan9252-perf-audit
status: delivered
updated: 2026-02-14
branch: master
commits: <no-commit — analysis-only on dirty master WIP>
---
# LAN9252 使用效率审计
## Report
**What was built** — 一次只读审计:梳理 LAN9252(EXMC 16-bit HBI Indexed)在当前固件中的内存占用与热路径耗时,回答「内存是否用得差」与「还有哪些提速空间」。结论:**DPRAM 用得满但是设计上的满,不算差;真正的速度瓶颈是 PRAM 字节搬运(约 0.44µs/B)**。已落地的优化(输出 12B 探针、段 2 影子复用、稀疏写)方向正确;下一步若继续提速,优先做输入 PDO 差分写,其次评估 csrRead 双读校验的分级。
**Verification** — 本报告为静态代码审计,对照仓库内 DWT 实测注释与布局常量交叉验证;未在本回合重跑上机测量(DIFF-STATS 当前 `CONFIG_ECAT_DIFF_STATS=0`,写侧基线须置 0 重测才可对拍)。
**Journey log**
1. 先验怀疑「内存差」不成立:3-buffer 硬上限把 out+in 钉在 ≤1024B,当前 984B 已是极限附近。
2. 1.6.0 PRAM 批量弹 DWORD 因短读损坏回退——任何再加速不能走回未弄清语义的批量模式。
3. 工作区 master 上有未提交的 1.6.22–1.6.24(稀疏写 + pmDrive),审计不改动这些 WIP。
---
## [S1] Problem
用户问题:
1. LAN9252 使用是否有提升效率/速度的优化空间?
2. 现在是不是内存空间使用比较差?
需要基于当前代码(含未提交 1.6.22 稀疏写)给出可执行的结论与优先级,而不是泛泛「还能优化」。
## [S2] Design / 分析结论
### S2.1 内存:不是“差”,是 3-buffer 硬上限下的近满设计
**ESC 片内 DPRAM 4KB 布局(LAN9252)**
| 区域 | 范围 | 占用 |
|------|------|------|
| 邮箱 SM0/SM1 | 0x1000–0x13FF | 2×256B 固定 |
| 过程数据 SM2(out)+SM3(in) | 0x1400–0x1FFF | 3072B 可用 |
| 3-buffer 实占 | (384+600)×3 = **2952B** | 余 **120B** |
推导见 `comm/ethercat_pdo_size.h`:`(out+in)×3 ≤ 3072` ⇒ **out+in ≤ 1024B 是硬上限**。当前 384+600=984B,已用到 96%。
若将来还要更大,只有两条路,都有代价:
- 邮箱压到 `MIN_MBX_SIZE(0x22)` → 上限约 1280B,拖慢 SDO/FoE
- 改 1-buffer → 上限 3072B,失去三缓冲并发保护
**结论:DPRAM 满是约束结果,不是浪费。段 2 密排(1.6.21)已经把空节点从有效载荷里挤掉。**
**MCU 侧 RAM**
- `aPdInputData` + `aPdOutputData` = 1024B(SSC 对象存储)
- `EthercatPdo::s_out/s_in/s_build` = 3×384~600B 级
- 段 2 影子 `s_motShadow` ≈ 512B 量级
- DIFF-STATS 自持 600B 快照(仅开启时)
相对 GD32E508 的 128KB SRAM 可忽略。**MCU 内存不是瓶颈。**
### S2.2 速度:热成本在 PRAM 搬运,不在“存不下”
**已实测的通道速率(仓库内 DWT 注释,0x2000 sub9/sub10)**
| 场景 | 耗时 | 占 1ms 周期 |
|------|------|-------------|
| 输入 640B 整包写(历史) | 296µs | 29.7% |
| 输入 496B 整包写(1.6.1) | 218.7µs | 21.9% |
| 输入 600B 整包写(1.6.15) | ≈265µs(600×0.441) | 26.5% |
| 满载 1024B PRAM(历史估算) | ≈218µs(更早时序) | — |
| 输出 384B 全量读(被探针跳过) | ≈190µs | 已优化 |
通道近似线性:**≈0.44–0.463 µs/B**。写多少、读多少,时间就按字节缩放。
**已落地的减法(正确方向)**
1. **输出按需读**(1.6.0,`ecatappl.c`):每拍先读 12B 头探针;seq 无变化则跳过 384B 全量读,只补读末字应答。
2. **段 2 差分填充**(1.6.21,`ethercat_can_bridge.cpp::fillMotorRound`):节点集合与各 seq 未变时整段复用影子,跳过 MotorState 重读与逐槽拷贝——省的是 MCU 侧填充,不直接省 PRAM 字节。
3. **稀疏写 ESC**(1.6.22,工作区未提交):`CONFIG_ECAT_SPARSE_ESC_WRITE=1` 时,密排 count 有效则只写「段1+段2有效区」与「段3+段5」,跳过空槽区。count=3 时约省 (9−3)×56 ≈ 336B PRAM 写 ≈ 150µs/拍。magic 不对/count 撑满自动回退整包。
4. **输入 PDO 瘦身**(1.6.1,历史):640→496 已证明“减字节=减时间”;1.6.15 为批量窗扩回 600 是容量权衡,不是速度回退。
**未做 / 被否决的加速**
| 尝试 | 结果 | 依据 |
|------|------|------|
| PRAM 批量弹 DWORD(用 AVAIL_CNT) | **已回退**(1.6.2) | 短读首 DWORD 新鲜、后续取陈旧项;窗口冻结 |
| EXMC 慢档时序试速 | 无效 | 当年失败根因是访问模型错误,与时序无关 |
| 压缩总线时序 | **不建议** | BUSLAT 1→4 是为温漂可靠(+3HCLK≈16.7ns/访问) |
### S2.3 仍有空间的优化点(按 ROI 排序)
#### P1 — 输入 PDO 真正的差分写 ESC
- **现状**:填充侧有段 2 影子;**写侧**仍是整包 600B 或稀疏两段,段 1/3/5 每拍全写。
- **前提**:开 `CONFIG_ECAT_DIFF_STATS=1` 采 0x2000 sub13..23(changedMax / runsMax / 各段变化字节),拿分布再定 A1(语义分区条件写)或 A2(块粒度差分,N 由 runsMax 定)。
- **预期**:若稳定工况下每拍变化字节数远小于 600,写侧可再省数十至上百 µs。
- **风险**:run 碎片会把一次大写退化成多次带 AVAIL 轮询的小写;必须设碎片上限,超限回退整段/整包。
- **验收**:`CONFIG_ECAT_DIFF_STATS=0` 对拍 sub10 与 load_perm;布局/SII/主站解码不变。
#### P2 — csrRead 双读校验分级
- **现状**:`EscLan9252::csrRead` 对每次事务读两遍 CSR_DATA 并比较(1.5.59),不一致整事务重跑一次。对 EXMC 毛刺防护有效,但 **CSR 路径访问次数近似翻倍**。
- **热路径调用方**:AL Event(每拍 1–2 次)、邮箱长度/SM 状态、看门狗状态、LED、诊断。
- **可行做法**:提供 `csrReadOnce`(或按寄存器关键级别开关);**几何/状态关键寄存器(SM、AL、邮箱)保持双读**;LED/诊断/低频路径可单读。
- **风险**:去掉防护的路径可能偶发读错;须限定白名单,不可一刀切。
- **预期**:CSR 间接访问(多步 HBI)减少约一半;相对 600B PRAM 写占比更小,属次优先。
#### P3 — 稀疏写收益标定(已实现,待验收)
- 工作区已实现 `sparseEscWritePlan` + `CONFIG_ECAT_SPARSE_ESC_WRITE=1`。
- **验收动作**:同工况开关 0/1 各测一轮 sub10;count 越小收益越大。
- 空洞区为 SM 缓冲上次残留,主站必须只按 count 迭代(1.6.21 已如此)。
#### 不建议再碰
- **EXMC 时序收紧**:BUSLAT=4 / DSET=20 为全温区可靠性的已知代价;已有“慢档无效、访问模型才是当年根因”的记录。除非板级时序重测给出明确裕量,否则不动。
- **恢复 PRAM 批量弹 DWORD**:短读语义未弄清前禁止。
- **改 1-buffer 扩 DPRAM**:失去三缓冲,并发读写会拿到半新半旧过程数据。
### S2.4 访问模型备忘(审计时反复踩过的坑)
```
主站帧
→ LAN9252 DPRAM (4KB)
邮箱 0x1000+ / PDO 0x1400+
← PDI (HBI Indexed)
HBI_IDX_0 / HBI_DATA_0 ← 仅系统 CSR + 访问端口
ECAT_CSR_CMD/DATA 0x304/0x300 ← 核心 CSR 0x0000–0x0FFF
PRAM FIFO 0x000–0x03C + 0x308–0x314 ← 过程 RAM 0x1000–0x1FFF
→ EXMC NE0 @ 0x60000000, 16-bit
```
核心 CSR **不能**直读 HBI 索引(1.5.37 之前全 0 的根因)。过程数据 **必须**走 PRAM FIFO。
## [S3] Out of Scope
- 本次不改代码、不烧录、不重生成 SII。
- 不审计主站侧解码或 CAN 桥竞态(另见 1.6.9–1.6.21 修复记录)。
- 不处理工作区未提交的 pmDrive/稀疏写合入决策(见 Tasks 遗留项)。
- 不重新设计 PDO 布局或 EEPROM。
## 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)
- [ ] T6: (遗留)验收工作区稀疏写 A/B — acceptance: CONFIG_ECAT_SPARSE_ESC_WRITE 0/1 同工况 sub10 对比入档 (covers: S2.3)