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
+12
View File
@@ -93,3 +93,15 @@ TEST_READY.md
TEST_READY.html TEST_READY.html
convert_md_to_html.py convert_md_to_html.py
*.html *.html
# IDE / 多智能体过程产物
.trae/
.worktrees/
.qoder/
.zcode/
docs/traework-import/
# 本机固件 keep / 安全评审过程稿
firmware_keep/
SECURITY_AUDIT_REPORT.*
ORIGINAL_REQUEST.md
+15 -15
View File
@@ -114,6 +114,10 @@ bool EscExmcBus::init() {
rcu_periph_clock_enable(RCU_GPIOE); rcu_periph_clock_enable(RCU_GPIOE);
rcu_periph_clock_enable(RCU_AF); rcu_periph_clock_enable(RCU_AF);
rcu_periph_clock_enable(RCU_EXMC); 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 推挽 */ /* 配置 16-bit 数据线 D0-D15 为 AF 推挽 */
gpio_init(CONFIG_EXMC_D0_PORT, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, gpio_init(CONFIG_EXMC_D0_PORT, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ,
CONFIG_EXMC_D0_PIN); CONFIG_EXMC_D0_PIN);
@@ -196,19 +200,15 @@ bool EscExmcBus::init() {
norsram_init.memory_type = EXMC_MEMORY_TYPE_SRAM; norsram_init.memory_type = EXMC_MEMORY_TYPE_SRAM;
norsram_init.address_data_mux = DISABLE; norsram_init.address_data_mux = DISABLE;
/* 读/写共用时序(非 extended mode) /* 读/写共用时序(非 extended mode)。时序余量按 DS 表 9-4/9-5 独立核算,见下方 BUSLAT。 */
* 2026-08-20 慢档测试(16/16/256 HCLK)无效,已还原:慢档时序未带来任何
* 读数变化,速度因素彻底排除(与 bit-bang 40-80us 仍失败一致)。
* 注:那轮测试的失败根因是访问模型错误(直读核心 CSR 索引空间,1.5.39 修复),
* 与时序无关;时序余量本身仍需按 DS 表 9-4/9-5 独立核算,见下方 BUSLAT。 */
rw_timing.asyn_access_mode = EXMC_ACCESS_MODE_A; rw_timing.asyn_access_mode = EXMC_ACCESS_MODE_A;
rw_timing.syn_data_latency = EXMC_DATALAT_2_CLK; rw_timing.syn_data_latency = EXMC_DATALAT_2_CLK;
rw_timing.syn_clk_division = EXMC_SYN_CLOCK_RATIO_DISABLE; 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。 * 该间隔由 EXMC 在两次访问之间插入的总线空闲期承担 = (1 固有 + BUSLAT) HCLK。
* BUSLAT=1 → 2 HCLK ≈ 11.1ns,低于 13ns 下限,仅靠 AHB 取指间隙偶然兜底: * BUSLAT=1 → 2 HCLK ≈ 11.1ns,低于 13ns 下限,仅靠 AHB 取指间隙偶然兜底:
* 常温刚好可用,温度升高(传播延迟增大)/电压下降(驱动减弱)即越界,表现为 * 常温刚好可用,温度升高(传播延迟增大)/电压下降(驱动减弱)即越界,表现为
* 偶发读到 0xFFFF 或错值——与实测的温漂偶发读错吻合。 * 偶发读到 0xFFFF 或错值。
* BUSLAT=4 → 5 HCLK ≈ 27.8ns,余量 2.1 倍,覆盖全温区与电压波动。 * BUSLAT=4 → 5 HCLK ≈ 27.8ns,余量 2.1 倍,覆盖全温区与电压波动。
* 代价:每次 HBI 访问多 3 HCLK(16.7ns)。满载 1024B 过程数据按每 DWORD * 代价:每次 HBI 访问多 3 HCLK(16.7ns)。满载 1024B 过程数据按每 DWORD
* 两次 16-bit 访问计,增加约 256×2×16.7ns ≈ 8.6us,相对原 218us 增幅 3.9%, * 两次 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", 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); (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。 * 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|... 逐字段解码打印, * 期望 ASET=4/AHLD=2/DSET=20/BUSLAT=4 → 0x0313_0000|... 逐字段解码打印,
* 避免只看整字导致的位域误读;BUSLAT 实际值 <4 说明库或硬件截断,须立即排查。 */ * 避免只看整字导致的位域误读;BUSLAT 实际值 <4 说明库或硬件截断,须立即排查。 */
@@ -271,8 +271,8 @@ bool EscExmcBus::init() {
"ESC driver\r\n"); "ESC driver\r\n");
/* ---- 硬件诊断: 转储关键寄存器 + GPIO 手动输出测试 ---- /* ---- 硬件诊断: 转储关键寄存器 + GPIO 手动输出测试 ----
* 1.5.33: 默认关闭。硬件层已确认全通,探针仅换板/补焊排查浮空时经 * 默认关闭。硬件层已确认全通,探针仅换板/补焊排查浮空时经
* CONFIG_ESC_EXMC_DIAG 临时开启(会拆 EXMC 引脚成 GPIO,正常运行不该跑)。 */ * CONFIG_ESC_EXMC_DIAG 临时开启(会拆 EXMC 引脚成 GPIO,正常运行不该跑)。 */
#if CONFIG_ESC_EXMC_DIAG #if CONFIG_ESC_EXMC_DIAG
diagDump(); diagDump();
#endif #endif
@@ -284,8 +284,8 @@ bool EscExmcBus::init() {
/* ======================================================================== /* ========================================================================
* 硬件诊断: 精简版 —— 只保留核心探活 + bit-bang + 位级回显 * 硬件诊断: 精简版 —— 只保留核心探活 + bit-bang + 位级回显
* 1.5.33: 整段由 CONFIG_ESC_EXMC_DIAG 门控,默认不编译。bbDelay/bbReadData/ * 整段由 CONFIG_ESC_EXMC_DIAG 门控,默认不编译。bbDelay/bbReadData/
* bbDriveData 等位拍辅助函数在上方匿名 namespace 中,continuityDrive 仍复用,保留。 * bbDriveData 等位拍辅助函数在上方匿名 namespace 中,continuityDrive 仍复用,保留。
* ======================================================================== */ * ======================================================================== */
#if CONFIG_ESC_EXMC_DIAG #if CONFIG_ESC_EXMC_DIAG
void EscExmcBus::diagDump() { void EscExmcBus::diagDump() {
@@ -301,7 +301,7 @@ void EscExmcBus::diagDump() {
return; return;
} }
printf("[EXMC-DIAG] --- bit-bang probe (bypass EXMC) ---\r\n"); printf("[EXMC-DIAG] --- bit-bang probe (bypass EXMC) ---\r\n");
/* NRST 实际电平回读(1.5.26):输入通路常连引脚,输出态亦读真实电压。 /* NRST 实际电平回读:输入通路常连引脚,输出态亦读真实电压。
* 驱动高却 LOW => ECAT_NRST 被外部按低 => LAN9252 一直在复位态。 */ * 驱动高却 LOW => ECAT_NRST 被外部按低 => LAN9252 一直在复位态。 */
printf("[EXMC-DIAG] NRST(PB10) reads %s\r\n", printf("[EXMC-DIAG] NRST(PB10) reads %s\r\n",
(gpio_input_bit_get(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN) == SET) (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_ctl0_e = GPIO_CTL0(GPIOE);
s_save_ctl1_e = GPIO_CTL1(GPIOE); s_save_ctl1_e = GPIO_CTL1(GPIOE);
/* 测试期屏蔽 SYNC0/ESC IRQ/SYNC1:ISR 的 HBI 访问落在已被切成 GPIO 态的 /* 测试期屏蔽 SYNC0/ESC IRQ/SYNC1:ISR 的 HBI 访问落在已被切成 GPIO 态的
* 总线上只会读回垃圾并错写 IDX,SSC 状态可能被污染(1.5.29)。 */ * 总线上只会读回垃圾并错写 IDX,SSC 状态可能被污染。 */
s_maskPrev = EscExmcBus::maskEnter(); s_maskPrev = EscExmcBus::maskEnter();
/* 全部数据线 + 控制线设为 OUT_PP */ /* 全部数据线 + 控制线设为 OUT_PP */
gpio_init(GPIOD, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, gpio_init(GPIOD, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
@@ -571,7 +571,7 @@ uint32_t EscExmcBus::maskEnter() {
/* EXTI0=SYNC0(preempt=0) 无法被 BASEPRI 屏蔽,只能关 NVIC; /* EXTI0=SYNC0(preempt=0) 无法被 BASEPRI 屏蔽,只能关 NVIC;
* EXTI1=ESC IRQ / EXTI2=SYNC1 (均 preempt=1) 的 PDI_Isr/Sync1_Isr 同样做 * EXTI1=ESC IRQ / EXTI2=SYNC1 (均 preempt=1) 的 PDI_Isr/Sync1_Isr 同样做
* HBI 两步事务访问,一并屏蔽——否则主循环事务会被 Sync1_Isr 的 * HBI 两步事务访问,一并屏蔽——否则主循环事务会被 Sync1_Isr 的
* PDO_InputMapping→HW_EscWriteIsr 打断而错址(1.5.29 修复)。 */ * PDO_InputMapping→HW_EscWriteIsr 打断而错址。 */
if (NVIC_GetEnableIRQ(EXTI0_IRQn) != 0U) { if (NVIC_GetEnableIRQ(EXTI0_IRQn) != 0U) {
prev |= 1U; prev |= 1U;
NVIC_DisableIRQ(EXTI0_IRQn); NVIC_DisableIRQ(EXTI0_IRQn);
+86 -87
View File
@@ -7,19 +7,17 @@
* - A2=1,A1=0 (偏移 0x40000) -> HBI_DATA_0 低 16 位 * - A2=1,A1=0 (偏移 0x40000) -> HBI_DATA_0 低 16 位
* - A2=1,A1=1 (偏移 0x60000) -> HBI_DATA_0 高 16 位 * - A2=1,A1=1 (偏移 0x60000) -> HBI_DATA_0 高 16 位
* *
* 地址线换算(1.5.27 修正,此前 0x20000/0x30000 是按 8-bit 规则算的,DATA 访问全部 * 地址线换算(16-bit 数据宽度时 EXMC 内部将 HADDR[25:1] 接到 EXMC_A[24:0]):
* 落在 HBI_IDX_0 高半字,IDX_0=0x1234 探针假通过而 BYTE_TEST 永远失败):
* 板线:LAN9252 A1(Pin25)→PD11=EXMC_A16,A2(Pin29)→PD12=EXMC_A17。 * 板线:LAN9252 A1(Pin25)→PD11=EXMC_A16,A2(Pin29)→PD12=EXMC_A17。
* GD32E50x 用户手册 p869:16-bit 数据宽度时 EXMC 内部将 HADDR[25:1] 接到 * GD32E50x 用户手册 p869:16-bit 数据宽度时 EXMC 内部将 HADDR[25:1] 接到
* EXMC_A[24:0],即 EXMC_A16=CPU 偏移 bit17(0x20000)、EXMC_A17=bit18(0x40000)。 * EXMC_A[24:0],即 EXMC_A16=CPU 偏移 bit17(0x20000)、EXMC_A17=bit18(0x40000)。
* LAN9252 手册 Table 9-3:A2=1 选 HBI_DATA_0,A1 选低/高半字。 * 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 等)与访问端口; * HBI 索引直接寻址的只有 LAN9252 系统 CSR(BYTE_TEST 0x064 等)与访问端口;
* EtherCAT 核心 CSR(0x0000-0x0FFF)经 ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300) * EtherCAT 核心 CSR(0x0000-0x0FFF)经 ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300)
* 间接访问,过程 RAM(0x1000-0x1FFF)经 PRAM FIFO(0x000-0x03C + 0x308-0x314) 访问。 * 间接访问,过程 RAM(0x1000-0x1FFF)经 PRAM FIFO(0x000-0x03C + 0x308-0x314) 访问。
* 1.5.37 及之前把索引空间当统一寻址,导致全部核心 CSR 读 0/写丢弃—— * 核心 CSR 不在 HBI 直接空间,直读恒 0、写丢弃。
* 主站恒读 AL=INIT 的真正根因(网络侧寄存器诊断证实 EEPROM/核心一直正常)。
*/ */
#include "comm/esc_lan9252.h" #include "comm/esc_lan9252.h"
#include "comm/esc_exmc_bus.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 引脚。 */ constexpr uint32_t INT_EN_ECAT_INT = 1UL << 0; /**< 允许 EtherCAT 事件驱动 IRQ 引脚。 */
/* ESC 过程数据看门狗(DS 12.14.41-43)。SII 不携带看门狗字段,芯片复位默认 /* 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。 */ * 100ms 手动调试周期,SM2 每 100ms 被判定超时,SSC 以 0x001B 拖回 SAFEOP。 */
constexpr uint16_t ESC_REG_WD_DIVIDER = 0x0400U; /**< 看门狗分频(100µs 基准,复位默认 0x09C2)。 */ constexpr uint16_t ESC_REG_WD_DIVIDER = 0x0400U; /**< 看门狗分频(100µs 基准,复位默认 0x09C2)。 */
constexpr uint16_t ESC_REG_WD_TIME_PD = 0x0420U; /**< 过程数据看门狗步数(复位默认 0x03E8=100ms)。 */ constexpr uint16_t ESC_REG_WD_TIME_PD = 0x0420U; /**< 过程数据看门狗步数(复位默认 0x03E8=100ms)。 */
@@ -233,9 +231,8 @@ bool EscLan9252::s_pmSuspended = false;
void EscLan9252::hardwareReset() void EscLan9252::hardwareReset()
{ {
/* 受控复位 ECAT_NRST(PB10 → LAN9252 Pin11 RST#,SCH 第1页 Pin47)。 /* 受控复位 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 * 统一 30ms 低脉宽同时覆盖两个下限。RST# 释放瞬间重锁 CHIP_MODE0/1 strap
* (LED_LINK1/2 经 R35/R36 10k 下拉 → 00b = 2 端口 HBI 模式)。 * (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# 拉低 */ gpio_bit_reset(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN); /* RST# 拉低 */
SysTickTimer::delayMs(CONFIG_ESC_NRST_ASSERT_MS); SysTickTimer::delayMs(CONFIG_ESC_NRST_ASSERT_MS);
gpio_bit_set(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN); /* 释放复位 */ 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 输入通路常连引脚,输出态也能读到真实电压。 /* 回读实际电平:GD32 输入通路常连引脚,输出态也能读到真实电压。
* 驱动高却读回低 => ECAT_NRST 网络被外部短路到地(或引脚损伤), * 驱动高却读回低 => ECAT_NRST 网络被外部短路到地(或引脚损伤),
* LAN9252 被按死在复位态 —— 直接解释 HBI 全浮空 + 晶振照振。 */ * LAN9252 被按死在复位态 —— 直接解释 HBI 全浮空 + 晶振照振。 */
@@ -255,7 +255,7 @@ void EscLan9252::hardwareReset()
printf("[ECAT][ESC] hardwareReset: RST# low %ums -> released, pin reads %s\r\n", printf("[ECAT][ESC] hardwareReset: RST# low %ums -> released, pin reads %s\r\n",
(unsigned)CONFIG_ESC_NRST_ASSERT_MS, (unsigned)CONFIG_ESC_NRST_ASSERT_MS,
pinHigh ? "HIGH" : "*** LOW (net shorted to GND / pin damaged) ***"); pinHigh ? "HIGH" : "*** LOW (net shorted to GND / pin damaged) ***");
/* RST# 复位清掉 PM 睡眠态(芯片回 D0),挂起门控随之解除(1.6.27) */ /* RST# 复位清掉 PM 睡眠态(芯片回 D0),挂起门控随之解除 */
s_pmSuspended = false; 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, gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
CONFIG_ESC_NRST_PIN); CONFIG_ESC_NRST_PIN);
gpio_bit_set(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN); gpio_bit_set(CONFIG_ESC_NRST_PORT, CONFIG_ESC_NRST_PIN);
/* 复位释放后芯片重走上电流程回 D0,睡眠挂起随之解除(1.6.27) */ /* 复位释放后芯片重走上电流程回 D0,睡眠挂起随之解除 */
s_pmSuspended = false; s_pmSuspended = false;
break; break;
case 1: /* 持续拉低(手动保持复位) */ case 1: /* 持续拉低(手动保持复位) */
@@ -317,12 +317,12 @@ uint8_t EscLan9252::pmDrive(uint8_t op)
uint8_t status; uint8_t status;
if (op == 0U) { if (op == 0U) {
/* 状态查询。注意:1.6.24 实测 HBI 读事务本身就会唤醒睡眠中的芯片, /* 状态查询。注意:HBI 读事务本身就会唤醒睡眠中的芯片,
* 本操作的语义 = "读状态(若在睡眠则主动打破睡眠)",非无侵入观测。 */ * 本操作的语义 = "读状态(若在睡眠则主动打破睡眠)",非无侵入观测。 */
const uint32_t pmt = readReg32(LAN9252_REG_PMT_CTRL); const uint32_t pmt = readReg32(LAN9252_REG_PMT_CTRL);
status = pmPackStatus(pmt); status = pmPackStatus(pmt);
/* 读到 D0(SLEEP_EN=0):芯片不在睡眠,若门控仍挂起(如 op4 唤醒超时后 /* 读到 D0(SLEEP_EN=0):芯片不在睡眠,若门控仍挂起(如 op4 唤醒超时后
* 芯片延迟自醒)则自愈解除,恢复主循环/ISR 的 HBI 访问(1.6.27)。 */ * 芯片延迟自醒)则自愈解除,恢复主循环/ISR 的 HBI 访问。 */
if ((status & 0x02U) == 0U && s_pmSuspended) { if ((status & 0x02U) == 0U && s_pmSuspended) {
s_pmSuspended = false; s_pmSuspended = false;
printf("[ECAT][PM] status: chip in D0, pm suspend gate released\r\n"); 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 无效)。 */ /* PM_MODE 与 PM_SLEEP_EN 必须分两次写(DS 6.3.5.1 注:同一写中改 MODE 置 EN 无效)。 */
writeReg32(LAN9252_REG_PMT_CTRL, v); writeReg32(LAN9252_REG_PMT_CTRL, v);
writeReg32(LAN9252_REG_PMT_CTRL, v | PMT_CTRL_PM_SLEEP_EN); 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, * ethercat_sync 三 ISR 清 pending 后直接返回)——本函数运行于 CS EXTI ISR,
* 退出后主循环第一圈 ECAT_Main 读 0x220 就会把刚睡着的芯片叫醒; * 退出后主循环第一圈 ECAT_Main 读 0x220 就会把刚睡着的芯片叫醒;
* 此前置位窗口内本 ISR 是唯一运行者(maskEnter 已屏 EXTI0/1),无竞态。 */ * 此前置位窗口内本 ISR 是唯一运行者(maskEnter 已屏 EXTI0/1),无竞态。 */
s_pmSuspended = true; s_pmSuspended = true;
/* 1.6.24 实测:进入睡眠后任何 HBI 访问(含读!)都会立即触发唤醒—— /* 进入睡眠后任何 HBI 访问(含读!)都会立即触发唤醒——
* 手册 6.3.4.2 只说 HBI "write" 唤醒,但芯片实际按主机 CS 活动判定, * 手册 6.3.4.2 只说 HBI "write" 唤醒,但芯片实际按主机 CS 活动判定,
* 写入后的读回验证会在 <50ms 内把芯片叫回 D0(板级复现:进 D1 后 * 写入后的读回验证会把芯片叫回 D0。
* 3s/6s 静默仍自动回 D0,唯读回验证为已知访问;去掉后待验证)。
* 故此处置信构造返回值,不再读回;睡眠态观测交给"读状态"(读即唤醒, * 故此处置信构造返回值,不再读回;睡眠态观测交给"读状态"(读即唤醒,
* 语义=主动打破睡眠),页面轮询须在睡眠态暂停。 */ * 语义=主动打破睡眠),页面轮询须在睡眠态暂停。 */
status = (uint8_t)(0x02U | ((uint32_t)op << 2)); /* READY=0 | SLEEP_EN=1 | MODE=op */ 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)"); restored ? "OK" : "FAIL (retry action 34)");
} }
if (awake) { if (awake) {
/* 唤醒成功:解除 HBI 挂起,主循环/ISR 恢复运行(1.6.27)。 /* 唤醒成功:解除 HBI 挂起,主循环/ISR 恢复运行。
* 超时则保持挂起——芯片大概率仍睡着;若实际已醒,下一次 op0 * 超时则保持挂起——芯片大概率仍睡着;若实际已醒,下一次 op0
* 状态查询读到 D0 会自愈解除。 */ * 状态查询读到 D0 会自愈解除。 */
s_pmSuspended = false; s_pmSuspended = false;
@@ -407,69 +406,76 @@ uint8_t EscLan9252::pmDrive(uint8_t op)
bool EscLan9252::init() bool EscLan9252::init()
{ {
s_busReady = false; /* 1.6.40:s_busReady = EXMC+BYTE_TEST 往返通过(IDX 残值不挡) */ s_busReady = false; /* EXMC+BYTE_TEST; warm 先探再定是否 RST# */
s_coreReady = false; /* 1.5.39:s_coreReady 语义 = EEPROM 加载完成、EtherCAT 核心使能 */ s_coreReady = false; /* s_coreReady 语义 = EEPROM 加载完成、EtherCAT 核心使能 */
/* 使能 DWT 周期计数器,供 hw_gd32exmc/SSC 执行时间测量 */ /* 使能 DWT 周期计数器,供 hw_gd32exmc/SSC 执行时间测量 */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0U; DWT->CYCCNT = 0U;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
/* 受控 RST# 复位(仅首次;重复调用 init 不得二次复位,否则清掉 SSC 配置) */ /* 先起 EXMC 再谈复位。
if (!s_resetDone) { * warm jump:BOOT 刚用过 HBI,芯片活着;此时若先 RST# 再起 EXMC,等于
hardwareReset(); * 把一条本来能通的总线按进复位重协商,一旦 RST# 释放时序不稳就整段假死。
s_resetDone = true; * cold:POR 后芯片也需要 EXMC 才谈得上探活。
} * 顺序固定为:EXMC → 无复位探 BYTE_TEST → 不通才 RST# 重来。 */
if (!EscExmcBus::init()) { if (!EscExmcBus::init()) {
printf("[ECAT][ESC] init: FAIL, EXMC bus not ready (see [ECAT][EXMC] log)\r\n"); printf("[ECAT][ESC] init: FAIL, EXMC bus not ready (see [ECAT][EXMC] log)\r\n");
return false; 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; uint16_t idx0 = 0U;
uint32_t byteTest = 0U; uint32_t byteTest = 0U;
for (uint8_t attempt = 0U; attempt < 2U; ++attempt) {
if (attempt > 0U) { /* 第 0 轮:不复位直接探。warm 残值下 BOOT 刚写过 HBI,BYTE_TEST 应立刻可读。 */
printf("[ECAT][ESC] probe#%u fail (idx0=0x%04X byte_test=0x%08lX), 补发 RST# 重探\r\n", idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
(unsigned)attempt, (unsigned)idx0, (unsigned long)byteTest); byteTest = readReg32(LAN9252_REG_BYTE_TEST);
hardwareReset(); if (byteTest == LAN9252_BYTE_TEST_VALUE) {
} printf("[ECAT][ESC] probe: BYTE_TEST ok without RST# (idx0=0x%04X) — 总线已活,跳过复位\r\n",
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET); (unsigned)idx0);
/* 仅在「像复位中/未驱动」时轮询 0x1234;warm 残值(非 0/0xFFFF/0x1234)说明 s_resetDone = true;
* 芯片活着且带着上次索引,直接测 BYTE_TEST,不空等。 */ } else {
const bool idxLooksIdle = (idx0 == 0x0000U) || (idx0 == 0xFFFFU); printf("[ECAT][ESC] probe: BYTE_TEST=0x%08lX idx0=0x%04X, 补 RST# 最多 3 轮\r\n",
if (idxLooksIdle || idx0 == 0x1234U) { (unsigned long)byteTest, (unsigned)idx0);
for (uint32_t waited = 0U; bool alive = false;
idx0 != 0x1234U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS; for (uint8_t attempt = 0U; attempt < 3U; ++attempt) {
waited += CONFIG_ESC_HBI_POLL_STEP_MS) { if (!s_resetDone || attempt > 0U) {
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS); hardwareReset();
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET); 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 (!alive) {
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",
break; (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) { if (idx0 != 0x1234U) {
printf("[ECAT][ESC] init: WARN idx0=0x%04X != 0x1234, BYTE_TEST ok -> 复位态未知但通路可用,放行\r\n", printf("[ECAT][ESC] init: WARN idx0=0x%04X != 0x1234, BYTE_TEST ok -> 复位态未知但通路可用,放行\r\n",
(unsigned)idx0); (unsigned)idx0);
@@ -478,10 +484,9 @@ bool EscLan9252::init()
(unsigned)idx0); (unsigned)idx0);
s_busReady = true; s_busReady = true;
/* 等待 PDI 就绪、EtherCAT 核心可交互(1.5.37 新增,1.5.39 改经 CSR 间接端口读—— /* 等待 PDI 就绪、EtherCAT 核心可交互。
* 此前直读 HBI 索引,DL_STATUS 属 EtherCAT 核心寄存器,根本不在直接地址空间)。 * DL_STATUS 属 EtherCAT 核心寄存器,不在 HBI 直接空间,必须经 CSR 间接端口读。
* 判据:DL_STATUS(0x0110) bit0 = PDI operational / EEPROM loaded。 * 判据:DL_STATUS(0x0110) bit0 = PDI operational / EEPROM loaded。
* 网络侧诊断已证实本板 EEPROM 加载与核心均正常,此等待应几乎立即通过;
* 超时意味着 PDI 真的未就绪(EEPROM 内容/I2C/strap 问题),此时 SSC 的 * 超时意味着 PDI 真的未就绪(EEPROM 内容/I2C/strap 问题),此时 SSC 的
* AL Event Mask/邮箱 SM 配置会落空,由 initial.cpp 按 isCoreReady() 门控跳过。 */ * AL Event Mask/邮箱 SM 配置会落空,由 initial.cpp 按 isCoreReady() 门控跳过。 */
uint16_t dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U, false); 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。 * @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,即 * (HBI 索引直接可达,不走 CSR 间接端口),复位默认值均为 0,即
* IRQ_EN=0(引脚永久去断言) 且 ECAT_INT_EN=0(EtherCAT 事件不上报)。 * IRQ_EN=0(引脚永久去断言) 且 ECAT_INT_EN=0(EtherCAT 事件不上报)。
* 此前固件从未配置它们,导致 PDI_Isr 一次都没被调用过:SSC 因此 * 不配这两个寄存器,PDI_Isr 不会触发,SSC 永远等不到 PROCESS_OUTPUT_EVENT,
* 永远等不到 PROCESS_OUTPUT_EVENT、bEcatFirstOutputsReceived 恒 FALSE,
* SAFEOP->OP 被无限挂起(表现为主站 "进 OP 失败 code=0x0000")。 * SAFEOP->OP 被无限挂起(表现为主站 "进 OP 失败 code=0x0000")。
* 极性必须与 ethercat_sync.cpp 的 EXTI 配置一致:那里是上拉输入 + * 极性必须与 ethercat_sync.cpp 的 EXTI 配置一致:那里是上拉输入 +
* 下降沿触发,故此处选推挽(IRQ_TYPE=1) + 低有效(IRQ_POL=0)。 * 下降沿触发,故此处选推挽(IRQ_TYPE=1) + 低有效(IRQ_POL=0)。
@@ -521,7 +525,7 @@ bool EscLan9252::init()
*/ */
void EscLan9252::configPdWatchdog() void EscLan9252::configPdWatchdog()
{ {
/* 1.5.45:过程数据看门狗 100ms(复位默认)→1s。 /* 过程数据看门狗 100ms(复位默认)→1s。
* 主站侧手动单步/100ms 调试周期会触发 SM2 看门狗:SSC 在 CheckIfEcatError() * 主站侧手动单步/100ms 调试周期会触发 SM2 看门狗:SSC 在 CheckIfEcatError()
* 读 0x0440 发现已过期,调 AL_ControlInd(STATE_SAFEOP, 0x001B) 把 OP 拖回 SAFEOP, * 读 0x0440 发现已过期,调 AL_ControlInd(STATE_SAFEOP, 0x001B) 把 OP 拖回 SAFEOP,
* 表现为"100ms 周期全 WKC 错、10/50ms 正常"。分频保持默认 100µs,只延长步数; * 表现为"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; /* DS 12.11.1:写 CSR_CMD(BUSY|R_nW|SIZE|ADDR) 启动,busy 自清后读 CSR_DATA;
* 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。 * 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。
* 1.5.59 加固(verify=true):CSR_DATA 是锁存寄存器(非 FIFO),命令不变时连读两次必一致; * verify=true(默认):CSR_DATA 是锁存寄存器(非 FIFO),命令不变时连读两次必一致;
* 不一致即本拍 EXMC 毛刺——历史上这类错读曾让 GetSyncMan 把 0x1204/0x1208 * 不一致即本拍 EXMC 毛刺——错读可能让 GetSyncMan 把错误值缓存成邮箱几何
* 缓存成邮箱几何(→SSC 邮箱楔死,根治见 mailbox.c 构建期常量),或让 * (→SSC 邮箱楔死,邮箱几何另有 mailbox.c 构建期常量兜底),或让
* CheckSmSettings 以 0x0016/0x001F 瞬态打断状态转换。对错读整事务重跑一次; * CheckSmSettings 以瞬态值打断状态转换。对错读整事务重跑一次;
* 两轮仍不一致则交出末拍(与旧行为一致的兜底,邮箱几何另有常量兜底)。 * 两轮仍不一致则交出末拍。
* 1.6.25 verify=false:单读路径,供诊断回读/MII busy 轮询/init 轮询等低风险 * verify=false:单读路径,供诊断回读/MII busy 轮询/init 轮询等低风险
* 调用方使用;SM/AL/邮箱等状态机关键寄存器必须保持默认双读。 */ * 调用方使用;SM/AL/邮箱等状态机关键寄存器必须保持默认双读。 */
if (!verify) { if (!verify) {
uint32_t n = POLL_MAX; 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)。 * 首 DWORD 有效字节从 (addr&3) 起(表 12-7),末 DWORD 尾部无效(表 12-8)。
* 别名口 0x00-0x1C 功能相同,轮换寻址即官方突发用法;每读满一个 DWORD 弹一项。 * 别名口 0x00-0x1C 功能相同,轮换寻址即官方突发用法;每读满一个 DWORD 弹一项。
* *
* 1.6.2 回退逐 DWORD 轮询,弹前必查 AVAIL 位。1.6.0 曾信 CMD 的 * 必须逐 DWORD 轮询并弹前查 AVAIL 位。CMD 的 AVAIL_CNT(bits[12:8])批量弹模式
* AVAIL_CNT(bits[12:8])一次弹一批:实测 12B 短读(1.6.0 输出探针,主站窗口 * 对短读不可靠:首 DWORD 新鲜、后续 DWORD 可能取到 FIFO 陈旧项。邮箱短读/探针
* 请求 WIN@8 靠它送达)首 DWORD 新鲜、后续 DWORD 取到 FIFO 陈旧项,窗口 * 类小传输只验证过"逐 DWORD 查 AVAIL"模式,不要改回批量弹 DWORD。 */
* 基址冻结,槽位 7..26 的电机状态因此永不上报(仅 seq 沿触发的 384B 全量读
* 路径能拿到新 WIN)。批量模式 1.6.0 上机对拍本来就是零收益,384/496B 长读
* 未见损坏,但邮箱短读/探针这类小传输只验证过"逐 DWORD 查 AVAIL"的旧模式
* ——在弄清该计数器对短读的精确语义之前,不要恢复批量弹 DWORD。 */
if (len == 0U) { if (len == 0U) {
return; 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) 起, /* DS 12.11.4:写 ADDR_LEN 再置 BUSY 启动;首 DWORD 有效字节从 (addr&3) 起,
* 引擎只取有效字节;别名口 0x20-0x3C 轮换;写满一个 DWORD 推一项。 * 引擎只取有效字节;别名口 0x20-0x3C 轮换;写满一个 DWORD 推一项。
* 1.6.2 与 pramRead 同步回退逐 DWORD 轮询(推前必查 AVAIL 位):1.6.0 批量 * 与 pramRead 同样逐 DWORD 轮询(推前必查 AVAIL 位),邮箱小 PDU 写同类风险。 */
* 模式实测零收益,且与短读损坏同源(见 pramRead 注),邮箱小 PDU 写同类风险。 */
if (len == 0U) { if (len == 0U) {
return; return;
} }
@@ -683,7 +682,7 @@ uint32_t EscLan9252::readChipId()
uint16_t EscLan9252::initOnlyReadReg(uint16_t addr) uint16_t EscLan9252::initOnlyReadReg(uint16_t addr)
{ {
/* 1.5.39:ESC 核心 CSR 经间接端口读(此前直读 HBI 索引,核心寄存器不在直接空间,恒 0) */ /* ESC 核心 CSR 经间接端口读(核心寄存器不在 HBI 直接空间,直读恒 0) */
return (uint16_t)csrRead(addr, 2U); return (uint16_t)csrRead(addr, 2U);
} }
+16 -16
View File
@@ -2,7 +2,7 @@
* @file esc_lan9252.h * @file esc_lan9252.h
* @brief LAN9252 EtherCAT 从站控制器(ESC)驱动接口。 * @brief LAN9252 EtherCAT 从站控制器(ESC)驱动接口。
* @note 通过 EscExmcBus 16-bit 并行总线,以 HBI Indexed Address Mode 访问 LAN9252。 * @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 等)+ 访问端口 * - HBI 直接地址空间:仅 LAN9252 系统 CSR(BYTE_TEST 0x064 等)+ 访问端口
* (PRAM FIFO 0x000-0x03C、ECAT_CSR_DATA/CMD 0x300/0x304、PRAM_ADDR_LEN/CMD * (PRAM FIFO 0x000-0x03C、ECAT_CSR_DATA/CMD 0x300/0x304、PRAM_ADDR_LEN/CMD
* 0x308-0x314)——经 HBI_IDX_0/HBI_DATA_0 直接读写; * 0x308-0x314)——经 HBI_IDX_0/HBI_DATA_0 直接读写;
@@ -29,7 +29,7 @@
class EscLan9252 { class EscLan9252 {
public: public:
/** @brief 初始化 EXMC 总线与 ESC 基本寄存器。 /** @brief 初始化 EXMC 总线与 ESC 基本寄存器。
* @note 1.5.25 起内部先执行一次 hardwareReset() 受控复位(s_resetDone 门控, * @note 内部先执行一次 hardwareReset() 受控复位(s_resetDone 门控,
* 重复调用不会二次复位 SSC 已配置好的芯片),再探活 EXMC/HBI。 * 重复调用不会二次复位 SSC 已配置好的芯片),再探活 EXMC/HBI。
* @return bool true 表示 EXMC 总线探活通过(可继续读取芯片 ID)。 */ * @return bool true 表示 EXMC 总线探活通过(可继续读取芯片 ID)。 */
static bool init(); static bool init();
@@ -65,20 +65,20 @@ public:
static uint32_t readChipId(); static uint32_t readChipId();
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SSC 移植层(HW_*)。 /** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SSC 移植层(HW_*)。
* @note 1.5.39 起经 CSR_CMD/CSR_DATA 间接端口读 ESC 核心 CSR(此前直读 HBI 索引, * @note 经 CSR_CMD/CSR_DATA 间接端口读 ESC 核心 CSR
* 而 EtherCAT 核心寄存器根本不在 HBI 直接地址空间,恒读 0)。 */ * (EtherCAT 核心寄存器不在 HBI 直接地址空间,直读恒 0)。 */
static uint16_t initOnlyReadReg(uint16_t addr); static uint16_t initOnlyReadReg(uint16_t addr);
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用。 */ /** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用。 */
static void initOnlyWriteReg(uint16_t addr, uint16_t v); static void initOnlyWriteReg(uint16_t addr, uint16_t v);
/** @brief ESC 核心 CSR 间接读(ECAT_CSR_CMD/DATA 端口,0x0000-0x0FFF 地址空间)。 /** @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)必须经 * (DS 12.11/表 12-13),EtherCAT 核心寄存器(0x0000-0x0FFF)必须经
* CSR_CMD(0x304)/CSR_DATA(0x300) 间接访问;过程 RAM(0x1000-0x1FFF) * CSR_CMD(0x304)/CSR_DATA(0x300) 间接访问;过程 RAM(0x1000-0x1FFF)
* 必须经 PRAM FIFO。size=1/2/4(size2 需偶地址,size4 需 4 字节对齐)。 * 必须经 PRAM FIFO。size=1/2/4(size2 需偶地址,size4 需 4 字节对齐)。
* 调用方负责 HBI 互斥(主循环 EscExmcBus::maskEnter / 已在 ISR 上下文)。 * 调用方负责 HBI 互斥(主循环 EscExmcBus::maskEnter / 已在 ISR 上下文)。
* @param verify true(默认)=双读比对+不一致重试(1.5.59,防 EXMC 毛刺,SM/AL/邮箱 * @param verify true(默认)=双读比对+不一致重试(防 EXMC 毛刺,SM/AL/邮箱
* 等关键路径必须开);false=单读(诊断回读/MII busy 轮询/init 等低风险路径, * 等关键路径必须开);false=单读(诊断回读/MII busy 轮询/init 等低风险路径,
* 省约一半 CSR 间接访问)。 */ * 省约一半 CSR 间接访问)。 */
static uint32_t csrRead(uint16_t addr, uint8_t size, bool verify = true); 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); static void pramWrite(uint16_t addr, const uint8_t *buf, uint16_t len);
/** @brief EXMC 总线 + BYTE_TEST 往返探针通过(init 成功)后为 true。 /** @brief EXMC 总线 + BYTE_TEST 往返探针通过(init 成功)后为 true。
* @note 1.6.40 起 HBI_IDX_0 复位默认值降级为诊断读数,不再参与放行 * @note HBI_IDX_0 复位默认值仅作诊断读数,不参与放行
* (它只证明"芯片刚出过复位",证明不了 HBI 通路)。 * (它只证明"芯片刚出过复位",证明不了 HBI 通路)。
* 与 init() 返回值一致,可作运行期查询。 */ * 与 init() 返回值一致,可作运行期查询。 */
static bool isBusReady() { return s_busReady; } static bool isBusReady() { return s_busReady; }
/** @brief EEPROM(SII) 加载完成、EtherCAT 核心使能(1.5.39)。 /** @brief EEPROM(SII) 加载完成、EtherCAT 核心使能。
* @note false 时核心寄存器对 PDI 读全 0、写静默丢弃,SSC MainInit 会全部 * @note false 时核心寄存器对 PDI 读全 0、写静默丢弃,SSC MainInit 会全部
* 落空(AL Event Mask/邮箱 SM 配置写不进),调用方必须跳过 SSC 初始化。 */ * 落空(AL Event Mask/邮箱 SM 配置写不进),调用方必须跳过 SSC 初始化。 */
static bool isCoreReady() { return s_coreReady; } static bool isCoreReady() { return s_coreReady; }
/** @brief ESC 已被 pmDrive 置入 D1-D3 睡眠、所有 HBI 消费者必须静默(1.6.27)。 /** @brief ESC 已被 pmDrive 置入 D1-D3 睡眠、所有 HBI 消费者必须静默。
* @note 1.6.24 实测睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。置位期间 * @note 睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。置位期间
* task_runner 停调 MainLoop()/AL 守护/AL 心跳,ethercat_sync 三个 EXTI * task_runner 停调 MainLoop()/AL 守护/AL 心跳,ethercat_sync 三个 EXTI
* ISR 清 pending 后直接返回——否则芯片进入睡眠后毫秒级就被主循环的 * ISR 清 pending 后直接返回——否则芯片进入睡眠后毫秒级就被主循环的
* ECAT_Main(读 0x220)叫醒,根本睡不住(2026-09-12 休眠 ~20s 即"复活")。 * ECAT_Main(读 0x220)叫醒,根本睡不住。
* 解除:pmDrive 唤醒/读到 D0、nrstDrive 释放复位、hardwareReset。 * 解除:pmDrive 唤醒/读到 D0、nrstDrive 释放复位、hardwareReset。
* ISR 上下文安全:读一个 bool 静态成员,无锁。 */ * ISR 上下文安全:读一个 bool 静态成员,无锁。 */
static bool pmSuspended() { return s_pmSuspended; } static bool pmSuspended() { return s_pmSuspended; }
private: private:
/** @brief 使能 LAN9252 的 IRQ 引脚输出(IRQ_CFG 0x054 / INT_EN 0x05C)。 /** @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 从不执行, * 不配则 EtherCAT 事件永不到达 PB1、SSC 的 PDI_Isr 从不执行,
* bEcatFirstOutputsReceived 恒 FALSE 使 SAFEOP->OP 被无限挂起。 */ * SAFEOP->OP 被无限挂起。 */
static void enableEcatIrqOutput(); static void enableEcatIrqOutput();
/** @brief 过程数据看门狗 100ms(复位默认)→1s(1.5.45)。 /** @brief 过程数据看门狗 100ms(复位默认)→1s。
* @note 由 init() 在 PDI/核心就绪后调用。写 ESC_REG_WD_TIME_PD(0x0420)=10000 * @note 由 init() 在 PDI/核心就绪后调用。写 ESC_REG_WD_TIME_PD(0x0420)=10000
* (100µs 分频保持默认)。不写则主站 100ms 手动调试周期触发 SM2 看门狗, * (100µs 分频保持默认)。不写则主站 100ms 手动调试周期触发 SM2 看门狗,
* SSC 以 0x001B 拖回 SAFEOP。 */ * SSC 以 0x001B 拖回 SAFEOP。 */
static void configPdWatchdog(); static void configPdWatchdog();
static bool s_busReady; /**< init 成功(EXMC+BYTE_TEST 往返,1.6.40;IDX 残值不挡)后置位。 */ static bool s_busReady; /**< init 成功(EXMC+BYTE_TEST 往返;IDX 残值不挡)后置位。 */
static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位(1.5.39)。 */ static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位。 */
static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */ static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */
static bool s_pmPhyDown; /**< pmDrive 进 D3 时两 PHY 已被置 PowerDown,唤醒后需恢复。 */ static bool s_pmPhyDown; /**< pmDrive 进 D3 时两 PHY 已被置 PowerDown,唤醒后需恢复。 */
static bool s_pmSuspended; /**< ESC 睡眠挂起:置位期间全固件 HBI 消费者静默(见 pmSuspended)。 */ static bool s_pmSuspended; /**< ESC 睡眠挂起:置位期间全固件 HBI 消费者静默(见 pmSuspended)。 */
+100 -24
View File
@@ -1,10 +1,11 @@
/** /**
* @file ethercat_can_bridge.cpp * @file ethercat_can_bridge.cpp
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.6.21:段 2 电机整轮密排)。 * @brief EtherCAT PDO ↔ CAN 转发桥实现。
*/ */
#include "comm/ethercat_can_bridge.h" #include "comm/ethercat_can_bridge.h"
#include "device/canfd.h" #include "device/canfd.h"
#include "device/motor_state.h" #include "device/motor_state.h"
#include "sys/systick.h"
#include "RTE_Components.h" #include "RTE_Components.h"
#include CMSIS_device_header /* __DMB():seqlock 的编译器/总线屏障 */ #include CMSIS_device_header /* __DMB():seqlock 的编译器/总线屏障 */
@@ -17,6 +18,9 @@ volatile bool EthercatCanBridge::s_coldStartPending = false;
volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U; volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U;
volatile uint16_t EthercatCanBridge::s_txCount = 0U; volatile uint16_t EthercatCanBridge::s_txCount = 0U;
volatile uint16_t EthercatCanBridge::s_dropCount = 0U; volatile uint16_t EthercatCanBridge::s_dropCount = 0U;
volatile 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_rxValid = false;
volatile bool EthercatCanBridge::s_rxPublished = 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; volatile uint32_t EthercatCanBridge::s_rxCount = 0U;
uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0}; 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}; uint8_t EthercatCanBridge::s_motShadow[kEcatMotRegionBytes] = {0};
uint32_t EthercatCanBridge::s_motPubSeq[kMotorMaxNodes + 1U] = {0}; uint32_t EthercatCanBridge::s_motPubSeq[kMotorMaxNodes + 1U] = {0};
uint8_t EthercatCanBridge::s_motPubOnline[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) { if (r == CanFd::CanTxResult::kOk) {
++s_txCount; ++s_txCount;
++sent; ++sent;
s_lastTxId = rd32le(&f[ECAT_FRM_OFF_ID]);
} else { } else {
s_lastTxResult = (uint8_t)r; s_lastTxResult = (uint8_t)r;
if (stopOnError) { if (stopOnError) {
@@ -170,7 +189,9 @@ uint8_t EthercatCanBridge::fillMotorRound(uint8_t *in)
const uint16_t region = motRegionLen(unchanged ? s_motPubCount : n); 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); memcpy(&in[ECAT_MOTW_OFF_COUNT], s_motShadow, region);
return s_motPubCount; return s_motPubCount;
} }
@@ -216,13 +237,14 @@ uint8_t EthercatCanBridge::fillMotorRound(uint8_t *in)
bool EthercatCanBridge::peekOutput(const uint8_t *head) bool EthercatCanBridge::peekOutput(const uint8_t *head)
{ {
/* 输出探针:12B 头里 SEQ@0 / FLAGS。seq 无变化沿且无清统计请求时返回 false,
* PDO_OutputMapping 只补读 SM2 末字,跳过 384B 全量读。
* 恒 true 会让 8kHz 每拍都全量读,单拍被 PRAM 打爆(6kHz 即掉速)。 */
if (head == nullptr) { if (head == nullptr) {
return false; return false;
} }
const uint8_t seq = head[ECAT_CAN_TX_OFF_SEQ]; const uint8_t seq = head[ECAT_CAN_TX_OFF_SEQ];
/* WIN@8 不再使用:1.6.21 电机整轮密排,无滑窗。 */
if (!s_seqPrimed) { if (!s_seqPrimed) {
s_seqPrimed = true; s_seqPrimed = true;
s_lastSeq = seq; s_lastSeq = seq;
@@ -250,10 +272,10 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
} }
const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ]; const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ];
s_outSeenSeq = seq; /* 诊断:processOutput 本拍看到的输出 seq */
if (!s_seqPrimed) { if (!s_seqPrimed) {
s_seqPrimed = true; s_seqPrimed = true;
s_lastSeq = seq;
s_coldStartPending = true; s_coldStartPending = true;
} }
@@ -262,20 +284,28 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
s_txCount = 0U; s_txCount = 0U;
s_dropCount = 0U; s_dropCount = 0U;
s_rxCount = 0U; s_rxCount = 0U;
s_rx00ffCount = 0U;
s_rxPublished = true; 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; bool executeCommand = false;
if (s_coldStartPending) { if (s_coldStartPending) {
s_coldStartPending = false; s_coldStartPending = false;
/* 冷启动首包:若包含有效单帧命令或批量帧,立即执行 */ /* 冷启动:有载荷就发(不依赖 s_lastSeq 是否已被探针碰过) */
const uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC]; if (hasPayload) {
const uint8_t batchCount = out[ECAT_TXB_OFF_COUNT];
if (dlc > 0U || batchCount > 0U) {
executeCommand = true; executeCommand = true;
} else {
s_lastSeq = seq;
return;
} }
} else if (seq != s_lastSeq) { } else if (seq != s_lastSeq) {
executeCommand = true; executeCommand = true;
} else if (hasPayload && seq != s_lastSeq) {
executeCommand = true;
} }
if (!executeCommand) { if (!executeCommand) {
@@ -284,7 +314,7 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
s_lastSeq = seq; s_lastSeq = seq;
const uint8_t ch = out[ECAT_CAN_TX_OFF_CH]; 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) { if (dlc > kEcatCanTxDataMax) {
dlc = kEcatCanTxDataMax; dlc = kEcatCanTxDataMax;
} }
@@ -299,6 +329,7 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
s_lastTxResult = (uint8_t)r; s_lastTxResult = (uint8_t)r;
if (r == CanFd::CanTxResult::kOk) { if (r == CanFd::CanTxResult::kOk) {
++s_txCount; ++s_txCount;
s_lastTxId = rd32le(&out[ECAT_CAN_TX_OFF_ID]);
} }
} }
} }
@@ -311,7 +342,9 @@ void EthercatCanBridge::buildInput(uint8_t *in)
if (in == nullptr) { if (in == nullptr) {
return; return;
} }
memset(in, 0, kEcatPdoInBytes); /* 不整包 memset(600):8kHz 下每拍 Process 都会走到这里,600B 清零
* 与后续覆盖写重复。各段自己写满有效域;未用槽由 fillMotorRound
* 按 count 截断,主站只迭代 count,空洞区可为残值。 */
/* ── 段 1(0..31):桥状态 + 短快照 ── */ /* ── 段 1(0..31):桥状态 + 短快照 ── */
in[ECAT_CAN_RX_OFF_ACK] = s_lastSeq; 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_TXCNT], s_txCount);
wr16le(&in[ECAT_CAN_RX_OFF_DROP], s_dropCount); wr16le(&in[ECAT_CAN_RX_OFF_DROP], s_dropCount);
wr32le(&in[ECAT_CAN_RX_OFF_RXCNT], s_rxCount); wr32le(&in[ECAT_CAN_RX_OFF_RXCNT], s_rxCount);
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); 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); wr32le(&in[ECAT_CAN_RX_OFF_ID], s_rxId);
memcpy(&in[ECAT_CAN_RX_OFF_DATA], s_rxData, kEcatCanRxDataMax); memcpy(&in[ECAT_CAN_RX_OFF_DATA], s_rxData, kEcatCanRxDataMax);
s_rxPublished = true; 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 uint8_t ch = s_rxValid ? s_rxCh : 0U;
const uint32_t backlog = CanFd::isChannelEnabled(ch) ? CanFd::txQueueBacklog(ch) : 0U; const uint32_t backlog = CanFd::isChannelEnabled(ch) ? CanFd::txQueueBacklog(ch) : 0U;
in[ECAT_CAN_RX_OFF_BACKLOG] = (backlog > 255U) ? 255U : (uint8_t)backlog; in[ECAT_CAN_RX_OFF_BACKLOG] = (backlog > 255U) ? 255U : (uint8_t)backlog;
/* ── 段 2(32..543):电机整轮密排(空槽不发,count=实际电机数)── */ /* ── 段 2(32..543):电机整轮密排 ── */
(void)fillMotorRound(in); (void)fillMotorRound(in);
/* ── 段 3(544..583):最近一帧完整快照 ── /* ── 段 3(544..583):长帧快照(通用,不专宠版本) ──
* 与段 1 共用 xrxStable 门控,两段必须来自同一帧。 */ * 语义 =「最近一帧非 STATUS 长帧」:CONFIG/版本/节点ID 谁后到谁上。
if (xrxStable && s_rxValid) { * STATUS(0x0100) 由段2 密排承载,不进本槽(否则 48B STATUS 会淹没一切)。
in[ECAT_XRX_OFF_CH] = s_rxCh; * 例外:仅当刚收到 0x00FF 的短粘滞窗口内,优先摆版本应答,
in[ECAT_XRX_OFF_DLC] = s_rxDlc; * 给主站一个稳定采样窗;窗口结束后立刻交还给 CONFIG 等,避免其它长帧失效。 */
in[ECAT_XRX_OFF_FLAGS] = 0x01U; if (xrxStable) {
wr32le(&in[ECAT_XRX_OFF_ID], s_rxId); const uint32_t nowMs = SysTickTimer::getMs();
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax); 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, void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
@@ -362,6 +416,12 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
return; return;
} }
/* STATUS(0x0100) 由 MotorState 段2 密排承载,不写入 xrx。
* 原注释本就如此约定;实现曾漏过滤,导致高频 STATUS 盖掉 CONFIG/版本。 */
if (canSubId(id) == 0x0100U) {
return;
}
uint8_t copyLen = length; uint8_t copyLen = length;
if (copyLen > kEcatCanXRxDataMax) { if (copyLen > kEcatCanXRxDataMax) {
copyLen = kEcatCanXRxDataMax; copyLen = kEcatCanXRxDataMax;
@@ -388,11 +448,27 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
s_rxValid = true; s_rxValid = true;
s_rxPublished = false; 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(); __DMB();
++s_xrxSeq; ++s_xrxSeq;
/* STATUS 走 MotorState → 段 2 整轮;非 STATUS 只更新段 1/3 最近一帧快照。
* 1.6.21 起不再写入历史覆盖窗口。 */
} }
extern "C" { extern "C" {
+34 -21
View File
@@ -1,6 +1,6 @@
/** /**
* @file ethercat_can_bridge.h * @file ethercat_can_bridge.h
* @brief EtherCAT PDO ↔ CAN 转发桥(1.6.21:段 2 改 9 电机密排整轮)。 * @brief EtherCAT PDO ↔ CAN 转发桥。
* *
* 输出 PDO(0x7000, 主站→从站, 384B) = 待发送的 CAN 帧(数据区 32B) * 输出 PDO(0x7000, 主站→从站, 384B) = 待发送的 CAN 帧(数据区 32B)
* 输入 PDO(0x6000, 从站→主站, 600B) = 桥状态 + 电机整轮状态 + 长帧快照 + 诊断 * 输入 PDO(0x6000, 从站→主站, 600B) = 桥状态 + 电机整轮状态 + 长帧快照 + 诊断
@@ -13,23 +13,17 @@
* TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态 * TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态
* 会随机丢帧。改为由 taskCanRxParse() 的分发链回调 onCanFrame() 投递。 * 会随机丢帧。改为由 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 个); * · 每一拍输入 PDO 携带**总线上已识别电机的完整一轮状态**(最多 9 个);
* · **空槽不发送**:密排,count = 本拍实际打包的电机数; * · **空槽不发送**:密排,count = 本拍实际打包的电机数;
* · 主站按 count 迭代,每槽自带 node_id,自动识别,无需 win_base 滑窗; * · 主站按 count 迭代,每槽自带 node_id,自动识别,无需 win_base 滑窗;
* · 不发段 4 历史帧;版本/CONFIG 仍走段 1/段 3「最近一帧」快照; * · 版本/CONFIG 走段 1/段 3「最近一帧」快照;
* · 上一轮已打包且 seq 未变的电机,本轮跳过 MotorState 重读与 48B 拷贝, * · 上一轮已打包且 seq 未变的电机,本轮跳过 MotorState 重读与 48B 拷贝,
* 直接复用影子区(s_motShadow)。 * 直接复用影子区(s_motShadow)。
* *
* 输入 PDO 总长仍 600B ⇒ 无需重刷 EEPROM/SII;段 2/3 偏移变了,**主站解码 * 输入 PDO 总长 600B;主站解码必须与段 2/3 偏移一致
* 必须同步**(tools/ecat 的 decode_input / transport-ecat.js 等)。 * (tools/ecat 的 decode_input / transport-ecat.js 等)。
*/ */
#ifndef COMM_ETHERCAT_CAN_BRIDGE_H #ifndef COMM_ETHERCAT_CAN_BRIDGE_H
#define COMM_ETHERCAT_CAN_BRIDGE_H #define COMM_ETHERCAT_CAN_BRIDGE_H
@@ -39,14 +33,14 @@
#include "comm/ethercat_pdo_size.h" #include "comm/ethercat_pdo_size.h"
#include "device/motor_state.h" /* kMotorMaxNodes / kMotorChannels */ #include "device/motor_state.h" /* kMotorMaxNodes / kMotorChannels */
/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。布局与 1.5.55 兼容。 */ /** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。 */
enum : uint8_t { enum : uint8_t {
ECAT_CAN_TX_OFF_SEQ = 0U, /**< 发送序号:变化沿触发一次发送(单帧+批量一起) */ ECAT_CAN_TX_OFF_SEQ = 0U, /**< 发送序号:变化沿触发一次发送(单帧+批量一起) */
ECAT_CAN_TX_OFF_CH = 1U, /**< 目标通道 0/1/2 */ ECAT_CAN_TX_OFF_CH = 1U, /**< 目标通道 0/1/2 */
ECAT_CAN_TX_OFF_DLC = 2U, /**< 数据长度 0..32;0 表示本次不发单帧(只发批量) */ ECAT_CAN_TX_OFF_DLC = 2U, /**< 数据长度 0..32;0 表示本次不发单帧(只发批量) */
ECAT_CAN_TX_OFF_FLAGS = 3U, /**< bit0=1 请求清零统计;其余保留 */ ECAT_CAN_TX_OFF_FLAGS = 3U, /**< bit0=1 请求清零统计;其余保留 */
ECAT_CAN_TX_OFF_ID = 4U, /**< CAN ID,小端 uint32(29 位扩展帧) */ ECAT_CAN_TX_OFF_ID = 4U, /**< CAN ID,小端 uint32(29 位扩展帧) */
ECAT_CAN_TX_OFF_WIN = 8U, /**< 1.6.21 起忽略(电机整轮密排,无滑窗);保留占位 */ ECAT_CAN_TX_OFF_WIN = 8U, /**< 当前忽略(电机整轮密排,无滑窗);保留占位 */
/* 9..15 保留:让数据区落在 16 字节边界 */ /* 9..15 保留:让数据区落在 16 字节边界 */
ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */ ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */
}; };
@@ -69,7 +63,7 @@ enum : uint8_t {
ECAT_FRM_HDR_SIZE = 8U, /**< 头长度,数据紧跟其后 */ ECAT_FRM_HDR_SIZE = 8U, /**< 头长度,数据紧跟其后 */
}; };
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照。 */ /** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照 + 收发诊断。 */
enum : uint8_t { enum : uint8_t {
ECAT_CAN_RX_OFF_ACK = 0U, /**< 回显已执行的发送序号,主站据此确认送达 */ ECAT_CAN_RX_OFF_ACK = 0U, /**< 回显已执行的发送序号,主站据此确认送达 */
ECAT_CAN_RX_OFF_CH = 1U, /**< 该帧来自哪个通道 */ ECAT_CAN_RX_OFF_CH = 1U, /**< 该帧来自哪个通道 */
@@ -82,10 +76,12 @@ enum : uint8_t {
ECAT_CAN_RX_OFF_BACKLOG = 21U, /**< 该通道软件 TX 队列积压 */ ECAT_CAN_RX_OFF_BACKLOG = 21U, /**< 该通道软件 TX 队列积压 */
ECAT_CAN_RX_OFF_TXCNT = 22U, /**< 累计发送受理帧数,小端 uint16 */ ECAT_CAN_RX_OFF_TXCNT = 22U, /**< 累计发送受理帧数,小端 uint16 */
ECAT_CAN_RX_OFF_DROP = 24U, /**< 段 1/段 3 快照「未取先覆」次数(u16 累计回绕) */ 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,本拍实际打包的电机数 * count = in[32] // 0..9,本拍实际打包的电机数
@@ -98,7 +94,7 @@ enum : uint16_t {
ECAT_MOTW_OFF_COUNT = 32U, /**< 本拍密排电机数 0..9(主站按此迭代) */ ECAT_MOTW_OFF_COUNT = 32U, /**< 本拍密排电机数 0..9(主站按此迭代) */
ECAT_MOTW_OFF_ACTIVE = 33U, /**< 在线电机数(与 count 可不同:离线但仍报最后一轮) */ ECAT_MOTW_OFF_ACTIVE = 33U, /**< 在线电机数(与 count 可不同:离线但仍报最后一轮) */
ECAT_MOTW_OFF_BOOT = 34U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */ 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_GLOBAL_SEQ = 36U, /**< 状态表全局刷新序号,小端 uint32 */
ECAT_MOTW_OFF_SLOTS = 40U, /**< 密排槽数组起点,count×56B */ ECAT_MOTW_OFF_SLOTS = 40U, /**< 密排槽数组起点,count×56B */
}; };
@@ -150,8 +146,8 @@ constexpr uint8_t kEcatMotHdrSize = 8U;
constexpr uint16_t kEcatMotRegionBytes = constexpr uint16_t kEcatMotRegionBytes =
(uint16_t)(kEcatMotHdrSize (uint16_t)(kEcatMotHdrSize
+ (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize); + (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize);
/** 布局魔数,写在 offset 35。主站据此区分 1.6.21 密排与 1.6.15 滑窗(两者 /** 布局魔数,写在 offset 35。主站据此区分密排布局与旧滑窗布局(两者
* Ibytes 同为 600,不能只靠长度分支)。0=旧布局。 */ * Ibytes 同为 600,不能只靠长度分支)。0=非密排布局。 */
constexpr uint8_t kEcatMotLayoutMagic = 0xA5U; constexpr uint8_t kEcatMotLayoutMagic = 0xA5U;
/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h。 */ /** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h。 */
@@ -194,7 +190,7 @@ class EthercatCanBridge {
public: public:
/** /**
* @brief 输出探针:seq 变化时请求全量读。 * @brief 输出探针:seq 变化时请求全量读。
* @note 1.6.21 起不再刷新 win_base(电机整轮密排,无滑窗);仍处理 seq 沿 * @note 不再刷新 win_base(电机整轮密排,无滑窗);仍处理 seq 沿
* 与 FLAGS bit0 清统计。 * 与 FLAGS bit0 清统计。
*/ */
static bool peekOutput(const uint8_t *head); static bool peekOutput(const uint8_t *head);
@@ -234,8 +230,16 @@ private:
static volatile uint8_t s_lastTxResult; static volatile uint8_t s_lastTxResult;
static volatile uint16_t s_txCount; static volatile uint16_t s_txCount;
static volatile uint16_t s_dropCount; static volatile uint16_t s_dropCount;
/** 最近一次 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_rxValid;
static volatile bool s_rxPublished; static volatile bool s_rxPublished;
static volatile uint32_t s_xrxSeq; static volatile uint32_t s_xrxSeq;
@@ -246,7 +250,16 @@ private:
static volatile uint32_t s_rxCount; static volatile uint32_t s_rxCount;
static uint8_t s_rxData[kEcatCanXRxDataMax]; static uint8_t s_rxData[kEcatCanXRxDataMax];
/* ── 段 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 + 头字段。
* 节点集合、seq、online、active/boot 全未变时整段复用,跳过 MotorState * 节点集合、seq、online、active/boot 全未变时整段复用,跳过 MotorState
* 重读与逐槽 48B 拷贝。掩码必须 u16:节点号 1..9,bit9 超出 uint8_t。 */ * 重读与逐槽 48B 拷贝。掩码必须 u16:节点号 1..9,bit9 超出 uint8_t。 */
+76 -32
View File
@@ -1,6 +1,6 @@
/** /**
* @file ethercat_pdo.cpp * @file ethercat_pdo.cpp
* @brief EtherCAT 过程数据(PDO)收发模块实现(1.5.55)。 * @brief EtherCAT 过程数据(PDO)收发模块实现。
*/ */
#include "comm/ethercat_pdo.h" #include "comm/ethercat_pdo.h"
#include "comm/ethercat_can_bridge.h" #include "comm/ethercat_can_bridge.h"
@@ -19,6 +19,13 @@
#include <cstring> #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(4) 必须在类内声明与出类定义上同时标注:AC6(Clang) 否则报
* "'alignas' must be specified on definition"。用途见 ethercat_pdo.h。 */ * "'alignas' must be specified on definition"。用途见 ethercat_pdo.h。 */
alignas(4) uint8_t EthercatPdo::s_out[EthercatPdo::kPdoOutBytes] = {0}; 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_outputCycles = 0U;
volatile uint32_t EthercatPdo::s_inputCycles = 0U; volatile uint32_t EthercatPdo::s_inputCycles = 0U;
volatile uint32_t EthercatPdo::s_reentryCount = 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) void EthercatPdo::readOutputs(const uint8_t *esc)
{ {
@@ -62,22 +72,15 @@ bool EthercatPdo::outputProbe(const uint8_t *head)
void EthercatPdo::process() void EthercatPdo::process()
{ {
/* 重入防护(1.6.7):SSC 的 ECAT_Application()→本函数 在**四个上下文**都会被 /* 重入防护:SSC 的 ECAT_Application()→本函数 在**四个上下文**都会被
* 调用 —— PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop(见 ssc/src/ecatappl.c * 调用 —— PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop(见 ssc/src/ecatappl.c
* 388、499、527、641 行)。而 MainLoop 那条路只用 DISABLE_ESC_INT() 关了 * 388、499、527、641 行)。而 MainLoop 那条路只用 DISABLE_ESC_INT() 关了
* EXTI1(ESC IRQ),**挡不住 EXTI0(Sync0,preempt=0) 和 EXTI2(Sync1,preempt=1)**。 * 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() * 抢占 → 内层重入本函数,memset 清空后重填整个输入 PDO,随后 PDO_InputMapping()
* 把这一份发给 ESC;外层回来又按自己那轮的进度接着往下写。于是发出去的镜像里 * 把这一份发给 ESC;外层回来又按自己那轮的进度接着往下写。于是发出去的镜像里
* 批量区相邻两条来自**不同轮次**,而两轮之间环形队列 tail 已推进 —— 第 2 条 * 批量区相邻两条来自**不同轮次**。
* 就带上了另一条帧的内容。实测特征与此吻合:批量区第 1 条几乎不坏(0.06%)、
* 第 2 条 30.75% 坏且内容是一条完整 CONFIG 帧;段 1/段 3 干净,因为它们每轮
* 内容相同,被覆盖也看不出来。
*
* 1.6.15 批量区改成定长覆盖窗口后,跨轮次混填不再产生"半新半旧"的假帧(每槽
* 自带 seqlock,且槽位固定、语义本就是"最近 N 帧"),但本防护仍然保留:省掉
* 一次无谓的整块重填,并让镜像保持同一轮次的一致性。
* *
* 处理:已在填充中就直接返回。输入 PDO 是周期量,跳过一次下一拍即补, * 处理:已在填充中就直接返回。输入 PDO 是周期量,跳过一次下一拍即补,
* 远好过发出一份跨轮次拼接的镜像。 */ * 远好过发出一份跨轮次拼接的镜像。 */
@@ -104,18 +107,48 @@ void EthercatPdo::process()
const uint8_t nextIdx = 1U - s_inActiveIdx; const uint8_t nextIdx = 1U - s_inActiveIdx;
uint8_t *build = s_inBuf[nextIdx]; 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::processOutput(s_out);
EthercatCanBridge::buildInput(build); EthercatCanBridge::buildInput(build);
fillDiagTail(build); fillDiagTail(build);
__DMB(); __DMB();
s_inActiveIdx = nextIdx; s_inActiveIdx = nextIdx;
memcpy(s_in, build, kPdoInBytes); /* 不再 memcpy 到 s_in:writeInputs/稀疏写/差分写都读 s_inBuf[activeIdx],
* 每拍多拷 600B 是纯浪费(8kHz 下不可接受)。 */
++s_processSeq;
#if CONFIG_ECAT_DIFF_STATS #if CONFIG_ECAT_DIFF_STATS
/* [DIFF-STATS] 只测量:与上一份已发布镜像比对并累计到 1s 窗口,经 0x2000 /* [DIFF-STATS] 只测量:与上一份已发布镜像比对并累计到 1s 窗口,经 0x2000
* sub13..23 上报。不改变上面的整块写行为,因此本版本固件的输出与 1.6.16 * sub13..23 上报。不改变上面的整块写行为。仪表自持上一份副本,这里不需要额外缓冲。 */
* 逐字节一致。仪表自持上一份副本,这里不需要额外缓冲。 */
EthercatDiffStats::observe(s_inBuf[nextIdx]); EthercatDiffStats::observe(s_inBuf[nextIdx]);
#endif #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)。 * @param[out] in 全长输入过程数据缓冲(buildInput 刚清零+填完段 1~4)。
* @note 负载率 1s 滚动一个窗口,seq 随窗口递增——主站只认"seq 在变"的负载值。 * @note 负载率 1s 滚动一个窗口,seq 随窗口递增——主站只认"seq 在变"的负载值。
* 每周期重写 6 字节比"值变化才写"的分支判断更便宜,也不引入隐藏状态。 * 每周期重写 6 字节比"值变化才写"的分支判断更便宜,也不引入隐藏状态。
*/ */
void EthercatPdo::fillDiagTail(uint8_t *in) void EthercatPdo::fillDiagTail(uint8_t *in)
{ {
static uint16_t s_diagTick = 0U;
const uint16_t load = CpuLoad::loadPermille(); const uint16_t load = CpuLoad::loadPermille();
in[ECAT_DIAG_OFF_LOAD] = (uint8_t)(load & 0xFFU); in[ECAT_DIAG_OFF_LOAD] = (uint8_t)(load & 0xFFU);
in[ECAT_DIAG_OFF_LOAD + 1] = (uint8_t)(load >> 8); in[ECAT_DIAG_OFF_LOAD + 1] = (uint8_t)(load >> 8);
/* CAN 取帧撕裂丢帧数(1.6.6)。只有 16 位,饱和不回绕 —— 主站看的是 /* CAN 取帧撕裂丢帧数。只有 16 位,饱和不回绕 —— 主站看的是
* "有没有涨、涨多快",回绕会被误读成计数清零。 */ * "有没有涨、涨多快",回绕会被误读成计数清零。 */
const CanFd::Diag *cd = CanFd::diag(); const CanFd::Diag *cd = CanFd::diag();
const uint32_t torn = (cd != nullptr) ? cd->rx_torn_count : 0U; 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] = (uint8_t)(torn16 & 0xFFU);
in[ECAT_DIAG_OFF_TORN + 1] = (uint8_t)(torn16 >> 8); in[ECAT_DIAG_OFF_TORN + 1] = (uint8_t)(torn16 >> 8);
const uint32_t seq = CpuLoad::windowSeq(); /* seq 写「本函数被调用次数」而不是 windowSeq。
const uint16_t seq16 = (uint16_t)seq; /* 1.6.12:u32 拆成 u16,高 2 字节让给 * windowSeq 依赖 CpuLoad::tick() 滚窗;Process 若在 tick 前停摆,
overfull 计数。主站只判"非 0 且在涨", * 主站会长期读到 0 且分不清「没跑」和「窗口未满」。自由计数每拍+1,
按 u32 读到 seq|overfull<<16 同样成立。 */ * 冻结看门狗与 app/load 的「seq 在涨」语义都更干净。 */
in[ECAT_DIAG_OFF_SEQ] = (uint8_t)(seq16 & 0xFFU); ++s_diagTick;
in[ECAT_DIAG_OFF_SEQ + 1] = (uint8_t)(seq16 >> 8); 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 改"满则丢新"后拼接未消失, /* CAN RX FIFO 满溢计数 RFO0:与内容级 reject 计数是否同涨,
* 本计数与 reject@494 是否同涨,是"残余拼接是否仍源自溢出"的判决性观测。 */ * 是"残余拼接是否仍源自溢出"的观测指标。 */
const uint32_t of = (cd != nullptr) ? cd->rx_fifo_overfull_count : 0U; const uint32_t of = (cd != nullptr) ? cd->rx_fifo_overfull_count : 0U;
const uint16_t of16 = (of > 0xFFFFU) ? 0xFFFFU : (uint16_t)of; const uint16_t of16 = (of > 0xFFFFU) ? 0xFFFFU : (uint16_t)of;
in[ECAT_DIAG_OFF_OVERFULL] = (uint8_t)(of16 & 0xFFU); in[ECAT_DIAG_OFF_OVERFULL] = (uint8_t)(of16 & 0xFFU);
in[ECAT_DIAG_OFF_OVERFULL + 1] = (uint8_t)(of16 >> 8); in[ECAT_DIAG_OFF_OVERFULL + 1] = (uint8_t)(of16 >> 8);
/* RFD0 释放握手超时计数(1.6.12,占用原 1.6.7 重入计数位 492——该计数引入 /* RFD0 释放握手超时计数。dequeue 握手没完成 = 窗口/读指针失同步前兆。 */
* 至今恒 0,转为纯内部)。dequeue 握手没完成 = 窗口/读指针失同步前兆。 */
const uint32_t rt = (cd != nullptr) ? cd->rx_release_timeout_count : 0U; const uint32_t rt = (cd != nullptr) ? cd->rx_release_timeout_count : 0U;
const uint16_t rt16 = (rt > 0xFFFFU) ? 0xFFFFU : (uint16_t)rt; const uint16_t rt16 = (rt > 0xFFFFU) ? 0xFFFFU : (uint16_t)rt;
in[ECAT_DIAG_OFF_RELTIMEOUT] = (uint8_t)(rt16 & 0xFFU); in[ECAT_DIAG_OFF_RELTIMEOUT] = (uint8_t)(rt16 & 0xFFU);
in[ECAT_DIAG_OFF_RELTIMEOUT + 1] = (uint8_t)(rt16 >> 8); 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 uint32_t rj = (cd != nullptr) ? cd->rx_content_reject_count : 0U;
const uint16_t rj16 = (rj > 0xFFFFU) ? 0xFFFFU : (uint16_t)rj; 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 #if !CONFIG_ECAT_SPARSE_ESC_WRITE
return false; return false;
#else #else
/* 仅 1.6.21 密排布局:count 有效且两段之间确有空洞才分段。 /* 仅密排布局:count 有效且两段之间确有空洞才分段。
* magic 不对 / count 越界 / count 撑满到与段3相接 → 整包,避免陈旧空洞被误读。 */ * magic 不对 / count 越界 / count 撑满到与段3相接 → 整包,避免陈旧空洞被误读。 */
const uint8_t activeIdx = s_inActiveIdx; const uint8_t activeIdx = s_inActiveIdx;
__DMB(); __DMB();
@@ -329,7 +363,7 @@ void EthercatPdo::snapshotOutput(uint8_t *dst)
extern "C" { extern "C" {
/* 1.6.0:输出探针,SSC PDI_Isr 的 PDO_OutputMapping 每周期调用(C 链接)。 /* 输出探针,SSC PDI_Isr 的 PDO_OutputMapping 每周期调用(C 链接)。
* 返回 0/1;SSC 侧声明为 UINT8,小返回值零扩展,调用约定兼容。 */ * 返回 0/1;SSC 侧声明为 UINT8,小返回值零扩展,调用约定兼容。 */
uint8_t EcatPdo_OutputProbe(const uint8_t *head) uint8_t EcatPdo_OutputProbe(const uint8_t *head)
{ {
@@ -341,6 +375,11 @@ void EcatPdo_ReadOutputs(const uint8_t *esc)
EthercatPdo::readOutputs(esc); EthercatPdo::readOutputs(esc);
} }
void EcatPdo_NoteOutputMapped(void)
{
s_outMappedThisCycle = true;
}
void EcatPdo_Process(void) void EcatPdo_Process(void)
{ {
EthercatPdo::process(); EthercatPdo::process();
@@ -351,7 +390,7 @@ void EcatPdo_WriteInputs(uint8_t *esc)
EthercatPdo::writeInputs(esc); EthercatPdo::writeInputs(esc);
} }
/* 1.6.22 稀疏写计划:SSC PDO_InputMapping 据此决定 HW_EscWriteIsr 写几段。 /* 稀疏写计划:SSC PDO_InputMapping 据此决定 HW_EscWriteIsr 写几段。
* 返回 1=两段有效;0=回退整包。 */ * 返回 1=两段有效;0=回退整包。 */
uint8_t EcatPdo_SparseEscPlan(uint16_t *off0, uint16_t *len0, uint8_t EcatPdo_SparseEscPlan(uint16_t *off0, uint16_t *len0,
uint16_t *off1, uint16_t *len1) 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; 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) uint8_t EcatPdo_EscWritePlan(uint16_t *offs, uint16_t *lens, uint8_t maxSegs)
{ {
if (offs == nullptr || lens == nullptr || maxSegs == 0U 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; return n;
} }
/* 1.6.34: HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */ /* HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
uint32_t EscExmcBus_MaskEnter(void) uint32_t EscExmcBus_MaskEnter(void)
{ {
return EscExmcBus::maskEnter(); return EscExmcBus::maskEnter();
@@ -400,4 +439,9 @@ void HW_EscExmcMaskExit(uint32_t prev)
EscExmcBus::maskExit(prev); EscExmcBus::maskExit(prev);
} }
uint32_t EcatPdo_ProcessSeq(void)
{
return EthercatPdo::processSeq();
}
} /* extern "C" */ } /* extern "C" */
+16 -8
View File
@@ -1,6 +1,6 @@
/** /**
* @file ethercat_pdo.h * @file ethercat_pdo.h
* @brief EtherCAT 过程数据(PDO)收发模块(1.5.54 由 dc_app.c 的散装 C 代码类化)。 * @brief EtherCAT 过程数据(PDO)收发模块。
* *
* 一个 EtherCAT 周期在 SSC 里被拆成三个回调,本类按这三步各提供一个入口: * 一个 EtherCAT 周期在 SSC 里被拆成三个回调,本类按这三步各提供一个入口:
* 1. APPL_OutputMapping() → readOutputs() 从 ESC 取走主站下发的输出数据 * 1. APPL_OutputMapping() → readOutputs() 从 ESC 取走主站下发的输出数据
@@ -11,7 +11,7 @@
* 是 SDO 存储,必须留在 C 侧)与三行 extern "C" 转发。这样"收发"这件事只有一个 * 是 SDO 存储,必须留在 C 侧)与三行 extern "C" 转发。这样"收发"这件事只有一个
* 归属:想加新的过程数据语义只改本类,不必再动 SSC 移植层。 * 归属:想加新的过程数据语义只改本类,不必再动 SSC 移植层。
* *
* @note 1.5.55 起输出/输入**长度不再相等**(64B / 384B),为电机控制台腾出 * @note 输出/输入**长度不相等**(64B / 384B),为电机控制台腾出
* 状态窗口空间。长度常量统一由 ethercat_can_bridge.h 提供,避免三处各写 * 状态窗口空间。长度常量统一由 ethercat_can_bridge.h 提供,避免三处各写
* 一个魔数后失配。 * 一个魔数后失配。
* *
@@ -49,11 +49,11 @@ public:
static void readOutputs(const uint8_t *esc); 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)。 * @param[in] head 输出 PDO 头 12B(覆盖 SEQ@0 / WIN@8 / FLAGS@3)。
* @return true=需要全量读(seq 沿或清统计请求);false=本周期跳过全量读。 * @return true=需要全量读(seq 沿或清统计请求);false=本周期跳过全量读。
* @note 每周期由 PDI_Isr 在读 ESC 前调用:做 seq 基准 priming 与沿判定; * @note 每周期由 PDI_Isr 在读 ESC 前调用:做 seq 基准 priming 与沿判定;
* s_lastSeq 的推进仍留在全量路径(processOutput)。1.6.21 起不再 * s_lastSeq 的推进仍留在全量路径(processOutput)。不再
* 维护 win_base(段 2 电机整轮密排,无滑窗)。 * 维护 win_base(段 2 电机整轮密排,无滑窗)。
*/ */
static bool outputProbe(const uint8_t *head); static bool outputProbe(const uint8_t *head);
@@ -73,7 +73,7 @@ public:
static void writeInputs(uint8_t *esc); 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] off0/len0 第一段相对 PDO 起点的偏移与长度
* @param[out] off1/len1 第二段 * @param[out] off1/len1 第二段
* @return true=可按两段写;false=应回退整包 600B(开关关/非密排/count 越界/两段相接) * @return true=可按两段写;false=应回退整包 600B(开关关/非密排/count 越界/两段相接)
@@ -83,7 +83,7 @@ public:
uint16_t *off1, uint16_t *len1); uint16_t *off1, uint16_t *len1);
/** /**
* @brief 输入 PDO 写 ESC 的多段计划(1.6.25 差分写;未启用时回退稀疏/整包)。 * @brief 输入 PDO 写 ESC 的多段差分计划(未启用时回退稀疏/整包)。
* @param[out] offs 段偏移数组,至少 kEscPlanMaxSegs 项 * @param[out] offs 段偏移数组,至少 kEscPlanMaxSegs 项
* @param[out] lens 段长度数组,至少 kEscPlanMaxSegs 项 * @param[out] lens 段长度数组,至少 kEscPlanMaxSegs 项
* @return 段数 1..kEscPlanMaxSegs;调用方逐段 HW_EscWriteIsr。 * @return 段数 1..kEscPlanMaxSegs;调用方逐段 HW_EscWriteIsr。
@@ -102,6 +102,9 @@ public:
/** @brief 累计交回输入的周期数。 */ /** @brief 累计交回输入的周期数。 */
static uint32_t inputCycles() { return s_inputCycles; } static uint32_t inputCycles() { return s_inputCycles; }
/** @brief process() 完整填完一拍输入的序号。新鲜度救援看这个,而不是
* InputMapping 的 s_inMapSeq——后者只证明「在拷陈旧 s_in」。 */
static uint32_t processSeq() { return s_processSeq; }
/** @brief process() 被重入拦下的次数。持续增长 = 跨上下文抢占确实在发生 /** @brief process() 被重入拦下的次数。持续增长 = 跨上下文抢占确实在发生
* (PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 互相打断),也就证明了 * (PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 互相打断),也就证明了
* 批量区跨轮次拼接的成因;恒为 0 则说明该机制推断有误。 */ * 批量区跨轮次拼接的成因;恒为 0 则说明该机制推断有误。 */
@@ -118,7 +121,7 @@ public:
private: private:
/** /**
* @brief 填充输入 PDO 段 5 设备诊断区(尾部 8 字节,1.5.61)。 * @brief 填充输入 PDO 段 5 设备诊断区(尾部 8 字节)。
* @param[out] in 全长输入过程数据缓冲。 * @param[out] in 全长输入过程数据缓冲。
* @note 布局常量 ECAT_DIAG_OFF_* 见 ethercat_can_bridge.h;当前携带 * @note 布局常量 ECAT_DIAG_OFF_* 见 ethercat_can_bridge.h;当前携带
* CPU 负载率(0.1% 单位) + 窗口序号,取自 sys/cpu_load.h。 * CPU 负载率(0.1% 单位) + 窗口序号,取自 sys/cpu_load.h。
@@ -136,7 +139,7 @@ private:
* ECAT_Application() 会在 PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 四个 * ECAT_Application() 会在 PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 四个
* 上下文被调用并互相抢占,直接写 s_in 会让读侧取到跨轮次的半成品。 */ * 上下文被调用并互相抢占,直接写 s_in 会让读侧取到跨轮次的半成品。 */
alignas(4) static uint8_t s_build[kPdoInBytes]; alignas(4) static uint8_t s_build[kPdoInBytes];
/** @brief 写 ESC 差分影子(1.6.25):段1 / 段2 / 段3+5 三区。 */ /** @brief 写 ESC 差分影子:段1 / 段2 / 段3+5 三区。 */
alignas(4) static uint8_t s_escSh1[32]; 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_escSh2[ECAT_XRX_OFF_CH - 32U];
alignas(4) static uint8_t s_escSh3[kEcatPdoInBytes - ECAT_XRX_OFF_CH]; 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_outputCycles;
static volatile uint32_t s_inputCycles; static volatile uint32_t s_inputCycles;
static volatile uint32_t s_processSeq;
}; };
extern "C" { extern "C" {
@@ -160,6 +164,10 @@ void EcatPdo_ReadOutputs(const uint8_t *esc);
void EcatPdo_Process(void); void EcatPdo_Process(void);
/** @brief 供 dc_app.c 调用:交回输入过程数据。 */ /** @brief 供 dc_app.c 调用:交回输入过程数据。 */
void EcatPdo_WriteInputs(uint8_t *esc); 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 */ #endif /* COMM_ETHERCAT_PDO_H */
+7 -14
View File
@@ -64,13 +64,12 @@ void EthercatSlave::init()
* (如 SM Watchdog=0x1B、FMMU 配置错、邮箱协议错),需主站介入。 */ * (如 SM Watchdog=0x1B、FMMU 配置错、邮箱协议错),需主站介入。 */
/* ============================================================ /* ============================================================
* bring-up 诊断(1.5.35 引入,1.5.39 重新定性):经 CSR 间接端口读三个关键寄存器 * bring-up 诊断:经 CSR 间接端口读三个关键寄存器
* + 一次写回环,确认 EtherCAT 核心侧状态。 * + 一次写回环,确认 EtherCAT 核心侧状态。
* 注意主站读身份/AL 走 ESC 以太网侧 + EEPROM(SII),完全不经过 GD32 的 PDI, * 注意主站读身份/AL 走 ESC 以太网侧 + EEPROM(SII),完全不经过 GD32 的 PDI,
* 所以"主站能读到身份"不能证明下面任何一项成立。 * 所以"主站能读到身份"不能证明下面任何一项成立。
* 1.5.39 起访问已改经 ECAT_CSR_CMD/CSR_DATA(EtherCAT 核心寄存器不在 HBI 直接 * 访问必须经 ECAT_CSR_CMD/CSR_DATA(EtherCAT 核心寄存器不在 HBI 直接
* 地址空间,DS 12.11);此前直读索引使本段恒读全 0,曾被误判为"EEPROM 加载竞态" * 地址空间,DS 12.11);直读索引恒读全 0。
* 与"HBI 写路径损坏"两个错误结论。现在读到全 0 才真正指向 EEPROM/PDI 故障,
* 交叉验证用 tools/ecat/ecat_esc_regs.py 从网络侧读同样的寄存器。 * 交叉验证用 tools/ecat/ecat_esc_regs.py 从网络侧读同样的寄存器。
* 本段在 EthercatSync::init() 使能 EXTI 之前执行,满足 initOnly* 的 INIT-ONLY 约束。 * 本段在 EthercatSync::init() 使能 EXTI 之前执行,满足 initOnly* 的 INIT-ONLY 约束。
* ============================================================ */ * ============================================================ */
@@ -87,9 +86,7 @@ void EthercatSlave::init()
/* (3) EEPROM 接口状态 0x0502-0x0503(16 位字)。位定义按 ETG.1000.4 表 49 /* (3) EEPROM 接口状态 0x0502-0x0503(16 位字)。位定义按 ETG.1000.4 表 49
* 与 SSC esc.h:bit11=Checksum Error、bit12=EEPROM loading status(0=OK)、 * 与 SSC esc.h:bit11=Checksum Error、bit12=EEPROM loading status(0=OK)、
* bit13=Missing EEPROM Acknowledge、bit14=Write Error、bit15=Busy。 * bit13=Missing EEPROM Acknowledge、bit14=Write Error、bit15=Busy。
* 注意 1.5.40 及之前本行把 bit11 打成 "loaded",语义还正好反了 * 0 全部代表正常。
* (bit11=1 才是校验错),于是正常板子恒显示 "loaded=0",字面像"EEPROM 未加载",
* 正是历史上 1.5.37 那个已撤回的错误结论的诱因;现按位分别命名,0 全部代表正常。
* 真正的"EEPROM 已加载/PDI 可用"判据是 DL_STATUS(0x0110) bit0,见上方 (1)。 */ * 真正的"EEPROM 已加载/PDI 可用"判据是 DL_STATUS(0x0110) bit0,见上方 (1)。 */
const uint16_t eepCsr = EscLan9252::initOnlyReadReg(0x0502U); const uint16_t eepCsr = EscLan9252::initOnlyReadReg(0x0502U);
LOG_ERR("[ECAT][DIAG] DL_STATUS(0x110)=0x%04X pdi_oper=%u | " 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 * 就是 PDI 侧可写(PDI 用它选择哪些 AL Event 产生 IRQ),不像 0x0012 Station
* Alias 在部分 ESC 上对 PDI 只读(会造成假 FAIL);本段在 MainInit/HW_Init * Alias 在部分 ESC 上对 PDI 只读(会造成假 FAIL);本段在 MainInit/HW_Init
* 之前执行,还原后不影响 SSC 后续建立的 mask 影子。 * 之前执行,还原后不影响 SSC 后续建立的 mask 影子。
* 1.5.39 起本回环经 ECAT_CSR_CMD/CSR_DATA 间接端口执行。历史注意:1.5.35 * 回环必须经 ECAT_CSR_CMD/CSR_DATA 间接端口执行。 */
* 该回环报 FAIL 曾被误判为"HBI 写路径损坏",真因是当时直读直写 HBI 索引——
* EtherCAT 核心寄存器不在 HBI 直接地址空间(DS 12.11),读恒 0、写被丢弃。
* 网络侧诊断(tools/ecat/ecat_esc_regs.py)已证实 ESC 本体与 EEPROM 一直正常。 */
const uint16_t maskSaved = EscLan9252::initOnlyReadReg(0x0204U); const uint16_t maskSaved = EscLan9252::initOnlyReadReg(0x0204U);
EscLan9252::initOnlyWriteReg(0x0204U, 0x0A50U); EscLan9252::initOnlyWriteReg(0x0204U, 0x0A50U);
const uint16_t maskRead = EscLan9252::initOnlyReadReg(0x0204U); 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 /* 是否有主站在线:DL_STATUS(0x0110) 的 Physical Link 位 bit4/5/6 对应 Port0/1/2
* (DS 12.14.19)。三个端口都无 link = 网线未插/无主站,此时 AL 状态机根本没人驱动 * (DS 12.14.19)。三个端口都无 link = 网线未插/无主站,此时 AL 状态机根本没人驱动
* (AL 迁移由主站写 AL Control 0x0120 触发,从站不自跳),停在 INIT 是唯一正确行为。 * (AL 迁移由主站写 AL Control 0x0120 触发,从站不自跳),停在 INIT 是唯一正确行为。
* 参考:接主站时实测 DL_STATUS=0x5611(Port0 有 link 有 comm); * 参考:接主站时 DL_STATUS 通常为 0x5611(Port0 有 link 有 comm);
* 空载时为 0x5503(三口 loop closed、link/comm 全 0)。 */ * 空载时为 0x5503(三口 loop closed、link/comm 全 0)。 */
const bool anyPhyLink = chipIdOk && ((dlStatus & 0x0070U) != 0U); 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"); LOG_ERR("[ECAT][SLAVE] ESC NOT ready, sync ISR disabled (check EXMC wiring/timing)\r\n");
} else if (s_alState < AL_STATE_SAFEOP) { } else if (s_alState < AL_STATE_SAFEOP) {
/* 区分"等主站"与"真故障":两者 AL 都 < SAFEOP,但只有后者需要排查。 /* 区分"等主站"与"真故障":两者 AL 都 < SAFEOP,但只有后者需要排查。
* 1.5.40 及之前一律打 FAIL(partial),而冷启动无主站时必然走到这里, * 冷启动无主站时必然走到这里,报 WAIT_MASTER 而非 FAIL。 */
* 与紧随其后的 "SSC MainInit OK" 自相矛盾,极易被误读为初始化失败。 */
if (!anyPhyLink) { if (!anyPhyLink) {
LOG_ERR("[ECAT][SLAVE] WAIT_MASTER: no PHY link (DL_STATUS=0x%04X), " LOG_ERR("[ECAT][SLAVE] WAIT_MASTER: no PHY link (DL_STATUS=0x%04X), "
"AL=0x%02X(%s) 由主站驱动,空载停在 INIT 属预期\r\n", "AL=0x%02X(%s) 由主站驱动,空载停在 INIT 属预期\r\n",
+1 -2
View File
@@ -1,8 +1,7 @@
/** /**
* @file ethercat_slave.h * @file ethercat_slave.h
* @brief EtherCAT 从站 init 探针与运行状态查询接口。 * @brief EtherCAT 从站 init 探针与运行状态查询接口。
* @note 1.5.27:原先的 periodicIsr()/三缓冲 PDO 桥是死代码(无任何调用点), * @note 运行期 PDO 交换由 SSC 协议栈完成 —— 周期路径为 Sync0_Isr →
* 运行期 PDO 交换实际由 SSC 协议栈完成 —— 周期路径为 Sync0_Isr →
* ECAT_Application → APPL_InputMapping/OutputMapping(comm/ssc/port/dc_app.c), * ECAT_Application → APPL_InputMapping/OutputMapping(comm/ssc/port/dc_app.c),
* 主循环路径为 MainLoop → ECAT_Main。本类仅保留 init 探针与诊断查询。 * 主循环路径为 MainLoop → ECAT_Main。本类仅保留 init 探针与诊断查询。
*/ */
+7 -9
View File
@@ -7,7 +7,7 @@
* SYNC1 (EXTI2, preempt=1) → Sync1_Isr() : 预留 * SYNC1 (EXTI2, preempt=1) → Sync1_Isr() : 预留
*/ */
#include "comm/ethercat_sync.h" #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 "sys/config.h"
#include "main.h" /* g_app_uptime_sec */ #include "main.h" /* g_app_uptime_sec */
#include "sys/cpu_load.h" #include "sys/cpu_load.h"
@@ -115,7 +115,7 @@ void EthercatSync::onSync0Exti()
/* 直接寄存器访问,绕过 GD32 库函数以消除可变抖动 */ /* 直接寄存器访问,绕过 GD32 库函数以消除可变抖动 */
if ((EXTI_PD & (uint32_t)CONFIG_ESC_SYNC0_EXTI) != RESET) { if ((EXTI_PD & (uint32_t)CONFIG_ESC_SYNC0_EXTI) != RESET) {
EXTI_PD = (uint32_t)CONFIG_ESC_SYNC0_EXTI; 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 引脚本应无沿,此分支只兜 * 访问全是 HBI,读即唤醒。睡眠中 SYNC0 引脚本应无沿,此分支只兜
* 进入睡眠瞬间的残留 pending。 */ * 进入睡眠瞬间的残留 pending。 */
if (EscLan9252::pmSuspended()) { if (EscLan9252::pmSuspended()) {
@@ -130,7 +130,7 @@ void EthercatSync::onEscIrqExti()
{ {
if (exti_interrupt_flag_get(CONFIG_ESC_IRQ_EXTI) != RESET) { if (exti_interrupt_flag_get(CONFIG_ESC_IRQ_EXTI) != RESET) {
exti_interrupt_flag_clear(CONFIG_ESC_IRQ_EXTI); 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()) { if (EscLan9252::pmSuspended()) {
return; return;
} }
@@ -141,9 +141,6 @@ void EthercatSync::onEscIrqExti()
* 代价是 PDI_Isr 剩余时长(µs 级)计入 SYNC0 抖动。 */ * 代价是 PDI_Isr 剩余时长(µs 级)计入 SYNC0 抖动。 */
const uint32_t prevSync0 = NVIC_GetEnableIRQ(CONFIG_ESC_SYNC0_IRQn); const uint32_t prevSync0 = NVIC_GetEnableIRQ(CONFIG_ESC_SYNC0_IRQn);
NVIC_DisableIRQ(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(); PDI_Isr();
if (prevSync0 != 0U) { if (prevSync0 != 0U) {
NVIC_EnableIRQ(CONFIG_ESC_SYNC0_IRQn); NVIC_EnableIRQ(CONFIG_ESC_SYNC0_IRQn);
@@ -155,7 +152,7 @@ void EthercatSync::onSync1Exti()
{ {
if (exti_interrupt_flag_get(CONFIG_ESC_SYNC1_EXTI) != RESET) { if (exti_interrupt_flag_get(CONFIG_ESC_SYNC1_EXTI) != RESET) {
exti_interrupt_flag_clear(CONFIG_ESC_SYNC1_EXTI); exti_interrupt_flag_clear(CONFIG_ESC_SYNC1_EXTI);
/* 1.6.27:ESC 睡眠挂起期直接返回——Sync1_Isr 内为 HBI 访问 */ /* ESC 睡眠挂起期直接返回——Sync1_Isr 内为 HBI 访问 */
if (EscLan9252::pmSuspended()) { if (EscLan9252::pmSuspended()) {
return; return;
} }
@@ -175,7 +172,7 @@ void EthercatSync::onTimerIrq()
#if (CONFIG_ETHERCAT_SYNC_MODE == 1) #if (CONFIG_ETHERCAT_SYNC_MODE == 1)
if (timer_interrupt_flag_get(TIMER0, TIMER_INT_FLAG_UP) != RESET) { if (timer_interrupt_flag_get(TIMER0, TIMER_INT_FLAG_UP) != RESET) {
timer_interrupt_flag_clear(TIMER0, TIMER_INT_FLAG_UP); timer_interrupt_flag_clear(TIMER0, TIMER_INT_FLAG_UP);
/* 1.6.27:ESC 睡眠挂起期跳过——Sync0_Isr 内为 HBI 访问 */ /* ESC 睡眠挂起期跳过——Sync0_Isr 内为 HBI 访问 */
if (!EscLan9252::pmSuspended()) { if (!EscLan9252::pmSuspended()) {
Sync0_Isr(); 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_GetSync1(void) { return EthercatSync::getSync1Count(); }
uint32_t EcatDbg_GetEscIrq(void) { return EthercatSync::getEscIrqCount(); } uint32_t EcatDbg_GetEscIrq(void) { return EthercatSync::getEscIrqCount(); }
uint32_t EcatDbg_GetUptimeSec(void){ return g_app_uptime_sec; } 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; } uint32_t EcatDbg_Dwt(void) { return DWT->CYCCNT; }
} /* extern "C" */ } /* extern "C" */
+80 -115
View File
@@ -11,10 +11,7 @@
* 0x6000 Input Data (从站→主站, 384 bytes) * 0x6000 Input Data (从站→主站, 384 bytes)
* 0x2000 SYNC Diag (只读: sync0/sync1/escirq/uptime, 主站 SDO 轮询看 DC 通路) * 0x2000 SYNC Diag (只读: sync0/sync1/escirq/uptime, 主站 SDO 轮询看 DC 通路)
* *
* 1.5.55:为电机控制台把过程数据从 32B/32B 扩到 64B/384B。 * 0x7000/0x6000/0x1600/0x1A00 为 OBJCODE_ARR(原因见下方 Entry Descriptions 注释)。
* 0x7000/0x6000/0x1600/0x1A00 由 OBJCODE_REC 改为 OBJCODE_ARR —— 关键原因见
* 下方 Entry Descriptions 注释:REC 会逐下标索引 pEntryDesc[Subindex],48 个
* 条目就得写 49 项描述符;ARR 由 OBJ_GetEntryDesc 复用 pEntryDesc[1],只需 2 项。
*/ */
#include "ecat_def.h" #include "ecat_def.h"
@@ -27,7 +24,11 @@
#include <string.h> #include <string.h>
#include <stddef.h> /* offsetof:下方布局守卫断言使用 */ #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 内部需要) * el9800appl.h 桩 include (SSC 内部需要)
@@ -39,18 +40,13 @@
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
* PDO 数据缓冲 * 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 与之绑定, * 长度的唯一真源是 comm/ethercat_pdo_size.h;本文件用 _Static_assert 与之绑定,
* 改一处漏一处会编译失败。为何硬上限是 out+in≤1024B 见该头文件的推导。 * 改一处漏一处会编译失败。为何硬上限是 out+in≤1024B 见该头文件的推导。
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
/** 输出/输入 PDO 的 64bit 条目数。 */ /** 输出/输入 PDO 的 64bit 条目数。 */
#define PDO_OUT_ENTRIES 48u /* 48×8 = 384 B */ #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) */ /** 输出数据对象 0x7000 (主站→从站, RxPDO) */
typedef struct OBJ_STRUCT_PACKED_START { typedef struct OBJ_STRUCT_PACKED_START {
@@ -69,7 +65,7 @@ TOBJ6000;
static TOBJ7000 s_outputObj = {PDO_OUT_ENTRIES, {0}}; static TOBJ7000 s_outputObj = {PDO_OUT_ENTRIES, {0}};
static TOBJ6000 s_inputObj = {PDO_IN_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_GetEntryOffset),故 UINT64 数组必须紧跟在 UINT16 之后落在字节 2。
* OBJ_STRUCT_PACKED_START 曾被留空导致其落在字节 8、SDO 读写错位,加断言防回归。 */ * OBJ_STRUCT_PACKED_START 曾被留空导致其落在字节 8、SDO 读写错位,加断言防回归。 */
_Static_assert(offsetof(TOBJ7000, u64Data) == 2, "TOBJ7000 未紧排:检查 OBJ_STRUCT_PACKED_START"); _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 u32Sync1; /* sub2 */
UINT32 u32EscIrq; /* sub3 */ UINT32 u32EscIrq; /* sub3 */
UINT32 u32UptimeS; /* sub4 */ UINT32 u32UptimeS; /* sub4 */
/* 1.6.0 剖分埋点(sub5..12,DWT 周期/计数累加,主站 1s 差分;定位完即移除) */ /* 剖分埋点(sub5..12,DWT 周期/计数累加,主站 1s 差分) */
UINT32 u32PdiCyc; /* sub5: PDI_Isr 全函数累计 */ UINT32 u32PdiCyc; /* sub5: PDI_Isr 全函数累计 */
UINT32 u32OutCyc; /* sub6: PDO_OutputMapping(输出探针/全量读) */ UINT32 u32OutCyc; /* sub6: PDO_OutputMapping(输出探针/全量读) */
UINT32 u32FreeRunCyc; /* sub7: MainLoop free-run 块(SM 同步下恒 0) */ 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 u32DsSeg1; /* sub18: 段1(0..31) 桥状态+短快照 变化字节合计 */
UINT32 u32DsSeg2; /* sub19: 段2(32..543) 电机整轮密排 */ UINT32 u32DsSeg2; /* sub19: 段2(32..543) 电机整轮密排 */
UINT32 u32DsSeg3; /* sub20: 段3(544..583) 长帧快照 */ UINT32 u32DsSeg3; /* sub20: 段3(544..583) 长帧快照 */
UINT32 u32DsSeg4; /* sub21: 已删(1.6.21 恒 0) */ UINT32 u32DsSeg4; /* sub21: 已删(恒 0) */
UINT32 u32DsSeg5; /* sub22: 段5(584..599) 诊断区+保留 */ UINT32 u32DsSeg5; /* sub22: 段5(584..599) 诊断区+保留 */
UINT32 u32DsCompareCyc; /* sub23: 比对自身累计 DWT 周期(u32 粒度,不含快照拷贝) */ UINT32 u32DsCompareCyc; /* sub23: 比对自身累计 DWT 周期(u32 粒度,不含快照拷贝) */
UINT32 u32OpFreshRescues; /* sub24: OP 输入新鲜度救援次数(主站网页看,不打串口) */
} OBJ_STRUCT_PACKED_END } OBJ_STRUCT_PACKED_END
TOBJ2000; TOBJ2000;
static TOBJ2000 s_syncDiag = {23, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
/* 同上:UINT32 首条目必须落在字节 2(未紧排时为 4)。 */ /* 同上:UINT32 首条目必须落在字节 2(未紧排时为 4)。 */
_Static_assert(offsetof(TOBJ2000, u32Sync0) == 2, "TOBJ2000 未紧排:检查 OBJ_STRUCT_PACKED_START"); _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_GetSync1(void);
extern UINT32 EcatDbg_GetEscIrq(void); extern UINT32 EcatDbg_GetEscIrq(void);
extern UINT32 EcatDbg_GetUptimeSec(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 UINT32 EcatDbg_Dwt(void);
extern volatile UINT32 g_dbg_pdiCyc; extern volatile UINT32 g_dbg_pdiCyc;
extern volatile UINT32 g_dbg_outCyc; 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 */ /* [DIFF-STATS] 差分写统计窗口值(comm/ethercat_diff_stats.cpp),out[0..10] 对应 sub13..23 */
void EcatDiffStats_GetWindow(UINT32 *out); void EcatDiffStats_GetWindow(UINT32 *out);
/* 1.6.33:OP 输入新鲜度 — APPL_InputMapping 每次成功刷新就自增 */ /* OP 输入新鲜度 — APPL_InputMapping 每次成功刷新就自增 */
static volatile UINT32 s_inMapSeq = 0U; static volatile UINT32 s_inMapSeq = 0U;
static UINT32 s_inMapSeqSeen = 0U; static UINT32 s_inMapSeqSeen = 0U;
static UINT32 s_inStaleBaseCyc = 0U; static UINT32 s_inStaleBaseCyc = 0U;
@@ -159,11 +156,11 @@ static UINT32 s_opFreshRescues = 0U;
* 不能用 UINT32 重复声明。 */ * 不能用 UINT32 重复声明。 */
extern uint32_t SystemCoreClock; extern uint32_t SystemCoreClock;
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim, 1.5.54)。 /* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim)。
* 1.5.53 时这里直接调 EcatCan_* 转发桥;1.5.54 把"一个 EtherCAT 周期怎么收发" * 本文件为纯 SSC 移植层:只留对象字典 + 三行转发。 */
* 收拢进 EthercatPdo 类,本文件退回纯 SSC 移植层:只留对象字典 + 三行转发。 */
extern void EcatPdo_ReadOutputs(const UINT8 *esc); extern void EcatPdo_ReadOutputs(const UINT8 *esc);
extern void EcatPdo_Process(void); extern void EcatPdo_Process(void);
extern uint32_t EcatPdo_ProcessSeq(void);
extern void EcatPdo_WriteInputs(UINT8 *esc); extern void EcatPdo_WriteInputs(UINT8 *esc);
/* 0x2502 用:进 BOOT 的异步请求(sys/ota_boot_request.cpp 的 extern "C" shim)。 /* 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) 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, { 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, 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), 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 * 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]。 * pEntryDesc[1] 描述所有子索引; 对 OBJCODE_REC 是逐下标索引 pEntryDesc[Subindex]。
* 因此 REC 对象的描述数组必须给满(Sub0..SubN 共 N+1 项), 否则 SDO Info 越界读到 * 因此 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 报错 * 条目类型完全一致时用 OBJCODE_ARR —— 描述数组固定 2 项, 由 SSC 复用 [1]。
* 而 sub1/2 侥幸通过, 正是越界特征)。
*
* 1.5.55: 输入 PDO 扩到 48 条目后, 继续用 REC 就得写 49 项描述符(且 0x1600/0x1A00
* 与 0x7000/0x6000 条目数不同, 不能再共用一个数组)。这些条目类型完全一致, 正是
* ARR 的语义, 故统一改为 OBJCODE_ARR —— 描述数组固定 2 项, 由 SSC 复用 [1]。
* 注意: 可被 PDO 映射的数据子条目必须带 OBJACCESS_RxPDOMAPPING/TxPDOMAPPING, * 注意: 可被 PDO 映射的数据子条目必须带 OBJACCESS_RxPDOMAPPING/TxPDOMAPPING,
* 否则主站校验 0x1600/0x1A00 的映射目标时会拒绝(官方 sampleappl.h 同此)。 * 否则主站校验 0x1600/0x1A00 的映射目标时会拒绝(官方 sampleappl.h 同此)。
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
@@ -270,13 +262,13 @@ OBJCONST TSDOINFOENTRYDESC OBJMEM asInputEntryDesc[] = {
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING} {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}
}; };
/* SYNC 诊断 0x2000: REC, sub0..sub23 共 24 项, 纯只读不参与 PDO 映射, /* SYNC 诊断 0x2000: REC, sub0..sub24 共 25 项, 纯只读不参与 PDO 映射,
* 改子索引数无需重刷 SII/EEPROM。sub5..12 为 1.6.0 剖分埋点(DWT 周期/计数 * 改子索引数无需重刷 SII/EEPROM。sub5..12 为剖分埋点(DWT 周期/计数累加);sub13..23 为 [DIFF-STATS] 差分写取证仪表;
* 累加,定位负载构成后移除);sub13..23 为 [DIFF-STATS] 差分写取证仪表。 * sub24 = OP 输入新鲜度救援次数(不打串口,网页 /api/sync_diag 读)。
* 行数必须与 TOBJ2000 字段一一对应:少一行不会编译报错,只会让 SDO 从错误的 * 行数必须与 TOBJ2000 字段一一对应:少一行不会编译报错,只会让 SDO 从错误的
* 偏移取值 —— 故逐行标号便于核对。 */ * 偏移取值 —— 故逐行标号便于核对。 */
OBJCONST TSDOINFOENTRYDESC OBJMEM asSyncDiagEntryDesc[] = { 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}, /* sub1 : SYNC0 计数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub2 : SYNC1 计数 */ {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub2 : SYNC1 计数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub3 : ESC IRQ 计数 */ {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}, /* sub21 : [DIFF-STATS] 段4 变化字节 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub22 : [DIFF-STATS] 段5 变化字节 */ {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub22 : [DIFF-STATS] 段5 变化字节 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* sub23 : [DIFF-STATS] 比对自身 DWT 周期 */ {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)}, {NULL, NULL, 0x6000, {DEFTYPE_UNSIGNED64, PDO_IN_ENTRIES | (OBJCODE_ARR << 8)},
asInputEntryDesc, aName0x6000, &s_inputObj, NULL, NULL, 0x0000}, asInputEntryDesc, aName0x6000, &s_inputObj, NULL, NULL, 0x0000},
/* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路; sub5..8 为 1.6.0 剖分埋点) */ /* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路) */
{NULL, NULL, 0x2000, {DEFTYPE_RECORD, 23 | (OBJCODE_REC << 8)}, {NULL, NULL, 0x2000, {DEFTYPE_RECORD, 24 | (OBJCODE_REC << 8)},
asSyncDiagEntryDesc, aName0x2000, &s_syncDiag, NULL, NULL, 0x0000}, asSyncDiagEntryDesc, aName0x2000, &s_syncDiag, NULL, NULL, 0x0000},
/* 0x2502: 引导程序交接请求(写 1 = 回 BOOT 等灌写;见 ObjBootReqWrite 注) */ /* 0x2502: 引导程序交接请求(写 1 = 回 BOOT 等灌写;见 ObjBootReqWrite 注) */
@@ -413,7 +406,7 @@ TOBJECT OBJMEM * VARMEM ApplicationObjDic = ApplicationObjDicList;
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
UINT16 APPL_GenerateMapping(UINT16 *pInputSize, UINT16 *pOutputSize) 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 */ *pOutputSize = ECAT_PDO_OUT_BYTES; /* 48×UINT64 = 384 bytes */
return 0; return 0;
} }
@@ -429,12 +422,12 @@ UINT16 APPL_StopMailboxHandler(void) { return 0; }
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
UINT16 APPL_StartInputHandler(UINT16 *pIntMask) UINT16 APPL_StartInputHandler(UINT16 *pIntMask)
{ {
/* 1.5.44:此处**不能**覆写 *pIntMask。 /* 此处**不能**覆写 *pIntMask。
* *
* pIntMask 指向 SSC 全局 u16ALEventMask,StartInputHandler() 已按同步模式算好: * pIntMask 指向 SSC 全局 u16ALEventMask,StartInputHandler() 已按同步模式算好:
* free-run + 有输出 PDO 时它是 PROCESS_OUTPUT_EVENT(0x0400)(ecatslv.c:959/1090), * free-run + 有输出 PDO 时它是 PROCESS_OUTPUT_EVENT(0x0400)(ecatslv.c:959/1090),
* 即 SM2 收到输出这一事件才是 SM-Synchron 模式的同步节拍。 * 即 SM2 收到输出这一事件才是 SM-Synchron 模式的同步节拍。
* 旧代码写死 0x0800(PROCESS_INPUT_EVENT) 把 0x0400 顶掉了,而随后的 * 若写死 0x0800(PROCESS_INPUT_EVENT) 会把 0x0400 顶掉,而随后的
* SetALEventMask() 只做“或”,于是寄存器 0x204 永远缺 SM2 位: * SetALEventMask() 只做“或”,于是寄存器 0x204 永远缺 SM2 位:
* 输出到达不再产生 IRQ,PDI_Isr 只能被主站“读输入”事件带起来, * 输出到达不再产生 IRQ,PDI_Isr 只能被主站“读输入”事件带起来,
* 输入刷新因此永远落在主站读之后 —— SM3 是 3-buffer 缓冲模式, * 输入刷新因此永远落在主站读之后 —— SM3 是 3-buffer 缓冲模式,
@@ -482,7 +475,7 @@ void APPL_InputMapping(UINT16 *pInputData)
s_syncDiag.u32Sync1 = EcatDbg_GetSync1(); s_syncDiag.u32Sync1 = EcatDbg_GetSync1();
s_syncDiag.u32EscIrq = EcatDbg_GetEscIrq(); s_syncDiag.u32EscIrq = EcatDbg_GetEscIrq();
s_syncDiag.u32UptimeS = EcatDbg_GetUptimeSec(); s_syncDiag.u32UptimeS = EcatDbg_GetUptimeSec();
/* 1.6.0 剖分埋点快照(sub5..12) */ /* 剖分埋点快照(sub5..12) */
s_syncDiag.u32PdiCyc = g_dbg_pdiCyc; s_syncDiag.u32PdiCyc = g_dbg_pdiCyc;
s_syncDiag.u32OutCyc = g_dbg_outCyc; s_syncDiag.u32OutCyc = g_dbg_outCyc;
s_syncDiag.u32FreeRunCyc = g_dbg_freerunCyc; s_syncDiag.u32FreeRunCyc = g_dbg_freerunCyc;
@@ -491,6 +484,7 @@ void APPL_InputMapping(UINT16 *pInputData)
s_syncDiag.u32InWriteCyc = g_dbg_inWriteCyc; s_syncDiag.u32InWriteCyc = g_dbg_inWriteCyc;
s_syncDiag.u32AlCyc = g_dbg_alCyc; s_syncDiag.u32AlCyc = g_dbg_alCyc;
s_syncDiag.u32AppCyc = g_dbg_appCyc; s_syncDiag.u32AppCyc = g_dbg_appCyc;
s_syncDiag.u32OpFreshRescues = s_opFreshRescues;
/* [DIFF-STATS] 差分写统计快照(sub13..23):一次取全 11 个窗口值, /* [DIFF-STATS] 差分写统计快照(sub13..23):一次取全 11 个窗口值,
* 取的都是"上一个已完成的 1s 窗口",故 SDO 读不会拿到半累积值。 */ * 取的都是"上一个已完成的 1s 窗口",故 SDO 读不会拿到半累积值。 */
@@ -510,12 +504,12 @@ void APPL_InputMapping(UINT16 *pInputData)
s_syncDiag.u32DsCompareCyc = ds[10]; s_syncDiag.u32DsCompareCyc = ds[10];
} }
/* 1.5.54:先让 EthercatPdo 把本周期的输入数据写进 0x6000 的对象存储, /* 先让 EthercatPdo 把本周期的输入数据写进 0x6000 的对象存储,
* 再交给 ESC。经由对象存储中转(而非直接写 pInputData)是为了让主站 * 再交给 ESC。经由对象存储中转(而非直接写 pInputData)是为了让主站
* SDO 读 0x6000 时看到与过程数据一致的内容。 */ * SDO 读 0x6000 时看到与过程数据一致的内容。 */
EcatPdo_WriteInputs((UINT8 *)&s_inputObj.u64Data[0]); EcatPdo_WriteInputs((UINT8 *)&s_inputObj.u64Data[0]);
memcpy(pInputData, &s_inputObj.u64Data[0], ECAT_PDO_IN_BYTES); memcpy(pInputData, &s_inputObj.u64Data[0], ECAT_PDO_IN_BYTES);
/* 1.6.33:OP 输入新鲜度不变量的活性计数。每次真正走到输入映射就自增, /* OP 输入新鲜度不变量的活性计数。每次真正走到输入映射就自增,
* 供 EcatDbg_OpInputFreshness 判断 PDI 路径是否还活着。 */ * 供 EcatDbg_OpInputFreshness 判断 PDI 路径是否还活着。 */
++s_inMapSeq; ++s_inMapSeq;
} }
@@ -534,11 +528,9 @@ void APPL_OutputMapping(UINT16 *pOutputData)
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
void APPL_Application(void) void APPL_Application(void)
{ {
/* 1.5.54:应用逻辑全部搬进 EthercatPdo::process()(当前实现是 PDO↔CAN 转发)。 /* 应用逻辑在 EthercatPdo::process()(当前实现是 PDO↔CAN 转发)。
* * 不 CpuLoad 包裹:SM/DC 下本函数在 PDI/Sync0 ISR 内已被 isrEnter/Exit
* 原 Phase 2 的 32B 回环已被取代:回环的验证价值由输入 PDO 的 seq 回显 * 计入;free-run 下若主循环每圈调用,再包会抬高 load_perm。 */
* (ECAT_CAN_RX_OFF_ACK)承担——主站写 seq=N 后应在下一周期读回 ack=N,
* 同样能证明 输出→应用→输入 整条过程数据链路通。 */
EcatPdo_Process(); EcatPdo_Process();
} }
@@ -547,11 +539,9 @@ void APPL_Application(void)
* [19:16]=AL 状态(0x0130 低 4 位) [31:24]=AL 错误码(0x0130 高 8 位) * [19:16]=AL 状态(0x0130 低 4 位) [31:24]=AL 错误码(0x0130 高 8 位)
* [15:0] =AL 事件寄存器(0x0220, bit0=主站 AL 控制请求待处理) * [15:0] =AL 事件寄存器(0x0220, bit0=主站 AL 控制请求待处理)
* *
* 1.5.59 字节序修正:ESC 寄存器经 PDI/CSR 读出即主机序(LAN9252 CSR 引擎按 * ESC 寄存器经 PDI/CSR 读出即主机序(LAN9252 CSR 引擎按
* 16-bit 主机总线给出寄存器"值";线上大端只存在于网络侧与 PRAM 原始帧数据), * 16-bit 主机总线给出寄存器"值";线上大端只存在于网络侧与 PRAM 原始帧数据),
* 与 SSC 自身 GetSyncMan/mailbox 的裸读约定一致。1.5.48~1.5.58 这里误加了 * 与 SSC 自身 GetSyncMan/mailbox 的裸读约定一致,勿加 SWAPWORD。
* SWAPWORD,心跳打印的 al/evt 全是字节交换后的错值——那段时期的串口日志
* (如 evt=0x0400)需按字节交换重新解读后才能当证据用。
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
UINT32 EcatDbg_ReadAlDiag(void) 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 节流调用) * (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 不变式是 * 守护(1)0x204 缺位自愈:从站 AL>=SAFEOP 且有输出 PDO 时,SSC 不变式是
* "0x204 ⊇ u16ALEventMask"。读实际值比对,缺位则补写并打 WARN。 * "0x204 ⊇ u16ALEventMask"。读实际值比对,缺位则补写并打 WARN。
* 1.5.60 加固:期望值 u16ALEventMask 本身也可能缺 SM2 位——下行转换路径 * 先补期望值再比对——下行转换路径(ecatslv.c AL_ControlInd 尾部)同时清
* (ecatslv.c AL_ControlInd 尾部)同时清 0x204 的 PDO 事件位、bEscIntEnabled、 * 0x204 的 PDO 事件位、bEscIntEnabled、bEcatFirstOutputsReceived;若期望值
* bEcatFirstOutputsReceived,恢复全靠 SAFEOP 重入的 StartInputHandler + * 本身缺 SM2 位,旧判定恒通过、缺位永远补不回。SM-Synchron+有输出 PDO 的
* SetALEventMask(ecatslv.c:1506);重入没走全时 0x204 丢位。旧判定只比 * 不变式是 u16ALEventMask ⊇ PROCESS_OUTPUT_EVENT(ecatslv.c:959/1086)。
* "actual ⊇ 期望值",而期望值变量在下行路径并不清零——若错的是期望值本身,
* 比对恒通过、缺位永远补不回。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 该做的事:置位 * = PDI_Isr 通路楔死。从主循环直接完成 PDI_Isr 该做的事:置位
* bEcatFirstOutputsReceived + 读 SM2 首/末字应答事件(ecatappl.c:317-320 同款)。 * bEcatFirstOutputsReceived + 读 SM2 首/末字应答事件(ecatappl.c:317-320 同款)。
* 协议等价:bit10 挂起 = 主站已写过 SM2 = "收到输出"成立;AL_ControlRes 下一个 * 协议等价:bit10 挂起 = 主站已写过 SM2 = "收到输出"成立。
* MainLoop 周期即完成 SAFEOP→OP。WARN 附带全部内部旗标(bEscInt/bDcSync/ * WARN 附带全部内部旗标(bEscInt/bDcSync/期望与实际 mask/看门狗状态),
* 期望与实际 mask/看门狗状态),下次复现一行日志即可定位楔死路径。 * 下次复现一行日志即可定位楔死路径。
* *
* 自愈(3)OP 态 PDI_Isr 门控(1.5.60):AL==OP 且 bEscIntEnabled==FALSE 不可 * 自愈(3)OP 态 PDI_Isr 门控:AL==OP 且 bEscIntEnabled==FALSE 不可
* 合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改 AL 状态; * 合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改 AL 状态;
* 守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即重入路径漏了恢复段。 * 守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即重入路径漏了恢复段。
* 此时 PDI_Isr 整体被门控(ecatappl.c:282),PDO_OutputMapping 停调、输入冻结 * 此时 PDI_Isr 整体被门控(ecatappl.c:282),PDO_OutputMapping 停调、输入冻结。
* ——主站侧表现即楔死看门狗盯的窗口回显冻结(2026-09-05 实测 250µs 档每 * 修法:重开门 + 应答挂起的 SM2 事件(掩码补位由守护(1)在同次调用完成),
* 20~35s 触发一次重拉,wr 两小时涨 38)。修法:重开门 + 应答挂起的 SM2 事件 * 下一帧 LRW 的 SM2 中断即恢复整条过程数据链路。
* (掩码补位由守护(1)在同次调用完成),下一帧 LRW 的 SM2 中断即恢复整条
* 过程数据链路,主站看门狗(2s 阈值)从此摸不到触发条件。
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
UINT32 EcatDbg_GuardAleventMask(void) UINT32 EcatDbg_GuardAleventMask(void)
{ {
@@ -613,23 +591,11 @@ UINT32 EcatDbg_GuardAleventMask(void)
return 0xFFFFU; /* 状态不适用:返回哨兵值,调用方不打印 */ return 0xFFFFU; /* 状态不适用:返回哨兵值,调用方不打印 */
} }
/* (0) 邮箱 SM 地址/长度失配兜底(1.5.52 引入,1.5.59 修复字节序并降级为兜底): /* (0) 邮箱 SM 地址/长度失配兜底:
* * 邮箱几何构建期常量化(mailbox.c 直达 ecat_def.h:MBX_SM0/1_START/SIZE),
* 历史楔死(1.5.51 及更早,断电才清)的实锤证据链: * CSR 错值无进入缓存的路径;本守护为运行期内存踩踏的最后兜底:
* - ESC/SSC 邮箱处理全程活着(注入合法 CoE 请求后 200ms 内 SM1 出响应); * AL>=PREOP 时用寄存器真值校验 SSC 缓存的 start/len,失配即写回 + WARN。
* - 但响应帧头出现在 SM1 start+8 而非 start(且逐会话累积:+4→+8,断电才清零); * ESC 寄存器经 PDI/CSR 读出即主机序,勿加 SWAPWORD。 */
* - 网络侧(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,下面的失配判定恒短路,守护自上线起从未真正
* 执行过("自愈"名存实亡,楔死实际靠重枚举重读碰运气恢复)。 */
if (al >= 2U && bMbxRunning) { if (al >= 2U && bMbxRunning) {
UINT16 sm0Start = 0U, sm0Len = 0U, sm1Start = 0U, sm1Len = 0U; UINT16 sm0Start = 0U, sm0Len = 0U, sm1Start = 0U, sm1Len = 0U;
HW_EscReadWord(sm0Start, ESC_SYNCMAN_REG_OFFSET); HW_EscReadWord(sm0Start, ESC_SYNCMAN_REG_OFFSET);
@@ -656,13 +622,11 @@ UINT32 EcatDbg_GuardAleventMask(void)
return 0xFFFFU; /* 以下仅 SAFEOP+ 适用 */ return 0xFFFFU; /* 以下仅 SAFEOP+ 适用 */
} }
/* 1.5.59:PDI 寄存器读即主机序,不再 SWAPWORD——1.5.48~1.5.58 交换出的 /* PDI 寄存器读即主机序,勿加 SWAPWORD。 */
* evt/actual 是错值,0x204 缺位判定恒真(每 250ms 盲写一次 mask,值凑巧
* 正确故未暴露),SM2 事件判定则对在错位上。详见上方字节序修复说明。 */
evt = HW_GetALEventRegister(); evt = HW_GetALEventRegister();
HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET); HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET);
/* (1) 0x204 缺位自愈(1.5.60 加固:先补期望值,再比对补实际值) */ /* (1) 0x204 缺位自愈:先补期望值,再比对补实际值 */
if ((u16ALEventMask & PROCESS_OUTPUT_EVENT) == 0U) { if ((u16ALEventMask & PROCESS_OUTPUT_EVENT) == 0U) {
u16ALEventMask |= PROCESS_OUTPUT_EVENT; u16ALEventMask |= PROCESS_OUTPUT_EVENT;
printf("[ECAT][AEM] WARN: expected AL mask lacked SM2 bit -> 0x%04X\r\n", 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); (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),而下行必改 * 不可合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改
* AL 状态;守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即 SAFEOP * AL 状态;守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即 SAFEOP
* 重入路径漏了恢复段。此时 PDI_Isr 整体被门控(ecatappl.c:282), * 重入路径漏了恢复段。此时 PDI_Isr 整体被门控(ecatappl.c:282),
* PDO_OutputMapping 停调、输入冻结,只能靠主站看门狗重拉总线。 * PDO_OutputMapping 停调、输入冻结。
* 修法:重开门 + 应答挂起的 SM2 事件(ISR 非 OP 分支同款读首/末字); * 修法:重开门 + 应答挂起的 SM2 事件(ISR 非 OP 分支同款读首/末字);
* 0x204 掩码补位已由上面(1)在同次调用完成,下一帧 LRW 即恢复。 */ * 0x204 掩码补位已由上面(1)在同次调用完成,下一帧 LRW 即恢复。 */
if (al == 8U /* OP */ && nPdOutputSize > 0U && bEscIntEnabled == FALSE) { 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 分支被 * SM-Synchron 稳态下 MainLoop 的 free-run 分支被
* (!bEscIntEnabled || !bEcatFirstOutputsReceived) && !bDcSyncActive * (!bEscIntEnabled || !bEcatFirstOutputsReceived) && !bDcSyncActive
* 整段跳过,输入 PDO **只**由 PDI_Isr 的 PDO_InputMapping 刷新。 * 整段跳过,输入 PDO **只**由 PDI_Isr 的 PDO_InputMapping 刷新。
* PDI 通路一停(门控/掩码/EXTI),ESC 仍答 WKC、AL 仍 OP,主站读到的是 * PDI 通路一停(门控/掩码/EXTI),ESC 仍答 WKC、AL 仍 OP,主站读到的是
* 冻住的旧缓冲 —— 网页绿灯+高 Hz、CPU/CAN 全像没解析。既有 1.5.60 只救 * 冻住的旧缓冲。
* "门关着",救不了"门开着但 EXTI 不来"。
* *
* 根治:把「OP ⇒ 输入必须在 T ms 内被重写」做成不变量。 * 不变量:「OP ⇒ 输入必须在 T ms 内被重写」。
* - 正常路径:PDI_Isr 每拍自增 s_inMapSeq * - 正常路径:Process 每拍自增 seq
* - 异常路径:seq 停涨超过阈值 → 主循环强制 PDO_InputMapping * - 异常路径:seq 停涨超过阈值 → 主循环强制 PDO_InputMapping
* (屏中断防双写)+ 重开门/应答 SM2,主站侧 gseq/rx 立刻恢复前进 * (屏中断防双写)+ 重开门/应答 SM2,主站侧 gseq/rx 立刻恢复前进
* 不依赖主站重拉、不依赖人工复位。MainLoop 若本身死锁则本函数也停 —— * 不依赖主站重拉、不依赖人工复位。MainLoop 若本身死锁则本函数也停 ——
@@ -736,7 +698,9 @@ void EcatDbg_OpInputFreshness(void)
{ {
const UINT8 al = (UINT8)(nAlStatus & 0x0FU); const UINT8 al = (UINT8)(nAlStatus & 0x0FU);
const UINT32 now = EcatDbg_Dwt(); 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) { if (al != 8U || nPdInputSize == 0U) {
s_inMapSeqSeen = seq; s_inMapSeqSeen = seq;
@@ -759,7 +723,9 @@ void EcatDbg_OpInputFreshness(void)
} }
/* 强制刷新:先重开门/首输出标志(门关着时 PDI 永远不会回来), /* 强制刷新:先重开门/首输出标志(门关着时 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; UINT16 dummy = 0U;
const UINT16 evt = HW_GetALEventRegister(); const UINT16 evt = HW_GetALEventRegister();
@@ -771,19 +737,18 @@ void EcatDbg_OpInputFreshness(void)
HW_EscReadWord(dummy, nEscAddrOutputData); HW_EscReadWord(dummy, nEscAddrOutputData);
HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U)); HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U));
} }
/* 救援路径计入 busy(低频;与 ISR/任务派发同一口径)。
* 不要把 MainLoop 整段包进 CpuLoad——见 task_runner / cpu_load.h。 */
CpuLoad_Enter();
const uint32_t prevMask = EscExmcBus_MaskEnter(); const uint32_t prevMask = EscExmcBus_MaskEnter();
EcatPdo_Process();
PDO_InputMapping(); PDO_InputMapping();
EscExmcBus_MaskExit(prevMask); EscExmcBus_MaskExit(prevMask);
CpuLoad_Leave();
++s_opFreshRescues; ++s_opFreshRescues;
s_inMapSeqSeen = s_inMapSeq; s_inMapSeqSeen = EcatPdo_ProcessSeq();
s_inStaleBaseCyc = EcatDbg_Dwt(); s_inStaleBaseCyc = EcatDbg_Dwt();
printf("[ECAT][FRESH] WARN: OP input frozen (seq=%lu) evt=0x%04X " /* 不打串口:计数经 0x2000:24 / /api/sync_diag 上到网页,避免 5ms 刷屏。 */
"(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);
} }
} }
+3 -5
View File
@@ -3,8 +3,6 @@
* @brief DCU EtherCAT 应用层接口声明 * @brief DCU EtherCAT 应用层接口声明
* *
* 提供 SSC 所需的 APPL_ 回调函数声明。 * 提供 SSC 所需的 APPL_ 回调函数声明。
* Phase 1: 桩实现,仅保证编译通过。
* Phase 3: 接入实际 PDO 映射(DCU 传感器输入 / 控制输出)。
*/ */
#ifndef COMM_SSC_PORT_DC_APP_H #ifndef COMM_SSC_PORT_DC_APP_H
#define 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_OutputMapping(UINT16 *pOutputData);
void APPL_Application(void); void APPL_Application(void);
/* 1.6.33:OP 输入新鲜度不变量(task_runner 每圈调用;内部廉价,仅冻结时强制重写) */ /* OP 输入新鲜度不变量(task_runner 每圈调用;内部廉价,仅冻结时强制重写) */
void EcatDbg_OpInputFreshness(void); void EcatDbg_OpInputFreshness(void);
UINT32 EcatDbg_OpFreshRescues(void); UINT32 EcatDbg_OpFreshRescues(void);
/* 1.6.34: HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */ /* HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
uint32_t EscExmcBus_MaskEnter(void); uint32_t EscExmcBus_MaskEnter(void);
void EscExmcBus_MaskExit(uint32_t prev); void EscExmcBus_MaskExit(uint32_t prev);
uint32_t HW_EscExmcMaskEnter(void); uint32_t HW_EscExmcMaskEnter(void);
void HW_EscExmcMaskExit(uint32_t prev); void HW_EscExmcMaskExit(uint32_t prev);
/* 1.6.34: 复位输出 sequence 状态机 */ /* 复位输出 sequence 状态机 */
void EcatCan_ResetSequenceState(void); void EcatCan_ResetSequenceState(void);
#ifdef __cplusplus #ifdef __cplusplus
+17 -29
View File
@@ -144,22 +144,17 @@
#endif #endif
/** 看门狗由定时器中断检查。 /** 看门狗由定时器中断检查。
* 1.5.47:1→0。ECAT_TIMER_INT=1 要求端口提供调用 ECAT_CheckTimer() 的 1ms 硬件 * ECAT_TIMER_INT=0:MainLoop 按 DWT CYCCNT 轮询(hw_gd32exmc.cpp 的
* 定时器 ISR——本工程从未实现,导致 ECAT_CheckTimer 在整个固件里零调用者: * HW_GetTimerValue/HW_ClearTimerValue),每 1ms 调一次 ECAT_CheckTimer。
* (a) EsmTimeoutCounter 永不递减 → 任何 pending 状态迁移(SAFEOP→OP 等)永不 * 若置 1 则要求端口提供调用 ECAT_CheckTimer() 的 1ms 硬件定时器 ISR——
* 超时,从站会无限期忽略一切 AL Control(实测"进 PREOP 失败: AL=SAFEOP * 本工程无该 ISR,ECAT_CheckTimer 零调用者会导致 EsmTimeoutCounter 永不递减
* code=0x0000"的卡死就是它); * (状态迁移永不超时)及 DC PLL 计数器不前进(残留 DC 激活位时 SAFEOP+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。 */
#ifndef ECAT_TIMER_INT #ifndef ECAT_TIMER_INT
#define ECAT_TIMER_INT 0 #define ECAT_TIMER_INT 0
#endif #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 #ifndef ECAT_TIMER_INC_P_MS
#define ECAT_TIMER_INC_P_MS 180000U #define ECAT_TIMER_INC_P_MS 180000U
#endif #endif
@@ -535,13 +530,10 @@
------ ------
------ Struct Packing ------ Struct Packing
------ ------
------ 1.5.42 修正:此前三对宏全部留空,注释误以为"AC6 不需要"。实际这些宏就是 SSC ------ 这些宏是 SSC 紧排结构体的唯一手段——SSC 的 SDO 读写按"si0 占 16bit,
------ 用来紧排结构体的唯一手段——SSC 的 SDO 读写按"si0 占 16bit,其后各条目按 ------ 其后各条目按 BitLength 紧排"计算字节偏移(OBJ_GetEntryOffset, objdef.c:609),
------ BitLength 紧排"计算字节偏移(OBJ_GetEntryOffset, objdef.c:609),留空则结构体 ------ 留空则结构体按编译器自然对齐,与该假设不符,导致 SDO 单条目访问
------ 按编译器自然对齐,与该假设不符。armclang 实测:{UINT16; UINT64[4]} 的 UINT64 ------ 全部读写错位内存;SSC 内部 0x1C3x/0x10F1 等同样受影响。
------ 落在字节 8 而非 2(编译期断言验证),{UINT16; UINT32 x4} 落在字节 4 而非 2,
------ 于是 0x7000/0x6000/0x2000 的 SDO 单条目访问全部读写错位内存;SSC 内部
------ 0x1C3x(TSYNCMANPAR)/0x10F1 等同样受影响。
------ AC6/armclang 支持 GCC 风格 __attribute__((packed)),置于 struct 关键字之后 ------ AC6/armclang 支持 GCC 风格 __attribute__((packed)),置于 struct 关键字之后
------ (即 START 位置)对整个结构体生效。 ------ (即 START 位置)对整个结构体生效。
-----------------------------------------------------------------------------------------*/ -----------------------------------------------------------------------------------------*/
@@ -590,11 +582,8 @@
------ ------
------ PDO Size (需与 DCU_ESI.xml / gen_eeprom.py / dc_app.c 三处一致) ------ PDO Size (需与 DCU_ESI.xml / gen_eeprom.py / dc_app.c 三处一致)
------ ------
------ 1.5.56 扩到 DPRAM 硬上限:64B/384B → 384B/640B。 ------ 上限依据:邮箱占 0x1000-0x13FF,过程数据区仅 0x1400-0x1FFF=3072B,
------ 输出 384B = 48×64bit:兼容单帧区 48B + CANFD 批量发送区 336B ------ SM2/SM3 均 3-buffer 实占 3×size ⇒ out+in ≤ 1024B。
------ 输入 640B = 80×64bit:桥状态 32B + 电机窗口 288B + 长帧 40B + 批量接收区 280B
------ 上限依据:邮箱占 0x1000-0x13FF,过程数据区仅 0x1400-0x1FFF=3072B,
------ SM2/SM3 均 3-buffer 实占 3×size ⇒ out+in ≤ 1024B。384+640 恰好用满。
------ 推导与实测(SII 余量/EXMC 耗时)见 comm/ethercat_pdo_size.h。 ------ 推导与实测(SII 余量/EXMC 耗时)见 comm/ethercat_pdo_size.h。
------ ------
------ 这两个宏是**编译期硬门限**,必须先于其它改动生效: ------ 这两个宏是**编译期硬门限**,必须先于其它改动生效:
@@ -604,12 +593,12 @@
------ 忘改这里会在进 SAFEOP 时报 AL 错误,而不是静默截断。 ------ 忘改这里会在进 SAFEOP 时报 AL 错误,而不是静默截断。
-----------------------------------------------------------------------------------------*/ -----------------------------------------------------------------------------------------*/
/** 最大输出 PDO 字节数(RxPDO,主站→从站)。1.5.56: 0x40 → 0x180 = 384B */ /** 最大输出 PDO 字节数(RxPDO,主站→从站)。0x180 = 384B */
#ifndef MAX_PD_OUTPUT_SIZE #ifndef MAX_PD_OUTPUT_SIZE
#define MAX_PD_OUTPUT_SIZE 0x180 #define MAX_PD_OUTPUT_SIZE 0x180
#endif #endif
/** 最大输入 PDO 字节数(TxPDO,从站→主站)。1.5.56: 0x140 → 0x280 = 640B */ /** 最大输入 PDO 字节数(TxPDO,从站→主站)。0x280 = 640B */
#ifndef MAX_PD_INPUT_SIZE #ifndef MAX_PD_INPUT_SIZE
#define MAX_PD_INPUT_SIZE 0x280 #define MAX_PD_INPUT_SIZE 0x280
#endif #endif
@@ -710,12 +699,11 @@
#define MAX_MBX_READ_ADDRESS 0x1FFF #define MAX_MBX_READ_ADDRESS 0x1FFF
#endif #endif
/* 1.5.59 根治邮箱楔死:本板邮箱几何构建期常量。 /* 本板邮箱几何构建期常量。
* 取值 = gen_eeprom.py 的 MBX_RX_OFFSET/SIZE(0x1000/256) 与 MBX_TX_OFFSET/SIZE * 取值 = gen_eeprom.py 的 MBX_RX_OFFSET/SIZE(0x1000/256) 与 MBX_TX_OFFSET/SIZE
* (0x1200/256),即 DCU_ESI.xml <Sm> 与 ESC 上电自 SII 加载的寄存器真值;主站 * (0x1200/256),即 DCU_ESI.xml <Sm> 与 ESC 上电自 SII 加载的寄存器真值;主站
* config_init 也只是照抄同一份 SII。MBX_StartMailboxHandler 以此直达缓存, * config_init 也只是照抄同一份 SII。MBX_StartMailboxHandler 以此直达缓存,
* 不再信任单次 CSR 间接读(错值曾以 0x1204/0x1208 进入缓存 → 所有 SDO 响应 * 不信任单次 CSR 间接读,CSR 读仅作交叉校验(mailbox.c 打 WARN)。
* 写错址 → 断电才解的楔死),CSR 读仅作交叉校验(mailbox.c 打 WARN)。
* ★ 改 SII 邮箱几何必须同步改这里(gen_eeprom.py 顶部注释同样有约束)。★ */ * ★ 改 SII 邮箱几何必须同步改这里(gen_eeprom.py 顶部注释同样有约束)。★ */
#ifndef MBX_SM0_START #ifndef MBX_SM0_START
#define MBX_SM0_START 0x1000 #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. 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 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 local spec copy ETG1000_6_V1i0i3):
layout was shifted by 2 words from word 4 onwards and used XOR instead of CRC-8):
Word 0x00 PDI Control (byte0=PDI type 0x8D HBI 16-bit indexed, Word 0x00 PDI Control (byte0=PDI type 0x8D HBI 16-bit indexed,
byte1=ESC Configuration 0x00) byte1=ESC Configuration 0x00)
@@ -62,7 +61,6 @@ MBX_TX_SIZE = 256
# Process Data configuration # 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 一致 # 必须与 comm/ethercat_pdo_size.h 的 ECAT_PDO_OUT_BYTES / ECAT_PDO_IN_BYTES 一致
# ——固件侧有 _Static_assert 守着,但 EEPROM 这一侧无法编译期校验,只能靠这里对齐。 # ——固件侧有 _Static_assert 守着,但 EEPROM 这一侧无法编译期校验,只能靠这里对齐。
# #
@@ -70,10 +68,8 @@ MBX_TX_SIZE = 256
# 邮箱 SM0/SM1 各 256B 固定占 0x1000-0x13FF,过程数据区只剩 0x1400-0x1FFF=3072B。 # 邮箱 SM0/SM1 各 256B 固定占 0x1000-0x13FF,过程数据区只剩 0x1400-0x1FFF=3072B。
# SM2/SM3 都是 3-buffer 模式(control 0x64 / 0x20,bit1=0),实际占用 3×size: # SM2/SM3 都是 3-buffer 模式(control 0x64 / 0x20,bit1=0),实际占用 3×size:
# SM2 384B -> 0x1400-0x187F (384×3 = 1152B) # SM2 384B -> 0x1400-0x187F (384×3 = 1152B)
# SM3 600B -> 0x1880-0x1F67 (600×3 = 1800B) ← 1.6.15,DPRAM 留 120B 余量 # SM3 600B -> 0x1880-0x1F67 (600×3 = 1800B), DPRAM 留 120B 余量
# (384+600)×3 = 2952 ≤ 3072 = 过程数据区全长。历史上 1.5.56 曾取 640B # (384+600)×3 = 2952 ≤ 3072 = 过程数据区全长。
# ((384+640)×3 = 3072 恰好用满),1.6.1 瘦身到 496B 换 CPU,1.6.15 回到 600B
# 换批量区容量——推导与取舍见 comm/ethercat_pdo_size.h。
# SSC 在 ecatslv.c:708-721 按 (start + size*buffer) 做重叠检查,撞了会在 # SSC 在 ecatslv.c:708-721 按 (start + size*buffer) 做重叠检查,撞了会在
# PREOP->SAFEOP 返回 ALSTATUSCODE_INVALIDSMOUTCFG,不是静默出错。 # PREOP->SAFEOP 返回 ALSTATUSCODE_INVALIDSMOUTCFG,不是静默出错。
# 要再更大只能压邮箱(拖慢 SDO/FoE)或改 1-buffer(失去读写并发保护),见 # 要再更大只能压邮箱(拖慢 SDO/FoE)或改 1-buffer(失去读写并发保护),见
@@ -81,14 +77,12 @@ MBX_TX_SIZE = 256
PD_OUT_OFFSET = 0x1400 # SM2: RxPDO (Master -> Slave outputs) PD_OUT_OFFSET = 0x1400 # SM2: RxPDO (Master -> Slave outputs)
PD_OUT_SIZE = 384 # 48×64bit PD_OUT_SIZE = 384 # 48×64bit
PD_IN_OFFSET = 0x1880 # SM3: TxPDO (Slave -> Master inputs) 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) PD_IN_SIZE = 600 # 75×64bit
# 1.6.1 瘦身 640→496:批量接收条目数据 64→32B + 段5诊断区前移到 484。 # 批量接收为**定长覆盖窗口** 5 槽(每槽 12B 头含 u32 seqlock
# 1.6.15 496→600:批量接收改**定长覆盖窗口** 5 槽(每槽 12B 头含 u32 seqlock # + 32B 数据 = 44B,共 220B,落在 364..583),段5诊断区 584。
# + 32B 数据 = 44B,共 220B,落在 364..583),段5诊断区 484→584。 # 5 槽 = 5000 帧/s@1ms。
# 动机:旧"环形队列+变长紧排"容量 3 帧/拍,现场 4000 帧/s 下每拍恒丢 1 帧,
# 且饱和态喂出了 1.6.9~1.6.13 追的错标/拼接竞态。5 槽 = 5000 帧/s@1ms。
# SM3 实占 3×600=1800B (0x1880-0x1F67),out+in=984 ≤ 1024 硬上限,留 120B 余量; # 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 并断电冷启动。 # 改后必须重烧 EEPROM 并断电冷启动。
# 3-buffer 占用与 DPRAM 上限自检:改尺寸/偏移时立刻在生成阶段拦住, # 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. # 0x8D=HBI 16-bit). 0x05 is a reserved value on LAN9252 and disables the HBI PDI.
PDI_CONTROL = 0x8D PDI_CONTROL = 0x8D
# Word 0 high byte = ESC Configuration register (0x0141): all zero # 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 ESC_CONFIG = 0x00
# Word 1 low byte = PDI Configuration register (0x0150) HBI-mode bits: ALE # 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 # 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 # 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 # (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 # 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... # Same value as the official LAN9252-EVB-HBIXML.xml ConfigData 890E80CC...
SYNC_LATCH_CFG = 0xCC SYNC_LATCH_CFG = 0xCC
# Word 2 = Pulse Length of SyncSignals register (0x0982), units of 10ns # 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] chunk = eeprom[row:row+16]
hex_str = ", ".join(f"0x{b:02X}" for b in chunk) hex_str = ", ".join(f"0x{b:02X}" for b in chunk)
if all(b == 0xFF 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 # those array elements zero-initialized (C static init) instead of
# the erased-0xFF state, and the firmware burns the whole array. # the erased-0xFF state, and the firmware burns the whole array.
if row % 256 == 0: if row % 256 == 0:
@@ -3,13 +3,13 @@
* @brief SSC V5.11 硬件 port 层 — GD32E508 + LAN9252 (EXMC 16-bit HBI Indexed Mode) * @brief SSC V5.11 硬件 port 层 — GD32E508 + LAN9252 (EXMC 16-bit HBI Indexed Mode)
* *
* 实现 el9800hw.h 中声明的全部 HW_* 函数,将 SSC 的 ESC 访问请求 * 实现 el9800hw.h 中声明的全部 HW_* 函数,将 SSC 的 ESC 访问请求
* 转发到 EscLan9252 的间接访问端口(1.5.39 按 DS 12.11 修正访问模型): * 转发到 EscLan9252 的间接访问端口:
* - EtherCAT 核心 CSR(0x0000-0x0FFF:AL Status/Event、SyncManager、LED 等) * - EtherCAT 核心 CSR(0x0000-0x0FFF:AL Status/Event、SyncManager、LED 等)
* → ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300) 间接端口; * → ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300) 间接端口;
* - 过程 RAM(0x1000-0x1FFF:邮箱 0x1000+、PDO 0x1400+) * - 过程 RAM(0x1000-0x1FFF:邮箱 0x1000+、PDO 0x1400+)
* → PRAM FIFO(0x000-0x03C 数据口 + 0x308-0x314 地址/命令)。 * → PRAM FIFO(0x000-0x03C 数据口 + 0x308-0x314 地址/命令)。
* 1.5.37 及之前直读/直写 HBI 索引(误以为索引空间统一可寻址),核心 CSR 全部 * 核心 CSR 不在 HBI 直接空间,直读/直写会使核心 CSR 全部
* 读 0/写丢弃——SSC 对主站 AL Control 完全失聪,主站恒读 AL=INIT。 * 读 0/写丢弃,SSC 对主站 AL Control 完全失聪。
* *
* 并发保护(不变): * 并发保护(不变):
* - HW_EscRead/HW_EscWrite 等主循环函数:内部经 EscExmcBus::maskEnter/Exit * - 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)。 * @brief 禁用指定 SM 通道 (清 SM Activate 寄存器 0x0806+8n bit0)。
* @param channel SM 通道号 (0-15) * @param channel SM 通道号 (0-15)
* @note 1.5.39:偏移由 +4 改为 +6——+4 是 SM Control(操作模式字段), * @note SM Activate 在偏移 +6(bit0=SM Enable);+4 是 SM Control(操作模式字段)。
* Activate(bit0=SM Enable)在 +6;旧值从未真正生效过(访问层全空)。
*/ */
void HW_DisableSyncManChannel(UINT8 channel) void HW_DisableSyncManChannel(UINT8 channel)
{ {
+77 -120
View File
@@ -11,94 +11,6 @@
This file contains the Process Data interface This file contains the Process Data interface
\version 5.11 \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 aPdOutputData[(MAX_PD_OUTPUT_SIZE>>1)];
UINT16 aPdInputData[(MAX_PD_INPUT_SIZE>>1)]; UINT16 aPdInputData[(MAX_PD_INPUT_SIZE>>1)];
/*ECAT_DBG_START(1.6.0 临时剖分埋点)*/ /*ECAT_DBG_START(剖分埋点)*/
/* 1.6.0 负载归因埋点:DWT 周期累加器,经 0x2000 sub5..8 上报,主站 1s 差分出速率。 /* 负载归因埋点:DWT 周期累加器,经 0x2000 sub5..8 上报,主站 1s 差分出速率。
* 只为定位 free-run 模式下每周期 508µs 计入时间的真实去向;定位完成后整块移除。
* EcatDbg_Dwt() 定义在 comm/ethercat_sync.cpp (extern "C")。 */ * EcatDbg_Dwt() 定义在 comm/ethercat_sync.cpp (extern "C")。 */
extern UINT32 EcatDbg_Dwt(void); extern UINT32 EcatDbg_Dwt(void);
#define ecatdbg_dwt() EcatDbg_Dwt() #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); 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); 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); extern UINT8 EcatPdo_EscWritePlan(UINT16 *offs, UINT16 *lens, UINT8 maxSegs);
volatile UINT32 g_dbg_pdiCyc = 0; /* sub5: PDI_Isr 全函数累计周期 */ volatile UINT32 g_dbg_pdiCyc = 0; /* sub5: PDI_Isr 全函数累计周期 */
volatile UINT32 g_dbg_outCyc = 0; /* sub6: PDO_OutputMapping(输出探针/全量读) */ volatile UINT32 g_dbg_outCyc = 0; /* sub6: PDO_OutputMapping(输出探针/全量读) */
@@ -200,10 +117,10 @@ BOOL bInitFinished = FALSE; /** < \brief indicates if the initialization is fini
*//////////////////////////////////////////////////////////////////////////////////////// *////////////////////////////////////////////////////////////////////////////////////////
void PDO_InputMapping(void) void PDO_InputMapping(void)
{ {
/*ECAT_DBG(1.6.0 剖分:输入映射全程 + 过程数据写细分)*/ /*ECAT_DBG(剖分:输入映射全程 + 过程数据写细分)*/
/* 1.6.22 稀疏写:APPL 仍整填 aPdInputData(对齐 SDO 0x6000),但 HW_EscWriteIsr /* 稀疏写:APPL 仍整填 aPdInputData(对齐 SDO 0x6000),但 HW_EscWriteIsr
* 只写密排有效段+段3/5,跳过空槽区 —— 省的是 PRAM 字节时间,不是 RAM 拷贝。 * 只写密排有效段+段3/5,跳过空槽区 —— 省的是 PRAM 字节时间,不是 RAM 拷贝。
* 1.6.25 差分写:在稀疏之上再跳过「与影子一致且 3-buffer 冲刷完成」的段。 */ * 差分写:在稀疏之上再跳过「与影子一致且 3-buffer 冲刷完成」的段。 */
const UINT32 dbgT0 = ecatdbg_dwt(); const UINT32 dbgT0 = ecatdbg_dwt();
APPL_InputMapping((UINT16*)aPdInputData); APPL_InputMapping((UINT16*)aPdInputData);
const UINT32 dbgT1 = ecatdbg_dwt(); const UINT32 dbgT1 = ecatdbg_dwt();
@@ -228,32 +145,46 @@ void PDO_InputMapping(void)
to the hardware. This function is only called in case of an SM2 to the hardware. This function is only called in case of an SM2
(output process data) event. (output process data) event.
1.6.0 输出按需全量读(负载优化):每周期先读输出 PDO 头 12B 探针(覆盖 SEQ@0 输出按需全量读:每周期先读输出 PDO 头 12B 探针(覆盖 SEQ@0
与 WIN@8),经 EcatPdo_OutputProbe 判定——seq 无变化沿且无清统计请求时, 与 WIN@8),经 EcatPdo_OutputProbe 判定——seq 无变化沿且无清统计请求时,
跳过 384B 全量读(只补读 SM2 末字完成事件应答),省掉每周期 ~190µs 的 跳过全量读(只补读 SM2 末字完成事件应答)。主站滚动窗口扫槽依赖的
PRAM 搬运(含 ESC 核仲裁等待)。主站滚动窗口扫槽依赖的 WIN 字节在探针内 WIN 字节在探针内每周期刷新,窗口扫描不受影响。
每周期刷新,窗口扫描不受影响。SM2 应答语义不变:首字(探针内)+末字 SM2 应答语义不变:首字(探针内)+末字(两分支各自覆盖)。
(两分支各自覆盖)。
*//////////////////////////////////////////////////////////////////////////////////////// *////////////////////////////////////////////////////////////////////////////////////////
void PDO_OutputMapping(void) void PDO_OutputMapping(void)
{ {
/*ECAT_DBG(1.6.0 剖分:输出映射全程)*/ /*ECAT_DBG(剖分:输出映射全程)*/
const UINT32 dbgT0 = ecatdbg_dwt(); 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]; UINT8 head[12];
HW_EscReadIsr(((MEM_ADDR *) head), nEscAddrOutputData, 12U ); HW_EscReadIsr(((MEM_ADDR *) head), nEscAddrOutputData, 12U );
if ( EcatPdo_OutputProbe(head) ) 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); APPL_OutputMapping((UINT16*) aPdOutputData);
EcatPdo_NoteOutputMapped();
} }
else else
{ {
/* 无新输出:补读 SM2 末字完成事件应答(首字已随探针读过) */ if (nPdOutputSize >= 2U) {
HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2)); HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2));
}
} }
g_dbg_outCyc += ecatdbg_dwt() - dbgT0; g_dbg_outCyc += ecatdbg_dwt() - dbgT0;
} }
@@ -342,7 +273,7 @@ void HandleBusCycleCalculation(void)
void PDI_Isr(void) void PDI_Isr(void)
{ {
/*ECAT_DBG(1.6.0 剖分:ISR 全程)*/ /*ECAT_DBG(剖分:ISR 全程)*/
const UINT32 dbgT0 = ecatdbg_dwt(); const UINT32 dbgT0 = ecatdbg_dwt();
if(bEscIntEnabled) if(bEscIntEnabled)
{ {
@@ -397,17 +328,30 @@ void PDI_Isr(void)
/*ECATCHANGE_END(V5.11) ECAT4*/ /*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(); const UINT32 dbgTb = ecatdbg_dwt();
ECAT_Application(); ECAT_Application();
g_dbg_appCyc += ecatdbg_dwt() - dbgTb; g_dbg_appCyc += ecatdbg_dwt() - dbgTb;
} }
if ( bEcatInputUpdateRunning if ( bEcatInputUpdateRunning
/*ECATCHANGE_START(V5.11) ESM7*/ /*ECATCHANGE_START(V5.11) ESM7*/
&& ((sSyncManInPar.u16SyncType == SYNCTYPE_SM_SYNCHRON) || (sSyncManInPar.u16SyncType == SYNCTYPE_SM2_SYNCHRON)) && ((sSyncManInPar.u16SyncType == SYNCTYPE_SM_SYNCHRON) || (sSyncManInPar.u16SyncType == SYNCTYPE_SM2_SYNCHRON))
/*ECATCHANGE_END(V5.11) ESM7*/ /*ECATCHANGE_END(V5.11) ESM7*/
@@ -438,7 +382,7 @@ void PDI_Isr(void)
HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2)); HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2));
} }
} //if(bEscIntEnabled) } //if(bEscIntEnabled)
/*ECAT_DBG(1.6.0 剖分:ISR 全程 收尾)*/ /*ECAT_DBG(剖分:ISR 全程 收尾)*/
g_dbg_pdiCyc += ecatdbg_dwt() - dbgT0; g_dbg_pdiCyc += ecatdbg_dwt() - dbgT0;
g_dbg_pdiCnt++; g_dbg_pdiCnt++;
} }
@@ -653,15 +597,28 @@ void MainLoop(void)
} }
DISABLE_ESC_INT(); DISABLE_ESC_INT();
/*ECAT_DBG(1.6.0 剖分:free-run 应用+输入映射块)*/ /*ECAT_DBG(剖分:free-run 应用+输入映射块)*/
{ {
const UINT32 dbgT1 = ecatdbg_dwt(); 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 ) if ( bEcatInputUpdateRunning )
{ {
/* EtherCAT slave is at least in SAFE-OPERATIONAL, update inputs */ PDO_InputMapping();
PDO_InputMapping(); }
CpuLoad_Leave();
} }
g_dbg_freerunCyc += ecatdbg_dwt() - dbgT1; 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. and psStoreMbx will be sent (in MBX_CopyToSendMailbox) and stored in psReadMbx, psStoreMbx will be set to 0.
\version 5.11 \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 "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 MBX_StartMailboxHandler(void)
{ {
UINT16 result = 0; UINT16 result = 0;
/* 1.5.59 根治"邮箱楔死"(响应帧头落在 SM1 start+4/+8、逐会话漂移,断电才清): /* 邮箱几何构建期常量(ecat_def.h:MBX_SM0/1_START/SIZE)直达缓存,
* 根因链 = INIT→PREOP 时经 GetSyncMan/CSR 单次间接读(EXMC HBI)把 SM0/SM1 * 不信任 GetSyncMan/CSR 单次间接读——CSR 读降级为交叉校验:
* 几何缓存进 u16EscAddr* 与 u16*MbxSize 等变量;总线毛刺给出的"合法"错值 * 失配打 WARN(说明 ESC 寄存器被非常规改写,需查)但缓存仍取常量。
* (0x1204/0x1208 均为偶地址且在检查范围内)即被缓存,此后所有 SDO 响应写错址, * 运行期内存踩踏也会在每次 INIT→PREOP 被常量覆写。
* 主站按协议从 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 被常量覆写。
* 注意:ESC 寄存器经 PDI/CSR 读出即主机序(与 GetSyncMan 既有路径一致,勿加 * 注意:ESC 寄存器经 PDI/CSR 读出即主机序(与 GetSyncMan 既有路径一致,勿加
* SWAPWORD;PRAM 帧数据才是线上大端,那边交换是对的)。 */ * SWAPWORD;PRAM 帧数据才是线上大端,那边交换是对的)。 */
/*ECATCHANGE_START(V5.11) HW1*/ /*ECATCHANGE_START(V5.11) HW1*/
+83 -50
View File
@@ -36,13 +36,10 @@ static inline bool releaseFifo0(uint32_t canPeriph)
CAN_RFIFO0(canPeriph) = CAN_RFIFO0_RFD0; CAN_RFIFO0(canPeriph) = CAN_RFIFO0_RFD0;
for (uint32_t n = 0U; n < 2000U; ++n) { for (uint32_t n = 0U; n < 2000U; ++n) {
if ((CAN_RFIFO0(canPeriph) & CAN_RFIFO0_RFD0) == 0U) { if ((CAN_RFIFO0(canPeriph) & CAN_RFIFO0_RFD0) == 0U) {
/* 1.6.11 注:这里曾加过 128/2048 拍的 settle 延迟,假设"自清后 MDATA /* 节点指纹实验表明拼接是 FIFO 缓冲指针与帧边界失同步
* 还在搬运"是装载时间竞态 —— 实测无效(正弦指令负载下拼接率不变)。 * (STATUS 帧头 24B = 完整 CONFIG,错位恰为 CONFIG 帧长 6 字),
* 节点指纹实验证明拼接是 FIFO 缓冲指针与帧边界失同步(node3 的
* STATUS 帧头 24B = node2 的完整 CONFIG,错位恰为 CONFIG 帧长 6 字),
* 不是延迟能挡的;结构矛盾的帧由 handleRxInterrupt 的内容校验计数 * 不是延迟能挡的;结构矛盾的帧由 handleRxInterrupt 的内容校验计数
* (1.6.12 起只计不丢 —— 管道必须诚实;源头修复见 1.6.13 的 * (只计不丢 —— 管道必须诚实;源头修复见 CONFIG_IRQ_PREEMPT_CAN_RX)。 */
* CONFIG_IRQ_PREEMPT_CAN_RX=0)。 */
return true; return true;
} }
} }
@@ -55,7 +52,7 @@ static inline bool releaseFifo0(uint32_t canPeriph)
* 地址 CAN_RFIFOMDATA0 连读 dlen/4 次才取完(48B = 12 次),加上头部两次,一条 * 地址 CAN_RFIFOMDATA0 连读 dlen/4 次才取完(48B = 12 次),加上头部两次,一条
* 帧就是十几次有先后依赖的总线访问,整个过程**不是原子的**。若这期间硬件把下 * 帧就是十几次有先后依赖的总线访问,整个过程**不是原子的**。若这期间硬件把下
* 一帧搬进邮箱窗口,就会读出"前几个字属于上一帧、后几个字属于这一帧"的拼接 * 一帧搬进邮箱窗口,就会读出"前几个字属于上一帧、后几个字属于这一帧"的拼接
* 体,而 id/dlc 取自最开始那次读 —— 实测表现为 STATUS 帧的 ID 配上 CONFIG 帧 * 体,而 id/dlc 取自最开始那次读 —— 表现为 STATUS 帧的 ID 配上 CONFIG 帧
* 的前 12~24 字节,且混合边界 100% 落在 4 字节边界上。 * 的前 12~24 字节,且混合边界 100% 落在 4 字节边界上。
* *
* 判据用 MP 寄存器:TS[31:16] 是硬件接收时间戳、DLENC[3:0] 是长度,同一帧内两 * 判据用 MP 寄存器:TS[31:16] 是硬件接收时间戳、DLENC[3:0] 是长度,同一帧内两
@@ -67,8 +64,8 @@ static bool receiveFrameChecked(uint32_t canPeriph, can_receive_message_struct *
{ {
/* 整段取数放进 BASEPRI 临界区:屏蔽 preempt>=1,把 EtherCAT 的 ESC(1)/SYNC1(1) /* 整段取数放进 BASEPRI 临界区:屏蔽 preempt>=1,把 EtherCAT 的 ESC(1)/SYNC1(1)
* 挡在外面。取数是十几次有先后依赖的寄存器读,被抢占就会读出跨帧拼接体。 * 挡在外面。取数是十几次有先后依赖的寄存器读,被抢占就会读出跨帧拼接体。
* 1.6.13 起本 ISR 自身就是 preempt=0(CONFIG_IRQ_PREEMPT_CAN_RX),与 SYNC0 * 本 ISR 自身是 preempt=0(CONFIG_IRQ_PREEMPT_CAN_RX),与 SYNC0
* 同级互不抢占,取数自此真正原子;MI/MP 回读校验保留,作为对硬件自发换帧 * 同级互不抢占,取数真正原子;MI/MP 回读校验保留,作为对硬件自发换帧
* (dequeue 异步完成等)的兜底观测 —— rx_torn 计数即"该病理是否还在发生"。 */ * (dequeue 异步完成等)的兜底观测 —— rx_torn 计数即"该病理是否还在发生"。 */
const uint32_t basepri = RT_CRITICAL_SAVE(); const uint32_t basepri = RT_CRITICAL_SAVE();
@@ -109,7 +106,7 @@ static bool receiveFrameChecked(uint32_t canPeriph, can_receive_message_struct *
/* 取数后回读 MI+MP:任一变化 ⇒ 邮箱窗口在取数期间被换帧,这一帧是拼接体。 /* 取数后回读 MI+MP:任一变化 ⇒ 邮箱窗口在取数期间被换帧,这一帧是拼接体。
* 必须连 MI(完整 ID)一起比 —— 只比 MP 的 TS/DLENC 会漏:TS 是 16 位计数器, * 必须连 MI(完整 ID)一起比 —— 只比 MP 的 TS/DLENC 会漏:TS 是 16 位计数器,
* 相邻两帧可能同值,DLENC 也常常相同(都是 48),于是"ID 报 STATUS、数据却是 * 相邻两帧可能同值,DLENC 也常常相同(都是 48),于是"ID 报 STATUS、数据却是
* CONFIG"这种错配能从校验里溜过去(实测占坏帧的绝大多数)。ID 变了就一定是 * CONFIG"这种错配能从校验里溜过去。ID 变了就一定是
* 换了帧,这条判据没有巧合空间。 */ * 换了帧,这条判据没有巧合空间。 */
const uint32_t keep = CAN_RFIFOMP_TS | CAN_RFIFOMP_DLENC; const uint32_t keep = CAN_RFIFOMP_TS | CAN_RFIFOMP_DLENC;
const uint32_t mp1 = CAN_RFIFOMP(canPeriph, CAN_FIFO0); const uint32_t mp1 = CAN_RFIFOMP(canPeriph, CAN_FIFO0);
@@ -419,13 +416,11 @@ bool CanFd::initNominalTiming(uint32_t canPeriph)
can_parameter.auto_bus_off_recovery = ENABLE; can_parameter.auto_bus_off_recovery = ENABLE;
can_parameter.auto_wake_up = DISABLE; can_parameter.auto_wake_up = DISABLE;
can_parameter.auto_retrans = ENABLE; /* 默认自动重发兜底偶发 ACK 丢失;OTA 停等协议期间由 setChannelAutoRetrans(ch,false) 临时关闭,避免重发打穿电机单缓冲 RX */ can_parameter.auto_retrans = ENABLE; /* 默认自动重发兜底偶发 ACK 丢失;OTA 停等协议期间由 setChannelAutoRetrans(ch,false) 临时关闭,避免重发打穿电机单缓冲 RX */
/* 1.6.11 真根因:这里曾是 ENABLE —— FIFO 满时**覆盖最旧邮箱**,而最旧邮箱 /* rec_fifo_overwrite = DISABLE:FIFO 满时丢新帧而非覆盖最旧邮箱。
* 正是 CPU 正在读取的输出窗口。buildInput 在 PDI_Isr(preempt1) 占 ~357µs * 覆盖最旧会伤及 CPU 正在读取的输出窗口。buildInput 在 PDI_Isr(preempt1)
* 会压住 CAN ISR(preempt2),发指令时电机 CONFIG 翻倍到 2kHz/节点、总线 * 占 ~357µs 会压住 CAN ISR(preempt2),发指令时电机 CONFIG 翻倍到 2kHz/节点、
* ~8kHz,阻塞窗口内到达 ~3 帧恰好打满 FIFO 深度 → 覆盖进行到一半被读走 * 总线 ~8kHz,阻塞窗口内到达 ~3 帧恰好打满 FIFO 深度 → 覆盖进行到一半被读走
* = 跨节点/跨类型拼接帧(指纹实验实锤:node2 的 CONFIG 条目带 node3 的 * = 跨节点/跨类型拼接帧。DISABLE 后溢出丢新帧并置 RFO0
* 速度限 150 与反相角度;settle 延迟/重取/MI-MP 校验都挡不住,因为
* MI/MP 是覆盖序列里最后更新的一环)。DISABLE = 溢出丢新帧并置 RFO0
* (rx_fifo_overfull_count 已在统计):STATUS/CONFIG 均周期量,丢一拍下拍 * (rx_fifo_overfull_count 已在统计):STATUS/CONFIG 均周期量,丢一拍下拍
* 即补;覆盖递上来的才是网页乱码。与本文件软件队列"满丢新不覆旧"同则。 */ * 即补;覆盖递上来的才是网页乱码。与本文件软件队列"满丢新不覆旧"同则。 */
can_parameter.rec_fifo_overwrite = DISABLE; can_parameter.rec_fifo_overwrite = DISABLE;
@@ -579,7 +574,7 @@ void CanFd::initChannelHardware(uint8_t i)
CAN_TSTAT(ch.canPeriph) = CAN_TSTAT_MTF0 | CAN_TSTAT_MTF1 | CAN_TSTAT_MTF2; CAN_TSTAT(ch.canPeriph) = CAN_TSTAT_MTF0 | CAN_TSTAT_MTF1 | CAN_TSTAT_MTF2;
can_interrupt_enable(ch.canPeriph, CAN_INT_TME | CAN_INT_RFNE0 | CAN_INT_RFF0 | CAN_INT_RFO0); can_interrupt_enable(ch.canPeriph, CAN_INT_TME | CAN_INT_RFNE0 | CAN_INT_RFF0 | CAN_INT_RFO0);
nvic_irq_enable(ch.txIrq, CONFIG_IRQ_PREEMPT_CAN, static_cast<uint8_t>(i)); nvic_irq_enable(ch.txIrq, CONFIG_IRQ_PREEMPT_CAN, static_cast<uint8_t>(i));
/* 1.6.13:RX 提到 preempt=0 —— 见 sys/config.h CONFIG_IRQ_PREEMPT_CAN_RX 注释。 */ /* RX 提到 preempt=0 —— 见 sys/config.h CONFIG_IRQ_PREEMPT_CAN_RX 注释。 */
nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN_RX, static_cast<uint8_t>(i)); nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN_RX, static_cast<uint8_t>(i));
s_rxQueues[i].head = 0U; s_rxQueues[i].head = 0U;
@@ -946,7 +941,7 @@ void CanFd::init()
/** /**
* @brief L0 轻量自检:无总线发帧,仅校验使能通道控制器自身状态(GD32 侧故障)。 * @brief L0 轻量自检:无总线发帧,仅校验使能通道控制器自身状态(GD32 侧故障)。
* @note 1.5.35:不再检查 BUS_OFF/TEC/REC——未接从站的通道 RX 浮空噪声会累积 REC, * @note 不检查 BUS_OFF/TEC/REC——未接从站的通道 RX 浮空噪声会累积 REC,
* 「没接设备」不等于错误;总线侧健康由 0xC1 反馈帧运行期上报。 * 「没接设备」不等于错误;总线侧健康由 0xC1 反馈帧运行期上报。
*/ */
bool CanFd::selfTestL0(uint8_t *fault_code, uint8_t *fault_channel) bool CanFd::selfTestL0(uint8_t *fault_code, uint8_t *fault_channel)
@@ -1136,11 +1131,13 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
tx_msg.tx_ft = CAN_FT_DATA; tx_msg.tx_ft = CAN_FT_DATA;
tx_msg.tx_dlen = dlc; tx_msg.tx_dlen = dlc;
#ifdef GD32E508 #ifdef GD32E508
tx_msg.fd_flag = (dlc > 8U) ? ENABLE : DISABLE; /* 只要通道硬件已启用 CAN-FD(3路均由 initFdMode 开启),统一发 FD 帧;
* 避免 DLC<=8 时回退经典 CAN 导致对端电机 DMA / 报文解析异常。 */
tx_msg.fd_flag = ENABLE;
#if (CONFIG_CAN_FD_BRS_ENABLE != 0) #if (CONFIG_CAN_FD_BRS_ENABLE != 0)
/* 电机 BOOT 收不了 BRS 数据段,OTA 期间 relay 对该通道压制(setChannelBrsSuppress), /* 电机 BOOT 收不了 BRS 数据段,OTA 期间 relay 对该通道压制(setChannelBrsSuppress),
* DLC>8 帧回仲裁段速率(FD-no-BRS,OTA 长期验证过的组合)。 */ * 压制中回退仲裁段速率(FD-no-BRS,OTA 长期验证过的组合)。日常发送统一启用 BRS 高速段。 */
tx_msg.fd_brs = (dlc > 8U && !s_brsSuppress[channelIndex]) ? ENABLE : DISABLE; tx_msg.fd_brs = (!s_brsSuppress[channelIndex]) ? ENABLE : DISABLE;
#else #else
tx_msg.fd_brs = DISABLE; tx_msg.fd_brs = DISABLE;
#endif #endif
@@ -1467,24 +1464,18 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
/* 自己取帧并校验(见 receiveFrameChecked):厂商库不校验也不等 RFD 自清, /* 自己取帧并校验(见 receiveFrameChecked):厂商库不校验也不等 RFD 自清,
* 会把"半条上一帧 + 半条本帧"的拼接体当正常帧交上来。 * 会把"半条上一帧 + 半条本帧"的拼接体当正常帧交上来。
* *
* 1.6.10:**校验失败只丢不重取**。1.6.7 起这里是"原地重取最多 3 次", * **校验失败只丢不重取**。RFIFOMDATA0 是"重复读同一地址、硬件读
* 实测被证伪为撕裂的直接制造者:RFIFOMDATA0 是"重复读同一地址、硬件读 * 指针自动前进",取数失败时读指针已随取数前进若干字,
* 指针自动前进"(厂商驱动证实),attempt1 失败时读指针已随取数前进若干 * 重取会从错位指针开读 —— MI/MP 回读比对的是"窗口身份",
* 字,重取的 attempt2 从错位指针开读 —— MI/MP 回读比对的是"窗口身份",
* 管不住"指针相对窗口起点的错位",于是校验通过、内容却是"上一帧的尾巴 * 管不住"指针相对窗口起点的错位",于是校验通过、内容却是"上一帧的尾巴
* 拼下一帧的开头"。自测实锤:重取版 1.6.9 在电机活跃时错标率 9.155% * 拼下一帧的开头"。
* (56835/620838),而 rx_torn 全程恒 0 —— 拼接恰好都从校验里溜过去。 * 丢帧无害:STATUS/CONFIG 都是 1kHz 周期量,下拍即补;递上去才会变乱码。 */
* 丢帧无害:STATUS/CONFIG 都是 1kHz 周期量,下拍即补;递上去才会变乱码。
* CAN ISR 是 preempt=2,EtherCAT 的 SYNC(0)/ESC(1)/SYNC1(1) 都能抢占它,
* 这正是取数被打断的来源,也解释了为什么只有 EtherCAT 链路上看得到拼接、
* WiFi 链路干净。 */
const bool intact = receiveFrameChecked(ch.canPeriph, &rx_message); const bool intact = receiveFrameChecked(ch.canPeriph, &rx_message);
if (!releaseFifo0(ch.canPeriph)) { if (!releaseFifo0(ch.canPeriph)) {
s_diag.rx_release_timeout_count++; s_diag.rx_release_timeout_count++;
} }
if (!intact) { if (!intact) {
/* 校验判为拼接:整帧作废,不计 rx_count、不入队、不转发。 /* 校验判为拼接:整帧作废,不计 rx_count、不入队、不转发。 */
* 本版起 rx_torn_count 恢复真实语义 —— 它恒 0 说明校验没在挡东西。 */
s_diag.rx_torn_count++; s_diag.rx_torn_count++;
continue; continue;
} }
@@ -1494,28 +1485,25 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
const uint32_t rxId = (rx_message.rx_ff == CAN_FF_STANDARD) const uint32_t rxId = (rx_message.rx_ff == CAN_FF_STANDARD)
? rx_message.rx_sfid : rx_message.rx_efid; ? rx_message.rx_sfid : rx_message.rx_efid;
/* ── 1.6.11:内容级拼接帧拦截 ── /* ── 内容级拼接帧拦截 ──
* MI/MP 回读校验管不住 FIFO 缓冲指针与帧边界的失同步(1.6.10 的 * MI/MP 回读校验管不住 FIFO 缓冲指针与帧边界的失同步
* settle 延迟实测无效,指纹实验证明 node3 的 STATUS 帧头 24B 可以是 * (指针病理,软件无法从取数时序上根除)。
* node2 的完整 CONFIG —— 错位恰为 CONFIG 帧长 6 字,与混合帧长交错 * 在入队前做结构自洽校验:拼接帧的 (id, data) 必矛盾,而真帧必自洽。
* 相关,是 CAN IP 的指针病理,软件无法从取数时序上根除)。
* 改为在入队前做结构自洽校验:拼接帧的 (id, data) 必矛盾,而真帧必自洽。
* STATUS(0x000N0100, dlc48):CONFIG 头灌入必然把 MCU/Motor 温度推出 * STATUS(0x000N0100, dlc48):CONFIG 头灌入必然把 MCU/Motor 温度推出
* [-40,125]℃ 物理量程(CONFIG B0-1 = 03+限压低字节 → 123~250℃ 或 * [-40,125]℃ 物理量程(CONFIG B0-1 = 03+限压低字节 → 123~250℃ 或
* 负值),母线电压同步失序; * 负值),母线电压同步失序;
* CONFIG(0x000N0101, dlc24):DBC v0.74 规定 B15-23 未使用恒 0, * CONFIG(0x000N0101, dlc24):DBC v0.74 规定 B15-23 未使用恒 0,
* STATUS 体灌入几乎必非 0;模式字节限 0..7(电机模式表最大 7)。 * STATUS 体灌入几乎必非 0;模式字节限 0..7(电机模式表最大 7)。
* 命中即计数(1.6.12 起不再丢帧——管道必须诚实,计数仅做"源头是否 * 命中即计数(不丢帧——管道必须诚实,计数仅做"源头是否
* 还在产拼接帧"的测量):源头修复生效后本计数应恒 0,非 0 即失效告警。 * 还在产拼接帧"的测量):源头修复生效后本计数应恒 0,非 0 即失效告警。
* 计数经段5@494 上报。 * 计数经段5@494 上报。
* 残余风险:限压字节恰使温度落回量程内的理论组合(24V 台架实测限压 * 残余风险:限压字节恰使温度落回量程内的理论组合
* ≥15V → 对应"温度"≥153℃,仍在量程外),可接受。 */ * (24V 台架实测限压 ≥15V → 对应"温度"≥153℃,仍在量程外),可接受。 */
{ {
const uint16_t idLo = (uint16_t)(rxId & 0xFFFFU); const uint16_t idLo = (uint16_t)(rxId & 0xFFFFU);
bool reject = false; bool reject = false;
/* 上一帧已接受的温度/电压(按节点):拼接帧的 CONFIG 字节可能恰好落进 /* 上一帧已接受的温度/电压(按节点):拼接帧的 CONFIG 字节可能恰好落进
* 静态量程(实测 vel=150 指纹时 0301→2.59℃、5a00→0.9℃、cace→52.9V * 静态量程,但绝不可能在 1ms 帧间隔里物理地跳过去 —— 斜率判据
* 全部"合法"漏过),但绝不可能在 1ms 帧间隔里物理地跳过去 —— 斜率判据
* 是静态量程补不上的洞。阈值留 3 个数量级余量:温度 20℃/帧(实际 * 是静态量程补不上的洞。阈值留 3 个数量级余量:温度 20℃/帧(实际
* ≤0.005℃),电压 5V/帧(实际 ≤0.05V)。 */ * ≤0.005℃),电压 5V/帧(实际 ≤0.05V)。 */
static int16_t s_lastMcu[16]; static int16_t s_lastMcu[16];
@@ -1540,7 +1528,7 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
if (dmcu < 0) { dmcu = (int16_t)-dmcu; } if (dmcu < 0) { dmcu = (int16_t)-dmcu; }
if (dmot < 0) { dmot = (int16_t)-dmot; } if (dmot < 0) { dmot = (int16_t)-dmot; }
/* 只比温度斜率:拼接帧的 CONFIG 头必然把 mcu/mot 推出 20℃/帧 /* 只比温度斜率:拼接帧的 CONFIG 头必然把 mcu/mot 推出 20℃/帧
* (实测最小跳变 35℃,而真实温度 ≤0.005℃/帧,4 个数量级余量)。 * (实际温度 ≤0.005℃/帧,4 个数量级余量)。
* 电压不比斜率 —— 正弦换向/堵转时母线真实跌落可超 5V/帧, * 电压不比斜率 —— 正弦换向/堵转时母线真实跌落可超 5V/帧,
* 误杀会让显示冻结;拼接的电压特征(≈48V)已由量程+温度斜率兜住。 */ * 误杀会让显示冻结;拼接的电压特征(≈48V)已由量程+温度斜率兜住。 */
if (dmcu > 2000 || dmot > 2000) { if (dmcu > 2000 || dmot > 2000) {
@@ -1564,8 +1552,8 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
} }
} }
if (reject) { if (reject) {
/* 1.6.12:只计不丢 —— 数据管道必须诚实(用户裁定:过滤是遮羞布, /* 只计不丢 —— 数据管道必须诚实。
* 修的是采集源头)。网页/ESP32/主站看到的就是源头产出的原始字节, * 网页/ESP32/主站看到的就是源头产出的原始字节,
* 本计数退化为纯测量仪表:源头修好 = 恒 0;还在涨 = 源头问题仍在, * 本计数退化为纯测量仪表:源头修好 = 恒 0;还在涨 = 源头问题仍在,
* 且网页如实显示着,不存在"看着干净其实坏了"。 */ * 且网页如实显示着,不存在"看着干净其实坏了"。 */
s_diag.rx_content_reject_count++; s_diag.rx_content_reject_count++;
@@ -1737,6 +1725,15 @@ void CanFd::handleTxInterrupt(uint8_t channelIndex)
drainTxQueueIsr(channelIndex); drainTxQueueIsr(channelIndex);
} }
/* ── 空通道自动识别状态(2026-09-18)──
* 无对端的通道 RX 浮空 → 持续 STUFF、REC 冲 ~130 停在 ERR_PASSIVE,每 5s 刷一行
* [CAN-HW-ERR](config.h 老注释"CH1/CH2 未接设备,关闭避免持续 CAN 错误 + 占 SPI
* 带宽")。改为运行时自动判定:连续 CONFIG_CAN_QUIET_WINDOW 个窗口无有效帧增量
* 且有错误 → 静默上报;出现帧增量立刻恢复。只抑制上报,硬件保持 NORMAL 接收。 */
static uint8_t s_chanQuiet[3] = {0U, 0U, 0U}; /* 1=该通道已自动静默上报 */
static uint32_t s_rxCountPrev[3] = {0U, 0U, 0U}; /* 上一拍末尾的 rx_count */
static uint8_t s_quietMiss[3] = {0U, 0U, 0U}; /* 连续"无帧且有错"的窗口数 */
/** /**
* @brief 刷新三路 CAN 通道的 1Hz 反馈快照。 * @brief 刷新三路 CAN 通道的 1Hz 反馈快照。
* @note 在临界区内拷贝实时计数与最近一帧信息到 s_feedback;保证 SPI 命令读到一致快照。 * @note 在临界区内拷贝实时计数与最近一帧信息到 s_feedback;保证 SPI 命令读到一致快照。
@@ -1766,6 +1763,37 @@ void CanFd::refreshFeedbackSnapshot()
} }
s_feedbackSeq++; s_feedbackSeq++;
/* 空通道自动识别:本拍无有效帧增量且有错误 → 记一次 miss,连续达到窗口长度才静默
* (加滞回,避免偶发丢拍/慢流量误判);一旦有帧增量立刻恢复上报。
* 时间基 = s_feedbackSeq(本函数 1Hz,见下面 %5 的 5s 打印节拍)。 */
for (size_t i = 0; i < kChannelCount; ++i) {
const uint32_t rxNow = s_channelStats[i].rx_count;
const uint32_t rxDelta = rxNow - s_rxCountPrev[i];
s_rxCountPrev[i] = rxNow;
const bool hasErr = (hw_tec[i] != 0U || hw_rec[i] != 0U
|| (hw_err[i] & (CAN_ERR_WERR | CAN_ERR_PERR | CAN_ERR_BOERR)) != 0U);
if (rxDelta > 0U) {
s_quietMiss[i] = 0U;
if (s_chanQuiet[i] != 0U) {
s_chanQuiet[i] = 0U;
CAN_LOGI("[CAN] CH%u 恢复上报:检测到有效帧 (rx=%lu)\r\n",
static_cast<unsigned>(i), static_cast<unsigned long>(rxNow));
}
} else if (hasErr) {
if (s_quietMiss[i] < 0xFFU) s_quietMiss[i]++;
if (s_chanQuiet[i] == 0U
&& s_feedbackSeq >= CONFIG_CAN_QUIET_AFTER_SEC
&& s_quietMiss[i] >= CONFIG_CAN_QUIET_WINDOW_SEC) {
s_chanQuiet[i] = 1U;
LOG_ERR("[CAN] CH%u 无有效帧且错误累积 → 自动静默上报(本通道无设备/无对端;"
"硬件保持接收,插上设备有帧即自动恢复;TEC=%lu REC=%lu)\r\n",
static_cast<unsigned>(i),
static_cast<unsigned long>(hw_tec[i]), static_cast<unsigned long>(hw_rec[i]));
}
} else {
s_quietMiss[i] = 0U;
}
}
/* 每 5 秒采样硬件错误态;正常心跳不打印(统计走 SPI 0xC1)。 /* 每 5 秒采样硬件错误态;正常心跳不打印(统计走 SPI 0xC1)。
* 仅当 TEC/REC 或告警位非零时输出 [CAN-HW-ERR];全量 seq 心跳见 CONFIG_UART_DIAG_LEVEL。 */ * 仅当 TEC/REC 或告警位非零时输出 [CAN-HW-ERR];全量 seq 心跳见 CONFIG_UART_DIAG_LEVEL。 */
if (s_feedbackSeq % 5U == 0U) { if (s_feedbackSeq % 5U == 0U) {
@@ -1781,6 +1809,7 @@ void CanFd::refreshFeedbackSnapshot()
&& (hw_err[i] & (CAN_ERR_WERR | CAN_ERR_PERR | CAN_ERR_BOERR)) == 0U) { && (hw_err[i] & (CAN_ERR_WERR | CAN_ERR_PERR | CAN_ERR_BOERR)) == 0U) {
continue; continue;
} }
if (s_chanQuiet[i] != 0U) continue; /* 已自动静默的空通道:不再刷错误 */
LOG_ERR("[CAN-HW-ERR] CH%u ERR=0x%08lX ERRN=%lu(%s) TEC=%lu REC=%lu TSTAT=0x%08lX\r\n", LOG_ERR("[CAN-HW-ERR] CH%u ERR=0x%08lX ERRN=%lu(%s) TEC=%lu REC=%lu TSTAT=0x%08lX\r\n",
static_cast<unsigned>(i), static_cast<unsigned>(i),
static_cast<unsigned long>(hw_err[i]), static_cast<unsigned long>(hw_err[i]),
@@ -1814,6 +1843,10 @@ void CanFd::refreshFeedbackSnapshot()
for (size_t i = 0; i < kChannelCount; ++i) { for (size_t i = 0; i < kChannelCount; ++i) {
uint8_t cur = s_feedback[i].stats.bus_state; uint8_t cur = s_feedback[i].stats.bus_state;
if (cur != s_prevBusState[i]) { if (cur != s_prevBusState[i]) {
if (s_chanQuiet[i] != 0U) { /* 静默通道的状态变化只记账不打印 */
s_prevBusState[i] = cur;
continue;
}
const char *stateStr = "NORMAL"; const char *stateStr = "NORMAL";
if (cur & (1U << 0)) stateStr = "INIT_MODE"; if (cur & (1U << 0)) stateStr = "INIT_MODE";
else if (cur & (1U << 3)) stateStr = "BUS_OFF"; else if (cur & (1U << 3)) stateStr = "BUS_OFF";
+4 -4
View File
@@ -167,7 +167,7 @@ public:
/** /**
* @brief 直接向指定 CAN 通道发送一帧(绕过软件队列)。 * @brief 直接向指定 CAN 通道发送一帧(绕过软件队列)。
* @param allow_abort 邮箱全占时是否允许 abort 一个 PENDING 邮箱并重投一次。 * @param allow_abort 邮箱全占时是否允许 abort 一个 PENDING 邮箱并重投一次。
* - true(0xC0 单帧路径默认):非阻塞顶替,适合"最新命令优先"的单帧发送; * - true(底层非阻塞顶替):适合最新单发覆盖场景;高总线负载下慎用(可能 abort 掉正在等仲裁的帧);
* - false(突发/队列路径):邮箱满直接返回 kErrMailbox,交由 enqueueTx * - false(突发/队列路径):邮箱满直接返回 kErrMailbox,交由 enqueueTx
* 的软件队列缓存,由 TX 完成中断回灌——避免 abort 把同通道已装载的 * 的软件队列缓存,由 TX 完成中断回灌——避免 abort 把同通道已装载的
* 旧帧静默丢弃(>3 帧同通道突发只剩最后 3 帧上线的根因)。 * 旧帧静默丢弃(>3 帧同通道突发只剩最后 3 帧上线的根因)。
@@ -182,7 +182,7 @@ public:
/** @brief 运行时切换指定通道 CAN 自动重发(ARD 位,需 init-mode);停等式 OTA 期间关闭、结束后恢复。 */ /** @brief 运行时切换指定通道 CAN 自动重发(ARD 位,需 init-mode);停等式 OTA 期间关闭、结束后恢复。 */
static void setChannelAutoRetrans(uint8_t channelIndex, bool enable); static void setChannelAutoRetrans(uint8_t channelIndex, bool enable);
/** @brief 运行时压制指定通道 TX 帧的 BRS:压制中 DLC>8 的 FD 帧数据段回退仲裁段速率。 /** @brief 运行时压制指定通道 TX 帧的 BRS:压制中 FD 帧数据段回退仲裁段速率。
* 电机 BOOT 无法正确接收 BRS 高速段(见 sys/config.h CONFIG_CAN_FD_BRS_ENABLE 注释), * 电机 BOOT 无法正确接收 BRS 高速段(见 sys/config.h CONFIG_CAN_FD_BRS_ENABLE 注释),
* 电机 OTA relay 经 enterState 钩子与 setChannelAutoRetrans 同点切换,OTA 结束必恢复。 */ * 电机 OTA relay 经 enterState 钩子与 setChannelAutoRetrans 同点切换,OTA 结束必恢复。 */
static void setChannelBrsSuppress(uint8_t channelIndex, bool suppress); static void setChannelBrsSuppress(uint8_t channelIndex, bool suppress);
@@ -246,14 +246,14 @@ public:
/** @brief 软件 TX 队列当前积压帧数(统计/诊断用)。 */ /** @brief 软件 TX 队列当前积压帧数(统计/诊断用)。 */
static uint32_t txQueueBacklog(uint8_t channelIndex); static uint32_t txQueueBacklog(uint8_t channelIndex);
/** @brief 接收队列容量深度(固定为 kRxQueueDepth = 16)。 */ /** @brief 接收队列容量深度(2 的幂;8kHz PDO 下主循环间隙短,加深缓冲防 STATUS 溢出丢帧)。 */
static constexpr uint8_t rxQueueDepth() { return kRxQueueDepth; } static constexpr uint8_t rxQueueDepth() { return kRxQueueDepth; }
/** @brief 查询指定通道软件接收队列当前积压帧数(无锁 SPSC,由 tail 与 head 计算)。 */ /** @brief 查询指定通道软件接收队列当前积压帧数(无锁 SPSC,由 tail 与 head 计算)。 */
static uint32_t rxQueueCount(uint8_t channelIndex); static uint32_t rxQueueCount(uint8_t channelIndex);
private: private:
static constexpr uint8_t kRxQueueDepth = 16U; static constexpr uint8_t kRxQueueDepth = 64U; /* 16→64:1kHz STATUS×3 在 PDI 风暴下不再轻易打满 */
static constexpr uint8_t kRxQueueMask = kRxQueueDepth - 1U; static constexpr uint8_t kRxQueueMask = kRxQueueDepth - 1U;
/** 每通道软件突发队列深度:覆盖整块突发(CAN_BURST_HARD_MAX=16 帧)中同一通道的 /** 每通道软件突发队列深度:覆盖整块突发(CAN_BURST_HARD_MAX=16 帧)中同一通道的
* 全部超载帧(3 邮箱之外最多 13 帧 = 16-3),保证任意 DLC 的整块突发不因队列满丢帧。 */ * 全部超载帧(3 邮箱之外最多 13 帧 = 16-3),保证任意 DLC 的整块突发不因队列满丢帧。 */
@@ -76,7 +76,11 @@ public:
return; return;
} }
const CanFd::CanTxResult result = CanFd::transmitFrame(can_ch, can_id, can_data, can_dlc); /* 0xC0 单帧发送改走 enqueueTx:
* 1) allow_abort=false 杜绝硬件邮箱被后续发送主动 abort 抹杀;
* 2) 电机高频上报抢占仲裁导致邮箱全占时,帧自动进入 13 深度软件队列,
* 由 TX 中断 drainTxQueueIsr 持续回灌空闲邮箱发送,彻底消除丢帧。 */
const CanFd::CanTxResult result = CanFd::enqueueTx(can_ch, can_id, can_data, can_dlc);
if (result == CanFd::CanTxResult::kOk) { if (result == CanFd::CanTxResult::kOk) {
postCanSendLog(bus, "OK", can_ch, can_id, can_data, can_dlc); postCanSendLog(bus, "OK", can_ch, can_id, can_data, can_dlc);
const uint8_t ack = 0x00U; const uint8_t ack = 0x00U;
+13 -16
View File
@@ -370,17 +370,15 @@ void AppSpiCommand::configureExti()
{ {
gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15); gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15);
exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH); exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH);
/* 1.6.35:先清 STAT 再开 IMR。原 enable→clear 顺序在 warm jump 下有害—— /* 先清 STAT 再开 IMR。warm jump 下 BOOT 关中断跳转前 ESP32 持续拉 CS,
* BOOT 关中断跳转(v1.1.0.15)前 ESP32 持续拉 CS,EXTI15 STAT 大概率残留; * EXTI15 STAT 大概率残留;若先 enable IMR,残留位会立即回放成一次假 CS
* 本函数在 AppSpiCommandBus.init() 里跑,此刻 PRIMASK 已被 init 开头清零, * 沿进 NVIC。 */
* 若先 enable IMR,那个残留位会立即回放成一次假 CS 沿进 NVIC。 */
exti_interrupt_flag_clear(EXTI_15); exti_interrupt_flag_clear(EXTI_15);
exti_interrupt_enable(EXTI_15); exti_interrupt_enable(EXTI_15);
/* 1.6.32:SPI CS EXTI 抢占级 1→0。原 1 与 PDI/SYNC1/SCHED 同级不抢占 /* SPI CS EXTI 抢占级 0。抢占级 1 与 PDI/SYNC1/SCHED 同级不抢占
* (1.6.13 实测 PDI 窗口 ~357µs),且被 RT_CRITICAL(BASEPRI≥1) 完全屏蔽; * (PDI 窗口可达数百 µs),且被 RT_CRITICAL(BASEPRI≥1) 完全屏蔽;
* release ISR 一旦拖过 ESP32 的 CS 间隔(0.4.4 起 1ms),re-arm 落空, * release ISR 一旦拖过 ESP32 的 CS 间隔(1ms),re-arm 落空,
* 下一笔查询帧头部丢失→GD32 解析失败回 0x82 0xE8→迟到被 serviceRing * 下一笔查询帧头部丢失。提到 0 后仅与 SYNC0/CAN-RX
* 偷读("偷读迟到 RESPONSE 0xEE/E8"风暴根因)。提到 0 后仅与 SYNC0/CAN-RX
* (µs 级)同级排队,release 处理确定性 µs 级完成。代价:0xA2 pmDrive 的 * (µs 级)同级排队,release 处理确定性 µs 级完成。代价:0xA2 pmDrive 的
* 忙等(≤10ms,手动 PM 诊断才用)会阻塞 SYNC0——诊断窗口,可接受。 */ * 忙等(≤10ms,手动 PM 诊断才用)会阻塞 SYNC0——诊断窗口,可接受。 */
nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U); nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U);
@@ -534,7 +532,7 @@ void AppSpiCommand::processCommand()
if (!spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr)) { if (!spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr)) {
postLogFmt("[SPI-APP] 非v2帧 0x%02X -> ERROR", m_rxBuf[0]); postLogFmt("[SPI-APP] 非v2帧 0x%02X -> ERROR", m_rxBuf[0]);
/* 1.6.30: 0xE8 payload 携带接收诊断——[0]=0xE8, [1]=rxLen, [2..5]=rxBuf 前 4 字节。 /* 0xE8 payload 携带接收诊断——[0]=0xE8, [1]=rxLen, [2..5]=rxBuf 前 4 字节。
* ESP32 /system/status rsp_err_last_p0..p3 直读,无需串口即可判定损坏形态 * ESP32 /system/status rsp_err_last_p0..p3 直读,无需串口即可判定损坏形态
* (全FF=DMA 未装/CS 丢失;SOF 错位=相位漂移;SOF 对但 CRC 败=帧内位错)。 */ * (全FF=DMA 未装/CS 丢失;SOF 错位=相位漂移;SOF 对但 CRC 败=帧内位错)。 */
uint16_t tx_len = 0U; uint16_t tx_len = 0U;
@@ -554,8 +552,8 @@ void AppSpiCommand::processCommand()
return; return;
} }
/* 1.6.29:逐帧"收到非刷写"行已移除(UART 刷屏);CONFIG_SPI_APP_LOG_ENABLE /* 正常帧不打日志(避免 UART 刷屏);CONFIG_SPI_APP_LOG_ENABLE
* 现在只覆盖下方异常路径(非v2帧/未知type/SEQ跳变/session不匹配),正常态静默。 */ * 只覆盖异常路径(非v2帧/未知type/SEQ跳变/session不匹配),正常态静默。 */
g_pendingAckSeq = hdr.tx_seq; g_pendingAckSeq = hdr.tx_seq;
@@ -650,15 +648,14 @@ void AppSpiCommand::processCommand()
if (!handled) { if (!handled) {
postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type); postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type);
uint8_t err = 0xE9U; /* 1.6.29: E9=无 handler 认领 */ uint8_t err = 0xE9U; /* E9=无 handler 认领 */
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
} }
if (tx_len == 0U) { if (tx_len == 0U) {
uint8_t err = 0xEAU; /* 1.6.29: EA=handler 命中但未产出响应 */ uint8_t err = 0xEAU; /* EA=handler 命中但未产出响应 */
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
/* 1.6.29:这条兜底原本完全静默——0x82 0xEE 的三大来源里唯一不可见的。 /* handler 命中但没产出响应(典型=buildResponse 构帧失败/业务拒绝)走此兜底错误路径。 */
* handler 命中但没产出响应(典型=buildResponse 构帧失败/业务拒绝)。 */
postLogFmt("[SPI-APP] handler t=0x%02X tx_len=0 -> ERROR", frame_type); postLogFmt("[SPI-APP] handler t=0x%02X tx_len=0 -> ERROR", frame_type);
} }
+2 -3
View File
@@ -15,8 +15,7 @@ uint8_t Watchdog::init()
{ {
/* FWDGT 的 PSC/RLD 写入经 LSI(IRC40K) 时钟域同步(PUD/RUD 由硬件清零)。 /* FWDGT 的 PSC/RLD 写入经 LSI(IRC40K) 时钟域同步(PUD/RUD 由硬件清零)。
* BOOT 只检测 FWDGT 复位标志、从不启动看门狗,冷启动时 LSI 未开—— * BOOT 只检测 FWDGT 复位标志、从不启动看门狗,冷启动时 LSI 未开——
* 直接配置会让 PUD/RUD 永远等不到清零(库超时返回 ERROR,1.5.34 起暴露为 init 失败; * 直接配置会让 PUD/RUD 永远等不到清零(库超时返回 ERROR)。
* 此前静默:FWDGT 实际按复位默认值 DIV4/0xFFF 运行,超时就与配置不符)。
* 必须先手动开 LSI 并等就绪,再配置。 */ * 必须先手动开 LSI 并等就绪,再配置。 */
rcu_osci_on(RCU_IRC40K); rcu_osci_on(RCU_IRC40K);
if (rcu_osci_stab_wait(RCU_IRC40K) != SUCCESS) { if (rcu_osci_stab_wait(RCU_IRC40K) != SUCCESS) {
@@ -24,7 +23,7 @@ uint8_t Watchdog::init()
} }
/* PSC/RLD 配置函数自带 ErrStatus(等待 PUD/RUD 清零带超时); /* PSC/RLD 配置函数自带 ErrStatus(等待 PUD/RUD 清零带超时);
* 此前返回值被忽略,写入失败会无声变成"看门狗实际未按预期运行"。 */ * 必须检查返回值,否则写入失败会让看门狗实际未按预期运行。 */
fwdgt_write_enable(); fwdgt_write_enable();
uint8_t fault = kFaultOk; uint8_t fault = kFaultOk;
if (fwdgt_prescaler_value_config((uint16_t)CONFIG_FWDGT_PSC) != SUCCESS) { if (fwdgt_prescaler_value_config((uint16_t)CONFIG_FWDGT_PSC) != SUCCESS) {
+59
View File
@@ -0,0 +1,59 @@
/**
* @file ota_layout.h
* @brief APP/BOOT 共用 OTA Flash 布局(唯一真源)。
* @note 两侧 sys/config.h 都必须 include 本头,禁止再抄地址常量。
* 相对路径:BOOT/APP 的 sys/config.h → ../../../shared/ota_layout.h
*
* 约定:0x00000000=AVAL(有效),0xFFFFFFFF=NOT_AVAL(擦除态/无效)。
*/
#ifndef SHARED_OTA_LAYOUT_H
#define SHARED_OTA_LAYOUT_H
#include <stdint.h>
/* ---- 整片 Flash 区段(GD32E508VE 512KB) ---- */
#define OTA_LAYOUT_BOOT_START 0x08000000U
#define OTA_LAYOUT_BOOT_SIZE 0x00012000U /* 72KB */
#define OTA_LAYOUT_APP_BACK_START 0x08012000U /* 216KB 备份区 */
#define OTA_LAYOUT_APP_MAIN_START 0x08048000U /* 216KB 主应用区 = APP VTOR */
#define OTA_LAYOUT_APP_MAX_SIZE 0x00035FFCU /* 216KB-4B,末尾 CRC32 */
#define OTA_LAYOUT_CONFIG_PAGE 0x0807E000U /* OTA 状态配置页 2KB */
#define OTA_LAYOUT_SLEEP_PAGE 0x0807E800U /* 睡眠配置页 2KB */
#define OTA_LAYOUT_PAGE_SIZE 0x00000800U /* FMC 单页 2KB */
/* ---- OTA 配置页 word 布局(word N = PAGE + 4*N) ---- */
#define OTA_LAYOUT_WORD_LATEST_AVAL 0U
#define OTA_LAYOUT_WORD_BACKUP_AVAL 1U
#define OTA_LAYOUT_WORD_LATEST_EN 2U
#define OTA_LAYOUT_WORD_BACKUP_EN 3U
#define OTA_LAYOUT_WORD_LATEST_CHECK 4U
#define OTA_LAYOUT_WORD_BACKUP_CHECK 5U
#define OTA_LAYOUT_WORD_BOOT_VER0 6U /* major|minor */
#define OTA_LAYOUT_WORD_BOOT_VER1 7U /* patch|build */
#define OTA_LAYOUT_WORD_ECAT_ARM 8U /* 0xACA70001 才允许 ECAT SDO 刷 APP */
#define OTA_LAYOUT_WORD_CRASH_COUNT 9U /* 连续 FWDGT 崩溃次数;APP 起板成功清 0 */
#define OTA_LAYOUT_ADDR_LATEST_AVAL (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_LATEST_AVAL)
#define OTA_LAYOUT_ADDR_BACKUP_AVAL (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BACKUP_AVAL)
#define OTA_LAYOUT_ADDR_LATEST_EN (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_LATEST_EN)
#define OTA_LAYOUT_ADDR_BACKUP_EN (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BACKUP_EN)
#define OTA_LAYOUT_ADDR_LATEST_CHECK (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_LATEST_CHECK)
#define OTA_LAYOUT_ADDR_BACKUP_CHECK (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BACKUP_CHECK)
#define OTA_LAYOUT_ADDR_BOOT_VER0 (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BOOT_VER0)
#define OTA_LAYOUT_ADDR_BOOT_VER1 (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_BOOT_VER1)
#define OTA_LAYOUT_ADDR_ECAT_ARM (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_ECAT_ARM)
#define OTA_LAYOUT_ADDR_CRASH_COUNT (OTA_LAYOUT_CONFIG_PAGE + 4U * OTA_LAYOUT_WORD_CRASH_COUNT)
/** 配置页有效 word 数(擦页回写必须覆盖到此);页 2KB,余量充足。 */
#define OTA_LAYOUT_WORD_COUNT 10U
/** ECAT 下载 arm 令牌。避开擦除态与 AVAL。 */
#define OTA_LAYOUT_ECAT_ARM_KEY 0xACA70001U
/** 连续 FWDGT 达到该次数后 BOOT 停止自动跳 APP,只接受外部灌写。 */
#define OTA_LAYOUT_CRASH_STAY_MAX 3U
#define OTA_LAYOUT_AVAL 0x00000000U
#define OTA_LAYOUT_NOT_AVAL 0xFFFFFFFFU
#endif /* SHARED_OTA_LAYOUT_H */
+34 -69
View File
@@ -9,6 +9,8 @@
#include <stdint.h> #include <stdint.h>
#include <stddef.h> #include <stddef.h>
#include "gd32e50x.h" #include "gd32e50x.h"
/* APP/BOOT 共用 OTA Flash 布局(唯一真源);相对路径见该头注释。 */
#include "../shared/ota_layout.h"
/* ============================================================ /* ============================================================
* 功能开关(调试/特性总闸 — 改这里即可开关,改完重编) * 功能开关(调试/特性总闸 — 改这里即可开关,改完重编)
@@ -40,22 +42,21 @@
* @note 0=不识别版本帧、不推送。与 ESP32 端 motor_version 缓存配对。 * @note 0=不识别版本帧、不推送。与 ESP32 端 motor_version 缓存配对。
* 版本为按需查询的低频数据,独立于 0xC5 周期推送。 * 版本为按需查询的低频数据,独立于 0xC5 周期推送。
*/ */
#define CONFIG_MOTOR_VERSION_ENABLE 1 #define CONFIG_MOTOR_VERSION_ENABLE 1
/** @brief 最大电机节点数 (node ID 1-9)。 */ /** @brief 最大电机节点数 (node ID 1-9)。 */
constexpr uint8_t CONFIG_MOTOR_MAX_NODES = 9U; constexpr uint8_t CONFIG_MOTOR_MAX_NODES = 9U;
/** @brief 电机离线判定超时 (ms)。超过此时间未收到 STATUS 帧则标记 offline。 */ /** @brief 电机离线判定超时 (ms)。超过此时间未收到 STATUS 帧则标记 offline。 */
constexpr uint16_t CONFIG_MOTOR_OFFLINE_MS = 3000U; constexpr uint16_t CONFIG_MOTOR_OFFLINE_MS = 3000U;
/** @brief SPI 命令逐帧日志(#if)。1.6.29 诊断:开启后仅异常路径会输出 /** @brief SPI 命令逐帧日志(#if)。开启后仅异常路径输出
* (非v2帧/未知type/SEQ跳变/session不匹配),逐帧"收到非刷写"行已在 * (非v2帧/未知type/SEQ跳变/session不匹配),正常状态 UART 静默。 */
* app_spi_command.cpp 关闭,正常状态 UART 静默。定位完改回 0。 */
#define CONFIG_SPI_APP_LOG_ENABLE 1 #define CONFIG_SPI_APP_LOG_ENABLE 1
/** @brief CAN_SEND 成功帧详细日志(#if)。 */ /** @brief CAN_SEND 成功帧详细日志(#if)。 */
#define CONFIG_SPI_CAN_SEND_VERBOSE_LOG 0 #define CONFIG_SPI_CAN_SEND_VERBOSE_LOG 0
/** @brief CAN-FD BRS 使能(canfd.cpp #if)。 /** @brief CAN-FD BRS 使能(canfd.cpp #if)。
* 关闭 BRS: 数据段用仲裁段同速率,牢牢性更高。 * 关闭 BRS: 数据段用仲裁段同速率,牢牢性更高。
* 电机 BOOT 无法正确接收 BRS 高速段→nonce(16B FD+BRS)被拒绝→握手失败。 * 电机 BOOT 无法正确接收 BRS 高速段→nonce(16B FD+BRS)被拒绝→握手失败。
* 1.6.1 起:保持 1U 也可——motor_ota_relay 在 OTA 期间经 CanFd::setChannelBrsSuppress * motor_ota_relay 在 OTA 期间经 CanFd::setChannelBrsSuppress 按通道压制 BRS,
* 按通道压制 BRS(与 ARD 切换同一 enterState 钩子,结束必恢复),日常吞吐不受影响。 */ * 结束必恢复,日常吞吐不受影响。 */
#define CONFIG_CAN_FD_BRS_ENABLE 1U #define CONFIG_CAN_FD_BRS_ENABLE 1U
/** @brief 启动时总线探测帧;正常固件关。 */ /** @brief 启动时总线探测帧;正常固件关。 */
#define CONFIG_CAN_STARTUP_BUS_TEST_ENABLE 0 #define CONFIG_CAN_STARTUP_BUS_TEST_ENABLE 0
@@ -70,10 +71,18 @@ constexpr uint16_t CONFIG_MOTOR_OFFLINE_MS = 3000U;
* 电机拓扑下无对端应答,默认关以消除持续错误帧。需要回放测试波形时打开。 * 电机拓扑下无对端应答,默认关以消除持续错误帧。需要回放测试波形时打开。
*/ */
#define CONFIG_CAN_PERIODIC_TEST_TX_ENABLE 0 #define CONFIG_CAN_PERIODIC_TEST_TX_ENABLE 0
/** @brief CAN0/1/2 通道使能。CH1/CH2 未接设备,关闭避免持续 CAN 错误 + 占 SPI 带宽。 */ /** @brief CAN0/1/2 通道使能(**强制开关,兜底用**)。
* 日常不再靠它区分"有没有接设备":未接设备的通道 RX 浮空会持续 STUFF、REC 冲 ~130,
* 现由 canfd.cpp 的"空通道自动识别"在运行时判定并静默上报(只抑制打印,硬件保持
* NORMAL 接收,插上设备有帧即自动恢复)。三路默认全开,交给自动逻辑。 */
constexpr bool CONFIG_CAN_CH0_ENABLE = true; constexpr bool CONFIG_CAN_CH0_ENABLE = true;
constexpr bool CONFIG_CAN_CH1_ENABLE = true; constexpr bool CONFIG_CAN_CH1_ENABLE = true;
constexpr bool CONFIG_CAN_CH2_ENABLE = true; constexpr bool CONFIG_CAN_CH2_ENABLE = true;
/** @brief 空通道自动静默的观察窗口:连续多少个 1Hz 窗口"无有效帧且有错误"才判定为空通道。
* 加滞回,避免偶发丢拍/慢流量(<1Hz)被误判。 */
constexpr uint32_t CONFIG_CAN_QUIET_WINDOW_SEC = 5U;
/** @brief 启动后多久开始允许自动静默(避开电机上电/自检期的正常错误)。 */
constexpr uint32_t CONFIG_CAN_QUIET_AFTER_SEC = 10U;
/** @brief 主循环 WFI(main.cpp #if)。 */ /** @brief 主循环 WFI(main.cpp #if)。 */
#define CONFIG_MAIN_LOOP_WFI_ENABLE 0 #define CONFIG_MAIN_LOOP_WFI_ENABLE 0
/** @brief EtherCAT SYNC0 同步模式(#if)。 */ /** @brief EtherCAT SYNC0 同步模式(#if)。 */
@@ -96,22 +105,20 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
* 不能与历史写侧基准对拍——要看差分潜力就只看 sub13..22,要看真实写侧耗时请置 0 重测。 */ * 不能与历史写侧基准对拍——要看差分潜力就只看 sub13..22,要看真实写侧耗时请置 0 重测。 */
#define CONFIG_ECAT_DIFF_STATS 0 #define CONFIG_ECAT_DIFF_STATS 0
/** /**
* @brief 输入 PDO 稀疏写 ESC(1.6.22 P1):按密排 count 只写「段1+段2有效区」与「段3+段5」, * @brief 输入 PDO 稀疏写 ESC:按密排 count 只写「段1+段2有效区」与「段3+段5」,
* 跳过未用到的空槽区(40+count*56 .. 543),省 HW_EscWriteIsr 的 PRAM 字节数。 * 跳过未用到的空槽区(40+count*56 .. 543),省 HW_EscWriteIsr 的 PRAM 字节数。
* @note 0=恒 600B 整包写(A/B 基线);1=分段写。非 1.6.21 密排布局(magic≠0xA5)或 * @note 0=恒 600B 整包写;1=分段写。非密排布局(magic≠0xA5)或 count 撑满时
* count 撑满时自动回退整包。**总长/SII/主站解码均不变**;空洞区在 ESC 中为该 * 自动回退整包。**总长/SII/主站解码均不变**;空洞区在 ESC 中为该 SM 缓冲
* SM 缓冲上次残留,主站必须继续只按 count 迭代(1.6.21 已如此)。 * 上次残留,主站必须继续只按 count 迭代。
* 验收看 0x2000 sub10(EscWrite 周期)与 load_perm,同工况开关各测一轮。
*/ */
#define CONFIG_ECAT_SPARSE_ESC_WRITE 1 #define CONFIG_ECAT_SPARSE_ESC_WRITE 1
/** /**
* @brief 输入 PDO 差分写 ESC(1.6.25 P1):在稀疏写之上,按「段头(0..543)+段尾(544..599)」 * @brief 输入 PDO 差分写 ESC:在稀疏写之上,按「段头(0..543)+段尾(544..599)」
* 与上次写入影子比对;内容稳定且冲刷完成则整段跳过,省 PRAM 字节。 * 与上次写入影子比对;内容稳定且冲刷完成则整段跳过,省 PRAM 字节。
* @note 3-buffer 安全:SM3 三缓冲轮转,不能只对「上一拍发布镜像」比——目标缓冲 * @note 3-buffer 安全:SM3 三缓冲轮转,不能只对「上一拍发布镜像」比——目标缓冲
* 里是 2 拍前的内容。采用「变化后连续写 3 拍冲刷」协议:变化当拍写并置 * 里是 2 拍前的内容。采用「变化后连续写 3 拍冲刷」协议:变化当拍写并置
* flush=2,随后 2 拍强制写完 3 个缓冲,稳定后才允许跳过。全段跳过时强制 * flush=2,随后 2 拍强制写完 3 个缓冲,稳定后才允许跳过。全段跳过时强制
* 写末 2 字节完成 SM 事件应答。0=关闭,退回稀疏写/整包。SII/主站解码不变。 * 写末 2 字节完成 SM 事件应答。0=关闭,退回稀疏写/整包。SII/主站解码不变。
* 验收:CONFIG_ECAT_DIFF_STATS 取数后对拍 sub10;或与 1.6.22 基线 A/B。
*/ */
#define CONFIG_ECAT_DIFF_ESC_WRITE 1 #define CONFIG_ECAT_DIFF_ESC_WRITE 1
/** @brief 启动横幅与 main 进度打印。 */ /** @brief 启动横幅与 main 进度打印。 */
@@ -149,46 +156,7 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
* ============================================================ */ * ============================================================ */
constexpr uint32_t APP_FW_VERSION_MAJOR = 1U; constexpr uint32_t APP_FW_VERSION_MAJOR = 1U;
constexpr uint32_t APP_FW_VERSION_MINOR = 6U; constexpr uint32_t APP_FW_VERSION_MINOR = 6U;
/* 1.6.34:EtherCAT 侧补「APP→BOOT 交接」+ 版本自证。 constexpr uint32_t APP_FW_VERSION_PATCH = 51U; /* 1.6.50: CAN 空通道自动识别与降噪 */
* (1) 新增 CoE 0x2502「GD32 boot request」(VAR UINT32, ACCESS_READWRITE,
* comm/ssc/port/dc_app.c):主站写 1 就请求本 APP 回引导程序。动作复用量产
* SPI 0xB0 那条异步链(回调只置 pending,配置页 CHECK/AVAL/EN 三字的 Flash 写
* + NVIC_SystemReset 在主循环 OtaBootRequest::process() 下一圈做),所以 SDO
* 响应先发、复位后到,不会把 SM1 半包留在总线上。APP 写的 EN=NOT_AVAL 正好命中
* BOOT 救砖门 ecatArmed()=!(CHECK==AVAL&&EN==AVAL) ⇒ 直灌自动解锁,不需 word8 令牌。
* 动因:0x2500/0x2501 只在 BOOT,实测对跑着的 APP 写 0x2500 回
* abort=0x06020000「对象不存在」,跑着的机器必须先交接才能走总线。
* (2) CoE 0x100A 从 SSC 占位 "1.0" 改成真版本串(DEVICE_SW_VERSION_LEN 0x3→0x10、
* acSoftwareversion 显式 LEN+1 防越界读、initial.cpp 起板时写),主站刷完可以在
* 总线上自证"新 APP 起来了、是哪一版",不必再靠串口或 SPI PING 身份块。 */
/* 1.6.40:HBI 起板探针放行判据从「HBI_IDX_0==0x1234」换成「BYTE_TEST 往返」,
* 根治 warm jump 后自检「降级 ethercat / EXMC 总线未就绪」+ 0xA2 全回 0xE1。
* 实测链(2026-09-16):BOOT 1.1.0.18 短路跳转前 RST# 后,刷完 warm jump 必现
* fault_codes[10]=1、module_mask 缺 bit10/11;reset(芯片不掉电)却正常。
* 根因不在 BOOT 发没发复位:cold 路径芯片自带 POR、IDX_0 天然 0x1234,与
* PB10 那发有没有落地无关;warm 路径 BOOT 的 bootEcatDeinit 最后一条 HBI 写
* 把索引寄存器留在 0x0054(IRQ_CFG),「读复位默认值」的探针对 warm 必然假阴性。
* 本版:BYTE_TEST(索引写+数据读完整往返)为唯一放行门;任一判据不过补一发
* RST# 再探一轮(SSC 未起,零代价);两轮仍不通=总线物理不通,维持降级;
* BYTE_TEST 通但 IDX_0 异常=放行+诊断。s_busReady 语义同步改写(0xE1 门不变)。 */
constexpr uint32_t APP_FW_VERSION_PATCH = 40U; /* 1.6.33: OP 输入新鲜度不变量(EcatDbg_OpInputFreshness)——SM-Synchron 下输入 PDO 只靠 PDI_Isr,通路一停主站读到冻缓冲(WKC/AL 仍正常=假连接);现 OP 内输入停更>5ms 由主循环强制 PDO_InputMapping+重开门。根治"连接看起来正常但 CPU/CAN 不更新、复位 GD32 才好"。 */ /* 1.6.32: SPI CS EXTI 提到抢占级0(见 app_spi_command configureExti),根治 CS release 被 PDI/RT_CRITICAL 延迟导致的 RX 撕裂(0xE8) *//* /* 1.6.28:ESC 睡眠挂起门控(EscLan9252::pmSuspended)。pmDrive 进 D1-D3 置位后全固件 HBI 消费者静默——task_runner 停调 MainLoop/AL 守护/AL 心跳(MainLoop 每圈 HBI 读 0x220,睡眠中任何 HBI 访问含读都把芯片打回 D0,2026-09-12 实测休眠 ~20s 即被主站"复活":芯片毫秒级被 GD32 自己叫醒、SM 失效后主站 wedge 重建 OP);ethercat_sync 三 EXTI ISR 与 timer Sync0 路径清 pending 即返。解除:pmDrive 唤醒成功/op0 读到 D0 自愈/nrstDrive 释放复位/hardwareReset。配套主站侧 ecat_native /api/pm/hold 楔死看门狗豁免。 */ /* A/B 结束: 恢复 1.6.25 稀疏+差分写(用户裁定, 主站侧已有后台恢复兜底) */ /* A/B C组: SPARSE=0+DIFF=0, 回 1.6.21 恒全量 600B 写 ESC(查开机楔死) */ /* 1.6.25:P1 输入 PDO 差分写 ESC(三区:段1 桥状态/段2 电机密排/段3+5 快照诊断;变化后 3-buffer 冲刷协议,稳定段跳过;全跳过写末 2 字节完成 SM)。P2 csrRead 分级:verify 参数,SM/AL/邮箱默认双读,诊断/MII busy/init 轮询单读。开关 CONFIG_ECAT_DIFF_ESC_WRITE。 */ /* 1.6.24:pmDrive 进 D1/D2/D3 移除读回验证。板级实测:进入睡眠后任何 HBI 访问(含读,手册 6.3.4.2 只说写唤醒,芯片实际按主机 CS 活动判定)都会 <50ms 触发唤醒回 D0——读回验证自证失败,改为置信构造返回值(READY=0|SLEEP_EN=1|MODE=op);「读状态」语义改为"若在睡眠则主动打破睡眠"。 */ /* 1.6.23:LAN9252 芯片级电源管理诊断路径。EscLan9252::pmDrive()(0xA2 action 30~34:30 读状态/31 进 D1/32 进 D2/33 进 D3/34 手动唤醒),按 DS 6.3 写 PMT_CTRL(0x084) PM_MODE+PM_SLEEP_EN(分两次写);D3 前置两 PHY General Power Down(经 ESC 核心 MII 管理 0x0510-0x0514,PHY A 地址 0/2 运行期判定),唤醒=HBI 写 BYTE_TEST 后轮询 BYTE_TEST/READY(DWT CYCCNT 忙等,50µs 步进、上限 10ms——0xA2 在 CS EXTI ISR 上下文执行,禁用 SysTickTimer::delayMs 会死等;HBI 段 maskEnter 屏 EXTI0/1 防 SYNC0 突入)。唤醒后自动恢复 PHY 上电(自协商需数秒)。休眠期间 EtherCAT 通信中断属预期,主站 wkc 报错。 */ /* 1.6.22:P1 稀疏写 ESC。PDO_InputMapping 在密排布局下按 count 只写「段1+段2有效区(0..40+count*56)」与「段3+段5(544..599)」,跳过空槽区,省 HW_EscWriteIsr 的 PRAM 字节。开关 CONFIG_ECAT_SPARSE_ESC_WRITE=1;非密排/count 撑满自动回退整包。总长 600B/SII/主站解码不变。A/B:置 0 重编即 1.6.21 写侧。 */ /* 1.6.21:段 2 改「一帧=总线电机整轮状态」。用户裁定旧需求不对:不要 win_base 滑窗扫 27 槽,也不要段 4 历史帧覆盖窗反复重发。实测病理:接 3 个电机网页却看到 5 个状态帧——固定 5 槽里空槽 node_id=0 也占位,主站不滤就多报。本版:(1) 段 2 密排,count=本拍实际电机数(0..9),空节点不占位,槽序按 node_id 升序,同号多通道取 seq 最新;主站 `for i in 0..count` 按槽内 node_id 自动识别,无需滑窗。(2) 删段 4 覆盖窗口(fillRxWindow/RxWinSlot 整套),非 STATUS 仍走段 1/3 最近一帧。(3) 差分填充:节点集合与各 seq 未变时整段复用影子,跳过 MotorState 重读与逐槽 48B 拷贝。(4) 输入 PDO 总长仍 600B,SII 无需重刷;段 2 扩到 32..543、段 3 前移到 544、段 5 仍 584。**主站解码必须同步**(decode_input/transport-ecat.js:count 密排 + 段 3 偏移 320→544)。 */
/* 1.6.19:[DIFF-STATS] 仪表成本终于测对了,并推翻 1.6.18 自己的判断。1.6.18 认定"是 -Oz 把 memcpy(&a,p,4) 编成函数调用",改成手拼 4 字节移位,结果反汇编出**每字 8 条 ldrb.w**、比开销从 21.3µs/拍涨到 41µs/拍 —— 方向完全反了。真根因不在这里:alignas(4) 标在数组声明上只到调用点为止,而 observe(const uint8_t*) 的形参签名不带对齐信息,优化器无从证明指针 4 字节对齐,两版都只能逐字节走。修法是对两个入参各做一次 __builtin_assume_aligned(p,4) 再走 memcpy —— 反汇编确认循环体已缩成 ldr.w/ldr.w/cmp/bne 每字 4 条 (150 字 ≈ 750 cycle ≈ 4.2µs/拍,与最初估算同量级),且整个函数 bl 归零(600B 快照拷贝也被折成字搬运)。教训:**测量工具的开销会直接变成结论**,"差分判断很贵"这句话差一点就被我自己造的假象坐实。被测量(变化字节数 25%、run 峰值 12)不受此影响,两版读数一致,仍然有效。 */
/* 1.6.18:修 [DIFF-STATS] 仪表自己的假象 —— 比对循环里 `memcpy(&a,p,4)` 在 -Oz 下被编成**函数调用**而非单条 LDR,首轮实测 compare 开销虚报成 21.3µs/拍(≈300 次调用×995 拍),若拿它当"差分判断很贵"的证据就结论反了。改成小端 4 字节移位拼装:合法、不碰严格别名、且是 Clang 认得折 LDR 的模式。顺带把 alignas(4) 的理由改成"服务整块 memcpy",比对本身不再依赖它。Code 52716→52768(+52B),代价是体积不是速度。 */
/* 1.6.17:新增 [DIFF-STATS] 临时仪表,为"输入 PDO 改差分写"取证,**零行为变更**。动机:差分写的收益完全取决于每拍真正变了多少字节、变化点碎成几个 run,而 run 碎片数决定它会不会把一次 600B 整块写退化成几百次带 PRAM_AVAIL 轮询的小写——这两个数今天没有实测依据,所以先只上仪表再决定做不做。实现:comm/ethercat_diff_stats.{h,cpp},每拍在 PDI_Isr 里与上一份已发布镜像做 u32 粒度比对,按 1s 窗口(DWT 周期,同 cpu_load 手法)滚动,getter 只回报"上一个完整窗口"以免主站读到半累积值;量为 cycles / changedMax / runsTotal / runsMax / 段1..段5 各自变化字节 / 比对自身 DWT 开销。经对象字典 0x2000 sub13..23 上报(新增 11 个只读 UINT32)。**只加 SDO 子索引、PDO 长度与映射逐字节不动 ⇒ 无需重生成 SII、无需断电冷启动**,可与主站独立升级。整模块由 CONFIG_ECAT_DIFF_STATS 门控,置 0 后与 1.6.16 等价;挂接点统一 [DIFF-STATS] 标记,删除=删 2 文件+删标记行。已知副作用:开启时 sub9/sub10 写侧耗时含比对开销,不能直接与旧基线对拍,真实写侧基准须置 0 重测。 */
/* 1.6.16:修段 1/段 3 快照 drop 的计数口径(纯判据修复,PDO 布局逐字节不动)。病理:`onCanFrame` 里判的是 `if (s_rxValid) ++s_dropCount`,而 s_rxValid 置位后**永不清零**(只有 flags bit0 清统计会归零计数,不动 valid),于是开机收到第一帧之后**每一帧都算一次丢弃**,drop ≡ rx−1 (mod 2^16)。实测 period=1ms、CAN 侧 6000 帧/s 时 drop 同样 6000/s,与 rx_count 逐拍 1:1(Δ 完全相等),(rx−1) mod 65536 与上报值精确吻合。后果:主站 `/api/motor/state` 的 bridge.drop 被 transport-ecat.js 当状态栏 **ERR** 显示、且它是桥全局值三通道同数 → 网页"ERR 多得离谱"其实只是收帧速率,零灵敏度、纯假仪表;calibrate_ecat_metrics 一类拿 drop 做分子分母的判据恒得 100%。本版加 s_rxPublished(写侧置 false / buildInput 段 1 发布分支置 true,与段 4 的 s_rxWinSeen 同一套"未被任何一拍取走才算丢"约定),判据改 `s_rxValid && !s_rxPublished`;计数落在 xrxSeq 奇数窗口内,消费者判奇会跳过段 1/段 3 故不会并发置位,读+自增对消费者等效原子。flags bit0 清统计时一并置 published=true,免得清零后的第一帧被误计。s_rxValid 语义不变("有过接收",flags bit0 与 backlog 取通道都依赖它常驻),故 ecat.html 短快照、daemon 的 xrx 分派、ESP32 侧 0xC1 全部零改动。**修完不会归零**:段 1/段 3 是单槽、每拍只能发布 1 帧,6000 帧/s @1ms 下仍约 5000/s(≈rx 的 0.83),这个数自此真正的含义是"快照槽位跟不上收帧速率",不是总线错误;真·CAN 错误仍看 ESP32 `/can/feedback` 的 err/TEC/REC,真·入窗丢帧看段 4 的 OVF(362) 与 seq 空洞。 */
/* 1.6.15:批量接收改**定长覆盖窗口**,输入 PDO 496→600(75×64bit)。动机:1.6.1 瘦身后批量区是"环形队列 + 变长紧排排空",容量 3 帧/拍(=3000 帧/s),现场 4000 帧/s 下每拍恒丢 1 帧(can_cadence lost=512/drain FAIL);更糟的是排空赤字把队列长期压在饱和态,而 1.6.9~1.6.13 追的错标/拼接竞态正是饱和态喂出来的(判满用的 tail 一旦滞后,生产者就写进消费者刚读过的槽)。本版按用户裁定改成覆盖式:生产者(onCanFrame)按自己游标写、绕圈覆盖,消费者(fillRxWindow)每拍整块取值 —— 没有 tail/判满/pending,那个滞后握手的载体直接不存在。定长(5×44B@364)让主站不必推游标,1.6.10 那类边界越界写诊断区的错在结构上不可能再犯。**每槽必须带 u32 seqlock**:buildInput 跑在 PDI_Isr(preempt=0) 会抢占主循环写侧,没有它主站能拿到"新 ID + 旧数据"拼出的假帧(不是"差一个周期",是现实中不存在但看起来合法的帧,与刚修的拼接同形)。seq 兼作主站去重与丢帧判据(空洞数=被覆盖未取的帧),与从站侧新加的 OVF("未取先覆"次数) 互为对账仪表。语义变更:批量区从"保证不丢的事件流"变成"最近 N 帧窗口"——同一帧会连续几拍重复出现,低频一次性帧(版本/节点应答)在突发下可能被静默覆盖;主站 daemon 层按 seq 去重后归一成事件流,故 motor.js/ecat.html/can_cadence.py 判据不变、前端零改动。容量:5 槽=5000 帧/s@1ms,对 4000 帧/s 留 25% 余量(STATUS 自 1.6.9 起不入窗,实际入窗量是 CONFIG 量级);覆盖窗口**不创造容量**,槽数 < 帧/拍 仍会静默丢帧。段1..3(0..359) 逐字节不动,段5 诊断区 484→584。三端同步:ethercat_pdo_size.h / bridge.h+cpp / dc_app.c(62→75) / gen_eeprom.py / DCU_ESI.xml / ecat_native.c decode_input(新增 600..639 分支,向后自适应 496 与 640)。必须重烧固件 + 烧 EEPROM + 断电冷启动,只烧一边 SAFEOP 报 AL 0x001A/0x0011。 */
/* 1.6.14:CAN-FD 数据段 2Mbps→5Mbps(CONFIG_CAN_DATA_PRESCALER 9→3、CONFIG_CAN_DATA_BS1 3TQ→4TQ ⇒ 90MHz/(3×6TQ),采样点 80%→83.3%;TDC filter/offset 4→12 tq 维持约 400ns,因 TDCF/TDCO 以数据段 tq 为单位)。动机:电机 STATUS48B+CONFIG24B×2 节点@1kHz 加 1kHz 下行 CMD,2Mbps 下负载约 120%(实测总线帧数被钉在 ~4993 帧/s),高频下发时 RX FIFO 拥塞。仲裁段仍 1Mbps 不变。配套:电机 APP 与 BOOT 的 data_prescaler 同步改 2(60MHz/(2×6TQ)=5Mbps)——FD 数据段速率必须全总线一致,节点 TX 都带 fd_brs。 */
/* 1.6.13:根治 —— 1.6.12 的 PDI 入口预抽实测(60s 正弦指令负载,链路活性已断言):overfull 50/s→0 但指纹违规仍 ~92/s、torn 在无溢出阶段 1-7/s 持续涨 —— 判决:溢出只是共犯之一,**357µs 的 PDI 阻塞窗口本身**才是病理母体(窗口内 FIFO 满溢毁窗口 + 被冻结的取帧路径留着不依赖溢出的换帧路径,MI/MP 只抓到少数)。本版釜底抽薪:CAN RX ISR 提到 preempt=0(CONFIG_IRQ_PREEMPT_CAN_RX,与 SYNC0 同级互不抢占)—— RX 不再被 PDI/SYNC1/SCHED 压住,FIFO 深度 ≤2、溢出与冻结同时消失,receiveFrameChecked 的多字取数自此真正原子;TX ISR 留在 2(PDI 上下文有 TX 提交,提 0 会引入提交/释放竞争)。代价:SYNC0/PDI 偶发 µs 级延迟(RX ISR ~3µs/帧×8kHz≈2.4% 占空),相对 1ms 周期是零头。PDI 预抽 pumpAllChannels 随之撤除(失去意义且引入 PDI/ISR 双消费者风险)。保留 1.6.12 的诚实管道(校验只计不丢)与诊断布局(overfull@490、relto@492、seq u16@488)作为验证仪表:修好 = reject/torn/overfull 恒 0。 */
/* 1.6.12:源头修复 —— 用户裁定"过滤是遮羞布,修采集源头,前端只许诚实反馈"。1.6.11 上线后实测(repro_cmdload 正弦负载)reject@494 仍以 ~384/s 增长:RFOD=DISABLE 挡不住溢出毁窗口(本 IP 满溢时 DISCARD 语义存疑,或毁帧发生在满态下的读并发路径),内容校验丢弃只是把坏数据拦在计数前。本版两处:(1) **源头**:CanFd::pumpAllChannels() 挂进 onEscIrqExti 的 PDI_Isr 入口 —— 在 buildInput 357µs 长阻塞(CAN ISR preempt=2 全程被压)开始之前把三路 FIFO 抽干,阻塞窗口起点深度=0、窗口内到达 ≤2-3 帧,3 深度 FIFO 从"必然打满"变成"必然不满";preempt=1 + SYNC0 已关的上下文保证与 CAN ISR 取帧互斥,与主循环批量队列生产零交集。(2) **诚实管道**:内容校验改为只计不丢(reject@494 恒 0 = 源头修好;还在涨 = 源头未愈且页面如实显示),诊断区加挂判决性测量:seq 488 拆 u16、overfull(RFO0)@490、release_timeout@492(原重入计数位,恒 0 让出)。reject 与 overfull 是否同涨 = "残余拼接是否仍源自溢出"的判决。 */
/* 1.6.11:根治残余拼接 —— 1.6.10 的 settle 延迟实测无效(正弦指令负载下拼接率不变),节点指纹实验(两电机反相正弦+速度限 200/150 打指纹)实锤拼接是**跨节点**的:node3 的 STATUS 帧头 24B = node2 的完整 CONFIG(速度指纹 200.0、角度 1142° 均属 node2 轨迹),且 CONFIG↔CONFIG 同型拼接结构校验抓不到(两边都合法)—— 电机 TX 不可能发出别的电机的字节 ⇒ 混合点在 GD32 RX 侧。真根因:can_init 的 rec_fifo_overwrite=ENABLE —— FIFO 满时覆盖最旧邮箱,而最旧邮箱正是 CPU 正在读的输出窗口;buildInput 在 PDI_Isr 占 ~357µs 压住 CAN ISR,发指令时总线 ~8kHz、阻塞窗口内 ~3 帧打满 FIFO 深度 → 覆盖进行中被读走 = 拼接(MI/MP 回读挡不住:MI 是覆盖序列最后更新的一环;settle 延迟也挡不住:非时间竞态而是溢出策略)。本版:(1) rec_fifo_overwrite=DISABLE —— 溢出丢新帧置 RFO0(rx_fifo_overfull_count 已统计),周期量丢一拍下拍即补,与软件队列"满丢新不覆旧"同则;(2) 撤 settle 延迟(无效且白烧 CPU);(3) 保留 1.6.10 的重取循环删除与边界修正,并新增入队前内容校验(STATUS 温度/电压量程+帧间斜率、CONFIG 未使用区/模式)丢结构矛盾帧、计数段5@494 —— 作为溢出之外的纵深防御(实测能挡跨型拼接,挡不住同型 CONFIG↔CONFIG,故必须配合 (1))。验证:正弦指令负载 60s,指纹违规 40→0。 */
/* 1.6.10:根治拼接帧 —— 1.6.9 的"队列覆写"定性被自测推翻为**次要**:段 2 电机槽(MotorState 路径,与批量环形队列完全独立)出现同签名拼接(node2 槽头 = node3 CONFIG 前缀 03c12200,解出 mcuTemp=-161.25),且 rx_torn/pdo_reentry 自启动恒 0 —— 两条独立下游同时中招 ⇒ 混合点在共同上游 CAN RX ISR;校验恒不报 ⇒ 拼接发生在 MI/MP 判据盲区。真根因=1.6.7 的"校验失败原地重取":RFIFOMDATA0 读指针随读自动前进且不因换帧复位,attempt1 失败后 attempt2 从错位指针开读,MI/MP 比对的是窗口身份、管不住指针错位,于是"校验通过、内容为上一帧尾巴拼下一帧开头"。实测(自测脚本 scripts/selftest_torn.py,按拍去重+灵敏度自证):空闲 0%(35k 条),电机活跃时 9.155%(56835/620838,STATUS 错标为 CONFIG 占绝对多数)。本版:(1) 去掉重取循环,校验失败即 release+丢帧+rx_torn 计数(1.6.6 语义,STATUS/CONFIG 均 1kHz 周期量,丢一拍下拍即补);(2) fillRxBatch 边界改判段 5 起点 ECAT_DIAG_OFF_LOAD(484) 而非 PDO 全长(496)——潜伏越界写诊断区,当前容量下触不到,容量放宽即咬人;(3) releaseFifo0 自清后补 settle 延迟 —— 烧(1)(2)后实测活跃流量下仍残余 0.11% 跨节点拼接(node2 槽头=node3 CONFIG 前缀,且 rx_torn 已正常计数 146,证明窗口换帧路径已被挡住),定性为 RFD 自清只代表 dequeue 握手完成、下一帧 MDATA 还在逐字搬运(MI/MP 先落数据区后落),while 连排的第二次读紧跟 release 即拿到陈旧头+新鲜尾,MI/MP 回读对这种陈旧全盲;空闲每 ISR 单帧故 0%、活跃连排才中招,与实测分布吻合。 */
/* 1.6.9:拿到实证并定位真根因 —— 给主站加了同拍 raw+parsed 的只读诊断路由(/api/motor/rxraw)后抓到:**坏帧在原始 PDO 字节里就已经是错的**(如 off=72 处条目声称 id=0x20101/dlc=24,数据却是合法 STATUS:mcuTemp=35.92 motTemp=33.55 pwrV=23.972),而主站 parsed 与 raw 逐字节一致 —— 坏在从站写入侧,与主站解析无关。根因:批量接收环形队列长期饱和下,生产者覆写了消费者正在读的槽。实测证据:CAN 侧约 4000 帧/s 而批量区每拍最多 3 条(=3000 帧/s),净积压使 pending 恒 8/12、lost 几乎每拍都有、count 恒取满 3;消费者 fillRxBatch 又攒到循环末才发布 s_rxRingTail,生产者判满用的正是这个滞后值,于是把新帧写进消费者刚读过但未发布的槽,拷进 PDO 的 id/dlc/data 遂来自不同帧(错标率 1.77%)。两处修复:(1) onCanFrame 入队**过滤 STATUS 帧** —— 完整 STATUS 本就由段 2 按槽上报,主站 drainRx() 拿到 STATUS 也直接 continue 丢掉,让它占队列纯属重复,正是它把队列压爆;(2) fillRxBatch 改为整条拷贝到局部副本再用,并逐条发布 tail,使生产者判满始终基于真实进度。
1.6.8:曾判定为"输入 PDO 填充跨上下文重入"并加了双缓冲发布 + 重入计数(段5@492)。**该假说事后被证伪**:SM-Synchron 稳态下 MainLoop 分支的门控 (!bEscIntEnabled||!bEcatFirstOutputsReceived)&&!bDcSyncActive 恒为假不执行,Sync0 因 free-run 无 DC 脉冲不触发,Sync1 与 PDI_Isr 同为 preempt=1 不能抢占 —— buildInput 实际只在 PDI_Isr 单上下文跑。双缓冲发布本身是正确加固,予以保留。
1.6.7:接着 1.6.6 治 CAN 取帧拼接。1.6.6 的校验只读 MP 的 TS/DLENC,TS 是 16 位计数器、相邻帧可能同值,DLENC 更常同为 48,于是"ID 报 STATUS、数据却是 CONFIG 帧"这种错配从校验漏了过去。本版三处改动:(1) 回读校验再加上 MI(完整 ID/帧格式),ID 变了就一定是换了帧,没有巧合空间;(2) 校验失败改为原地重取最多 3 次而不是直接丢弃 —— 失败时窗口里躺着的正是完整的新帧,重读一次通常就能拿全;(3) 整段取数放进 BASEPRI 临界区,挡住 preempt>=1 的 EtherCAT 中断。注:事后(1.6.8)证明本版方向不是主因,但这三条本身仍是正确的加固,予以保留。
1.6.6:根治 ECAT/CAN 偶发乱码 —— 取帧不再用厂商库 can_message_receive。一条 48B CANFD 报文要对同一地址 CAN_RFIFOMDATA0 连读 12 次才取完,整个过程不原子;而厂商库置 RFD0 后既不等硬件自清也不校验,取数过程会横跨 dequeue、读出两帧拼接。现改为本地 receiveFrameChecked():取数前后各读一次 MP,比对 TS[31:16] 与 DLENC,不一致即判定邮箱窗口被换帧,整帧丢弃并计入 Diag::rx_torn_count(经输入 PDO 段 5 偏移 486 原保留位上报,PDO 长度不变、EEPROM 无需重刷);releaseFifo0() 置位后轮询等自清,且改用直接赋值 —— 原来的 |= 是读改写,会把期间硬件置起的 RFF0/RFO0(rc_w1) 顺手清掉,FIFO 溢出证据就是这么丢的。另修 ISR 里 frame.id/stats.last_rx_id 取自三路共享的 s_diag.last_rx_id(而 data 取自局部 rx_message,会串出"A 通道 id 配 B 通道数据"),一律改用本帧局部 rxId。
1.6.5:段 2 电机槽补 seqlock(MotorSlot.wr_seq)。buildInput 跑在 PDI_Isr,会抢占主循环的 MotorState::onCanFrame,而那里是"先 seq=++global_seq、后 memcpy(slot.data,48)"——抢占落在 memcpy 中途就会发布"新 seq + 半新半旧 48B",主站只按 seq 判新鲜度分辨不出,混合边界若落在 float 的符号/指数字节(B16/B20/B24)就是网页上的偶发离谱值。现消费者见 wr_seq 为奇数即跳过本槽(该 56B 留零,主站按 node_id==0 跳过、下拍自愈),win_count 改报窗口跨度而非填充数以保持按 i 定位。CONFIG 路径不套锁:它只写 config_* 与恒等的 node_id/channel,buildInput 不同读这些。槽布局不变 ⇒ 输入 PDO 仍 496B,EEPROM/SII 无需重刷。
1.6.4:电机槽 40B→56B(8B 头 + v0.74 的 48B STATUS),每拍槽数 7→5(段 2 的 280B 正好 5×56)。修掉两件事:buildInput 按 48B 拷贝时溢出 16B 盖住段 3 长帧快照;主站侧 STATUS 被截到 32B 使 InitStatus(B32-35) 永远拿不到、网页把"无数据"显示成 NORMAL。槽数×槽长不变 ⇒ 输入 PDO 仍 496B,EEPROM/SII 无需重刷;主站需同步 MOT_SLOT_SIZE / MOT_SLOTS_PER_PDO / MOT_STATUS_DLC。
1.6.3:processOutput 不再从 out 回读 WIN@8——1.6.0 起 out 只在 seq 沿那拍新鲜,无沿周期里它是上一次全量读的残值,于是每拍用开机首拍的 0 盖掉 peekOutput 刚取到的新值,电机状态扫描窗口被钉死在基址 0,槽位 7..26(node3 及以上)永不上报,只有主站真发一帧才复位。WIN 的每拍刷新改由 peekOutput 独占。判据:不发任何帧,从站回显 win_base 也应等于主站请求值。 */
constexpr uint32_t APP_FW_VERSION_BUILD = 0U; constexpr uint32_t APP_FW_VERSION_BUILD = 0U;
@@ -298,7 +266,7 @@ constexpr uint32_t CONFIG_CAN_RX_FILTER_ID = CONFIG_CAN_STATUS_ID;
/** @brief CAN 接收过滤器标准帧掩码;0x7FF 表示精确匹配 11-bit ID。 */ /** @brief CAN 接收过滤器标准帧掩码;0x7FF 表示精确匹配 11-bit ID。 */
constexpr uint32_t CONFIG_CAN_RX_FILTER_MASK = 0x7FFU; constexpr uint32_t CONFIG_CAN_RX_FILTER_MASK = 0x7FFU;
/** @brief 单次 CAN 接收解析任务最多消费的软件队列帧数。 */ /** @brief 单次 CAN 接收解析任务最多消费的软件队列帧数。 */
constexpr uint32_t CONFIG_CAN_RX_PARSE_BUDGET = 48U; constexpr uint32_t CONFIG_CAN_RX_PARSE_BUDGET = 64U; /* 8kHz PDO 下主循环间隙短,单次多清一些 */
/** @brief 用户调度定时器实例。 */ /** @brief 用户调度定时器实例。 */
constexpr uint32_t CONFIG_SCHED_TIMER_INSTANCE = TIMER2; constexpr uint32_t CONFIG_SCHED_TIMER_INSTANCE = TIMER2;
@@ -325,7 +293,7 @@ constexpr uint32_t CONFIG_TASK_PERIOD_TEMP_SAMPLE_TICKS = 10U;
/** @brief 调度基准节拍:10ms 计数周期,单位 100us tick。 */ /** @brief 调度基准节拍:10ms 计数周期,单位 100us tick。 */
constexpr uint32_t CONFIG_TASK_PERIOD_CAN_RX_TICKS = 100U; constexpr uint32_t CONFIG_TASK_PERIOD_CAN_RX_TICKS = 100U;
/** @brief 调度基准节拍:CAN 周期发送任务周期,100 tick = 10ms。 /** @brief 调度基准节拍:CAN 周期发送任务周期,100 tick = 10ms。
* @note 原 1ms/帧过密,改为 10ms/帧以降低 CAN 总线负载与 GD32 邮箱压力。 */ * @note 1ms/帧过密,10ms/帧可降低 CAN 总线负载与 GD32 邮箱压力。 */
constexpr uint32_t CONFIG_TASK_PERIOD_CAN_TX_TICKS = 100U; constexpr uint32_t CONFIG_TASK_PERIOD_CAN_TX_TICKS = 100U;
/** @brief 调度基准节拍:心跳周期,单位 100us tick。 */ /** @brief 调度基准节拍:心跳周期,单位 100us tick。 */
constexpr uint32_t CONFIG_TASK_PERIOD_HEARTBEAT_TICKS = 10000U; constexpr uint32_t CONFIG_TASK_PERIOD_HEARTBEAT_TICKS = 10000U;
@@ -352,6 +320,11 @@ constexpr uint32_t CONFIG_FWDGT_TIMEOUT_MS = 3000U;
* BOOT 版本在 OTA 配置页中的 Flash 存储地址 * BOOT 版本在 OTA 配置页中的 Flash 存储地址
* 与 BOOT 工程 sys/config.h 保持一致 * 与 BOOT 工程 sys/config.h 保持一致
* ============================================================ */ * ============================================================ */
/**
* @brief APP 持续喂狗满该秒数后清 BOOT 侧连续 FWDGT 崩溃计数(word9)。
* @note 必须 > FWDGT 超时(3s)数倍;init 不清,避免 3s 内崩溃永远攒不到阈值。
*/
constexpr uint32_t CONFIG_CRASH_CLEAR_AFTER_SEC = 30U;
constexpr uint32_t CONFIG_OTA_CONFIG_PAGE_ADDR = 0x0807E000U; constexpr uint32_t CONFIG_OTA_CONFIG_PAGE_ADDR = 0x0807E000U;
constexpr uint32_t CONFIG_BOOT_VERSION_ADDR0 = CONFIG_OTA_CONFIG_PAGE_ADDR + 0x18U; constexpr uint32_t CONFIG_BOOT_VERSION_ADDR0 = CONFIG_OTA_CONFIG_PAGE_ADDR + 0x18U;
constexpr uint32_t CONFIG_BOOT_VERSION_ADDR1 = CONFIG_OTA_CONFIG_PAGE_ADDR + 0x1CU; constexpr uint32_t CONFIG_BOOT_VERSION_ADDR1 = CONFIG_OTA_CONFIG_PAGE_ADDR + 0x1CU;
@@ -365,16 +338,10 @@ constexpr uint32_t CONFIG_NVIC_PRIORITY_GROUP = NVIC_PRIGROUP_PRE4_SUB0
constexpr uint32_t CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC = 0U; constexpr uint32_t CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC = 0U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_SCHED = 1U; constexpr uint32_t CONFIG_IRQ_PREEMPT_SCHED = 1U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_CAN = 2U; constexpr uint32_t CONFIG_IRQ_PREEMPT_CAN = 2U;
/* 1.6.13:CAN RX 提到 preempt=0(与 EtherCAT SYNC0 同级)。 /* CAN RX 抢占级 = 0(与 EtherCAT SYNC0 同级、互不抢占)。
* 1.6.12 的 PDI 入口预抽实测:溢出确实大降(overfull 50/s→0)但拼接照旧 * 目的:RX ISR 不被 PDI/SYNC1/SCHED 压住,FIFO 深度可压到 ≤2,
* (指纹违规 ~92/s,且 torn 在无溢出阶段仍 1-7/s 在涨)—— 357µs 的 PDI * 多字取帧真正原子。TX 留在 2:PDI 上下文有 TX 提交,提 0 会引入
* 阻塞窗口本身就是病理:窗口内 FIFO 满溢毁窗口的同时,被冻结的取帧路径 * 提交/释放竞争。代价:SYNC0/PDI 偶发 µs 级延迟(RX ~3µs/帧×8kHz≈2.4%)。 */
* 还留着不依赖溢出的换帧路径(MI/MP 只抓到少数)。preempt=0 一并掐断:
* RX ISR 不再被 PDI/SYNC1/SCHED 压住(FIFO 深度 ≤2,溢出与冻结同时消失),
* 且与 SYNC0 同级互不抢占 —— receiveFrameChecked 的多字取数自此真正原子。
* TX ISR 留在 2:PDI 上下文有 TX 提交,提到 0 会引入提交/释放竞争。
* 代价:SYNC0/PDI 偶发 µs 级延迟(RX ISR ~3µs/帧 × ~8kHz ≈ 2.4% 占空),
* 相对 1ms 周期与 165µs 总线时间是零头。 */
constexpr uint32_t CONFIG_IRQ_PREEMPT_CAN_RX = 0U; constexpr uint32_t CONFIG_IRQ_PREEMPT_CAN_RX = 0U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_ETHERCAT_ESC = 1U; constexpr uint32_t CONFIG_IRQ_PREEMPT_ETHERCAT_ESC = 1U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC1 = 1U; constexpr uint32_t CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC1 = 1U;
@@ -382,9 +349,7 @@ constexpr uint32_t CONFIG_IRQ_PREEMPT_SPI_USART = 3U;
/* ============================================================ /* ============================================================
* EtherCAT LAN9252 配置(开关见顶部 CONFIG_MAIN_LOOP_WFI / ETHERCAT_SYNC_MODE) * EtherCAT LAN9252 配置(开关见顶部 CONFIG_MAIN_LOOP_WFI / ETHERCAT_SYNC_MODE)
* ============================================================ */ * ============================================================ */
/* 1.5.27:CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD / CONFIG_ESC_PDI_INPUT_OFFSET / /* PDO 交换由 SSC(dc_app.c APPL_*) 完成。 */
* CONFIG_ESC_PDI_OUTPUT_OFFSET / CONFIG_ETHERCAT_PDO_SIZE 随死代码 EthercatSlave::
* periodicIsr 三缓冲一并删除;PDO 交换由 SSC(dc_app.c APPL_*, 4×64bit=32B) 完成。 */
/* ---------- EXMC 引脚配置(GD32E508 EXMC 标准映射) ---------- */ /* ---------- EXMC 引脚配置(GD32E508 EXMC 标准映射) ---------- */
constexpr uint32_t CONFIG_ESC_EXMC_BASE_ADDR = 0x60000000U; constexpr uint32_t CONFIG_ESC_EXMC_BASE_ADDR = 0x60000000U;
+5
View File
@@ -115,3 +115,8 @@ __attribute__((section(".ramfunc"), noinline)) void CpuLoad::tick()
cpuLoadRollWindow(now); cpuLoadRollWindow(now);
} }
} }
/* C 侧挂钩:供 ecatappl.c / dc_app.c 计入 EtherCAT 过程数据路径。
* 与 beginWork/endWork 共用深度计数器,嵌套不重不漏。 */
extern "C" void CpuLoad_Enter(void) { CpuLoad::beginWork(); }
extern "C" void CpuLoad_Leave(void) { CpuLoad::endWork(); }
+18 -5
View File
@@ -16,11 +16,12 @@
* - 全部挂钩 ISR:TIMER2(100µs 调度)、SysTick、EXTI0/1/2(EtherCAT Sync0/PDI/ * - 全部挂钩 ISR:TIMER2(100µs 调度)、SysTick、EXTI0/1/2(EtherCAT Sync0/PDI/
* Sync1)、TIMER0(DC 定时档)、CAN0-2 RX+TX、SPI0+EXTI10_15、USART0。 * Sync1)、TIMER0(DC 定时档)、CAN0-2 RX+TX、SPI0+EXTI10_15、USART0。
* DC 模式下 Sync0_Isr 的 EXMC 过程数据搬运由此计入——这是大头,不能漏。 * DC 模式下 Sync0_Isr 的 EXMC 过程数据搬运由此计入——这是大头,不能漏。
* - 主循环非延迟段:TaskManager 派发的周期任务、0xB0 进 BOOT 处理、 * - 主循环非延迟段:TaskManager 派发任务、BOOT、OTA relay、新鲜度救援、
* 电机 OTA relay 推进(isActive() 门控,空闲时零挂钩开销)。 * free-run 下 **1ms 节流** 的过程数据应用(有 CpuLoad 挂钩)。
* 不计入项:SSC MainLoop 轮询、AppSpiCommandBus.poll()、看门狗喂狗等常开检查 * - PDI/Sync0 ISR 全程(含 SM 类型下每拍 Process/映射)——负载随 PDO Hz 升。
* (free-run 调试模式下 PDO 应用跑在主循环轮询路径,不计入;生产走 DC 模式, * 不计入:MainLoop/SSC/poll 空转。
* 等价工作在 Sync0 ISR 内已计入)。 * 性能约束:PDI 热路径不得「凡 AL 事件就 Process/InputMapping」——8kHz 时
* 单拍超 ~120µs 墙会掉速;输出用 12B 探针,process 兜底读带新鲜度门。
* *
* 并发协议(无关中断、无临界区):enter/exit 用 LDREX/STREX 原子改深度, * 并发协议(无关中断、无临界区):enter/exit 用 LDREX/STREX 原子改深度,
* 任一优先级可抢占任意上下文而不破坏计数——STREX 失败方重试时会看到已修正的 * 任一优先级可抢占任意上下文而不破坏计数——STREX 失败方重试时会看到已修正的
@@ -34,6 +35,17 @@
#include <stdint.h> #include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/** C 侧挂钩(ecatappl.c / dc_app.c)。与 beginWork/endWork 同一深度计数器。 */
void CpuLoad_Enter(void);
void CpuLoad_Leave(void);
#ifdef __cplusplus
}
#endif
#ifdef __cplusplus
/** /**
* @brief CPU 负载率测量。 * @brief CPU 负载率测量。
* @note 全静态类,与 SysTickTimer 同风格。 * @note 全静态类,与 SysTickTimer 同风格。
@@ -93,5 +105,6 @@ public:
static volatile uint16_t s_loadPerm; /**< 最近一个完整窗口的负载率(0.1%) */ static volatile uint16_t s_loadPerm; /**< 最近一个完整窗口的负载率(0.1%) */
static volatile uint32_t s_seq; /**< 完成滚动的窗口计数 */ static volatile uint32_t s_seq; /**< 完成滚动的窗口计数 */
}; };
#endif /* __cplusplus */
#endif /* SYS_CPU_LOAD_H */ #endif /* SYS_CPU_LOAD_H */
+9 -11
View File
@@ -7,7 +7,7 @@
* ADC → CANFD → OtaBootRequest → TaskManager → Scheduler → * ADC → CANFD → OtaBootRequest → TaskManager → Scheduler →
* EtherCAT(+Sync 可选) → Watchdog → report * EtherCAT(+Sync 可选) → Watchdog → report
* *
* 1.5.34:各模块 init 从"盲配置"改为带检查(寄存器回读 / 节拍验证 / 校准超时), * 各模块 init 带检查(寄存器回读 / 节拍验证 / 校准超时),
* 失败不置位;复位源/模块位图/critical 失败位图经 getter 导出供 SPI 0xA3 上报。 * 失败不置位;复位源/模块位图/critical 失败位图经 getter 导出供 SPI 0xA3 上报。
*/ */
@@ -38,8 +38,7 @@ uint8_t AppInitializer::s_resetCause = 0U;
uint8_t AppInitializer::s_faultCodes[AppInitializer::kModuleCount] = {0U}; uint8_t AppInitializer::s_faultCodes[AppInitializer::kModuleCount] = {0U};
/* CoE 0x100A「Software version」写入口,实现在 comm/ssc/src/coeappl.c(C 链接)。 /* CoE 0x100A「Software version」写入口,实现在 comm/ssc/src/coeappl.c(C 链接)。
* APP 侧原本一直回 SSC 占位 "1.0",主站在总线上读不到版本,只能靠 SPI PING 的 * 主站总线读此对象可自证「新 APP 真起来了」。 */
* 4 字节二进制身份块;这条总线读数是刷写后「新 APP 真起来了」的自证。 */
extern "C" void AppSetSoftwareVersion(const char *version); extern "C" void AppSetSoftwareVersion(const char *version);
namespace { namespace {
@@ -200,9 +199,8 @@ void AppInitializer::report()
fail_list[0] != '\0' ? fail_list : "?"); fail_list[0] != '\0' ? fail_list : "?");
} }
/* 日志完整性自证:串口 TX 缓冲溢出会静默吞字节,1.5.40 及之前实测使 [ECAT] 诊断 /* 日志完整性自证:串口 TX 缓冲溢出会静默吞字节。
* 出现 "bus eaabled"/"pdi_type=0x8Dexect" 等掉字,并两次导致 LAN9252 故障误定性。 * 缓冲满时自旋腾位,仅 USART 外设异常才丢弃;非 0 即代表上面的日志不可全信。 */
* 1.5.41 起缓冲满时自旋腾位,仅 USART 外设异常才丢弃;非 0 即代表上面的日志不可全信。 */
const uint32_t dropped = Usart0::droppedBytes(); const uint32_t dropped = Usart0::droppedBytes();
if (dropped != 0U) { if (dropped != 0U) {
printf("[APP] WARN: uart dropped %u bytes, 以上日志可能不完整\r\n", printf("[APP] WARN: uart dropped %u bytes, 以上日志可能不完整\r\n",
@@ -308,13 +306,11 @@ void AppInitializer::init()
} }
/* ---- EtherCAT: EXMC 总线探活 → SSC 协议栈初始化 → SYNC0/IRQ 中断使能 ---- /* ---- EtherCAT: EXMC 总线探活 → SSC 协议栈初始化 → SYNC0/IRQ 中断使能 ----
* 1.5.27:改经 EthercatSlave::init()(内部调用 EscLan9252::init 并记录
* chipId/AL 状态诊断,此前该函数无调用点、s_escReady 恒为 false)。
* 门控用 EscLan9252::isBusReady()(BYTE_TEST 通过)而非 isEscReady(): * 门控用 EscLan9252::isBusReady()(BYTE_TEST 通过)而非 isEscReady():
* 冷启动时 AL=INIT,SSC 必须照常启动并等待主站配置。 * 冷启动时 AL=INIT,SSC 必须照常启动并等待主站配置。
* 1.5.39:追加 isCoreReady() 门控——EscLan9252::init() 已等待 EEPROM(SII) * 另需 isCoreReady()——EscLan9252::init() 已等待 EEPROM(SII)
* 加载完成(DL_STATUS bit0);未加载时 EtherCAT 核心寄存器写静默丢弃, * 加载完成(DL_STATUS bit0);未加载时 EtherCAT 核心寄存器写静默丢弃,
* SSC 的 AL Event Mask/邮箱 SM 配置会全部落空(主站恒读 AL=INIT 的根因), * SSC 的 AL Event Mask/邮箱 SM 配置会全部落空(主站恒读 AL=INIT),
* 此时跳过 SSC 并以独立故障码上报。 * 此时跳过 SSC 并以独立故障码上报。
* EEPROM 校验/烧录延迟到主循环执行,避免阻塞 init(烧录需 ~20s)。 */ * EEPROM 校验/烧录延迟到主循环执行,避免阻塞 init(烧录需 ~20s)。 */
EthercatSlave::init(); EthercatSlave::init();
@@ -337,7 +333,9 @@ void AppInitializer::init()
printf("[ECAT] EXMC bus not ready, SSC skipped\r\n"); printf("[ECAT] EXMC bus not ready, SSC skipped\r\n");
} }
/* 看门狗放最后:PSC/RLD 写入经硬件确认才置位 */ /* 看门狗放最后:PSC/RLD 写入经硬件确认才置位。
* 崩溃计数不在这里清:须喂狗稳定满 CONFIG_CRASH_CLEAR_AFTER_SEC
* (task_runner 每圈 maybeClearCrashCount),否则主循环崩溃攒不到阈值。 */
const uint8_t wdgFault = Watchdog::init(); const uint8_t wdgFault = Watchdog::init();
if (wdgFault == Watchdog::kFaultOk) { if (wdgFault == Watchdog::kFaultOk) {
mark(kWatchdog); mark(kWatchdog);
+36 -13
View File
@@ -10,18 +10,18 @@
#include "sys/systick.h" #include "sys/systick.h"
namespace { namespace {
/** Bootloader OTA 配置页地址,与 BOOT 工程 sys/config.h 保持一致。 */ /** Bootloader OTA 配置页地址与 word 布局:唯一真源 shared/ota_layout.h。 */
constexpr uint32_t kOtaConfigPageAddr = 0x0807E000U; constexpr uint32_t kOtaConfigPageAddr = OTA_LAYOUT_CONFIG_PAGE;
constexpr uint32_t kAppLatestAvalAddr = kOtaConfigPageAddr + 0x00U; constexpr uint32_t kAppLatestAvalAddr = OTA_LAYOUT_ADDR_LATEST_AVAL;
constexpr uint32_t kAppLatestEnAddr = kOtaConfigPageAddr + 0x08U; constexpr uint32_t kAppLatestEnAddr = OTA_LAYOUT_ADDR_LATEST_EN;
constexpr uint32_t kAppLatestCheckAddr = kOtaConfigPageAddr + 0x10U; constexpr uint32_t kAppLatestCheckAddr = OTA_LAYOUT_ADDR_LATEST_CHECK;
/** BOOT 版本号在 OTA 配置页中的存储地址:word6=major|minor, word7=patch|build。 */ constexpr uint32_t kBootVersionAddr0 = OTA_LAYOUT_ADDR_BOOT_VER0;
constexpr uint32_t kBootVersionAddr0 = kOtaConfigPageAddr + 0x18U; constexpr uint32_t kBootVersionAddr1 = OTA_LAYOUT_ADDR_BOOT_VER1;
constexpr uint32_t kBootVersionAddr1 = kOtaConfigPageAddr + 0x1CU; constexpr uint32_t kBootCrashCountAddr = OTA_LAYOUT_ADDR_CRASH_COUNT;
constexpr uint32_t kOtaAval = 0x00000000U; constexpr uint32_t kOtaAval = OTA_LAYOUT_AVAL;
constexpr uint32_t kOtaNotAval = 0xFFFFFFFFU; constexpr uint32_t kOtaNotAval = OTA_LAYOUT_NOT_AVAL;
constexpr uint32_t kFmcPageSize = 0x00000800U; constexpr uint32_t kFmcPageSize = OTA_LAYOUT_PAGE_SIZE;
constexpr uint32_t kRebootDelayMs = 20U; constexpr uint32_t kRebootDelayMs = 20U;
static bool writeWord(uint32_t addr, uint32_t value) static bool writeWord(uint32_t addr, uint32_t value)
{ {
@@ -122,6 +122,29 @@ BootVersion OtaBootRequest::readBootVersion()
}; };
} }
/**
* @brief 清零连续 FWDGT 崩溃计数。
* @note 与 BOOT OTA_CRASH_STAY_MAX 门配套。写 0(AVAL)可对 1→0 直接编程。
*/
bool OtaBootRequest::clearCrashCount()
{
return otaConfigSet(kBootCrashCountAddr, kOtaAval);
}
/**
* @brief 喂狗运行满 CONFIG_CRASH_CLEAR_AFTER_SEC 后清崩溃计数(每上电至多一次)。
*/
void OtaBootRequest::maybeClearCrashCount(uint32_t uptime_sec)
{
static bool s_cleared = false;
if (s_cleared || uptime_sec < CONFIG_CRASH_CLEAR_AFTER_SEC) {
return;
}
if (clearCrashCount()) {
s_cleared = true;
}
}
/** /**
* @brief 写入 Bootloader OTA 等待标志,并安排稍后软复位。 * @brief 写入 Bootloader OTA 等待标志,并安排稍后软复位。
* @return true 表示标志写入成功;false 表示 Flash 配置页写入失败。 * @return true 表示标志写入成功;false 表示 Flash 配置页写入失败。
@@ -197,7 +220,7 @@ void OtaBootRequest::process()
if(!ok) { if(!ok) {
s_spiFrozenForBoot = false; s_spiFrozenForBoot = false;
nvic_irq_enable(SPI0_IRQn, 2U, 0U); nvic_irq_enable(SPI0_IRQn, 2U, 0U);
nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U); /* 1.6.32: 与 configureExti 一致提到抢占级 0 */ nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U); /* 与 configureExti 一致:抢占级 0 */
/* requestEnterBoot 失败时 s_rebootPending 仍为 false */ /* requestEnterBoot 失败时 s_rebootPending 仍为 false */
return; return;
} }
+13
View File
@@ -40,6 +40,19 @@ public:
*/ */
static BootVersion readBootVersion(); static BootVersion readBootVersion();
/**
* @brief 清零 BOOT 侧连续 FWDGT 崩溃计数(配置页 word9)。
* @note 不要在 init 里立刻调用;见 maybeClearCrashCount()。
*/
static bool clearCrashCount();
/**
* @brief APP 持续喂狗满 CONFIG_CRASH_CLEAR_AFTER_SEC 后清一次崩溃计数。
* @param[in] uptime_sec 当前 APP 运行秒数(g_app_uptime_sec)。
* @note 挂在主循环;init 未走完或 30s 内崩溃时计数保留,BOOT 才能攒到阈值。
*/
static void maybeClearCrashCount(uint32_t uptime_sec);
/** /**
* @brief 尝试处理一帧 CAN 接收报文。 * @brief 尝试处理一帧 CAN 接收报文。
* @param[in] channel 报文来源 CAN 通道。 * @param[in] channel 报文来源 CAN 通道。
+44 -37
View File
@@ -237,7 +237,7 @@ void TaskRunner::taskCanRxParse()
s_runtime.last_rx_data, s_runtime.last_rx_data,
s_runtime.last_rx_len); s_runtime.last_rx_len);
#endif #endif
/* 1.5.53:旁路投递给 EtherCAT PDO 桥。放在分发链末尾且**不消费**帧 /* 旁路投递给 EtherCAT PDO 桥。放在分发链末尾且**不消费**帧
* (不 return、不 continue),因此 OTA relay / 电机状态的取帧不受影响—— * (不 return、不 continue),因此 OTA relay / 电机状态的取帧不受影响——
* 本队列只有 taskCanRxParse 一个消费者,新增第二个消费者会互相抢帧。 */ * 本队列只有 taskCanRxParse 一个消费者,新增第二个消费者会互相抢帧。 */
EthercatCanBridge::onCanFrame(s_runtime.last_rx_channel, EthercatCanBridge::onCanFrame(s_runtime.last_rx_channel,
@@ -284,31 +284,26 @@ bool TaskRunner::copyAdcSample(Adc::Sample *out)
/* SSC 侧 AL 诊断快照(dc_app.c): [19:16]=AL 状态 [31:24]=AL 错误码 [15:0]=AL 事件寄存器 */ /* SSC 侧 AL 诊断快照(dc_app.c): [19:16]=AL 状态 [31:24]=AL 错误码 [15:0]=AL 事件寄存器 */
extern "C" uint32_t EcatDbg_ReadAlDiag(void); extern "C" uint32_t EcatDbg_ReadAlDiag(void);
/* SSC 侧 AL Event Mask(0x204) 守护(dc_app.c, 1.5.48):缺位自愈+WARN,返回实际值/0xFFFF=不适用 */ /* SSC 侧 AL Event Mask(0x204) 守护(dc_app.c):缺位自愈+WARN,返回实际值/0xFFFF=不适用 */
extern "C" uint32_t EcatDbg_GuardAleventMask(void); extern "C" uint32_t EcatDbg_GuardAleventMask(void);
/* 1.6.33:OP 输入新鲜度不变量 — 输入 PDO 停更则主循环强制重写(根治假连接) */ /* OP 输入新鲜度不变量 — 输入 PDO 停更则主循环强制重写 */
extern "C" void EcatDbg_OpInputFreshness(void); extern "C" void EcatDbg_OpInputFreshness(void);
/** /**
* @brief 1.5.48/1.5.49 AL Event Mask(0x204) 守护 + SM2 事件楔死自愈 + 1.5.52 邮箱 * @brief AL Event Mask(0x204) 守护 + SM2 事件楔死自愈 + 邮箱 SM 地址/长度失配自愈:
* SM 地址/长度失配自愈:250ms 周期。 * 250ms 周期。
* @note 实测"停止回 INIT 后重进 OP"概率性挂起(SAFEOP+0x001B, WKC=3/3 但 * @note PDI_Isr 通路楔死时(0x220 bit10 挂起)守护在 SAFEOP 后 1s 内替 PDI_Isr
* bEcatFirstOutputsReceived 不置位,0x220 bit10 挂起 10s 无人处理)—— * 完成工作(置位标志+应答事件),主站 12s OP 等待窗口内可自愈;WARN 携带
* PDI_Isr 通路楔死。守护在 SAFEOP 后 1s 内替 PDI_Isr 完成工作(置位 * 全部内部旗标,下次复现一行日志定位楔死路径(详见 dc_app.c 注释)。
* 标志+应答事件),主站 12s OP 等待窗口内可自愈;WARN 携带全部内部旗标, * 周期必须快过主站 SDO 超时(~700ms),否则自愈永远落在主站放弃之后。
* 下次复现一行日志定位楔死路径(详见 dc_app.c 注释)。 * 邮箱几何已构建期常量化(mailbox.c),CSR 错值无进入缓存路径;
* 1.5.52:周期 1s→250ms——新加的邮箱地址自愈(1.5.52 根治 SSC 侧 * 本守护降级为运行期内存踩踏兜底。
* u16EscAddrSendMbx 错位楔死,详见 dc_app.c)必须快过主站 SDO 超时
* (~700ms),否则自愈永远落在主站放弃之后,形同虚设。
* 1.5.59:邮箱楔死已根治(mailbox.c 几何常量化,CSR 错值无进入缓存路径),
* 本守护降级为运行期内存踩踏兜底;且修复了 1.5.52~1.5.58 的 SWAPWORD
* 字节序 bug——那段时期本守护的失配判定恒短路,实际从未生效过。
*/ */
static void ethercatAleMaskGuard() static void ethercatAleMaskGuard()
{ {
static uint32_t s_lastGuardMs = 0U; static uint32_t s_lastGuardMs = 0U;
if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) { if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) {
/* 1.6.27:睡眠挂起期一并静默——守护内部读 SM 起止/长度与 0x204/0x220 /* 睡眠挂起期一并静默——守护内部读 SM 起止/长度与 0x204/0x220
* 全是 HBI 访问,会把睡着的芯片叫醒 */ * 全是 HBI 访问,会把睡着的芯片叫醒 */
return; return;
} }
@@ -320,7 +315,7 @@ static void ethercatAleMaskGuard()
} }
/** /**
* @brief 1.5.32 ECAT 心跳:主循环存活证明 + ESC AL 状态/事件寄存器快照。 * @brief ECAT 心跳:主循环存活证明 + ESC AL 状态/事件寄存器快照。
* @note 仅在同步 ISR 激活(SSC 在跑)时打印。心跳停 = 主循环卡死; * @note 仅在同步 ISR 激活(SSC 在跑)时打印。心跳停 = 主循环卡死;
* al=0x1 且 evt bit0 长期置位 = SSC 没处理 AL 请求; * al=0x1 且 evt bit0 长期置位 = SSC 没处理 AL 请求;
* al 与主站读到的不一致 = ESC 写回路径问题。 * al 与主站读到的不一致 = ESC 写回路径问题。
@@ -328,7 +323,7 @@ static void ethercatAleMaskGuard()
static void ethercatAlHeartbeat() static void ethercatAlHeartbeat()
{ {
if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) { if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) {
/* 1.6.27:睡眠挂起期静默——读 0x0130/0x0220 即 HBI 访问,读即唤醒 */ /* 睡眠挂起期静默——读 0x0130/0x0220 即 HBI 访问,读即唤醒 */
return; return;
} }
const uint32_t alDiag = EcatDbg_ReadAlDiag(); const uint32_t alDiag = EcatDbg_ReadAlDiag();
@@ -433,8 +428,7 @@ void TaskRunner::taskDiagnosticPrint()
#endif #endif
if (EthercatSlave::isEscReady()) { if (EthercatSlave::isEscReady()) {
/* 1.5.27:periodicIsr 已删除(死代码,无调用点),PDO 交换由 SSC /* PDO 交换由 SSC Sync0_Isr → ECAT_Application → dc_app.c APPL_* 完成。 */
* Sync0_Isr → ECAT_Application → dc_app.c APPL_* 完成。 */
printf("[ECAT] ESC ready (chip id=0x%08lX), PDO via SSC Sync0_Isr\r\n", printf("[ECAT] ESC ready (chip id=0x%08lX), PDO via SSC Sync0_Isr\r\n",
(unsigned long)EthercatSlave::getChipId()); (unsigned long)EthercatSlave::getChipId());
} else { } else {
@@ -506,7 +500,6 @@ void TaskRunner::taskDiagnosticPrint()
(unsigned long)g_app_uptime_sec); (unsigned long)g_app_uptime_sec);
s_prev_esc_ready = esc_ready; s_prev_esc_ready = esc_ready;
} }
/* 1.5.27:periodicIsr ISR 统计随死代码一并删除 */
/* DC SYNC0 通路验证:仅在有脉冲增量时打印(未接主站时静默)。 */ /* DC SYNC0 通路验证:仅在有脉冲增量时打印(未接主站时静默)。 */
ethercatSyncCounterPrint(); ethercatSyncCounterPrint();
ethercatAlHeartbeat(); ethercatAlHeartbeat();
@@ -546,11 +539,23 @@ void TaskRunner::runPending()
{ {
Watchdog::feed(); Watchdog::feed();
/* 高 PDO 频率时:有帧就解析,但**每圈限一次**且预算内尽量清空。
* 每圈都跑满 budget 在 2kHz 可能抢太多主循环(实测达成率掉到 85%)。
* 8kHz 下主循环间隙极短,仍需入口排水,否则 gseq/s 从 ~3750 掉到数百。 */
if (CanFd::hasRxFrame()) {
CpuLoad::beginWork();
taskCanRxParse();
CpuLoad::endWork();
}
/* 喂狗稳定满 30s 才清 BOOT 崩溃计数(每上电一次)。init 就清会让
* 主循环 3s 内 FWDGT 崩溃永远攒不到 OTA_CRASH_STAY_MAX。 */
OtaBootRequest::maybeClearCrashCount(g_app_uptime_sec);
/* === HBI 周期诊断:每 20 秒重新跑一次 EXMC/HBI 探活,方便热插拔/焊接后观察 === /* === HBI 周期诊断:每 20 秒重新跑一次 EXMC/HBI 探活,方便热插拔/焊接后观察 ===
* 1.5.27:EtherCAT 同步 ISR 激活后停止(diagDump 会把 EXMC 引脚切成 GPIO, * EtherCAT 同步 ISR 激活后停止(diagDump 会把 EXMC 引脚切成 GPIO,
* 拆毁运行中的总线;诊断的 bit-bang 也与 HBI 事务互斥冲突)。 * 拆毁运行中的总线;诊断的 bit-bang 也与 HBI 事务互斥冲突)。
* 1.5.33:整块由 CONFIG_ESC_EXMC_DIAG 门控,默认关闭——硬件已确认全通, * 整块由 CONFIG_ESC_EXMC_DIAG 门控,默认关闭;换板/补焊排查浮空时置 1 恢复。 */
* 不再每 20s 拆总线做探针;换板/补焊排查浮空时置 1 恢复。 */
#if CONFIG_ESC_EXMC_DIAG #if CONFIG_ESC_EXMC_DIAG
static uint32_t s_lastDiagSec = 0xFFFFFFFFU; static uint32_t s_lastDiagSec = 0xFFFFFFFFU;
if (!EthercatSync::isActive() if (!EthercatSync::isActive()
@@ -562,9 +567,9 @@ void TaskRunner::runPending()
} }
#endif #endif
/* 延迟 EEPROM 烧录已删除(1.5.29):ESI 镜像改由 EtherCAT 主站侧刷写 /* ESI 镜像由 EtherCAT 主站侧刷写
* (tools/ecat/ecat_flash_eeprom.py 经 ESC 0x0500 EEPROM 接口), * (tools/ecat/ecat_flash_eeprom.py 经 ESC 0x0500 EEPROM 接口),
* 固件不再承担烧录/校验职责。 */ * 固件不承担烧录/校验职责。 */
/* poll() 含按铃超时看门狗(cancelRing),必须每轮无条件执行。 /* poll() 含按铃超时看门狗(cancelRing),必须每轮无条件执行。
* 主动推送已随 2026-08 请求驱动改造移除,此处仅需消费请求响应的按铃。 */ * 主动推送已随 2026-08 请求驱动改造移除,此处仅需消费请求响应的按铃。 */
@@ -580,7 +585,7 @@ void TaskRunner::runPending()
/* 电机 OTA relay:活跃时每圈推进状态机(等 ACK / 超时 / 发下一包)。 /* 电机 OTA relay:活跃时每圈推进状态机(等 ACK / 超时 / 发下一包)。
* 非活跃时立即返回,开销可忽略。 */ * 非活跃时立即返回,开销可忽略。 */
/* 负载率挂钩(1.5.63):relay 活跃时才计 busy——isActive() 是廉价枚举判断, /* 负载率挂钩:relay 活跃时才计 busy——isActive() 是廉价枚举判断,
* 空闲路径零挂钩开销;计数段覆盖等 ACK/超时/发包的真实工作。 */ * 空闲路径零挂钩开销;计数段覆盖等 ACK/超时/发包的真实工作。 */
const bool motorOtaActive = MotorOtaRelay::isActive(); const bool motorOtaActive = MotorOtaRelay::isActive();
if (motorOtaActive) { if (motorOtaActive) {
@@ -594,26 +599,28 @@ void TaskRunner::runPending()
CpuLoad::endWork(); CpuLoad::endWork();
} }
/* 2026-08 请求驱动改造:0xC1 CAN 反馈 / 0xC5 电机状态 / 0xC4 ADC 已改为 /* 仅保留事件驱动、低频的 0xC6 电机版本推送。 */
* ESP32 周期查询(主动按铃推送全部移除,避免 OTA 期间悬空按铃刷屏)。
* 仅保留事件驱动、低频的 0xC6 电机版本推送。 */
motorVersionPushStep(); motorVersionPushStep();
/* SSC 协议栈主循环:AL 状态机 + 邮箱处理 + FreeRun PDO。 /* SSC 协议栈主循环:AL 状态机 + 邮箱处理 + FreeRun PDO。
* 每轮无条件调用,SSC 内部自行判断是否有待处理事件。 * 每轮无条件调用,SSC 内部自行判断是否有待处理事件。
* DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。 * DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。
* 1.6.27:ESC 睡眠(D1-D3)挂起期整段停调——MainLoop 每圈必读 0x220(AL 事件 * ESC 睡眠(D1-D3)挂起期整段停调——MainLoop 每圈必读 0x220(AL 事件
* 寄存器,经 HBI),而睡眠中任何 HBI 访问(含读)都会把芯片打回 D0, * 寄存器,经 HBI),而睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。
* 2026-09-12 实测休眠后 ~20s 即被"复活"。 */ *
* 负载口径(恢复 1.5.63):MainLoop **不**计入 busy。
* 实时模式主循环是 NOP 忙等,每圈都调 MainLoop;整段计入会把
* load_perm 抬到恒 ~99%(250Hz 也「占满」,与历史 4kHz≈80% 曲线对不上)。
* 过程数据成本应由 PDI/Sync0 ISR 挂钩体现,见 sys/cpu_load.h。 */
if (!EscLan9252::pmSuspended()) { if (!EscLan9252::pmSuspended()) {
MainLoop(); MainLoop();
/* 1.6.33:OP 输入新鲜度不变量。与 MainLoop 同拍调用——正常时 /* OP 输入新鲜度不变量。与 MainLoop 同拍调用——正常时
* PDI_Isr 已在刷新,这里只做廉价 seq 比对;停更超 5ms 才强制重写。 * PDI_Isr 已在刷新,这里只做廉价 seq 比对;停更超 5ms 才强制重写。
* 睡眠期与 MainLoop 一并跳过(读 HBI 会唤醒芯片)。 */ * 睡眠期与 MainLoop 一并跳过(读 HBI 会唤醒芯片)。 */
EcatDbg_OpInputFreshness(); EcatDbg_OpInputFreshness();
} }
/* 1.5.48:AL Event Mask(0x204) 守护——每圈入口执行,内部 250ms 节流。 /* AL Event Mask(0x204) 守护——每圈入口执行,内部 250ms 节流。
* 放 MainLoop 旁而非诊断任务:守护必须独立于诊断打印频率, * 放 MainLoop 旁而非诊断任务:守护必须独立于诊断打印频率,
* OP 等待期(12s)内从站进 SAFEOP 后 1s 内即介入自愈。 */ * OP 等待期(12s)内从站进 SAFEOP 后 1s 内即介入自愈。 */
ethercatAleMaskGuard(); ethercatAleMaskGuard();
@@ -621,7 +628,7 @@ void TaskRunner::runPending()
const uint32_t pending = TaskManager::takePending(); const uint32_t pending = TaskManager::takePending();
if(pending == 0U) return; if(pending == 0U) return;
/* 负载率挂钩(1.5.63):派发的周期任务属"非延迟工作",整段计入 busy; /* 负载率挂钩:派发的周期任务属"非延迟工作",整段计入 busy;
* 前面的常开轮询(Watchdog/poll/MainLoop/guard)不计入,见 sys/cpu_load.h。 */ * 前面的常开轮询(Watchdog/poll/MainLoop/guard)不计入,见 sys/cpu_load.h。 */
CpuLoad::beginWork(); CpuLoad::beginWork();
if(pending & (1UL << TASK_ADC_VREF_SAMPLE)) taskAdcVrefSample(); if(pending & (1UL << TASK_ADC_VREF_SAMPLE)) taskAdcVrefSample();
File diff suppressed because it is too large Load Diff