feat(comm): 1.6.51 CAN 空通道自动静默 + PDO 诊断 + OTA 布局共用头

变更点:
1. CAN-FD(device/CANFD.cpp、sys/config.h)
   - 发送统一 FD+BRS(DLC≤8 不再回退经典 CAN);OTA 期仍按通道压制 BRS
   - 空通道自动识别:连续 5 个 1Hz 窗口无有效帧且有错误 → 静默上报;有帧即恢复
   - 三路 CAN 默认全开;RX parse budget 48→64
2. EtherCAT 桥 / PDO
   - 段1 增 txtxid/rx00ff;版本应答 0x00FF 粘滞发布,避免被 CONFIG 冲掉
   - STATUS 禁止写入 xrx 快照;processSeq 新鲜度救援
3. OTA / 配置
   - sys/config.h include 仓库内 shared/ota_layout.h(与 BOOT 共用,不再依赖仓外路径)
   - ota_boot_request 对齐配置页 word 布局
4. docs/audit_ecat_spi_wifi_ota_compatibility.md:ECAT/SPI/WiFi/OTA 兼容性审计
5. .gitignore:忽略 .trae/、docs/traework-import、评审过程稿
This commit is contained in:
cbb
2026-09-20 22:58:09 +08:00
parent 63af85e7be
commit 5fd986299c
32 changed files with 2592 additions and 781 deletions
+15 -15
View File
@@ -114,6 +114,10 @@ bool EscExmcBus::init() {
rcu_periph_clock_enable(RCU_GPIOE);
rcu_periph_clock_enable(RCU_AF);
rcu_periph_clock_enable(RCU_EXMC);
/* 先 deinit 再 init。BOOT 跳转前 exmc_norsram_disable + 关时钟,
* SNCTL/SNTCFG 仍留着 BOOT 的配置;APP 若只 enable+init 可能叠在残留位上。
* deinit 把 Region0 打回复位默认,再按下面参数重铺。 */
exmc_norsram_deinit(CONFIG_ESC_EXMC_BANK_NOR_SRAM_REGION0);
/* 配置 16-bit 数据线 D0-D15 为 AF 推挽 */
gpio_init(CONFIG_EXMC_D0_PORT, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ,
CONFIG_EXMC_D0_PIN);
@@ -196,19 +200,15 @@ bool EscExmcBus::init() {
norsram_init.memory_type = EXMC_MEMORY_TYPE_SRAM;
norsram_init.address_data_mux = DISABLE;
/* 读/写共用时序(非 extended mode)
* 2026-08-20 慢档测试(16/16/256 HCLK)无效,已还原:慢档时序未带来任何
* 读数变化,速度因素彻底排除(与 bit-bang 40-80us 仍失败一致)。
* 注:那轮测试的失败根因是访问模型错误(直读核心 CSR 索引空间,1.5.39 修复),
* 与时序无关;时序余量本身仍需按 DS 表 9-4/9-5 独立核算,见下方 BUSLAT。 */
/* 读/写共用时序(非 extended mode)。时序余量按 DS 表 9-4/9-5 独立核算,见下方 BUSLAT。 */
rw_timing.asyn_access_mode = EXMC_ACCESS_MODE_A;
rw_timing.syn_data_latency = EXMC_DATALAT_2_CLK;
rw_timing.syn_clk_division = EXMC_SYN_CLOCK_RATIO_DISABLE;
/* BUSLAT: 1→4 (1.5.57)。DS 表 9-4/9-5 要求 trdrd/twrwr/twrrd ≥ 13ns,
/* BUSLAT: 4。DS 表 9-4/9-5 要求 trdrd/twrwr/twrrd ≥ 13ns,
* 该间隔由 EXMC 在两次访问之间插入的总线空闲期承担 = (1 固有 + BUSLAT) HCLK。
* BUSLAT=1 → 2 HCLK ≈ 11.1ns,低于 13ns 下限,仅靠 AHB 取指间隙偶然兜底:
* 常温刚好可用,温度升高(传播延迟增大)/电压下降(驱动减弱)即越界,表现为
* 偶发读到 0xFFFF 或错值——与实测的温漂偶发读错吻合。
* 偶发读到 0xFFFF 或错值。
* BUSLAT=4 → 5 HCLK ≈ 27.8ns,余量 2.1 倍,覆盖全温区与电压波动。
* 代价:每次 HBI 访问多 3 HCLK(16.7ns)。满载 1024B 过程数据按每 DWORD
* 两次 16-bit 访问计,增加约 256×2×16.7ns ≈ 8.6us,相对原 218us 增幅 3.9%,
@@ -243,7 +243,7 @@ bool EscExmcBus::init() {
ECAT_LOGI("[ECAT][EXMC] init: SNCTL0 fix: before=0x%08lX after=0x%08lX (bit7 reserved cleared)\r\n",
(unsigned long)snctl_before, (unsigned long)snctl_after);
/* 时序寄存器实测回读:确认写入的 BUSLAT 真正落到硬件(1.5.57)。
/* 时序寄存器回读:确认写入的 BUSLAT 真正落到硬件。
* SNTCFG0 位域: [3:0]=ASET-1 [7:4]=AHLD-1 [15:8]=DSET-1 [19:16]=BUSLAT-1。
* 期望 ASET=4/AHLD=2/DSET=20/BUSLAT=4 → 0x0313_0000|... 逐字段解码打印,
* 避免只看整字导致的位域误读;BUSLAT 实际值 <4 说明库或硬件截断,须立即排查。 */
@@ -271,8 +271,8 @@ bool EscExmcBus::init() {
"ESC driver\r\n");
/* ---- 硬件诊断: 转储关键寄存器 + GPIO 手动输出测试 ----
* 1.5.33: 默认关闭。硬件层已确认全通,探针仅换板/补焊排查浮空时经
* CONFIG_ESC_EXMC_DIAG 临时开启(会拆 EXMC 引脚成 GPIO,正常运行不该跑)。 */
* 默认关闭。硬件层已确认全通,探针仅换板/补焊排查浮空时经
* CONFIG_ESC_EXMC_DIAG 临时开启(会拆 EXMC 引脚成 GPIO,正常运行不该跑)。 */
#if CONFIG_ESC_EXMC_DIAG
diagDump();
#endif
@@ -284,8 +284,8 @@ bool EscExmcBus::init() {
/* ========================================================================
* 硬件诊断: 精简版 —— 只保留核心探活 + bit-bang + 位级回显
* 1.5.33: 整段由 CONFIG_ESC_EXMC_DIAG 门控,默认不编译。bbDelay/bbReadData/
* bbDriveData 等位拍辅助函数在上方匿名 namespace 中,continuityDrive 仍复用,保留。
* 整段由 CONFIG_ESC_EXMC_DIAG 门控,默认不编译。bbDelay/bbReadData/
* bbDriveData 等位拍辅助函数在上方匿名 namespace 中,continuityDrive 仍复用,保留。
* ======================================================================== */
#if CONFIG_ESC_EXMC_DIAG
void EscExmcBus::diagDump() {
@@ -301,7 +301,7 @@ void EscExmcBus::diagDump() {
return;
}
printf("[EXMC-DIAG] --- bit-bang probe (bypass EXMC) ---\r\n");
/* NRST 实际电平回读(1.5.26):输入通路常连引脚,输出态亦读真实电压。
/* NRST 实际电平回读:输入通路常连引脚,输出态亦读真实电压。
* 驱动高却 LOW => ECAT_NRST 被外部按低 => LAN9252 一直在复位态。 */
printf("[EXMC-DIAG] NRST(PB10) reads %s\r\n",
(gpio_input_bit_get(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN) == SET)
@@ -459,7 +459,7 @@ static void continuityEnterTestMode() {
s_save_ctl0_e = GPIO_CTL0(GPIOE);
s_save_ctl1_e = GPIO_CTL1(GPIOE);
/* 测试期屏蔽 SYNC0/ESC IRQ/SYNC1:ISR 的 HBI 访问落在已被切成 GPIO 态的
* 总线上只会读回垃圾并错写 IDX,SSC 状态可能被污染(1.5.29)。 */
* 总线上只会读回垃圾并错写 IDX,SSC 状态可能被污染。 */
s_maskPrev = EscExmcBus::maskEnter();
/* 全部数据线 + 控制线设为 OUT_PP */
gpio_init(GPIOD, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
@@ -571,7 +571,7 @@ uint32_t EscExmcBus::maskEnter() {
/* EXTI0=SYNC0(preempt=0) 无法被 BASEPRI 屏蔽,只能关 NVIC;
* EXTI1=ESC IRQ / EXTI2=SYNC1 (均 preempt=1) 的 PDI_Isr/Sync1_Isr 同样做
* HBI 两步事务访问,一并屏蔽——否则主循环事务会被 Sync1_Isr 的
* PDO_InputMapping→HW_EscWriteIsr 打断而错址(1.5.29 修复)。 */
* PDO_InputMapping→HW_EscWriteIsr 打断而错址。 */
if (NVIC_GetEnableIRQ(EXTI0_IRQn) != 0U) {
prev |= 1U;
NVIC_DisableIRQ(EXTI0_IRQn);
+86 -87
View File
@@ -7,19 +7,17 @@
* - 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 永远失败):
* 地址线换算(16-bit 数据宽度时 EXMC 内部将 HADDR[25:1] 接到 EXMC_A[24:0]):
* 板线: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 修正):
* 访问模型(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/核心一直正常)。
* 核心 CSR 不在 HBI 直接空间,直读恒 0、写丢弃。
*/
#include "comm/esc_lan9252.h"
#include "comm/esc_exmc_bus.h"
@@ -59,7 +57,7 @@ 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——低于实测可用的
* 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)。 */
@@ -233,9 +231,8 @@ 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,
* 且要求释放前时钟输入已稳定。固件执行到此点时上电早已完成、晶振已稳定,
* 手册 18.6.3 要求 tpurstd(电源稳定→RST#释放)≥25ms、trstia(低脉宽)≥200µs,
* 且释放前时钟输入已稳定。固件执行到此点时上电早已完成、晶振已稳定,
* 统一 30ms 低脉宽同时覆盖两个下限。RST# 释放瞬间重锁 CHIP_MODE0/1 strap
* (LED_LINK1/2 经 R35/R36 10k 下拉 → 00b = 2 端口 HBI 模式)。
* 释放后保持推挽输出高:空闲电平确定,并保留运行期软件复位手段;
@@ -246,7 +243,10 @@ void EscLan9252::hardwareReset()
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);
/* 释放后等 50ms:warm jump 后芯片不是冷上电,内部状态机/PHY
* 重锁比 POR 慢;等太短时随后的 HBI 探针会读到启动中垃圾值。
* 50ms 仍远小于 300ms 轮询窗,且 init 单次成本可接受。 */
SysTickTimer::delayMs(50U);
/* 回读实际电平:GD32 输入通路常连引脚,输出态也能读到真实电压。
* 驱动高却读回低 => ECAT_NRST 网络被外部短路到地(或引脚损伤),
* LAN9252 被按死在复位态 —— 直接解释 HBI 全浮空 + 晶振照振。 */
@@ -255,7 +255,7 @@ void EscLan9252::hardwareReset()
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) */
/* RST# 复位清掉 PM 睡眠态(芯片回 D0),挂起门控随之解除 */
s_pmSuspended = false;
}
@@ -269,7 +269,7 @@ uint8_t EscLan9252::nrstDrive(uint8_t action)
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) */
/* 复位释放后芯片重走上电流程回 D0,睡眠挂起随之解除 */
s_pmSuspended = false;
break;
case 1: /* 持续拉低(手动保持复位) */
@@ -317,12 +317,12 @@ uint8_t EscLan9252::pmDrive(uint8_t op)
uint8_t status;
if (op == 0U) {
/* 状态查询。注意:1.6.24 实测 HBI 读事务本身就会唤醒睡眠中的芯片,
/* 状态查询。注意:HBI 读事务本身就会唤醒睡眠中的芯片,
* 本操作的语义 = "读状态(若在睡眠则主动打破睡眠)",非无侵入观测。 */
const uint32_t pmt = readReg32(LAN9252_REG_PMT_CTRL);
status = pmPackStatus(pmt);
/* 读到 D0(SLEEP_EN=0):芯片不在睡眠,若门控仍挂起(如 op4 唤醒超时后
* 芯片延迟自醒)则自愈解除,恢复主循环/ISR 的 HBI 访问(1.6.27)。 */
* 芯片延迟自醒)则自愈解除,恢复主循环/ISR 的 HBI 访问。 */
if ((status & 0x02U) == 0U && s_pmSuspended) {
s_pmSuspended = false;
printf("[ECAT][PM] status: chip in D0, pm suspend gate released\r\n");
@@ -344,15 +344,14 @@ uint8_t EscLan9252::pmDrive(uint8_t op)
/* 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/守护/心跳,
/* 立即挂起全部 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 访问(含读!)都会立即触发唤醒——
/* 进入睡眠后任何 HBI 访问(含读!)都会立即触发唤醒——
* 手册 6.3.4.2 只说 HBI "write" 唤醒,但芯片实际按主机 CS 活动判定,
* 写入后的读回验证会在 <50ms 内把芯片叫回 D0(板级复现:进 D1 后
* 3s/6s 静默仍自动回 D0,唯读回验证为已知访问;去掉后待验证)。
* 写入后的读回验证会把芯片叫回 D0。
* 故此处置信构造返回值,不再读回;睡眠态观测交给"读状态"(读即唤醒,
* 语义=主动打破睡眠),页面轮询须在睡眠态暂停。 */
status = (uint8_t)(0x02U | ((uint32_t)op << 2)); /* READY=0 | SLEEP_EN=1 | MODE=op */
@@ -390,7 +389,7 @@ uint8_t EscLan9252::pmDrive(uint8_t op)
restored ? "OK" : "FAIL (retry action 34)");
}
if (awake) {
/* 唤醒成功:解除 HBI 挂起,主循环/ISR 恢复运行(1.6.27)。
/* 唤醒成功:解除 HBI 挂起,主循环/ISR 恢复运行。
* 超时则保持挂起——芯片大概率仍睡着;若实际已醒,下一次 op0
* 状态查询读到 D0 会自愈解除。 */
s_pmSuspended = false;
@@ -407,69 +406,76 @@ uint8_t EscLan9252::pmDrive(uint8_t op)
bool EscLan9252::init()
{
s_busReady = false; /* 1.6.40:s_busReady = EXMC+BYTE_TEST 往返通过(IDX 残值不挡) */
s_coreReady = false; /* 1.5.39:s_coreReady 语义 = EEPROM 加载完成、EtherCAT 核心使能 */
s_busReady = false; /* EXMC+BYTE_TEST; warm 先探再定是否 RST# */
s_coreReady = false; /* 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;
}
/* 先起 EXMC 再谈复位。
* warm jump:BOOT 刚用过 HBI,芯片活着;此时若先 RST# 再起 EXMC,等于
* 把一条本来能通的总线按进复位重协商,一旦 RST# 释放时序不稳就整段假死。
* cold:POR 后芯片也需要 EXMC 才谈得上探活。
* 顺序固定为:EXMC → 无复位探 BYTE_TEST → 不通才 RST# 重来。 */
if (!EscExmcBus::init()) {
printf("[ECAT][ESC] init: FAIL, EXMC bus not ready (see [ECAT][EXMC] log)\r\n");
return false;
}
/* 1.6.40: 放行判据换到能兜住的那侧——BYTE_TEST 完整往返(索引写+数据低/高
* 字读)才证明 HBI 通路通;HBI_IDX_0 只证明「芯片刚出过复位」(复位默认值
* 0x1234,DS 9.5.4.2),证明不了通路。warm jump(BOOT 刷完直跳)时 BOOT 的
* bootEcatDeinit 最后一条 HBI 写把索引寄存器留在 0x0054(IRQ_CFG),而跳转前
* 那发硬复位已被证明会把 warm jump 打死(BOOT 1.1.0.18 已短路)——于是这条
* 「读复位默认值」的探针对 warm 路径必然假阴性;cold 路径芯片自带 POR 天然
* 0x1234,与那发复位有没有落地无关——这正是「reset 能起、刷完起不来」的根因。
* BYTE_TEST 每次访问前都会重写索引,与复位态无关。
* 结构:BYTE_TEST 为唯一放行门;不过才补一发 RST# 再探一轮(此刻 SSC 尚未
* MainInit,二次复位零代价;s_resetDone 只保护运行期重复 init,不挡 init 内
* 重试)。两轮 BYTE_TEST 仍不通 = 总线物理不通,维持降级。
* warm 残值(如 0x0054)跳过 0x1234 轮询,避免无意义 300ms 等待 + 二次复位。 */
uint16_t idx0 = 0U;
uint32_t byteTest = 0U;
for (uint8_t attempt = 0U; attempt < 2U; ++attempt) {
if (attempt > 0U) {
printf("[ECAT][ESC] probe#%u fail (idx0=0x%04X byte_test=0x%08lX), 补发 RST# 重探\r\n",
(unsigned)attempt, (unsigned)idx0, (unsigned long)byteTest);
hardwareReset();
}
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
/* 仅在「像复位中/未驱动」时轮询 0x1234;warm 残值(非 0/0xFFFF/0x1234)说明
* 芯片活着且带着上次索引,直接测 BYTE_TEST,不空等。 */
const bool idxLooksIdle = (idx0 == 0x0000U) || (idx0 == 0xFFFFU);
if (idxLooksIdle || idx0 == 0x1234U) {
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);
/* 第 0 轮:不复位直接探。warm 残值下 BOOT 刚写过 HBI,BYTE_TEST 应立刻可读。 */
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
byteTest = readReg32(LAN9252_REG_BYTE_TEST);
if (byteTest == LAN9252_BYTE_TEST_VALUE) {
printf("[ECAT][ESC] probe: BYTE_TEST ok without RST# (idx0=0x%04X) — 总线已活,跳过复位\r\n",
(unsigned)idx0);
s_resetDone = true;
} else {
printf("[ECAT][ESC] probe: BYTE_TEST=0x%08lX idx0=0x%04X, 补 RST# 最多 3 轮\r\n",
(unsigned long)byteTest, (unsigned)idx0);
bool alive = false;
for (uint8_t attempt = 0U; attempt < 3U; ++attempt) {
if (!s_resetDone || attempt > 0U) {
hardwareReset();
s_resetDone = true;
}
/* 复位后重配 EXMC:BOOT 关时钟时 PD/PE 停在 AF,MCU 侧寄存器也可能残留 */
if (!EscExmcBus::init()) {
continue;
}
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
/* 仅在「像复位中/未驱动」时轮询 0x1234;warm 残值直接测 BYTE_TEST */
const bool idxLooksIdle = (idx0 == 0x0000U) || (idx0 == 0xFFFFU);
if (idxLooksIdle || idx0 == 0x1234U) {
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);
}
}
byteTest = readReg32(LAN9252_REG_BYTE_TEST);
if (byteTest == LAN9252_BYTE_TEST_VALUE) {
alive = true;
printf("[ECAT][ESC] probe#%u: BYTE_TEST ok (idx0=0x%04X)\r\n",
(unsigned)(attempt + 1U), (unsigned)idx0);
break;
}
printf("[ECAT][ESC] probe#%u fail (idx0=0x%04X byte_test=0x%08lX)\r\n",
(unsigned)(attempt + 1U), (unsigned)idx0, (unsigned long)byteTest);
}
byteTest = readReg32(LAN9252_REG_BYTE_TEST);
if (byteTest == LAN9252_BYTE_TEST_VALUE) {
break;
if (!alive) {
printf("[ECAT][ESC] init: FAIL, BYTE_TEST=0x%08lX (expect 0x87654321), idx0=0x%04X -> LAN not driving bus\r\n",
(unsigned long)byteTest, (unsigned)idx0);
printf("[ECAT][ESC] hint: check CS(PD7) pulse, CHIP_MODE0/1 straps=00, RST#/power\r\n");
return false;
}
}
if (byteTest != LAN9252_BYTE_TEST_VALUE) {
printf("[ECAT][ESC] init: FAIL, BYTE_TEST=0x%08lX (expect 0x87654321), idx0=0x%04X -> LAN not driving bus\r\n",
(unsigned long)byteTest, (unsigned)idx0);
printf("[ECAT][ESC] hint: check CS(PD7) pulse, CHIP_MODE0/1 straps=00, RST#/power\r\n");
return false;
}
if (idx0 != 0x1234U) {
printf("[ECAT][ESC] init: WARN idx0=0x%04X != 0x1234, BYTE_TEST ok -> 复位态未知但通路可用,放行\r\n",
(unsigned)idx0);
@@ -478,10 +484,9 @@ bool EscLan9252::init()
(unsigned)idx0);
s_busReady = true;
/* 等待 PDI 就绪、EtherCAT 核心可交互(1.5.37 新增,1.5.39 改经 CSR 间接端口读——
* 此前直读 HBI 索引,DL_STATUS 属 EtherCAT 核心寄存器,根本不在直接地址空间)。
/* 等待 PDI 就绪、EtherCAT 核心可交互。
* DL_STATUS 属 EtherCAT 核心寄存器,不在 HBI 直接空间,必须经 CSR 间接端口读。
* 判据: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);
@@ -509,11 +514,10 @@ bool EscLan9252::init()
/**
* @brief 使能 LAN9252 的 IRQ 引脚输出,让 EtherCAT 事件真正到达 GD32 的 PB1。
* @note 1.5.43 新增。IRQ_CFG(0x054)/INT_EN(0x05C) 都是 LAN9252 系统 CSR
* @note 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,
* 不配这两个寄存器,PDI_Isr 不会触发,SSC 永远等不到 PROCESS_OUTPUT_EVENT,
* SAFEOP->OP 被无限挂起(表现为主站 "进 OP 失败 code=0x0000")。
* 极性必须与 ethercat_sync.cpp 的 EXTI 配置一致:那里是上拉输入 +
* 下降沿触发,故此处选推挽(IRQ_TYPE=1) + 低有效(IRQ_POL=0)。
@@ -521,7 +525,7 @@ bool EscLan9252::init()
*/
void EscLan9252::configPdWatchdog()
{
/* 1.5.45:过程数据看门狗 100ms(复位默认)→1s。
/* 过程数据看门狗 100ms(复位默认)→1s。
* 主站侧手动单步/100ms 调试周期会触发 SM2 看门狗:SSC 在 CheckIfEcatError()
* 读 0x0440 发现已过期,调 AL_ControlInd(STATE_SAFEOP, 0x001B) 把 OP 拖回 SAFEOP,
* 表现为"100ms 周期全 WKC 错、10/50ms 正常"。分频保持默认 100µs,只延长步数;
@@ -562,12 +566,12 @@ 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 轮询等低风险
* verify=true(默认):CSR_DATA 是锁存寄存器(非 FIFO),命令不变时连读两次必一致;
* 不一致即本拍 EXMC 毛刺——错读可能让 GetSyncMan 把错误值缓存成邮箱几何
* (→SSC 邮箱楔死,邮箱几何另有 mailbox.c 构建期常量兜底),或让
* CheckSmSettings 以瞬态值打断状态转换。对错读整事务重跑一次;
* 两轮仍不一致则交出末拍。
* verify=false:单读路径,供诊断回读/MII busy 轮询/init 轮询等低风险
* 调用方使用;SM/AL/邮箱等状态机关键寄存器必须保持默认双读。 */
if (!verify) {
uint32_t n = POLL_MAX;
@@ -611,13 +615,9 @@ void EscLan9252::pramRead(uint16_t addr, uint8_t *buf, uint16_t len)
* 首 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。 */
* 必须逐 DWORD 轮询并弹前查 AVAIL 位。CMD 的 AVAIL_CNT(bits[12:8])批量弹模式
* 对短读不可靠:首 DWORD 新鲜、后续 DWORD 可能取到 FIFO 陈旧项。邮箱短读/探针
* 类小传输只验证过"逐 DWORD 查 AVAIL"模式,不要改回批量弹 DWORD。 */
if (len == 0U) {
return;
}
@@ -646,8 +646,7 @@ 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 写同类风险。 */
* 与 pramRead 同样逐 DWORD 轮询(推前必查 AVAIL 位),邮箱小 PDU 写同类风险。 */
if (len == 0U) {
return;
}
@@ -683,7 +682,7 @@ uint32_t EscLan9252::readChipId()
uint16_t EscLan9252::initOnlyReadReg(uint16_t addr)
{
/* 1.5.39:ESC 核心 CSR 经间接端口读(此前直读 HBI 索引,核心寄存器不在直接空间,恒 0) */
/* ESC 核心 CSR 经间接端口读(核心寄存器不在 HBI 直接空间,直读恒 0) */
return (uint16_t)csrRead(addr, 2U);
}
+16 -16
View File
@@ -2,7 +2,7 @@
* @file esc_lan9252.h
* @brief LAN9252 EtherCAT 从站控制器(ESC)驱动接口。
* @note 通过 EscExmcBus 16-bit 并行总线,以 HBI Indexed Address Mode 访问 LAN9252。
* 访问模型(1.5.39 按 DS 12.11 修正,此前误以为整个 16 位索引空间统一可寻址):
* 访问模型(DS 12.11):
* - HBI 直接地址空间:仅 LAN9252 系统 CSR(BYTE_TEST 0x064 等)+ 访问端口
* (PRAM FIFO 0x000-0x03C、ECAT_CSR_DATA/CMD 0x300/0x304、PRAM_ADDR_LEN/CMD
* 0x308-0x314)——经 HBI_IDX_0/HBI_DATA_0 直接读写;
@@ -29,7 +29,7 @@
class EscLan9252 {
public:
/** @brief 初始化 EXMC 总线与 ESC 基本寄存器。
* @note 1.5.25 起内部先执行一次 hardwareReset() 受控复位(s_resetDone 门控,
* @note 内部先执行一次 hardwareReset() 受控复位(s_resetDone 门控,
* 重复调用不会二次复位 SSC 已配置好的芯片),再探活 EXMC/HBI。
* @return bool true 表示 EXMC 总线探活通过(可继续读取芯片 ID)。 */
static bool init();
@@ -65,20 +65,20 @@ public:
static uint32_t readChipId();
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SSC 移植层(HW_*)。
* @note 1.5.39 起经 CSR_CMD/CSR_DATA 间接端口读 ESC 核心 CSR(此前直读 HBI 索引,
* 而 EtherCAT 核心寄存器根本不在 HBI 直接地址空间,恒读 0)。 */
* @note 经 CSR_CMD/CSR_DATA 间接端口读 ESC 核心 CSR
* (EtherCAT 核心寄存器不在 HBI 直接地址空间,直读恒 0)。 */
static uint16_t initOnlyReadReg(uint16_t addr);
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用。 */
static void initOnlyWriteReg(uint16_t addr, uint16_t v);
/** @brief ESC 核心 CSR 间接读(ECAT_CSR_CMD/DATA 端口,0x0000-0x0FFF 地址空间)。
* @note 1.5.39:LAN9252 的 HBI/SPI 直接地址空间只有 LAN9252 系统 CSR 与访问端口
* @note LAN9252 的 HBI/SPI 直接地址空间只有 LAN9252 系统 CSR 与访问端口
* (DS 12.11/表 12-13),EtherCAT 核心寄存器(0x0000-0x0FFF)必须经
* CSR_CMD(0x304)/CSR_DATA(0x300) 间接访问;过程 RAM(0x1000-0x1FFF)
* 必须经 PRAM FIFO。size=1/2/4(size2 需偶地址,size4 需 4 字节对齐)。
* 调用方负责 HBI 互斥(主循环 EscExmcBus::maskEnter / 已在 ISR 上下文)。
* @param verify true(默认)=双读比对+不一致重试(1.5.59,防 EXMC 毛刺,SM/AL/邮箱
* @param verify true(默认)=双读比对+不一致重试(防 EXMC 毛刺,SM/AL/邮箱
* 等关键路径必须开);false=单读(诊断回读/MII busy 轮询/init 等低风险路径,
* 省约一半 CSR 间接访问)。 */
static uint32_t csrRead(uint16_t addr, uint8_t size, bool verify = true);
@@ -93,40 +93,40 @@ public:
static void pramWrite(uint16_t addr, const uint8_t *buf, uint16_t len);
/** @brief EXMC 总线 + BYTE_TEST 往返探针通过(init 成功)后为 true。
* @note 1.6.40 起 HBI_IDX_0 复位默认值降级为诊断读数,不再参与放行
* @note HBI_IDX_0 复位默认值仅作诊断读数,不参与放行
* (它只证明"芯片刚出过复位",证明不了 HBI 通路)。
* 与 init() 返回值一致,可作运行期查询。 */
static bool isBusReady() { return s_busReady; }
/** @brief EEPROM(SII) 加载完成、EtherCAT 核心使能(1.5.39)。
/** @brief EEPROM(SII) 加载完成、EtherCAT 核心使能。
* @note false 时核心寄存器对 PDI 读全 0、写静默丢弃,SSC MainInit 会全部
* 落空(AL Event Mask/邮箱 SM 配置写不进),调用方必须跳过 SSC 初始化。 */
static bool isCoreReady() { return s_coreReady; }
/** @brief ESC 已被 pmDrive 置入 D1-D3 睡眠、所有 HBI 消费者必须静默(1.6.27)。
* @note 1.6.24 实测睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。置位期间
/** @brief ESC 已被 pmDrive 置入 D1-D3 睡眠、所有 HBI 消费者必须静默。
* @note 睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。置位期间
* task_runner 停调 MainLoop()/AL 守护/AL 心跳,ethercat_sync 三个 EXTI
* ISR 清 pending 后直接返回——否则芯片进入睡眠后毫秒级就被主循环的
* ECAT_Main(读 0x220)叫醒,根本睡不住(2026-09-12 休眠 ~20s 即"复活")。
* ECAT_Main(读 0x220)叫醒,根本睡不住。
* 解除:pmDrive 唤醒/读到 D0、nrstDrive 释放复位、hardwareReset。
* ISR 上下文安全:读一个 bool 静态成员,无锁。 */
static bool pmSuspended() { return s_pmSuspended; }
private:
/** @brief 使能 LAN9252 的 IRQ 引脚输出(IRQ_CFG 0x054 / INT_EN 0x05C)。
* @note 1.5.43 新增,由 init() 在 PDI/核心就绪后调用。两寄存器复位默认全 0,
* @note 由 init() 在 PDI/核心就绪后调用。两寄存器复位默认全 0,
* 不配则 EtherCAT 事件永不到达 PB1、SSC 的 PDI_Isr 从不执行,
* bEcatFirstOutputsReceived 恒 FALSE 使 SAFEOP->OP 被无限挂起。 */
* SAFEOP->OP 被无限挂起。 */
static void enableEcatIrqOutput();
/** @brief 过程数据看门狗 100ms(复位默认)→1s(1.5.45)。
/** @brief 过程数据看门狗 100ms(复位默认)→1s。
* @note 由 init() 在 PDI/核心就绪后调用。写 ESC_REG_WD_TIME_PD(0x0420)=10000
* (100µs 分频保持默认)。不写则主站 100ms 手动调试周期触发 SM2 看门狗,
* SSC 以 0x001B 拖回 SAFEOP。 */
static void configPdWatchdog();
static bool s_busReady; /**< init 成功(EXMC+BYTE_TEST 往返,1.6.40;IDX 残值不挡)后置位。 */
static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位(1.5.39)。 */
static bool s_busReady; /**< init 成功(EXMC+BYTE_TEST 往返;IDX 残值不挡)后置位。 */
static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位。 */
static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */
static bool s_pmPhyDown; /**< pmDrive 进 D3 时两 PHY 已被置 PowerDown,唤醒后需恢复。 */
static bool s_pmSuspended; /**< ESC 睡眠挂起:置位期间全固件 HBI 消费者静默(见 pmSuspended)。 */
+100 -24
View File
@@ -1,10 +1,11 @@
/**
* @file ethercat_can_bridge.cpp
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.6.21:段 2 电机整轮密排)。
* @brief EtherCAT PDO ↔ CAN 转发桥实现。
*/
#include "comm/ethercat_can_bridge.h"
#include "device/canfd.h"
#include "device/motor_state.h"
#include "sys/systick.h"
#include "RTE_Components.h"
#include CMSIS_device_header /* __DMB():seqlock 的编译器/总线屏障 */
@@ -17,6 +18,9 @@ volatile bool EthercatCanBridge::s_coldStartPending = false;
volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U;
volatile uint16_t EthercatCanBridge::s_txCount = 0U;
volatile uint16_t EthercatCanBridge::s_dropCount = 0U;
volatile uint32_t EthercatCanBridge::s_lastTxId = 0U;
volatile uint16_t EthercatCanBridge::s_rx00ffCount = 0U;
volatile uint8_t EthercatCanBridge::s_outSeenSeq = 0U;
volatile bool EthercatCanBridge::s_rxValid = false;
volatile bool EthercatCanBridge::s_rxPublished = false;
@@ -28,6 +32,20 @@ volatile uint32_t EthercatCanBridge::s_rxId = 0U;
volatile uint32_t EthercatCanBridge::s_rxCount = 0U;
uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0};
volatile bool EthercatCanBridge::s_verValid = false;
volatile uint32_t EthercatCanBridge::s_verHoldUntilMs = 0U;
uint8_t EthercatCanBridge::s_verCh = 0U;
uint8_t EthercatCanBridge::s_verDlc = 0U;
uint32_t EthercatCanBridge::s_verId = 0U;
uint8_t EthercatCanBridge::s_verData[kEcatCanXRxDataMax] = {0};
namespace {
/** 版本应答在 PDO 段3 的保持时长(ms)。主站 1ms 周期下足够被采到。 */
constexpr uint32_t kVerHoldMs = 300U;
inline uint16_t canSubId(uint32_t id) { return static_cast<uint16_t>(id & 0xFFFFU); }
} // namespace
uint8_t EthercatCanBridge::s_motShadow[kEcatMotRegionBytes] = {0};
uint32_t EthercatCanBridge::s_motPubSeq[kMotorMaxNodes + 1U] = {0};
uint8_t EthercatCanBridge::s_motPubOnline[kMotorMaxNodes + 1U] = {0};
@@ -98,6 +116,7 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
if (r == CanFd::CanTxResult::kOk) {
++s_txCount;
++sent;
s_lastTxId = rd32le(&f[ECAT_FRM_OFF_ID]);
} else {
s_lastTxResult = (uint8_t)r;
if (stopOnError) {
@@ -170,7 +189,9 @@ uint8_t EthercatCanBridge::fillMotorRound(uint8_t *in)
const uint16_t region = motRegionLen(unchanged ? s_motPubCount : n);
if (unchanged) {
/* 集合+seq+online 未变:整段复用影子,避免 8kHz 下每拍重建密排区。
* 头字段 ACTIVE/BOOT/MAGIC/GSEQ 在影子起点 COUNT 之后,一并复用。 */
if (unchanged && s_motShadowValid && s_motPubCount > 0U) {
memcpy(&in[ECAT_MOTW_OFF_COUNT], s_motShadow, region);
return s_motPubCount;
}
@@ -216,13 +237,14 @@ uint8_t EthercatCanBridge::fillMotorRound(uint8_t *in)
bool EthercatCanBridge::peekOutput(const uint8_t *head)
{
/* 输出探针:12B 头里 SEQ@0 / FLAGS。seq 无变化沿且无清统计请求时返回 false,
* PDO_OutputMapping 只补读 SM2 末字,跳过 384B 全量读。
* 恒 true 会让 8kHz 每拍都全量读,单拍被 PRAM 打爆(6kHz 即掉速)。 */
if (head == nullptr) {
return false;
}
const uint8_t seq = head[ECAT_CAN_TX_OFF_SEQ];
/* WIN@8 不再使用:1.6.21 电机整轮密排,无滑窗。 */
if (!s_seqPrimed) {
s_seqPrimed = true;
s_lastSeq = seq;
@@ -250,10 +272,10 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
}
const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ];
s_outSeenSeq = seq; /* 诊断:processOutput 本拍看到的输出 seq */
if (!s_seqPrimed) {
s_seqPrimed = true;
s_lastSeq = seq;
s_coldStartPending = true;
}
@@ -262,20 +284,28 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
s_txCount = 0U;
s_dropCount = 0U;
s_rxCount = 0U;
s_rx00ffCount = 0U;
s_rxPublished = true;
}
const uint8_t dlcRaw = out[ECAT_CAN_TX_OFF_DLC];
const uint8_t batchCount = out[ECAT_TXB_OFF_COUNT];
const bool hasPayload = (dlcRaw > 0U) || (batchCount > 0U);
bool executeCommand = false;
if (s_coldStartPending) {
s_coldStartPending = false;
/* 冷启动首包:若包含有效单帧命令或批量帧,立即执行 */
const uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC];
const uint8_t batchCount = out[ECAT_TXB_OFF_COUNT];
if (dlc > 0U || batchCount > 0U) {
/* 冷启动:有载荷就发(不依赖 s_lastSeq 是否已被探针碰过) */
if (hasPayload) {
executeCommand = true;
} else {
s_lastSeq = seq;
return;
}
} else if (seq != s_lastSeq) {
executeCommand = true;
} else if (hasPayload && seq != s_lastSeq) {
executeCommand = true;
}
if (!executeCommand) {
@@ -284,7 +314,7 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
s_lastSeq = seq;
const uint8_t ch = out[ECAT_CAN_TX_OFF_CH];
uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC];
uint8_t dlc = dlcRaw;
if (dlc > kEcatCanTxDataMax) {
dlc = kEcatCanTxDataMax;
}
@@ -299,6 +329,7 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
s_lastTxResult = (uint8_t)r;
if (r == CanFd::CanTxResult::kOk) {
++s_txCount;
s_lastTxId = rd32le(&out[ECAT_CAN_TX_OFF_ID]);
}
}
}
@@ -311,7 +342,9 @@ void EthercatCanBridge::buildInput(uint8_t *in)
if (in == nullptr) {
return;
}
memset(in, 0, kEcatPdoInBytes);
/* 不整包 memset(600):8kHz 下每拍 Process 都会走到这里,600B 清零
* 与后续覆盖写重复。各段自己写满有效域;未用槽由 fillMotorRound
* 按 count 截断,主站只迭代 count,空洞区可为残值。 */
/* ── 段 1(0..31):桥状态 + 短快照 ── */
in[ECAT_CAN_RX_OFF_ACK] = s_lastSeq;
@@ -319,6 +352,8 @@ void EthercatCanBridge::buildInput(uint8_t *in)
wr16le(&in[ECAT_CAN_RX_OFF_TXCNT], s_txCount);
wr16le(&in[ECAT_CAN_RX_OFF_DROP], s_dropCount);
wr32le(&in[ECAT_CAN_RX_OFF_RXCNT], s_rxCount);
wr32le(&in[ECAT_CAN_RX_OFF_TXTXID], s_lastTxId);
wr16le(&in[ECAT_CAN_RX_OFF_RX00FF], s_rx00ffCount);
const bool xrxStable = ((s_xrxSeq & 1U) == 0U);
@@ -333,26 +368,45 @@ void EthercatCanBridge::buildInput(uint8_t *in)
wr32le(&in[ECAT_CAN_RX_OFF_ID], s_rxId);
memcpy(&in[ECAT_CAN_RX_OFF_DATA], s_rxData, kEcatCanRxDataMax);
s_rxPublished = true;
} else {
in[ECAT_CAN_RX_OFF_FLAGS] = 0U;
in[ECAT_CAN_RX_OFF_CH] = 0U;
in[ECAT_CAN_RX_OFF_DLC] = 0U;
}
const uint8_t ch = s_rxValid ? s_rxCh : 0U;
const uint32_t backlog = CanFd::isChannelEnabled(ch) ? CanFd::txQueueBacklog(ch) : 0U;
in[ECAT_CAN_RX_OFF_BACKLOG] = (backlog > 255U) ? 255U : (uint8_t)backlog;
/* ── 段 2(32..543):电机整轮密排(空槽不发,count=实际电机数)── */
/* ── 段 2(32..543):电机整轮密排 ── */
(void)fillMotorRound(in);
/* ── 段 3(544..583):最近一帧完整快照 ──
* 与段 1 共用 xrxStable 门控,两段必须来自同一帧。 */
if (xrxStable && s_rxValid) {
in[ECAT_XRX_OFF_CH] = s_rxCh;
in[ECAT_XRX_OFF_DLC] = s_rxDlc;
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
wr32le(&in[ECAT_XRX_OFF_ID], s_rxId);
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
/* ── 段 3(544..583):长帧快照(通用,不专宠版本) ──
* 语义 =「最近一帧非 STATUS 长帧」:CONFIG/版本/节点ID 谁后到谁上。
* STATUS(0x0100) 由段2 密排承载,不进本槽(否则 48B STATUS 会淹没一切)。
* 例外:仅当刚收到 0x00FF 的短粘滞窗口内,优先摆版本应答,
* 给主站一个稳定采样窗;窗口结束后立刻交还给 CONFIG 等,避免其它长帧失效。 */
if (xrxStable) {
const uint32_t nowMs = SysTickTimer::getMs();
const bool stickyLive = s_verValid && static_cast<int32_t>(nowMs - s_verHoldUntilMs) < 0;
/* 段3 RSVD@547:processOutput 看到的输出 seq(诊断用,不参与解析) */
in[ECAT_XRX_OFF_RSVD] = s_outSeenSeq;
if (stickyLive) {
in[ECAT_XRX_OFF_CH] = s_verCh;
in[ECAT_XRX_OFF_DLC] = s_verDlc;
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
wr32le(&in[ECAT_XRX_OFF_ID], s_verId);
memcpy(&in[ECAT_XRX_OFF_DATA], s_verData, kEcatCanXRxDataMax);
} else if (s_rxValid) {
in[ECAT_XRX_OFF_CH] = s_rxCh;
in[ECAT_XRX_OFF_DLC] = s_rxDlc;
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
wr32le(&in[ECAT_XRX_OFF_ID], s_rxId);
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
}
}
/* 段 4(历史覆盖窗口)已删除;段 5 由 EthercatPdo::fillDiagTail 写。 */
/* 段 5 由 EthercatPdo::fillDiagTail 写。 */
}
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
@@ -362,6 +416,12 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
return;
}
/* STATUS(0x0100) 由 MotorState 段2 密排承载,不写入 xrx。
* 原注释本就如此约定;实现曾漏过滤,导致高频 STATUS 盖掉 CONFIG/版本。 */
if (canSubId(id) == 0x0100U) {
return;
}
uint8_t copyLen = length;
if (copyLen > kEcatCanXRxDataMax) {
copyLen = kEcatCanXRxDataMax;
@@ -388,11 +448,27 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
s_rxValid = true;
s_rxPublished = false;
if (canSubId(id) == 0x00FFU) {
if (s_rx00ffCount < 0xFFFFU) {
++s_rx00ffCount;
}
}
/* 版本应答:只做短粘滞,供主站在窗口内采到;不长期霸占 xrx */
if (canSubId(id) == 0x00FFU) {
s_verCh = channel;
s_verDlc = length;
s_verId = id;
memcpy(s_verData, data, copyLen);
if (copyLen < kEcatCanXRxDataMax) {
memset(&s_verData[copyLen], 0, (size_t)(kEcatCanXRxDataMax - copyLen));
}
s_verHoldUntilMs = SysTickTimer::getMs() + kVerHoldMs;
s_verValid = true;
}
__DMB();
++s_xrxSeq;
/* STATUS 走 MotorState → 段 2 整轮;非 STATUS 只更新段 1/3 最近一帧快照。
* 1.6.21 起不再写入历史覆盖窗口。 */
}
extern "C" {
+34 -21
View File
@@ -1,6 +1,6 @@
/**
* @file ethercat_can_bridge.h
* @brief EtherCAT PDO ↔ CAN 转发桥(1.6.21:段 2 改 9 电机密排整轮)。
* @brief EtherCAT PDO ↔ CAN 转发桥。
*
* 输出 PDO(0x7000, 主站→从站, 384B) = 待发送的 CAN 帧(数据区 32B)
* 输入 PDO(0x6000, 从站→主站, 600B) = 桥状态 + 电机整轮状态 + 长帧快照 + 诊断
@@ -13,23 +13,17 @@
* TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态
* 会随机丢帧。改为由 taskCanRxParse() 的分发链回调 onCanFrame() 投递。
*
* ── 1.6.21:段 2 改「一帧 = 总线电机整轮状态」,删段 4 历史帧窗口 ──
* 段 2 语义:一帧 = 总线电机整轮状态。
*
* 旧布局的问题(用户实测:接 3 个电机网页却看到 5 个状态帧):
* 1. 段 2 是固定 5 槽滑窗(win_base 要 6 拍才扫完 27 槽),空槽 node_id=0
* 也占位,win_count 报的是跨度不是电机数 —— 主站若不滤空槽就会多报;
* 2. 段 4 覆盖窗口把最近几条非 STATUS 帧反复重发,属于「历史帧 / 重复帧」。
*
* 本版语义改为用户需求:
* · 每一拍输入 PDO 携带**总线上已识别电机的完整一轮状态**(最多 9 个);
* · **空槽不发送**:密排,count = 本拍实际打包的电机数;
* · 主站按 count 迭代,每槽自带 node_id,自动识别,无需 win_base 滑窗;
* · 不发段 4 历史帧;版本/CONFIG 仍走段 1/段 3「最近一帧」快照;
* · 版本/CONFIG 走段 1/段 3「最近一帧」快照;
* · 上一轮已打包且 seq 未变的电机,本轮跳过 MotorState 重读与 48B 拷贝,
* 直接复用影子区(s_motShadow)。
*
* 输入 PDO 总长仍 600B ⇒ 无需重刷 EEPROM/SII;段 2/3 偏移变了,**主站解码
* 必须同步**(tools/ecat 的 decode_input / transport-ecat.js 等)。
* 输入 PDO 总长 600B;主站解码必须与段 2/3 偏移一致
* (tools/ecat 的 decode_input / transport-ecat.js 等)。
*/
#ifndef COMM_ETHERCAT_CAN_BRIDGE_H
#define COMM_ETHERCAT_CAN_BRIDGE_H
@@ -39,14 +33,14 @@
#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):单帧发送。 */
enum : uint8_t {
ECAT_CAN_TX_OFF_SEQ = 0U, /**< 发送序号:变化沿触发一次发送(单帧+批量一起) */
ECAT_CAN_TX_OFF_CH = 1U, /**< 目标通道 0/1/2 */
ECAT_CAN_TX_OFF_DLC = 2U, /**< 数据长度 0..32;0 表示本次不发单帧(只发批量) */
ECAT_CAN_TX_OFF_FLAGS = 3U, /**< bit0=1 请求清零统计;其余保留 */
ECAT_CAN_TX_OFF_ID = 4U, /**< CAN ID,小端 uint32(29 位扩展帧) */
ECAT_CAN_TX_OFF_WIN = 8U, /**< 1.6.21 起忽略(电机整轮密排,无滑窗);保留占位 */
ECAT_CAN_TX_OFF_WIN = 8U, /**< 当前忽略(电机整轮密排,无滑窗);保留占位 */
/* 9..15 保留:让数据区落在 16 字节边界 */
ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */
};
@@ -69,7 +63,7 @@ enum : uint8_t {
ECAT_FRM_HDR_SIZE = 8U, /**< 头长度,数据紧跟其后 */
};
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照。 */
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照 + 收发诊断。 */
enum : uint8_t {
ECAT_CAN_RX_OFF_ACK = 0U, /**< 回显已执行的发送序号,主站据此确认送达 */
ECAT_CAN_RX_OFF_CH = 1U, /**< 该帧来自哪个通道 */
@@ -82,10 +76,12 @@ enum : uint8_t {
ECAT_CAN_RX_OFF_BACKLOG = 21U, /**< 该通道软件 TX 队列积压 */
ECAT_CAN_RX_OFF_TXCNT = 22U, /**< 累计发送受理帧数,小端 uint16 */
ECAT_CAN_RX_OFF_DROP = 24U, /**< 段 1/段 3 快照「未取先覆」次数(u16 累计回绕) */
ECAT_CAN_RX_OFF_TXTXID = 26U, /**< 最近 enqueueTx 成功的 CAN ID,小端 uint32 */
ECAT_CAN_RX_OFF_RX00FF = 30U, /**< CAN RX 收到 0x00FF 次数,小端 uint16 */
};
/**
* 输入 PDO 段 2(32..543):电机整轮状态(密排,1.6.21)。
* 输入 PDO 段 2(32..543):电机整轮状态(密排)。
*
* 主站解析:
* count = in[32] // 0..9,本拍实际打包的电机数
@@ -98,7 +94,7 @@ enum : uint16_t {
ECAT_MOTW_OFF_COUNT = 32U, /**< 本拍密排电机数 0..9(主站按此迭代) */
ECAT_MOTW_OFF_ACTIVE = 33U, /**< 在线电机数(与 count 可不同:离线但仍报最后一轮) */
ECAT_MOTW_OFF_BOOT = 34U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */
ECAT_MOTW_OFF_MAGIC = 35U, /**< 恒 kEcatMotLayoutMagic(0xA5):1.6.21 密排标识 */
ECAT_MOTW_OFF_MAGIC = 35U, /**< 恒 kEcatMotLayoutMagic(0xA5):密排布局标识 */
ECAT_MOTW_OFF_GLOBAL_SEQ = 36U, /**< 状态表全局刷新序号,小端 uint32 */
ECAT_MOTW_OFF_SLOTS = 40U, /**< 密排槽数组起点,count×56B */
};
@@ -150,8 +146,8 @@ constexpr uint8_t kEcatMotHdrSize = 8U;
constexpr uint16_t kEcatMotRegionBytes =
(uint16_t)(kEcatMotHdrSize
+ (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize);
/** 布局魔数,写在 offset 35。主站据此区分 1.6.21 密排与 1.6.15 滑窗(两者
* Ibytes 同为 600,不能只靠长度分支)。0=旧布局。 */
/** 布局魔数,写在 offset 35。主站据此区分密排布局与旧滑窗布局(两者
* Ibytes 同为 600,不能只靠长度分支)。0=非密排布局。 */
constexpr uint8_t kEcatMotLayoutMagic = 0xA5U;
/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h。 */
@@ -194,7 +190,7 @@ class EthercatCanBridge {
public:
/**
* @brief 输出探针:seq 变化时请求全量读。
* @note 1.6.21 起不再刷新 win_base(电机整轮密排,无滑窗);仍处理 seq 沿
* @note 不再刷新 win_base(电机整轮密排,无滑窗);仍处理 seq 沿
* 与 FLAGS bit0 清统计。
*/
static bool peekOutput(const uint8_t *head);
@@ -234,8 +230,16 @@ private:
static volatile uint8_t s_lastTxResult;
static volatile uint16_t s_txCount;
static volatile uint16_t s_dropCount;
/** 最近一次 enqueueTx 成功的 CAN ID(诊断:ECAT 是否真的发出去) */
static volatile uint32_t s_lastTxId;
/** CAN RX 收到 0x00FF(版本应答)次数(诊断:电机是否回帧) */
static volatile uint16_t s_rx00ffCount;
/** processOutput 本拍读到的输出 PDO seq(诊断:ESC 输出是否到达应用层) */
static volatile uint8_t s_outSeenSeq;
/* 最近一帧接收快照:taskCanRxParse 写、PDO 读。s_xrxSeq 为 seqlock。 */
/* 最近一帧接收快照:taskCanRxParse 写、PDO 读。s_xrxSeq 为 seqlock。
* STATUS(0x0100) 禁止写入本快照 —— 它由 MotorState 段2 密排承载;
* 否则高频 STATUS 会持续冲掉版本应答(0x00FF)等低频长帧。 */
static volatile bool s_rxValid;
static volatile bool s_rxPublished;
static volatile uint32_t s_xrxSeq;
@@ -246,7 +250,16 @@ private:
static volatile uint32_t s_rxCount;
static uint8_t s_rxData[kEcatCanXRxDataMax];
/* ── 段 2 差分填充影子(1.6.21)──
/* 版本应答粘滞快照:0x00FF 到达后在 PDO 段3 保持发布一段时间,
* 避免被随后的 CONFIG 等帧在主站采样前覆盖。 */
static volatile bool s_verValid;
static volatile uint32_t s_verHoldUntilMs;
static uint8_t s_verCh;
static uint8_t s_verDlc;
static uint32_t s_verId;
static uint8_t s_verData[kEcatCanXRxDataMax];
/* ── 段 2 差分填充影子 ──
* 上一拍已发布的密排区副本 + 各节点上次发布的 seq/online + 头字段。
* 节点集合、seq、online、active/boot 全未变时整段复用,跳过 MotorState
* 重读与逐槽 48B 拷贝。掩码必须 u16:节点号 1..9,bit9 超出 uint8_t。 */
+76 -32
View File
@@ -1,6 +1,6 @@
/**
* @file ethercat_pdo.cpp
* @brief EtherCAT 过程数据(PDO)收发模块实现(1.5.55)。
* @brief EtherCAT 过程数据(PDO)收发模块实现。
*/
#include "comm/ethercat_pdo.h"
#include "comm/ethercat_can_bridge.h"
@@ -19,6 +19,13 @@
#include <cstring>
extern "C" {
/* SSC 输出过程数据:SM2 地址与长度(ecatslv.c,类型为 SSC UINT16=uint16) */
extern uint16_t nEscAddrOutputData;
extern uint16_t nPdOutputSize;
void HW_EscRead(void *pData, uint16_t Address, uint16_t Len);
}
/* alignas(4) 必须在类内声明与出类定义上同时标注:AC6(Clang) 否则报
* "'alignas' must be specified on definition"。用途见 ethercat_pdo.h。 */
alignas(4) uint8_t EthercatPdo::s_out[EthercatPdo::kPdoOutBytes] = {0};
@@ -45,6 +52,9 @@ bool EthercatPdo::s_escSh3Valid = false;
volatile uint32_t EthercatPdo::s_outputCycles = 0U;
volatile uint32_t EthercatPdo::s_inputCycles = 0U;
volatile uint32_t EthercatPdo::s_reentryCount = 0U;
/** OutputMapping 本拍是否已刷新 s_out(process 兜底读的新鲜度门) */
static volatile bool s_outMappedThisCycle = false;
volatile uint32_t EthercatPdo::s_processSeq = 0U;
void EthercatPdo::readOutputs(const uint8_t *esc)
{
@@ -62,22 +72,15 @@ bool EthercatPdo::outputProbe(const uint8_t *head)
void EthercatPdo::process()
{
/* 重入防护(1.6.7):SSC 的 ECAT_Application()→本函数 在**四个上下文**都会被
/* 重入防护:SSC 的 ECAT_Application()→本函数 在**四个上下文**都会被
* 调用 —— PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop(见 ssc/src/ecatappl.c
* 388、499、527、641 行)。而 MainLoop 那条路只用 DISABLE_ESC_INT() 关了
* EXTI1(ESC IRQ),**挡不住 EXTI0(Sync0,preempt=0) 和 EXTI2(Sync1,preempt=1)**。
*
* 竞态:MainLoop 把 s_in 填到一半(如刚写完批量区第 1 条)时被 Sync0/Sync1
* 竞态:MainLoop 把 s_in 填到一半时被 Sync0/Sync1
* 抢占 → 内层重入本函数,memset 清空后重填整个输入 PDO,随后 PDO_InputMapping()
* 把这一份发给 ESC;外层回来又按自己那轮的进度接着往下写。于是发出去的镜像里
* 批量区相邻两条来自**不同轮次**,而两轮之间环形队列 tail 已推进 —— 第 2 条
* 就带上了另一条帧的内容。实测特征与此吻合:批量区第 1 条几乎不坏(0.06%)、
* 第 2 条 30.75% 坏且内容是一条完整 CONFIG 帧;段 1/段 3 干净,因为它们每轮
* 内容相同,被覆盖也看不出来。
*
* 1.6.15 批量区改成定长覆盖窗口后,跨轮次混填不再产生"半新半旧"的假帧(每槽
* 自带 seqlock,且槽位固定、语义本就是"最近 N 帧"),但本防护仍然保留:省掉
* 一次无谓的整块重填,并让镜像保持同一轮次的一致性。
* 批量区相邻两条来自**不同轮次**。
*
* 处理:已在填充中就直接返回。输入 PDO 是周期量,跳过一次下一拍即补,
* 远好过发出一份跨轮次拼接的镜像。 */
@@ -104,18 +107,48 @@ void EthercatPdo::process()
const uint8_t nextIdx = 1U - s_inActiveIdx;
uint8_t *build = s_inBuf[nextIdx];
/* 输出兜底全量读:仅当 OutputMapping 本拍未刷新 s_out,且
* (s_out 看起来是死的 或 距上次强制读已跑过一段 process) 才读。
* 每拍 process() 都 HW_EscRead 384B 会与 PDO_OutputMapping 叠成双份 HBI,
* PDO 2kHz 即把 ISR 成本打满(历史探针版 4kHz load≈80%)。 */
static uint32_t s_lastForceReadSeq = 0U;
if (nPdOutputSize > 0U && nEscAddrOutputData != 0U && !s_outMappedThisCycle) {
const bool outDead = (s_out[0] == 0U && s_out[1] == 0U && s_out[8] == 0U);
const uint32_t seqNow = s_processSeq;
/* 仅 s_out 疑似死数据时强制全量读;探针已处理 seq 沿。
* 按拍数兜底间隔拉到 256,避免 8kHz 下周期性 384B HBI。 */
if (outDead || (uint32_t)(seqNow - s_lastForceReadSeq) >= 256U) {
const uint16_t nread = (nPdOutputSize > kPdoOutBytes)
? kPdoOutBytes
: nPdOutputSize;
uint8_t escOut[kEcatPdoOutBytes];
const uint16_t prefix = (nread < 52U) ? nread : 52U;
HW_EscRead(escOut, nEscAddrOutputData, prefix);
const uint8_t batchCount = (prefix > 48U) ? escOut[48] : 0U;
if (batchCount > 0U && nread > prefix) {
HW_EscRead(&escOut[prefix],
(uint16_t)(nEscAddrOutputData + prefix),
(uint16_t)(nread - prefix));
}
readOutputs(escOut);
s_lastForceReadSeq = seqNow;
}
}
s_outMappedThisCycle = false;
EthercatCanBridge::processOutput(s_out);
EthercatCanBridge::buildInput(build);
fillDiagTail(build);
__DMB();
s_inActiveIdx = nextIdx;
memcpy(s_in, build, kPdoInBytes);
/* 不再 memcpy 到 s_in:writeInputs/稀疏写/差分写都读 s_inBuf[activeIdx],
* 每拍多拷 600B 是纯浪费(8kHz 下不可接受)。 */
++s_processSeq;
#if CONFIG_ECAT_DIFF_STATS
/* [DIFF-STATS] 只测量:与上一份已发布镜像比对并累计到 1s 窗口,经 0x2000
* sub13..23 上报。不改变上面的整块写行为,因此本版本固件的输出与 1.6.16
* 逐字节一致。仪表自持上一份副本,这里不需要额外缓冲。 */
* sub13..23 上报。不改变上面的整块写行为。仪表自持上一份副本,这里不需要额外缓冲。 */
EthercatDiffStats::observe(s_inBuf[nextIdx]);
#endif
@@ -123,18 +156,19 @@ void EthercatPdo::process()
}
/**
* @brief 填充输入 PDO 段 5 设备诊断区(632..639,1.5.61)。
* @brief 填充输入 PDO 段 5 设备诊断区(632..639)。
* @param[out] in 全长输入过程数据缓冲(buildInput 刚清零+填完段 1~4)。
* @note 负载率 1s 滚动一个窗口,seq 随窗口递增——主站只认"seq 在变"的负载值。
* 每周期重写 6 字节比"值变化才写"的分支判断更便宜,也不引入隐藏状态。
*/
void EthercatPdo::fillDiagTail(uint8_t *in)
{
static uint16_t s_diagTick = 0U;
const uint16_t load = CpuLoad::loadPermille();
in[ECAT_DIAG_OFF_LOAD] = (uint8_t)(load & 0xFFU);
in[ECAT_DIAG_OFF_LOAD + 1] = (uint8_t)(load >> 8);
/* CAN 取帧撕裂丢帧数(1.6.6)。只有 16 位,饱和不回绕 —— 主站看的是
/* CAN 取帧撕裂丢帧数。只有 16 位,饱和不回绕 —— 主站看的是
* "有没有涨、涨多快",回绕会被误读成计数清零。 */
const CanFd::Diag *cd = CanFd::diag();
const uint32_t torn = (cd != nullptr) ? cd->rx_torn_count : 0U;
@@ -142,28 +176,28 @@ void EthercatPdo::fillDiagTail(uint8_t *in)
in[ECAT_DIAG_OFF_TORN] = (uint8_t)(torn16 & 0xFFU);
in[ECAT_DIAG_OFF_TORN + 1] = (uint8_t)(torn16 >> 8);
const uint32_t seq = CpuLoad::windowSeq();
const uint16_t seq16 = (uint16_t)seq; /* 1.6.12:u32 拆成 u16,高 2 字节让给
overfull 计数。主站只判"非 0 且在涨",
按 u32 读到 seq|overfull<<16 同样成立。 */
in[ECAT_DIAG_OFF_SEQ] = (uint8_t)(seq16 & 0xFFU);
in[ECAT_DIAG_OFF_SEQ + 1] = (uint8_t)(seq16 >> 8);
/* seq 写「本函数被调用次数」而不是 windowSeq。
* windowSeq 依赖 CpuLoad::tick() 滚窗;Process 若在 tick 前停摆,
* 主站会长期读到 0 且分不清「没跑」和「窗口未满」。自由计数每拍+1,
* 冻结看门狗与 app/load 的「seq 在涨」语义都更干净。 */
++s_diagTick;
in[ECAT_DIAG_OFF_SEQ] = (uint8_t)(s_diagTick & 0xFFU);
in[ECAT_DIAG_OFF_SEQ + 1] = (uint8_t)(s_diagTick >> 8);
/* CAN RX FIFO 满溢计数 RFO0(1.6.12 测量):1.6.11 改"满则丢新"后拼接未消失,
* 本计数与 reject@494 是否同涨,是"残余拼接是否仍源自溢出"的判决性观测。 */
/* CAN RX FIFO 满溢计数 RFO0:与内容级 reject 计数是否同涨,
* 是"残余拼接是否仍源自溢出"的观测指标。 */
const uint32_t of = (cd != nullptr) ? cd->rx_fifo_overfull_count : 0U;
const uint16_t of16 = (of > 0xFFFFU) ? 0xFFFFU : (uint16_t)of;
in[ECAT_DIAG_OFF_OVERFULL] = (uint8_t)(of16 & 0xFFU);
in[ECAT_DIAG_OFF_OVERFULL + 1] = (uint8_t)(of16 >> 8);
/* RFD0 释放握手超时计数(1.6.12,占用原 1.6.7 重入计数位 492——该计数引入
* 至今恒 0,转为纯内部)。dequeue 握手没完成 = 窗口/读指针失同步前兆。 */
/* RFD0 释放握手超时计数。dequeue 握手没完成 = 窗口/读指针失同步前兆。 */
const uint32_t rt = (cd != nullptr) ? cd->rx_release_timeout_count : 0U;
const uint16_t rt16 = (rt > 0xFFFFU) ? 0xFFFFU : (uint16_t)rt;
in[ECAT_DIAG_OFF_RELTIMEOUT] = (uint8_t)(rt16 & 0xFFU);
in[ECAT_DIAG_OFF_RELTIMEOUT + 1] = (uint8_t)(rt16 >> 8);
/* 内容级拼接帧拦截次数(1.6.11):MI/MP 校验挡不住的指针错位拼接由入队前
/* 内容级拼接帧拦截次数:MI/MP 校验挡不住的指针错位拼接由入队前
* 结构自洽校验兜底,语义同上 —— 主站看"涨不涨"判断拼接是否仍在发生。 */
const uint32_t rj = (cd != nullptr) ? cd->rx_content_reject_count : 0U;
const uint16_t rj16 = (rj > 0xFFFFU) ? 0xFFFFU : (uint16_t)rj;
@@ -191,7 +225,7 @@ bool EthercatPdo::sparseEscWritePlan(uint16_t *off0, uint16_t *len0,
#if !CONFIG_ECAT_SPARSE_ESC_WRITE
return false;
#else
/* 仅 1.6.21 密排布局:count 有效且两段之间确有空洞才分段。
/* 仅密排布局:count 有效且两段之间确有空洞才分段。
* magic 不对 / count 越界 / count 撑满到与段3相接 → 整包,避免陈旧空洞被误读。 */
const uint8_t activeIdx = s_inActiveIdx;
__DMB();
@@ -329,7 +363,7 @@ void EthercatPdo::snapshotOutput(uint8_t *dst)
extern "C" {
/* 1.6.0:输出探针,SSC PDI_Isr 的 PDO_OutputMapping 每周期调用(C 链接)。
/* 输出探针,SSC PDI_Isr 的 PDO_OutputMapping 每周期调用(C 链接)。
* 返回 0/1;SSC 侧声明为 UINT8,小返回值零扩展,调用约定兼容。 */
uint8_t EcatPdo_OutputProbe(const uint8_t *head)
{
@@ -341,6 +375,11 @@ void EcatPdo_ReadOutputs(const uint8_t *esc)
EthercatPdo::readOutputs(esc);
}
void EcatPdo_NoteOutputMapped(void)
{
s_outMappedThisCycle = true;
}
void EcatPdo_Process(void)
{
EthercatPdo::process();
@@ -351,7 +390,7 @@ void EcatPdo_WriteInputs(uint8_t *esc)
EthercatPdo::writeInputs(esc);
}
/* 1.6.22 稀疏写计划:SSC PDO_InputMapping 据此决定 HW_EscWriteIsr 写几段。
/* 稀疏写计划:SSC PDO_InputMapping 据此决定 HW_EscWriteIsr 写几段。
* 返回 1=两段有效;0=回退整包。 */
uint8_t EcatPdo_SparseEscPlan(uint16_t *off0, uint16_t *len0,
uint16_t *off1, uint16_t *len1)
@@ -359,7 +398,7 @@ uint8_t EcatPdo_SparseEscPlan(uint16_t *off0, uint16_t *len0,
return EthercatPdo::sparseEscWritePlan(off0, len0, off1, len1) ? 1U : 0U;
}
/* 1.6.25 差分写计划:SSC PDO_InputMapping 逐段 HW_EscWriteIsr。 */
/* 差分写计划: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
@@ -379,7 +418,7 @@ uint8_t EcatPdo_EscWritePlan(uint16_t *offs, uint16_t *lens, uint8_t maxSegs)
return n;
}
/* 1.6.34: HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
/* HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
uint32_t EscExmcBus_MaskEnter(void)
{
return EscExmcBus::maskEnter();
@@ -400,4 +439,9 @@ void HW_EscExmcMaskExit(uint32_t prev)
EscExmcBus::maskExit(prev);
}
uint32_t EcatPdo_ProcessSeq(void)
{
return EthercatPdo::processSeq();
}
} /* extern "C" */
+16 -8
View File
@@ -1,6 +1,6 @@
/**
* @file ethercat_pdo.h
* @brief EtherCAT 过程数据(PDO)收发模块(1.5.54 由 dc_app.c 的散装 C 代码类化)。
* @brief EtherCAT 过程数据(PDO)收发模块。
*
* 一个 EtherCAT 周期在 SSC 里被拆成三个回调,本类按这三步各提供一个入口:
* 1. APPL_OutputMapping() → readOutputs() 从 ESC 取走主站下发的输出数据
@@ -11,7 +11,7 @@
* 是 SDO 存储,必须留在 C 侧)与三行 extern "C" 转发。这样"收发"这件事只有一个
* 归属:想加新的过程数据语义只改本类,不必再动 SSC 移植层。
*
* @note 1.5.55 起输出/输入**长度不再相等**(64B / 384B),为电机控制台腾出
* @note 输出/输入**长度不相等**(64B / 384B),为电机控制台腾出
* 状态窗口空间。长度常量统一由 ethercat_can_bridge.h 提供,避免三处各写
* 一个魔数后失配。
*
@@ -49,11 +49,11 @@ public:
static void readOutputs(const uint8_t *esc);
/**
* @brief 输出探针(1.6.0 输出按需全量读,配合 SSC PDO_OutputMapping)。
* @brief 输出探针(输出按需全量读,配合 SSC PDO_OutputMapping)。
* @param[in] head 输出 PDO 头 12B(覆盖 SEQ@0 / WIN@8 / FLAGS@3)。
* @return true=需要全量读(seq 沿或清统计请求);false=本周期跳过全量读。
* @note 每周期由 PDI_Isr 在读 ESC 前调用:做 seq 基准 priming 与沿判定;
* s_lastSeq 的推进仍留在全量路径(processOutput)。1.6.21 起不再
* s_lastSeq 的推进仍留在全量路径(processOutput)。不再
* 维护 win_base(段 2 电机整轮密排,无滑窗)。
*/
static bool outputProbe(const uint8_t *head);
@@ -73,7 +73,7 @@ public:
static void writeInputs(uint8_t *esc);
/**
* @brief 稀疏写 ESC 的两段计划(1.6.22):段1+段2有效区、段3+段5;跳过空槽区。
* @brief 稀疏写 ESC 的两段计划:段1+段2有效区、段3+段5;跳过空槽区。
* @param[out] off0/len0 第一段相对 PDO 起点的偏移与长度
* @param[out] off1/len1 第二段
* @return true=可按两段写;false=应回退整包 600B(开关关/非密排/count 越界/两段相接)
@@ -83,7 +83,7 @@ public:
uint16_t *off1, uint16_t *len1);
/**
* @brief 输入 PDO 写 ESC 的多段计划(1.6.25 差分写;未启用时回退稀疏/整包)。
* @brief 输入 PDO 写 ESC 的多段差分计划(未启用时回退稀疏/整包)。
* @param[out] offs 段偏移数组,至少 kEscPlanMaxSegs 项
* @param[out] lens 段长度数组,至少 kEscPlanMaxSegs 项
* @return 段数 1..kEscPlanMaxSegs;调用方逐段 HW_EscWriteIsr。
@@ -102,6 +102,9 @@ public:
/** @brief 累计交回输入的周期数。 */
static uint32_t inputCycles() { return s_inputCycles; }
/** @brief process() 完整填完一拍输入的序号。新鲜度救援看这个,而不是
* InputMapping 的 s_inMapSeq——后者只证明「在拷陈旧 s_in」。 */
static uint32_t processSeq() { return s_processSeq; }
/** @brief process() 被重入拦下的次数。持续增长 = 跨上下文抢占确实在发生
* (PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 互相打断),也就证明了
* 批量区跨轮次拼接的成因;恒为 0 则说明该机制推断有误。 */
@@ -118,7 +121,7 @@ public:
private:
/**
* @brief 填充输入 PDO 段 5 设备诊断区(尾部 8 字节,1.5.61)。
* @brief 填充输入 PDO 段 5 设备诊断区(尾部 8 字节)。
* @param[out] in 全长输入过程数据缓冲。
* @note 布局常量 ECAT_DIAG_OFF_* 见 ethercat_can_bridge.h;当前携带
* CPU 负载率(0.1% 单位) + 窗口序号,取自 sys/cpu_load.h。
@@ -136,7 +139,7 @@ private:
* ECAT_Application() 会在 PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 四个
* 上下文被调用并互相抢占,直接写 s_in 会让读侧取到跨轮次的半成品。 */
alignas(4) static uint8_t s_build[kPdoInBytes];
/** @brief 写 ESC 差分影子(1.6.25):段1 / 段2 / 段3+5 三区。 */
/** @brief 写 ESC 差分影子:段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];
@@ -151,6 +154,7 @@ private:
static volatile uint32_t s_outputCycles;
static volatile uint32_t s_inputCycles;
static volatile uint32_t s_processSeq;
};
extern "C" {
@@ -160,6 +164,10 @@ void EcatPdo_ReadOutputs(const uint8_t *esc);
void EcatPdo_Process(void);
/** @brief 供 dc_app.c 调用:交回输入过程数据。 */
void EcatPdo_WriteInputs(uint8_t *esc);
/** @brief 供 dc_app.c 新鲜度救援:process() 是否还在推进。 */
uint32_t EcatPdo_ProcessSeq(void);
/** @brief PDO_OutputMapping 探针命中并完成全量读后调用(本拍 s_out 已新鲜)。 */
void EcatPdo_NoteOutputMapped(void);
}
#endif /* COMM_ETHERCAT_PDO_H */
+7 -14
View File
@@ -64,13 +64,12 @@ void EthercatSlave::init()
* (如 SM Watchdog=0x1B、FMMU 配置错、邮箱协议错),需主站介入。 */
/* ============================================================
* bring-up 诊断(1.5.35 引入,1.5.39 重新定性):经 CSR 间接端口读三个关键寄存器
* bring-up 诊断:经 CSR 间接端口读三个关键寄存器
* + 一次写回环,确认 EtherCAT 核心侧状态。
* 注意主站读身份/AL 走 ESC 以太网侧 + EEPROM(SII),完全不经过 GD32 的 PDI,
* 所以"主站能读到身份"不能证明下面任何一项成立。
* 1.5.39 起访问已改经 ECAT_CSR_CMD/CSR_DATA(EtherCAT 核心寄存器不在 HBI 直接
* 地址空间,DS 12.11);此前直读索引使本段恒读全 0,曾被误判为"EEPROM 加载竞态"
* 与"HBI 写路径损坏"两个错误结论。现在读到全 0 才真正指向 EEPROM/PDI 故障,
* 访问必须经 ECAT_CSR_CMD/CSR_DATA(EtherCAT 核心寄存器不在 HBI 直接
* 地址空间,DS 12.11);直读索引恒读全 0。
* 交叉验证用 tools/ecat/ecat_esc_regs.py 从网络侧读同样的寄存器。
* 本段在 EthercatSync::init() 使能 EXTI 之前执行,满足 initOnly* 的 INIT-ONLY 约束。
* ============================================================ */
@@ -87,9 +86,7 @@ void EthercatSlave::init()
/* (3) EEPROM 接口状态 0x0502-0x0503(16 位字)。位定义按 ETG.1000.4 表 49
* 与 SSC esc.h:bit11=Checksum Error、bit12=EEPROM loading status(0=OK)、
* bit13=Missing EEPROM Acknowledge、bit14=Write Error、bit15=Busy。
* 注意 1.5.40 及之前本行把 bit11 打成 "loaded",语义还正好反了
* (bit11=1 才是校验错),于是正常板子恒显示 "loaded=0",字面像"EEPROM 未加载",
* 正是历史上 1.5.37 那个已撤回的错误结论的诱因;现按位分别命名,0 全部代表正常。
* 0 全部代表正常。
* 真正的"EEPROM 已加载/PDI 可用"判据是 DL_STATUS(0x0110) bit0,见上方 (1)。 */
const uint16_t eepCsr = EscLan9252::initOnlyReadReg(0x0502U);
LOG_ERR("[ECAT][DIAG] DL_STATUS(0x110)=0x%04X pdi_oper=%u | "
@@ -109,10 +106,7 @@ void EthercatSlave::init()
* 就是 PDI 侧可写(PDI 用它选择哪些 AL Event 产生 IRQ),不像 0x0012 Station
* Alias 在部分 ESC 上对 PDI 只读(会造成假 FAIL);本段在 MainInit/HW_Init
* 之前执行,还原后不影响 SSC 后续建立的 mask 影子。
* 1.5.39 起本回环经 ECAT_CSR_CMD/CSR_DATA 间接端口执行。历史注意:1.5.35
* 该回环报 FAIL 曾被误判为"HBI 写路径损坏",真因是当时直读直写 HBI 索引——
* EtherCAT 核心寄存器不在 HBI 直接地址空间(DS 12.11),读恒 0、写被丢弃。
* 网络侧诊断(tools/ecat/ecat_esc_regs.py)已证实 ESC 本体与 EEPROM 一直正常。 */
* 回环必须经 ECAT_CSR_CMD/CSR_DATA 间接端口执行。 */
const uint16_t maskSaved = EscLan9252::initOnlyReadReg(0x0204U);
EscLan9252::initOnlyWriteReg(0x0204U, 0x0A50U);
const uint16_t maskRead = EscLan9252::initOnlyReadReg(0x0204U);
@@ -129,7 +123,7 @@ void EthercatSlave::init()
/* 是否有主站在线:DL_STATUS(0x0110) 的 Physical Link 位 bit4/5/6 对应 Port0/1/2
* (DS 12.14.19)。三个端口都无 link = 网线未插/无主站,此时 AL 状态机根本没人驱动
* (AL 迁移由主站写 AL Control 0x0120 触发,从站不自跳),停在 INIT 是唯一正确行为。
* 参考:接主站时实测 DL_STATUS=0x5611(Port0 有 link 有 comm);
* 参考:接主站时 DL_STATUS 通常为 0x5611(Port0 有 link 有 comm);
* 空载时为 0x5503(三口 loop closed、link/comm 全 0)。 */
const bool anyPhyLink = chipIdOk && ((dlStatus & 0x0070U) != 0U);
@@ -137,8 +131,7 @@ void EthercatSlave::init()
LOG_ERR("[ECAT][SLAVE] ESC NOT ready, sync ISR disabled (check EXMC wiring/timing)\r\n");
} else if (s_alState < AL_STATE_SAFEOP) {
/* 区分"等主站"与"真故障":两者 AL 都 < SAFEOP,但只有后者需要排查。
* 1.5.40 及之前一律打 FAIL(partial),而冷启动无主站时必然走到这里,
* 与紧随其后的 "SSC MainInit OK" 自相矛盾,极易被误读为初始化失败。 */
* 冷启动无主站时必然走到这里,报 WAIT_MASTER 而非 FAIL。 */
if (!anyPhyLink) {
LOG_ERR("[ECAT][SLAVE] WAIT_MASTER: no PHY link (DL_STATUS=0x%04X), "
"AL=0x%02X(%s) 由主站驱动,空载停在 INIT 属预期\r\n",
+1 -2
View File
@@ -1,8 +1,7 @@
/**
* @file ethercat_slave.h
* @brief EtherCAT 从站 init 探针与运行状态查询接口。
* @note 1.5.27:原先的 periodicIsr()/三缓冲 PDO 桥是死代码(无任何调用点),
* 运行期 PDO 交换实际由 SSC 协议栈完成 —— 周期路径为 Sync0_Isr →
* @note 运行期 PDO 交换由 SSC 协议栈完成 —— 周期路径为 Sync0_Isr →
* ECAT_Application → APPL_InputMapping/OutputMapping(comm/ssc/port/dc_app.c),
* 主循环路径为 MainLoop → ECAT_Main。本类仅保留 init 探针与诊断查询。
*/
+7 -9
View File
@@ -7,7 +7,7 @@
* SYNC1 (EXTI2, preempt=1) → Sync1_Isr() : 预留
*/
#include "comm/ethercat_sync.h"
#include "comm/esc_lan9252.h" /* pmSuspended:ESC 睡眠挂起期 ISR 拒碰 HBI(1.6.27) */
#include "comm/esc_lan9252.h" /* pmSuspended:ESC 睡眠挂起期 ISR 拒碰 HBI */
#include "sys/config.h"
#include "main.h" /* g_app_uptime_sec */
#include "sys/cpu_load.h"
@@ -115,7 +115,7 @@ void EthercatSync::onSync0Exti()
/* 直接寄存器访问,绕过 GD32 库函数以消除可变抖动 */
if ((EXTI_PD & (uint32_t)CONFIG_ESC_SYNC0_EXTI) != RESET) {
EXTI_PD = (uint32_t)CONFIG_ESC_SYNC0_EXTI;
/* 1.6.27:ESC 睡眠挂起期,清了 pending 就走——Sync0_Isr 内的 PRAM/CSR
/* ESC 睡眠挂起期,清了 pending 就走——Sync0_Isr 内的 PRAM/CSR
* 访问全是 HBI,读即唤醒。睡眠中 SYNC0 引脚本应无沿,此分支只兜
* 进入睡眠瞬间的残留 pending。 */
if (EscLan9252::pmSuspended()) {
@@ -130,7 +130,7 @@ void EthercatSync::onEscIrqExti()
{
if (exti_interrupt_flag_get(CONFIG_ESC_IRQ_EXTI) != RESET) {
exti_interrupt_flag_clear(CONFIG_ESC_IRQ_EXTI);
/* 1.6.27:ESC 睡眠挂起期直接返回——PDI_Isr 会读 0x220/PRAM(HBI),读即唤醒 */
/* ESC 睡眠挂起期直接返回——PDI_Isr 会读 0x220/PRAM(HBI),读即唤醒 */
if (EscLan9252::pmSuspended()) {
return;
}
@@ -141,9 +141,6 @@ void EthercatSync::onEscIrqExti()
* 代价是 PDI_Isr 剩余时长(µs 级)计入 SYNC0 抖动。 */
const uint32_t prevSync0 = NVIC_GetEnableIRQ(CONFIG_ESC_SYNC0_IRQn);
NVIC_DisableIRQ(CONFIG_ESC_SYNC0_IRQn);
/* 1.6.12 曾在此预抽 CAN FIFO(pumpAllChannels),实测溢出大降但拼接照旧
* —— 病理在 357µs 阻塞窗口本身,1.6.13 直接把 CAN RX 提到 preempt=0
* 根治(见 sys/config.h),预抽失去意义且引入 PDI/ISR 双消费者风险,撤除。 */
PDI_Isr();
if (prevSync0 != 0U) {
NVIC_EnableIRQ(CONFIG_ESC_SYNC0_IRQn);
@@ -155,7 +152,7 @@ void EthercatSync::onSync1Exti()
{
if (exti_interrupt_flag_get(CONFIG_ESC_SYNC1_EXTI) != RESET) {
exti_interrupt_flag_clear(CONFIG_ESC_SYNC1_EXTI);
/* 1.6.27:ESC 睡眠挂起期直接返回——Sync1_Isr 内为 HBI 访问 */
/* ESC 睡眠挂起期直接返回——Sync1_Isr 内为 HBI 访问 */
if (EscLan9252::pmSuspended()) {
return;
}
@@ -175,7 +172,7 @@ void EthercatSync::onTimerIrq()
#if (CONFIG_ETHERCAT_SYNC_MODE == 1)
if (timer_interrupt_flag_get(TIMER0, TIMER_INT_FLAG_UP) != RESET) {
timer_interrupt_flag_clear(TIMER0, TIMER_INT_FLAG_UP);
/* 1.6.27:ESC 睡眠挂起期跳过——Sync0_Isr 内为 HBI 访问 */
/* ESC 睡眠挂起期跳过——Sync0_Isr 内为 HBI 访问 */
if (!EscLan9252::pmSuspended()) {
Sync0_Isr();
}
@@ -214,8 +211,9 @@ uint32_t EcatDbg_GetSync0(void) { return EthercatSync::getSync0Count(); }
uint32_t EcatDbg_GetSync1(void) { return EthercatSync::getSync1Count(); }
uint32_t EcatDbg_GetEscIrq(void) { return EthercatSync::getEscIrqCount(); }
uint32_t EcatDbg_GetUptimeSec(void){ return g_app_uptime_sec; }
uint32_t EcatDbg_GetTick1ms(void) { return g_scheduler.tick_1ms; }
/* 1.6.0 剖分埋点:供 ecatappl.c (C) 读取 DWT 周期计数 */
/* 剖分埋点:供 ecatappl.c (C) 读取 DWT 周期计数 */
uint32_t EcatDbg_Dwt(void) { return DWT->CYCCNT; }
} /* extern "C" */
+80 -115
View File
@@ -11,10 +11,7 @@
* 0x6000 Input Data (从站→主站, 384 bytes)
* 0x2000 SYNC Diag (只读: sync0/sync1/escirq/uptime, 主站 SDO 轮询看 DC 通路)
*
* 1.5.55:为电机控制台把过程数据从 32B/32B 扩到 64B/384B。
* 0x7000/0x6000/0x1600/0x1A00 由 OBJCODE_REC 改为 OBJCODE_ARR —— 关键原因见
* 下方 Entry Descriptions 注释:REC 会逐下标索引 pEntryDesc[Subindex],48 个
* 条目就得写 49 项描述符;ARR 由 OBJ_GetEntryDesc 复用 pEntryDesc[1],只需 2 项。
* 0x7000/0x6000/0x1600/0x1A00 为 OBJCODE_ARR(原因见下方 Entry Descriptions 注释)。
*/
#include "ecat_def.h"
@@ -27,7 +24,11 @@
#include <string.h>
#include <stddef.h> /* offsetof:下方布局守卫断言使用 */
#include <stdio.h> /* printf:0x204 守护 WARN 日志(1.5.48) */
#include <stdio.h> /* printf:0x204 守护 WARN 日志 */
/* CPU 负载挂钩(sys/cpu_load.cpp,与 /api/app/load 同口径) */
extern void CpuLoad_Enter(void);
extern void CpuLoad_Leave(void);
/*---------------------------------------------------------------------------
* el9800appl.h 桩 include (SSC 内部需要)
@@ -39,18 +40,13 @@
/*---------------------------------------------------------------------------
* PDO 数据缓冲
*
* 1.5.56 扩到 DPRAM 硬上限:输出 48×64bit=384B,输入 80×64bit=640B(非对称)。
* 1.6.1 输入瘦身 640→496(62×64bit):批量接收条目数据 64→32B + 诊断区前移,
* 依据与推导见 comm/ethercat_pdo_size.h 的 1.6.1 注。
* 1.6.15 输入 496→600(75×64bit):批量接收改定长覆盖窗口 5 槽(每槽带 u32
* seqlock,槽长 44B),诊断区随之后移到 584。推导见该头文件的 1.6.15 注。
* 长度的唯一真源是 comm/ethercat_pdo_size.h;本文件用 _Static_assert 与之绑定,
* 改一处漏一处会编译失败。为何硬上限是 out+in≤1024B 见该头文件的推导。
*---------------------------------------------------------------------------*/
/** 输出/输入 PDO 的 64bit 条目数。 */
#define PDO_OUT_ENTRIES 48u /* 48×8 = 384 B */
#define PDO_IN_ENTRIES 75u /* 75×8 = 600 B (1.6.15 覆盖窗口,原 62×8=496) */
#define PDO_IN_ENTRIES 75u /* 75×8 = 600 B (覆盖窗口) */
/** 输出数据对象 0x7000 (主站→从站, RxPDO) */
typedef struct OBJ_STRUCT_PACKED_START {
@@ -69,7 +65,7 @@ TOBJ6000;
static TOBJ7000 s_outputObj = {PDO_OUT_ENTRIES, {0}};
static TOBJ6000 s_inputObj = {PDO_IN_ENTRIES, {0}};
/* 布局守卫(1.5.42):SSC 的 SDO 单条目访问按"si0 占 16bit,其后按 BitLength 紧排"
/* 布局守卫:SSC 的 SDO 单条目访问按"si0 占 16bit,其后按 BitLength 紧排"
* 计算偏移(OBJ_GetEntryOffset),故 UINT64 数组必须紧跟在 UINT16 之后落在字节 2。
* OBJ_STRUCT_PACKED_START 曾被留空导致其落在字节 8、SDO 读写错位,加断言防回归。 */
_Static_assert(offsetof(TOBJ7000, u64Data) == 2, "TOBJ7000 未紧排:检查 OBJ_STRUCT_PACKED_START");
@@ -98,7 +94,7 @@ typedef struct OBJ_STRUCT_PACKED_START {
UINT32 u32Sync1; /* sub2 */
UINT32 u32EscIrq; /* sub3 */
UINT32 u32UptimeS; /* sub4 */
/* 1.6.0 剖分埋点(sub5..12,DWT 周期/计数累加,主站 1s 差分;定位完即移除) */
/* 剖分埋点(sub5..12,DWT 周期/计数累加,主站 1s 差分) */
UINT32 u32PdiCyc; /* sub5: PDI_Isr 全函数累计 */
UINT32 u32OutCyc; /* sub6: PDO_OutputMapping(输出探针/全量读) */
UINT32 u32FreeRunCyc; /* sub7: MainLoop free-run 块(SM 同步下恒 0) */
@@ -118,14 +114,15 @@ typedef struct OBJ_STRUCT_PACKED_START {
UINT32 u32DsSeg1; /* sub18: 段1(0..31) 桥状态+短快照 变化字节合计 */
UINT32 u32DsSeg2; /* sub19: 段2(32..543) 电机整轮密排 */
UINT32 u32DsSeg3; /* sub20: 段3(544..583) 长帧快照 */
UINT32 u32DsSeg4; /* sub21: 已删(1.6.21 恒 0) */
UINT32 u32DsSeg4; /* sub21: 已删(恒 0) */
UINT32 u32DsSeg5; /* sub22: 段5(584..599) 诊断区+保留 */
UINT32 u32DsCompareCyc; /* sub23: 比对自身累计 DWT 周期(u32 粒度,不含快照拷贝) */
UINT32 u32OpFreshRescues; /* sub24: OP 输入新鲜度救援次数(主站网页看,不打串口) */
} OBJ_STRUCT_PACKED_END
TOBJ2000;
static TOBJ2000 s_syncDiag = {23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
static TOBJ2000 s_syncDiag = {24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
/* 同上:UINT32 首条目必须落在字节 2(未紧排时为 4)。 */
_Static_assert(offsetof(TOBJ2000, u32Sync0) == 2, "TOBJ2000 未紧排:检查 OBJ_STRUCT_PACKED_START");
@@ -135,7 +132,7 @@ extern UINT32 EcatDbg_GetSync0(void);
extern UINT32 EcatDbg_GetSync1(void);
extern UINT32 EcatDbg_GetEscIrq(void);
extern UINT32 EcatDbg_GetUptimeSec(void);
/* 1.6.0 剖分埋点:ecatappl.c 累加器 + ethercat_sync.cpp 的 DWT 读 */
/* 剖分埋点:ecatappl.c 累加器 + ethercat_sync.cpp 的 DWT 读 */
extern UINT32 EcatDbg_Dwt(void);
extern volatile UINT32 g_dbg_pdiCyc;
extern volatile UINT32 g_dbg_outCyc;
@@ -149,7 +146,7 @@ extern volatile UINT32 g_dbg_appCyc;
/* [DIFF-STATS] 差分写统计窗口值(comm/ethercat_diff_stats.cpp),out[0..10] 对应 sub13..23 */
void EcatDiffStats_GetWindow(UINT32 *out);
/* 1.6.33:OP 输入新鲜度 — APPL_InputMapping 每次成功刷新就自增 */
/* OP 输入新鲜度 — APPL_InputMapping 每次成功刷新就自增 */
static volatile UINT32 s_inMapSeq = 0U;
static UINT32 s_inMapSeqSeen = 0U;
static UINT32 s_inStaleBaseCyc = 0U;
@@ -159,11 +156,11 @@ static UINT32 s_opFreshRescues = 0U;
* 不能用 UINT32 重复声明。 */
extern uint32_t SystemCoreClock;
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim, 1.5.54)。
* 1.5.53 时这里直接调 EcatCan_* 转发桥;1.5.54 把"一个 EtherCAT 周期怎么收发"
* 收拢进 EthercatPdo 类,本文件退回纯 SSC 移植层:只留对象字典 + 三行转发。 */
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim)。
* 本文件为纯 SSC 移植层:只留对象字典 + 三行转发。 */
extern void EcatPdo_ReadOutputs(const UINT8 *esc);
extern void EcatPdo_Process(void);
extern uint32_t EcatPdo_ProcessSeq(void);
extern void EcatPdo_WriteInputs(UINT8 *esc);
/* 0x2502 用:进 BOOT 的异步请求(sys/ota_boot_request.cpp 的 extern "C" shim)。
@@ -216,7 +213,7 @@ static TOBJ_RXPDO_MAPPING sRxPDOMap = {PDO_OUT_ENTRIES, {
PDO_MAP_8(0x7000, 24), PDO_MAP_8(0x7000, 32), PDO_MAP_8(0x7000, 40)
}};
/* 0x1A00: 映射 0x6000:01..75, 每个 64 bits(1.6.15 覆盖窗口 62→75:9×8 + 3) */
/* 0x1A00: 映射 0x6000:01..75, 每个 64 bits(9×8 + 3) */
static TOBJ_TXPDO_MAPPING sTxPDOMap = {PDO_IN_ENTRIES, {
PDO_MAP_8(0x6000, 0), PDO_MAP_8(0x6000, 8), PDO_MAP_8(0x6000, 16),
PDO_MAP_8(0x6000, 24), PDO_MAP_8(0x6000, 32), PDO_MAP_8(0x6000, 40),
@@ -231,16 +228,11 @@ _Static_assert(sizeof(sTxPDOMap.aEntries) / 4 == PDO_IN_ENTRIES, "0x1A00 条目
/*---------------------------------------------------------------------------
* Entry Descriptions
*
* 重要(1.5.42 修正): OBJ_GetEntryDesc()(objdef.c:554) 只对 OBJCODE_ARR 复用
* OBJ_GetEntryDesc()(objdef.c:554) 只对 OBJCODE_ARR 复用
* pEntryDesc[1] 描述所有子索引; 对 OBJCODE_REC 是逐下标索引 pEntryDesc[Subindex]。
* 因此 REC 对象的描述数组必须给满(Sub0..SubN 共 N+1 项), 否则 SDO Info 越界读到
* 相邻常量数组的垃圾, 主站 config_map 报 "Unsupported access to an object" /
* "Attempt to read to a write only object"(实测 0x1600/0x1A00 的 sub0/3/4 报错
* 而 sub1/2 侥幸通过, 正是越界特征)。
*
* 1.5.55: 输入 PDO 扩到 48 条目后, 继续用 REC 就得写 49 项描述符(且 0x1600/0x1A00
* 与 0x7000/0x6000 条目数不同, 不能再共用一个数组)。这些条目类型完全一致, 正是
* ARR 的语义, 故统一改为 OBJCODE_ARR —— 描述数组固定 2 项, 由 SSC 复用 [1]。
* 相邻常量数组的垃圾。
* 条目类型完全一致时用 OBJCODE_ARR —— 描述数组固定 2 项, 由 SSC 复用 [1]。
* 注意: 可被 PDO 映射的数据子条目必须带 OBJACCESS_RxPDOMAPPING/TxPDOMAPPING,
* 否则主站校验 0x1600/0x1A00 的映射目标时会拒绝(官方 sampleappl.h 同此)。
*---------------------------------------------------------------------------*/
@@ -270,13 +262,13 @@ OBJCONST TSDOINFOENTRYDESC OBJMEM asInputEntryDesc[] = {
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}
};
/* SYNC 诊断 0x2000: REC, sub0..sub23 共 24 项, 纯只读不参与 PDO 映射,
* 改子索引数无需重刷 SII/EEPROM。sub5..12 为 1.6.0 剖分埋点(DWT 周期/计数
* 累加,定位负载构成后移除);sub13..23 为 [DIFF-STATS] 差分写取证仪表。
/* SYNC 诊断 0x2000: REC, sub0..sub24 共 25 项, 纯只读不参与 PDO 映射,
* 改子索引数无需重刷 SII/EEPROM。sub5..12 为剖分埋点(DWT 周期/计数累加);sub13..23 为 [DIFF-STATS] 差分写取证仪表;
* sub24 = OP 输入新鲜度救援次数(不打串口,网页 /api/sync_diag 读)。
* 行数必须与 TOBJ2000 字段一一对应:少一行不会编译报错,只会让 SDO 从错误的
* 偏移取值 —— 故逐行标号便于核对。 */
OBJCONST TSDOINFOENTRYDESC OBJMEM asSyncDiagEntryDesc[] = {
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* sub0 : 子索引个数 = 23 */
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* sub0 : 子索引个数 = 24 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub1 : SYNC0 计数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub2 : SYNC1 计数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub3 : ESC IRQ 计数 */
@@ -300,6 +292,7 @@ OBJCONST TSDOINFOENTRYDESC OBJMEM asSyncDiagEntryDesc[] = {
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub21 : [DIFF-STATS] 段4 变化字节 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub22 : [DIFF-STATS] 段5 变化字节 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub23 : [DIFF-STATS] 比对自身 DWT 周期 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub24 : OP 输入新鲜度救援次数 */
};
/*---------------------------------------------------------------------------
@@ -394,8 +387,8 @@ TOBJECT OBJMEM ApplicationObjDicList[] = {
{NULL, NULL, 0x6000, {DEFTYPE_UNSIGNED64, PDO_IN_ENTRIES | (OBJCODE_ARR << 8)},
asInputEntryDesc, aName0x6000, &s_inputObj, NULL, NULL, 0x0000},
/* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路; sub5..8 为 1.6.0 剖分埋点) */
{NULL, NULL, 0x2000, {DEFTYPE_RECORD, 23 | (OBJCODE_REC << 8)},
/* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路) */
{NULL, NULL, 0x2000, {DEFTYPE_RECORD, 24 | (OBJCODE_REC << 8)},
asSyncDiagEntryDesc, aName0x2000, &s_syncDiag, NULL, NULL, 0x0000},
/* 0x2502: 引导程序交接请求(写 1 = 回 BOOT 等灌写;见 ObjBootReqWrite 注) */
@@ -413,7 +406,7 @@ TOBJECT OBJMEM * VARMEM ApplicationObjDic = ApplicationObjDicList;
*---------------------------------------------------------------------------*/
UINT16 APPL_GenerateMapping(UINT16 *pInputSize, UINT16 *pOutputSize)
{
*pInputSize = ECAT_PDO_IN_BYTES; /* 75×UINT64 = 600 bytes (1.6.15) */
*pInputSize = ECAT_PDO_IN_BYTES; /* 75×UINT64 = 600 bytes */
*pOutputSize = ECAT_PDO_OUT_BYTES; /* 48×UINT64 = 384 bytes */
return 0;
}
@@ -429,12 +422,12 @@ UINT16 APPL_StopMailboxHandler(void) { return 0; }
*---------------------------------------------------------------------------*/
UINT16 APPL_StartInputHandler(UINT16 *pIntMask)
{
/* 1.5.44:此处**不能**覆写 *pIntMask。
/* 此处**不能**覆写 *pIntMask。
*
* pIntMask 指向 SSC 全局 u16ALEventMask,StartInputHandler() 已按同步模式算好:
* free-run + 有输出 PDO 时它是 PROCESS_OUTPUT_EVENT(0x0400)(ecatslv.c:959/1090),
* 即 SM2 收到输出这一事件才是 SM-Synchron 模式的同步节拍。
* 旧代码写死 0x0800(PROCESS_INPUT_EVENT) 把 0x0400 顶掉了,而随后的
* 若写死 0x0800(PROCESS_INPUT_EVENT) 会把 0x0400 顶掉,而随后的
* SetALEventMask() 只做“或”,于是寄存器 0x204 永远缺 SM2 位:
* 输出到达不再产生 IRQ,PDI_Isr 只能被主站“读输入”事件带起来,
* 输入刷新因此永远落在主站读之后 —— SM3 是 3-buffer 缓冲模式,
@@ -482,7 +475,7 @@ void APPL_InputMapping(UINT16 *pInputData)
s_syncDiag.u32Sync1 = EcatDbg_GetSync1();
s_syncDiag.u32EscIrq = EcatDbg_GetEscIrq();
s_syncDiag.u32UptimeS = EcatDbg_GetUptimeSec();
/* 1.6.0 剖分埋点快照(sub5..12) */
/* 剖分埋点快照(sub5..12) */
s_syncDiag.u32PdiCyc = g_dbg_pdiCyc;
s_syncDiag.u32OutCyc = g_dbg_outCyc;
s_syncDiag.u32FreeRunCyc = g_dbg_freerunCyc;
@@ -491,6 +484,7 @@ void APPL_InputMapping(UINT16 *pInputData)
s_syncDiag.u32InWriteCyc = g_dbg_inWriteCyc;
s_syncDiag.u32AlCyc = g_dbg_alCyc;
s_syncDiag.u32AppCyc = g_dbg_appCyc;
s_syncDiag.u32OpFreshRescues = s_opFreshRescues;
/* [DIFF-STATS] 差分写统计快照(sub13..23):一次取全 11 个窗口值,
* 取的都是"上一个已完成的 1s 窗口",故 SDO 读不会拿到半累积值。 */
@@ -510,12 +504,12 @@ void APPL_InputMapping(UINT16 *pInputData)
s_syncDiag.u32DsCompareCyc = ds[10];
}
/* 1.5.54:先让 EthercatPdo 把本周期的输入数据写进 0x6000 的对象存储,
/* 先让 EthercatPdo 把本周期的输入数据写进 0x6000 的对象存储,
* 再交给 ESC。经由对象存储中转(而非直接写 pInputData)是为了让主站
* SDO 读 0x6000 时看到与过程数据一致的内容。 */
EcatPdo_WriteInputs((UINT8 *)&s_inputObj.u64Data[0]);
memcpy(pInputData, &s_inputObj.u64Data[0], ECAT_PDO_IN_BYTES);
/* 1.6.33:OP 输入新鲜度不变量的活性计数。每次真正走到输入映射就自增,
/* OP 输入新鲜度不变量的活性计数。每次真正走到输入映射就自增,
* 供 EcatDbg_OpInputFreshness 判断 PDI 路径是否还活着。 */
++s_inMapSeq;
}
@@ -534,11 +528,9 @@ void APPL_OutputMapping(UINT16 *pOutputData)
*---------------------------------------------------------------------------*/
void APPL_Application(void)
{
/* 1.5.54:应用逻辑全部搬进 EthercatPdo::process()(当前实现是 PDO↔CAN 转发)。
*
* 原 Phase 2 的 32B 回环已被取代:回环的验证价值由输入 PDO 的 seq 回显
* (ECAT_CAN_RX_OFF_ACK)承担——主站写 seq=N 后应在下一周期读回 ack=N,
* 同样能证明 输出→应用→输入 整条过程数据链路通。 */
/* 应用逻辑在 EthercatPdo::process()(当前实现是 PDO↔CAN 转发)。
* 不 CpuLoad 包裹:SM/DC 下本函数在 PDI/Sync0 ISR 内已被 isrEnter/Exit
* 计入;free-run 下若主循环每圈调用,再包会抬高 load_perm。 */
EcatPdo_Process();
}
@@ -547,11 +539,9 @@ void APPL_Application(void)
* [19:16]=AL 状态(0x0130 低 4 位) [31:24]=AL 错误码(0x0130 高 8 位)
* [15:0] =AL 事件寄存器(0x0220, bit0=主站 AL 控制请求待处理)
*
* 1.5.59 字节序修正:ESC 寄存器经 PDI/CSR 读出即主机序(LAN9252 CSR 引擎按
* ESC 寄存器经 PDI/CSR 读出即主机序(LAN9252 CSR 引擎按
* 16-bit 主机总线给出寄存器"值";线上大端只存在于网络侧与 PRAM 原始帧数据),
* 与 SSC 自身 GetSyncMan/mailbox 的裸读约定一致。1.5.48~1.5.58 这里误加了
* SWAPWORD,心跳打印的 al/evt 全是字节交换后的错值——那段时期的串口日志
* (如 evt=0x0400)需按字节交换重新解读后才能当证据用。
* 与 SSC 自身 GetSyncMan/mailbox 的裸读约定一致,勿加 SWAPWORD。
*---------------------------------------------------------------------------*/
UINT32 EcatDbg_ReadAlDiag(void)
{
@@ -561,41 +551,29 @@ UINT32 EcatDbg_ReadAlDiag(void)
}
/*---------------------------------------------------------------------------
* AL Event Mask (0x204) 守护 + SM2 事件楔死自愈 — 1.5.48 新增, 1.5.49/1.5.60 扩展
* AL Event Mask (0x204) 守护 + SM2 事件楔死自愈
* (C 链接, task_runner 250ms 节流调用)
*
* 背景:实测"停止回 INIT 后重进 OP"概率性失败——主站 LRW WKC=3/3(输出
* 确实写进 SM2),但从站 9s ESM 超时报 0x001B:bEcatFirstOutputsReceived 一直
* 没被置位。该标志只在 PDI_Isr 收到 SM2 事件(AL Event 0x220 bit10)时置位。
* 01:03 串口日志钉死现场:evt=0x0400 在 0x220 挂起 10s 无人处理(PDI_Isr 的
* SM2 分支从未执行),escirq 2.6/s 证明 EXTI 只在 AL Control 事件时跳动——
* 即 PDI_Isr 在跑但 SM2 分支被某个内部旗标挡住(bEscIntEnabled 门控)。
*
* 守护(1)0x204 缺位自愈:从站 AL>=SAFEOP 且有输出 PDO 时,SSC 不变式是
* "0x204 ⊇ u16ALEventMask"。读实际值比对,缺位则补写并打 WARN。
* 1.5.60 加固:期望值 u16ALEventMask 本身也可能缺 SM2 位——下行转换路径
* (ecatslv.c AL_ControlInd 尾部)同时清 0x204 的 PDO 事件位、bEscIntEnabled、
* bEcatFirstOutputsReceived,恢复全靠 SAFEOP 重入的 StartInputHandler +
* SetALEventMask(ecatslv.c:1506);重入没走全时 0x204 丢位。旧判定只比
* "actual ⊇ 期望值",而期望值变量在下行路径并不清零——若错的是期望值本身,
* 比对恒通过、缺位永远补不回。SM-Synchron+有输出 PDO 的不变式是
* u16ALEventMask ⊇ PROCESS_OUTPUT_EVENT(ecatslv.c:959/1086),先补期望再比对。
* 先补期望值再比对——下行转换路径(ecatslv.c AL_ControlInd 尾部)同时清
* 0x204 的 PDO 事件位、bEscIntEnabled、bEcatFirstOutputsReceived;若期望值
* 本身缺 SM2 位,旧判定恒通过、缺位永远补不回。SM-Synchron+有输出 PDO 的
* 不变式是 u16ALEventMask ⊇ PROCESS_OUTPUT_EVENT(ecatslv.c:959/1086)。
*
* 自愈(2)SM2 事件楔死(1.5.49):SAFEOP 下 0x220 bit10 挂起 且 标志未置位
* 自愈(2)SM2 事件楔死:SAFEOP 下 0x220 bit10 挂起 且 标志未置位
* = PDI_Isr 通路楔死。从主循环直接完成 PDI_Isr 该做的事:置位
* bEcatFirstOutputsReceived + 读 SM2 首/末字应答事件(ecatappl.c:317-320 同款)。
* 协议等价:bit10 挂起 = 主站已写过 SM2 = "收到输出"成立;AL_ControlRes 下一个
* MainLoop 周期即完成 SAFEOP→OP。WARN 附带全部内部旗标(bEscInt/bDcSync/
* 期望与实际 mask/看门狗状态),下次复现一行日志即可定位楔死路径。
* 协议等价:bit10 挂起 = 主站已写过 SM2 = "收到输出"成立。
* WARN 附带全部内部旗标(bEscInt/bDcSync/期望与实际 mask/看门狗状态),
* 下次复现一行日志即可定位楔死路径。
*
* 自愈(3)OP 态 PDI_Isr 门控(1.5.60):AL==OP 且 bEscIntEnabled==FALSE 不可
* 自愈(3)OP 态 PDI_Isr 门控:AL==OP 且 bEscIntEnabled==FALSE 不可
* 合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改 AL 状态;
* 守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即重入路径漏了恢复段。
* 此时 PDI_Isr 整体被门控(ecatappl.c:282),PDO_OutputMapping 停调、输入冻结
* ——主站侧表现即楔死看门狗盯的窗口回显冻结(2026-09-05 实测 250µs 档每
* 20~35s 触发一次重拉,wr 两小时涨 38)。修法:重开门 + 应答挂起的 SM2 事件
* (掩码补位由守护(1)在同次调用完成),下一帧 LRW 的 SM2 中断即恢复整条
* 过程数据链路,主站看门狗(2s 阈值)从此摸不到触发条件。
* 此时 PDI_Isr 整体被门控(ecatappl.c:282),PDO_OutputMapping 停调、输入冻结。
* 修法:重开门 + 应答挂起的 SM2 事件(掩码补位由守护(1)在同次调用完成),
* 下一帧 LRW 的 SM2 中断即恢复整条过程数据链路。
*---------------------------------------------------------------------------*/
UINT32 EcatDbg_GuardAleventMask(void)
{
@@ -613,23 +591,11 @@ UINT32 EcatDbg_GuardAleventMask(void)
return 0xFFFFU; /* 状态不适用:返回哨兵值,调用方不打印 */
}
/* (0) 邮箱 SM 地址/长度失配兜底(1.5.52 引入,1.5.59 修复字节序并降级为兜底):
*
* 历史楔死(1.5.51 及更早,断电才清)的实锤证据链:
* - ESC/SSC 邮箱处理全程活着(注入合法 CoE 请求后 200ms 内 SM1 出响应);
* - 但响应帧头出现在 SM1 start+8 而非 start(且逐会话累积:+4→+8,断电才清零);
* - 网络侧(FPRD)读 SM0/SM1 寄存器 start 恒正确(0x1000/0x1200);
* => SSC 缓存的 u16EscAddrSendMbx(Init→PreOP 时经 GetSyncMan/CSR 间接读)拿到了错值,
* 主站按协议从 0x1200 读 → 帧头永远错位 → SOEM 报 WkcError/MailboxError。
* 1.5.59 根治:mailbox.c 的 MBX_StartMailboxHandler 改用构建期常量直达
* (ecat_def.h:MBX_SM0/1_START/SIZE),CSR 错值从此没有进入缓存的路径——
* 楔死无建立点,本守护降级为"运行期内存踩踏"的最后兜底:AL>=PREOP 时用
* 寄存器真值校验 SSC 缓存的 start/len,失配即写回 + WARN。
*
* ★ 1.5.59 字节序修复:ESC 寄存器经 PDI/CSR 读出即主机序(与 SSC 自身
* GetSyncMan/mailbox 路径一致)。1.5.52~1.5.58 这里误加了 SWAPWORD——
* sm0Len 被换成 0x0001 < 8,下面的失配判定恒短路,守护自上线起从未真正
* 执行过("自愈"名存实亡,楔死实际靠重枚举重读碰运气恢复)。 */
/* (0) 邮箱 SM 地址/长度失配兜底:
* 邮箱几何构建期常量化(mailbox.c 直达 ecat_def.h:MBX_SM0/1_START/SIZE),
* CSR 错值无进入缓存的路径;本守护为运行期内存踩踏的最后兜底:
* AL>=PREOP 时用寄存器真值校验 SSC 缓存的 start/len,失配即写回 + WARN。
* ESC 寄存器经 PDI/CSR 读出即主机序,勿加 SWAPWORD。 */
if (al >= 2U && bMbxRunning) {
UINT16 sm0Start = 0U, sm0Len = 0U, sm1Start = 0U, sm1Len = 0U;
HW_EscReadWord(sm0Start, ESC_SYNCMAN_REG_OFFSET);
@@ -656,13 +622,11 @@ UINT32 EcatDbg_GuardAleventMask(void)
return 0xFFFFU; /* 以下仅 SAFEOP+ 适用 */
}
/* 1.5.59:PDI 寄存器读即主机序,不再 SWAPWORD——1.5.48~1.5.58 交换出的
* evt/actual 是错值,0x204 缺位判定恒真(每 250ms 盲写一次 mask,值凑巧
* 正确故未暴露),SM2 事件判定则对在错位上。详见上方字节序修复说明。 */
/* PDI 寄存器读即主机序,勿加 SWAPWORD。 */
evt = HW_GetALEventRegister();
HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET);
/* (1) 0x204 缺位自愈(1.5.60 加固:先补期望值,再比对补实际值) */
/* (1) 0x204 缺位自愈:先补期望值,再比对补实际值 */
if ((u16ALEventMask & PROCESS_OUTPUT_EVENT) == 0U) {
u16ALEventMask |= PROCESS_OUTPUT_EVENT;
printf("[ECAT][AEM] WARN: expected AL mask lacked SM2 bit -> 0x%04X\r\n",
@@ -692,11 +656,11 @@ UINT32 EcatDbg_GuardAleventMask(void)
(unsigned)u16ALEventMask, (unsigned)actual, (unsigned)wdState);
}
/* (3) OP 态 PDI_Isr 门控自愈(1.5.60 新增):OP 下 bEscIntEnabled==FALSE
/* (3) OP 态 PDI_Isr 门控自愈:OP 下 bEscIntEnabled==FALSE
* 不可合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改
* AL 状态;守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即 SAFEOP
* 重入路径漏了恢复段。此时 PDI_Isr 整体被门控(ecatappl.c:282),
* PDO_OutputMapping 停调、输入冻结,只能靠主站看门狗重拉总线。
* PDO_OutputMapping 停调、输入冻结。
* 修法:重开门 + 应答挂起的 SM2 事件(ISR 非 OP 分支同款读首/末字);
* 0x204 掩码补位已由上面(1)在同次调用完成,下一帧 LRW 即恢复。 */
if (al == 8U /* OP */ && nPdOutputSize > 0U && bEscIntEnabled == FALSE) {
@@ -715,18 +679,16 @@ UINT32 EcatDbg_GuardAleventMask(void)
}
/*---------------------------------------------------------------------------
* OP 输入新鲜度不变量 — 1.6.33
* OP 输入新鲜度不变量
*
* 结构洞(用户实锤:1ms 也复现,复位 GD32 APP 才好):
* SM-Synchron 稳态下 MainLoop 的 free-run 分支被
* (!bEscIntEnabled || !bEcatFirstOutputsReceived) && !bDcSyncActive
* 整段跳过,输入 PDO **只**由 PDI_Isr 的 PDO_InputMapping 刷新。
* PDI 通路一停(门控/掩码/EXTI),ESC 仍答 WKC、AL 仍 OP,主站读到的是
* 冻住的旧缓冲 —— 网页绿灯+高 Hz、CPU/CAN 全像没解析。既有 1.5.60 只救
* "门关着",救不了"门开着但 EXTI 不来"。
* 冻住的旧缓冲。
*
* 根治:把「OP ⇒ 输入必须在 T ms 内被重写」做成不变量。
* - 正常路径:PDI_Isr 每拍自增 s_inMapSeq
* 不变量:「OP ⇒ 输入必须在 T ms 内被重写」。
* - 正常路径:Process 每拍自增 seq
* - 异常路径:seq 停涨超过阈值 → 主循环强制 PDO_InputMapping
* (屏中断防双写)+ 重开门/应答 SM2,主站侧 gseq/rx 立刻恢复前进
* 不依赖主站重拉、不依赖人工复位。MainLoop 若本身死锁则本函数也停 ——
@@ -736,7 +698,9 @@ void EcatDbg_OpInputFreshness(void)
{
const UINT8 al = (UINT8)(nAlStatus & 0x0FU);
const UINT32 now = EcatDbg_Dwt();
const UINT32 seq = s_inMapSeq;
/* 活性看 processSeq(Process 真填过输入),不能看 s_inMapSeq。
* InputMapping 可在 Process 停摆时继续拷陈旧 s_in,seq 照涨 → 救援永不触发。 */
const UINT32 seq = EcatPdo_ProcessSeq();
if (al != 8U || nPdInputSize == 0U) {
s_inMapSeqSeen = seq;
@@ -759,7 +723,9 @@ void EcatDbg_OpInputFreshness(void)
}
/* 强制刷新:先重开门/首输出标志(门关着时 PDI 永远不会回来),
* 再在 EXMC 互斥窗口(仅屏蔽 EXTI0/1/2)内做一次输入映射,保证不关全局中断(Preempt 0 CAN RX 不受影响)。 */
* 再在 EXMC 互斥窗口(仅屏蔽 EXTI0/1/2)内做一次输入映射,保证不关全局中断(Preempt 0 CAN RX 不受影响)。
* 必须先 EcatPdo_Process() 再 PDO_InputMapping()。
* Process 先填好 buildInput+fillDiagTail,InputMapping 再发布,救援才有内容可写。 */
{
UINT16 dummy = 0U;
const UINT16 evt = HW_GetALEventRegister();
@@ -771,19 +737,18 @@ void EcatDbg_OpInputFreshness(void)
HW_EscReadWord(dummy, nEscAddrOutputData);
HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U));
}
/* 救援路径计入 busy(低频;与 ISR/任务派发同一口径)。
* 不要把 MainLoop 整段包进 CpuLoad——见 task_runner / cpu_load.h。 */
CpuLoad_Enter();
const uint32_t prevMask = EscExmcBus_MaskEnter();
EcatPdo_Process();
PDO_InputMapping();
EscExmcBus_MaskExit(prevMask);
CpuLoad_Leave();
++s_opFreshRescues;
s_inMapSeqSeen = s_inMapSeq;
s_inMapSeqSeen = EcatPdo_ProcessSeq();
s_inStaleBaseCyc = EcatDbg_Dwt();
printf("[ECAT][FRESH] WARN: OP input frozen (seq=%lu) evt=0x%04X "
"(gate=%u first=%u dc=%u) -> forced PDO_InputMapping #%lu\r\n",
(unsigned long)seq, (unsigned)evt,
(unsigned)(bEscIntEnabled ? 1U : 0U),
(unsigned)(bEcatFirstOutputsReceived ? 1U : 0U),
(unsigned)(bDcSyncActive ? 1U : 0U),
(unsigned long)s_opFreshRescues);
/* 不打串口:计数经 0x2000:24 / /api/sync_diag 上到网页,避免 5ms 刷屏。 */
}
}
+3 -5
View File
@@ -3,8 +3,6 @@
* @brief DCU EtherCAT 应用层接口声明
*
* 提供 SSC 所需的 APPL_ 回调函数声明。
* Phase 1: 桩实现,仅保证编译通过。
* Phase 3: 接入实际 PDO 映射(DCU 传感器输入 / 控制输出)。
*/
#ifndef COMM_SSC_PORT_DC_APP_H
#define COMM_SSC_PORT_DC_APP_H
@@ -33,17 +31,17 @@ void APPL_InputMapping(UINT16 *pInputData);
void APPL_OutputMapping(UINT16 *pOutputData);
void APPL_Application(void);
/* 1.6.33:OP 输入新鲜度不变量(task_runner 每圈调用;内部廉价,仅冻结时强制重写) */
/* OP 输入新鲜度不变量(task_runner 每圈调用;内部廉价,仅冻结时强制重写) */
void EcatDbg_OpInputFreshness(void);
UINT32 EcatDbg_OpFreshRescues(void);
/* 1.6.34: HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
/* HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
uint32_t EscExmcBus_MaskEnter(void);
void EscExmcBus_MaskExit(uint32_t prev);
uint32_t HW_EscExmcMaskEnter(void);
void HW_EscExmcMaskExit(uint32_t prev);
/* 1.6.34: 复位输出 sequence 状态机 */
/* 复位输出 sequence 状态机 */
void EcatCan_ResetSequenceState(void);
#ifdef __cplusplus
+17 -29
View File
@@ -144,22 +144,17 @@
#endif
/** 看门狗由定时器中断检查。
* 1.5.47:1→0。ECAT_TIMER_INT=1 要求端口提供调用 ECAT_CheckTimer() 的 1ms 硬件
* 定时器 ISR——本工程从未实现,导致 ECAT_CheckTimer 在整个固件里零调用者:
* (a) EsmTimeoutCounter 永不递减 → 任何 pending 状态迁移(SAFEOP→OP 等)永不
* 超时,从站会无限期忽略一切 AL Control(实测"进 PREOP 失败: AL=SAFEOP
* code=0x0000"的卡死就是它);
* (b) DC_CheckWatchdog/PLL 计数器(i16WaitForPllRunningCnt)不前进 → 若 ESC
* 残留 DC 激活位(0x0981,上个 DC 会话遗留、跨重连不清),从站按 DC 同步
* 模式进 SAFEOP→OP,OP 等待 PLL 序列永远不满足 → SAFEOP+0x0000 永久挂起
* (00:27 复测日志: 等待期轨迹恒 SAFEOP+0x0000,LRW 每 7ms 也救不回)。
* 改 0 后 MainLoop 按 DWT CYCCNT 轮询(hw_gd32exmc.cpp 的 HW_GetTimerValue/
* HW_ClearTimerValue 本就是为此写的死代码),每 1ms 调一次 ECAT_CheckTimer。 */
* ECAT_TIMER_INT=0:MainLoop 按 DWT CYCCNT 轮询(hw_gd32exmc.cpp 的
* HW_GetTimerValue/HW_ClearTimerValue),每 1ms 调一次 ECAT_CheckTimer。
* 若置 1 则要求端口提供调用 ECAT_CheckTimer() 的 1ms 硬件定时器 ISR——
* 本工程无该 ISR,ECAT_CheckTimer 零调用者会导致 EsmTimeoutCounter 永不递减
* (状态迁移永不超时)及 DC PLL 计数器不前进(残留 DC 激活位时 SAFEOP+0x0000
* 永久挂起)。 */
#ifndef ECAT_TIMER_INT
#define ECAT_TIMER_INT 0
#endif
/** 1.5.47:DWT CYCCNT @180MHz 的每毫秒 tick 数(ECAT_TIMER_INT=0 时 MainLoop 用)。 */
/** DWT CYCCNT @180MHz 的每毫秒 tick 数(ECAT_TIMER_INT=0 时 MainLoop 用)。 */
#ifndef ECAT_TIMER_INC_P_MS
#define ECAT_TIMER_INC_P_MS 180000U
#endif
@@ -535,13 +530,10 @@
------
------ Struct Packing
------
------ 1.5.42 修正:此前三对宏全部留空,注释误以为"AC6 不需要"。实际这些宏就是 SSC
------ 用来紧排结构体的唯一手段——SSC 的 SDO 读写按"si0 占 16bit,其后各条目按
------ BitLength 紧排"计算字节偏移(OBJ_GetEntryOffset, objdef.c:609),留空则结构体
------ 按编译器自然对齐,与该假设不符。armclang 实测:{UINT16; UINT64[4]} 的 UINT64
------ 落在字节 8 而非 2(编译期断言验证),{UINT16; UINT32 x4} 落在字节 4 而非 2,
------ 于是 0x7000/0x6000/0x2000 的 SDO 单条目访问全部读写错位内存;SSC 内部
------ 0x1C3x(TSYNCMANPAR)/0x10F1 等同样受影响。
------ 这些宏是 SSC 紧排结构体的唯一手段——SSC 的 SDO 读写按"si0 占 16bit,
------ 其后各条目按 BitLength 紧排"计算字节偏移(OBJ_GetEntryOffset, objdef.c:609),
------ 留空则结构体按编译器自然对齐,与该假设不符,导致 SDO 单条目访问
------ 全部读写错位内存;SSC 内部 0x1C3x/0x10F1 等同样受影响。
------ AC6/armclang 支持 GCC 风格 __attribute__((packed)),置于 struct 关键字之后
------ (即 START 位置)对整个结构体生效。
-----------------------------------------------------------------------------------------*/
@@ -590,11 +582,8 @@
------
------ PDO Size (需与 DCU_ESI.xml / gen_eeprom.py / dc_app.c 三处一致)
------
------ 1.5.56 扩到 DPRAM 硬上限:64B/384B → 384B/640B。
------ 输出 384B = 48×64bit:兼容单帧区 48B + CANFD 批量发送区 336B
------ 输入 640B = 80×64bit:桥状态 32B + 电机窗口 288B + 长帧 40B + 批量接收区 280B
------ 上限依据:邮箱占 0x1000-0x13FF,过程数据区仅 0x1400-0x1FFF=3072B,
------ SM2/SM3 均 3-buffer 实占 3×size ⇒ out+in ≤ 1024B。384+640 恰好用满。
------ 上限依据:邮箱占 0x1000-0x13FF,过程数据区仅 0x1400-0x1FFF=3072B,
------ SM2/SM3 均 3-buffer 实占 3×size ⇒ out+in ≤ 1024B。
------ 推导与实测(SII 余量/EXMC 耗时)见 comm/ethercat_pdo_size.h。
------
------ 这两个宏是**编译期硬门限**,必须先于其它改动生效:
@@ -604,12 +593,12 @@
------ 忘改这里会在进 SAFEOP 时报 AL 错误,而不是静默截断。
-----------------------------------------------------------------------------------------*/
/** 最大输出 PDO 字节数(RxPDO,主站→从站)。1.5.56: 0x40 → 0x180 = 384B */
/** 最大输出 PDO 字节数(RxPDO,主站→从站)。0x180 = 384B */
#ifndef MAX_PD_OUTPUT_SIZE
#define MAX_PD_OUTPUT_SIZE 0x180
#endif
/** 最大输入 PDO 字节数(TxPDO,从站→主站)。1.5.56: 0x140 → 0x280 = 640B */
/** 最大输入 PDO 字节数(TxPDO,从站→主站)。0x280 = 640B */
#ifndef MAX_PD_INPUT_SIZE
#define MAX_PD_INPUT_SIZE 0x280
#endif
@@ -710,12 +699,11 @@
#define MAX_MBX_READ_ADDRESS 0x1FFF
#endif
/* 1.5.59 根治邮箱楔死:本板邮箱几何构建期常量。
/* 本板邮箱几何构建期常量。
* 取值 = gen_eeprom.py 的 MBX_RX_OFFSET/SIZE(0x1000/256) 与 MBX_TX_OFFSET/SIZE
* (0x1200/256),即 DCU_ESI.xml <Sm> 与 ESC 上电自 SII 加载的寄存器真值;主站
* config_init 也只是照抄同一份 SII。MBX_StartMailboxHandler 以此直达缓存,
* 不再信任单次 CSR 间接读(错值曾以 0x1204/0x1208 进入缓存 → 所有 SDO 响应
* 写错址 → 断电才解的楔死),CSR 读仅作交叉校验(mailbox.c 打 WARN)。
* 不信任单次 CSR 间接读,CSR 读仅作交叉校验(mailbox.c 打 WARN)。
* ★ 改 SII 邮箱几何必须同步改这里(gen_eeprom.py 顶部注释同样有约束)。★ */
#ifndef MBX_SM0_START
#define MBX_SM0_START 0x1000
+11 -17
View File
@@ -4,8 +4,7 @@ Generate LAN9252 EEPROM binary image (ESI/SII) for GD32E508 DCU.
Outputs a C header file with the EEPROM content as a byte array.
Layout per ETG.1000.6 section 5.4 "SII coding" (Table 16/17, verified against the
local spec copy ETG1000_6_V1i0i3; 1.5.27 rewrite of the previous generator whose
layout was shifted by 2 words from word 4 onwards and used XOR instead of CRC-8):
local spec copy ETG1000_6_V1i0i3):
Word 0x00 PDI Control (byte0=PDI type 0x8D HBI 16-bit indexed,
byte1=ESC Configuration 0x00)
@@ -62,7 +61,6 @@ MBX_TX_SIZE = 256
# Process Data configuration
#
# 1.5.56 扩到 DPRAM 硬上限 64/384 -> 384/640(需要一周期合并多条 CANFD 帧)。
# 必须与 comm/ethercat_pdo_size.h 的 ECAT_PDO_OUT_BYTES / ECAT_PDO_IN_BYTES 一致
# ——固件侧有 _Static_assert 守着,但 EEPROM 这一侧无法编译期校验,只能靠这里对齐。
#
@@ -70,10 +68,8 @@ MBX_TX_SIZE = 256
# 邮箱 SM0/SM1 各 256B 固定占 0x1000-0x13FF,过程数据区只剩 0x1400-0x1FFF=3072B。
# SM2/SM3 都是 3-buffer 模式(control 0x64 / 0x20,bit1=0),实际占用 3×size:
# SM2 384B -> 0x1400-0x187F (384×3 = 1152B)
# SM3 600B -> 0x1880-0x1F67 (600×3 = 1800B) ← 1.6.15,DPRAM 留 120B 余量
# (384+600)×3 = 2952 ≤ 3072 = 过程数据区全长。历史上 1.5.56 曾取 640B
# ((384+640)×3 = 3072 恰好用满),1.6.1 瘦身到 496B 换 CPU,1.6.15 回到 600B
# 换批量区容量——推导与取舍见 comm/ethercat_pdo_size.h。
# SM3 600B -> 0x1880-0x1F67 (600×3 = 1800B), DPRAM 留 120B 余量
# (384+600)×3 = 2952 ≤ 3072 = 过程数据区全长。
# SSC 在 ecatslv.c:708-721 按 (start + size*buffer) 做重叠检查,撞了会在
# PREOP->SAFEOP 返回 ALSTATUSCODE_INVALIDSMOUTCFG,不是静默出错。
# 要再更大只能压邮箱(拖慢 SDO/FoE)或改 1-buffer(失去读写并发保护),见
@@ -81,14 +77,12 @@ MBX_TX_SIZE = 256
PD_OUT_OFFSET = 0x1400 # SM2: RxPDO (Master -> Slave outputs)
PD_OUT_SIZE = 384 # 48×64bit
PD_IN_OFFSET = 0x1880 # SM3: TxPDO (Slave -> Master inputs)
PD_IN_SIZE = 600 # 75×64bit (1.6.15 覆盖窗口;1.6.1 曾瘦身到 496=62×64bit,更早 640=80×64bit)
# 1.6.1 瘦身 640→496:批量接收条目数据 64→32B + 段5诊断区前移到 484。
# 1.6.15 496→600:批量接收改**定长覆盖窗口** 5 槽(每槽 12B 头含 u32 seqlock
# + 32B 数据 = 44B,共 220B,落在 364..583),段5诊断区 484→584。
# 动机:旧"环形队列+变长紧排"容量 3 帧/拍,现场 4000 帧/s 下每拍恒丢 1 帧,
# 且饱和态喂出了 1.6.9~1.6.13 追的错标/拼接竞态。5 槽 = 5000 帧/s@1ms。
PD_IN_SIZE = 600 # 75×64bit
# 批量接收为**定长覆盖窗口** 5 槽(每槽 12B 头含 u32 seqlock
# + 32B 数据 = 44B,共 220B,落在 364..583),段5诊断区 584。
# 5 槽 = 5000 帧/s@1ms。
# SM3 实占 3×600=1800B (0x1880-0x1F67),out+in=984 ≤ 1024 硬上限,留 120B 余量;
# 依据见 comm/ethercat_pdo_size.h 的 1.6.1 / 1.6.15 注。
# 依据见 comm/ethercat_pdo_size.h。
# 改后必须重烧 EEPROM 并断电冷启动。
# 3-buffer 占用与 DPRAM 上限自检:改尺寸/偏移时立刻在生成阶段拦住,
@@ -107,7 +101,7 @@ assert PD_OUT_SIZE % 8 == 0 and PD_IN_SIZE % 8 == 0, \
# 0x8D=HBI 16-bit). 0x05 is a reserved value on LAN9252 and disables the HBI PDI.
PDI_CONTROL = 0x8D
# Word 0 high byte = ESC Configuration register (0x0141): all zero
# (the old 0x0C hit "NOT set by EEPROM" DC bits - removed in 1.5.27).
# (0x0C would hit "NOT set by EEPROM" DC bits).
ESC_CONFIG = 0x00
# Word 1 low byte = PDI Configuration register (0x0150) HBI-mode bits: ALE
# qual / RD_WR mode / CS / RD / WR / ALE polarities. 0x00 = normal active-low
@@ -119,7 +113,7 @@ PDI_CONFIG = 0x00
# SYNC1 Output=1, SYNC0 Map=1, SYNC0 Output=1, both Driver/Polarity=00
# (push-pull ACTIVE LOW, DS p247). Active-low pulses pair with the firmware's
# falling-edge EXTI; with 0x00 the SYNC0/1 pins stay LATCH inputs and a DC
# SYNC0 event would never reach PB0 -> process data dead in DC mode (1.5.28).
# SYNC0 event would never reach PB0 -> process data dead in DC mode.
# Same value as the official LAN9252-EVB-HBIXML.xml ConfigData 890E80CC...
SYNC_LATCH_CFG = 0xCC
# Word 2 = Pulse Length of SyncSignals register (0x0982), units of 10ns
@@ -353,7 +347,7 @@ for row in range(0, EEPROM_SIZE, 16):
chunk = eeprom[row:row+16]
hex_str = ", ".join(f"0x{b:02X}" for b in chunk)
if all(b == 0xFF for b in chunk):
# 1.5.28: emit the real 0xFF bytes. A comment-only padding row leaves
# Emit the real 0xFF bytes. A comment-only padding row leaves
# those array elements zero-initialized (C static init) instead of
# the erased-0xFF state, and the firmware burns the whole array.
if row % 256 == 0:
@@ -3,13 +3,13 @@
* @brief SSC V5.11 硬件 port 层 — GD32E508 + LAN9252 (EXMC 16-bit HBI Indexed Mode)
*
* 实现 el9800hw.h 中声明的全部 HW_* 函数,将 SSC 的 ESC 访问请求
* 转发到 EscLan9252 的间接访问端口(1.5.39 按 DS 12.11 修正访问模型):
* 转发到 EscLan9252 的间接访问端口:
* - EtherCAT 核心 CSR(0x0000-0x0FFF:AL Status/Event、SyncManager、LED 等)
* → ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300) 间接端口;
* - 过程 RAM(0x1000-0x1FFF:邮箱 0x1000+、PDO 0x1400+)
* → PRAM FIFO(0x000-0x03C 数据口 + 0x308-0x314 地址/命令)。
* 1.5.37 及之前直读/直写 HBI 索引(误以为索引空间统一可寻址),核心 CSR 全部
* 读 0/写丢弃——SSC 对主站 AL Control 完全失聪,主站恒读 AL=INIT。
* 核心 CSR 不在 HBI 直接空间,直读/直写会使核心 CSR 全部
* 读 0/写丢弃,SSC 对主站 AL Control 完全失聪。
*
* 并发保护(不变):
* - HW_EscRead/HW_EscWrite 等主循环函数:内部经 EscExmcBus::maskEnter/Exit
@@ -229,8 +229,7 @@ void HW_EscWriteIsr(MEM_ADDR *pData, UINT16 Address, UINT16 Len)
/**
* @brief 禁用指定 SM 通道 (清 SM Activate 寄存器 0x0806+8n bit0)。
* @param channel SM 通道号 (0-15)
* @note 1.5.39:偏移由 +4 改为 +6——+4 是 SM Control(操作模式字段),
* Activate(bit0=SM Enable)在 +6;旧值从未真正生效过(访问层全空)。
* @note SM Activate 在偏移 +6(bit0=SM Enable);+4 是 SM Control(操作模式字段)。
*/
void HW_DisableSyncManChannel(UINT8 channel)
{
+77 -120
View File
@@ -11,94 +11,6 @@
This file contains the Process Data interface
\version 5.11
<br>Changes to version V5.10.1:<br>
V5.11 COE3: change 0x10F3.2 (Sync Error limit) from UINT32 to UINT16 (according to the ETG.1020)<br>
V5.11 ECAT1: update EEPROM access reset operation<br>
V5.11 ECAT10: change PROTO handling to prevent compiler errors<br>
V5.11 ECAT11: create application interface function pointer, add eeprom emulation interface functions<br>
V5.11 ECAT2: update EEPROM access retry cycle (add 10ms delay between two retry cycles)<br>
V5.11 ECAT3: handle bus cycle calculation for input/output only devices and create warning diag message only if calculation failed<br>
V5.11 ECAT4: enhance SM/Sync monitoring for input/output only slaves<br>
V5.11 ECAT6: add function to calculate bus cycle time<br>
V5.11 ECAT8: call PDO_InputMapping only once if DC is enabled and COE is not supported<br>
V5.11 EEPROM1: fix compiler error during pEEPROM pointer initialization<br>
V5.11 EEPROM2: write Station alias value to EEPROM data register on EEPROM reload command<br>
V5.11 EEPROM3: clear EEPROM error bits<br>
V5.11 EEPROM4: prevent the variable in the EEPROM busy loop to be removed by the compiler<br>
V5.11 ESM7: "add Sync define for 0x22 (""SYNCTYPE_SM2_SYNCHRON""), support value 0x22 for 0x1C33.1 (SM2 sync)"<br>
<br>Changes to version V5.01:<br>
V5.10 COE1: Define one entry description for all 0x1C3x objects and change data type of SI11,12,13 to UINT16 (according ETG.1020)<br>
V5.10 ECAT1: Correct calculation of blinking and flashing sequence<br>
V5.10 ECAT13: Update Synchronisation handling (FreeRun,SM Sync, Sync0, Sync1)<br>
Compare DC UINT configuration (by ESC Config data) vs. DC activation register settings<br>
Update 0x1C3x entries<br>
V5.10 ECAT2: Prevent EEPROM data null pointer access (if the pointer is null an command error is set)<br>
EEPROM emulation return command error if unknown command was received<br>
V5.10 ECAT4: Update alignment marco for 8 to 15 bit alignments (16 and 32 Bit controllers)<br>
Bugfix calculate LED blink frequency<br>
V5.10 ECAT7: Add "bInitFinished" to indicate if the initialization is complete<br>
V5.10 HW2: Change HW_GetTimer return value to UINT32<br>
<br>Changes to version V5.0:<br>
V5.01 APPL3: Include library demo application<br>
V5.01 ESC1: Change ESC access function (if EEPROM Emulation is active)<br>
V5.01 ESC2: Add missed value swapping<br>
<br>Changes to version V4.40:<br>
V5.0 TEST1: Add test application. See Application Note ET9300 for more details.<br>
V5.0 ECAT2: Application specific functions are moved to application files.<br>
V5.0 ECAT3: Global dummy variables used for dummy ESC operations.<br>
V5.0 ESC1: ESC 32Bit Access added.<br>
V5.0 ESC3: Add EEPROM emulation support.<br>
V5.0 ESM3: Handling pending ESM transitions.<br>
V5.0 ESC5: Enhance EEPROM access handling.<br>
V5.0 PDO1: AL Event flags are not rechecked in PDO_OutputMappping(). (Already checked before call function)<br>
V5.0 SYNC1: Add missed SM event indication (0x1C32/0x1C33 SI11).<br>
<br>Changes to version V4.30:<br>
V4.40 DIAG1: add diagnosis message support<br>
V4.40 PDO1: merge content of HW_InputMapping (spihw.c/mcihw.c) to PDO_InputMapping. merge content of HW_OutputMapping (spihw.c/mcihw.c) to PDO_OutputMapping.<br>
V4.40 PDO2: Generic process data length calculation<br>
V4.40 ECAT2: call cyclic CheckIfLocalError() to check the local flags<br>
V4.40 HW0: Generic hardware access functions. Add Function (PDI_Isr()), content merged from spihw.c and mcihw.c.<br>
V4.40 WD1: define (ESC_SM_WD_SUPPORTED) to choose ESC SyncManager watchdog or local watchdog<br>
V4.40 ESM2: Change state transition behaviour from SafeOP to OP<br>
V4.40 TIMER1: Change bus cycle time calculation and trigger of ECAT_CheckTimer() if ECAT_TIMER_INT is reset<br>
V4.40 HW1: Add support for fc1100 hardware<br>
<br>Changes to version V4.20:<br>
V4.30 EL9800: EL9800_x cyclic application is moved to el9800.c<br>
V4.30 OBJ 3: add object dictionary initialization<br>
V4.30 SYNC: add CalcSMCycleTime() (calculation of bus cycle time); change synchronisation control functions<br>
V4.30 PDO: include PDO specific functions (moved from coeappl.c).<br>
xxx_InputMapping(); xxx_OutputMapping(); xxx_ReadInputs(); xxx_ResetOutputs(); xxx_Application()<br>
V4.30 CiA402: Add CiA402_StateMachine() and CiA402_Application() call<br>
V4.20 DC 1: Add DC pending Statemachine handling<br>
V4.20 PIC24: Add EL9800_4 (PIC24) required source code<br>
V4.20 LED 1: Modified LED Handling<br>
V4.11 APPL 1: The checkWatchdog() function should not called in checkTimer() if this function is triggered by an Interrupt<br>
<br>Changes to version V4.08:<br>
V4.10 LED 1: The handling of the EtherCAT-Error-LED was added<br>
V4.10 AOE 3: The AoE fragment size has to be initialized during the state transition<br>
from INIT to PREOP<br>
<br>Changes to version V4.07:<br>
V4.08 LED 1: The handling of the EtherCAT-LED can be (de-)selected by the switch LEDS_SUPPORTED<br>
because the ET1100 and ET1200 have an output port which could be connected directly.<br>
<br>Changes to version V4.01:<br>
V4.02 ECAT 1: The watchdog timer variables shall be initialized.<br>
<br>Changes to version V4.00:<br>
V4.01 APPL 1: If the application is running in synchron mode and no SM event<br>
is received, the application should be called from the main loop<br>
V4.01 APPL 2: In FreeRun mode the output should only be copied if the slave is in OP<br>
<br>Changes to version V3.20:<br>
V4.00 APPL 1: The watchdog checking should be done by a microcontroller<br>
timer because the watchdog trigger of the ESC will be reset too<br>
if only a part of the sync manager data is written<br>
V4.00 APPL 2: The setting of EtherCAT state LEDs were included<br>
V4.00 APPL 3: The outputs should be reset to a safe state,<br>
when the state OP is left<br>
V4.00 APPL 4: An example for the EEPROM access through the ESC is shown in<br>
the function APPL_StartMailboxHandler<br>
V4.00 APPL 5: The inputs should be read once when the state transition<br>
from PREOP to SAFEOP is made<br>
V4.00 APPL 6: The main function was split in MainInit and MainLoop
*/
@@ -157,17 +69,22 @@ BOOL bCycleTimeMeasurementStarted; // indicates if the bus cycle measurement is
UINT16 aPdOutputData[(MAX_PD_OUTPUT_SIZE>>1)];
UINT16 aPdInputData[(MAX_PD_INPUT_SIZE>>1)];
/*ECAT_DBG_START(1.6.0 临时剖分埋点)*/
/* 1.6.0 负载归因埋点:DWT 周期累加器,经 0x2000 sub5..8 上报,主站 1s 差分出速率。
* 只为定位 free-run 模式下每周期 508µs 计入时间的真实去向;定位完成后整块移除。
/*ECAT_DBG_START(剖分埋点)*/
/* 负载归因埋点:DWT 周期累加器,经 0x2000 sub5..8 上报,主站 1s 差分出速率。
* EcatDbg_Dwt() 定义在 comm/ethercat_sync.cpp (extern "C")。 */
extern UINT32 EcatDbg_Dwt(void);
#define ecatdbg_dwt() EcatDbg_Dwt()
/* 1.6.0:输出探针(实现见 comm/ethercat_pdo.cpp,C 链接) */
/* CPU 负载挂钩(sys/cpu_load.cpp) */
extern void CpuLoad_Enter(void);
extern void CpuLoad_Leave(void);
/* 1ms 调度节拍(ethercat_sync.cpp extern "C") */
extern UINT32 EcatDbg_GetTick1ms(void);
/* 输出探针(实现见 comm/ethercat_pdo.cpp,C 链接) */
extern UINT8 EcatPdo_OutputProbe(UINT8 const *head);
/* 1.6.22:稀疏写 ESC 计划(密排 count 只写有效段; 实现见 ethercat_pdo.cpp) */
extern void EcatPdo_NoteOutputMapped(void);
/* 稀疏写 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) */
/* 差分写 ESC 多段计划(段头/段尾 + 3-buffer 冲刷; 实现见 ethercat_pdo.cpp) */
extern UINT8 EcatPdo_EscWritePlan(UINT16 *offs, UINT16 *lens, UINT8 maxSegs);
volatile UINT32 g_dbg_pdiCyc = 0; /* sub5: PDI_Isr 全函数累计周期 */
volatile UINT32 g_dbg_outCyc = 0; /* sub6: PDO_OutputMapping(输出探针/全量读) */
@@ -200,10 +117,10 @@ BOOL bInitFinished = FALSE; /** < \brief indicates if the initialization is fini
*////////////////////////////////////////////////////////////////////////////////////////
void PDO_InputMapping(void)
{
/*ECAT_DBG(1.6.0 剖分:输入映射全程 + 过程数据写细分)*/
/* 1.6.22 稀疏写:APPL 仍整填 aPdInputData(对齐 SDO 0x6000),但 HW_EscWriteIsr
/*ECAT_DBG(剖分:输入映射全程 + 过程数据写细分)*/
/* 稀疏写:APPL 仍整填 aPdInputData(对齐 SDO 0x6000),但 HW_EscWriteIsr
* 只写密排有效段+段3/5,跳过空槽区 —— 省的是 PRAM 字节时间,不是 RAM 拷贝。
* 1.6.25 差分写:在稀疏之上再跳过「与影子一致且 3-buffer 冲刷完成」的段。 */
* 差分写:在稀疏之上再跳过「与影子一致且 3-buffer 冲刷完成」的段。 */
const UINT32 dbgT0 = ecatdbg_dwt();
APPL_InputMapping((UINT16*)aPdInputData);
const UINT32 dbgT1 = ecatdbg_dwt();
@@ -228,32 +145,46 @@ void PDO_InputMapping(void)
to the hardware. This function is only called in case of an SM2
(output process data) event.
1.6.0 输出按需全量读(负载优化):每周期先读输出 PDO 头 12B 探针(覆盖 SEQ@0
输出按需全量读:每周期先读输出 PDO 头 12B 探针(覆盖 SEQ@0
与 WIN@8),经 EcatPdo_OutputProbe 判定——seq 无变化沿且无清统计请求时,
跳过 384B 全量读(只补读 SM2 末字完成事件应答),省掉每周期 ~190µs 的
PRAM 搬运(含 ESC 核仲裁等待)。主站滚动窗口扫槽依赖的 WIN 字节在探针内
每周期刷新,窗口扫描不受影响。SM2 应答语义不变:首字(探针内)+末字
(两分支各自覆盖)。
跳过全量读(只补读 SM2 末字完成事件应答)。主站滚动窗口扫槽依赖的
WIN 字节在探针内每周期刷新,窗口扫描不受影响。
SM2 应答语义不变:首字(探针内)+末字(两分支各自覆盖)。
*////////////////////////////////////////////////////////////////////////////////////////
void PDO_OutputMapping(void)
{
/*ECAT_DBG(1.6.0 剖分:输出映射全程)*/
/*ECAT_DBG(剖分:输出映射全程)*/
const UINT32 dbgT0 = ecatdbg_dwt();
/* 1.6.0:输出按需全量读——探针 12B 判定是否真的有新输出 */
/* 输出映射:12B 探针判定是否有新输出。
* 「每拍始终全量读 384B」会在 PDO 提速后把 PDI 成本抬高一个数量级
* (历史:探针版 4kHz load≈80%;全量+process 双读时 2kHz 即饱和)。
* 探针命中才全量读;否则只补读 SM2 末字。s_out 长期不更新由
* EthercatPdo::process() 按新鲜度门兜底全量读(不是每拍)。 */
UINT8 head[12];
HW_EscReadIsr(((MEM_ADDR *) head), nEscAddrOutputData, 12U );
if ( EcatPdo_OutputProbe(head) )
{
HW_EscReadIsr(((MEM_ADDR *)aPdOutputData), nEscAddrOutputData, nPdOutputSize );
/* 有新 seq 时不必每次拉满 384B:先读命令前缀(含批量 count@48)。
* 8kHz 主站每拍都递增 seq 时,若 count==0 仍全量读会每拍吃满 PRAM,
* 把 CAN 解析饿死(gseq/s 从 ~3750 掉到数百)。 */
const UINT16 prefix = (nPdOutputSize < 52U) ? nPdOutputSize : 52U;
HW_EscReadIsr(((MEM_ADDR *)aPdOutputData), nEscAddrOutputData, prefix);
const UINT8 batchCount = (prefix > 48U) ? ((const UINT8 *)aPdOutputData)[48] : 0U;
if (batchCount > 0U && nPdOutputSize > prefix) {
HW_EscReadIsr(((MEM_ADDR *)((UINT8 *)aPdOutputData + prefix)),
(UINT16)(nEscAddrOutputData + prefix),
(UINT16)(nPdOutputSize - prefix));
}
APPL_OutputMapping((UINT16*) aPdOutputData);
EcatPdo_NoteOutputMapped();
}
else
{
/* 无新输出:补读 SM2 末字完成事件应答(首字已随探针读过) */
HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2));
if (nPdOutputSize >= 2U) {
HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2));
}
}
g_dbg_outCyc += ecatdbg_dwt() - dbgT0;
}
@@ -342,7 +273,7 @@ void HandleBusCycleCalculation(void)
void PDI_Isr(void)
{
/*ECAT_DBG(1.6.0 剖分:ISR 全程)*/
/*ECAT_DBG(剖分:ISR 全程)*/
const UINT32 dbgT0 = ecatdbg_dwt();
if(bEscIntEnabled)
{
@@ -397,17 +328,30 @@ void PDI_Isr(void)
/*ECATCHANGE_END(V5.11) ECAT4*/
/*
Call ECAT_Application() in SM Sync mode
Call ECAT_Application() in SM Sync mode (non-DC only).
DC:应用节拍已由 Sync0_Isr 拥有(bDcSyncActive),PDI 再跑一次
会把 Process/buildInput 算两遍 load_perm。
非 DC:仍只在 SM/SM2 类型时在 PDI 内跑应用。
**不要**「凡有 AL 过程事件就跑」——8kHz 时 PDI 每拍都带事件,
Process+InputMapping 热路径会被放大,单拍超过 ~120µs 墙,
实测 6kHz 即掉出(曾可 8kHz)。
负载随 PDO 上升由 ISR 包装 + free-run 1ms 节流路径体现,
见 sys/cpu_load.h;输出侧保持 12B 探针,避免每拍全量读。
*/
if (sSyncManOutPar.u16SyncType == SYNCTYPE_SM_SYNCHRON)
if (!bDcSyncActive
&& (sSyncManOutPar.u16SyncType == SYNCTYPE_SM_SYNCHRON
|| sSyncManOutPar.u16SyncType == SYNCTYPE_SM2_SYNCHRON
|| sSyncManInPar.u16SyncType == SYNCTYPE_SM_SYNCHRON
|| sSyncManInPar.u16SyncType == SYNCTYPE_SM2_SYNCHRON))
{
/* The Application is synchronized to process data Sync Manager event*/
const UINT32 dbgTb = ecatdbg_dwt();
ECAT_Application();
g_dbg_appCyc += ecatdbg_dwt() - dbgTb;
}
if ( bEcatInputUpdateRunning
if ( bEcatInputUpdateRunning
/*ECATCHANGE_START(V5.11) ESM7*/
&& ((sSyncManInPar.u16SyncType == SYNCTYPE_SM_SYNCHRON) || (sSyncManInPar.u16SyncType == SYNCTYPE_SM2_SYNCHRON))
/*ECATCHANGE_END(V5.11) ESM7*/
@@ -438,7 +382,7 @@ void PDI_Isr(void)
HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2));
}
} //if(bEscIntEnabled)
/*ECAT_DBG(1.6.0 剖分:ISR 全程 收尾)*/
/*ECAT_DBG(剖分:ISR 全程 收尾)*/
g_dbg_pdiCyc += ecatdbg_dwt() - dbgT0;
g_dbg_pdiCnt++;
}
@@ -653,15 +597,28 @@ void MainLoop(void)
}
DISABLE_ESC_INT();
/*ECAT_DBG(1.6.0 剖分:free-run 应用+输入映射块)*/
/*ECAT_DBG(剖分:free-run 应用+输入映射块)*/
{
const UINT32 dbgT1 = ecatdbg_dwt();
ECAT_Application();
/*
* free-run 主循环:**严格 1ms 节流**再跑应用并计入 busy。
* 不要「AL 有过程事件就跑」——事件位可能连续置位,退化成
* 每圈 MainLoop 都 Process,热路径被抬爆(8kHz 掉到 6kHz)。
* SM 模式(bEscIntEnabled)本分支本就不进;PDO 高速走 PDI ISR,
* 负载由 EXTI1 挂钩计入。
*/
static UINT32 s_freeRunAppMs = 0xFFFFFFFFu;
const UINT32 nowMs = EcatDbg_GetTick1ms();
if (nowMs != s_freeRunAppMs) {
s_freeRunAppMs = nowMs;
CpuLoad_Enter();
ECAT_Application();
if ( bEcatInputUpdateRunning )
{
/* EtherCAT slave is at least in SAFE-OPERATIONAL, update inputs */
PDO_InputMapping();
if ( bEcatInputUpdateRunning )
{
PDO_InputMapping();
}
CpuLoad_Leave();
}
g_dbg_freerunCyc += ecatdbg_dwt() - dbgT1;
}
+5 -53
View File
@@ -67,48 +67,6 @@ When the repeated mailbox service was sent (call of MBX_MailboxReadInd), psReadM
and psStoreMbx will be sent (in MBX_CopyToSendMailbox) and stored in psReadMbx, psStoreMbx will be set to 0.
\version 5.11
<br>Changes to version V5.10:<br>
V5.11 ECAT10: change PROTO handling to prevent compiler errors<br>
V5.11 ECAT7: add missing big endian swapping<br>
V5.11 HW1: "move hardware independent functions ""HW_DisableSyncManChannel()"", ""HW_EnableSyncManChannel()"", ""HW_GetSyncMan()"", ""HW_ResetALEventMask()"", ""HW_SetALEventMask()"" to ecatalv.c"<br>
V5.11 MBX2: "check in IP if enough dynamic memory is available to handle mailbox communication, if it is not the case the mbx error ""no memory"" is returned on any mbx request"<br>
V5.11 MBX3: set application triggered emergency and EoE data to pending if no mailbox queue is supported and another mailbox request is currently handled, Handle only one mailbox request at a time (in case that MAILBPX_QUEUE is disabled)<br>
V5.11 TEST4: add new mailbox test behaviour (the master mailbox cnt shall be incremented by 1 and the slave mailbox cnt is alternating)<br>
<br>Changes to version V5.01:<br>
V5.10 EOE3: Change local send frame pending indication variable to a global variable (it need to be resetted if the mailbox is stopped and a frame is pending)<br>
V5.10 ESC6: Update SyncManager register size (only for 8Bit ESC access)<br>
V5.10 MBX2: Allocate only memory for current configured mailbox size (max supported size was allocated before)<br>
<br>Changes to version V5.0:<br>
V5.01 ESC2: Add missed value swapping<br>
V5.01 MBX2: Return an error if length specified in mailbnox header is too large.<br>
<br>Changes to version V4.40:<br>
V5.0 ECAT3: Global dummy variables used for dummy ESC operations.<br>
V5.0 ESC1: ESC 32Bit Access added.<br>
V5.0 MBX2: Prevent double swapping (MBX repeat service) of MBX length field.<br>
V5.0 MBX4: Unlock SM buffer based on SM state.<br>
<br>Changes to version V4.11:<br>
V4.40 MBX8: Handle pending mailbox data even if mailbox queue is enabled<br>
V4.40 MBX7: Reset SyncManger 1 Repeat Acknowledge bit if local variable is reset<br>
V4.40 EMCY2: Send queued emergency messages<br>
V4.40 BOOT2: Support only FoE in Bootstrap state<br>
V4.40 MBX5: Prevent multiple free buffer<br>
V4.40 MBX6: change return value if no mailbox buffer is available<br>
<br>Changes to version V4.08:<br>
V4.11 MBX 1: free some dynamic memory to avoid memory leaks in the mailbox queue<br>
<br>Changes to version V4.07:<br>
V4.10 MBX 1: check Mailbox message buffer and print emergency messages<br>
during state change Init -> PreOP<br>
<br>Changes to version V4.07:<br>
V4.08 MBX 1: The mailbox queue was added (switch MAILBOX_QUEUE), if the<br>
mailbox should be handled in the ESC-Interrupt Service Routine<br>
the defines ENTER_MBX_CRITICAL and LEAVE_MBX_CRITICAL could be used<br>
to disable the interrupts or change the priority<br>
V4.08 AOE 1: AoE was added<br>
<br>Changes to version V4.06:<br>
V4.07 ECAT 1: The sources for SPI and MCI were merged (in ecat_def.h<br>
set the switch MCI_HW to 1 when using the MCI,<br>
set the switch SPI_HW to 1 when using the SPI
*/
@@ -133,7 +91,7 @@ V4.07 ECAT 1: The sources for SPI and MCI were merged (in ecat_def.h<br>
#include "ecatcoe.h"
#include <stdio.h> /* printf: SM 几何交叉校验 WARN(1.5.59,见 MBX_StartMailboxHandler) */
#include <stdio.h> /* printf: SM 几何交叉校验 WARN(见 MBX_StartMailboxHandler) */
/*--------------------------------------------------------------------------------------
------
@@ -284,16 +242,10 @@ void MBX_Init(void)
UINT16 MBX_StartMailboxHandler(void)
{
UINT16 result = 0;
/* 1.5.59 根治"邮箱楔死"(响应帧头落在 SM1 start+4/+8、逐会话漂移,断电才清):
* 根因链 = INIT→PREOP 时经 GetSyncMan/CSR 单次间接读(EXMC HBI)把 SM0/SM1
* 几何缓存进 u16EscAddr* 与 u16*MbxSize 等变量;总线毛刺给出的"合法"错值
* (0x1204/0x1208 均为偶地址且在检查范围内)即被缓存,此后所有 SDO 响应写错址,
* 主站按协议从 0x1200 读永远错位,重枚举重读仍错 → 只有断电能解。本板邮箱
* 几何是 SII 固定值(gen_eeprom.py 的 MBX_RX/TX_OFFSET/SIZE,ESC 上电自行
* 加载,主站 config_init 也只是照抄),改为构建期常量(ecat_def.h:
* MBX_SM0/1_START/SIZE)直达缓存,CSR 读降级为交叉校验:失配打 WARN(说明
* ESC 寄存器被非常规改写,需查)但缓存仍取常量——CSR 错值从此没有进入
* 缓存的路径,楔死无建立点;运行期内存踩踏也会在每次 INIT→PREOP 被常量覆写。
/* 邮箱几何构建期常量(ecat_def.h:MBX_SM0/1_START/SIZE)直达缓存,
* 不信任 GetSyncMan/CSR 单次间接读——CSR 读降级为交叉校验:
* 失配打 WARN(说明 ESC 寄存器被非常规改写,需查)但缓存仍取常量。
* 运行期内存踩踏也会在每次 INIT→PREOP 被常量覆写。
* 注意:ESC 寄存器经 PDI/CSR 读出即主机序(与 GetSyncMan 既有路径一致,勿加
* SWAPWORD;PRAM 帧数据才是线上大端,那边交换是对的)。 */
/*ECATCHANGE_START(V5.11) HW1*/