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 重测。 */