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