Files
DCU_APP/GD32E508_DCU_APP/comm/esc_lan9252.cpp
T
cbb 6136c2363f feat(ethercat): 添加LAN9252睡眠挂起机制防止HBI访问冲突
添加了s_pmSuspended状态标志来管理LAN9252的电源管理模式,在芯片进入D1-D3
睡眠状态时阻止HBI访问以避免唤醒问题。同步修改了ethercat_sync中断处理程序,
在睡眠挂起期间跳过HBI相关操作确保芯片能够正常维持睡眠状态。

fix(spi): 提升SPI CS EXTI抢占优先级避免帧丢失

将SPI CS EXTI中断优先级从1提升至0,使其能够抢占PDI/SYNC1等关键中断,确
保SPI帧释放处理能够在微秒级完成,避免因延时导致的ESP32 CS间隔超时和帧
丢失问题。

feat(ssc): 实现OP输入新鲜度不变量监控机制

添加了输入映射序列号监控机制,当PDI路径中断时能够检测到输入数据冻结并
自动触发强制刷新,解决结构洞问题导致的输入数据停止更新现象。

chore(pdo): 更新PDO输入字节注释说明布局变更历史
2026-09-13 18:59:57 +08:00

674 lines
34 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file esc_lan9252.cpp
* @brief LAN9252 EtherCAT ESC 驱动实现。
*
* 硬件使用 16-bit HBI Indexed Address Mode:
* - A2=0,A1=0 (偏移 0x00000) -> HBI_IDX_0
* - A2=1,A1=0 (偏移 0x40000) -> HBI_DATA_0 低 16 位
* - A2=1,A1=1 (偏移 0x60000) -> HBI_DATA_0 高 16 位
*
* 地址线换算(1.5.27 修正,此前 0x20000/0x30000 是按 8-bit 规则算的,DATA 访问全部
* 落在 HBI_IDX_0 高半字,IDX_0=0x1234 探针假通过而 BYTE_TEST 永远失败):
* 板线:LAN9252 A1(Pin25)→PD11=EXMC_A16,A2(Pin29)→PD12=EXMC_A17。
* GD32E50x 用户手册 p869:16-bit 数据宽度时 EXMC 内部将 HADDR[25:1] 接到
* EXMC_A[24:0],即 EXMC_A16=CPU 偏移 bit17(0x20000)、EXMC_A17=bit18(0x40000)。
* LAN9252 手册 Table 9-3:A2=1 选 HBI_DATA_0,A1 选低/高半字。
*
* 访问模型(1.5.39 按 DS 12.11/表 12-13 修正):
* HBI 索引直接寻址的只有 LAN9252 系统 CSR(BYTE_TEST 0x064 等)与访问端口;
* EtherCAT 核心 CSR(0x0000-0x0FFF)经 ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300)
* 间接访问,过程 RAM(0x1000-0x1FFF)经 PRAM FIFO(0x000-0x03C + 0x308-0x314) 访问。
* 1.5.37 及之前把索引空间当统一寻址,导致全部核心 CSR 读 0/写丢弃——
* 主站恒读 AL=INIT 的真正根因(网络侧寄存器诊断证实 EEPROM/核心一直正常)。
*/
#include "comm/esc_lan9252.h"
#include "comm/esc_exmc_bus.h"
#include "sys/config.h"
#include "sys/systick.h"
#include <stdio.h>
#include "RTE_Components.h"
#include CMSIS_device_header
#include "gd32e50x_gpio.h"
#include "gd32e50x_rcu.h"
namespace {
/* HBI Indexed Mode 下 EXMC 偏移(A2=LAN9252 Pin29←PD12=EXMC_A17, A1=Pin25←PD11=EXMC_A16)
* EXMC 16-bit 模式地址左移 1 位:A16 引脚=CPU 偏移 0x20000,A17 引脚=CPU 偏移 0x40000。 */
constexpr uint32_t HBI_IDX0_OFFSET = 0x00000U; /* A2=0, A1=0 */
constexpr uint32_t HBI_DATA0_LOW_OFF = 0x40000U; /* A2=1(EXMC_A17), A1=0(EXMC_A16) */
constexpr uint32_t HBI_DATA0_HIGH_OFF = 0x60000U; /* A2=1(EXMC_A17), A1=1(EXMC_A16) */
/* ESC 关键寄存器字节地址。
* 注: LAN9252 的 HBI 在 EEPROM 空白/无效时依然工作(数据手册 9.5.4.2:
* 先轮询 HBI_IDX_0 默认值 0x1234,再轮询 BYTE_TEST(0x064)=0x87654321)。
* 芯片身份探针用 BYTE_TEST 寄存器(0x064),不是 ESC 0x0000(那是类型寄存器=0xC0)。 */
constexpr uint16_t LAN9252_REG_BYTE_TEST = 0x0064U; /**< 字节顺序测试寄存器(默认 0x87654321,LAN9252 手册 16.1.2)。 */
constexpr uint32_t LAN9252_BYTE_TEST_VALUE = 0x87654321U; /**< BYTE_TEST 默认值(小端序)。 */
constexpr uint16_t ESC_REG_DL_STATUS = 0x0110U; /**< DL Status:bit0 = PDI operational / EEPROM loaded。 */
constexpr uint16_t ESC_REG_EEP_CSR = 0x0502U; /**< EEPROM Control/Status:bit11=校验错,bit12=加载错(0=OK)。 */
/* LAN9252 系统中断寄存器(HBI 索引直接可达,DS 8.3 表 0.1)。
* IRQ 引脚上电默认全关:IRQ_CFG.IRQ_EN=0 且 INT_EN.ECAT_INT_EN=0,
* 不配这两个寄存器则 PB1 永远收不到中断,SSC 的 PDI_Isr 从不执行。 */
constexpr uint16_t LAN9252_REG_IRQ_CFG = 0x0054U; /**< 中断配置:bit8=IRQ_EN,bit4=IRQ_POL,bit0=IRQ_TYPE。 */
constexpr uint16_t LAN9252_REG_INT_STS = 0x0058U; /**< 中断状态(R/WC):bit0=ECAT_INT。 */
constexpr uint16_t LAN9252_REG_INT_EN = 0x005CU; /**< 中断使能:bit0=ECAT_INT_EN。 */
constexpr uint32_t IRQ_CFG_IRQ_EN = 1UL << 8; /**< IRQ 引脚输出总闸。 */
constexpr uint32_t IRQ_CFG_IRQ_POL = 1UL << 4; /**< 1=高有效,0=低有效(开漏时忽略本位)。 */
constexpr uint32_t IRQ_CFG_IRQ_TYPE = 1UL << 0; /**< 1=推挽,0=开漏(开漏强制低有效)。 */
constexpr uint32_t INT_EN_ECAT_INT = 1UL << 0; /**< 允许 EtherCAT 事件驱动 IRQ 引脚。 */
/* ESC 过程数据看门狗(DS 12.14.41-43)。SII 不携带看门狗字段,芯片复位默认
* divider=0x09C2(100µs)/WD Time PD=0x03E8(1000) 即 100ms——低于实测可用的
* 100ms 手动调试周期,SM2 每 100ms 被判定超时,SSC 以 0x001B 拖回 SAFEOP。 */
constexpr uint16_t ESC_REG_WD_DIVIDER = 0x0400U; /**< 看门狗分频(100µs 基准,复位默认 0x09C2)。 */
constexpr uint16_t ESC_REG_WD_TIME_PD = 0x0420U; /**< 过程数据看门狗步数(复位默认 0x03E8=100ms)。 */
constexpr uint16_t ESC_WD_TIME_PD_STEPS = 10000U; /**< 10000 × 100µs = 1s。 */
/* LAN9252 直接映射的 ESC 访问端口(DS 表 12-13;经 HBI 索引直接读写) */
constexpr uint16_t ECAT_PRAM_RD_DATA = 0x0000U; /**< PRAM 读 FIFO(0x00-0x1C 别名,功能相同,突发用) */
constexpr uint16_t ECAT_PRAM_WR_DATA = 0x0020U; /**< PRAM 写 FIFO(0x20-0x3C 别名) */
constexpr uint16_t ECAT_CSR_DATA_REG = 0x0300U; /**< ESC 核心 CSR 间接访问数据寄存器 */
constexpr uint16_t ECAT_CSR_CMD_REG = 0x0304U; /**< ESC 核心 CSR 间接访问命令寄存器 */
constexpr uint16_t ECAT_PRAM_RD_ADDR_LEN = 0x0308U; /**< PRAM 读地址/长度 */
constexpr uint16_t ECAT_PRAM_RD_CMD = 0x030CU; /**< PRAM 读命令/状态 */
constexpr uint16_t ECAT_PRAM_WR_ADDR_LEN = 0x0310U; /**< PRAM 写地址/长度 */
constexpr uint16_t ECAT_PRAM_WR_CMD = 0x0314U; /**< PRAM 写命令/状态 */
constexpr uint32_t CSR_BUSY = 1UL << 31; /**< CSR_CMD:写 1 启动,完成自清 */
constexpr uint32_t CSR_RNW = 1UL << 30; /**< CSR_CMD:1=读 0=写 */
constexpr uint32_t PRAM_BUSY = 1UL << 31; /**< PRAM_*_CMD:写 1 启动,完成自清 */
constexpr uint32_t PRAM_AVAIL = 1UL << 0; /**< PRAM_*_CMD:数据可用(读)/空间可用(写) */
constexpr uint32_t POLL_MAX = 2000UL; /**< busy/avail 轮询上限(~ms 级,防芯片异常时死等) */
/* 电源管理(DS 6.3/表 5-1):芯片级睡眠 D1-D3。
* READY=0 期间仅 HW_CFG/PMT_CTRL/BYTE_TEST/RESET_CTL 四个寄存器可访问(DS 6.2.3),
* 恰好覆盖睡眠进入与唤醒流程所需;PMT_CTRL 可在睡眠态安全读(DS 6.3.5.1 注)。 */
constexpr uint16_t LAN9252_REG_HW_CFG = 0x0074U; /**< 硬件配置:bit27=READY(与 PMT_CTRL bit0 同源,DS 6.3.5.1)。 */
constexpr uint16_t LAN9252_REG_PMT_CTRL = 0x0084U; /**< 功耗管理控制:bit0=READY,bit28=PM_SLEEP_EN,bit31:29=PM_MODE。 */
constexpr uint32_t PMT_CTRL_READY = 1UL << 0;
constexpr uint32_t PMT_CTRL_PM_MODE_MSK = 7UL << 29; /**< 001=D1 010=D2 011=D3。 */
constexpr uint32_t PMT_CTRL_PM_SLEEP_EN = 1UL << 28; /**< 置 1 进入 PM_MODE 指定的睡眠档(须与 PM_MODE 分两次写)。 */
/* ESC 核心 MII 管理(ETG.1000.4 ESC 寄存器 0x0510-0x0514,经 csrRead/csrWrite 间接访问;
* 表 5-1 证实 PHY 寄存器不在 HBI 直接空间)。用于 D3 前置的 PHY General Power Down
* (DS 6.3.4:D3 要求两 PHY 先 General Power Down)与唤醒后恢复。 */
constexpr uint16_t ESC_REG_MII_CTRL = 0x0510U; /**< bit0=CMD(写 1 启动自清) bit1=WRITE bit8=BUSY。 */
constexpr uint16_t ESC_REG_MII_ADDR = 0x0512U; /**< bit4:0=PHY_ADDR bit15:5=REG_ADDR。 */
constexpr uint16_t ESC_REG_MII_DATA = 0x0514U; /**< bit15:0=MII 数据。 */
constexpr uint16_t MII_CTRL_CMD = 1U << 0;
constexpr uint16_t MII_CTRL_WRITE = 1U << 1;
constexpr uint16_t MII_CTRL_BUSY = 1U << 8;
constexpr uint8_t MII_REG_BMCR = 0U; /**< IEEE 802.3 Basic Control 寄存器 0。 */
constexpr uint16_t MII_BMCR_POWER_DOWN = 1U << 11; /**< PHY_PWR_DWN(DS 11.2.8)。 */
constexpr uint8_t MII_PHY_ADDR_B = 1U; /**< PHY B 地址恒 1(DS 11.1.1);A 模式相关 0/2,运行期判定。 */
__attribute__((section(".ramfunc"), noinline))
void setHbiIndex(uint16_t addr)
{
EscExmcBus::write16(HBI_IDX0_OFFSET, addr);
}
__attribute__((section(".ramfunc"), noinline))
uint16_t readDataLow()
{
return EscExmcBus::read16(HBI_DATA0_LOW_OFF);
}
__attribute__((section(".ramfunc"), noinline))
uint16_t readDataHigh()
{
return EscExmcBus::read16(HBI_DATA0_HIGH_OFF);
}
__attribute__((section(".ramfunc"), noinline))
void writeDataLow(uint16_t v)
{
EscExmcBus::write16(HBI_DATA0_LOW_OFF, v);
}
__attribute__((section(".ramfunc"), noinline))
void writeDataHigh(uint16_t v)
{
EscExmcBus::write16(HBI_DATA0_HIGH_OFF, v);
}
__attribute__((section(".ramfunc"), noinline))
uint32_t readReg32(uint16_t addr)
{
setHbiIndex(addr);
uint16_t lo = readDataLow();
uint16_t hi = readDataHigh();
return ((uint32_t)hi << 16) | lo;
}
__attribute__((section(".ramfunc"), noinline))
void writeReg32(uint16_t addr, uint32_t v)
{
setHbiIndex(addr);
writeDataLow((uint16_t)(v & 0xFFFFU));
writeDataHigh((uint16_t)(v >> 16));
}
/* ---- 电源管理 helper(仅 pmDrive 诊断路径使用) ---- */
/** @brief DWT CYCCNT 忙等微秒。
* @note EXTI ISR 上下文(0xA2 在 AppSpiCommand::onCsRelease 内同步执行)禁止用
* SysTickTimer::delayMs:其基于 SysTick 中断递增 s_ms,中断被 CS EXTI
* 压住时计数冻结会死等。DWT CYCCNT 已由 init() 使能,纯寄存器读,ISR 安全。 */
__attribute__((section(".ramfunc"), noinline))
void pmBusyWaitUs(uint32_t us)
{
const uint32_t start = DWT->CYCCNT;
const uint32_t cycles = us * (SystemCoreClock / 1000000U);
while ((DWT->CYCCNT - start) < cycles) { }
}
/** @brief ESC 核心 MII 管理事务(读/写一个 PHY 寄存器)。
* @note 调用方必须已持 HBI 互斥(pmDrive 的 maskEnter 段内)。MII 事务典型 ~25µs
* (2.5MHz MDIO × 32bit 帧结构),BUSY 轮询上限 POLL_MAX 防芯片异常死等。 */
bool miiXfer(uint8_t phyAddr, uint8_t regAddr, bool write, uint16_t &data)
{
EscLan9252::csrWrite(ESC_REG_MII_ADDR, ((uint32_t)regAddr << 5) | phyAddr, 2U);
if (write) {
EscLan9252::csrWrite(ESC_REG_MII_DATA, data, 2U);
}
EscLan9252::csrWrite(ESC_REG_MII_CTRL,
write ? (MII_CTRL_CMD | MII_CTRL_WRITE) : MII_CTRL_CMD, 2U);
for (uint32_t i = 0U; i < POLL_MAX; i++) {
if ((EscLan9252::csrRead(ESC_REG_MII_CTRL, 2U, 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 */
bool EscLan9252::s_busReady = false;
bool EscLan9252::s_coreReady = false;
bool EscLan9252::s_resetDone = false;
bool EscLan9252::s_pmPhyDown = false;
bool EscLan9252::s_pmSuspended = false;
void EscLan9252::hardwareReset()
{
/* 受控复位 ECAT_NRST(PB10 → LAN9252 Pin11 RST#,SCH 第1页 Pin47)。
* 2026-08-22(1.5.25) 前固件从未初始化 PB10,RST# 仅靠 R44 10k 上拉自然释放,
* 不满足手册 18.6.3:tpurstd(电源稳定→RST#释放)≥25ms、trstia(低脉宽)≥200µs,
* 且要求释放前时钟输入已稳定。固件执行到此点时上电早已完成、晶振已稳定,
* 统一 30ms 低脉宽同时覆盖两个下限。RST# 释放瞬间重锁 CHIP_MODE0/1 strap
* (LED_LINK1/2 经 R35/R36 10k 下拉 → 00b = 2 端口 HBI 模式)。
* 释放后保持推挽输出高:空闲电平确定,并保留运行期软件复位手段;
* GD32 自身复位期间引脚回浮空,由 R44 上拉维持高,不会误复位 ESC。 */
rcu_periph_clock_enable(RCU_GPIOB);
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
CONFIG_ESC_NRST_PIN);
gpio_bit_reset(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN); /* RST# 拉低 */
SysTickTimer::delayMs(CONFIG_ESC_NRST_ASSERT_MS);
gpio_bit_set(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN); /* 释放复位 */
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
/* 回读实际电平:GD32 输入通路常连引脚,输出态也能读到真实电压。
* 驱动高却读回低 => ECAT_NRST 网络被外部短路到地(或引脚损伤),
* LAN9252 被按死在复位态 —— 直接解释 HBI 全浮空 + 晶振照振。 */
const bool pinHigh = (gpio_input_bit_get(CONFIG_ESC_NRST_PORT,
CONFIG_ESC_NRST_PIN) == SET);
printf("[ECAT][ESC] hardwareReset: RST# low %ums -> released, pin reads %s\r\n",
(unsigned)CONFIG_ESC_NRST_ASSERT_MS,
pinHigh ? "HIGH" : "*** LOW (net shorted to GND / pin damaged) ***");
/* RST# 复位清掉 PM 睡眠态(芯片回 D0),挂起门控随之解除(1.6.27) */
s_pmSuspended = false;
}
uint8_t EscLan9252::nrstDrive(uint8_t action)
{
/* SPI 0xA2 action 20+ 的 NRST 手动诊断入口。
* 返回值 = 实际引脚电平(0/1),非 OK 码;0xEE = 无效 action。 */
switch (action) {
case 0: /* 恢复自动态:推挽输出高(与 hardwareReset 释放后一致) */
case 2: /* 持续拉高 */
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
CONFIG_ESC_NRST_PIN);
gpio_bit_set(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN);
/* 复位释放后芯片重走上电流程回 D0,睡眠挂起随之解除(1.6.27) */
s_pmSuspended = false;
break;
case 1: /* 持续拉低(手动保持复位) */
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
CONFIG_ESC_NRST_PIN);
gpio_bit_reset(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN);
break;
case 3: /* 释放:浮空输入,网络交给 R44 10k 上拉 */
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_IN_FLOATING, 0,
CONFIG_ESC_NRST_PIN);
s_pmSuspended = false;
break;
case 4: /* 只读回,不改驱动 */
break;
default:
return 0xEE;
}
const bool level = (gpio_input_bit_get(CONFIG_ESC_NRST_PORT,
CONFIG_ESC_NRST_PIN) == SET);
static const char *const kName[5] = {
"auto(high)", "drive LOW", "drive HIGH", "float", "read-only"};
printf("[ECAT][NRST] %s -> pin reads %s\r\n", kName[action],
level ? "HIGH" : "LOW");
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);
/* 读到 D0(SLEEP_EN=0):芯片不在睡眠,若门控仍挂起(如 op4 唤醒超时后
* 芯片延迟自醒)则自愈解除,恢复主循环/ISR 的 HBI 访问(1.6.27)。 */
if ((status & 0x02U) == 0U && s_pmSuspended) {
s_pmSuspended = false;
printf("[ECAT][PM] status: chip in D0, pm suspend gate released\r\n");
}
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.27:立即挂起全部 HBI 消费者(task_runner 停 MainLoop/守护/心跳,
* ethercat_sync 三 ISR 清 pending 后直接返回)——本函数运行于 CS EXTI ISR,
* 退出后主循环第一圈 ECAT_Main 读 0x220 就会把刚睡着的芯片叫醒;
* 此前置位窗口内本 ISR 是唯一运行者(maskEnter 已屏 EXTI0/1),无竞态。 */
s_pmSuspended = true;
/* 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)");
}
if (awake) {
/* 唤醒成功:解除 HBI 挂起,主循环/ISR 恢复运行(1.6.27)。
* 超时则保持挂起——芯片大概率仍睡着;若实际已醒,下一次 op0
* 状态查询读到 D0 会自愈解除。 */
s_pmSuspended = false;
}
status = awake ? pmPackStatus(readReg32(LAN9252_REG_PMT_CTRL)) : 0U;
printf("[ECAT][PM] wake: %s -> status=0x%02X (READY=%u SLEEP_EN=%u)\r\n",
awake ? "OK" : "TIMEOUT (10ms, poll action 30 / retry 34)",
status, (unsigned)(status & 1U), (unsigned)((status >> 1) & 1U));
}
EscExmcBus::maskExit(mask);
return status;
}
bool EscLan9252::init()
{
s_busReady = false; /* 1.5.27:s_busReady 语义 = init 完整成功(EXMC+探针+BYTE_TEST) */
s_coreReady = false; /* 1.5.39:s_coreReady 语义 = EEPROM 加载完成、EtherCAT 核心使能 */
/* 使能 DWT 周期计数器,供 hw_gd32exmc/SSC 执行时间测量 */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0U;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
/* 受控 RST# 复位(仅首次;重复调用 init 不得二次复位,否则清掉 SSC 配置) */
if (!s_resetDone) {
hardwareReset();
s_resetDone = true;
}
if (!EscExmcBus::init()) {
printf("[ECAT][ESC] init: FAIL, EXMC bus not ready (see [ECAT][EXMC] log)\r\n");
return false;
}
/* HBI 存活探针(数据手册 9.5.4.2):先直接读 HBI_IDX_0(offset 0,不写任何东西),
* 其复位默认值 = 0x1234。RST# 释放后 ESC 需完成内部启动与 EEPROM 加载,
* 故按手册建议轮询(此前为单次读,复位后立即读可能仍在启动中)。
* 若超时仍 != 0x1234(通常是 0x0000/0xFFFF 或上次写入的回显),
* 说明 LAN9252 完全没有驱动总线——CS 未拉低、CHIP_MODE strap 非 00(三端口=SPI only)
* 或芯片未出复位/未上电,而不是 EEPROM 问题(HBI 在 EEPROM 空白时也工作)。 */
uint16_t idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
for (uint32_t waited = 0U;
idx0 != 0x1234U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS;
waited += CONFIG_ESC_HBI_POLL_STEP_MS) {
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
}
if (idx0 != 0x1234U) {
printf("[ECAT][ESC] init: FAIL, HBI_IDX_0=0x%04X (expect 0x1234) -> LAN not driving bus\r\n",
(unsigned)idx0);
printf("[ECAT][ESC] hint: check CS(PD7) pulse, CHIP_MODE0/1 straps=00, RST#/power\r\n");
return false;
}
/* 再读 BYTE_TEST(0x064)确认接口完全就绪,默认 0x87654321(小端)。
* 该值只有 HBI_DATA_0 偏移正确(EXMC 16-bit 地址左移 1 位)才可能读到。 */
const uint32_t byteTest = readReg32(LAN9252_REG_BYTE_TEST);
if (byteTest != LAN9252_BYTE_TEST_VALUE) {
printf("[ECAT][ESC] init: FAIL, BYTE_TEST=0x%08lX (expect 0x87654321)\r\n",
(unsigned long)byteTest);
printf("[ECAT][ESC] hint: IDX probe passed but DATA access failed -> check HBI_DATA_0 offsets (0x40000/0x60000)\r\n");
return false;
}
printf("[ECAT][ESC] init: OK, HBI alive (IDX=0x1234, BYTE_TEST=0x87654321)\r\n");
s_busReady = true;
/* 等待 PDI 就绪、EtherCAT 核心可交互(1.5.37 新增,1.5.39 改经 CSR 间接端口读——
* 此前直读 HBI 索引,DL_STATUS 属 EtherCAT 核心寄存器,根本不在直接地址空间)。
* 判据:DL_STATUS(0x0110) bit0 = PDI operational / EEPROM loaded。
* 网络侧诊断已证实本板 EEPROM 加载与核心均正常,此等待应几乎立即通过;
* 超时意味着 PDI 真的未就绪(EEPROM 内容/I2C/strap 问题),此时 SSC 的
* AL Event Mask/邮箱 SM 配置会落空,由 initial.cpp 按 isCoreReady() 门控跳过。 */
uint16_t dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U, false);
uint32_t waited = 0U;
while ((dlStatus & 0x0001U) == 0U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS) {
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
waited += CONFIG_ESC_HBI_POLL_STEP_MS;
dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U, false);
}
if ((dlStatus & 0x0001U) != 0U) {
s_coreReady = true;
printf("[ECAT][ESC] PDI operational, EtherCAT core ready (DL_STATUS=0x%04X, waited %ums)\r\n",
(unsigned)dlStatus, (unsigned)waited);
enableEcatIrqOutput();
configPdWatchdog();
} else {
printf("[ECAT][ESC] init: FAIL, PDI not operational after %ums "
"(DL_STATUS=0x%04X EEP_CSR=0x%04X)\r\n",
(unsigned)CONFIG_ESC_HBI_POLL_TIMEOUT_MS, (unsigned)dlStatus,
(unsigned)csrRead(ESC_REG_EEP_CSR, 2U, false));
printf("[ECAT][ESC] hint: 用 tools/ecat/ecat_esc_regs.py 从网络侧读 0x0502 错误位区分 EEPROM/I2C/内容问题\r\n");
}
return true;
}
/**
* @brief 使能 LAN9252 的 IRQ 引脚输出,让 EtherCAT 事件真正到达 GD32 的 PB1。
* @note 1.5.43 新增。IRQ_CFG(0x054)/INT_EN(0x05C) 都是 LAN9252 系统 CSR
* (HBI 索引直接可达,不走 CSR 间接端口),复位默认值均为 0,即
* IRQ_EN=0(引脚永久去断言) 且 ECAT_INT_EN=0(EtherCAT 事件不上报)。
* 此前固件从未配置它们,导致 PDI_Isr 一次都没被调用过:SSC 因此
* 永远等不到 PROCESS_OUTPUT_EVENT、bEcatFirstOutputsReceived 恒 FALSE,
* SAFEOP->OP 被无限挂起(表现为主站 "进 OP 失败 code=0x0000")。
* 极性必须与 ethercat_sync.cpp 的 EXTI 配置一致:那里是上拉输入 +
* 下降沿触发,故此处选推挽(IRQ_TYPE=1) + 低有效(IRQ_POL=0)。
* 注意 AL Event Mask(0x204) 由 SSC 在状态迁移时自行维护,不在此处设置。
*/
void EscLan9252::configPdWatchdog()
{
/* 1.5.45:过程数据看门狗 100ms(复位默认)→1s。
* 主站侧手动单步/100ms 调试周期会触发 SM2 看门狗:SSC 在 CheckIfEcatError()
* 读 0x0440 发现已过期,调 AL_ControlInd(STATE_SAFEOP, 0x001B) 把 OP 拖回 SAFEOP,
* 表现为"100ms 周期全 WKC 错、10/50ms 正常"。分频保持默认 100µs,只延长步数;
* 正常周期(≤50ms)远小于 1s,行为不变。 */
csrWrite(ESC_REG_WD_TIME_PD, ESC_WD_TIME_PD_STEPS, 2U);
const uint32_t rd = csrRead(ESC_REG_WD_TIME_PD, 2U, false);
if ((rd & 0xFFFFU) == ESC_WD_TIME_PD_STEPS) {
printf("[ECAT][ESC] PD watchdog = 1s (0x0420=%u x 100us, default was 100ms)\r\n",
(unsigned)ESC_WD_TIME_PD_STEPS);
} else {
printf("[ECAT][ESC] WARN: PD watchdog writeback 0x%04lX != %u, 100ms 调试周期会被 0x001B 拖回 SAFEOP\r\n",
(unsigned long)(rd & 0xFFFFU), (unsigned)ESC_WD_TIME_PD_STEPS);
}
}
void EscLan9252::enableEcatIrqOutput()
{
/* 先清掉可能悬留的状态位(R/WC:写 1 清零),避免使能瞬间就有一个陈旧电平。 */
writeReg32(LAN9252_REG_INT_STS, INT_EN_ECAT_INT);
/* 允许 EtherCAT 事件汇入 IRQ。 */
writeReg32(LAN9252_REG_INT_EN, INT_EN_ECAT_INT);
/* 打开引脚总闸:推挽 + 低有效(IRQ_POL=0 即不置该位),去断言间隔保持 0=禁用。 */
writeReg32(LAN9252_REG_IRQ_CFG, IRQ_CFG_IRQ_EN | IRQ_CFG_IRQ_TYPE);
const uint32_t cfg = readReg32(LAN9252_REG_IRQ_CFG);
const uint32_t en = readReg32(LAN9252_REG_INT_EN);
if ((cfg & IRQ_CFG_IRQ_EN) != 0U && (en & INT_EN_ECAT_INT) != 0U) {
printf("[ECAT][ESC] IRQ output enabled (IRQ_CFG=0x%08lX INT_EN=0x%08lX, push-pull active-low)\r\n",
(unsigned long)cfg, (unsigned long)en);
} else {
printf("[ECAT][ESC] WARN: IRQ enable readback failed (IRQ_CFG=0x%08lX INT_EN=0x%08lX)"
" -> PDI_Isr 不会触发,SAFEOP->OP 会卡住\r\n",
(unsigned long)cfg, (unsigned long)en);
}
}
uint32_t EscLan9252::csrRead(uint16_t addr, uint8_t size, bool verify)
{
/* DS 12.11.1:写 CSR_CMD(BUSY|R_nW|SIZE|ADDR) 启动,busy 自清后读 CSR_DATA;
* 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。
* 1.5.59 加固(verify=true):CSR_DATA 是锁存寄存器(非 FIFO),命令不变时连读两次必一致;
* 不一致即本拍 EXMC 毛刺——历史上这类错读曾让 GetSyncMan 把 0x1204/0x1208
* 缓存成邮箱几何(→SSC 邮箱楔死,根治见 mailbox.c 构建期常量),或让
* CheckSmSettings 以 0x0016/0x001F 瞬态打断状态转换。对错读整事务重跑一次;
* 两轮仍不一致则交出末拍(与旧行为一致的兜底,邮箱几何另有常量兜底)。
* 1.6.25 verify=false:单读路径,供诊断回读/MII busy 轮询/init 轮询等低风险
* 调用方使用;SM/AL/邮箱等状态机关键寄存器必须保持默认双读。 */
if (!verify) {
uint32_t n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
writeReg32(ECAT_CSR_CMD_REG, CSR_BUSY | CSR_RNW | ((uint32_t)size << 16) | addr);
n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
return readReg32(ECAT_CSR_DATA_REG);
}
for (uint8_t attempt = 0U; attempt < 2U; ++attempt)
{
uint32_t n = POLL_MAX;
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) {}
const uint32_t d1 = readReg32(ECAT_CSR_DATA_REG);
const uint32_t d2 = readReg32(ECAT_CSR_DATA_REG);
if (d1 == d2)
{
return d1;
}
}
return readReg32(ECAT_CSR_DATA_REG);
}
void EscLan9252::csrWrite(uint16_t addr, uint32_t data, uint8_t size)
{
/* DS 12.11.2:先写 CSR_DATA(数据在低位),再写 CSR_CMD(BUSY|SIZE|ADDR,R_nW=0)。 */
uint32_t n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
writeReg32(ECAT_CSR_DATA_REG, data);
writeReg32(ECAT_CSR_CMD_REG, CSR_BUSY | ((uint32_t)size << 16) | addr);
n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
}
void EscLan9252::pramRead(uint16_t addr, uint8_t *buf, uint16_t len)
{
/* DS 12.11.3:写 ADDR_LEN 再置 BUSY 启动;引擎按 DWORD 向下对齐填充读 FIFO,
* 首 DWORD 有效字节从 (addr&3) 起(表 12-7),末 DWORD 尾部无效(表 12-8)。
* 别名口 0x00-0x1C 功能相同,轮换寻址即官方突发用法;每读满一个 DWORD 弹一项。
*
* 1.6.2 回退逐 DWORD 轮询,弹前必查 AVAIL 位。1.6.0 曾信 CMD 的
* AVAIL_CNT(bits[12:8])一次弹一批:实测 12B 短读(1.6.0 输出探针,主站窗口
* 请求 WIN@8 靠它送达)首 DWORD 新鲜、后续 DWORD 取到 FIFO 陈旧项,窗口
* 基址冻结,槽位 7..26 的电机状态因此永不上报(仅 seq 沿触发的 384B 全量读
* 路径能拿到新 WIN)。批量模式 1.6.0 上机对拍本来就是零收益,384/496B 长读
* 未见损坏,但邮箱短读/探针这类小传输只验证过"逐 DWORD 查 AVAIL"的旧模式
* ——在弄清该计数器对短读的精确语义之前,不要恢复批量弹 DWORD。 */
if (len == 0U) {
return;
}
writeReg32(ECAT_PRAM_RD_ADDR_LEN, ((uint32_t)len << 16) | addr);
writeReg32(ECAT_PRAM_RD_CMD, PRAM_BUSY);
uint16_t done = 0U;
uint16_t alias = 0U;
while (done < len) {
uint32_t n = POLL_MAX;
while ((readReg32(ECAT_PRAM_RD_CMD) & PRAM_AVAIL) == 0U && --n != 0U) {}
if (n == 0U) {
break; /* 芯片异常:避免死等,交出残缺数据 */
}
const uint32_t dw = readReg32((uint16_t)(ECAT_PRAM_RD_DATA + alias));
alias = (uint16_t)((alias + 4U) & 0x1FU);
const uint8_t lead = (done == 0U) ? (uint8_t)(addr & 3U) : 0U;
const uint8_t take = (uint8_t)((len - done) < (4U - lead) ? (len - done) : (4U - lead));
for (uint8_t i = 0U; i < take; ++i) {
buf[done + i] = (uint8_t)(dw >> (8U * (lead + i)));
}
done = (uint16_t)(done + take);
}
}
void EscLan9252::pramWrite(uint16_t addr, const uint8_t *buf, uint16_t len)
{
/* DS 12.11.4:写 ADDR_LEN 再置 BUSY 启动;首 DWORD 有效字节从 (addr&3) 起,
* 引擎只取有效字节;别名口 0x20-0x3C 轮换;写满一个 DWORD 推一项。
* 1.6.2 与 pramRead 同步回退逐 DWORD 轮询(推前必查 AVAIL 位):1.6.0 批量
* 模式实测零收益,且与短读损坏同源(见 pramRead 注),邮箱小 PDU 写同类风险。 */
if (len == 0U) {
return;
}
writeReg32(ECAT_PRAM_WR_ADDR_LEN, ((uint32_t)len << 16) | addr);
writeReg32(ECAT_PRAM_WR_CMD, PRAM_BUSY);
uint16_t done = 0U;
uint16_t alias = 0U;
while (done < len) {
uint32_t n = POLL_MAX;
while ((readReg32(ECAT_PRAM_WR_CMD) & PRAM_AVAIL) == 0U && --n != 0U) {}
if (n == 0U) {
break; /* 芯片异常:放弃剩余字节 */
}
const uint8_t lead = (done == 0U) ? (uint8_t)((uint16_t)(addr + done) & 3U) : 0U;
const uint8_t take = (uint8_t)((len - done) < (4U - lead) ? (len - done) : (4U - lead));
uint32_t dw = 0U;
for (uint8_t i = 0U; i < take; ++i) {
dw |= (uint32_t)buf[done + i] << (8U * (lead + i));
}
writeReg32((uint16_t)(ECAT_PRAM_WR_DATA + alias), dw);
alias = (uint16_t)((alias + 4U) & 0x1FU);
done = (uint16_t)(done + take);
}
uint32_t n = POLL_MAX;
while ((readReg32(ECAT_PRAM_WR_CMD) & PRAM_BUSY) != 0U && --n != 0U) {}
}
uint32_t EscLan9252::readChipId()
{
/* INIT-ONLY:init 阶段中断未使能,直接读取 BYTE_TEST(32-bit) */
return readReg32(LAN9252_REG_BYTE_TEST);
}
uint16_t EscLan9252::initOnlyReadReg(uint16_t addr)
{
/* 1.5.39:ESC 核心 CSR 经间接端口读(此前直读 HBI 索引,核心寄存器不在直接空间,恒 0) */
return (uint16_t)csrRead(addr, 2U);
}
void EscLan9252::initOnlyWriteReg(uint16_t addr, uint16_t v)
{
csrWrite(addr, v, 2U);
}