Compare commits
3
Commits
959e2aef6e
...
6afaacd03a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6afaacd03a | ||
|
|
9ca3db26f8 | ||
|
|
1742d9a759 |
@@ -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 PRAM_AVAIL = 1UL << 0; /**< PRAM_*_CMD:数据可用(读)/空间可用(写) */
|
||||||
constexpr uint32_t POLL_MAX = 2000UL; /**< busy/avail 轮询上限(~ms 级,防芯片异常时死等) */
|
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))
|
__attribute__((section(".ramfunc"), noinline))
|
||||||
void setHbiIndex(uint16_t addr)
|
void setHbiIndex(uint16_t addr)
|
||||||
{
|
{
|
||||||
@@ -126,11 +148,86 @@ void writeReg32(uint16_t addr, uint32_t v)
|
|||||||
writeDataLow((uint16_t)(v & 0xFFFFU));
|
writeDataLow((uint16_t)(v & 0xFFFFU));
|
||||||
writeDataHigh((uint16_t)(v >> 16));
|
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, false) & MII_CTRL_BUSY) == 0U) {
|
||||||
|
if (!write) {
|
||||||
|
data = (uint16_t)EscLan9252::csrRead(ESC_REG_MII_DATA, 2U, false);
|
||||||
|
}
|
||||||
|
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 */
|
} /* anonymous namespace */
|
||||||
|
|
||||||
bool EscLan9252::s_busReady = false;
|
bool EscLan9252::s_busReady = false;
|
||||||
bool EscLan9252::s_coreReady = false;
|
bool EscLan9252::s_coreReady = false;
|
||||||
bool EscLan9252::s_resetDone = false;
|
bool EscLan9252::s_resetDone = false;
|
||||||
|
bool EscLan9252::s_pmPhyDown = false;
|
||||||
|
|
||||||
void EscLan9252::hardwareReset()
|
void EscLan9252::hardwareReset()
|
||||||
{
|
{
|
||||||
@@ -193,6 +290,98 @@ uint8_t EscLan9252::nrstDrive(uint8_t action)
|
|||||||
return level ? 1U : 0U;
|
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()
|
bool EscLan9252::init()
|
||||||
{
|
{
|
||||||
s_busReady = false; /* 1.5.27:s_busReady 语义 = init 完整成功(EXMC+探针+BYTE_TEST) */
|
s_busReady = false; /* 1.5.27:s_busReady 语义 = init 完整成功(EXMC+探针+BYTE_TEST) */
|
||||||
@@ -252,12 +441,12 @@ bool EscLan9252::init()
|
|||||||
* 网络侧诊断已证实本板 EEPROM 加载与核心均正常,此等待应几乎立即通过;
|
* 网络侧诊断已证实本板 EEPROM 加载与核心均正常,此等待应几乎立即通过;
|
||||||
* 超时意味着 PDI 真的未就绪(EEPROM 内容/I2C/strap 问题),此时 SSC 的
|
* 超时意味着 PDI 真的未就绪(EEPROM 内容/I2C/strap 问题),此时 SSC 的
|
||||||
* AL Event Mask/邮箱 SM 配置会落空,由 initial.cpp 按 isCoreReady() 门控跳过。 */
|
* 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;
|
uint32_t waited = 0U;
|
||||||
while ((dlStatus & 0x0001U) == 0U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS) {
|
while ((dlStatus & 0x0001U) == 0U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS) {
|
||||||
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
|
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
|
||||||
waited += 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) {
|
if ((dlStatus & 0x0001U) != 0U) {
|
||||||
s_coreReady = true;
|
s_coreReady = true;
|
||||||
@@ -269,7 +458,7 @@ bool EscLan9252::init()
|
|||||||
printf("[ECAT][ESC] init: FAIL, PDI not operational after %ums "
|
printf("[ECAT][ESC] init: FAIL, PDI not operational after %ums "
|
||||||
"(DL_STATUS=0x%04X EEP_CSR=0x%04X)\r\n",
|
"(DL_STATUS=0x%04X EEP_CSR=0x%04X)\r\n",
|
||||||
(unsigned)CONFIG_ESC_HBI_POLL_TIMEOUT_MS, (unsigned)dlStatus,
|
(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");
|
printf("[ECAT][ESC] hint: 用 tools/ecat/ecat_esc_regs.py 从网络侧读 0x0502 错误位区分 EEPROM/I2C/内容问题\r\n");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -295,7 +484,7 @@ void EscLan9252::configPdWatchdog()
|
|||||||
* 表现为"100ms 周期全 WKC 错、10/50ms 正常"。分频保持默认 100µs,只延长步数;
|
* 表现为"100ms 周期全 WKC 错、10/50ms 正常"。分频保持默认 100µs,只延长步数;
|
||||||
* 正常周期(≤50ms)远小于 1s,行为不变。 */
|
* 正常周期(≤50ms)远小于 1s,行为不变。 */
|
||||||
csrWrite(ESC_REG_WD_TIME_PD, ESC_WD_TIME_PD_STEPS, 2U);
|
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) {
|
if ((rd & 0xFFFFU) == ESC_WD_TIME_PD_STEPS) {
|
||||||
printf("[ECAT][ESC] PD watchdog = 1s (0x0420=%u x 100us, default was 100ms)\r\n",
|
printf("[ECAT][ESC] PD watchdog = 1s (0x0420=%u x 100us, default was 100ms)\r\n",
|
||||||
(unsigned)ESC_WD_TIME_PD_STEPS);
|
(unsigned)ESC_WD_TIME_PD_STEPS);
|
||||||
@@ -326,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;
|
/* DS 12.11.1:写 CSR_CMD(BUSY|R_nW|SIZE|ADDR) 启动,busy 自清后读 CSR_DATA;
|
||||||
* 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。
|
* 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。
|
||||||
* 1.5.59 加固:CSR_DATA 是锁存寄存器(非 FIFO),命令不变时连读两次必一致;
|
* 1.5.59 加固(verify=true):CSR_DATA 是锁存寄存器(非 FIFO),命令不变时连读两次必一致;
|
||||||
* 不一致即本拍 EXMC 毛刺——历史上这类错读曾让 GetSyncMan 把 0x1204/0x1208
|
* 不一致即本拍 EXMC 毛刺——历史上这类错读曾让 GetSyncMan 把 0x1204/0x1208
|
||||||
* 缓存成邮箱几何(→SSC 邮箱楔死,根治见 mailbox.c 构建期常量),或让
|
* 缓存成邮箱几何(→SSC 邮箱楔死,根治见 mailbox.c 构建期常量),或让
|
||||||
* CheckSmSettings 以 0x0016/0x001F 瞬态打断状态转换。对错读整事务重跑一次;
|
* 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)
|
for (uint8_t attempt = 0U; attempt < 2U; ++attempt)
|
||||||
{
|
{
|
||||||
uint32_t n = POLL_MAX;
|
uint32_t n = POLL_MAX;
|
||||||
|
|||||||
@@ -46,6 +46,18 @@ public:
|
|||||||
* 驱动高读回低 => ECAT_NRST 被按死在复位,LAS9252 永在复位态。 */
|
* 驱动高读回低 => ECAT_NRST 被按死在复位,LAS9252 永在复位态。 */
|
||||||
static uint8_t nrstDrive(uint8_t action);
|
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 打印诊断用)。
|
* @brief 读取 ESC 芯片 ID(INIT-ONLY,供 init 打印诊断用)。
|
||||||
* @return uint32_t 32-bit 芯片 ID(期望值 0x87654321)。
|
* @return uint32_t 32-bit 芯片 ID(期望值 0x87654321)。
|
||||||
@@ -65,8 +77,11 @@ public:
|
|||||||
* (DS 12.11/表 12-13),EtherCAT 核心寄存器(0x0000-0x0FFF)必须经
|
* (DS 12.11/表 12-13),EtherCAT 核心寄存器(0x0000-0x0FFF)必须经
|
||||||
* CSR_CMD(0x304)/CSR_DATA(0x300) 间接访问;过程 RAM(0x1000-0x1FFF)
|
* CSR_CMD(0x304)/CSR_DATA(0x300) 间接访问;过程 RAM(0x1000-0x1FFF)
|
||||||
* 必须经 PRAM FIFO。size=1/2/4(size2 需偶地址,size4 需 4 字节对齐)。
|
* 必须经 PRAM FIFO。size=1/2/4(size2 需偶地址,size4 需 4 字节对齐)。
|
||||||
* 调用方负责 HBI 互斥(主循环 EscExmcBus::maskEnter / 已在 ISR 上下文)。 */
|
* 调用方负责 HBI 互斥(主循环 EscExmcBus::maskEnter / 已在 ISR 上下文)。
|
||||||
static uint32_t csrRead(uint16_t addr, uint8_t size);
|
* @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。 */
|
/** @brief ESC 核心 CSR 间接写(ECAT_CSR_CMD/DATA 端口)。约束同 csrRead。 */
|
||||||
static void csrWrite(uint16_t addr, uint32_t data, uint8_t size);
|
static void csrWrite(uint16_t addr, uint32_t data, uint8_t size);
|
||||||
@@ -102,6 +117,7 @@ private:
|
|||||||
static bool s_busReady; /**< init 完整成功(EXMC+IDX 探针+BYTE_TEST)后置位。 */
|
static bool s_busReady; /**< init 完整成功(EXMC+IDX 探针+BYTE_TEST)后置位。 */
|
||||||
static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位(1.5.39)。 */
|
static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位(1.5.39)。 */
|
||||||
static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */
|
static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */
|
||||||
|
static bool s_pmPhyDown; /**< pmDrive 进 D3 时两 PHY 已被置 PowerDown,唤醒后需恢复。 */
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif /* COMM_ESC_LAN9252_H */
|
#endif /* COMM_ESC_LAN9252_H */
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @file ethercat_can_bridge.cpp
|
* @file ethercat_can_bridge.cpp
|
||||||
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.5.56)。
|
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.6.21:段 2 电机整轮密排)。
|
||||||
*/
|
*/
|
||||||
#include "comm/ethercat_can_bridge.h"
|
#include "comm/ethercat_can_bridge.h"
|
||||||
#include "device/canfd.h"
|
#include "device/canfd.h"
|
||||||
@@ -16,7 +16,6 @@ volatile bool EthercatCanBridge::s_seqPrimed = false;
|
|||||||
volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U;
|
volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U;
|
||||||
volatile uint16_t EthercatCanBridge::s_txCount = 0U;
|
volatile uint16_t EthercatCanBridge::s_txCount = 0U;
|
||||||
volatile uint16_t EthercatCanBridge::s_dropCount = 0U;
|
volatile uint16_t EthercatCanBridge::s_dropCount = 0U;
|
||||||
volatile uint8_t EthercatCanBridge::s_winBase = 0U;
|
|
||||||
|
|
||||||
volatile bool EthercatCanBridge::s_rxValid = false;
|
volatile bool EthercatCanBridge::s_rxValid = false;
|
||||||
volatile bool EthercatCanBridge::s_rxPublished = false;
|
volatile bool EthercatCanBridge::s_rxPublished = false;
|
||||||
@@ -28,15 +27,15 @@ volatile uint32_t EthercatCanBridge::s_rxId = 0U;
|
|||||||
volatile uint32_t EthercatCanBridge::s_rxCount = 0U;
|
volatile uint32_t EthercatCanBridge::s_rxCount = 0U;
|
||||||
uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0};
|
uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0};
|
||||||
|
|
||||||
EthercatCanBridge::RxWinSlot
|
uint8_t EthercatCanBridge::s_motShadow[kEcatMotRegionBytes] = {0};
|
||||||
EthercatCanBridge::s_rxWin[ECAT_RXB_SLOTS] = {};
|
uint32_t EthercatCanBridge::s_motPubSeq[kMotorMaxNodes + 1U] = {0};
|
||||||
volatile uint8_t EthercatCanBridge::s_rxWinWr = 0U;
|
uint8_t EthercatCanBridge::s_motPubOnline[kMotorMaxNodes + 1U] = {0};
|
||||||
volatile uint32_t EthercatCanBridge::s_rxWinSeq = 0U;
|
uint16_t EthercatCanBridge::s_motPubMask = 0U;
|
||||||
volatile uint8_t EthercatCanBridge::s_rxWinOvf = 0U;
|
uint8_t EthercatCanBridge::s_motPubCount = 0U;
|
||||||
volatile uint8_t EthercatCanBridge::s_rxWinSeen[ECAT_RXB_SLOTS] = {0};
|
bool EthercatCanBridge::s_motShadowValid = false;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端,不依赖编译器结构体布局。 */
|
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端。 */
|
||||||
inline uint32_t rd32le(const uint8_t *p)
|
inline uint32_t rd32le(const uint8_t *p)
|
||||||
{
|
{
|
||||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||||
@@ -51,13 +50,19 @@ inline void wr16le(uint8_t *p, uint16_t v)
|
|||||||
{
|
{
|
||||||
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
|
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 段 2 头 + count 槽在影子区中的总长度。 */
|
||||||
|
inline uint16_t motRegionLen(uint8_t count)
|
||||||
|
{
|
||||||
|
return (uint16_t)(kEcatMotHdrSize + (uint16_t)count * kEcatMotSlotSize);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||||
{
|
{
|
||||||
uint8_t want = out[ECAT_TXB_OFF_COUNT];
|
uint8_t want = out[ECAT_TXB_OFF_COUNT];
|
||||||
if (want == 0U) {
|
if (want == 0U) {
|
||||||
return 0U; /* 未使用批量区:行为与 1.5.55 完全一致 */
|
return 0U;
|
||||||
}
|
}
|
||||||
if (want > kEcatTxBatchMax) {
|
if (want > kEcatTxBatchMax) {
|
||||||
want = kEcatTxBatchMax;
|
want = kEcatTxBatchMax;
|
||||||
@@ -68,8 +73,6 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
|||||||
uint16_t off = ECAT_TXB_OFF_ITEMS;
|
uint16_t off = ECAT_TXB_OFF_ITEMS;
|
||||||
|
|
||||||
for (uint8_t i = 0U; i < want; ++i) {
|
for (uint8_t i = 0U; i < want; ++i) {
|
||||||
/* 不信任主站给的 count/dlc:每步都验证头与数据都完整落在 PDO 内。
|
|
||||||
* 越界即停 —— 这是唯一的边界判据,count 只是上限提示。 */
|
|
||||||
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE) > kEcatPdoOutBytes) {
|
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE) > kEcatPdoOutBytes) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -87,9 +90,6 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
|||||||
if (!CanFd::isChannelEnabled(ch)) {
|
if (!CanFd::isChannelEnabled(ch)) {
|
||||||
r = CanFd::CanTxResult::kErrInvalid;
|
r = CanFd::CanTxResult::kErrInvalid;
|
||||||
} else {
|
} else {
|
||||||
/* 与单帧路径同样用 enqueueTx:批量下发必然连续打同一通道,
|
|
||||||
* transmitFrame 的 allow_abort 会让后帧顶掉前帧(历史上 7 帧
|
|
||||||
* 只发出 3 帧的根因),队列语义才不静默丢帧。 */
|
|
||||||
r = CanFd::enqueueTx(ch, rd32le(&f[ECAT_FRM_OFF_ID]),
|
r = CanFd::enqueueTx(ch, rd32le(&f[ECAT_FRM_OFF_ID]),
|
||||||
&f[ECAT_FRM_HDR_SIZE], dlc);
|
&f[ECAT_FRM_HDR_SIZE], dlc);
|
||||||
}
|
}
|
||||||
@@ -108,56 +108,110 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
|||||||
return sent;
|
return sent;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EthercatCanBridge::fillRxWindow(uint8_t *in)
|
/**
|
||||||
|
* 段 2:电机整轮密排。
|
||||||
|
*
|
||||||
|
* 1. 扫 MotorState,每个节点号 1..9 在 3 个通道里取 seq 最新的一条;
|
||||||
|
* 2. 若节点集合与各节点 seq 均与上一拍发布相同 → 整段复用影子区(跳过重读);
|
||||||
|
* 3. 否则按 node_id 升序密排写入,空节点不占位;seqlock 奇数(写入中)的槽
|
||||||
|
* 本拍省略,下拍自愈 —— 不发半新半旧的 48B。
|
||||||
|
*/
|
||||||
|
uint8_t EthercatCanBridge::fillMotorRound(uint8_t *in)
|
||||||
{
|
{
|
||||||
/* ── 1.6.15:定长覆盖窗口,整块取值 ──
|
const MotorStateTable &mt = MotorState::table();
|
||||||
* 生产者不等人:每拍把 N 个槽原样拷进 PDO,槽里是谁就是谁。同一帧会在连续
|
|
||||||
* 几拍里重复出现,直到被下一帧覆盖。语义是"最近 N 帧窗口",不是"保证不丢的
|
|
||||||
* 事件流"——主站按每槽 seq 去重后才归一成事件流上报给上层。
|
|
||||||
*
|
|
||||||
* 相比旧的环形队列排空,这里没有 tail/head/判满/pending:1.6.9 记录的那个
|
|
||||||
* "滞后的 tail 让生产者写进消费者刚读过的槽"错标竞态,其载体(tail 握手)
|
|
||||||
* 在本布局里根本不存在。定长也让主站不必推游标,1.6.10 那类边界越界写诊断区
|
|
||||||
* 的错在结构上不可能再犯。
|
|
||||||
*
|
|
||||||
* 每槽 seqlock 判稳:奇数=生产者正在写这 12B 头 + 32B 数据,本拍留零跳过该槽,
|
|
||||||
* 下拍自愈(与段 1/段 2/段 3 既有约定一致)。本函数跑在 PDI_Isr/Sync0_Isr
|
|
||||||
* (preempt=0),单核下执行期间生产者必然冻结,判一次奇偶即可定性。 */
|
|
||||||
in[ECAT_RXB_OFF_SLOT_COUNT] = ECAT_RXB_SLOTS;
|
|
||||||
in[ECAT_RXB_OFF_WR_IDX] = s_rxWinWr;
|
|
||||||
in[ECAT_RXB_OFF_OVF] = s_rxWinOvf;
|
|
||||||
s_rxWinOvf = 0U; /* 回填后清零:OVF 是"自上次回填以来"的增量 */
|
|
||||||
|
|
||||||
for (uint8_t i = 0U; i < ECAT_RXB_SLOTS; ++i) {
|
/* ── 1. 每节点选一条最新槽 ── */
|
||||||
const RxWinSlot &sl = s_rxWin[i];
|
const MotorSlot *picks[kMotorMaxNodes];
|
||||||
const uint32_t seq = sl.seq;
|
uint8_t nodes[kMotorMaxNodes];
|
||||||
if ((seq & 1U) != 0U) {
|
uint8_t n = 0U;
|
||||||
continue; /* 生产者正在写这槽:本拍不报,下拍自愈 */
|
|
||||||
|
for (uint8_t node = 1U; node <= kMotorMaxNodes; ++node) {
|
||||||
|
const MotorSlot *best = nullptr;
|
||||||
|
for (uint8_t ch = 0U; ch < kMotorChannels; ++ch) {
|
||||||
|
const MotorSlot &s =
|
||||||
|
mt.slots[(uint8_t)((node - 1U) * kMotorChannels + ch)];
|
||||||
|
if (s.node_id == 0U) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (best == nullptr || s.seq >= best->seq) {
|
||||||
|
best = &s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best != nullptr) {
|
||||||
|
picks[n] = best;
|
||||||
|
nodes[n] = node;
|
||||||
|
++n;
|
||||||
}
|
}
|
||||||
__DMB(); /* 判稳先于取值:不许把下面的读重排到奇偶检查之前 */
|
|
||||||
if ((sl.flags & 0x01U) == 0U) {
|
|
||||||
continue; /* 从未写过:留零,主站按 flags bit0 判空 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RAM 侧存全量 64B,只有拷进 PDO 这一步按 32B 截断;DLC 字节填**原始值**,
|
/* ── 2. 判「上一轮已填充且未变」──
|
||||||
* 主站据此 + flags bit1 判断是否被截。 */
|
* 节点集合一致 + 每节点 seq/online 一致 + 头字段(active/boot)一致 + 写侧
|
||||||
const uint8_t dlc = sl.dlc;
|
* seqlock 偶数 → 复用影子。online 必须比:checkOffline 超时置 0 时不 bump
|
||||||
const uint8_t wire = (dlc > kEcatRxBatchDataMax) ? kEcatRxBatchDataMax : dlc;
|
* seq,只比 seq 会把「已离线」永远报成在线。 */
|
||||||
uint8_t *p = &in[ECAT_RXB_OFF_ITEMS + (uint16_t)i * ECAT_RXB_SLOT_SIZE];
|
bool unchanged = s_motShadowValid && (n == s_motPubCount)
|
||||||
|
&& mt.active_count == s_motShadow[ECAT_MOTW_OFF_ACTIVE - ECAT_MOTW_OFF_COUNT]
|
||||||
p[ECAT_RXW_OFF_CH] = sl.ch;
|
&& mt.boot_active == s_motShadow[ECAT_MOTW_OFF_BOOT - ECAT_MOTW_OFF_COUNT]
|
||||||
p[ECAT_RXW_OFF_DLC] = dlc;
|
&& s_motShadow[ECAT_MOTW_OFF_MAGIC - ECAT_MOTW_OFF_COUNT]
|
||||||
p[ECAT_RXW_OFF_FLAGS] = (uint8_t)(0x01U |
|
== kEcatMotLayoutMagic;
|
||||||
(dlc > kEcatRxBatchDataMax ? 0x02U : 0x00U));
|
if (unchanged) {
|
||||||
wr32le(&p[ECAT_RXW_OFF_ID], sl.id);
|
for (uint8_t i = 0U; i < n; ++i) {
|
||||||
wr32le(&p[ECAT_RXW_OFF_SEQ], seq);
|
const uint8_t node = nodes[i];
|
||||||
memcpy(&p[ECAT_RXW_OFF_DATA], sl.data, wire);
|
const MotorSlot &s = *picks[i];
|
||||||
|
if ((s.wr_seq & 1U) != 0U
|
||||||
/* 标记"主站已取过":生产者下次覆盖本槽时就不算丢帧。只有"未取先覆"
|
|| s.seq != s_motPubSeq[node]
|
||||||
* 才计 OVF —— 覆盖已送达的帧是窗口的设计意图,不是损失。 */
|
|| s.online != s_motPubOnline[node]
|
||||||
s_rxWinSeen[i] = 1U;
|
|| (s_motPubMask & (uint16_t)(1U << node)) == 0U) {
|
||||||
|
unchanged = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t region = motRegionLen(unchanged ? s_motPubCount : n);
|
||||||
|
|
||||||
|
if (unchanged) {
|
||||||
|
memcpy(&in[ECAT_MOTW_OFF_COUNT], s_motShadow, region);
|
||||||
|
return s_motPubCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 3. 重建密排区 ── */
|
||||||
|
in[ECAT_MOTW_OFF_ACTIVE] = mt.active_count;
|
||||||
|
in[ECAT_MOTW_OFF_BOOT] = mt.boot_active;
|
||||||
|
in[ECAT_MOTW_OFF_MAGIC] = kEcatMotLayoutMagic;
|
||||||
|
wr32le(&in[ECAT_MOTW_OFF_GLOBAL_SEQ], mt.global_seq);
|
||||||
|
|
||||||
|
uint8_t w = 0U;
|
||||||
|
uint16_t newMask = 0U;
|
||||||
|
for (uint8_t i = 0U; i < n; ++i) {
|
||||||
|
const MotorSlot &s = *picks[i];
|
||||||
|
if ((s.wr_seq & 1U) != 0U) {
|
||||||
|
continue; /* 写入中:本拍省略,下拍自愈 */
|
||||||
|
}
|
||||||
|
__DMB();
|
||||||
|
|
||||||
|
uint8_t *p = &in[ECAT_MOTW_OFF_SLOTS + (uint16_t)w * kEcatMotSlotSize];
|
||||||
|
p[ECAT_MOT_OFF_NODE] = nodes[i];
|
||||||
|
p[ECAT_MOT_OFF_CH] = s.channel;
|
||||||
|
p[ECAT_MOT_OFF_ONLINE] = s.online;
|
||||||
|
p[ECAT_MOT_OFF_DLC] = s.dlc;
|
||||||
|
wr32le(&p[ECAT_MOT_OFF_SEQ], s.seq);
|
||||||
|
memcpy(&p[ECAT_MOT_OFF_STATUS], s.data, kMotorStatusDlc);
|
||||||
|
|
||||||
|
s_motPubSeq[nodes[i]] = s.seq;
|
||||||
|
s_motPubOnline[nodes[i]] = s.online;
|
||||||
|
newMask |= (uint16_t)(1U << nodes[i]);
|
||||||
|
++w;
|
||||||
|
}
|
||||||
|
|
||||||
|
in[ECAT_MOTW_OFF_COUNT] = w;
|
||||||
|
|
||||||
|
/* 更新影子:下次集合+seq 未变时整段复用 */
|
||||||
|
memcpy(s_motShadow, &in[ECAT_MOTW_OFF_COUNT], motRegionLen(w));
|
||||||
|
s_motPubMask = newMask;
|
||||||
|
s_motPubCount = w;
|
||||||
|
s_motShadowValid = true;
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
bool EthercatCanBridge::peekOutput(const uint8_t *head)
|
bool EthercatCanBridge::peekOutput(const uint8_t *head)
|
||||||
{
|
{
|
||||||
@@ -166,20 +220,14 @@ bool EthercatCanBridge::peekOutput(const uint8_t *head)
|
|||||||
}
|
}
|
||||||
const uint8_t seq = head[ECAT_CAN_TX_OFF_SEQ];
|
const uint8_t seq = head[ECAT_CAN_TX_OFF_SEQ];
|
||||||
|
|
||||||
/* 窗口下标每周期都必须刷新(主站靠滚动 WIN 扫 27 槽),与 seq 沿无关。
|
/* WIN@8 不再使用:1.6.21 电机整轮密排,无滑窗。 */
|
||||||
* 越界值钳到 0,避免读到槽数组外(与 processOutput 同款钳制)。 */
|
|
||||||
const uint8_t win = head[ECAT_CAN_TX_OFF_WIN];
|
|
||||||
s_winBase = (win < kMotorSlotCount) ? win : 0U;
|
|
||||||
|
|
||||||
/* 上电/重连后首帧只记基准(与 processOutput 的 priming 分支语义一致) */
|
|
||||||
if (!s_seqPrimed) {
|
if (!s_seqPrimed) {
|
||||||
s_seqPrimed = true;
|
s_seqPrimed = true;
|
||||||
s_lastSeq = seq;
|
s_lastSeq = seq;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* seq 沿 → 全量读;FLAGS bit0(清统计请求)允许不带 seq 沿(同 processOutput)。
|
|
||||||
* s_lastSeq 不在这里推进:全量路径上 processOutput 的沿处理只做一次。 */
|
|
||||||
if (seq != s_lastSeq) {
|
if (seq != s_lastSeq) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -194,32 +242,21 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
|||||||
|
|
||||||
const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ];
|
const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ];
|
||||||
|
|
||||||
/* WIN 不在这里取:1.6.0 起 out 只在 seq 沿那拍才是新鲜的,无沿的周期里它是
|
|
||||||
* 上一次全量读的残值。在这里回写 s_winBase,等于用开机首拍的 0 每拍盖掉
|
|
||||||
* peekOutput() 刚取到的新值 —— 窗口被钉死在基址 0,槽位 7..26(node3 及
|
|
||||||
* 以上)永不上报,直到主站真发一帧触发一次全量读才复位。
|
|
||||||
* 每拍刷新由 peekOutput()(SSC PDO_OutputMapping 的 12B 探针)独占负责。 */
|
|
||||||
|
|
||||||
/* 上电/重连后第一次看到输出数据时只记录基准,不发送:否则主站残留的
|
|
||||||
* 非零 seq 会被当成一次新请求,在进 OP 的瞬间凭空发出一帧。 */
|
|
||||||
if (!s_seqPrimed) {
|
if (!s_seqPrimed) {
|
||||||
s_seqPrimed = true;
|
s_seqPrimed = true;
|
||||||
s_lastSeq = seq;
|
s_lastSeq = seq;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* flags bit0:清零统计(一次性,不依赖 seq 沿,便于主站随时归零) */
|
|
||||||
if ((out[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U) {
|
if ((out[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U) {
|
||||||
s_txCount = 0U;
|
s_txCount = 0U;
|
||||||
s_dropCount = 0U;
|
s_dropCount = 0U;
|
||||||
s_rxCount = 0U;
|
s_rxCount = 0U;
|
||||||
/* 快照区里残留的那帧早已发布过;不置 published 会让清零后的第一帧
|
|
||||||
* 被误计成一次"未取先覆",清零口径就脏了。 */
|
|
||||||
s_rxPublished = true;
|
s_rxPublished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seq == s_lastSeq) {
|
if (seq == s_lastSeq) {
|
||||||
return; /* 没有新的发送请求 */
|
return;
|
||||||
}
|
}
|
||||||
s_lastSeq = seq;
|
s_lastSeq = seq;
|
||||||
|
|
||||||
@@ -229,17 +266,10 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
|||||||
dlc = kEcatCanTxDataMax;
|
dlc = kEcatCanTxDataMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 单帧区先发(保持 1.5.55 语义),再发批量区。
|
|
||||||
* dlc==0 表示本次只用批量区,不发单帧 —— 否则会凭空发出一帧 0 长度报文。 */
|
|
||||||
if (dlc > 0U) {
|
if (dlc > 0U) {
|
||||||
/* isChannelEnabled 内含越界检查,是"通道号合法且可用"的唯一判据 */
|
|
||||||
if (!CanFd::isChannelEnabled(ch)) {
|
if (!CanFd::isChannelEnabled(ch)) {
|
||||||
s_lastTxResult = (uint8_t)CanFd::CanTxResult::kErrInvalid;
|
s_lastTxResult = (uint8_t)CanFd::CanTxResult::kErrInvalid;
|
||||||
} else {
|
} else {
|
||||||
/* 用 enqueueTx 而非 transmitFrame:PDO 是周期性场景,可能连续多帧打到
|
|
||||||
* 同一通道。transmitFrame 默认 allow_abort=true 会在邮箱满时顶掉已装载
|
|
||||||
* 的旧帧(历史上 7 帧突发只发出 3 帧的根因);enqueueTx 队列空时直通、
|
|
||||||
* 满时排队,不静默丢帧。 */
|
|
||||||
const CanFd::CanTxResult r =
|
const CanFd::CanTxResult r =
|
||||||
CanFd::enqueueTx(ch, rd32le(&out[ECAT_CAN_TX_OFF_ID]),
|
CanFd::enqueueTx(ch, rd32le(&out[ECAT_CAN_TX_OFF_ID]),
|
||||||
&out[ECAT_CAN_TX_OFF_DATA], dlc);
|
&out[ECAT_CAN_TX_OFF_DATA], dlc);
|
||||||
@@ -250,7 +280,6 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 批量区:同一个 seq 沿里一次性发 N 帧,供多轴同步下发。 */
|
|
||||||
(void)sendBatch(out);
|
(void)sendBatch(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,21 +288,15 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
|||||||
if (in == nullptr) {
|
if (in == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/* 必须按新的全长清零。旧代码写死 memset(in,0,32),扩容后会把 32 字节以后
|
|
||||||
* 的槽区留成上一周期的残留数据,主站读到的是"看起来合法但已过期"的电机状态。 */
|
|
||||||
memset(in, 0, kEcatPdoInBytes);
|
memset(in, 0, kEcatPdoInBytes);
|
||||||
|
|
||||||
/* ── 段 1(0..31):桥状态 + 短快照,布局与 1.5.53 逐字节兼容 ── */
|
/* ── 段 1(0..31):桥状态 + 短快照 ── */
|
||||||
in[ECAT_CAN_RX_OFF_ACK] = s_lastSeq;
|
in[ECAT_CAN_RX_OFF_ACK] = s_lastSeq;
|
||||||
in[ECAT_CAN_RX_OFF_TXRES] = s_lastTxResult;
|
in[ECAT_CAN_RX_OFF_TXRES] = s_lastTxResult;
|
||||||
wr16le(&in[ECAT_CAN_RX_OFF_TXCNT], s_txCount);
|
wr16le(&in[ECAT_CAN_RX_OFF_TXCNT], s_txCount);
|
||||||
wr16le(&in[ECAT_CAN_RX_OFF_DROP], s_dropCount);
|
wr16le(&in[ECAT_CAN_RX_OFF_DROP], s_dropCount);
|
||||||
wr32le(&in[ECAT_CAN_RX_OFF_RXCNT], s_rxCount);
|
wr32le(&in[ECAT_CAN_RX_OFF_RXCNT], s_rxCount);
|
||||||
|
|
||||||
/* seqlock 判稳(见 onCanFrame):奇数=生产者正在写这组快照。单核下 ISR
|
|
||||||
* 执行期间生产者冻结,读一次即可定性;本周期跳过段 1/段 3(开头已清零,
|
|
||||||
* 语义=无有效快照),下周期自愈。批量区(段 4)每槽另有自己的 seqlock,
|
|
||||||
* 不受本组影响。 */
|
|
||||||
const bool xrxStable = ((s_xrxSeq & 1U) == 0U);
|
const bool xrxStable = ((s_xrxSeq & 1U) == 0U);
|
||||||
|
|
||||||
if (xrxStable && s_rxValid) {
|
if (xrxStable && s_rxValid) {
|
||||||
@@ -285,10 +308,7 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
|||||||
in[ECAT_CAN_RX_OFF_DLC] = s_rxDlc;
|
in[ECAT_CAN_RX_OFF_DLC] = s_rxDlc;
|
||||||
in[ECAT_CAN_RX_OFF_FLAGS] = flags;
|
in[ECAT_CAN_RX_OFF_FLAGS] = flags;
|
||||||
wr32le(&in[ECAT_CAN_RX_OFF_ID], s_rxId);
|
wr32le(&in[ECAT_CAN_RX_OFF_ID], s_rxId);
|
||||||
/* 段 1 只放前 8 字节(兼容),完整 payload 见段 3 */
|
|
||||||
memcpy(&in[ECAT_CAN_RX_OFF_DATA], s_rxData, kEcatCanRxDataMax);
|
memcpy(&in[ECAT_CAN_RX_OFF_DATA], s_rxData, kEcatCanRxDataMax);
|
||||||
/* 本拍已把这一帧发布进过程数据映像(段 3 与本块同门控,故只在此处置位一次):
|
|
||||||
* 之后再被新帧覆盖属于"最新值"设计意图,不算丢。 */
|
|
||||||
s_rxPublished = true;
|
s_rxPublished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,56 +316,11 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
|||||||
const uint32_t backlog = CanFd::isChannelEnabled(ch) ? CanFd::txQueueBacklog(ch) : 0U;
|
const uint32_t backlog = CanFd::isChannelEnabled(ch) ? CanFd::txQueueBacklog(ch) : 0U;
|
||||||
in[ECAT_CAN_RX_OFF_BACKLOG] = (backlog > 255U) ? 255U : (uint8_t)backlog;
|
in[ECAT_CAN_RX_OFF_BACKLOG] = (backlog > 255U) ? 255U : (uint8_t)backlog;
|
||||||
|
|
||||||
/* ── 段 2(32..319):电机状态窗口 ──
|
/* ── 段 2(32..543):电机整轮密排(空槽不发,count=实际电机数)── */
|
||||||
* 直接读 MotorState 聚合表,不再依赖"最近一帧 CAN"。窗口起点由主站通过
|
(void)fillMotorRound(in);
|
||||||
* 输出 PDO 的 WIN 字节指定,主站每周期 +5 即可 6 周期扫完 27 个槽。
|
|
||||||
*
|
|
||||||
* 上下文安全(1.6.5 修正):状态表由主循环的 taskCanRxParse 写,而本函数跑在
|
|
||||||
* PDI_Isr(SM 同步模式)/Sync0_Isr(DC 模式),会抢占写侧那次
|
|
||||||
* memcpy(slot.data, 48B)。旧注释称"最坏是 seq 与 data 不同步一拍,主站按 seq
|
|
||||||
* 判新旧即可自愈"是错的:写侧先 seq 后 data,抢占留下的恰好是"新 seq +
|
|
||||||
* 半新半旧 data",而主站只拿 seq 判新鲜度,分辨不出同一 seq 下 payload 换过
|
|
||||||
* 内容,混合值照原样进界面(边界落在 B16/B20/B24 这类 float 符号/指数字节上
|
|
||||||
* 就是离谱值)。改用与段 1/段 3 同一套 seqlock(MotorSlot.wr_seq):奇数即
|
|
||||||
* 跳过本槽,与本片既有的"本拍不报、下拍自愈"约定一致。 */
|
|
||||||
const MotorStateTable &mt = MotorState::table();
|
|
||||||
const uint8_t base = (s_winBase < kMotorSlotCount) ? s_winBase : 0U;
|
|
||||||
|
|
||||||
in[ECAT_MOTW_OFF_ACTIVE] = mt.active_count;
|
/* ── 段 3(544..583):最近一帧完整快照 ──
|
||||||
in[ECAT_MOTW_OFF_BOOT] = mt.boot_active;
|
* 与段 1 共用 xrxStable 门控,两段必须来自同一帧。 */
|
||||||
in[ECAT_MOTW_OFF_WIN_BASE] = base;
|
|
||||||
wr32le(&in[ECAT_MOTW_OFF_GLOBAL_SEQ], mt.global_seq);
|
|
||||||
|
|
||||||
uint8_t count = 0U;
|
|
||||||
for (uint8_t i = 0U; i < kEcatMotSlotsPerPdo; ++i) {
|
|
||||||
const uint8_t idx = (uint8_t)(base + i);
|
|
||||||
if (idx >= kMotorSlotCount) {
|
|
||||||
break; /* 窗口尾部越界即停,不回绕:主站据 win_count 判断 */
|
|
||||||
}
|
|
||||||
const MotorSlot &s = mt.slots[idx];
|
|
||||||
uint8_t *p = &in[ECAT_MOTW_OFF_SLOTS + (uint16_t)i * kEcatMotSlotSize];
|
|
||||||
|
|
||||||
/* 槽按 i 定位,跳过的那格照样占位,所以 win_count 报的是跨度而不是实际
|
|
||||||
* 填充数:主站 decode_input 对 node_id==0 的块直接 continue。 */
|
|
||||||
count = static_cast<uint8_t>(i + 1U);
|
|
||||||
if ((s.wr_seq & 1U) != 0U) {
|
|
||||||
continue; /* 生产者正在写这 48B:本拍留零,下拍自愈 */
|
|
||||||
}
|
|
||||||
__DMB(); /* 判稳先于取值:不许把下面的读重排到奇偶检查之前 */
|
|
||||||
|
|
||||||
p[ECAT_MOT_OFF_NODE] = s.node_id;
|
|
||||||
p[ECAT_MOT_OFF_CH] = s.channel;
|
|
||||||
p[ECAT_MOT_OFF_ONLINE] = s.online;
|
|
||||||
p[ECAT_MOT_OFF_DLC] = s.dlc;
|
|
||||||
wr32le(&p[ECAT_MOT_OFF_SEQ], s.seq);
|
|
||||||
memcpy(&p[ECAT_MOT_OFF_STATUS], s.data, kMotorStatusDlc);
|
|
||||||
}
|
|
||||||
in[ECAT_MOTW_OFF_WIN_COUNT] = count;
|
|
||||||
|
|
||||||
/* ── 段 3(320..359):最近一帧完整快照 ──
|
|
||||||
* 版本帧(0x00FF)/CONFIG 帧(0x0101) 都是 DLC 24,段 1 的 8 字节装不下。
|
|
||||||
* 与段 1 共用 xrxStable 门控:两段必须来自同一帧,否则 ecat.html 的短快照
|
|
||||||
* 和电机页的长快照会各自解出半新半旧的 ID/数据。 */
|
|
||||||
if (xrxStable && s_rxValid) {
|
if (xrxStable && s_rxValid) {
|
||||||
in[ECAT_XRX_OFF_CH] = s_rxCh;
|
in[ECAT_XRX_OFF_CH] = s_rxCh;
|
||||||
in[ECAT_XRX_OFF_DLC] = s_rxDlc;
|
in[ECAT_XRX_OFF_DLC] = s_rxDlc;
|
||||||
@@ -354,11 +329,7 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
|||||||
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
|
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 段 4(360..583):CANFD 批量接收覆盖窗口 ──
|
/* 段 4(历史覆盖窗口)已删除;段 5 由 EthercatPdo::fillDiagTail 写。 */
|
||||||
* 段 1/段 3 只带"最近一帧",高速多帧场景下周期间到达的帧会互相覆盖
|
|
||||||
* (s_dropCount 就是在数这个)。批量区保留最近 N 帧的窗口,让主站在一个
|
|
||||||
* 周期内拿到帧序列而不是单个采样点;1.6.15 起改为定长覆盖(见 fillRxWindow)。 */
|
|
||||||
fillRxWindow(in);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||||
@@ -368,33 +339,15 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 缓存区已扩到 32B(段 3 用),truncated 仍按段 1 的 8B 口径上报,
|
|
||||||
* 这样 ecat.html 的 CAN 调试页语义不变。 */
|
|
||||||
uint8_t copyLen = length;
|
uint8_t copyLen = length;
|
||||||
if (copyLen > kEcatCanXRxDataMax) {
|
if (copyLen > kEcatCanXRxDataMax) {
|
||||||
copyLen = kEcatCanXRxDataMax;
|
copyLen = kEcatCanXRxDataMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 快照 seqlock(1.5.58)──
|
/* ── 快照 seqlock:奇数=正在写,消费者本拍跳过段 1/段 3 ── */
|
||||||
* s_rxData/s_rxCh/s_rxId/s_rxDlc/s_rxTruncated/s_rxValid 是一组,必须整体
|
++s_xrxSeq;
|
||||||
* 被 buildInput 读到。DC 模式下 buildInput 跑在 Sync0_Isr(preempt=0),会
|
|
||||||
* 抢占本函数(主循环 taskCanRxParse)。原注释声称"valid 最后置位保证一致"
|
|
||||||
* 是错的:valid 一旦置位就永不清零,ISR 落在"新 payload 已写完、id 还没
|
|
||||||
* 更新"的窗口里,会把新帧数据配旧帧 ID 发出去——STATUS 字节被标成 CONFIG
|
|
||||||
* (0x000N0101),前端按 CONFIG 布局解码出 -3e26 这类离谱目标值。
|
|
||||||
* 协议:seq 奇数=正在写。消费者在单核 ISR 里生产者必然冻结,判一次奇偶
|
|
||||||
* 即可,奇数则本周期跳过(段 1/段 3 已清零=无效),下周期自愈。
|
|
||||||
* 不关中断:critical.h 的 BASEPRI 方案放行 preempt=0,挡不住 Sync0;
|
|
||||||
* 而 seqlock 零抖动,与本文件既有无锁约定一致。
|
|
||||||
* __DMB 防编译器把非 volatile 的 memcpy 挪出两次 seq 自增之间。 */
|
|
||||||
++s_xrxSeq; /* → 奇数:快照进入"正在写" */
|
|
||||||
__DMB();
|
__DMB();
|
||||||
|
|
||||||
/* 「未取先覆」计数:上一帧还没被任何一拍 PDO 发布就被覆盖才算丢。
|
|
||||||
* 原判据只判 s_rxValid,而它置位后永不清零 → 第一帧之后每帧都计,
|
|
||||||
* drop 恒等于 rx−1(u16 回绕),变成跟着收帧速率涨、零灵敏度的假仪表。
|
|
||||||
* 放在奇数窗口内:消费者判到奇会跳过段 1/段 3、不会在此刻置 published,
|
|
||||||
* 故"读上一帧发布状态 + 自增"对消费者等效原子,不会多计一次。 */
|
|
||||||
if (s_rxValid && !s_rxPublished) {
|
if (s_rxValid && !s_rxPublished) {
|
||||||
++s_dropCount;
|
++s_dropCount;
|
||||||
}
|
}
|
||||||
@@ -406,67 +359,17 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
|||||||
|
|
||||||
s_rxCh = channel;
|
s_rxCh = channel;
|
||||||
s_rxId = id;
|
s_rxId = id;
|
||||||
s_rxDlc = length; /* 报告原始长度,便于主站发现被截断 */
|
s_rxDlc = length;
|
||||||
s_rxTruncated = (length > kEcatCanRxDataMax);
|
s_rxTruncated = (length > kEcatCanRxDataMax);
|
||||||
++s_rxCount;
|
++s_rxCount;
|
||||||
s_rxValid = true;
|
s_rxValid = true;
|
||||||
s_rxPublished = false; /* 这一帧还没被任何一拍 PDO 发布 */
|
s_rxPublished = false;
|
||||||
|
|
||||||
__DMB();
|
__DMB();
|
||||||
++s_xrxSeq; /* → 偶数:快照整体一致 */
|
++s_xrxSeq;
|
||||||
|
|
||||||
/* ── 写入批量接收覆盖窗口(1.6.15,取代 1.5.56 的环形队列)──
|
/* STATUS 走 MotorState → 段 2 整轮;非 STATUS 只更新段 1/3 最近一帧快照。
|
||||||
* 与上面的"最近一帧快照"并行维护:快照只留最新一帧,窗口留最近 N 帧的序列。
|
* 1.6.21 起不再写入历史覆盖窗口。 */
|
||||||
* 生产者按自己的游标写、绕圈覆盖,**不等消费者** —— 没有判满、没有背压、
|
|
||||||
* 没有 pending。代价是语义从"事件流"变成"最近 N 帧窗口":一帧在窗口里活到
|
|
||||||
* 被覆盖为止,主站按每槽 seq 去重后才归一成事件流。
|
|
||||||
*
|
|
||||||
* **STATUS 帧不入窗**(1.6.9):批量区存在的意义是捞那些段 2 装不下的帧
|
|
||||||
* (CONFIG / 版本 / 节点应答),而完整 STATUS 本来就由段 2 电机状态窗口按槽
|
|
||||||
* 上报;主站 transport-ecat.js 的 drainRx() 拿到 STATUS 也是直接 continue
|
|
||||||
* 丢掉。让它占窗口是纯重复,而且正是它把队列压爆的:实测 CAN 侧约 4000 帧/s,
|
|
||||||
* 旧批量区每拍最多回收 3 条(=3000 帧/s),净积压使队列长期饱和,才逼出了
|
|
||||||
* 生产者覆写消费者槽位的竞态(错标率 1.77%,表现为"CONFIG 的 ID 配 STATUS
|
|
||||||
* 的内容")。滤掉 STATUS 后入窗流量降到 CONFIG 量级。
|
|
||||||
*
|
|
||||||
* OVF 只数"**未取先覆**":覆盖一帧主站已经取走的数据是窗口的设计意图(不计),
|
|
||||||
* 覆盖一帧主站还没来得及取的数据才是真丢帧(计数)。该数与主站按 seq 空洞
|
|
||||||
* 算出的丢帧数应当一致 —— 两侧独立仪表互相对账,任一侧骗人都当场暴露。 */
|
|
||||||
if ((id & 0xFFFFU) == 0x0100U) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const uint8_t idx = s_rxWinWr;
|
|
||||||
RxWinSlot &sl = s_rxWin[idx];
|
|
||||||
|
|
||||||
if (s_rxWinSeen[idx] == 0U && sl.seq != 0U && s_rxWinOvf < 0xFFU) {
|
|
||||||
++s_rxWinOvf; /* 上一帧还没被任何一拍 PDO 取走就被覆盖 */
|
|
||||||
}
|
|
||||||
s_rxWinSeen[idx] = 0U;
|
|
||||||
|
|
||||||
/* 每槽 seqlock:奇数=正在写。消费者(PDI_Isr)落在这一瞬就跳过该槽,下拍
|
|
||||||
* 自愈。没有它,抢占在"新 payload 已写完、id 还没更新"之间,主站会拿到
|
|
||||||
* 一条现实中不存在、但看起来完全合法的假帧 —— 那不是"差一个周期"。
|
|
||||||
* 发布值取自全局计数器 s_rxWinSeq,故跨槽可比大小、可数空洞。 */
|
|
||||||
sl.seq = s_rxWinSeq + 1U; /* → 奇数:本槽进入"正在写" */
|
|
||||||
__DMB();
|
|
||||||
|
|
||||||
uint8_t n = length;
|
|
||||||
if (n > kEcatCanFdDataMax) {
|
|
||||||
n = kEcatCanFdDataMax;
|
|
||||||
}
|
|
||||||
sl.ch = channel;
|
|
||||||
sl.dlc = n;
|
|
||||||
sl.id = id;
|
|
||||||
sl.flags = 0x01U; /* bit0=本槽至少写过一帧 */
|
|
||||||
memcpy(sl.data, data, n);
|
|
||||||
|
|
||||||
s_rxWinSeq += 2U; /* 下一个发布值(恒偶) */
|
|
||||||
__DMB(); /* 字段写入先于 seq 发布落地 */
|
|
||||||
sl.seq = s_rxWinSeq; /* → 偶数:整槽一致 */
|
|
||||||
|
|
||||||
s_rxWinWr = (uint8_t)((idx + 1U) % ECAT_RXB_SLOTS);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* @file ethercat_can_bridge.h
|
* @file ethercat_can_bridge.h
|
||||||
* @brief EtherCAT PDO ↔ CAN 转发桥(1.5.55)。
|
* @brief EtherCAT PDO ↔ CAN 转发桥(1.6.21:段 2 改 9 电机密排整轮)。
|
||||||
*
|
*
|
||||||
* 输出 PDO(0x7000, 主站→从站, 64B) = 待发送的 CAN 帧(数据区 32B)
|
* 输出 PDO(0x7000, 主站→从站, 384B) = 待发送的 CAN 帧(数据区 32B)
|
||||||
* 输入 PDO(0x6000, 从站→主站, 384B) = 桥状态 + 电机状态窗口 + 长帧快照
|
* 输入 PDO(0x6000, 从站→主站, 600B) = 桥状态 + 电机整轮状态 + 长帧快照 + 诊断
|
||||||
*
|
*
|
||||||
* 发送用**序号沿触发**:仅当 seq 字节变化时才真正发一帧。PDO 是周期性刷新的,
|
* 发送用**序号沿触发**:仅当 seq 字节变化时才真正发一帧。PDO 是周期性刷新的,
|
||||||
* 若按「内容非空即发」会把同一帧每个周期重发一次;seq 变化沿是 PDO 里表达
|
* 若按「内容非空即发」会把同一帧每个周期重发一次;seq 变化沿是 PDO 里表达
|
||||||
@@ -13,40 +13,23 @@
|
|||||||
* TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态
|
* TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态
|
||||||
* 会随机丢帧。改为由 taskCanRxParse() 的分发链回调 onCanFrame() 投递。
|
* 会随机丢帧。改为由 taskCanRxParse() 的分发链回调 onCanFrame() 投递。
|
||||||
*
|
*
|
||||||
* ── 1.5.55 为电机控制台扩容(32B/32B → 64B/384B)──
|
* ── 1.6.21:段 2 改「一帧 = 总线电机整轮状态」,删段 4 历史帧窗口 ──
|
||||||
*
|
*
|
||||||
* 三个动机:
|
* 旧布局的问题(用户实测:接 3 个电机网页却看到 5 个状态帧):
|
||||||
* 1. TX 数据区 24B→32B。电机 CMD 是 DLC 32,旧 processOutput 里
|
* 1. 段 2 是固定 5 槽滑窗(win_base 要 6 拍才扫完 27 槽),空槽 node_id=0
|
||||||
* `dlc > kEcatCanTxDataMax` 直接钳位到 24,后 8 字节被**静默丢弃**且不报错。
|
* 也占位,win_count 报的是跨度不是电机数 —— 主站若不滤空槽就会多报;
|
||||||
* 2. 输入侧加电机状态窗口。单帧快照对电机控制台不够用:9 节点各 32B STATUS,
|
* 2. 段 4 覆盖窗口把最近几条非 STATUS 帧反复重发,属于「历史帧 / 重复帧」。
|
||||||
* 一周期只带回一帧、新帧还覆盖旧帧,永远取不齐。改为直接从 MotorState
|
|
||||||
* 状态表(已按 node×channel 聚合,见 device/motor_state.h)取一个窗口,
|
|
||||||
* 主站移动 win_base 即可 4 个周期扫完全部 27 槽。
|
|
||||||
* 3. 加长帧快照区。旧 RX 数据区只有 8B,版本帧(DLC24)/CONFIG 帧(DLC24) 会被截断。
|
|
||||||
*
|
*
|
||||||
* **输入 PDO 前 32 字节与 1.5.53 逐字节兼容**,偏移与语义全部不变。这样
|
* 本版语义改为用户需求:
|
||||||
* tools/ecat 的 CAN 调试页(ecat.html)与 ecat_server.py 的 _can_decode_input
|
* · 每一拍输入 PDO 携带**总线上已识别电机的完整一轮状态**(最多 9 个);
|
||||||
* 不改一行也照常工作,扩容只是在后面追加新区段。
|
* · **空槽不发送**:密排,count = 本拍实际打包的电机数;
|
||||||
|
* · 主站按 count 迭代,每槽自带 node_id,自动识别,无需 win_base 滑窗;
|
||||||
|
* · 不发段 4 历史帧;版本/CONFIG 仍走段 1/段 3「最近一帧」快照;
|
||||||
|
* · 上一轮已打包且 seq 未变的电机,本轮跳过 MotorState 重读与 48B 拷贝,
|
||||||
|
* 直接复用影子区(s_motShadow)。
|
||||||
*
|
*
|
||||||
* ── 1.5.56 扩到 DPRAM 硬上限并加 CANFD 批量帧区(64/384 → 384/640)──
|
* 输入 PDO 总长仍 600B ⇒ 无需重刷 EEPROM/SII;段 2/3 偏移变了,**主站解码
|
||||||
*
|
* 必须同步**(tools/ecat 的 decode_input / transport-ecat.js 等)。
|
||||||
* 动机:需要一个周期里合并下发/回收**多条 CANFD 帧**。1.5.55 的单帧槽
|
|
||||||
* (一个 seq 一帧)每周期最多一帧,4ms 周期就是 250 帧/s 上限,
|
|
||||||
* 而一次多轴同步下发就要 9 帧,靠排队要 36ms 才发完 —— 各轴指令不同拍,
|
|
||||||
* 失去"同步下发"的意义。
|
|
||||||
*
|
|
||||||
* 新增两个批量区,每条帧带 8B 头(ch/dlc/flags/rsvd + id)+ 数据:
|
|
||||||
* 输出 48..383(336B):批量发送,一个 seq 沿一次性发 N 帧
|
|
||||||
* 输入 360..:批量接收,一周期回收多帧而不是只带最近一帧
|
|
||||||
* (1.6.1 数据条目 64→32B;1.6.15 改定长覆盖窗口 5×44B 到 583,
|
|
||||||
* 段 5 诊断区 484..495 → 584..595,输入总长 496 → 600)
|
|
||||||
*
|
|
||||||
* 三条兼容性铁律(改动只做追加,不动既有偏移):
|
|
||||||
* 1. 输出 0..47 与 1.5.55 单帧布局完全一致(seq/ch/dlc/flags/id/win/data32);
|
|
||||||
* 2. 输入 0..359 与 1.5.55 完全一致(段1 桥状态 + 段2 电机窗口 + 段3 长帧);
|
|
||||||
* 3. 批量区**为空时**(count=0)行为与 1.5.55 完全相同,老主站照旧可用。
|
|
||||||
*
|
|
||||||
* 上限推导(为何是 384+640 而不是更大)见 comm/ethercat_pdo_size.h。
|
|
||||||
*/
|
*/
|
||||||
#ifndef COMM_ETHERCAT_CAN_BRIDGE_H
|
#ifndef COMM_ETHERCAT_CAN_BRIDGE_H
|
||||||
#define COMM_ETHERCAT_CAN_BRIDGE_H
|
#define COMM_ETHERCAT_CAN_BRIDGE_H
|
||||||
@@ -54,20 +37,21 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
#include "comm/ethercat_pdo_size.h"
|
#include "comm/ethercat_pdo_size.h"
|
||||||
|
#include "device/motor_state.h" /* kMotorMaxNodes / kMotorChannels */
|
||||||
|
|
||||||
/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。**与 1.5.55 完全兼容**。 */
|
/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。布局与 1.5.55 兼容。 */
|
||||||
enum : uint8_t {
|
enum : uint8_t {
|
||||||
ECAT_CAN_TX_OFF_SEQ = 0U, /**< 发送序号:变化沿触发一次发送(单帧+批量一起) */
|
ECAT_CAN_TX_OFF_SEQ = 0U, /**< 发送序号:变化沿触发一次发送(单帧+批量一起) */
|
||||||
ECAT_CAN_TX_OFF_CH = 1U, /**< 目标通道 0/1/2 */
|
ECAT_CAN_TX_OFF_CH = 1U, /**< 目标通道 0/1/2 */
|
||||||
ECAT_CAN_TX_OFF_DLC = 2U, /**< 数据长度 0..32;0 表示本次不发单帧(只发批量) */
|
ECAT_CAN_TX_OFF_DLC = 2U, /**< 数据长度 0..32;0 表示本次不发单帧(只发批量) */
|
||||||
ECAT_CAN_TX_OFF_FLAGS = 3U, /**< bit0=1 请求清零统计;其余保留 */
|
ECAT_CAN_TX_OFF_FLAGS = 3U, /**< bit0=1 请求清零统计;其余保留 */
|
||||||
ECAT_CAN_TX_OFF_ID = 4U, /**< CAN ID,小端 uint32(29 位扩展帧) */
|
ECAT_CAN_TX_OFF_ID = 4U, /**< CAN ID,小端 uint32(29 位扩展帧) */
|
||||||
ECAT_CAN_TX_OFF_WIN = 8U, /**< 请求回传的电机槽窗口起始下标 0..26 */
|
ECAT_CAN_TX_OFF_WIN = 8U, /**< 1.6.21 起忽略(电机整轮密排,无滑窗);保留占位 */
|
||||||
/* 9..15 保留:让数据区落在 16 字节边界,hexdump 排查时对齐好读 */
|
/* 9..15 保留:让数据区落在 16 字节边界 */
|
||||||
ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */
|
ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 输出 PDO 段 2(48..383):CANFD 批量发送区(1.5.56 新增)。 */
|
/** 输出 PDO 段 2(48..383):CANFD 批量发送区。 */
|
||||||
enum : uint16_t {
|
enum : uint16_t {
|
||||||
ECAT_TXB_OFF_COUNT = 48U, /**< 本次要发的帧数 0..kEcatTxBatchMax;0=不发批量 */
|
ECAT_TXB_OFF_COUNT = 48U, /**< 本次要发的帧数 0..kEcatTxBatchMax;0=不发批量 */
|
||||||
ECAT_TXB_OFF_FLAGS = 49U, /**< bit0=1 遇错即停(默认继续发完并各自记结果) */
|
ECAT_TXB_OFF_FLAGS = 49U, /**< bit0=1 遇错即停(默认继续发完并各自记结果) */
|
||||||
@@ -85,7 +69,7 @@ enum : uint8_t {
|
|||||||
ECAT_FRM_HDR_SIZE = 8U, /**< 头长度,数据紧跟其后 */
|
ECAT_FRM_HDR_SIZE = 8U, /**< 头长度,数据紧跟其后 */
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照。**与 1.5.53 完全兼容**。 */
|
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照。 */
|
||||||
enum : uint8_t {
|
enum : uint8_t {
|
||||||
ECAT_CAN_RX_OFF_ACK = 0U, /**< 回显已执行的发送序号,主站据此确认送达 */
|
ECAT_CAN_RX_OFF_ACK = 0U, /**< 回显已执行的发送序号,主站据此确认送达 */
|
||||||
ECAT_CAN_RX_OFF_CH = 1U, /**< 该帧来自哪个通道 */
|
ECAT_CAN_RX_OFF_CH = 1U, /**< 该帧来自哪个通道 */
|
||||||
@@ -97,231 +81,134 @@ enum : uint8_t {
|
|||||||
ECAT_CAN_RX_OFF_TXRES = 20U, /**< 最近一次发送结果(CanTxResult 原值,0=OK) */
|
ECAT_CAN_RX_OFF_TXRES = 20U, /**< 最近一次发送结果(CanTxResult 原值,0=OK) */
|
||||||
ECAT_CAN_RX_OFF_BACKLOG = 21U, /**< 该通道软件 TX 队列积压 */
|
ECAT_CAN_RX_OFF_BACKLOG = 21U, /**< 该通道软件 TX 队列积压 */
|
||||||
ECAT_CAN_RX_OFF_TXCNT = 22U, /**< 累计发送受理帧数,小端 uint16 */
|
ECAT_CAN_RX_OFF_TXCNT = 22U, /**< 累计发送受理帧数,小端 uint16 */
|
||||||
ECAT_CAN_RX_OFF_DROP = 24U, /**< 段 1/段 3 快照「未取先覆」次数:上一帧尚未被任何一拍
|
ECAT_CAN_RX_OFF_DROP = 24U, /**< 段 1/段 3 快照「未取先覆」次数(u16 累计回绕) */
|
||||||
PDO 发布就被新帧覆盖才 +1(u16,累计回绕)。 */
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 输入 PDO 段 2(32..319):电机状态窗口头 + 槽数组。 */
|
/**
|
||||||
|
* 输入 PDO 段 2(32..543):电机整轮状态(密排,1.6.21)。
|
||||||
|
*
|
||||||
|
* 主站解析:
|
||||||
|
* count = in[32] // 0..9,本拍实际打包的电机数
|
||||||
|
* for (i = 0; i < count; i++)
|
||||||
|
* slot = in[40 + i*56] // node_id/online/dlc/seq/status
|
||||||
|
*
|
||||||
|
* 空节点不占位;槽序按 node_id 升序;同号多通道取 seq 最新的一条。
|
||||||
|
*/
|
||||||
enum : uint16_t {
|
enum : uint16_t {
|
||||||
ECAT_MOTW_OFF_ACTIVE = 32U, /**< 在线电机数 */
|
ECAT_MOTW_OFF_COUNT = 32U, /**< 本拍密排电机数 0..9(主站按此迭代) */
|
||||||
ECAT_MOTW_OFF_BOOT = 33U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */
|
ECAT_MOTW_OFF_ACTIVE = 33U, /**< 在线电机数(与 count 可不同:离线但仍报最后一轮) */
|
||||||
ECAT_MOTW_OFF_WIN_BASE = 34U, /**< 本帧携带的槽窗口起始下标(回显主站请求) */
|
ECAT_MOTW_OFF_BOOT = 34U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */
|
||||||
ECAT_MOTW_OFF_WIN_COUNT = 35U, /**< 本帧携带的有效槽数(≤5) */
|
ECAT_MOTW_OFF_MAGIC = 35U, /**< 恒 kEcatMotLayoutMagic(0xA5):1.6.21 密排标识 */
|
||||||
ECAT_MOTW_OFF_GLOBAL_SEQ = 36U, /**< 状态表全局刷新序号,小端 uint32 */
|
ECAT_MOTW_OFF_GLOBAL_SEQ = 36U, /**< 状态表全局刷新序号,小端 uint32 */
|
||||||
ECAT_MOTW_OFF_SLOTS = 40U, /**< 槽数组起点,5×56B(40..319,与段 3 正好相接) */
|
ECAT_MOTW_OFF_SLOTS = 40U, /**< 密排槽数组起点,count×56B */
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 电机状态槽内部布局(56 字节)。 */
|
/** 电机状态槽内部布局(56 字节)。 */
|
||||||
enum : uint8_t {
|
enum : uint8_t {
|
||||||
ECAT_MOT_OFF_NODE = 0U, /**< 节点号 1..9,0=空槽 */
|
ECAT_MOT_OFF_NODE = 0U, /**< 节点号 1..9(密排下不会出现 0) */
|
||||||
ECAT_MOT_OFF_CH = 1U, /**< CAN 通道 0/1/2 */
|
ECAT_MOT_OFF_CH = 1U, /**< CAN 通道 0/1/2 */
|
||||||
ECAT_MOT_OFF_ONLINE = 2U, /**< 1=在线 */
|
ECAT_MOT_OFF_ONLINE = 2U, /**< 1=在线 */
|
||||||
ECAT_MOT_OFF_DLC = 3U, /**< 最后一帧 STATUS 的 DLC(正常 48) */
|
ECAT_MOT_OFF_DLC = 3U, /**< 最后一帧 STATUS 的 DLC(正常 48) */
|
||||||
ECAT_MOT_OFF_SEQ = 4U, /**< STATUS 刷新序号,小端 uint32 */
|
ECAT_MOT_OFF_SEQ = 4U, /**< STATUS 刷新序号,小端 uint32 */
|
||||||
ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,48 字节(v0.74:B32-35=InitStatus) */
|
ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,48 字节 */
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 输入 PDO 段 3(320..359):最近一帧**完整**快照(版本/CONFIG 帧要 24B)。 */
|
/** 输入 PDO 段 3(544..583):最近一帧完整快照(版本/CONFIG 帧要 24B+)。 */
|
||||||
enum : uint16_t {
|
enum : uint16_t {
|
||||||
ECAT_XRX_OFF_CH = 320U, /**< 通道 */
|
ECAT_XRX_OFF_CH = 544U, /**< 通道 */
|
||||||
ECAT_XRX_OFF_DLC = 321U, /**< 原始 DLC(可达 32) */
|
ECAT_XRX_OFF_DLC = 545U, /**< 原始 DLC(可达 32) */
|
||||||
ECAT_XRX_OFF_FLAGS = 322U, /**< bit0=有效 */
|
ECAT_XRX_OFF_FLAGS = 546U, /**< bit0=有效 */
|
||||||
ECAT_XRX_OFF_RSVD = 323U,
|
ECAT_XRX_OFF_RSVD = 547U,
|
||||||
ECAT_XRX_OFF_ID = 324U, /**< CAN ID,小端 uint32 */
|
ECAT_XRX_OFF_ID = 548U, /**< CAN ID,小端 uint32 */
|
||||||
ECAT_XRX_OFF_DATA = 328U, /**< 数据区,32 字节完整 payload */
|
ECAT_XRX_OFF_DATA = 552U, /**< 数据区,32 字节完整 payload */
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 输入 PDO 段 4(360..583):CANFD 批量接收**定长覆盖窗口**(1.6.15)。
|
/** 输入 PDO 段 5(584..595):设备诊断区(位置未变)。 */
|
||||||
*
|
|
||||||
* 与旧"环形队列 + 变长紧排排空"的区别:生产者(onCanFrame,主循环)按自己的
|
|
||||||
* 游标写槽、绕圈覆盖谁就是谁;消费者(fillRxWindow,PDI_Isr)每拍整块取值。
|
|
||||||
* 没有 tail、没有判满、没有 pending —— 1.6.9 修的那个"滞后的 tail 让生产者
|
|
||||||
* 写进消费者刚读过的槽"错标竞态,其载体(tail 握手)在本布局里不存在了。
|
|
||||||
*
|
|
||||||
* 定长是"整块取"的前提:槽下标 = (ECAT_RXB_OFF_ITEMS + i*ECAT_RXB_SLOT_SIZE),
|
|
||||||
* 主站按固定偏移读,不再需要"8 + min(dlc,上限)"推游标,也就不可能再出现
|
|
||||||
* 1.6.10 那种游标越进诊断区的边界错。
|
|
||||||
*
|
|
||||||
* 每槽自带 seqlock(ECAT_RXW_OFF_SEQ):PDI_Isr(preempt=0) 会抢占主循环写侧,
|
|
||||||
* 没有它主站能拿到"新 ID + 旧数据"拼出来的假帧 —— 那不是"差一个周期",是
|
|
||||||
* 现实中不存在但看起来完全合法的帧。seq 同时是主站去重与算丢帧的依据。
|
|
||||||
*
|
|
||||||
* 语义变更(相对旧批量区):**最近 N 帧窗口**,不是"保证不丢的事件流"。
|
|
||||||
* 同一帧会在连续几拍里重复出现(直到被覆盖),低频一次性帧(版本/节点应答)
|
|
||||||
* 在突发流量下可能被静默覆盖 —— 主站侧按 seq 去重后归一成事件流再上报,
|
|
||||||
* 并可用 seq 空洞算出被覆盖掉的帧数。 */
|
|
||||||
enum : uint16_t {
|
enum : uint16_t {
|
||||||
ECAT_RXB_OFF_SLOT_COUNT = 360U, /**< 窗口槽数(编译期常量 ECAT_RXB_SLOTS) */
|
ECAT_DIAG_OFF_LOAD = 584U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%) */
|
||||||
ECAT_RXB_OFF_WR_IDX = 361U, /**< 最近一次写入的槽下标 0..N-1(诊断节拍用) */
|
ECAT_DIAG_OFF_TORN = 586U, /**< CAN 取帧撕裂丢帧数(低 16 位,饱和) */
|
||||||
ECAT_RXB_OFF_OVF = 362U, /**< 自上次回填以来,写入了"主站尚未取过"槽的次数
|
ECAT_DIAG_OFF_SEQ = 588U, /**< 诊断区序号,小端 uint16,每完成 1s 窗口 +1 */
|
||||||
(饱和 255),回填后清零。与主站 seq 空洞是两个
|
ECAT_DIAG_OFF_OVERFULL = 590U, /**< CAN RX FIFO 满溢计数 RFO0(u16 饱和) */
|
||||||
独立仪表,但**度量窗口不同**:本计数是"还没拷进
|
ECAT_DIAG_OFF_RELTIMEOUT = 592U, /**< RFD0 释放握手超时计数(u16 饱和) */
|
||||||
PDO 镜像就被覆写"(亚拍级),主站算的是"两次解码
|
ECAT_DIAG_OFF_REJECT = 594U, /**< CAN 内容级拼接帧命中计数(u16 饱和) */
|
||||||
之间被覆写"。主站与从站同拍(1kHz)时两者应一致;
|
|
||||||
主站掉拍时主站数更大,那是真实差异不是仪表坏了。 */
|
|
||||||
ECAT_RXB_OFF_RSVD = 363U,
|
|
||||||
ECAT_RXB_OFF_ITEMS = 364U, /**< 槽数组起点,N × ECAT_RXB_SLOT_SIZE 定长 */
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 覆盖窗口单槽布局(44 字节)。头 12B + 数据 32B。 */
|
|
||||||
enum : uint8_t {
|
|
||||||
ECAT_RXW_OFF_CH = 0U, /**< 通道 0/1/2 */
|
|
||||||
ECAT_RXW_OFF_DLC = 1U, /**< **原始** DLC 0..64(不是线上长度) */
|
|
||||||
ECAT_RXW_OFF_FLAGS = 2U, /**< bit0=本槽至少写过一帧; bit1=线上数据被截断到 32B */
|
|
||||||
ECAT_RXW_OFF_RSVD = 3U,
|
|
||||||
ECAT_RXW_OFF_ID = 4U, /**< CAN ID,小端 uint32 */
|
|
||||||
ECAT_RXW_OFF_SEQ = 8U, /**< 全局单调 seq,小端 u32。奇数=写入中(消费者跳过);
|
|
||||||
偶数=整槽一致。跨槽共用一个计数器,故主站可比大小
|
|
||||||
排序、可用空洞数出被覆盖的帧。
|
|
||||||
**发布值恒为 2,4,6,…(奇数被"写入中"占用),所以
|
|
||||||
相邻两帧的 seq 差 2**:主站算空洞必须用 step/2-1,
|
|
||||||
按 step-1 会把每帧都虚报成丢 1 个(1.6.15 首版踩过,
|
|
||||||
症见 can_cadence 每拍 count=2/lost=2 而 OVF=0)。 */
|
|
||||||
ECAT_RXW_OFF_DATA = 12U, /**< 数据区,32 字节(kEcatRxBatchDataMax) */
|
|
||||||
};
|
|
||||||
/** 覆盖窗口单槽字节数:12B 头 + 32B 数据。改任一字段宽度都要重算总预算。 */
|
|
||||||
constexpr uint8_t ECAT_RXB_SLOT_SIZE = 44U;
|
|
||||||
/** 覆盖窗口槽数。5 槽 = 5000 帧/s@1ms;必须 ≥ 每拍到达帧数,否则静默丢帧。 */
|
|
||||||
constexpr uint8_t ECAT_RXB_SLOTS = 5U;
|
|
||||||
|
|
||||||
/** 输入 PDO 段 5(584..595):设备诊断区(1.5.61 尾部追加,1.6.1 前移到 484,
|
|
||||||
* 1.6.15 随批量覆盖窗口扩到 5 槽再前移到 584:364+5×44=584,
|
|
||||||
* 诊断区锚在紧随其后的 12 字节,写侧是 EthercatPdo::fillDiagTail())。 */
|
|
||||||
enum : uint16_t {
|
|
||||||
ECAT_DIAG_OFF_LOAD = 584U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%)。
|
|
||||||
中断 + 非延迟处理段(任务派发/OTA relay)占墙钟比,
|
|
||||||
不含常开轮询与空转;1s 滚动窗口(见 sys/cpu_load.h) */
|
|
||||||
ECAT_DIAG_OFF_TORN = 586U, /**< CAN 取帧撕裂丢帧数(低 16 位,小端,饱和不回绕)。
|
|
||||||
1.6.6 起:ISR 取帧时若 MP 的 TS/DLENC 前后不一致,
|
|
||||||
说明邮箱窗口在取数途中被硬件换帧,该帧整帧丢弃并
|
|
||||||
在此计数。这是"读到半条帧"的唯一可观测量 —— 主站
|
|
||||||
据此判断修复是否生效、总线负载是否高到频繁触发。 */
|
|
||||||
ECAT_DIAG_OFF_SEQ = 588U, /**< 诊断区序号,小端 uint16(1.6.12 起,原 u32 让出
|
|
||||||
高 2 字节),每完成一个 1s 统计窗口 +1,主站据此
|
|
||||||
判断负载值是否仍在滚动刷新。u16 足够:主站只判
|
|
||||||
"非 0 且在涨",18h 才回绕。 */
|
|
||||||
ECAT_DIAG_OFF_OVERFULL = 590U, /**< CAN RX FIFO 满溢计数 RFO0(u16 饱和,1.6.12)。
|
|
||||||
1.6.11 把溢出策略改成"满则丢新帧"后拼接并未消失
|
|
||||||
(段5@594 以 ~384/s 增长),必须直接观测溢出本身
|
|
||||||
与拼接率是否同涨,才能判定残余拼接是否仍源自溢出
|
|
||||||
(即本 IP 在 DISCARD 模式下照样毁窗口)。 */
|
|
||||||
ECAT_DIAG_OFF_RELTIMEOUT = 592U, /**< RFD0 释放握手超时计数(u16 饱和,1.6.12)。
|
|
||||||
原为 1.6.7 的 PDO 重入计数 ECAT_DIAG_OFF_REENTRY
|
|
||||||
(引入至今恒 0,不再上线,仍留内部计数)。取帧后
|
|
||||||
等 RFD0 自清 2000 次未果 = dequeue 未完成,窗口
|
|
||||||
与读指针可能已失同步 —— 拼接的直接前兆。 */
|
|
||||||
ECAT_DIAG_OFF_REJECT = 594U, /**< CAN 内容级拼接帧命中率(u16 饱和,1.6.11)。
|
|
||||||
入队前结构自洽校验(STATUS 温度量程+斜率、CONFIG
|
|
||||||
未使用区/模式)命中即计数。1.6.12 起**只计不丢**
|
|
||||||
—— 数据管道必须诚实,该计数退化为"源头是否还在
|
|
||||||
产生拼接帧"的纯测量仪表:修好 = 恒 0,没修好 =
|
|
||||||
网页看到什么,计数就是什么的倍率。 */
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 单帧数据在输出 PDO / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */
|
/** 单帧数据在输出 PDO / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */
|
||||||
constexpr uint8_t kEcatCanTxDataMax = 32U;
|
constexpr uint8_t kEcatCanTxDataMax = 32U;
|
||||||
/** 段 1 短快照数据区字节数(保持 1.5.53 兼容,勿改)。 */
|
/** 段 1 短快照数据区字节数。 */
|
||||||
constexpr uint8_t kEcatCanRxDataMax = 8U;
|
constexpr uint8_t kEcatCanRxDataMax = 8U;
|
||||||
/** 段 3 长快照数据区字节数。 */
|
/** 段 3 长快照数据区字节数。 */
|
||||||
constexpr uint8_t kEcatCanXRxDataMax = 32U;
|
constexpr uint8_t kEcatCanXRxDataMax = 32U;
|
||||||
|
|
||||||
/** 单个电机状态槽字节数:8B 头 + 48B STATUS(v0.74)。
|
/** 单个电机状态槽字节数:8B 头 + 48B STATUS。 */
|
||||||
* 1.6.4 前是 40B(只装得下 32B STATUS),于是 buildInput 按 48B 拷贝时会溢出
|
|
||||||
* 16B 盖到段 3,且主站永远拿不到 B32-35 的 InitStatus。 */
|
|
||||||
constexpr uint8_t kEcatMotSlotSize = 56U;
|
constexpr uint8_t kEcatMotSlotSize = 56U;
|
||||||
/** 一份输入 PDO 携带的电机状态槽数:段 2 可用 280B(40..319)÷ 56 = 5。
|
/** 一拍最多打包的电机数 = 节点号 1..9 各一槽(同号多通道取最新)。 */
|
||||||
* 27 槽需 ⌈27/5⌉ = 6 个基址扫完。槽数×槽长仍是 280B,输入 PDO 总长不变,
|
constexpr uint8_t kEcatMotSlotsPerPdo = kMotorMaxNodes; /* 9 */
|
||||||
* 所以 EEPROM/SII 无需重刷。 */
|
/** 段 2 头长度(COUNT..GLOBAL_SEQ,8B)。 */
|
||||||
constexpr uint8_t kEcatMotSlotsPerPdo = 5U;
|
constexpr uint8_t kEcatMotHdrSize = 8U;
|
||||||
|
/** 段 2 密排区最大字节:头 8B + 9×56B = 512B,落在 32..543。 */
|
||||||
|
constexpr uint16_t kEcatMotRegionBytes =
|
||||||
|
(uint16_t)(kEcatMotHdrSize
|
||||||
|
+ (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize);
|
||||||
|
/** 布局魔数,写在 offset 35。主站据此区分 1.6.21 密排与 1.6.15 滑窗(两者
|
||||||
|
* Ibytes 同为 600,不能只靠长度分支)。0=旧布局。 */
|
||||||
|
constexpr uint8_t kEcatMotLayoutMagic = 0xA5U;
|
||||||
|
|
||||||
/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h(C/C++ 共用)。 */
|
/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h。 */
|
||||||
constexpr uint16_t kEcatPdoOutBytes = ECAT_PDO_OUT_BYTES; /* 48×64bit = 384B */
|
constexpr uint16_t kEcatPdoOutBytes = ECAT_PDO_OUT_BYTES; /* 384B */
|
||||||
constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 75×64bit = 600B */
|
constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 600B */
|
||||||
|
|
||||||
/** CANFD 单帧数据上限(DLC 64)—— 覆盖窗口/发送批量区按此上限做容量推导。 */
|
/** CANFD 单帧数据上限。 */
|
||||||
constexpr uint8_t kEcatCanFdDataMax = 64U;
|
constexpr uint8_t kEcatCanFdDataMax = 64U;
|
||||||
|
|
||||||
/** 批量**接收**槽在 PDO 内的数据字节数上限(1.6.1 从 64 压到 32)。
|
|
||||||
* 真机采样 6086 帧 DLC 只有 24/32 两档;超长帧在装填时截断(flags bit1),
|
|
||||||
* RAM 侧 RxWinSlot 仍存全量 64B,截断只发生在拷进 PDO 这一步。
|
|
||||||
* 将来若要线上传 64B:槽长变 12+64=76,5 槽需 364+380+12=756B > 640 上限,
|
|
||||||
* 届时只能降到 3 槽(364+228+12=604 → 输入 608B),即"全量"与"帧数"二选一。 */
|
|
||||||
constexpr uint8_t kEcatRxBatchDataMax = 32U;
|
|
||||||
|
|
||||||
/** 批量发送区可用字节数与最坏情况(全 DLC64)帧数上限。 */
|
/** 批量发送区可用字节数与最坏情况(全 DLC64)帧数上限。 */
|
||||||
constexpr uint16_t kEcatTxBatchBytes = (uint16_t)(kEcatPdoOutBytes - ECAT_TXB_OFF_ITEMS);
|
constexpr uint16_t kEcatTxBatchBytes = (uint16_t)(kEcatPdoOutBytes - ECAT_TXB_OFF_ITEMS);
|
||||||
constexpr uint8_t kEcatTxBatchMax =
|
constexpr uint8_t kEcatTxBatchMax =
|
||||||
(uint8_t)(kEcatTxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax));
|
(uint8_t)(kEcatTxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax));
|
||||||
|
|
||||||
/* 布局守卫:段 2 的槽数组必须完整落在输入 PDO 内,段 3 也不能越界。
|
/* ── 布局守卫 ── */
|
||||||
* 改槽数/槽大小时忘了同步 PDO 长度,会在这里编译期失败。 */
|
static_assert(ECAT_MOTW_OFF_SLOTS
|
||||||
static_assert(ECAT_MOTW_OFF_SLOTS + kEcatMotSlotsPerPdo * kEcatMotSlotSize
|
+ (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize
|
||||||
<= ECAT_XRX_OFF_CH, "电机槽窗口与长帧快照区重叠");
|
== ECAT_XRX_OFF_CH,
|
||||||
static_assert(ECAT_XRX_OFF_DATA + kEcatCanXRxDataMax <= ECAT_RXB_OFF_SLOT_COUNT,
|
"段2密排区必须与段3首尾相接");
|
||||||
"长帧快照区与批量接收区重叠");
|
static_assert(ECAT_XRX_OFF_DATA + kEcatCanXRxDataMax == ECAT_DIAG_OFF_LOAD,
|
||||||
|
"段3必须与段5首尾相接");
|
||||||
static_assert(ECAT_CAN_TX_OFF_DATA + kEcatCanTxDataMax <= ECAT_TXB_OFF_COUNT,
|
static_assert(ECAT_CAN_TX_OFF_DATA + kEcatCanTxDataMax <= ECAT_TXB_OFF_COUNT,
|
||||||
"单帧发送数据区与批量发送区重叠");
|
"单帧发送数据区与批量发送区重叠");
|
||||||
static_assert(ECAT_RXB_OFF_ITEMS < kEcatPdoInBytes, "批量接收区起点越界");
|
|
||||||
static_assert(ECAT_TXB_OFF_ITEMS < kEcatPdoOutBytes, "批量发送区起点越界");
|
static_assert(ECAT_TXB_OFF_ITEMS < kEcatPdoOutBytes, "批量发送区起点越界");
|
||||||
/* 批量区至少要装得下一帧满长 CANFD,否则"批量"没有意义 */
|
|
||||||
static_assert(kEcatTxBatchMax >= 1U, "批量发送区装不下一帧 DLC64");
|
static_assert(kEcatTxBatchMax >= 1U, "批量发送区装不下一帧 DLC64");
|
||||||
/* 槽长必须容得下头 + 线上数据上限;改任一字段宽度都在这里拦下。 */
|
|
||||||
static_assert(ECAT_RXW_OFF_DATA + kEcatRxBatchDataMax <= ECAT_RXB_SLOT_SIZE,
|
|
||||||
"覆盖窗口槽长容不下头+数据");
|
|
||||||
static_assert(ECAT_RXW_OFF_DATA + kEcatRxBatchDataMax == ECAT_RXB_SLOT_SIZE,
|
|
||||||
"覆盖窗口槽长有空洞:定长布局不应留空隙");
|
|
||||||
/* 整个覆盖窗口 + 段 5 诊断区必须落在输入 PDO 内。这是 600B 这个数的来源:
|
|
||||||
* 少一个字节都会在 SAFEOP 报 AL 0x001A/0x0011,而根因在这里编译期就能抓到。 */
|
|
||||||
static_assert(ECAT_RXB_OFF_ITEMS + (uint16_t)ECAT_RXB_SLOTS * ECAT_RXB_SLOT_SIZE
|
|
||||||
<= ECAT_DIAG_OFF_LOAD, "覆盖窗口与段5诊断区重叠");
|
|
||||||
static_assert(ECAT_DIAG_OFF_REJECT + 2U <= kEcatPdoInBytes, "段5诊断区越界");
|
static_assert(ECAT_DIAG_OFF_REJECT + 2U <= kEcatPdoInBytes, "段5诊断区越界");
|
||||||
/* 窗口至少要能容下一拍可能到达的帧数:槽数 < 帧/拍 时超出部分会在两次 PDO
|
static_assert(ECAT_MOTW_OFF_COUNT + kEcatMotRegionBytes <= ECAT_XRX_OFF_CH,
|
||||||
* 拷贝之间被覆盖,属静默丢帧(主站只能靠 seq 空洞事后发现)。5 槽对现场
|
"段2密排区越界");
|
||||||
* 4000 帧/s(=4 帧/拍) 留 25% 余量。 */
|
|
||||||
static_assert(ECAT_RXB_SLOTS >= 5U, "覆盖窗口槽数低于现场每拍到达帧数");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief EtherCAT ↔ CAN 转发桥。
|
* @brief EtherCAT ↔ CAN 转发桥。
|
||||||
* @note 全静态;PDO 侧函数在 Sync0_Isr(preempt=0) 或主循环 MainLoop 上下文执行,
|
* @note 全静态;PDO 侧函数在 Sync0_Isr(preempt=0) 或主循环 MainLoop 上下文执行,
|
||||||
* onCanFrame() 在主循环 taskCanRxParse() 上下文执行。共享状态用 volatile +
|
* onCanFrame() 在主循环 taskCanRxParse() 上下文执行。共享状态用 volatile +
|
||||||
* 单生产者/单消费者语义,不需要临界区(投递只写、消费只读+清标志)。
|
* 单生产者/单消费者语义。多字段一组用 seqlock:
|
||||||
* 例外是"多字段一组"的三处,全部用 seqlock 而不是握手:
|
* - 段 1/段 3 的「最近一帧快照」→ s_xrxSeq;
|
||||||
* - 段 1/段 3 的"最近一帧快照"(id/dlc/data 必须配套)→ s_xrxSeq;
|
* - 电机槽 48B payload → MotorSlot.wr_seq。
|
||||||
* - 段 2 电机槽(48B payload 一次 memcpy)→ MotorSlot.wr_seq;
|
|
||||||
* - 段 4 覆盖窗口每槽(1.6.15)→ RxWinSlot.seq。
|
|
||||||
* Sync0 是 preempt=0,critical.h 的 BASEPRI 方案挡不住它,故只能无锁协议。
|
* Sync0 是 preempt=0,critical.h 的 BASEPRI 方案挡不住它,故只能无锁协议。
|
||||||
* 单核下 ISR 执行期间生产者必然冻结,所以读侧判一次奇偶即可定性。
|
|
||||||
*/
|
*/
|
||||||
class EthercatCanBridge {
|
class EthercatCanBridge {
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* @brief 处理一份输出 PDO:seq 变化时向 CAN 发一帧。
|
* @brief 输出探针:seq 变化时请求全量读。
|
||||||
* @param[in] out kEcatPdoOutBytes 字节输出过程数据。
|
* @note 1.6.21 起不再刷新 win_base(电机整轮密排,无滑窗);仍处理 seq 沿
|
||||||
* @note 由 APPL_Application() 每周期调用。
|
* 与 FLAGS bit0 清统计。
|
||||||
*/
|
*/
|
||||||
/** @brief 输出探针(1.6.0 输出按需全量读)。
|
|
||||||
*
|
|
||||||
* PDI_Isr 每周期先读输出 PDO 头 12B(覆盖 SEQ@0 与 WIN@8),由本函数判定
|
|
||||||
* 是否需要全量读 384B:
|
|
||||||
* - 始终从探针刷新 s_winBase(主站每周期移动窗口扫描,与 seq 沿无关);
|
|
||||||
* - 首帧只记 seq 基准(与 processOutput 的 priming 一致),返回 false;
|
|
||||||
* - seq 变化沿或 FLAGS bit0(清统计请求)→ 返回 true,调用方全量读。
|
|
||||||
* s_lastSeq 仍由 processOutput 在全量路径上推进,本函数不写,保证沿语义
|
|
||||||
* 只处理一次。 */
|
|
||||||
static bool peekOutput(const uint8_t *head);
|
static bool peekOutput(const uint8_t *head);
|
||||||
|
|
||||||
static void processOutput(const uint8_t *out);
|
static void processOutput(const uint8_t *out);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 填充输入 PDO:桥状态 + 电机状态窗口 + 长帧快照。
|
* @brief 填充输入 PDO:桥状态 + 电机整轮密排 + 长帧快照。
|
||||||
* @param[out] in kEcatPdoInBytes 字节输入过程数据(本函数负责全部写满)。
|
* @param[out] in kEcatPdoInBytes 字节输入过程数据。
|
||||||
*/
|
*/
|
||||||
static void buildInput(uint8_t *in);
|
static void buildInput(uint8_t *in);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief CAN 收帧投递入口。
|
* @brief CAN 收帧投递入口(旁路复制,不消费帧)。
|
||||||
* @note 由 TaskRunner::taskCanRxParse() 在分发链里调用,不消费帧(不返回
|
|
||||||
* "已处理"),只做旁路复制,因此不影响 OTA relay / 电机状态。
|
|
||||||
*/
|
*/
|
||||||
static void onCanFrame(uint8_t channel, uint32_t id,
|
static void onCanFrame(uint8_t channel, uint32_t id,
|
||||||
const uint8_t *data, uint8_t length);
|
const uint8_t *data, uint8_t length);
|
||||||
@@ -331,33 +218,21 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
* @brief 处理输出 PDO 的批量发送区(seq 沿内调用一次)。
|
* @brief 填充段 2 电机整轮密排区(含差分跳过)。
|
||||||
* @return 实际受理的帧数。
|
* @return 本拍打包的电机数(写入 count 字段)。
|
||||||
* @note 逐帧解析 (8B 头 + dlc),边界不足即停 —— 不信任主站给的 count,
|
|
||||||
* 越界的 count 只会让循环提前结束,不会读到 PDO 之外。
|
|
||||||
*/
|
*/
|
||||||
|
static uint8_t fillMotorRound(uint8_t *in);
|
||||||
|
|
||||||
static uint8_t sendBatch(const uint8_t *out);
|
static uint8_t sendBatch(const uint8_t *out);
|
||||||
|
|
||||||
/** @brief 把批量接收覆盖窗口整块拷进输入 PDO 段 4(1.6.15)。 */
|
static volatile uint8_t s_lastSeq;
|
||||||
static void fillRxWindow(uint8_t *in);
|
static volatile bool s_seqPrimed;
|
||||||
|
|
||||||
static volatile uint8_t s_lastSeq; /**< 上一次已执行的发送序号 */
|
|
||||||
static volatile bool s_seqPrimed; /**< 是否已记录过基准 seq(防上电首帧误发) */
|
|
||||||
static volatile uint8_t s_lastTxResult;
|
static volatile uint8_t s_lastTxResult;
|
||||||
static volatile uint16_t s_txCount;
|
static volatile uint16_t s_txCount;
|
||||||
static volatile uint16_t s_dropCount;
|
static volatile uint16_t s_dropCount;
|
||||||
/** 主站请求的电机槽窗口起始下标(输出 PDO 的 WIN 字节,每周期刷新)。 */
|
|
||||||
static volatile uint8_t s_winBase;
|
|
||||||
|
|
||||||
/* 最近一帧接收快照:taskCanRxParse 写、PDO 读。
|
/* 最近一帧接收快照:taskCanRxParse 写、PDO 读。s_xrxSeq 为 seqlock。 */
|
||||||
* s_xrxSeq 为 seqlock:奇数=正在写,buildInput 本周期跳过段 1/段 3。
|
|
||||||
* 没有它,Sync0_Isr 抢占在"数据已写完、id 未更新"之间会把新帧数据
|
|
||||||
* 配旧帧 ID 发出去(STATUS 被标成 CONFIG → 前端解出离谱目标值)。 */
|
|
||||||
static volatile bool s_rxValid;
|
static volatile bool s_rxValid;
|
||||||
/** 上一帧是否已被至少一拍 PDO 发布进段 1/段 3。s_rxValid 只表示"开机以来收过帧"
|
|
||||||
* (flags bit0 与 backlog 取通道都依赖它常驻),不能拿它当"未取走"判据,
|
|
||||||
* 否则 drop 会恒等于 rx 计数。写侧(onCanFrame)置 false,读侧(buildInput
|
|
||||||
* 发布分支)置 true;单核下 ISR 抢占主循环,两者不会并发写同一帧。 */
|
|
||||||
static volatile bool s_rxPublished;
|
static volatile bool s_rxPublished;
|
||||||
static volatile uint32_t s_xrxSeq;
|
static volatile uint32_t s_xrxSeq;
|
||||||
static volatile uint8_t s_rxCh;
|
static volatile uint8_t s_rxCh;
|
||||||
@@ -367,28 +242,16 @@ private:
|
|||||||
static volatile uint32_t s_rxCount;
|
static volatile uint32_t s_rxCount;
|
||||||
static uint8_t s_rxData[kEcatCanXRxDataMax];
|
static uint8_t s_rxData[kEcatCanXRxDataMax];
|
||||||
|
|
||||||
/* ── 批量接收定长覆盖窗口(1.6.15,取代 1.5.56 的环形队列)──
|
/* ── 段 2 差分填充影子(1.6.21)──
|
||||||
* 生产者(onCanFrame,主循环)按 s_rxWinWr 写槽并绕圈覆盖;消费者
|
* 上一拍已发布的密排区副本 + 各节点上次发布的 seq/online + 头字段。
|
||||||
* (fillRxWindow,PDI_Isr/Sync0_Isr)每拍整块取值,**不消费、不清除**。
|
* 节点集合、seq、online、active/boot 全未变时整段复用,跳过 MotorState
|
||||||
* 因此没有 head/tail 握手,也就没有 1.6.9 那类"滞后的 tail 让生产者写进
|
* 重读与逐槽 48B 拷贝。掩码必须 u16:节点号 1..9,bit9 超出 uint8_t。 */
|
||||||
* 消费者刚读过的槽"的错标窗口。
|
static uint8_t s_motShadow[kEcatMotRegionBytes];
|
||||||
* 一致性靠每槽 seqlock:写侧 seq 置奇 → 填字段 → __DMB → seq 置偶(=++全局
|
static uint32_t s_motPubSeq[kMotorMaxNodes + 1U]; /* 下标=节点号 */
|
||||||
* 计数)。读侧在 ISR 里,单核下生产者必然冻结,判一次奇偶即可,奇数跳过该槽。
|
static uint8_t s_motPubOnline[kMotorMaxNodes + 1U]; /* checkOffline 不 bump seq,须单独比 */
|
||||||
* s_rxWinSeen 用于数 OVF:写到一个 seen==0 的槽 = 覆盖了一帧主站还没取过的
|
static uint16_t s_motPubMask; /**< bit n=1:节点 n 在影子区中 */
|
||||||
* 数据,这才是真丢帧;覆盖已取过的槽是设计意图,不计。 */
|
static uint8_t s_motPubCount;
|
||||||
struct RxWinSlot {
|
static bool s_motShadowValid;
|
||||||
volatile uint32_t seq; /**< 奇=写入中;偶=整槽一致 */
|
|
||||||
uint32_t id;
|
|
||||||
uint8_t ch;
|
|
||||||
uint8_t dlc; /**< 原始 DLC 0..64 */
|
|
||||||
uint8_t flags; /**< bit0=写过; bit1=线上截断到 32B */
|
|
||||||
uint8_t data[kEcatCanFdDataMax]; /**< RAM 侧存全量 64B,PDO 拷贝时才截断 */
|
|
||||||
};
|
|
||||||
static RxWinSlot s_rxWin[ECAT_RXB_SLOTS];
|
|
||||||
static volatile uint8_t s_rxWinWr; /**< 写游标(下一个要覆盖的槽) */
|
|
||||||
static volatile uint32_t s_rxWinSeq; /**< 全局单调 seq 源(偶数发布) */
|
|
||||||
static volatile uint8_t s_rxWinOvf; /**< 未取先覆次数(饱和,回填后清零) */
|
|
||||||
static volatile uint8_t s_rxWinSeen[ECAT_RXB_SLOTS]; /**< 1=已被某拍 PDO 取过 */
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ constexpr uint8_t kSegCount = 5U;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 各段字节上界(开区间),用于把变化量归到布局段落。
|
* @brief 各段字节上界(开区间),用于把变化量归到布局段落。
|
||||||
* @note 584..599 的"段5 诊断 + 4B 保留"合并计:差分写不会单独优化保留区,
|
* @note 1.6.21 布局:段1(0..31) 段2(32..543) 段3(544..583) 段4 已删 段5(584..599)。
|
||||||
* 拆开只是多一档,但保留区若在涨就说明有东西在越界写,所以仍要看得见。
|
* 段4 上界=段5 起点,区间为空、恒 0,子索引保留以免动对象字典。
|
||||||
*/
|
*/
|
||||||
constexpr uint16_t kSegEnd[kSegCount] = {32U, 320U, 360U, 584U, kInBytes};
|
constexpr uint16_t kSegEnd[kSegCount] = {32U, 544U, 584U, 584U, kInBytes};
|
||||||
|
|
||||||
/** @brief 上一份已发布的镜像。alignas(4) 是让优化器能把 4 字节 memcpy 折成单条
|
/** @brief 上一份已发布的镜像。alignas(4) 是让优化器能把 4 字节 memcpy 折成单条
|
||||||
* LDR 的前提——丢了它,比对会从 ~150 次字访问退化成 600 次字节访问。 */
|
* LDR 的前提——丢了它,比对会从 ~150 次字访问退化成 600 次字节访问。 */
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ public:
|
|||||||
static uint32_t runsTotal() noexcept; /**< sub16 窗口内 run 总数(配 cycles 求均值) */
|
static uint32_t runsTotal() noexcept; /**< sub16 窗口内 run 总数(配 cycles 求均值) */
|
||||||
static uint32_t runsMax() noexcept; /**< sub17 单拍 run 数峰值,A2 的 N 由它定 */
|
static uint32_t runsMax() noexcept; /**< sub17 单拍 run 数峰值,A2 的 N 由它定 */
|
||||||
static uint32_t seg1Bytes() noexcept; /**< sub18 段1(0..31) 桥状态+短快照 */
|
static uint32_t seg1Bytes() noexcept; /**< sub18 段1(0..31) 桥状态+短快照 */
|
||||||
static uint32_t seg2Bytes() noexcept; /**< sub19 段2(32..319) 电机状态窗口 */
|
static uint32_t seg2Bytes() noexcept; /**< sub19 段2(32..543) 电机整轮密排 */
|
||||||
static uint32_t seg3Bytes() noexcept; /**< sub20 段3(320..359) 长帧快照 */
|
static uint32_t seg3Bytes() noexcept; /**< sub20 段3(544..583) 长帧快照 */
|
||||||
static uint32_t seg4Bytes() noexcept; /**< sub21 段4(360..583) 批量覆盖窗 */
|
static uint32_t seg4Bytes() noexcept; /**< sub21 已删(1.6.21 恒 0) */
|
||||||
static uint32_t seg5Bytes() noexcept; /**< sub22 段5(584..599) 诊断区+保留 */
|
static uint32_t seg5Bytes() noexcept; /**< sub22 段5(584..599) 诊断区+保留 */
|
||||||
static uint32_t compareCyc() noexcept; /**< sub23 比对自身累计 DWT 周期(本征开销) */
|
static uint32_t compareCyc() noexcept; /**< sub23 比对自身累计 DWT 周期(本征开销) */
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ alignas(4) uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0};
|
|||||||
/* 输入 PDO 的工作缓冲:buildInput 填这里,填完整块发布到 s_in(见 process())。
|
/* 输入 PDO 的工作缓冲:buildInput 填这里,填完整块发布到 s_in(见 process())。
|
||||||
* 读侧 writeInputs 只读 s_in,因此永远看不到"填到一半"的镜像。 */
|
* 读侧 writeInputs 只读 s_in,因此永远看不到"填到一半"的镜像。 */
|
||||||
alignas(4) uint8_t EthercatPdo::s_build[EthercatPdo::kPdoInBytes] = {0};
|
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_outputCycles = 0U;
|
||||||
volatile uint32_t EthercatPdo::s_inputCycles = 0U;
|
volatile uint32_t EthercatPdo::s_inputCycles = 0U;
|
||||||
@@ -154,6 +163,131 @@ void EthercatPdo::writeInputs(uint8_t *esc)
|
|||||||
++s_inputCycles;
|
++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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
void EthercatPdo::snapshotOutput(uint8_t *dst)
|
||||||
{
|
{
|
||||||
if (dst == nullptr) {
|
if (dst == nullptr) {
|
||||||
@@ -186,4 +320,32 @@ void EcatPdo_WriteInputs(uint8_t *esc)
|
|||||||
EthercatPdo::writeInputs(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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" */
|
} /* extern "C" */
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ public:
|
|||||||
* @brief 输出探针(1.6.0 输出按需全量读,配合 SSC PDO_OutputMapping)。
|
* @brief 输出探针(1.6.0 输出按需全量读,配合 SSC PDO_OutputMapping)。
|
||||||
* @param[in] head 输出 PDO 头 12B(覆盖 SEQ@0 / WIN@8 / FLAGS@3)。
|
* @param[in] head 输出 PDO 头 12B(覆盖 SEQ@0 / WIN@8 / FLAGS@3)。
|
||||||
* @return true=需要全量读(seq 沿或清统计请求);false=本周期跳过全量读。
|
* @return true=需要全量读(seq 沿或清统计请求);false=本周期跳过全量读。
|
||||||
* @note 每周期由 PDI_Isr 在读 ESC 前调用:内部刷新桥的 s_winBase 并做 seq
|
* @note 每周期由 PDI_Isr 在读 ESC 前调用:做 seq 基准 priming 与沿判定;
|
||||||
* 基准 priming;s_lastSeq 的推进仍留在全量路径(processOutput)。
|
* s_lastSeq 的推进仍留在全量路径(processOutput)。1.6.21 起不再
|
||||||
|
* 维护 win_base(段 2 电机整轮密排,无滑窗)。
|
||||||
*/
|
*/
|
||||||
static bool outputProbe(const uint8_t *head);
|
static bool outputProbe(const uint8_t *head);
|
||||||
|
|
||||||
@@ -66,10 +67,36 @@ public:
|
|||||||
/**
|
/**
|
||||||
* @brief 交回输入过程数据(从站→主站)。
|
* @brief 交回输入过程数据(从站→主站)。
|
||||||
* @param[out] esc 指向 ESC 输入缓冲的 kPdoInBytes 字节。
|
* @param[out] esc 指向 ESC 输入缓冲的 kPdoInBytes 字节。
|
||||||
* @note 由 APPL_InputMapping() 每周期调用。
|
* @note 由 APPL_InputMapping() 每周期调用。只填 RAM/对象存储;
|
||||||
|
* 真正写 ESC 的字节量由 PDO_InputMapping 经 sparseEscWritePlan 决定。
|
||||||
*/
|
*/
|
||||||
static void writeInputs(uint8_t *esc);
|
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 输入 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 累计取走输出的周期数(主站在发过程数据的证据)。 */
|
/** @brief 累计取走输出的周期数(主站在发过程数据的证据)。 */
|
||||||
static uint32_t outputCycles() { return s_outputCycles; }
|
static uint32_t outputCycles() { return s_outputCycles; }
|
||||||
|
|
||||||
@@ -109,6 +136,16 @@ private:
|
|||||||
* ECAT_Application() 会在 PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 四个
|
* ECAT_Application() 会在 PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 四个
|
||||||
* 上下文被调用并互相抢占,直接写 s_in 会让读侧取到跨轮次的半成品。 */
|
* 上下文被调用并互相抢占,直接写 s_in 会让读侧取到跨轮次的半成品。 */
|
||||||
alignas(4) static uint8_t s_build[kPdoInBytes];
|
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())。 */
|
/** @brief process() 被重入拦下的次数(诊断用,见 reentryCount())。 */
|
||||||
static volatile uint32_t s_reentryCount;
|
static volatile uint32_t s_reentryCount;
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,10 @@
|
|||||||
* 算出的丢帧数与从站侧 OVF 是否一致)。
|
* 算出的丢帧数与从站侧 OVF 是否一致)。
|
||||||
* 600 = 75×8;SM3 实占 3×600=1800B(0x1880-0x1F67),out+in=984 ≤ 1024 硬上限,
|
* 600 = 75×8;SM3 实占 3×600=1800B(0x1880-0x1F67),out+in=984 ≤ 1024 硬上限,
|
||||||
* DPRAM 仍留 120B 余量。改这里必须同步重生成 SII(gen_eeprom.py)并断电冷启动。
|
* DPRAM 仍留 120B 余量。改这里必须同步重生成 SII(gen_eeprom.py)并断电冷启动。
|
||||||
|
*
|
||||||
|
* ── 1.6.21:总长仍 600B,内部布局改「电机整轮密排」──
|
||||||
|
* 段 2 扩到 32..543(头 8B + 最多 9×56B),段 3 前移到 544..583,段 5 仍 584。
|
||||||
|
* 删段 4 历史覆盖窗。长度不变 ⇒ SII/EEPROM 无需重刷;主站解码偏移必须同步。
|
||||||
*/
|
*/
|
||||||
#define ECAT_PDO_IN_BYTES 600
|
#define ECAT_PDO_IN_BYTES 600
|
||||||
|
|
||||||
|
|||||||
@@ -116,9 +116,9 @@ typedef struct OBJ_STRUCT_PACKED_START {
|
|||||||
UINT32 u32DsRunsTotal; /* sub16: 窗口内 run(连续变化段)总数,配 sub14 求均值 */
|
UINT32 u32DsRunsTotal; /* sub16: 窗口内 run(连续变化段)总数,配 sub14 求均值 */
|
||||||
UINT32 u32DsRunsMax; /* sub17: 单拍 run 数峰值 —— 块粒度差分的碎片上限由它定 */
|
UINT32 u32DsRunsMax; /* sub17: 单拍 run 数峰值 —— 块粒度差分的碎片上限由它定 */
|
||||||
UINT32 u32DsSeg1; /* sub18: 段1(0..31) 桥状态+短快照 变化字节合计 */
|
UINT32 u32DsSeg1; /* sub18: 段1(0..31) 桥状态+短快照 变化字节合计 */
|
||||||
UINT32 u32DsSeg2; /* sub19: 段2(32..319) 电机状态窗口 */
|
UINT32 u32DsSeg2; /* sub19: 段2(32..543) 电机整轮密排 */
|
||||||
UINT32 u32DsSeg3; /* sub20: 段3(320..359) 长帧快照 */
|
UINT32 u32DsSeg3; /* sub20: 段3(544..583) 长帧快照 */
|
||||||
UINT32 u32DsSeg4; /* sub21: 段4(360..583) 批量覆盖窗 */
|
UINT32 u32DsSeg4; /* sub21: 已删(1.6.21 恒 0) */
|
||||||
UINT32 u32DsSeg5; /* sub22: 段5(584..599) 诊断区+保留 */
|
UINT32 u32DsSeg5; /* sub22: 段5(584..599) 诊断区+保留 */
|
||||||
UINT32 u32DsCompareCyc; /* sub23: 比对自身累计 DWT 周期(u32 粒度,不含快照拷贝) */
|
UINT32 u32DsCompareCyc; /* sub23: 比对自身累计 DWT 周期(u32 粒度,不含快照拷贝) */
|
||||||
} OBJ_STRUCT_PACKED_END
|
} OBJ_STRUCT_PACKED_END
|
||||||
|
|||||||
@@ -165,6 +165,10 @@ extern UINT32 EcatDbg_Dwt(void);
|
|||||||
#define ecatdbg_dwt() EcatDbg_Dwt()
|
#define ecatdbg_dwt() EcatDbg_Dwt()
|
||||||
/* 1.6.0:输出探针(实现见 comm/ethercat_pdo.cpp,C 链接) */
|
/* 1.6.0:输出探针(实现见 comm/ethercat_pdo.cpp,C 链接) */
|
||||||
extern UINT8 EcatPdo_OutputProbe(UINT8 const *head);
|
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_pdiCyc = 0; /* sub5: PDI_Isr 全函数累计周期 */
|
||||||
volatile UINT32 g_dbg_outCyc = 0; /* sub6: PDO_OutputMapping(输出探针/全量读) */
|
volatile UINT32 g_dbg_outCyc = 0; /* sub6: PDO_OutputMapping(输出探针/全量读) */
|
||||||
volatile UINT32 g_dbg_freerunCyc = 0; /* sub7: MainLoop free-run 分支(SM 同步下恒 0) */
|
volatile UINT32 g_dbg_freerunCyc = 0; /* sub7: MainLoop free-run 分支(SM 同步下恒 0) */
|
||||||
@@ -197,10 +201,24 @@ BOOL bInitFinished = FALSE; /** < \brief indicates if the initialization is fini
|
|||||||
void PDO_InputMapping(void)
|
void PDO_InputMapping(void)
|
||||||
{
|
{
|
||||||
/*ECAT_DBG(1.6.0 剖分:输入映射全程 + 640B 写细分)*/
|
/*ECAT_DBG(1.6.0 剖分:输入映射全程 + 640B 写细分)*/
|
||||||
|
/* 1.6.22 稀疏写:APPL 仍整填 aPdInputData(对齐 SDO 0x6000),但 HW_EscWriteIsr
|
||||||
|
* 只写密排有效段+段3/5,跳过空槽区 —— 省的是 PRAM 字节时间,不是 RAM 拷贝。
|
||||||
|
* 1.6.25 差分写:在稀疏之上再跳过「与影子一致且 3-buffer 冲刷完成」的段。 */
|
||||||
const UINT32 dbgT0 = ecatdbg_dwt();
|
const UINT32 dbgT0 = ecatdbg_dwt();
|
||||||
APPL_InputMapping((UINT16*)aPdInputData);
|
APPL_InputMapping((UINT16*)aPdInputData);
|
||||||
const UINT32 dbgT1 = ecatdbg_dwt();
|
const UINT32 dbgT1 = ecatdbg_dwt();
|
||||||
|
{
|
||||||
|
UINT16 offs[4], lens[4];
|
||||||
|
const UINT8 nseg = EcatPdo_EscWritePlan(offs, lens, 4U);
|
||||||
|
if (nseg == 0U) {
|
||||||
HW_EscWriteIsr(((MEM_ADDR *) aPdInputData), nEscAddrInputData, nPdInputSize );
|
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;
|
g_dbg_inWriteCyc += ecatdbg_dwt() - dbgT1;
|
||||||
g_dbg_inCyc += ecatdbg_dwt() - dbgT0;
|
g_dbg_inCyc += ecatdbg_dwt() - dbgT0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* @file app_spi_cmd_exmc_diag.cpp
|
* @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 → 分流:
|
* 网页按钮 → ESP32 /spi/send → SPI 0xA2 → 本 handler → 分流:
|
||||||
* action 0~10 → EscExmcBus::continuityDrive() (EXMC 数据/控制线 pattern)
|
* action 0~10 → EscExmcBus::continuityDrive() (EXMC 数据/控制线 pattern)
|
||||||
* action 20~24 → EscLan9252::nrstDrive() (ECAT_NRST/PB10 拉低/拉高/浮空/回读)
|
* action 20~24 → EscLan9252::nrstDrive() (ECAT_NRST/PB10 拉低/拉高/浮空/回读)
|
||||||
|
* action 30~34 → EscLan9252::pmDrive() (电源管理:读状态/进D1/D2/D3/手动唤醒)
|
||||||
* 立即返回 ACK,不阻塞 SPI 总线。电平保持到下次调用或 action=0(stop)。
|
* 立即返回 ACK,不阻塞 SPI 总线。电平保持到下次调用或 action=0(stop)。
|
||||||
* NRST action 返回的 status 字节 = 实际引脚电平(0/1),非 OK 码。
|
* 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/spi_command_handler.h"
|
||||||
#include "device/spi/app_spi_command.h"
|
#include "device/spi/app_spi_command.h"
|
||||||
@@ -29,7 +32,9 @@ public:
|
|||||||
uint8_t status = 0xEE;
|
uint8_t status = 0xEE;
|
||||||
if (ctx.payload_len >= 1) {
|
if (ctx.payload_len >= 1) {
|
||||||
const uint8_t action = ctx.payload[0];
|
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);
|
status = EscLan9252::nrstDrive(action - 20U);
|
||||||
} else {
|
} else {
|
||||||
status = EscExmcBus::continuityDrive(action);
|
status = EscExmcBus::continuityDrive(action);
|
||||||
|
|||||||
@@ -93,6 +93,25 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
|
|||||||
* 实测见 sub23),因此 **sub9/sub10 的写侧耗时会包含这部分**,不能直接与历史
|
* 实测见 sub23),因此 **sub9/sub10 的写侧耗时会包含这部分**,不能直接与历史
|
||||||
* 296µs/265µs 对拍——要看差分潜力就只看 sub13..22,要看真实写侧耗时请置 0 重测。 */
|
* 296µs/265µs 对拍——要看差分潜力就只看 sub13..22,要看真实写侧耗时请置 0 重测。 */
|
||||||
#define CONFIG_ECAT_DIFF_STATS 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 输入 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 进度打印。 */
|
/** @brief 启动横幅与 main 进度打印。 */
|
||||||
#define CONFIG_APP_BANNER_LOG_ENABLE 1
|
#define CONFIG_APP_BANNER_LOG_ENABLE 1
|
||||||
|
|
||||||
@@ -128,7 +147,7 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
|
|||||||
* ============================================================ */
|
* ============================================================ */
|
||||||
constexpr uint32_t APP_FW_VERSION_MAJOR = 1U;
|
constexpr uint32_t APP_FW_VERSION_MAJOR = 1U;
|
||||||
constexpr uint32_t APP_FW_VERSION_MINOR = 6U;
|
constexpr uint32_t APP_FW_VERSION_MINOR = 6U;
|
||||||
constexpr uint32_t APP_FW_VERSION_PATCH = 20U; /* 1.6.20:**基线对照版**,仅把 CONFIG_ECAT_DIFF_STATS 置 0,用来量仪表自身成本与生产口径的真实负载水位。动机:1.6.19 读到 load 51.5%,但该读数含 observe() 每拍的 600B 比对 + 快照拷贝,拿它当"差分能省 19 个点"的分母是被污染的 —— 同工况同判据各读一次,两数相减才是仪表成本。PDO 布局/映射逐字节未动,无需重刷 SII。 */
|
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.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.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 重测。 */
|
/* 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 重测。 */
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
---
|
||||||
|
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 使用效率审计 + 后续 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 重测才可对拍)。
|
||||||
|
|
||||||
|
**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。
|
||||||
|
4. 1.6.25 已实现 P1 差分写(三区 + 3-buffer 冲刷)与 P2 csrRead verify 分级;上机对拍 sub10 待做。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [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 修复记录)。
|
||||||
|
- 不重新设计 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)
|
||||||
|
- [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)
|
||||||
Reference in New Issue
Block a user