Compare commits

...
5 Commits
Author SHA1 Message Date
cbb 28d8426052 feat(ethercat): 添加ESP32使能控制功能通过EtherCAT SDO
添加0x2503对象字典项用于通过EtherCAT SDO控制ESP32 WiFi使能信号,
解决WiFi关断后无法重新开启的问题。同时添加sys/esp32_en.h/.cpp文件
提供GPIO控制接口。

SPI诊断命令(0xA2)扩展支持ESP32 EN引脚控制,用于诊断备援。
ESP32掉电重启后自动清理SPI链路状态,防止通信异常。
2026-09-24 03:18:18 +08:00
cbb 5fd986299c 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、评审过程稿
2026-09-20 22:58:09 +08:00
cbb 63af85e7be feat(ethercat): 1.6.40 起板探针改 BYTE_TEST 放行, 根治 warm jump 后 EtherCAT 降级 2026-09-16 01:54:25 +08:00
cbb 7d9d9c70a1 feat(app): 1.6.39 —— 1.6.29 起未上板业务改动入库,启动链与已提交基线对齐
主体为工作区累积的业务改动(CAN/PDO/桥/SSC/task_manager 等,详目见
config.h 版本注释链),新增 CoE 0x100A 真版本上报与 0x2502 APP→BOOT
总线交接。启动链(startup/SystemInit/initial/systick/hardfault)与
HEAD 逐字节一致;1.6.35~38 的跳转诊断实验(3M 化/BKP 面包屑/
haltWithWatchdog)未提交、已整体回退,补丁存 _revert_backup_20260916/。
真机实测(配对 BOOT 1.1.0.18):刷写 warm jump 与 reset cold jump 均可。
2026-09-16 00:48:34 +08:00
cbb 6136c2363f feat(ethercat): 添加LAN9252睡眠挂起机制防止HBI访问冲突
添加了s_pmSuspended状态标志来管理LAN9252的电源管理模式,在芯片进入D1-D3
睡眠状态时阻止HBI访问以避免唤醒问题。同步修改了ethercat_sync中断处理程序,
在睡眠挂起期间跳过HBI相关操作确保芯片能够正常维持睡眠状态。

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

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

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

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

chore(pdo): 更新PDO输入字节注释说明布局变更历史
2026-09-13 18:59:57 +08:00
44 changed files with 3782 additions and 825 deletions
+20
View File
@@ -85,3 +85,23 @@ uv2csolution.log
# legacy intermediate project file # legacy intermediate project file
*.cprj *.cprj
# Verification & Test suite process artifacts
/tests/
/.agents/
TEST_READY.md
TEST_READY.html
convert_md_to_html.py
*.html
# IDE / 多智能体过程产物
.trae/
.worktrees/
.qoder/
.zcode/
docs/traework-import/
# 本机固件 keep / 安全评审过程稿
firmware_keep/
SECURITY_AUDIT_REPORT.*
ORIGINAL_REQUEST.md
@@ -27,6 +27,8 @@ project:
- file: ./sys/hardfault.cpp - file: ./sys/hardfault.cpp
- file: ./sys/cpp_stubs.cpp - file: ./sys/cpp_stubs.cpp
- file: ./sys/config.h - file: ./sys/config.h
- file: ./sys/esp32_en.h
- file: ./sys/esp32_en.cpp
- group: device - group: device
files: files:
- file: ./device/adc.h - file: ./device/adc.h
+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);
+107 -65
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)。 */
@@ -228,13 +226,13 @@ bool EscLan9252::s_busReady = false;
bool EscLan9252::s_coreReady = false; bool EscLan9252::s_coreReady = false;
bool EscLan9252::s_resetDone = false; bool EscLan9252::s_resetDone = false;
bool EscLan9252::s_pmPhyDown = false; bool EscLan9252::s_pmPhyDown = false;
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 模式)。
* 释放后保持推挽输出高:空闲电平确定,并保留运行期软件复位手段; * 释放后保持推挽输出高:空闲电平确定,并保留运行期软件复位手段;
@@ -245,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 全浮空 + 晶振照振。 */
@@ -254,6 +255,8 @@ 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),挂起门控随之解除 */
s_pmSuspended = false;
} }
uint8_t EscLan9252::nrstDrive(uint8_t action) uint8_t EscLan9252::nrstDrive(uint8_t action)
@@ -266,6 +269,8 @@ 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,睡眠挂起随之解除 */
s_pmSuspended = false;
break; break;
case 1: /* 持续拉低(手动保持复位) */ case 1: /* 持续拉低(手动保持复位) */
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,
@@ -275,6 +280,7 @@ uint8_t EscLan9252::nrstDrive(uint8_t action)
case 3: /* 释放:浮空输入,网络交给 R44 10k 上拉 */ case 3: /* 释放:浮空输入,网络交给 R44 10k 上拉 */
gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_IN_FLOATING, 0, gpio_init(CONFIG_ESC_NRST_PORT, GPIO_MODE_IN_FLOATING, 0,
CONFIG_ESC_NRST_PIN); CONFIG_ESC_NRST_PIN);
s_pmSuspended = false;
break; break;
case 4: /* 只读回,不改驱动 */ case 4: /* 只读回,不改驱动 */
break; break;
@@ -311,10 +317,16 @@ 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 唤醒超时后
* 芯片延迟自醒)则自愈解除,恢复主循环/ISR 的 HBI 访问。 */
if ((status & 0x02U) == 0U && s_pmSuspended) {
s_pmSuspended = false;
printf("[ECAT][PM] status: chip in D0, pm suspend gate released\r\n");
}
printf("[ECAT][PM] status: PMT_CTRL=0x%08lX READY=%u SLEEP_EN=%u MODE=D%u\r\n", printf("[ECAT][PM] status: PMT_CTRL=0x%08lX READY=%u SLEEP_EN=%u MODE=D%u\r\n",
(unsigned long)pmt, (unsigned long)pmt,
(unsigned)((status >> 0) & 1U), (unsigned)((status >> 1) & 1U), (unsigned)((status >> 0) & 1U), (unsigned)((status >> 1) & 1U),
@@ -332,10 +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.24 实测:进入睡眠后任何 HBI 访问(含读!)都会立即触发唤醒—— /* 立即挂起全部 HBI 消费者(task_runner 停 MainLoop/守护/心跳,
* ethercat_sync 三 ISR 清 pending 后直接返回)——本函数运行于 CS EXTI ISR,
* 退出后主循环第一圈 ECAT_Main 读 0x220 就会把刚睡着的芯片叫醒;
* 此前置位窗口内本 ISR 是唯一运行者(maskEnter 已屏 EXTI0/1),无竞态。 */
s_pmSuspended = true;
/* 进入睡眠后任何 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 */
@@ -372,6 +388,12 @@ uint8_t EscLan9252::pmDrive(uint8_t op)
printf("[ECAT][PM] wake: PHY power-up %s (autoneg re-negotiating)\r\n", printf("[ECAT][PM] wake: PHY power-up %s (autoneg re-negotiating)\r\n",
restored ? "OK" : "FAIL (retry action 34)"); restored ? "OK" : "FAIL (retry action 34)");
} }
if (awake) {
/* 唤醒成功:解除 HBI 挂起,主循环/ISR 恢复运行。
* 超时则保持挂起——芯片大概率仍睡着;若实际已醒,下一次 op0
* 状态查询读到 D0 会自愈解除。 */
s_pmSuspended = false;
}
status = awake ? pmPackStatus(readReg32(LAN9252_REG_PMT_CTRL)) : 0U; status = awake ? pmPackStatus(readReg32(LAN9252_REG_PMT_CTRL)) : 0U;
printf("[ECAT][PM] wake: %s -> status=0x%02X (READY=%u SLEEP_EN=%u)\r\n", printf("[ECAT][PM] wake: %s -> status=0x%02X (READY=%u SLEEP_EN=%u)\r\n",
awake ? "OK" : "TIMEOUT (10ms, poll action 30 / retry 34)", awake ? "OK" : "TIMEOUT (10ms, poll action 30 / retry 34)",
@@ -384,61 +406,87 @@ uint8_t EscLan9252::pmDrive(uint8_t op)
bool EscLan9252::init() bool EscLan9252::init()
{ {
s_busReady = false; /* 1.5.27:s_busReady 语义 = init 完整成功(EXMC+探针+BYTE_TEST) */ 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;
} }
/* HBI 存活探针(数据手册 9.5.4.2):先直接读 HBI_IDX_0(offset 0,不写任何东西), uint16_t idx0 = 0U;
* 其复位默认值 = 0x1234。RST# 释放后 ESC 需完成内部启动与 EEPROM 加载, uint32_t byteTest = 0U;
* 故按手册建议轮询(此前为单次读,复位后立即读可能仍在启动中)。
* 若超时仍 != 0x1234(通常是 0x0000/0xFFFF 或上次写入的回显), /* 第 0 轮:不复位直接探。warm 残值下 BOOT 刚写过 HBI,BYTE_TEST 应立刻可读。 */
* 说明 LAN9252 完全没有驱动总线——CS 未拉低、CHIP_MODE strap 非 00(三端口=SPI only) idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
* 或芯片未出复位/未上电,而不是 EEPROM 问题(HBI 在 EEPROM 空白时也工作)。 */ byteTest = readReg32(LAN9252_REG_BYTE_TEST);
uint16_t idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET); if (byteTest == LAN9252_BYTE_TEST_VALUE) {
printf("[ECAT][ESC] probe: BYTE_TEST ok without RST# (idx0=0x%04X) — 总线已活,跳过复位\r\n",
(unsigned)idx0);
s_resetDone = true;
} else {
printf("[ECAT][ESC] probe: BYTE_TEST=0x%08lX idx0=0x%04X, 补 RST# 最多 3 轮\r\n",
(unsigned long)byteTest, (unsigned)idx0);
bool alive = false;
for (uint8_t attempt = 0U; attempt < 3U; ++attempt) {
if (!s_resetDone || attempt > 0U) {
hardwareReset();
s_resetDone = true;
}
/* 复位后重配 EXMC:BOOT 关时钟时 PD/PE 停在 AF,MCU 侧寄存器也可能残留 */
if (!EscExmcBus::init()) {
continue;
}
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
/* 仅在「像复位中/未驱动」时轮询 0x1234;warm 残值直接测 BYTE_TEST */
const bool idxLooksIdle = (idx0 == 0x0000U) || (idx0 == 0xFFFFU);
if (idxLooksIdle || idx0 == 0x1234U) {
for (uint32_t waited = 0U; for (uint32_t waited = 0U;
idx0 != 0x1234U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS; idx0 != 0x1234U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS;
waited += CONFIG_ESC_HBI_POLL_STEP_MS) { waited += CONFIG_ESC_HBI_POLL_STEP_MS) {
SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS); SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS);
idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET); idx0 = EscExmcBus::read16(HBI_IDX0_OFFSET);
} }
if (idx0 != 0x1234U) { }
printf("[ECAT][ESC] init: FAIL, HBI_IDX_0=0x%04X (expect 0x1234) -> LAN not driving bus\r\n", byteTest = readReg32(LAN9252_REG_BYTE_TEST);
(unsigned)idx0); 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);
}
if (!alive) {
printf("[ECAT][ESC] init: FAIL, BYTE_TEST=0x%08lX (expect 0x87654321), idx0=0x%04X -> LAN not driving bus\r\n",
(unsigned long)byteTest, (unsigned)idx0);
printf("[ECAT][ESC] hint: check CS(PD7) pulse, CHIP_MODE0/1 straps=00, RST#/power\r\n"); printf("[ECAT][ESC] hint: check CS(PD7) pulse, CHIP_MODE0/1 straps=00, RST#/power\r\n");
return false; return false;
} }
/* 再读 BYTE_TEST(0x064)确认接口完全就绪,默认 0x87654321(小端)。
* 该值只有 HBI_DATA_0 偏移正确(EXMC 16-bit 地址左移 1 位)才可能读到。 */
const uint32_t byteTest = readReg32(LAN9252_REG_BYTE_TEST);
if (byteTest != LAN9252_BYTE_TEST_VALUE) {
printf("[ECAT][ESC] init: FAIL, BYTE_TEST=0x%08lX (expect 0x87654321)\r\n",
(unsigned long)byteTest);
printf("[ECAT][ESC] hint: IDX probe passed but DATA access failed -> check HBI_DATA_0 offsets (0x40000/0x60000)\r\n");
return false;
} }
printf("[ECAT][ESC] init: OK, HBI alive (IDX=0x1234, BYTE_TEST=0x87654321)\r\n");
if (idx0 != 0x1234U) {
printf("[ECAT][ESC] init: WARN idx0=0x%04X != 0x1234, BYTE_TEST ok -> 复位态未知但通路可用,放行\r\n",
(unsigned)idx0);
}
printf("[ECAT][ESC] init: OK, HBI alive (idx0=0x%04X, BYTE_TEST=0x87654321)\r\n",
(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);
@@ -466,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)。
@@ -478,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,只延长步数;
@@ -519,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;
@@ -568,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;
} }
@@ -603,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;
} }
@@ -640,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);
} }
+26 -14
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);
@@ -92,32 +92,44 @@ public:
/** @brief 过程 RAM(0x1000-0x1FFF)经 PRAM 写 FIFO 写入 len 字节。 */ /** @brief 过程 RAM(0x1000-0x1FFF)经 PRAM 写 FIFO 写入 len 字节。 */
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 总线 + HBI_IDX_0 探针 + BYTE_TEST 全部通过(init 成功)后为 true。 /** @brief EXMC 总线 + BYTE_TEST 往返探针通过(init 成功)后为 true。
* @note 与 init() 返回值一致,可作运行期查询。 */ * @note HBI_IDX_0 复位默认值仅作诊断读数,不参与放行
* (它只证明"芯片刚出过复位",证明不了 HBI 通路)。
* 与 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 消费者必须静默。
* @note 睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。置位期间
* task_runner 停调 MainLoop()/AL 守护/AL 心跳,ethercat_sync 三个 EXTI
* ISR 清 pending 后直接返回——否则芯片进入睡眠后毫秒级就被主循环的
* ECAT_Main(读 0x220)叫醒,根本睡不住。
* 解除:pmDrive 唤醒/读到 D0、nrstDrive 释放复位、hardwareReset。
* ISR 上下文安全:读一个 bool 静态成员,无锁。 */
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+IDX 探针+BYTE_TEST)后置位。 */ 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)。 */
}; };
#endif /* COMM_ESC_LAN9252_H */ #endif /* COMM_ESC_LAN9252_H */
+122 -18
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 的编译器/总线屏障 */
@@ -13,9 +14,13 @@
volatile uint8_t EthercatCanBridge::s_lastSeq = 0U; volatile uint8_t EthercatCanBridge::s_lastSeq = 0U;
volatile bool EthercatCanBridge::s_seqPrimed = false; volatile bool EthercatCanBridge::s_seqPrimed = false;
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;
@@ -27,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};
@@ -97,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) {
@@ -169,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;
} }
@@ -215,17 +237,19 @@ 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;
return false; s_coldStartPending = true;
return true; /* 冷启动首包强制全量读,避免漏掉首帧命令 */
} }
if (seq != s_lastSeq) { if (seq != s_lastSeq) {
@@ -234,6 +258,13 @@ bool EthercatCanBridge::peekOutput(const uint8_t *head)
return (head[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U; return (head[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U;
} }
void EthercatCanBridge::resetSequenceState()
{
s_seqPrimed = false;
s_lastSeq = 0U;
s_coldStartPending = false;
}
void EthercatCanBridge::processOutput(const uint8_t *out) void EthercatCanBridge::processOutput(const uint8_t *out)
{ {
if (out == nullptr) { if (out == nullptr) {
@@ -241,27 +272,49 @@ 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;
return;
} }
/* 无论 seq 是否变化,优先处理非 seq 驱动的命令(FLAGS bit0 清统计) */
if ((out[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U) { if ((out[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U) {
s_txCount = 0U; s_txCount = 0U;
s_dropCount = 0U; s_dropCount = 0U;
s_rxCount = 0U; s_rxCount = 0U;
s_rx00ffCount = 0U;
s_rxPublished = true; s_rxPublished = true;
} }
if (seq == s_lastSeq) { const uint8_t dlcRaw = out[ECAT_CAN_TX_OFF_DLC];
const uint8_t batchCount = out[ECAT_TXB_OFF_COUNT];
const bool hasPayload = (dlcRaw > 0U) || (batchCount > 0U);
bool executeCommand = false;
if (s_coldStartPending) {
s_coldStartPending = false;
/* 冷启动:有载荷就发(不依赖 s_lastSeq 是否已被探针碰过) */
if (hasPayload) {
executeCommand = true;
} else {
s_lastSeq = seq;
return;
}
} else if (seq != s_lastSeq) {
executeCommand = true;
} else if (hasPayload && seq != s_lastSeq) {
executeCommand = true;
}
if (!executeCommand) {
return; return;
} }
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;
} }
@@ -276,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]);
} }
} }
} }
@@ -288,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;
@@ -296,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);
@@ -310,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 会淹没一切)。
* 例外:仅当刚收到 0x00FF 的短粘滞窗口内,优先摆版本应答,
* 给主站一个稳定采样窗;窗口结束后立刻交还给 CONFIG 等,避免其它长帧失效。 */
if (xrxStable) {
const uint32_t nowMs = SysTickTimer::getMs();
const bool stickyLive = s_verValid && static_cast<int32_t>(nowMs - s_verHoldUntilMs) < 0;
/* 段3 RSVD@547:processOutput 看到的输出 seq(诊断用,不参与解析) */
in[ECAT_XRX_OFF_RSVD] = s_outSeenSeq;
if (stickyLive) {
in[ECAT_XRX_OFF_CH] = s_verCh;
in[ECAT_XRX_OFF_DLC] = s_verDlc;
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
wr32le(&in[ECAT_XRX_OFF_ID], s_verId);
memcpy(&in[ECAT_XRX_OFF_DATA], s_verData, kEcatCanXRxDataMax);
} else if (s_rxValid) {
in[ECAT_XRX_OFF_CH] = s_rxCh; in[ECAT_XRX_OFF_CH] = s_rxCh;
in[ECAT_XRX_OFF_DLC] = s_rxDlc; in[ECAT_XRX_OFF_DLC] = s_rxDlc;
in[ECAT_XRX_OFF_FLAGS] = 0x01U; in[ECAT_XRX_OFF_FLAGS] = 0x01U;
wr32le(&in[ECAT_XRX_OFF_ID], s_rxId); wr32le(&in[ECAT_XRX_OFF_ID], s_rxId);
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax); 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,
@@ -339,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;
@@ -365,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" {
@@ -384,4 +483,9 @@ void EcatCan_BuildInput(uint8_t *in)
EthercatCanBridge::buildInput(in); EthercatCanBridge::buildInput(in);
} }
void EcatCan_ResetSequenceState(void)
{
EthercatCanBridge::resetSequenceState();
}
} /* extern "C" */ } /* extern "C" */
+40 -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,11 +190,14 @@ 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);
/** @brief 复位输出 sequence 与冷启动同步状态。 */
static void resetSequenceState();
static void processOutput(const uint8_t *out); static void processOutput(const uint8_t *out);
/** /**
@@ -227,11 +226,20 @@ private:
static volatile uint8_t s_lastSeq; static volatile uint8_t s_lastSeq;
static volatile bool s_seqPrimed; static volatile bool s_seqPrimed;
static volatile bool s_coldStartPending;
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;
@@ -242,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。 */
@@ -259,6 +276,8 @@ extern "C" {
void EcatCan_ProcessOutput(const uint8_t *out); void EcatCan_ProcessOutput(const uint8_t *out);
/** @brief 供 dc_app.c 调用的 shim:填充输入 PDO。 */ /** @brief 供 dc_app.c 调用的 shim:填充输入 PDO。 */
void EcatCan_BuildInput(uint8_t *in); void EcatCan_BuildInput(uint8_t *in);
/** @brief 供 dc_app.c 调用的 shim:复位输出 sequence 状态机。 */
void EcatCan_ResetSequenceState(void);
} }
#endif /* COMM_ETHERCAT_CAN_BRIDGE_H */ #endif /* COMM_ETHERCAT_CAN_BRIDGE_H */
+142 -46
View File
@@ -1,12 +1,15 @@
/** /**
* @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"
#include "comm/esc_exmc_bus.h"
#include "device/canfd.h" #include "device/canfd.h"
#include "sys/config.h" #include "sys/config.h"
#include "sys/cpu_load.h" #include "sys/cpu_load.h"
#include "RTE_Components.h"
#include CMSIS_device_header /* __DMB() */
/* [DIFF-STATS] 临时仪表挂接(删除:去掉本块与 process() 内的 observe 块)。 /* [DIFF-STATS] 临时仪表挂接(删除:去掉本块与 process() 内的 observe 块)。
* 上一行的 sys/config.h 必须在此之前:宏未定义时 #if 会静默按 0 求值, * 上一行的 sys/config.h 必须在此之前:宏未定义时 #if 会静默按 0 求值,
* 开关就会被悄悄当成"关掉"。 */ * 开关就会被悄悄当成"关掉"。 */
@@ -16,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};
@@ -23,6 +33,12 @@ alignas(4) uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0};
/* 输入 PDO 的工作缓冲:buildInput 填这里,填完整块发布到 s_in(见 process())。 /* 输入 PDO 的工作缓冲:buildInput 填这里,填完整块发布到 s_in(见 process())。
* 读侧 writeInputs 只读 s_in,因此永远看不到"填到一半"的镜像。 */ * 读侧 writeInputs 只读 s_in,因此永远看不到"填到一半"的镜像。 */
alignas(4) uint8_t EthercatPdo::s_build[EthercatPdo::kPdoInBytes] = {0}; alignas(4) uint8_t EthercatPdo::s_build[EthercatPdo::kPdoInBytes] = {0};
namespace {
/* 待交回主站的输入数据双缓冲(ping-pong,配合 __DMB() 保证原子发布与 tear-free 读写) */
alignas(4) uint8_t s_inBuf[2][EthercatPdo::kPdoInBytes] = {{0}, {0}};
volatile uint8_t s_inActiveIdx = 0U;
}
alignas(4) uint8_t EthercatPdo::s_escSh1[32] = {0}; alignas(4) uint8_t EthercatPdo::s_escSh1[32] = {0};
alignas(4) uint8_t EthercatPdo::s_escSh2[ECAT_XRX_OFF_CH - 32U] = {0}; alignas(4) uint8_t EthercatPdo::s_escSh2[ECAT_XRX_OFF_CH - 32U] = {0};
alignas(4) uint8_t EthercatPdo::s_escSh3[kEcatPdoInBytes - ECAT_XRX_OFF_CH] = {0}; alignas(4) uint8_t EthercatPdo::s_escSh3[kEcatPdoInBytes - ECAT_XRX_OFF_CH] = {0};
@@ -36,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)
{ {
@@ -53,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 是周期量,跳过一次下一拍即补,
* 远好过发出一份跨轮次拼接的镜像。 */ * 远好过发出一份跨轮次拼接的镜像。 */
@@ -90,34 +102,73 @@ void EthercatPdo::process()
* 内层被门挡住后,它随后的 PDO_InputMapping() 仍会去读 s_in,若 buildInput * 内层被门挡住后,它随后的 PDO_InputMapping() 仍会去读 s_in,若 buildInput
* 直接写 s_in,读到的就是外层写到一半的半成品。填在别处、整块发布之后, * 直接写 s_in,读到的就是外层写到一半的半成品。填在别处、整块发布之后,
* 读侧要么拿到上一份完整镜像、要么拿到这一份完整镜像,不存在中间态。 */ * 读侧要么拿到上一份完整镜像、要么拿到这一份完整镜像,不存在中间态。 */
/* 双缓冲发布:填入非活跃槽 nextIdx,完成后通过 __DMB() 与 s_inActiveIdx 切换
* 实现原子发布,彻底避免中断读侧(PDI_Isr / Sync0_Isr)与主循环之间的读写撕裂。 */
const uint8_t nextIdx = 1U - s_inActiveIdx;
uint8_t *build = s_inBuf[nextIdx];
/* 输出兜底全量读:仅当 OutputMapping 本拍未刷新 s_out,且
* (s_out 看起来是死的 或 距上次强制读已跑过一段 process) 才读。
* 每拍 process() 都 HW_EscRead 384B 会与 PDO_OutputMapping 叠成双份 HBI,
* PDO 2kHz 即把 ISR 成本打满(历史探针版 4kHz load≈80%)。 */
static uint32_t s_lastForceReadSeq = 0U;
if (nPdOutputSize > 0U && nEscAddrOutputData != 0U && !s_outMappedThisCycle) {
const bool outDead = (s_out[0] == 0U && s_out[1] == 0U && s_out[8] == 0U);
const uint32_t seqNow = s_processSeq;
/* 仅 s_out 疑似死数据时强制全量读;探针已处理 seq 沿。
* 按拍数兜底间隔拉到 256,避免 8kHz 下周期性 384B HBI。 */
if (outDead || (uint32_t)(seqNow - s_lastForceReadSeq) >= 256U) {
const uint16_t nread = (nPdOutputSize > kPdoOutBytes)
? kPdoOutBytes
: nPdOutputSize;
uint8_t escOut[kEcatPdoOutBytes];
const uint16_t prefix = (nread < 52U) ? nread : 52U;
HW_EscRead(escOut, nEscAddrOutputData, prefix);
const uint8_t batchCount = (prefix > 48U) ? escOut[48] : 0U;
if (batchCount > 0U && nread > prefix) {
HW_EscRead(&escOut[prefix],
(uint16_t)(nEscAddrOutputData + prefix),
(uint16_t)(nread - prefix));
}
readOutputs(escOut);
s_lastForceReadSeq = seqNow;
}
}
s_outMappedThisCycle = false;
EthercatCanBridge::processOutput(s_out); EthercatCanBridge::processOutput(s_out);
EthercatCanBridge::buildInput(s_build); EthercatCanBridge::buildInput(build);
fillDiagTail(s_build); fillDiagTail(build);
memcpy(s_in, s_build, kPdoInBytes);
__DMB();
s_inActiveIdx = nextIdx;
/* 不再 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_in);
#endif #endif
s_building = false; s_building = false;
} }
/** /**
* @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;
@@ -125,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;
@@ -159,7 +210,9 @@ void EthercatPdo::writeInputs(uint8_t *esc)
if (esc == nullptr) { if (esc == nullptr) {
return; return;
} }
memcpy(esc, s_in, kPdoInBytes); const uint8_t activeIdx = s_inActiveIdx;
__DMB();
memcpy(esc, s_inBuf[activeIdx], kPdoInBytes);
++s_inputCycles; ++s_inputCycles;
} }
@@ -172,12 +225,15 @@ 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相接 → 整包,避免陈旧空洞被误读。 */
if (s_in[ECAT_MOTW_OFF_MAGIC] != kEcatMotLayoutMagic) { const uint8_t activeIdx = s_inActiveIdx;
__DMB();
const uint8_t *cur = s_inBuf[activeIdx];
if (cur[ECAT_MOTW_OFF_MAGIC] != kEcatMotLayoutMagic) {
return false; return false;
} }
const uint8_t count = s_in[ECAT_MOTW_OFF_COUNT]; const uint8_t count = cur[ECAT_MOTW_OFF_COUNT];
if (count > kEcatMotSlotsPerPdo) { if (count > kEcatMotSlotsPerPdo) {
return false; return false;
} }
@@ -253,19 +309,25 @@ uint8_t EthercatPdo::escWritePlan(uint16_t *offs, uint16_t *lens)
return n; return n;
#else #else
/* 三区差分:段1 桥状态 / 段2 电机密排(稀疏写有效长) / 段3+5 快照诊断。 /* 三区差分:段1 桥状态 / 段2 电机密排(稀疏写有效长) / 段3+5 快照诊断。
* 段1 计数器几乎每拍变,段2 稳定时省最大头,段3+5 多数拍稳定。 */ * 段1 计数器几乎每拍变,段2 稳定时省最大头,段3+5 多数拍稳定。
* 无论各段是否变化,必须保证每周期必定写入 SM3 结束地址 (kPdoInBytes - 1),
* 以触发 LAN9252 3-buffer 提交换冲,否则主站将读到冻结的旧缓冲。 */
const uint8_t activeIdx = s_inActiveIdx;
__DMB();
const uint8_t *cur = s_inBuf[activeIdx];
uint8_t n = 0U; uint8_t n = 0U;
if (diffNeedWrite(s_in, 0U, 32U, s_escSh1, s_escSh1Valid, s_escFlush1)) { if (diffNeedWrite(cur, 0U, 32U, s_escSh1, s_escSh1Valid, s_escFlush1)) {
offs[n] = 0U; offs[n] = 0U;
lens[n] = 32U; lens[n] = 32U;
++n; ++n;
} }
if (diffNeedWrite(s_in, 32U, (uint16_t)(ECAT_XRX_OFF_CH - 32U), if (diffNeedWrite(cur, 32U, (uint16_t)(ECAT_XRX_OFF_CH - 32U),
s_escSh2, s_escSh2Valid, s_escFlush2)) { s_escSh2, s_escSh2Valid, s_escFlush2)) {
offs[n] = 32U; offs[n] = 32U;
/* 写长:稀疏时只写到 count*56,但影子比对的是完整 32..543(含清零空洞)。 /* 写长:稀疏时只写到 count*56,但影子比对的是完整 32..543(含清零空洞)。
* 空洞不写入 ESC 无妨:主站只按 count 迭代。 */ * 空洞不写入 ESC 无妨:主站只按 count 迭代。 */
const uint16_t wlen = escSeg2WriteLen(s_in); const uint16_t wlen = escSeg2WriteLen(cur);
lens[n] = (wlen > 32U) ? (uint16_t)(wlen - 32U) : 0U; lens[n] = (wlen > 32U) ? (uint16_t)(wlen - 32U) : 0U;
if (lens[n] == 0U) { if (lens[n] == 0U) {
/* count=0:段2 头 8 字节仍要写(count/active/magic) */ /* count=0:段2 头 8 字节仍要写(count/active/magic) */
@@ -273,16 +335,19 @@ uint8_t EthercatPdo::escWritePlan(uint16_t *offs, uint16_t *lens)
} }
++n; ++n;
} }
if (diffNeedWrite(s_in, ECAT_XRX_OFF_CH, (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH), bool seg3Included = false;
if (diffNeedWrite(cur, ECAT_XRX_OFF_CH, (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH),
s_escSh3, s_escSh3Valid, s_escFlush3)) { s_escSh3, s_escSh3Valid, s_escFlush3)) {
offs[n] = ECAT_XRX_OFF_CH; offs[n] = ECAT_XRX_OFF_CH;
lens[n] = (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH); lens[n] = (uint16_t)(kPdoInBytes - ECAT_XRX_OFF_CH);
++n; ++n;
seg3Included = true;
} }
if (n == 0U) { if (!seg3Included) {
offs[0] = (uint16_t)(kPdoInBytes - 2U); /* 段 3 未包含时,补写末 2 字节以保证触碰 SM3 结束地址触发硬件提交 */
lens[0] = 2U; offs[n] = (uint16_t)(kPdoInBytes - 2U);
n = 1U; lens[n] = 2U;
++n;
} }
return n; return n;
#endif #endif
@@ -298,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)
{ {
@@ -310,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();
@@ -320,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)
@@ -328,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
@@ -348,4 +418,30 @@ uint8_t EcatPdo_EscWritePlan(uint16_t *offs, uint16_t *lens, uint8_t maxSegs)
return n; return n;
} }
/* HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
uint32_t EscExmcBus_MaskEnter(void)
{
return EscExmcBus::maskEnter();
}
void EscExmcBus_MaskExit(uint32_t prev)
{
EscExmcBus::maskExit(prev);
}
uint32_t HW_EscExmcMaskEnter(void)
{
return EscExmcBus::maskEnter();
}
void HW_EscExmcMaskExit(uint32_t 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 */
+4 -33
View File
@@ -52,40 +52,11 @@
/** /**
* 输入过程数据字节数(从站→主站,SM3):75×64bit = 600B。 * 输入过程数据字节数(从站→主站,SM3):75×64bit = 600B。
* 布局(1.6.21 密排):段1..2 0..543 · 段3 544..583 · 段5 584..595。
* 改长度须同步 bridge/dc_app/gen_eeprom/ESI/主站 decode,并烧 EEPROM 冷启动。
* *
* ── 1.6.1 瘦身 640 → 496(2026-09-05)── * 注意:DWT sub10 量的是从站 PDI 写 ESC 耗时,**不是**主站 rx 段。
* 固件 DWT 探针(0x2000 sub9/sub10)实测:PDO_InputMapping 里 HW_EscWriteIsr * 主站 rx 主要由线速往返决定(约 1KB @100Mbit ≈ 80µs 量级),两边 µs 不要对拍。
* 640B 写恒为 296µs/拍(29.7% CPU),且与 SYNC0 相位**无关**(主站 dc_bench
* 五形态对拍极差 0.04%)——PRAM 通道 ≈0.463µs/B 线性,写多少省多少,唯一的
* 减法就是减字节。真机 CAN 流量采样(6086 帧)DLC 只有 24/32 两档,批量接收
* 区每帧 64B 数据是纯富余:条目数据 64→32B(DLC 真值保留,超长帧截断、
* flags bit1 标记),诊断区前移,段 1/电机窗口/长帧快照全部不动。
*
* ── 1.6.15 扩到 600:批量接收改定长覆盖窗口(2026-09-08)──
* 瘦身后实测 496B 写 218.7µs/拍(21.9%)。批量区当时是"变长紧排 + 环形队列
* 排空",容量 3 帧/拍(=3000 帧/s),现场 4000 帧/s 下每拍恒丢 1 帧,且排空赤字
* 把队列长期压在饱和态——1.6.9~1.6.13 追的错标/拼接竞态正是饱和态喂出来的
* (判满用的 tail 一旦滞后,生产者就会写进消费者刚读过的槽)。
*
* 本版把批量区改成**定长覆盖窗口**:生产者按自己的游标写、绕圈覆盖,消费者
* 每拍整块取值,不再有 tail/判满/pending。代价是每槽要带一个 u32 seqlock
* (PDI_Isr 会抢占主循环写侧,没有它主站就能拿到"新 ID + 旧数据"的假帧),
* 所以 5 槽的预算是 5×44 而不是 5×40:
* 段1..3 0..359(不动) + 头 4B(360..363) + 5×44B(364..583)
* + 段5 诊断 12B(584..595) + 保留 4B = 600B
* 5 槽 = 5000 帧/s,对 4000 帧/s 留出 25% 余量;写侧 600×0.441 ≈ 265µs/拍
* (26.5%,仍远好于旧 640B 的 296µs,更是 4kHz 周期的 87.5% 占空不可行方案的
* 零头,见 tools/ecat/dc_bench.py 固化结论)。
*
* 覆盖窗口**不创造容量**:PDO 每拍能带几帧只由字节数决定,要接住 N 帧/拍,
* 槽数必须 ≥ N,否则一帧在两次拷贝之间就被覆盖掉(改完可看主站按 seq 空洞
* 算出的丢帧数与从站侧 OVF 是否一致)。
* 600 = 75×8;SM3 实占 3×600=1800B(0x1880-0x1F67),out+in=984 ≤ 1024 硬上限,
* DPRAM 仍留 120B 余量。改这里必须同步重生成 SII(gen_eeprom.py)并断电冷启动。
*
* ── 1.6.21:总长仍 600B,内部布局改「电机整轮密排」──
* 段 2 扩到 32..543(头 8B + 最多 9×56B),段 3 前移到 544..583,段 5 仍 584。
* 删段 4 历史覆盖窗。长度不变 ⇒ SII/EEPROM 无需重刷;主站解码偏移必须同步。
*/ */
#define ECAT_PDO_IN_BYTES 600 #define ECAT_PDO_IN_BYTES 600
+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 探针与诊断查询。
*/ */
+20 -4
View File
@@ -7,6 +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 */
#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"
@@ -114,6 +115,12 @@ 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;
/* ESC 睡眠挂起期,清了 pending 就走——Sync0_Isr 内的 PRAM/CSR
* 访问全是 HBI,读即唤醒。睡眠中 SYNC0 引脚本应无沿,此分支只兜
* 进入睡眠瞬间的残留 pending。 */
if (EscLan9252::pmSuspended()) {
return;
}
++s_sync0Count; ++s_sync0Count;
Sync0_Isr(); Sync0_Isr();
} }
@@ -123,6 +130,10 @@ 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);
/* ESC 睡眠挂起期直接返回——PDI_Isr 会读 0x220/PRAM(HBI),读即唤醒 */
if (EscLan9252::pmSuspended()) {
return;
}
++s_escIrqCount; ++s_escIrqCount;
/* PDI_Isr(preempt=1) 可被 Sync0_Isr(preempt=0) 抢占;HBI Indexed 是两步 /* PDI_Isr(preempt=1) 可被 Sync0_Isr(preempt=0) 抢占;HBI Indexed 是两步
* 事务(IDX 写 + DATA 读),被抢占即错址。此处屏蔽 EXTI0 实现互斥: * 事务(IDX 写 + DATA 读),被抢占即错址。此处屏蔽 EXTI0 实现互斥:
@@ -130,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);
@@ -144,6 +152,10 @@ 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);
/* ESC 睡眠挂起期直接返回——Sync1_Isr 内为 HBI 访问 */
if (EscLan9252::pmSuspended()) {
return;
}
++s_sync1Count; ++s_sync1Count;
/* 同 onEscIrqExti:Sync1_Isr(preempt=1) 内的 HBI 访问需与 SYNC0 互斥 */ /* 同 onEscIrqExti:Sync1_Isr(preempt=1) 内的 HBI 访问需与 SYNC0 互斥 */
const uint32_t prevSync0 = NVIC_GetEnableIRQ(CONFIG_ESC_SYNC0_IRQn); const uint32_t prevSync0 = NVIC_GetEnableIRQ(CONFIG_ESC_SYNC0_IRQn);
@@ -160,8 +172,11 @@ 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);
/* ESC 睡眠挂起期跳过——Sync0_Isr 内为 HBI 访问 */
if (!EscLan9252::pmSuspended()) {
Sync0_Isr(); Sync0_Isr();
} }
}
#else #else
/* EXTI 模式下 TIMER0 不使用,本函数为占位;CONFIG_ETHERCAT_SYNC_MODE=1 时激活。 */ /* EXTI 模式下 TIMER0 不使用,本函数为占位;CONFIG_ETHERCAT_SYNC_MODE=1 时激活。 */
#endif #endif
@@ -196,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" */
+272 -99
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,13 +146,31 @@ 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);
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim, 1.5.54)。 /* OP 输入新鲜度 — APPL_InputMapping 每次成功刷新就自增 */
* 1.5.53 时这里直接调 EcatCan_* 转发桥;1.5.54 把"一个 EtherCAT 周期怎么收发" static volatile UINT32 s_inMapSeq = 0U;
* 收拢进 EthercatPdo 类,本文件退回纯 SSC 移植层:只留对象字典 + 三行转发。 */ static UINT32 s_inMapSeqSeen = 0U;
static UINT32 s_inStaleBaseCyc = 0U;
static UINT32 s_opFreshRescues = 0U;
#include <stdint.h>
/* DFP 的 SystemCoreClock 是 uint32_t(=unsigned int);SSC 的 UINT32 是 unsigned long,
* 不能用 UINT32 重复声明。 */
extern uint32_t SystemCoreClock;
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim)。
* 本文件为纯 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)。
* 只置 pending 标志,真正的配置页写入 + 复位在主循环下一圈,见 ObjBootReqWrite 注。 */
extern void EcatEnterBootRequestAsync(void);
/* 0x2503 用:ESP32 EN(PC9) 控制(sys/esp32_en.cpp 的 extern "C" shim)。
* 经 EtherCAT SDO 下发,**不走 ESP32 WiFi**——WiFi 关断后主站仍能拉回使能。 */
extern UINT8 EcatEsp32EnDrive(UINT8 action);
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
* PDO Assign 对象 (0x1C12 / 0x1C13) * PDO Assign 对象 (0x1C12 / 0x1C13)
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
@@ -202,7 +217,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),
@@ -217,16 +232,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 同此)。
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
@@ -256,13 +266,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 计数 */
@@ -286,8 +296,101 @@ 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 输入新鲜度救援次数 */
}; };
/*---------------------------------------------------------------------------
* 0x2502 引导程序交接请求(主站 → APP:「让你回 BOOT」)
*
* 为什么要有它:EtherCAT 直灌的会话对象在 **BOOT** 里(0x2500/0x2501),APP 的字典
* 根本没有这两个对象 —— 实测对着 APP 写 0x2500 回 abort=0x06020000「对象不存在」。
* 所以一台正在跑 APP 的机器必须先交接,才能走总线刷写。量产那条交接只有
* SPI 0xB0 和 CAN 0x320+"OTA!",没有总线内的入口;常驻刷写必须补上。
*
* 语义:写 1 = 请求进 BOOT;写 0 是无操作(不做"取消",避免半路误清)。
* 动作复用现成的 OtaBootRequest 异步链:这里**只置 pending 标志**,配置页三字的
* Flash 写入 + NVIC_SystemReset 由主循环 OtaBootRequest::process() 下一圈执行
* (sys/initial.cpp 每圈调;与 SPI 0xB0 在 ISR 里只置标志同一个形状),
* 所以 SDO 响应先发出去、复位后到,不会把 SM1 半包留在总线上。
*
* 交接后板子会稳定停在 BOOT:APP 写的是 EN=NOT_AVAL,而 BOOT 的救砖门
* ecatArmed() = !(CHECK==AVAL && EN==AVAL) ⇒ 顺手就把 0x2500 直灌解锁了,
* 不需要 word8 令牌(也别依赖它:APP 的 otaConfigSet 只保全 8 个字,一擦页就没了)。
*---------------------------------------------------------------------------*/
OBJCONST TSDOINFOENTRYDESC OBJMEM sEntryDesc0x2502[] = {
/* VAR 只有 :00 一个条目,且必须带写权限位(SSC 的通用写权限门在 objdef.c:1325,
* 早于 1352 的回调分发 —— BOOT 的 0x2500 就是栽在只给 ACCESS_READ 上) */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE},
};
static OBJCONST UCHAR aName0x2502[] = "GD32 boot request";
static UINT32 s_bootReq = 0U; /**< 最近一次写入值,读回用于主站自证 */
static UINT8 ObjBootReqWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
{
const UINT8 *p8 = (const UINT8 *)pData;
UINT32 v;
(void)index;
(void)bCompleteAccess;
if (subindex != 0U) {
return ABORTIDX_SUBINDEX_NOT_EXISTING;
}
if (pData == NULL || dataSize < 4U) {
return ABORTIDX_PARAM_LENGTH_TOO_SHORT;
}
v = (UINT32)p8[0] | ((UINT32)p8[1] << 8) | ((UINT32)p8[2] << 16) | ((UINT32)p8[3] << 24);
s_bootReq = v;
if (v == 1U) {
EcatEnterBootRequestAsync();
}
return 0U;
}
/*---------------------------------------------------------------------------
* 0x2503 ESP32 WiFi EN(PC9 / WLESS_EN)控制(主站 → APP)
*
* 为什么走 EtherCAT 而不是 SPI 0xA2 / WiFi HTTP:WiFi 使能被拉低后 ESP32
* 当场掉线,经 /spi/send 的控制路径跟被控对象同生共死——关得掉、开不回来。
* EtherCAT 主站在 PC 侧,与 ESP32 完全解耦,关断/开启都能下发。
*
* 语义(UINT32 bit[7:0]=action,与 sys/esp32_en.h Esp32En::drive 对齐):
* 0=拉高(开) 1=拉低保持(关) 2=拉高 3=浮空 4=只读
* 读回:bit[7:0] = 实际引脚电平 0/1(0xEE=无效 action)。
*---------------------------------------------------------------------------*/
OBJCONST TSDOINFOENTRYDESC OBJMEM sEntryDesc0x2503[] = {
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READWRITE},
};
static OBJCONST UCHAR aName0x2503[] = "ESP32 WiFi EN";
static UINT32 s_esp32EnVal = 1U; /**< 读回缓存:默认 1=上电已释放 EN */
static UINT8 ObjEsp32EnWrite(UINT16 index, UINT8 subindex, UINT32 dataSize,
UINT16 MBXMEM *pData, UINT8 bCompleteAccess)
{
const UINT8 *p8 = (const UINT8 *)pData;
UINT32 v;
UINT8 action, st;
(void)index;
(void)bCompleteAccess;
if (subindex != 0U) {
return ABORTIDX_SUBINDEX_NOT_EXISTING;
}
if (pData == NULL || dataSize < 4U) {
return ABORTIDX_PARAM_LENGTH_TOO_SHORT;
}
v = (UINT32)p8[0] | ((UINT32)p8[1] << 8) | ((UINT32)p8[2] << 16) | ((UINT32)p8[3] << 24);
action = (UINT8)(v & 0xFFU);
st = EcatEsp32EnDrive(action);
s_esp32EnVal = (UINT32)st;
return 0U;
}
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
* Object Names * Object Names
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
@@ -329,10 +432,18 @@ 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 注) */
{NULL, NULL, 0x2502, {DEFTYPE_UNSIGNED32, 0 | (OBJCODE_VAR << 8)},
sEntryDesc0x2502, aName0x2502, &s_bootReq, NULL, ObjBootReqWrite, 0x0000},
/* 0x2503: ESP32 WiFi EN(PC9) —— 经 EtherCAT 控制,WiFi 关断后仍可拉回 */
{NULL, NULL, 0x2503, {DEFTYPE_UNSIGNED32, 0 | (OBJCODE_VAR << 8)},
sEntryDesc0x2503, aName0x2503, &s_esp32EnVal, NULL, ObjEsp32EnWrite, 0x0000},
/* 终止条目 */ /* 终止条目 */
{NULL, NULL, 0xFFFF, {0, 0}, NULL, NULL, NULL, NULL, NULL, 0x000} {NULL, NULL, 0xFFFF, {0, 0}, NULL, NULL, NULL, NULL, NULL, 0x000}
}; };
@@ -344,7 +455,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;
} }
@@ -360,12 +471,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 缓冲模式,
@@ -381,8 +492,17 @@ UINT16 APPL_StopInputHandler(void) { return 0; }
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
* Output Handler (SAFEOP→OP) * Output Handler (SAFEOP→OP)
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
UINT16 APPL_StartOutputHandler(void) { return 0; } UINT16 APPL_StartOutputHandler(void)
UINT16 APPL_StopOutputHandler(void) { return 0; } {
EcatCan_ResetSequenceState();
return 0;
}
UINT16 APPL_StopOutputHandler(void)
{
EcatCan_ResetSequenceState();
return 0;
}
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
* APPL_AckErrorInd * APPL_AckErrorInd
@@ -404,7 +524,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;
@@ -413,6 +533,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 读不会拿到半累积值。 */
@@ -432,11 +553,14 @@ 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);
/* OP 输入新鲜度不变量的活性计数。每次真正走到输入映射就自增,
* 供 EcatDbg_OpInputFreshness 判断 PDI 路径是否还活着。 */
++s_inMapSeq;
} }
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
@@ -453,11 +577,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();
} }
@@ -466,11 +588,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)
{ {
@@ -480,41 +600,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)
{ {
@@ -532,23 +640,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);
@@ -575,13 +671,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",
@@ -611,11 +705,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) {
@@ -632,3 +726,82 @@ UINT32 EcatDbg_GuardAleventMask(void)
} }
return actual; return actual;
} }
/*---------------------------------------------------------------------------
* OP 输入新鲜度不变量
*
* SM-Synchron 稳态下 MainLoop 的 free-run 分支被
* (!bEscIntEnabled || !bEcatFirstOutputsReceived) && !bDcSyncActive
* 整段跳过,输入 PDO **只**由 PDI_Isr 的 PDO_InputMapping 刷新。
* PDI 通路一停(门控/掩码/EXTI),ESC 仍答 WKC、AL 仍 OP,主站读到的是
* 冻住的旧缓冲。
*
* 不变量:「OP ⇒ 输入必须在 T ms 内被重写」。
* - 正常路径:Process 每拍自增 seq
* - 异常路径:seq 停涨超过阈值 → 主循环强制 PDO_InputMapping
* (屏中断防双写)+ 重开门/应答 SM2,主站侧 gseq/rx 立刻恢复前进
* 不依赖主站重拉、不依赖人工复位。MainLoop 若本身死锁则本函数也停 ——
* 那是另一类故障(心跳会停),不在本不变量覆盖范围。
*---------------------------------------------------------------------------*/
void EcatDbg_OpInputFreshness(void)
{
const UINT8 al = (UINT8)(nAlStatus & 0x0FU);
const UINT32 now = EcatDbg_Dwt();
/* 活性看 processSeq(Process 真填过输入),不能看 s_inMapSeq。
* InputMapping 可在 Process 停摆时继续拷陈旧 s_in,seq 照涨 → 救援永不触发。 */
const UINT32 seq = EcatPdo_ProcessSeq();
if (al != 8U || nPdInputSize == 0U) {
s_inMapSeqSeen = seq;
s_inStaleBaseCyc = now;
return;
}
if (seq != s_inMapSeqSeen) {
s_inMapSeqSeen = seq;
s_inStaleBaseCyc = now;
return;
}
/* seq 未推进:等满阈值再介入,避开两拍之间的正常空隙。
* 5ms @1ms 周期 = 连丢 ≥5 拍;@4ms 周期 = 连丢 1~2 拍,仍足够判死。 */
{
const UINT32 thresh = (SystemCoreClock / 1000U) * 5U;
if ((UINT32)(now - s_inStaleBaseCyc) < thresh) {
return;
}
}
/* 强制刷新:先重开门/首输出标志(门关着时 PDI 永远不会回来),
* 再在 EXMC 互斥窗口(仅屏蔽 EXTI0/1/2)内做一次输入映射,保证不关全局中断(Preempt 0 CAN RX 不受影响)。
* 必须先 EcatPdo_Process() 再 PDO_InputMapping()。
* Process 先填好 buildInput+fillDiagTail,InputMapping 再发布,救援才有内容可写。 */
{
UINT16 dummy = 0U;
const UINT16 evt = HW_GetALEventRegister();
if (!bEscIntEnabled) {
bEscIntEnabled = TRUE;
}
if (!bEcatFirstOutputsReceived) {
bEcatFirstOutputsReceived = TRUE;
HW_EscReadWord(dummy, nEscAddrOutputData);
HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U));
}
/* 救援路径计入 busy(低频;与 ISR/任务派发同一口径)。
* 不要把 MainLoop 整段包进 CpuLoad——见 task_runner / cpu_load.h。 */
CpuLoad_Enter();
const uint32_t prevMask = EscExmcBus_MaskEnter();
EcatPdo_Process();
PDO_InputMapping();
EscExmcBus_MaskExit(prevMask);
CpuLoad_Leave();
++s_opFreshRescues;
s_inMapSeqSeen = EcatPdo_ProcessSeq();
s_inStaleBaseCyc = EcatDbg_Dwt();
/* 不打串口:计数经 0x2000:24 / /api/sync_diag 上到网页,避免 5ms 刷屏。 */
}
}
UINT32 EcatDbg_OpFreshRescues(void)
{
return s_opFreshRescues;
}
+14 -2
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
@@ -12,6 +10,7 @@
#include "ecat_def.h" #include "ecat_def.h"
#include "esc.h" #include "esc.h"
#include "ecatslv.h" #include "ecatslv.h"
#include <stdint.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@@ -32,6 +31,19 @@ void APPL_InputMapping(UINT16 *pInputData);
void APPL_OutputMapping(UINT16 *pOutputData); void APPL_OutputMapping(UINT16 *pOutputData);
void APPL_Application(void); void APPL_Application(void);
/* OP 输入新鲜度不变量(task_runner 每圈调用;内部廉价,仅冻结时强制重写) */
void EcatDbg_OpInputFreshness(void);
UINT32 EcatDbg_OpFreshRescues(void);
/* HBI/EXMC 事务互斥(仅屏蔽 EXTI0/1/2,不关全局中断) */
uint32_t EscExmcBus_MaskEnter(void);
void EscExmcBus_MaskExit(uint32_t prev);
uint32_t HW_EscExmcMaskEnter(void);
void HW_EscExmcMaskExit(uint32_t prev);
/* 复位输出 sequence 状态机 */
void EcatCan_ResetSequenceState(void);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+19 -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。
------ 输出 384B = 48×64bit:兼容单帧区 48B + CANFD 批量发送区 336B
------ 输入 640B = 80×64bit:桥状态 32B + 电机窗口 288B + 长帧 40B + 批量接收区 280B
------ 上限依据:邮箱占 0x1000-0x13FF,过程数据区仅 0x1400-0x1FFF=3072B, ------ 上限依据:邮箱占 0x1000-0x13FF,过程数据区仅 0x1400-0x1FFF=3072B,
------ SM2/SM3 均 3-buffer 实占 3×size ⇒ out+in ≤ 1024B。384+640 恰好用满。 ------ SM2/SM3 均 3-buffer 实占 3×size ⇒ out+in ≤ 1024B。
------ 推导与实测(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
@@ -666,8 +655,10 @@
#define DEVICE_SW_VERSION "1.0" #define DEVICE_SW_VERSION "1.0"
#endif #endif
/* 0x100A 的位长。SSC 默认 3 字符只装得下占位 "1.0";这里抬到 16,真实值由
* sys/initial.cpp 起板时写进 acSoftwareversion(与 BOOT 侧同款改动)。 */
#ifndef DEVICE_SW_VERSION_LEN #ifndef DEVICE_SW_VERSION_LEN
#define DEVICE_SW_VERSION_LEN 0x3 #define DEVICE_SW_VERSION_LEN 0x10
#endif #endif
/*----------------------------------------------------------------------------------------- /*-----------------------------------------------------------------------------------------
@@ -708,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)
{ {
+20 -1
View File
@@ -209,8 +209,27 @@ OBJCONST UCHAR OBJMEM aName0x1009[] = "Hardware version";
-----------------------------------------------*/ -----------------------------------------------*/
/** /**
* \brief 0x100A (Software version) variable to handle the object data * \brief 0x100A (Software version) variable to handle the object data
*
* 必须显式给到 DEVICE_SW_VERSION_LEN+1:条目描述符按 LEN 声明位长,SSC 会照长度
* 整块回给主站;用 =DEVICE_SW_VERSION 推出来只有 4 字节,读到的是越界内容。
* 真实版本号由 sys/initial.cpp 起板时调 AppSetSoftwareVersion() 覆盖。
*/ */
CHAR acSoftwareversion[] = DEVICE_SW_VERSION; CHAR acSoftwareversion[DEVICE_SW_VERSION_LEN + 1] = DEVICE_SW_VERSION;
/** @brief 覆盖 0x100A 上报的软件版本串(容量只有本文件知道,故 setter 放这儿)。 */
void AppSetSoftwareVersion(const char *version)
{
unsigned int i = 0U;
if (version == NULL) {
return;
}
/* 本文件没 include stdio,手写有界拷贝:超长就截断,末尾必带 NUL。 */
for (; i + 1U < sizeof(acSoftwareversion) && version[i] != '\0'; i++) {
acSoftwareversion[i] = version[i];
}
acSoftwareversion[i] = '\0';
}
/** /**
* \brief 0x1009 (Hardware version) entry description * \brief 0x1009 (Hardware version) entry description
+72 -115
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,23 +69,28 @@ 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(输出探针/全量读) */
volatile UINT32 g_dbg_freerunCyc = 0; /* sub7: MainLoop free-run 分支(SM 同步下恒 0) */ volatile UINT32 g_dbg_freerunCyc = 0; /* sub7: MainLoop free-run 分支(SM 同步下恒 0) */
volatile UINT32 g_dbg_pdiCnt = 0; /* sub8: PDI_Isr 触发次数(=每周期 1 次) */ volatile UINT32 g_dbg_pdiCnt = 0; /* sub8: PDI_Isr 触发次数(=每周期 1 次) */
volatile UINT32 g_dbg_inCyc = 0; /* sub9: PDO_InputMapping 全程(APPL+640B 写) */ volatile UINT32 g_dbg_inCyc = 0; /* sub9: PDO_InputMapping 全程(APPL+过程数据写) */
volatile UINT32 g_dbg_inWriteCyc = 0; /* sub10: 其中 HW_EscWriteIsr 640B 过程数据写 */ volatile UINT32 g_dbg_inWriteCyc = 0; /* sub10: 其中 HW_EscWriteIsr 640B 过程数据写 */
volatile UINT32 g_dbg_alCyc = 0; /* sub11: PDI_Isr 内 AL 事件读×2+超限应答 */ volatile UINT32 g_dbg_alCyc = 0; /* sub11: PDI_Isr 内 AL 事件读×2+超限应答 */
volatile UINT32 g_dbg_appCyc = 0; /* sub12: ECAT_Application(桥 processOutput) */ volatile UINT32 g_dbg_appCyc = 0; /* sub12: ECAT_Application(桥 processOutput) */
@@ -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 剖分:输入映射全程 + 640B 写细分)*/ /*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,33 +145,47 @@ 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,11 +328,24 @@ 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;
@@ -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,16 +597,29 @@ 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();
/*
* 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(); 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;
} }
ENABLE_ESC_INT(); ENABLE_ESC_INT();
+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*/
+177 -96
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);
@@ -255,6 +252,49 @@ static void hardResetCan(uint32_t canPeriph, rcu_periph_enum canRcu)
} }
} }
/**
* @brief CAN TX 并发保护 RAII 守卫。
*
* 屏蔽 EXTI10_15_IRQn(SPI CS Assert/Release, Preempt 0)与该通道的 txIrq(Preempt 2),
* 防止 EtherCAT PDI_Isr(Preempt 1)与 SPI CS EXTI ISR(Preempt 0)在操作 s_txQueues
* 或硬件发送邮箱寄存器时产生并发撕裂与重入竞争。
* 具备重入安全性:若某中断当前未使能(例如已被外层守卫屏蔽),退出时不会误使能。
*/
class CanTxConcurrencyGuard {
public:
explicit CanTxConcurrencyGuard(uint8_t channelIndex)
: m_txIrq(kChannels[channelIndex].txIrq)
{
m_exti15_enabled = (NVIC_GetEnableIRQ(EXTI10_15_IRQn) != 0U);
m_tx_enabled = (NVIC_GetEnableIRQ(m_txIrq) != 0U);
if (m_exti15_enabled) {
NVIC_DisableIRQ(EXTI10_15_IRQn);
}
if (m_tx_enabled) {
NVIC_DisableIRQ(m_txIrq);
}
__DSB();
__ISB();
}
~CanTxConcurrencyGuard() {
if (m_tx_enabled) {
NVIC_EnableIRQ(m_txIrq);
}
if (m_exti15_enabled) {
NVIC_EnableIRQ(EXTI10_15_IRQn);
}
__DSB();
__ISB();
}
private:
IRQn_Type m_txIrq;
bool m_exti15_enabled;
bool m_tx_enabled;
};
} // namespace } // namespace
CanFd::Diag CanFd::s_diag{}; CanFd::Diag CanFd::s_diag{};
@@ -376,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;
@@ -536,12 +574,11 @@ 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;
s_rxQueues[i].tail = 0U; s_rxQueues[i].tail = 0U;
s_rxQueues[i].count = 0U;
s_txQueues[i].head = 0U; s_txQueues[i].head = 0U;
s_txQueues[i].tail = 0U; s_txQueues[i].tail = 0U;
s_txQueues[i].count = 0U; s_txQueues[i].count = 0U;
@@ -904,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)
@@ -982,23 +1019,20 @@ CanFd::Mode CanFd::mode()
* @param[in] 无。 * @param[in] 无。
* @param[out] 无。 * @param[out] 无。
* @retval bool true 表示至少一条接收队列非空,false 表示当前三路均无待消费报文。 * @retval bool true 表示至少一条接收队列非空,false 表示当前三路均无待消费报文。
* @note 无锁检查 head != tail,免关中断。
*/ */
bool CanFd::hasRxFrame() bool CanFd::hasRxFrame()
{ {
bool found = false;
uint32_t save = RT_CRITICAL_SAVE();
for(size_t i = 0; i < kChannelCount; ++i) { for(size_t i = 0; i < kChannelCount; ++i) {
if(s_rxQueues[i].count != 0U) { if(s_rxQueues[i].head != s_rxQueues[i].tail) {
found = true; return true;
break;
} }
} }
RT_CRITICAL_RESTORE(save); return false;
return found;
} }
/** /**
* @brief 轮转读取并消费一帧接收报文。 * @brief 轮转读取并消费一帧接收报文(Lock-Free SPSC 消费者端)。
* @param[in] 无。 * @param[in] 无。
* @param[out] id 输出报文标识符;允许为空。 * @param[out] id 输出报文标识符;允许为空。
* @param[out] data 输出报文数据缓存;允许为空。 * @param[out] data 输出报文数据缓存;允许为空。
@@ -1006,43 +1040,38 @@ bool CanFd::hasRxFrame()
* @param[out] channel 输出报文来源通道号;允许为空。 * @param[out] channel 输出报文来源通道号;允许为空。
* @retval bool true 表示成功消费一帧报文,false 表示当前三路接收队列均为空。 * @retval bool true 表示成功消费一帧报文,false 表示当前三路接收队列均为空。
* @note 消费顺序采用从 `s_nextConsumeChannel` 开始的轮转扫描,避免长期偏向固定通道。 * @note 消费顺序采用从 `s_nextConsumeChannel` 开始的轮转扫描,避免长期偏向固定通道。
* 基于 SPSC 无锁环形缓冲区,消费端读取 payload 并执行 __DMB() 后推进 head,
* 无需关中断或依赖 BASEPRI,彻底消除与 Preempt 0 RX ISR 的并发撕裂。
*/ */
bool CanFd::consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel) bool CanFd::consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel)
{ {
RxFrame localFrame{};
bool found = false;
uint32_t save = RT_CRITICAL_SAVE();
for(size_t n = 0; n < kChannelCount; ++n) { for(size_t n = 0; n < kChannelCount; ++n) {
const uint8_t idx = static_cast<uint8_t>((s_nextConsumeChannel + n) % kChannelCount); const uint8_t idx = static_cast<uint8_t>((s_nextConsumeChannel + n) % kChannelCount);
auto &queue = s_rxQueues[idx]; auto &queue = s_rxQueues[idx];
if(queue.count == 0U) { const uint8_t head = queue.head;
const uint8_t tail = queue.tail;
if(head == tail) {
continue; continue;
} }
localFrame = queue.frames[queue.head]; const auto &src = queue.frames[head];
queue.head = static_cast<uint8_t>((queue.head + 1U) % kRxQueueDepth); if(channel) *channel = src.channel;
queue.count--; if(id) *id = src.id;
if(length) *length = src.length;
if(data && src.length > 0U) {
for(uint8_t i = 0; i < src.length; ++i) {
data[i] = src.data[i];
}
}
__DMB();
queue.head = static_cast<uint8_t>((head + 1U) & kRxQueueMask);
s_nextConsumeChannel = static_cast<uint8_t>((idx + 1U) % kChannelCount); s_nextConsumeChannel = static_cast<uint8_t>((idx + 1U) % kChannelCount);
found = true;
break;
}
RT_CRITICAL_RESTORE(save);
if(!found) {
return false;
}
if(channel) *channel = localFrame.channel;
if(id) *id = localFrame.id;
if(length) *length = localFrame.length;
if(data) {
for(uint8_t i = 0; i < localFrame.length; ++i) {
data[i] = localFrame.data[i];
}
}
return true; return true;
} }
return false;
}
/** /**
* @brief 直接向指定 CAN 通道发送一帧任意数据报文(扩展帧)。 * @brief 直接向指定 CAN 通道发送一帧任意数据报文(扩展帧)。
@@ -1068,6 +1097,8 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
return CanTxResult::kErrInvalid; return CanTxResult::kErrInvalid;
} }
CanTxConcurrencyGuard guard(channelIndex);
/* 诊断:捕获任何通过 transmitFrame 发 0x100 DLC=16 的帧(排查幽灵帧来源) */ /* 诊断:捕获任何通过 transmitFrame 发 0x100 DLC=16 的帧(排查幽灵帧来源) */
if (ext_id == 0x00000100U && dlc == 16U) { if (ext_id == 0x00000100U && dlc == 16U) {
CAN_LOGI("[CAN-DIAG] TX 0x100 DLC=16 via transmitFrame ch=%u\r\n", CAN_LOGI("[CAN-DIAG] TX 0x100 DLC=16 via transmitFrame ch=%u\r\n",
@@ -1100,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
@@ -1181,6 +1214,10 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id,
TxQueue &q = s_txQueues[channelIndex]; TxQueue &q = s_txQueues[channelIndex];
/* 关中断并发保护:屏蔽 EXTI10_15_IRQn(SPI burst, Preempt 0)与 txIrq(Preempt 2),
* 解决 PDI_Isr(Preempt 1)与 SPI CS release(Preempt 0)并发访问 s_txQueues 的竞态。 */
CanTxConcurrencyGuard guard(channelIndex);
/* 快速路径:队列空 → 直通硬件邮箱。装得下立即返回,不用等待 TX 完成中断。 /* 快速路径:队列空 → 直通硬件邮箱。装得下立即返回,不用等待 TX 完成中断。
* ★ allow_abort=false:邮箱满时禁止 abort 旧帧顶替(那会把同通道已装载的 * ★ allow_abort=false:邮箱满时禁止 abort 旧帧顶替(那会把同通道已装载的
* 旧帧静默丢弃,>3 帧同通道突发只剩最后 3 帧上线),一律落入软件队列, * 旧帧静默丢弃,>3 帧同通道突发只剩最后 3 帧上线),一律落入软件队列,
@@ -1193,16 +1230,9 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id,
/* 邮箱满:落入下面的入队路径 */ /* 邮箱满:落入下面的入队路径 */
} }
/* 关中段:EXTI15(SPI burst) 与 CANx TX ISR 并发,保护 head/tail/count。
* 用 CMSIS NVIC 接口(SPL 的 nvic_irq_enable 是 3 参版)。
* 关的是该通道自身的 txIrq,避免关全局影响其他通道的实时性。 */
const IRQn_Type irqn = kChannels[channelIndex].txIrq;
NVIC_DisableIRQ(irqn);
if (q.count >= kTxQueueDepth) { if (q.count >= kTxQueueDepth) {
/* 队列也满了:彻底装不下。此时不能再返回 kOk, /* 队列也满了:彻底装不下。此时不能再返回 kOk,
* 否则前端会误判成功;只能丢弃并返回 kErrMailbox。 */ * 否则前端会误判成功;只能丢弃并返回 kErrMailbox。 */
NVIC_EnableIRQ(irqn);
s_diag.error_count++; s_diag.error_count++;
s_channelStats[channelIndex].error_count++; s_channelStats[channelIndex].error_count++;
return CanTxResult::kErrMailbox; return CanTxResult::kErrMailbox;
@@ -1217,14 +1247,13 @@ CanFd::CanTxResult CanFd::enqueueTx(uint8_t channelIndex, uint32_t ext_id,
q.tail = static_cast<uint8_t>((q.tail + 1U) % kTxQueueDepth); q.tail = static_cast<uint8_t>((q.tail + 1U) % kTxQueueDepth);
q.count++; q.count++;
NVIC_EnableIRQ(irqn);
return CanTxResult::kOk; return CanTxResult::kOk;
} }
/** /**
* @brief CAN TX 完成中断末尾调用:用硬件邮箱空闲出的位置,把队列里的帧装回去。 * @brief CAN TX 完成中断末尾调用:用硬件邮箱空闲出的位置,把队列里的帧装回去。
* @note 只在 ISR 内调用;关中断保护已在 transmitFrame 返回值判定外层完成, * @note 由 CAN TX 完成 ISR 调用;使用 CanTxConcurrencyGuard 屏蔽 EXTI10_15 与 txIrq,
* 本函数本身不关中(邮箱空位是"单调减少再单调增加",多读无害)。 * 防止出队期间被高优先级的 SPI CS release 抢占导致 head/count 撕裂。
*/ */
void CanFd::drainTxQueueIsr(uint8_t channelIndex) void CanFd::drainTxQueueIsr(uint8_t channelIndex)
{ {
@@ -1232,6 +1261,8 @@ void CanFd::drainTxQueueIsr(uint8_t channelIndex)
return; return;
} }
TxQueue &q = s_txQueues[channelIndex]; TxQueue &q = s_txQueues[channelIndex];
CanTxConcurrencyGuard guard(channelIndex);
while (q.count > 0U) { while (q.count > 0U) {
TxQueuedFrame &f = q.frames[q.head]; TxQueuedFrame &f = q.frames[q.head];
/* 复用 transmitFrame 的装载逻辑;allow_abort=false:这里邮箱刚空出来, /* 复用 transmitFrame 的装载逻辑;allow_abort=false:这里邮箱刚空出来,
@@ -1261,6 +1292,16 @@ uint32_t CanFd::txQueueBacklog(uint8_t channelIndex)
return s_txQueues[channelIndex].count; return s_txQueues[channelIndex].count;
} }
uint32_t CanFd::rxQueueCount(uint8_t channelIndex)
{
if (channelIndex >= kChannelCount) {
return 0U;
}
const uint8_t head = s_rxQueues[channelIndex].head;
const uint8_t tail = s_rxQueues[channelIndex].tail;
return static_cast<uint32_t>((tail - head) & kRxQueueMask);
}
void CanFd::abortPendingTx(uint8_t channelIndex) void CanFd::abortPendingTx(uint8_t channelIndex)
{ {
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) { if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
@@ -1423,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;
} }
@@ -1450,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];
@@ -1496,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) {
@@ -1520,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++;
@@ -1612,13 +1644,17 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
} }
auto &queue = s_rxQueues[channelIndex]; auto &queue = s_rxQueues[channelIndex];
if(queue.count >= kRxQueueDepth) { const uint8_t head = queue.head;
const uint8_t tail = queue.tail;
const uint8_t next_tail = static_cast<uint8_t>((tail + 1U) & kRxQueueMask);
if(next_tail == head) {
s_diag.rx_drop_count++; s_diag.rx_drop_count++;
stats.rx_drop_count++; stats.rx_drop_count++;
continue; continue;
} }
auto &frame = queue.frames[queue.tail]; auto &frame = queue.frames[tail];
frame.channel = channelIndex; frame.channel = channelIndex;
frame.id = rxId; frame.id = rxId;
frame.length = rx_message.rx_dlen; frame.length = rx_message.rx_dlen;
@@ -1626,8 +1662,8 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
frame.data[i] = rx_message.rx_data[i]; frame.data[i] = rx_message.rx_data[i];
} }
queue.tail = static_cast<uint8_t>((queue.tail + 1U) % kRxQueueDepth); __DMB();
queue.count++; queue.tail = next_tail;
} }
if(fifoFull) { if(fifoFull) {
@@ -1689,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 命令读到一致快照。
@@ -1718,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) {
@@ -1733,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]),
@@ -1766,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";
+15 -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,8 +246,15 @@ public:
/** @brief 软件 TX 队列当前积压帧数(统计/诊断用)。 */ /** @brief 软件 TX 队列当前积压帧数(统计/诊断用)。 */
static uint32_t txQueueBacklog(uint8_t channelIndex); static uint32_t txQueueBacklog(uint8_t channelIndex);
/** @brief 接收队列容量深度(2 的幂;8kHz PDO 下主循环间隙短,加深缓冲防 STATUS 溢出丢帧)。 */
static constexpr uint8_t rxQueueDepth() { return kRxQueueDepth; }
/** @brief 查询指定通道软件接收队列当前积压帧数(无锁 SPSC,由 tail 与 head 计算)。 */
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;
/** 每通道软件突发队列深度:覆盖整块突发(CAN_BURST_HARD_MAX=16 帧)中同一通道的 /** 每通道软件突发队列深度:覆盖整块突发(CAN_BURST_HARD_MAX=16 帧)中同一通道的
* 全部超载帧(3 邮箱之外最多 13 帧 = 16-3),保证任意 DLC 的整块突发不因队列满丢帧。 */ * 全部超载帧(3 邮箱之外最多 13 帧 = 16-3),保证任意 DLC 的整块突发不因队列满丢帧。 */
static constexpr uint8_t kTxQueueDepth = 13U; static constexpr uint8_t kTxQueueDepth = 13U;
@@ -259,11 +266,15 @@ private:
uint8_t length; uint8_t length;
}; };
/**
* @brief Lock-Free 单生产者单消费者 (SPSC) 环形接收队列。
* @note 生产者为 CAN RX ISR (Preempt 0) 写入 tail,消费者为 consumeRxFrame (Thread) 推进 head。
* 移除共享 count 变量,消除 BASEPRI=0x10 临界区对 Preempt 0 无效导致的并发撕裂与计数下溢风险。
*/
struct RxQueue { struct RxQueue {
RxFrame frames[kRxQueueDepth]; RxFrame frames[kRxQueueDepth];
volatile uint8_t head; volatile uint8_t head;
volatile uint8_t tail; volatile uint8_t tail;
volatile uint8_t count;
}; };
/** 排队等待硬件邮箱空位的 CAN 帧(仅 SDO 突发用,payload ≤ 64B)。 */ /** 排队等待硬件邮箱空位的 CAN 帧(仅 SDO 突发用,payload ≤ 64B)。 */
@@ -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;
@@ -1,13 +1,14 @@
/** /**
* @file app_spi_cmd_exmc_diag.cpp * @file app_spi_cmd_exmc_diag.cpp
* @brief SPI 0xA2 EXMC/HBI 诊断命令处理器:驱动 LAN9252 通断测试 pattern / NRST 手动诊断 / 电源管理。 * @brief SPI 0xA2 EXMC/HBI 诊断命令处理器:驱动 LAN9252 通断测试 pattern / NRST 手动诊断 / 电源管理 / ESP32 EN。
* *
* 网页按钮 → ESP32 /spi/send → SPI 0xA2 → 本 handler → 分流: * 网页按钮 → ESP32 /spi/send → SPI 0xA2 → 本 handler → 分流:
* action 0~10 → EscExmcBus::continuityDrive() (EXMC 数据/控制线 pattern) * action 0~10 → EscExmcBus::continuityDrive() (EXMC 数据/控制线 pattern)
* action 20~24 → EscLan9252::nrstDrive() (ECAT_NRST/PB10 拉低/拉高/浮空/回读) * action 20~24 → EscLan9252::nrstDrive() (ECAT_NRST/PB10 拉低/拉高/浮空/回读)
* action 30~34 → EscLan9252::pmDrive() (电源管理:读状态/进D1/D2/D3/手动唤醒) * action 30~34 → EscLan9252::pmDrive() (电源管理:读状态/进D1/D2/D3/手动唤醒)
* action 40~44 → Esp32En::drive() (ESP32 EN/PC9 诊断备援;开关请走 EtherCAT 0x2503)
* 立即返回 ACK,不阻塞 SPI 总线。电平保持到下次调用或 action=0(stop)。 * 立即返回 ACK,不阻塞 SPI 总线。电平保持到下次调用或 action=0(stop)。
* NRST action 返回的 status 字节 = 实际引脚电平(0/1),非 OK 码。 * NRST/ESP32_EN action 返回的 status 字节 = 实际引脚电平(0/1),非 OK 码。
* PM action 返回的 status 字节 = bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE; * PM action 返回的 status 字节 = bit0=READY bit1=PM_SLEEP_EN bit3:2=PM_MODE;
* 唤醒(34)另有 1=成功 0=超时;0xEE=无效 action,0xE1=HBI 未就绪。 * 唤醒(34)另有 1=成功 0=超时;0xEE=无效 action,0xE1=HBI 未就绪。
*/ */
@@ -16,6 +17,7 @@
#include "device/spi/app_spi_command_defs.h" #include "device/spi/app_spi_command_defs.h"
#include "comm/esc_exmc_bus.h" #include "comm/esc_exmc_bus.h"
#include "comm/esc_lan9252.h" #include "comm/esc_lan9252.h"
#include "sys/esp32_en.h"
namespace { namespace {
@@ -32,7 +34,9 @@ public:
uint8_t status = 0xEE; uint8_t status = 0xEE;
if (ctx.payload_len >= 1) { if (ctx.payload_len >= 1) {
const uint8_t action = ctx.payload[0]; const uint8_t action = ctx.payload[0];
if (action >= 30U) { if (action >= 40U) {
status = Esp32En::drive(action - 40U);
} else if (action >= 30U) {
status = EscLan9252::pmDrive(action - 30U); status = EscLan9252::pmDrive(action - 30U);
} else if (action >= 20U) { } else if (action >= 20U) {
status = EscLan9252::nrstDrive(action - 20U); status = EscLan9252::nrstDrive(action - 20U);
+75 -10
View File
@@ -38,6 +38,7 @@ extern "C" {
#include "sys/critical.h" #include "sys/critical.h"
#include "sys/systick.h" #include "sys/systick.h"
#include "sys/cpu_load.h" #include "sys/cpu_load.h"
#include "sys/esp32_en.h"
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
@@ -370,9 +371,18 @@ 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);
exti_interrupt_enable(EXTI_15); /* 先清 STAT 再开 IMR。warm jump 下 BOOT 关中断跳转前 ESP32 持续拉 CS,
* EXTI15 STAT 大概率残留;若先 enable IMR,残留位会立即回放成一次假 CS
* 沿进 NVIC。 */
exti_interrupt_flag_clear(EXTI_15); exti_interrupt_flag_clear(EXTI_15);
nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U); exti_interrupt_enable(EXTI_15);
/* SPI CS EXTI 抢占级 0。抢占级 1 与 PDI/SYNC1/SCHED 同级不抢占
* (PDI 窗口可达数百 µs),且被 RT_CRITICAL(BASEPRI≥1) 完全屏蔽;
* release ISR 一旦拖过 ESP32 的 CS 间隔(1ms),re-arm 落空,
* 下一笔查询帧头部丢失。提到 0 后仅与 SYNC0/CAN-RX
* (µs 级)同级排队,release 处理确定性 µs 级完成。代价:0xA2 pmDrive 的
* 忙等(≤10ms,手动 PM 诊断才用)会阻塞 SYNC0——诊断窗口,可接受。 */
nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U);
} }
@@ -523,21 +533,34 @@ 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]);
/* 0xE8 payload 携带接收诊断——[0]=0xE8, [1]=rxLen, [2..5]=rxBuf 前 4 字节。
* ESP32 /system/status rsp_err_last_p0..p3 直读,无需串口即可判定损坏形态
* (全FF=DMA 未装/CS 丢失;SOF 错位=相位漂移;SOF 对但 CRC 败=帧内位错)。 */
uint16_t tx_len = 0U; uint16_t tx_len = 0U;
uint8_t err = 0xEEU; uint8_t diag[8];
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); diag[0] = 0xE8U;
diag[1] = (m_rxLen > 255U) ? 255U : (uint8_t)m_rxLen;
diag[2] = (m_rxLen > 0U) ? m_rxBuf[0] : 0xFFU;
diag[3] = (m_rxLen > 1U) ? m_rxBuf[1] : 0xFFU;
diag[4] = (m_rxLen > 2U) ? m_rxBuf[2] : 0xFFU;
diag[5] = (m_rxLen > 3U) ? m_rxBuf[3] : 0xFFU;
diag[6] = 0x5AU;
diag[7] = 0xA5U;
/* 非v2帧时 hdr 未初始化:禁止缓存/推进序号(垃圾缓存会被 dup 重放) */
spi_build_frame(kRspError, diag, sizeof(diag), m_txBuf, &tx_len);
m_txLen = tx_len; m_txLen = tx_len;
finishResponseAndReady(); finishResponseAndReady();
return; return;
} }
if (CONFIG_SPI_APP_LOG_ENABLE != 0 && !isFlashRelatedFrame(frame_type)) { /* 正常帧不打日志(避免 UART 刷屏);CONFIG_SPI_APP_LOG_ENABLE
postLogFmt("[SPI-APP] 收到非刷写 SPI type=0x%02X", frame_type); * 只覆盖异常路径(非v2帧/未知type/SEQ跳变/session不匹配),正常态静默。 */
}
g_pendingAckSeq = hdr.tx_seq; g_pendingAckSeq = hdr.tx_seq;
if (frame_type == V2_TYPE_LINK_HELLO) { if (frame_type == V2_TYPE_LINK_HELLO) {
/* 合法 HELLO 已到:停掉起机补清,避免 reset 打断建连 */
Esp32En::cancelRelinkFromSpi();
g_sessionId = hdr.session_id; g_sessionId = hdr.session_id;
g_txSeq = 0; g_txSeq = 0;
g_expectedRxSeq = 0; g_expectedRxSeq = 0;
@@ -628,13 +651,15 @@ 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 = 0xEEU; 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 = 0xEEU; 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);
/* handler 命中但没产出响应(典型=buildResponse 构帧失败/业务拒绝)走此兜底错误路径。 */
postLogFmt("[SPI-APP] handler t=0x%02X tx_len=0 -> ERROR", frame_type);
} }
m_txLen = tx_len; m_txLen = tx_len;
@@ -671,6 +696,41 @@ void AppSpiCommand::cancelRing()
updateDataReady(); updateDataReady();
} }
void AppSpiCommand::resetEsp32Link()
{
/* ESP32 被 PC9 掉电可能死在半笔 CS 里,SPI0/DMA/EXTI 都可能卡死。
* 做一次完整外设重初始化(对齐 init 的硬件序列),而不是只清软件状态。
* 仅在 PC9 翻转、对端确定不在发帧时调用。 */
nvic_irq_disable(EXTI10_15_IRQn);
exti_interrupt_disable(EXTI_15);
g_v2Active = false;
g_sessionId = 0U;
g_txSeq = 0U;
g_expectedRxSeq = 0U;
g_pendingAckSeq = 0U;
g_localCredit = 1U;
s_lastReqSeq = 0xFFU;
s_lastRespLen = 0U;
s_hasCachedResp = false;
m_csActive = false;
m_rxLen = 0U;
m_ringPending = false;
m_ringDeadline = 0U;
m_txLen = 0U;
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
memset(m_rxBuf, 0x00, sizeof(m_rxBuf));
/* 硬件重初始化:SPI0 + DMA + CS EXTI(与 init() 同序列) */
configureDma();
configureSpi0();
armSpiDmasForNextCs();
updateDataReady();
configureExti();
printf("[SPI-APP] resetEsp32Link: SPI0/DMA/EXTI re-init done\r\n");
}
bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len) bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len)
{ {
/* 若上一次按铃尚未被 ESP32 读走,拒绝重复按铃 */ /* 若上一次按铃尚未被 ESP32 读走,拒绝重复按铃 */
@@ -767,6 +827,9 @@ void AppSpiCommand::onCsRelease()
} }
if (m_rxLen >= V2_OVERHEAD) { if (m_rxLen >= V2_OVERHEAD) {
processCommand(); processCommand();
} else {
/* 残包:SPI 可能已乱,重配外设 */
configureSpi0();
} }
armSpiDmasForNextCs(); armSpiDmasForNextCs();
updateDataReady(); updateDataReady();
@@ -821,7 +884,9 @@ void AppSpiCommand::onCsRelease()
if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) { if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) {
m_rxLen = 0U; m_rxLen = 0U;
/* v2 DATA_READY: 无待处理帧时 updateDataReady() 释放 PB6。 */ /* 起机毛刺/相位漂移后仅 re-arm DMA 不够:SPI0 可能已 OVR 卡死。
* 顺手重配 SPI0+DMA(不动 EXTI/session),下一笔 HELLO 才能收进来。 */
configureSpi0();
armSpiDmasForNextCs(); armSpiDmasForNextCs();
updateDataReady(); updateDataReady();
return; return;
@@ -89,6 +89,15 @@ public:
* 0xB0 进 BOOT 等路径需公开调用,避免 0xC1 残留盖住 ACK。 */ * 0xB0 进 BOOT 等路径需公开调用,避免 0xC1 残留盖住 ACK。 */
void cancelRing(); void cancelRing();
/**
* @brief ESP32 掉电/重上电后复位 SPI 链路(PC9 翻转时调用)。
*
* ESP32 被 EN 拉低时可能死在半笔 CS 事务里:session/序号/按铃/DMA 都会脏。
* 重上电 HELLO 前必须清干净,否则首帧对不齐 → 永远建不起 v2 会话。
* 清 session(HELLO 会重建)、取消按铃、re-arm DMA、释放 DATA_READY。
*/
void resetEsp32Link();
static constexpr uint16_t kRxBufSize = CONFIG_SPI_RX_BUF_SIZE; static constexpr uint16_t kRxBufSize = CONFIG_SPI_RX_BUF_SIZE;
static constexpr uint16_t kTxBufSize = CONFIG_SPI_TX_BUF_SIZE; static constexpr uint16_t kTxBufSize = CONFIG_SPI_TX_BUF_SIZE;
static constexpr uint16_t kLogBufSize = CONFIG_SPI_LOG_BUF_SIZE; static constexpr uint16_t kLogBufSize = CONFIG_SPI_LOG_BUF_SIZE;
@@ -21,7 +21,10 @@ static constexpr uint8_t kTypeOtaCancel = 0x03U;
static constexpr uint8_t kTypeAppPing = 0xA1U; static constexpr uint8_t kTypeAppPing = 0xA1U;
/** @brief EXMC/HBI 诊断命令(ESP32→GD32):payload[0]=action。 /** @brief EXMC/HBI 诊断命令(ESP32→GD32):payload[0]=action。
* 0=stop(恢复AF_PP), 1-4=数据pattern(0xFFFF/0x0000/0xAAAA/0x5555), * 0=stop(恢复AF_PP), 1-4=数据pattern(0xFFFF/0x0000/0xAAAA/0x5555),
* 5=CS低, 6=RD低, 7=WR低, 8=A1高, 9=A2高。用于 LAN9252 通断测试。 */ * 5=CS低, 6=RD低, 7=WR低, 8=A1高, 9=A2高。用于 LAN9252 通断测试。
* 20~24=ECAT_NRST(PB10) 30~34=LAN9252 电源管理
* 40~44=ESP32 EN(PC9):40/42=拉高 41=拉低保持 43=浮空 44=回读
* (运行期开关请走 EtherCAT SDO 0x2503,本组仅 SPI 诊断备援) */
static constexpr uint8_t kTypeExmcDiag = 0xA2U; static constexpr uint8_t kTypeExmcDiag = 0xA2U;
/** @brief APP 初始化状态查询(ESP32→GD32,1.5.34 新增):无 payload。 /** @brief APP 初始化状态查询(ESP32→GD32,1.5.34 新增):无 payload。
* 响应 ACK 25B(1.5.35 起;1.5.34 仅前 12B): * 响应 ACK 25B(1.5.35 起;1.5.34 仅前 12B):
+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 */
+37 -46
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"
/* ============================================================ /* ============================================================
* 功能开关(调试/特性总闸 — 改这里即可开关,改完重编) * 功能开关(调试/特性总闸 — 改这里即可开关,改完重编)
@@ -45,15 +47,16 @@
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)。 */ /** @brief SPI 命令逐帧日志(#if)。开启后仅异常路径输出
#define CONFIG_SPI_APP_LOG_ENABLE 0 * (非v2帧/未知type/SEQ跳变/session不匹配),正常状态 UART 静默。 */
#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
@@ -68,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)。 */
@@ -91,25 +102,23 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
* run 碎片有多严重、比对自身要多少钱)。临时仪表,取证完置 0 即与改造前逐字节等价。 * run 碎片有多严重、比对自身要多少钱)。临时仪表,取证完置 0 即与改造前逐字节等价。
* @note 开启时每拍在 PDI_Isr 内多一次 600B 字粒度比对 + 一次快照拷贝(约 10µs 量级, * @note 开启时每拍在 PDI_Isr 内多一次 600B 字粒度比对 + 一次快照拷贝(约 10µs 量级,
* 实测见 sub23),因此 **sub9/sub10 的写侧耗时会包含这部分**,不能直接与历史 * 实测见 sub23),因此 **sub9/sub10 的写侧耗时会包含这部分**,不能直接与历史
* 296µs/265µs 对拍——要看差分潜力就只看 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 进度打印。 */
@@ -147,24 +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;
constexpr uint32_t APP_FW_VERSION_PATCH = 27U; /* A/B 结束: 恢复 1.6.25 稀疏+差分写(用户裁定, 主站侧已有后台恢复兜底) */ /* A/B C组: SPARSE=0+DIFF=0, 回 1.6.21 恒全量 600B 写 ESC(查开机楔死) */ /* 1.6.25:P1 输入 PDO 差分写 ESC(三区:段1 桥状态/段2 电机密排/段3+5 快照诊断;变化后 3-buffer 冲刷协议,稳定段跳过;全跳过写末 2 字节完成 SM)。P2 csrRead 分级:verify 参数,SM/AL/邮箱默认双读,诊断/MII busy/init 轮询单读。开关 CONFIG_ECAT_DIFF_ESC_WRITE。 */ /* 1.6.24:pmDrive 进 D1/D2/D3 移除读回验证。板级实测:进入睡眠后任何 HBI 访问(含读,手册 6.3.4.2 只说写唤醒,芯片实际按主机 CS 活动判定)都会 <50ms 触发唤醒回 D0——读回验证自证失败,改为置信构造返回值(READY=0|SLEEP_EN=1|MODE=op);「读状态」语义改为"若在睡眠则主动打破睡眠"。 */ /* 1.6.23:LAN9252 芯片级电源管理诊断路径。EscLan9252::pmDrive()(0xA2 action 30~34:30 读状态/31 进 D1/32 进 D2/33 进 D3/34 手动唤醒),按 DS 6.3 写 PMT_CTRL(0x084) PM_MODE+PM_SLEEP_EN(分两次写);D3 前置两 PHY General Power Down(经 ESC 核心 MII 管理 0x0510-0x0514,PHY A 地址 0/2 运行期判定),唤醒=HBI 写 BYTE_TEST 后轮询 BYTE_TEST/READY(DWT CYCCNT 忙等,50µs 步进、上限 10ms——0xA2 在 CS EXTI ISR 上下文执行,禁用 SysTickTimer::delayMs 会死等;HBI 段 maskEnter 屏 EXTI0/1 防 SYNC0 突入)。唤醒后自动恢复 PHY 上电(自协商需数秒)。休眠期间 EtherCAT 通信中断属预期,主站 wkc 报错。 */ /* 1.6.22:P1 稀疏写 ESC。PDO_InputMapping 在密排布局下按 count 只写「段1+段2有效区(0..40+count*56)」与「段3+段5(544..599)」,跳过空槽区,省 HW_EscWriteIsr 的 PRAM 字节。开关 CONFIG_ECAT_SPARSE_ESC_WRITE=1;非密排/count 撑满自动回退整包。总长 600B/SII/主站解码不变。A/B:置 0 重编即 1.6.21 写侧。 */ /* 1.6.21:段 2 改「一帧=总线电机整轮状态」。用户裁定旧需求不对:不要 win_base 滑窗扫 27 槽,也不要段 4 历史帧覆盖窗反复重发。实测病理:接 3 个电机网页却看到 5 个状态帧——固定 5 槽里空槽 node_id=0 也占位,主站不滤就多报。本版:(1) 段 2 密排,count=本拍实际电机数(0..9),空节点不占位,槽序按 node_id 升序,同号多通道取 seq 最新;主站 `for i in 0..count` 按槽内 node_id 自动识别,无需滑窗。(2) 删段 4 覆盖窗口(fillRxWindow/RxWinSlot 整套),非 STATUS 仍走段 1/3 最近一帧。(3) 差分填充:节点集合与各 seq 未变时整段复用影子,跳过 MotorState 重读与逐槽 48B 拷贝。(4) 输入 PDO 总长仍 600B,SII 无需重刷;段 2 扩到 32..543、段 3 前移到 544、段 5 仍 584。**主站解码必须同步**(decode_input/transport-ecat.js:count 密排 + 段 3 偏移 320→544)。 */ constexpr uint32_t APP_FW_VERSION_PATCH = 51U; /* 1.6.50: CAN 空通道自动识别与降噪 */
/* 1.6.19:[DIFF-STATS] 仪表成本终于测对了,并推翻 1.6.18 自己的判断。1.6.18 认定"是 -Oz 把 memcpy(&a,p,4) 编成函数调用",改成手拼 4 字节移位,结果反汇编出**每字 8 条 ldrb.w**、比开销从 21.3µs/拍涨到 41µs/拍 —— 方向完全反了。真根因不在这里:alignas(4) 标在数组声明上只到调用点为止,而 observe(const uint8_t*) 的形参签名不带对齐信息,优化器无从证明指针 4 字节对齐,两版都只能逐字节走。修法是对两个入参各做一次 __builtin_assume_aligned(p,4) 再走 memcpy —— 反汇编确认循环体已缩成 ldr.w/ldr.w/cmp/bne 每字 4 条 (150 字 ≈ 750 cycle ≈ 4.2µs/拍,与最初估算同量级),且整个函数 bl 归零(600B 快照拷贝也被折成字搬运)。教训:**测量工具的开销会直接变成结论**,"差分判断很贵"这句话差一点就被我自己造的假象坐实。被测量(变化字节数 25%、run 峰值 12)不受此影响,两版读数一致,仍然有效。 */
/* 1.6.18:修 [DIFF-STATS] 仪表自己的假象 —— 比对循环里 `memcpy(&a,p,4)` 在 -Oz 下被编成**函数调用**而非单条 LDR,首轮实测 compare 开销虚报成 21.3µs/拍(≈300 次调用×995 拍),若拿它当"差分判断很贵"的证据就结论反了。改成小端 4 字节移位拼装:合法、不碰严格别名、且是 Clang 认得折 LDR 的模式。顺带把 alignas(4) 的理由改成"服务整块 memcpy",比对本身不再依赖它。Code 52716→52768(+52B),代价是体积不是速度。 */
/* 1.6.17:新增 [DIFF-STATS] 临时仪表,为"输入 PDO 改差分写"取证,**零行为变更**。动机:差分写的收益完全取决于每拍真正变了多少字节、变化点碎成几个 run,而 run 碎片数决定它会不会把一次 600B 整块写退化成几百次带 PRAM_AVAIL 轮询的小写——这两个数今天没有实测依据,所以先只上仪表再决定做不做。实现:comm/ethercat_diff_stats.{h,cpp},每拍在 PDI_Isr 里与上一份已发布镜像做 u32 粒度比对,按 1s 窗口(DWT 周期,同 cpu_load 手法)滚动,getter 只回报"上一个完整窗口"以免主站读到半累积值;量为 cycles / changedMax / runsTotal / runsMax / 段1..段5 各自变化字节 / 比对自身 DWT 开销。经对象字典 0x2000 sub13..23 上报(新增 11 个只读 UINT32)。**只加 SDO 子索引、PDO 长度与映射逐字节不动 ⇒ 无需重生成 SII、无需断电冷启动**,可与主站独立升级。整模块由 CONFIG_ECAT_DIFF_STATS 门控,置 0 后与 1.6.16 等价;挂接点统一 [DIFF-STATS] 标记,删除=删 2 文件+删标记行。已知副作用:开启时 sub9/sub10 写侧耗时含比对开销,不能直接与历史 296µs/265µs 对拍,真实写侧基准须置 0 重测。 */
/* 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 量级);覆盖窗口**不创造容量**,槽数 < 帧/拍 仍会静默丢帧。写侧 600×0.441≈265µs/拍(26.5%),仍优于旧 640B 的 296µs,更是"提到 4kHz"方案 87.5% 占空不可行结论(dc_bench 固化)的零头。段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;
@@ -222,6 +214,8 @@ constexpr uint32_t CONFIG_SPI_RING_TIMEOUT_MS = 50U;
* registerHandler 表满时静默丢弃,motor_ota 曾因容量=8 被丢弃(0xD0 回 0xEE)。 * registerHandler 表满时静默丢弃,motor_ota 曾因容量=8 被丢弃(0xD0 回 0xEE)。
* 取 14 留余量;新增 handler 时须确认本容量足够。 */ * 取 14 留余量;新增 handler 时须确认本容量足够。 */
constexpr uint8_t CONFIG_SPI_MAX_COMMANDS = 14U; constexpr uint8_t CONFIG_SPI_MAX_COMMANDS = 14U;
/** @brief GD32 APP 版本(HELLO_ACK/PING 身份块;改功能后请递增便于现场核对)。 */
/* 版本号在 app_spi_cmd_ping.cpp fillGd32AppIdentityPayload 维护 */
/** @brief 每通道不重复 ID 快照表最大条目数(与 ESP32 CANFD_ID_SNAPSHOT_MAX 对齐)。 */ /** @brief 每通道不重复 ID 快照表最大条目数(与 ESP32 CANFD_ID_SNAPSHOT_MAX 对齐)。 */
constexpr uint8_t CONFIG_CANFD_ID_SNAPSHOT_MAX = 16U; constexpr uint8_t CONFIG_CANFD_ID_SNAPSHOT_MAX = 16U;
/** @brief 快照条目最大数据字节数(CAN FD DLC 上限)。 */ /** @brief 快照条目最大数据字节数(CAN FD DLC 上限)。 */
@@ -274,7 +268,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;
@@ -301,7 +295,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;
@@ -328,6 +322,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;
@@ -341,16 +340,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;
@@ -358,9 +351,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 */
+109
View File
@@ -0,0 +1,109 @@
/**
* @file esp32_en.cpp
* @brief ESP32 EN (PC9) 驱动。PC9 拉高后延时 1.2s 复位 SPI,避开起机毛刺。
*/
#include "sys/esp32_en.h"
#include "sys/config.h"
#include "sys/task_manager.h"
#include "device/spi/app_spi_command.h"
#include <stdio.h>
namespace {
/** @brief PC9 拉高后延时多久再清 SPI(等 ESP32 ROM/APP 引脚初始化毛刺结束)。 */
constexpr uint32_t kPostBootRelinkDelayMs = 1200U;
/** @brief 延时复位排期;0=无。 */
volatile uint32_t s_relinkAtMs = 0U;
void drivePinLow()
{
gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
CONFIG_ESP32_RESET_PIN);
gpio_bit_reset(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN);
}
void drivePinHigh()
{
gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ,
CONFIG_ESP32_RESET_PIN);
gpio_bit_set(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN);
}
void floatPin()
{
gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_IN_FLOATING, 0,
CONFIG_ESP32_RESET_PIN);
}
bool readPin()
{
return gpio_input_bit_get(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN) == SET;
}
} // namespace
uint8_t Esp32En::drive(uint8_t action)
{
switch (action) {
case 0: /* 拉高(开 WiFi) */
case 2:
/* ESP32 仍死着:先清一次 SPI;起机毛刺会在 1.2s 后再清一次 */
s_relinkAtMs = 0U;
AppSpiCommandBus.resetEsp32Link();
drivePinHigh();
s_relinkAtMs = TaskManager::state().tick_1ms + kPostBootRelinkDelayMs;
break;
case 1: /* 拉低保持(关 WiFi) */
s_relinkAtMs = 0U;
drivePinLow();
AppSpiCommandBus.resetEsp32Link();
break;
case 3: /* 浮空 */
s_relinkAtMs = 0U;
floatPin();
AppSpiCommandBus.resetEsp32Link();
break;
case 4: /* 只读 */
break;
case 5: /* 仅复位 SPI(诊断;ESP32 空闲时调用,再让它 HELLO) */
s_relinkAtMs = 0U;
AppSpiCommandBus.resetEsp32Link();
break;
default:
return 0xEEU;
}
const bool level = readPin();
static const char *const kName[6] = {
"restore HIGH", "drive LOW (hold)", "drive HIGH", "float", "read-only", "spi-relink"};
printf("[APP][ESP32_EN] %s -> pin reads %s\r\n", kName[action],
level ? "HIGH" : "LOW");
return level ? 1U : 0U;
}
void Esp32En::poll()
{
if (s_relinkAtMs == 0U) {
return;
}
const uint32_t now = TaskManager::state().tick_1ms;
if ((int32_t)(now - s_relinkAtMs) < 0) {
return;
}
s_relinkAtMs = 0U;
AppSpiCommandBus.resetEsp32Link();
printf("[APP][ESP32_EN] post-boot SPI relink (glitch window over)\r\n");
}
void Esp32En::cancelRelinkFromSpi()
{
/* HELLO/业务帧已进来:不必再延时复位(会打断建连) */
s_relinkAtMs = 0U;
}
/* EtherCAT SDO 0x2503 回调入口(dc_app.c 为 C 侧)。 */
extern "C" uint8_t EcatEsp32EnDrive(uint8_t action)
{
return Esp32En::drive(action);
}
+42
View File
@@ -0,0 +1,42 @@
/**
* @file esp32_en.h
* @brief ESP32 EN/RESET (PC9 / WLESS_EN) 手动控制 —— 网页开关经 EtherCAT SDO 0x2503 下发。
*
* 硬件语义(见 sys/config.h CONFIG_ESP32_RESET_*):
* - PC9 推挽高 = 释放 ESP_CHIP_EN,ESP32 运行、WiFi 可用
* - PC9 推挽低 = 按死 EN,ESP32 硬复位/掉使能,WiFi 与 HTTP 全断
* - PC9 浮空 = 外部 C41/U11 把 ESP_CHIP_EN 拉低,同样关断(与复位后一致)
*
* 控制必须走 EtherCAT(与 ESP32 解耦):WiFi 关断后 ESP32 已死,
* 经 /spi/send 的路径关得掉、开不回来。
* 恢复:再次 SDO 写 action 0/2 拉高;或 GD32 复位/重新上电(initial.cpp 无条件拉高)。
*
* SPI 重建:ESP32 冷启动的 GPIO 毛刺会再次弄脏 SPI 链路,故拉高 PC9 后
* 在起机窗口内多次 resetEsp32Link(),赶在 HELLO 前把 session/DMA 清干净。
*/
#ifndef SYS_ESP32_EN_H
#define SYS_ESP32_EN_H
#include <stdint.h>
class Esp32En {
public:
/**
* @brief 驱动/回读 ESP32 EN(PC9)。EtherCAT SDO 0x2503 转发到此。
* @param action 0=拉高(开) 1=拉低保持(关) 2=拉高 3=浮空 4=只读 5=仅复位 SPI
* @return 实际引脚电平 0/1(与 nrstDrive 同口径);0xEE=无效 action。
*/
static uint8_t drive(uint8_t action);
/**
* @brief 起机窗口内补清 SPI 链路(TaskRunner::runPending 每圈调用)。
* @note 无排期时立即返回。PC9 拉高后早期少量补清;一旦收到 CS 事务即停,
* 避免打断正在建连的 HELLO。
*/
static void poll();
/** @brief SPI 收到任意 CS 事务时取消剩余补清(由 processCommand 调用)。 */
static void cancelRelinkFromSpi();
};
#endif /* SYS_ESP32_EN_H */
+22 -9
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 上报。
*/ */
@@ -37,6 +37,10 @@ uint8_t AppInitializer::s_initState = AppInitializer::kInitOk;
uint8_t AppInitializer::s_resetCause = 0U; 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 链接)。
* 主站总线读此对象可自证「新 APP 真起来了」。 */
extern "C" void AppSetSoftwareVersion(const char *version);
namespace { namespace {
struct InitEntry { struct InitEntry {
@@ -195,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",
@@ -279,6 +282,16 @@ void AppInitializer::init()
OtaBootRequest::init(); OtaBootRequest::init();
mark(kOtaBootReq); mark(kOtaBootReq);
/* 版本号写进 CoE 0x100A:唯一真源是 sys/config.h 的 APP_FW_VERSION_*,
* 不在别处再抄一遍字符串。 */
{
char ver[20];
snprintf(ver, sizeof ver, "%u.%u.%u",
(unsigned)APP_FW_VERSION_MAJOR, (unsigned)APP_FW_VERSION_MINOR,
(unsigned)APP_FW_VERSION_PATCH);
AppSetSoftwareVersion(ver);
}
TaskManager::init(); TaskManager::init();
mark(kTaskManager); mark(kTaskManager);
@@ -293,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();
@@ -322,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);
+47 -12
View File
@@ -10,17 +10,17 @@
#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, 1U, 1U); nvic_irq_enable(EXTI10_15_IRQn, 0U, 1U); /* 与 configureExti 一致:抢占级 0 */
/* requestEnterBoot 失败时 s_rebootPending 仍为 false */ /* requestEnterBoot 失败时 s_rebootPending 仍为 false */
return; return;
} }
@@ -223,3 +246,15 @@ void OtaBootRequest::process()
while(1) { while(1) {
} }
} }
/*--------------------------------------------------------------------------
* EtherCAT CoE 0x2502 的入口(comm/ssc/port/dc_app.c 是 C 文件,调不到类静态方法,
* 与 EcatPdo_* 同一形状)。写 1 走的是和 SPI 0xB0 完全相同的那条异步链:
* 这里只置标志,配置页三字的 Flash 写入 + NVIC_SystemReset 在主循环
* OtaBootRequest::process() 下一圈做 —— SDO 响应先发、复位后到,
* 不会把 SM1 半包留在总线上。
*-------------------------------------------------------------------------*/
extern "C" void EcatEnterBootRequestAsync(void)
{
OtaBootRequest::requestEnterBootAsync();
}
+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 通道。
+11 -9
View File
@@ -40,13 +40,15 @@ void TaskManager::init()
* @param[in] taskMask 待置位的任务位图掩码。 * @param[in] taskMask 待置位的任务位图掩码。
* @param[out] 无。 * @param[out] 无。
* @retval 无返回值。 * @retval 无返回值。
* @note 该操作不清除其他已存在的任务位。 * @note 该操作不清除其他已存在的任务位;通过 __LDREXW/__STREXW 独占访问循环实现无锁原子置位。
*/ */
void TaskManager::set(uint32_t taskMask) void TaskManager::set(uint32_t taskMask)
{ {
uint32_t save = RT_CRITICAL_SAVE(); uint32_t val;
g_scheduler.task_flags |= taskMask; do {
RT_CRITICAL_RESTORE(save); val = __LDREXW(&g_scheduler.task_flags);
} while (__STREXW(val | taskMask, &g_scheduler.task_flags) != 0U);
__DMB();
} }
/** /**
@@ -54,15 +56,15 @@ void TaskManager::set(uint32_t taskMask)
* @param[in] 无。 * @param[in] 无。
* @param[out] 无。 * @param[out] 无。
* @retval uint32_t 返回当前待执行任务位图。 * @retval uint32_t 返回当前待执行任务位图。
* @note 内部通过关中断实现对 `task_flags` 的原子交换。 * @note 通过 __LDREXW/__STREXW 独占访问循环实现对 `task_flags` 的原子读取与清零,彻底避免被 Preempt 0 ISR 抢占丢失标志。
*/ */
uint32_t TaskManager::takePending() uint32_t TaskManager::takePending()
{ {
uint32_t pending; uint32_t pending;
uint32_t save = RT_CRITICAL_SAVE(); do {
pending = g_scheduler.task_flags; pending = __LDREXW(&g_scheduler.task_flags);
g_scheduler.task_flags &= ~pending; } while (__STREXW(0U, &g_scheduler.task_flags) != 0U);
RT_CRITICAL_RESTORE(save); __DMB();
return pending; return pending;
} }
+2 -2
View File
@@ -33,7 +33,7 @@ public:
* @param[in] 无。 * @param[in] 无。
* @param[out] 无。 * @param[out] 无。
* @retval uint32_t 返回当前待执行任务位图。 * @retval uint32_t 返回当前待执行任务位图。
* @note 内部通过关中断保证原子性,供 TaskRunner 消费。 * @note 内部通过 Cortex-M 独占访问循环(__LDREXW/__STREXW)保证原子性,供 TaskRunner 消费。
*/ */
static uint32_t takePending(); static uint32_t takePending();
@@ -42,7 +42,7 @@ public:
* @param[in] taskMask 待置位的任务位图掩码。 * @param[in] taskMask 待置位的任务位图掩码。
* @param[out] 无。 * @param[out] 无。
* @retval 无返回值。 * @retval 无返回值。
* @note 内部通过 BASEPRI 临界区(RT_CRITICAL_SAVE/RESTORE)保证可由主循环与 ISR 共享调用。 * @note 内部通过 Cortex-M 独占访问循环(__LDREXW/__STREXW)保证可由主循环与任意抢占优先级 ISR 共享调用。
*/ */
static void set(uint32_t taskMask); static void set(uint32_t taskMask);
+60 -33
View File
@@ -11,15 +11,18 @@
#include "device/motor_state.h" #include "device/motor_state.h"
#include "device/motor_version.h" #include "device/motor_version.h"
#include "sys/config.h" #include "sys/config.h"
#include "sys/initial.h"
#include "sys/ota_boot_request.h" #include "sys/ota_boot_request.h"
#include "sys/motor_ota_relay.h" #include "sys/motor_ota_relay.h"
#include "sys/task_manager.h" #include "sys/task_manager.h"
#include "sys/cpu_load.h" #include "sys/cpu_load.h"
#include "sys/esp32_en.h"
#include "sys/task_runner.h" #include "sys/task_runner.h"
#include "comm/ethercat_slave.h" #include "comm/ethercat_slave.h"
#include "comm/ethercat_sync.h" #include "comm/ethercat_sync.h"
#include "comm/ethercat_can_bridge.h" #include "comm/ethercat_can_bridge.h"
#include "comm/esc_exmc_bus.h" #include "comm/esc_exmc_bus.h"
#include "comm/esc_lan9252.h"
extern "C" { extern "C" {
void MainLoop(void); void MainLoop(void);
@@ -235,7 +238,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,
@@ -282,28 +285,27 @@ 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);
/* OP 输入新鲜度不变量 — 输入 PDO 停更则主循环强制重写 */
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()) { if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) {
/* 睡眠挂起期一并静默——守护内部读 SM 起止/长度与 0x204/0x220
* 全是 HBI 访问,会把睡着的芯片叫醒 */
return; return;
} }
const uint32_t nowMs = g_scheduler.tick_1ms; const uint32_t nowMs = g_scheduler.tick_1ms;
@@ -314,14 +316,15 @@ 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 写回路径问题。
*/ */
static void ethercatAlHeartbeat() static void ethercatAlHeartbeat()
{ {
if (!EthercatSync::isActive()) { if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) {
/* 睡眠挂起期静默——读 0x0130/0x0220 即 HBI 访问,读即唤醒 */
return; return;
} }
const uint32_t alDiag = EcatDbg_ReadAlDiag(); const uint32_t alDiag = EcatDbg_ReadAlDiag();
@@ -426,8 +429,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 {
@@ -499,7 +501,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();
@@ -539,11 +540,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()
@@ -555,14 +568,17 @@ 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 请求驱动改造移除,此处仅需消费请求响应的按铃。 */
AppSpiCommandBus.poll(); AppSpiCommandBus.poll();
/* PC9 拉高后 ESP32 起机窗口内的 SPI 补清(GPIO 毛刺会弄脏链路)。 */
Esp32En::poll();
/* 0xB0 进 BOOT:必须每圈检查,不能只靠 1Hz TASK_HEARTBEAT。 /* 0xB0 进 BOOT:必须每圈检查,不能只靠 1Hz TASK_HEARTBEAT。
* 否则 Flash 写/复位被拖到下一秒,ESP32 HELLO 一直 role=app 超时。 */ * 否则 Flash 写/复位被拖到下一秒,ESP32 HELLO 一直 role=app 超时。 */
if (OtaBootRequest::isRebootPending()) { if (OtaBootRequest::isRebootPending()) {
@@ -573,7 +589,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) {
@@ -587,17 +603,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) 完成,此处仅处理低优先级逻辑。
* ESC 睡眠(D1-D3)挂起期整段停调——MainLoop 每圈必读 0x220(AL 事件
* 寄存器,经 HBI),而睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。
*
* 负载口径(恢复 1.5.63):MainLoop **不**计入 busy。
* 实时模式主循环是 NOP 忙等,每圈都调 MainLoop;整段计入会把
* load_perm 抬到恒 ~99%(250Hz 也「占满」,与历史 4kHz≈80% 曲线对不上)。
* 过程数据成本应由 PDI/Sync0 ISR 挂钩体现,见 sys/cpu_load.h。 */
if (!EscLan9252::pmSuspended()) {
MainLoop(); MainLoop();
/* OP 输入新鲜度不变量。与 MainLoop 同拍调用——正常时
* PDI_Isr 已在刷新,这里只做廉价 seq 比对;停更超 5ms 才强制重写。
* 睡眠期与 MainLoop 一并跳过(读 HBI 会唤醒芯片)。 */
EcatDbg_OpInputFreshness();
}
/* 1.5.48:AL Event Mask(0x204) 守护——每圈入口执行,内部 1s 节流。 /* AL Event Mask(0x204) 守护——每圈入口执行,内部 250ms 节流。
* 放 MainLoop 旁而非诊断任务:守护必须独立于诊断打印频率, * 放 MainLoop 旁而非诊断任务:守护必须独立于诊断打印频率,
* OP 等待期(12s)内从站进 SAFEOP 后 1s 内即介入自愈。 */ * OP 等待期(12s)内从站进 SAFEOP 后 1s 内即介入自愈。 */
ethercatAleMaskGuard(); ethercatAleMaskGuard();
@@ -605,7 +632,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
+313
View File
@@ -0,0 +1,313 @@
---
feature: tiered-pdo-mapping
status: designed
updated: 2026-09-12
branch: master (DCU_APP) + main (tools/ecat) — 无 worktree,沿用工作树
commits: <pending delivery>
---
# Tiered PDO Mapping(按电机数分档收缩 SM3)
## Report
(交付后填写)
---
## [S1] Problem
输入 PDO SM3 恒为 **600B**,与本拍实际密排电机数无关:
- 3 电机时仍占用 600B SM 与 ~984B 过程数据帧,100Mbps 上单帧线路时钟地板约 **81µs**。
- 从站稀疏写(1.6.22)只省 PRAM 搬运,**不减线长**。
- 用户场景:电机数长期固定(常见 3 台),偶发插拔;希望档内无感、跨档自动重映射,且重映射 **<500ms**、不 NRST、不撞 1s 过程数据看门狗。
现链路硬约束:
1. SSC `PREOP→SAFEOP` 调 `APPL_GenerateMapping` 得 `nPdInputSize`,`CheckSmSettings` 要求 **ESC SM3.Length == nPdInputSize**(`ecatslv.c:509`)。
2. SOEM `Ibytes` 来自 CoE PDO 映射位数,**不是**从站运行时 tier;主站不改写 SM/FMMU/`Ibytes` 则线帧不变。
3. 完整 `do_op_start`(重枚举 + 250×4ms 喂帧 + 最长数秒等 OP)无法满足 <500ms。
4. `decode_input` 密排魔数 `@35=0xA5` 已可区分布局,但 **段3@544 / 段5@584 写死**,短 SM 下会读越界/错位。
---
## [S2] Design
### S2.1 容量档与线格式
唯一真源(从站头 + 主站常量,数值必须一致):
```
ECAT_PDO_IN_MAX = 600
ECAT_PDO_IN_PREFIX = 40 // 段1 32B + 段2 头 8B,槽区起点 MOTW_OFF_SLOTS=40
ECAT_PDO_IN_XRX = 40 // 段3 长快照
ECAT_PDO_IN_DIAG = 16 // 段5 诊断(实际用 12B,对齐到 8)
ECAT_MOT_SLOT_SIZE = 56
in_bytes(cap) = 40 + cap*56 + 40 + 16 = 96 + 56*cap
```
| 档 | count 范围 | cap | in_bytes | 线过程数据 out+in | 约线路时钟* |
|----|------------|-----|----------|-------------------|-------------|
| A | 0–3 | 3 | **264** | 384+264=648 | ~52µs |
| B | 4–6 | 6 | **432** | 384+432=816 | ~65µs |
| C | 7–9 | 9 | **600** | 384+600=984 | ~81µs(现状)|
\*仅 eth 过程数据字节的 100Mbps 线时钟粗算,不含头/IFG 的完整帧模型见 `ECAT_BEAT_TAIL_REPORT`。
**布局语义(密排,魔数 `@35=0xA5` 不变):**
```
[0..31] 段1 桥状态(与现网一致)
[32..39] 段2 头:count/active/boot/magic/gseq
[40 .. 40+n*56) 密排槽,n = count ≤ cap
[40+cap*56 .. +40) 段3 xrx
[+40 .. +16) 段5 诊断(load/torn/seq/overfull/reltimeout/reject)
```
- **档内**(count 在同一 cap 槽位预算内变化):只更新 `count` 与槽内容,**不改 SM、不 PREOP**。
- **跨档**:走 S2.3 轻量重映射;两端同时切到目标 `in_bytes`。
- 非密排(magic≠0xA5 或 ilen≥640 旧布局)路径保持原样,本特性不碰。
主站由 `Ibytes` 反推 cap(与 tier 一一对应):
```
cap = (Ibytes - 96) / 56 // 264→3, 432→6, 600→9
xrx_off = 40 + cap*56
diag_off = xrx_off + 40
```
禁止再写死 544/584。
### S2.2 从站契约(DCU_APP,目标版本 **1.6.34**)
1. **SII/EEPROM 不改**:`gen_eeprom.py` 仍 `PD_IN_SIZE=600`,冷启动 SM3 默认 600,与旧主站兼容。
2. **`APPL_GenerateMapping`**(`dc_app.c`,`PREOP→SAFEOP` 时调用):
- 读 ESC 当前 SM3 `Length`(`GetSyncMan(PROCESS_DATA_IN)`)。
- 若 `Length` ∈ {264, 432, 600} → `*pInputSize = Length`(从站跟随主站)。
- 否则 → `*pInputSize = ECAT_PDO_IN_MAX`(600,保底)。
- `*pOutputSize` 仍 384,不变。
3. **`APPL_InputMapping`**:`memcpy(pInputData, …, nPdInputSize)`,不再写死 600。
4. **密排填充 / 稀疏写 / 差分写**(`ethercat_can_bridge.cpp` + diff/sparse 开关路径):
- 段3/段5 物理偏移改为 `40 + cap*56` 起,其中 `cap = (nPdInputSize - 96) / 56`(在 Safe/OP 内 `nPdInputSize` 已被 GenerateMapping 更新)。
- 稀疏写范围同步:段A=`0 .. 40+count*56`,段B=`xrx_off .. nPdInputSize`。
- `count > cap` 时本拍只打包前 `cap` 个(并保持现有“撑满回退整包”仅在 cap=9 时语义不变);正常调度不应出现,出现时记诊断。
5. **对象字典 0x6000**:仍按 600B 定义(SDO 可读全镜像);过程数据长度以 SM/`nPdInputSize` 为准。不引入多 TxPDO 映射子索引(v1 范围外)。
6. **版本**:`config.h` patch → **34**,注释写明 tiered SM3 跟随主站。
**从站不主动改 SM 长度**,避免与主站写顺序竞态。
### S2.3 主站契约(tools/ecat / `ecat_native.c`)
#### 常量与状态
```
tier_in_bytes[] = {264, 432, 600} // cap 3/6/9
g_tier_cap // 当前会话生效 cap,默认 9
g_tier_seen_count / g_tier_since // 滞回:观察到的新档 count 与起始时刻
g_tier_last_remap / g_tier_remaps // 诊断
```
`count` 取密排头 `g_head.wcount`(或 `inp[32]`),**不用** `active`(离线电机仍占槽报最后一轮,属于“槽预算”)。
#### 首次进 OP
- `do_op_start` 仍走完整 `config_init` + `config_map`(CoE 得 Ibytes=600)→ **首拍恒 Tier C**。
- 首拍 decode 得到 `count` 后进入滞回观察;不与 SAFEOP→OP 竞态。
#### 跨档轻量重映射 `do_tier_remap(target_cap)`
前置:`g_bus_open && g_op && 循环已 loop_stop(调用方持 g_bus_lock)`。
```
1. 记录 DC 形态 (dc_cycle_us, dc_shift_us) —— 与 wedge 恢复同源
2. OP → PREOP|ACK(不 INIT、不 config_init 重枚举)
- statecheck PREOP ≤400ms;失败则回退完整 do_op_start 一次
3. 写 0x0420 = 10000(1s WD,与 do_op_start 同款)
4. 改写 SM3:
- ec_slave[1].SM[3].SMlength = htoes(target_in_bytes) // StartAddr 仍 0x1880,flags 不动(3-buffer)
- ecx_FPWR / ec_FPWRw 写 ESC SM3 寄存器(与 config_map 写 SM 同基址)
5. 改写输入 FMMU 逻辑长度(仅输入 FMMU,LogStart 不变,LogLength = target_in_bytes)
6. 同步 SOEM 进程数据尺寸:
- ec_slave[1].Ibytes / Ibits = target_in_bytes / *8
- ec_group[0].Ibytes 调整,保证 send/receive_processdata 帧长 = O+new_I
- g_expected_wkc 重算(inputsWKC 对短映射通常仍 1,需实读确认)
7. PREOP → SAFEOP|ACK(≤400ms)—— 此时从站 GenerateMapping 读回新 SM3 长度
8. 短喂帧:1ms 节拍 pump ~150 次(≈150ms,≪1s WD),再请求 OP
9. OP|ACK,继续 pump 直到 AL=OP,总预算 ≤400ms;失败一次则完整 do_op_start
10. g_tier_cap = target_cap;g_rxb_layout 保持 dense(3);清空槽表按 do_op_start 会话语义
(可选:保留 g_slots,避免 remap 窗口 UI 闪空——v1 与 do_op_start 一致选择清空+重建)
```
**目标耗时:** PREOP/SAFEOP/OP 状态检查各一次成功路径通常 10–50ms,加 150ms 喂帧,合计 **远低于 500ms**;全失败回退完整路径仍可恢复(不保证 <500ms,记日志)。
**禁止:** NRST、`ec_close`、改邮箱 SM、改输出 SM2 长度。
#### 滞回与触发点
- 仅在 **循环运行且 OP** 时评估;放在 `wedge_worker` 100ms 节拍(或 pdo_worker 每 N 拍采样后置标志,由 wedge 线程统一处理——**不得在 pdo_worker 内 loop_stop 自己**)。
- 规则:
- `need_cap = cap_for_count(count)`
- 若 `need_cap == g_tier_cap` → 清观察状态
- 若 `need_cap != g_tier_cap`:
- 首次记下 `g_tier_seen_count` 与 `g_tier_since`
- **同一 need_cap 连续 ≥500ms** 才执行 remap
- 期间 count 又回到原档 → 清观察
- `pm/hold` 置位期间不 remap(与楔死看门狗一致)。
- remap 进行中 `g_tier_busy`:wedge 楔死恢复、`/api/motor/start`、SDO 独占路由互斥(复用 `g_bus_lock`)。
#### `decode_input` 密排分支
- 魔数判定不变。
- `cap = (ilen - 96) / 56`,非法(不能整除或 cap∉{3,6,9})时:按 cap=9 且 `off+size>ilen` 则 break 的现有保护,**不崩溃**;记 layout 异常。
- 段3/段5 偏移用 `xrx_off/diag_off`,替换 `XRX_DENSE_OFF_*` / `DIAG_OFF_*_WIN` 常量中的 544/584(常量改为宏函数或运行时变量)。
#### API / 观测
- `/api/motor/state` 或 busKv 增加:`tier_cap`、`tier_in_bytes`、`tier_remaps`、`tier_since`(可选)。
- `/api/pdo/tier`:`{"cap":3|6|9}` 手动钉档(立即 remap,不走 500ms 滞回);`{"auto":true}` 恢复自动。v1 至少实现手动钉档读写。
- `/api/open` 启动日志:`输入 %dB` 打印真实 `Ibytes`(remap 后),去掉写死「600B」文案。
#### 楔死恢复 / start / sdo
- `wedge_worker`、`/api/motor/state` 快路径、`/api/motor/start`:重拉 `do_op_start` 后 **重置 `g_tier_cap=9`**,再允许滞回把档缩回去(简单、可预期)。
- `/api/sdo` 仍 `loop_stop` 不恢复——tier 状态在下次 start 时回到 9,可接受。
### S2.4 前端 / 文档(次要)
- `lab/spi_test.html`:bus 总线卡展示 `tier in_bytes`(若 API 提供);layout=dense 分支的段5 注释改为“偏移随 cap”。
- `lab/pdo_layout.html`:统计卡从「600 B SM3」改为「264/432/600 分档」;**不阻塞固件交付**,可同 PR 或 follow-up。
- 注释清理:避免再写“线速两布局同为 600B”这类在 tier 生效后错误的句子。
### S2.5 错误与回退
| 情况 | 行为 |
|------|------|
| SM3 改写 FPWR 失败 | abort remap,保持旧 Ibytes,日志 WARN;下一滞回窗重试 |
| SAFEOP 失败(SSC 拒 SM) | 读 ALstatuscode;完整 `do_op_start`(回到 600)并记 `tier_remap_fail` |
| OP 失败 | 同上 |
| count 突然 > cap(从站 bug/撕裂) | decode 已有越界 break;从站侧 count>cap 截断;**不**立即 remap,等滞回 |
| 首拍 Ibytes 仍 600 且 count=3 | 正常进入滞回,500ms 后缩到 264 |
| 主站新、从站旧(GenerateMapping 恒 600) | 主站写 SM=264 后 SSC CheckSm 失败 → 回退 600,功能与今日等价 |
| 从站新、主站旧 | SM 仍被旧主站写 600,从站跟随 600,与今日等价 |
### S2.6 不做(Out of Scope 见 S3)但需预留
- 输出 SM2 分档
- 多 TxPDO 赋值(0x1C13 子索引选映射)
- EEPROM/ESI 为每档发一份 SII
- 改 1-buffer 换更大窗口
### S2.7 文件级改动清单(Implement 对照表)
> 行号以 2026-09-12 工作树为准,实现时以符号为准、行号仅作导航。
#### A. 从站 DCU_APP
| # | 文件 | 改什么 |
|---|------|--------|
| A1 | `GD32E508_DCU_APP/comm/ethercat_pdo_size.h` | 新增分档纯宏(C/C++ 共用):`ECAT_PDO_IN_TIERS {264,432,600}`、`ECAT_PDO_TIER_CAPS {3,6,9}`、`ECAT_PDO_IN_PREFIX=40`、`ECAT_PDO_IN_XRX=40`、`ECAT_PDO_IN_DIAG=16`、`ECAT_PDO_IN_BYTES_FOR_CAP(cap)=(96+56*(cap))`、`ECAT_PDO_CAP_FOR_IN_BYTES(ib)`。静态断言:三档均 ≤600 且 `%8==0`,cap=9 时等于现行 `ECAT_PDO_IN_BYTES`。`ECAT_PDO_IN_BYTES` 保留为 **max=600**(数组/EEPROM 上限),语义注释改为「上限,运行期可小于」。 |
| A2 | `GD32E508_DCU_APP/comm/ethercat_can_bridge.h` | 段3/段5 **枚举保持 cap=9 数值**(544/584…)作默认/静态断言基准;新增运行时偏移接口:`uint16_t ecatMotXrxOff(uint8_t cap)` / `ecatMotDiagOff(uint8_t cap)` / `ecatPdoInBytesForCap(uint8_t cap)`(inline 或声明+cpp)。`static_assert` 改为只约束 cap=9 与 max 的首尾相接。注释标明:短档时段3/段5 前移,主站必须按 Ibytes 解。 |
| A3 | `GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp` `buildInput` @286–333 | 段3 填充:`ECAT_XRX_OFF_*` → `xrx = ecatMotXrxOff(cap)` 相对写。`memset(in,0,kEcatPdoInBytes)` 可改为只清 `in_bytes`(或仍清 600,多清无害但稀疏/diff 会多比——**建议只清 in_bytes**)。`cap` 来源见 A5。段1/段2 偏移不变(0–39 + slots)。 |
| A4 | `GD32E508_DCU_APP/comm/ethercat_can_bridge.cpp` `fillMotorRound` @119+ | 逻辑不变;若 `count > cap` 截断到 cap(防越界写到段3)。影子区仍按 max 分配。 |
| A5 | `GD32E508_DCU_APP/comm/ssc/port/dc_app.c` `APPL_GenerateMapping` @355–360 | 读 `GetSyncMan(PROCESS_DATA_IN)`(channel=3)`Length`;∈{264,432,600} 则 `*pInputSize=Length`,否则 600。随后调用新增 `EcatPdo_OnPdoInBytes(*pInputSize)`(或写 `EthercatPdo` 静态),通知填充层当前 in_bytes/cap。输出仍 384。 |
| A6 | `GD32E508_DCU_APP/comm/ssc/port/dc_app.c` `APPL_InputMapping` @408–453 | `EcatPdo_WriteInputs` 之后:`memcpy(pInputData, …, nPdInputSize)`(替换写死 `ECAT_PDO_IN_BYTES`)。 |
| A7 | `GD32E508_DCU_APP/comm/ethercat_pdo.h` / `.cpp` | 新增静态 `s_inBytes`/`s_cap`(默认 600/9)+ `OnPdoInBytes(uint16)`。`fillDiagTail` @114–155:`ECAT_DIAG_OFF_*` → `diag=ecatMotDiagOff(cap)` 相对。`sparseEscWritePlan` @166–194:`ECAT_XRX_OFF_CH`/`kPdoInBytes` → 运行时 `xrx`/`in_bytes`;「撑满」条件改为 `headEnd >= xrx`。`escSeg2WriteLen` @199–216、diff 三区 @258–279:同改。影子数组 `s_escSh2/s_escSh3` **保持按 600 上限分配**,只比较/写 `in_bytes` 内长度。 |
| A8 | `GD32E508_DCU_APP/sys/config.h` @152 | `APP_FW_VERSION_PATCH` 33→**34**,追加 1.6.34 注释:SM3 分档跟随主站(GenerateMapping 读回 SM3.Length)。 |
| A9 | `GD32E508_DCU_APP/comm/ssc/port/gen_eeprom.py` | **不改** PD_IN_SIZE=600(v1 不刷 EEPROM)。可在文件头加一行注释:「运行期主站可把 SM3 改为 264/432,SII 仍 600」。 |
| A10 | `ethercat_diff_stats.cpp` | 若 `kInBytes` 宏绑死 600:改为读 `EthercatPdo::inBytes()` 或仍按 max 快照——**差分仪表在短档下勿拿 600 去比 in_bytes 内容**;实现时优先 `observe` 用当前 in_bytes。 |
#### B. 主站 tools/ecat
| # | 文件 | 改什么 |
|---|------|--------|
| B1 | `ecat_native.c` 顶部常量 @70–133 | 新增:`TIER_IN_BYTES[]`、`tier_cap_for_count(n)`、`tier_in_bytes(cap)`、`dense_xrx_off(ilen)`、`dense_diag_off(ilen)`(`cap=(ilen-96)/56`)。**保留** `XRX_DENSE_OFF_*`/`DIAG_OFF_*_WIN` 仅作文档/默认 cap=9;decode 路径改走函数。 |
| B2 | `ecat_native.c` 状态 @190–226 附近 | 新增:`g_tier_cap`(默认9)、`g_tier_auto`(默认1)、`g_tier_busy`、`g_tier_need_cap`、`g_tier_since`、`g_tier_remaps`、`g_tier_remap_fail`。 |
| B3 | `ecat_native.c` `decode_input` @450–530 | 密排分支:`xch/xda/diag` 用 `dense_*_off(ilen)`。若 `(ilen-96)%56!=0` 或 cap∉{3,6,9}:仍按魔数密排处理但 `off+size>ilen` 则 break(已有),并置 layout 异常标志。段5 读取位置同步改(@559–560 现用 `DIAG_OFF_LOAD_WIN`)。 |
| B4 | `ecat_native.c` 新函数 `do_tier_remap(int target_cap, …)` | 放在 `do_op_start`/`do_op_stop` 附近。实现 S2.3 十步;内部用 `ecx_FPWR`/`ec_FPWRw`:SM3 长度字 @ ESC `0x081A`(`ECT_REG_SM0+3*8+2`),整段 SM 重写可抄 `ethercatconfig.c` `ecx_map_sm` 的 FPWR 模式;输入 FMMU:读当前 FMMU 表,只改输入那条的 `LogLength=target_in`,FPWR 回去;`ec_slave[1].Ibytes=Ibits/8`、`ec_group[0].Ibytes` 收缩/回扩;`g_expected_wkc` 重算。成功:`g_tier_cap=target`;失败:`do_op_start` 回落并 `g_tier_cap=9`。 |
| B5 | `ecat_native.c` `wedge_worker` @3057–3100 | 在 `need` 楔死分支**之前**增加 tier 滞回评估(仅当 `g_loop_running && g_op && !g_tier_busy && !pm_hold && g_tier_auto`):用最新 `g_head.wcount`;同一 `need_cap≠g_tier_cap` 持续 ≥0.5s → `loop_stop` + `do_tier_remap` + `loop_start`。楔死恢复成功路径:`g_tier_cap=9`、清观察。 |
| B6 | `ecat_native.c` `do_op_start` 成功出口 @1178–1207 | `g_tier_cap=9`、清滞回观察。日志 `输入 %dB` 已打真实 Ibytes(@1180)——确认 remap 后再 start 也正确。 |
| B7 | `ecat_native.c` `/api/motor/start` @1987–2046 | 成功 `loop_start` 后同样 `g_tier_cap=9`(若走的是未 remap 的 do_op_start)。日志去掉写死「输入 600B」文案 @2039。 |
| B8 | `ecat_native.c` `/api/motor/state` @2365–2403 | 快路径楔死恢复成功后 `g_tier_cap=9`。`build_state_json` 增加 `tier_cap`/`tier_in_bytes`/`tier_remaps`(函数体搜 `build_state_json`)。 |
| B9 | `ecat_native.c` 新路由 `/api/pdo/tier` | GET:返回 `{ok, auto, tier_cap, tier_in_bytes, remaps, last_ms}`。POST body `{"cap":3\|6\|9}` 或 `{"auto":false\|true}`:钉档时持 `g_bus_lock`、`loop_stop`、`do_tier_remap`、`loop_start`;`auto` 只改标志。 |
| B10 | `ecat_native.c` `pdo_worker` @848–852 | 无需改 Ibytes 来源(已读 `ec_slave[1].Ibytes`)。**禁止**在此 remap。可选:每 N 拍把 `g_head.wcount` 时间戳写给 wedge(直接读 `g_head` 已无锁跨线程——与现状一致,保持)。 |
| B11 | `build_new.bat` / 工程 | 无新源文件则不用改;若 `do_tier_remap` 拆到新 `.c` 再加入 bat。 |
#### C. 前端 tools/ESP32(不阻塞固件)
| # | 文件 | 改什么 |
|---|------|--------|
| C1 | `lab/spi_test.html` | 总线/KV 区:若 API 返回 `tier_in_bytes` 则显示「SM3 264/432/600 · 档」。dense 段5 注释 @1198 改为「偏移随 cap」。CSS/JS 引用若外置需 `?v=`(本页自包含则内联即可)。 |
| C2 | `lab/pdo_layout.html` | 统计卡 @243–244、图例 @819/@841、说明 @264:由「600 B SM3」改为分档说明;图可标三档长度。 |
| C3 | `ECAT_BEAT_TAIL_REPORT.html` 等历史报告 | **不改**(已归档结论);避免改写历史数字。 |
#### D. 明确不改
- `ESP32_COMM/**`
- `gen_eeprom.py` 的 PD_IN_SIZE、`DCU_ESI.xml`
- `soem-src/**`(只调用,不改库;FMMU/SM 用现成 FPWR)
- 输出 SM2、邮箱 SM0/1
#### E. 实现顺序(文件依赖)
```
A1 → A2 → A5/A6(GenerateMapping+InputMapping)
→ A3/A4/A7(填充与稀疏/差分,依赖 A5 通知 in_bytes)
→ A8 版本
B1 → B3 decode → B4 remap → B5/B6/B7 滞回与重置 → B8/B9 API
C1/C2 可后置
```
双端可并行:A 与 B 仅共享数值表;联调以 V3–V8 为准。
---
## [S3] Out of Scope
- ESP32_COMM 固件(不解析 EtherCAT PDO)
- 输出 PDO 384B 收缩
- 刷 EEPROM / 冷启动依赖的 SII 多映射
- 电机 10+ 节点
- 用 tier 直接调 PDO 周期(0.25ms 等)
- 提交/合并(用户明确不自动 commit)
---
## Tasks
- [ ] T1: A1+A2+A5+A6 从站长度宏 + GenerateMapping 跟随 SM3 + InputMapping 按 nPdInputSize —— acceptance: 静态断言 264/432/600;主站手写 SM3=264 后 PREOP→SAFEOP 不报 0x001A/INVALIDSMINCFG;`nPdInputSize` 变 264 (covers: S2.2 S2.7-A)
- [ ] T2: A3+A4+A7 从站密排/稀疏/差分偏移 cap 相对 —— acceptance: 264B SM 下段3/段5 不越界,主站能读 load/torn;count≤3 稀疏写两段正确 (covers: S2.2 S2.7-A; depends: T1)
- [ ] T3: A8 版本 1.6.34 —— acceptance: PATCH=34 + 注释 (covers: S2.2; depends: T1)
- [ ] T4: B1+B3 主站 decode cap 相对 —— acceptance: ilen=264+magic 槽/段3/段5 正确;ilen=600 回归一致 (covers: S2.1 S2.3 S2.7-B)
- [ ] T5: B4 do_tier_remap + Ibytes/FMMU/group —— acceptance: 600→264→600 各一次 AL=OP、wkc 正、成功路径日志 <500ms (covers: S2.3; depends: T4)
- [ ] T6: B5+B6+B7 滞回 + start/wedge 重置 —— acceptance: 稳定 3 电机 0.5–1.0s 进 264;4 电机进 432;楔死后回 9 再缩回 (covers: S2.3; depends: T5)
- [ ] T7: B8+B9 `/api/pdo/tier` 与 state 字段 —— acceptance: POST cap 立即 remap;GET 含 tier_* (covers: S2.3; depends: T5)
- [ ] T8: V1–V10 双端编译+台架 —— acceptance: cbuild/build_native 通过;见 Verification (covers: S2.2 S2.3; depends: T1–T7)
- [ ] T9: C1+C2 前端分档展示 —— acceptance: 页面显示当前档;不阻塞 T8 (covers: S2.4; depends: T7)
---
## Verification(交付证据)
| 步骤 | 命令/操作 | 期望 |
|------|-----------|------|
| V1 | DCU cbuild(既有 Release+GD32E508VE) | 0 error |
| V2 | `build_new.bat` / VS2022 cl 原生主站 | 产出 `ecat_native.new.exe` |
| V3 | 冷启动 600 进 OP(3 电机) | 与今日一致,layout=dense |
| V4 | 等待 ≥1s | 自动 remap 到 264,日志含耗时 |
| V5 | `/api/motor/state` + spi_test | Ibytes/tier=264,count=3,wkc 稳定 |
| V6 | rx 线时(粗) | 过程数据字节 648 vs 984,rx 段应下降(允许 ±15µs 噪声) |
| V7 | 手动 `/api/pdo/tier` cap=9 | 回到 600 |
| V8 | 拔/加第 4 台(或模拟 count) | 滞回后 432 |
| V9 | 旧从站 + 新主站 | SAFEOP 失败路径回 600,总线仍可用 |
| V10 | 楔死/PM hold | hold 中不 remap;恢复后 tier 策略符合 S2.3 |
---
## Journey log(设计期)
1. 线时收益来自 **SM3 变短**,稀疏写不够——与 1.6.22/1.6.25 结论一致。
2. SSC 长度双源校验 ⇒ 必须主从同数;v1 选「主站写 SM,从站 GenerateMapping 读回」,避免 EEPROM 与多 PDO 映射。
3. 首拍恒 600 是为了兼容 config_map/CoE,用 500ms 滞回消化一次 remap,比改 CoE 映射表风险低。
4. 重映射不能走完整 `do_op_start`(重枚举+长喂帧)——独立轻量路径 + 失败回退完整路径。
5. 段3/段5 偏移写死是主站解码的隐雷;Ibytes 分支(ilen&lt;600→slim)会把 264 误判成瘦身布局——必须先用魔数 0xA5 拦下,再按 cap 排偏移。
+81
View File
@@ -0,0 +1,81 @@
---
feature: warm-jump-ecat-probe
status: delivered
updated: 2026-09-16
branch: master
commits: 7d9d9c7..uncommitted
---
# Warm Jump 后 EtherCAT 起板探针修复
## Report
**What was built** — APP 1.6.40:`EscLan9252::init` 起板探针放行门从
`HBI_IDX_0==0x1234` 改为 `BYTE_TEST==0x87654321`。warm jump 残留的
`HBI_IDX_0=0x0054` 只打 WARN 并放行;BYTE_TEST 不过才补一发 RST# 重探一轮。
cold 路径行为不变(仍等芯片出复位到 0x1234 再测 BYTE_TEST)。
**Verification** — 无 ARM 交叉编译环境;静态审查通过(spec 合规 / 正确性 /
代码风格),reviewer 无 critical。上板验收:ECAT 刷完 warm jump 后串口应见
`[ECAT][ESC] init: OK`(可带 `WARN idx0=0x0054`),`[APP] init` 不再报
`skipped/failed: ethercat`,主站可建连;reset 路径回归不破坏。
**Journey log**
- BOOT 1.1.0.18 短路 `escResetForJump` 是刻意实验;APP 侧旧探针把「复位默认值」
当成通路判据,在 warm 残值上必然假阴性。
- 主工作区曾有一份未完成的 1.6.40 草稿;本任务收口并跳过 warm 残值的 300ms 空等。
- 用户选了隔离 worktree,但本环境拦截 `git worktree add`,改动落在主工作区未提交 diff。
## [S1] Problem
刷完固件后 BOOT(1.1.0.18,跳转前 `escResetForJump()` 已短路)warm jump 进 APP:
APP banner 正常,但 EtherCAT 不可用(自检降级 / 主站无法建连)。按复位键走 cold
jump 后 EtherCAT 恢复。同一镜像、同一 APP 启动链,差异只在进入 APP 前 ESC 的
HBI 索引寄存器状态。
## [S2] Design
### 根因
1. BOOT `bootEcatDeinit()` 最后一条 `writeReg32(IRQ_CFG)` 把 LAN9252 的
`HBI_IDX_0` 留在 **0x0054**(索引模式残值,DS 9.5.4.2 复位默认才是 0x1234)。
2. 跳转前硬复位在 BOOT 1.1.0.18 被刻意短路(1.1.0.13/14 实测那发复位会打死
warm jump),warm 路径芯片 **不会** 回到 POR 默认态。
3. APP `EscLan9252::init()` 旧放行门是 `HBI_IDX_0==0x1234`。该值只证明
「芯片刚出过复位」,证明不了 HBI 通路。warm 路径上探针必然假阴性 →
`s_busReady=false` → SSC 跳过 → 降级 ethercat。
4. cold 路径芯片自带 POR,`IDX_0` 天然 0x1234,与 PB10 那发复位无关 ——
这正是「reset 能好、刷完不行」的分界。
### 修复契约
- **BYTE_TEST(0x064==0x87654321)是唯一物理通路门**。`readReg32` 每次访问前
都 `setHbiIndex(addr)`,完整「索引写 + 数据低/高字读」往返,与复位态无关。
- `HBI_IDX_0` 降级为诊断读数:异常时打 WARN 并放行,不再导致 `return false`。
- 任一判据不过 → 补一发 `hardwareReset()` 再探一轮(此刻 SSC 未 MainInit,
二次复位零配置代价)。两轮仍不通 = 总线物理不通,维持降级。
- `s_busReady` 语义同步:EXMC + BYTE_TEST 通过即 true(与 `initial.cpp` 的
`isBusReady()&&isCoreReady()` 门控一致)。
### 不做的事
- 不改 BOOT(1.1.0.18 保持 RST# 短路);APP 侧已能兜住 warm 残值。
- 不把 `HBI_IDX_0==0x1234` 恢复为硬门。
- 不动 PDO/SII/主站解码。
## [S3] Out of Scope
- BOOT `bootEcatDeinit` 的 PD/PE 浮空注释错误、`escResetForJump` 时序重设计。
- PHY 链路重协商窗口与主站 probe 重试(另见 `ecat_boot_handoff_link_issue.md`)。
- 1.6.35~38 启动链实验回退项(已在 1.6.39 完成)。
## Tasks
- [x] T1: 改 `EscLan9252::init` 探针 — 放行门改为 BYTE_TEST,IDX 残值 WARN 放行,失败补一发 RST# 重探 — acceptance: 代码与 [S2] 契约一致 (covers: S2)
- [x] T2: 同步 `esc_lan9252.h` 的 `isBusReady` 文档语义 — acceptance: 注释与实现一致 (covers: S2; depends: T1)
- [x] T3: `config.h` 版本 bump 1.6.40 + 来由注释 — acceptance: PATCH=40 且注释写明 warm-jump 根因 (covers: S2)
- [x] T4: 逻辑核对探针与调用方 — acceptance: 无 ARM 交叉编译环境,完成静态审查;`initial.cpp` 仍用 `isBusReady()&&isCoreReady()` 门控,语义兼容 (covers: S2; depends: T1)
### 工作区说明
用户选择隔离 worktree,但本环境拦截了 `git worktree add`(共享 ref store 保护)。改动落在主工作区未提交 diff,分支意图为 `fix/warm-jump-ecat-probe`。验收通过后需用户确认是否提交到 master。