diff --git a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml index 4a24dbe..f51633d 100644 --- a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml +++ b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml @@ -56,6 +56,7 @@ project: - file: ./device/spi/app_spi_cmd_boot_request.cpp - file: ./device/spi/app_spi_cmd_ota_wait.cpp - file: ./device/spi/app_spi_cmd_ping.cpp + - file: ./device/spi/app_spi_cmd_init_status.cpp - file: ./device/spi/app_spi_cmd_can_bridge.cpp - file: ./device/spi/app_spi_cmd_can_burst.cpp - file: ./device/spi/app_spi_cmd_can_feedback.cpp diff --git a/GD32E508_DCU_APP/RTE/Device/GD32E508VE/system_gd32e50x.c b/GD32E508_DCU_APP/RTE/Device/GD32E508VE/system_gd32e50x.c index 68212f9..67ff84c 100644 --- a/GD32E508_DCU_APP/RTE/Device/GD32E508VE/system_gd32e50x.c +++ b/GD32E508_DCU_APP/RTE/Device/GD32E508VE/system_gd32e50x.c @@ -657,13 +657,15 @@ static void system_clock_hxtal(void) /* if fail */ if(0U == (RCU_CTL & RCU_CTL_HXTALSTB)){ - /* HXTAL failed: output diagnostic via USART0 at IRC8M */ - *((volatile uint32_t *)0x40021018U) |= (1U << 2) | (1U << 0); + /* HXTAL failed: output diagnostic via USART0 at IRC8M. + * 修正(1.5.34):原段把 CTL0 写到 STAT0 偏移(0x40013800)且漏开 USART0EN(APB2EN bit14), + * 该诊断从未真正发出;正确布局:CTL0 @ USART0+0x0C,APB2EN bit14|bit2|bit0。 */ + *((volatile uint32_t *)0x40021018U) |= (1U << 14) | (1U << 2) | (1U << 0); { volatile uint32_t _ctl = *((volatile uint32_t *)0x40010804U); _ctl &= ~(0xFU << 4); _ctl |= (0xBU << 4); *((volatile uint32_t *)0x40010804U) = _ctl; } *((volatile uint32_t *)0x40013808U) = 69U; /* 8MHz/115200 */ - *((volatile uint32_t *)0x40013800U) = (1U << 3) | (1U << 13); + *((volatile uint32_t *)0x4001380CU) = (1U << 3) | (1U << 13); { const char *_msg = "HXTAL FAIL\r\n"; for(int i=0; _msg[i]; i++){ *((volatile uint32_t *)0x40013804U) = _msg[i]; @@ -713,13 +715,15 @@ static void system_clock_72m_hxtal(void) /* if fail */ if(0U == (RCU_CTL & RCU_CTL_HXTALSTB)){ - /* HXTAL failed: output diagnostic via USART0 at IRC8M */ - *((volatile uint32_t *)0x40021018U) |= (1U << 2) | (1U << 0); + /* HXTAL failed: output diagnostic via USART0 at IRC8M. + * 修正(1.5.34):原段把 CTL0 写到 STAT0 偏移(0x40013800)且漏开 USART0EN(APB2EN bit14), + * 该诊断从未真正发出;正确布局:CTL0 @ USART0+0x0C,APB2EN bit14|bit2|bit0。 */ + *((volatile uint32_t *)0x40021018U) |= (1U << 14) | (1U << 2) | (1U << 0); { volatile uint32_t _ctl = *((volatile uint32_t *)0x40010804U); _ctl &= ~(0xFU << 4); _ctl |= (0xBU << 4); *((volatile uint32_t *)0x40010804U) = _ctl; } *((volatile uint32_t *)0x40013808U) = 69U; /* 8MHz/115200 */ - *((volatile uint32_t *)0x40013800U) = (1U << 3) | (1U << 13); + *((volatile uint32_t *)0x4001380CU) = (1U << 3) | (1U << 13); { const char *_msg = "HXTAL FAIL\r\n"; for(int i=0; _msg[i]; i++){ *((volatile uint32_t *)0x40013804U) = _msg[i]; @@ -831,13 +835,15 @@ static void system_clock_120m_hxtal(void) /* if fail */ if(0U == (RCU_CTL & RCU_CTL_HXTALSTB)){ - /* HXTAL failed: output diagnostic via USART0 at IRC8M */ - *((volatile uint32_t *)0x40021018U) |= (1U << 2) | (1U << 0); + /* HXTAL failed: output diagnostic via USART0 at IRC8M. + * 修正(1.5.34):原段把 CTL0 写到 STAT0 偏移(0x40013800)且漏开 USART0EN(APB2EN bit14), + * 该诊断从未真正发出;正确布局:CTL0 @ USART0+0x0C,APB2EN bit14|bit2|bit0。 */ + *((volatile uint32_t *)0x40021018U) |= (1U << 14) | (1U << 2) | (1U << 0); { volatile uint32_t _ctl = *((volatile uint32_t *)0x40010804U); _ctl &= ~(0xFU << 4); _ctl |= (0xBU << 4); *((volatile uint32_t *)0x40010804U) = _ctl; } *((volatile uint32_t *)0x40013808U) = 69U; /* 8MHz/115200 */ - *((volatile uint32_t *)0x40013800U) = (1U << 3) | (1U << 13); + *((volatile uint32_t *)0x4001380CU) = (1U << 3) | (1U << 13); { const char *_msg = "HXTAL FAIL\r\n"; for(int i=0; _msg[i]; i++){ *((volatile uint32_t *)0x40013804U) = _msg[i]; @@ -950,13 +956,15 @@ static void system_clock_168m_hxtal(void) /* if fail */ if(0U == (RCU_CTL & RCU_CTL_HXTALSTB)){ - /* HXTAL failed: output diagnostic via USART0 at IRC8M */ - *((volatile uint32_t *)0x40021018U) |= (1U << 2) | (1U << 0); + /* HXTAL failed: output diagnostic via USART0 at IRC8M. + * 修正(1.5.34):原段把 CTL0 写到 STAT0 偏移(0x40013800)且漏开 USART0EN(APB2EN bit14), + * 该诊断从未真正发出;正确布局:CTL0 @ USART0+0x0C,APB2EN bit14|bit2|bit0。 */ + *((volatile uint32_t *)0x40021018U) |= (1U << 14) | (1U << 2) | (1U << 0); { volatile uint32_t _ctl = *((volatile uint32_t *)0x40010804U); _ctl &= ~(0xFU << 4); _ctl |= (0xBU << 4); *((volatile uint32_t *)0x40010804U) = _ctl; } *((volatile uint32_t *)0x40013808U) = 69U; /* 8MHz/115200 */ - *((volatile uint32_t *)0x40013800U) = (1U << 3) | (1U << 13); + *((volatile uint32_t *)0x4001380CU) = (1U << 3) | (1U << 13); { const char *_msg = "HXTAL FAIL\r\n"; for(int i=0; _msg[i]; i++){ *((volatile uint32_t *)0x40013804U) = _msg[i]; @@ -1069,13 +1077,15 @@ static void system_clock_180m_hxtal(void) /* if fail */ if(0U == (RCU_CTL & RCU_CTL_HXTALSTB)){ - /* HXTAL failed: output diagnostic via USART0 at IRC8M */ - *((volatile uint32_t *)0x40021018U) |= (1U << 2) | (1U << 0); + /* HXTAL failed: output diagnostic via USART0 at IRC8M. + * 修正(1.5.34):原段把 CTL0 写到 STAT0 偏移(0x40013800)且漏开 USART0EN(APB2EN bit14), + * 该诊断从未真正发出;正确布局:CTL0 @ USART0+0x0C,APB2EN bit14|bit2|bit0。 */ + *((volatile uint32_t *)0x40021018U) |= (1U << 14) | (1U << 2) | (1U << 0); { volatile uint32_t _ctl = *((volatile uint32_t *)0x40010804U); _ctl &= ~(0xFU << 4); _ctl |= (0xBU << 4); *((volatile uint32_t *)0x40010804U) = _ctl; } *((volatile uint32_t *)0x40013808U) = 69U; /* 8MHz/115200 */ - *((volatile uint32_t *)0x40013800U) = (1U << 3) | (1U << 13); + *((volatile uint32_t *)0x4001380CU) = (1U << 3) | (1U << 13); { const char *_msg = "HXTAL FAIL\r\n"; for(int i=0; _msg[i]; i++){ *((volatile uint32_t *)0x40013804U) = _msg[i]; diff --git a/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp b/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp index 6ef537c..779f41c 100644 --- a/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp +++ b/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp @@ -51,6 +51,9 @@ constexpr uint32_t kPinWr = 5U; /* PD5 */ constexpr uint32_t kPinA1 = 11U; /* PD11 */ constexpr uint32_t kPinA2 = 12U; /* PD12 */ +/* bbDelay/bbReadData 仅被 diagDump 使用,随之一起门控,避免默认关闭时 + * 触发 -Wunused-function;bbDriveData 被 continuityDrive 复用,保持常开。 */ +#if CONFIG_ESC_EXMC_DIAG void bbDelay() { for (volatile uint32_t i = 0U; i < 3000U; i++) { } /* ~40-80us @180MHz,远大于 HBI 时序要求 */ @@ -78,6 +81,7 @@ uint16_t bbReadData() { if (d & (1U << 10)) v |= (uint16_t)(1U << 15); /* D15 */ return v; } +#endif /* CONFIG_ESC_EXMC_DIAG (bbDelay + bbReadData) */ void bbDriveData(uint16_t v) { uint32_t dset = 0U, dclr = 0U, eset = 0U, eclr = 0U; @@ -233,8 +237,12 @@ bool EscExmcBus::init() { ECAT_LOGI("[ECAT][EXMC] init: Region0 enabled, chip ID will be verified by " "ESC driver\r\n"); - /* ---- 硬件诊断: 转储关键寄存器 + GPIO 手动输出测试 ---- */ + /* ---- 硬件诊断: 转储关键寄存器 + GPIO 手动输出测试 ---- + * 1.5.33: 默认关闭。硬件层已确认全通,探针仅换板/补焊排查浮空时经 + * CONFIG_ESC_EXMC_DIAG 临时开启(会拆 EXMC 引脚成 GPIO,正常运行不该跑)。 */ +#if CONFIG_ESC_EXMC_DIAG diagDump(); +#endif s_ok = true; ECAT_LOGI("[ECAT][EXMC] init: OK, bus enabled\r\n"); @@ -243,7 +251,10 @@ bool EscExmcBus::init() { /* ======================================================================== * 硬件诊断: 精简版 —— 只保留核心探活 + bit-bang + 位级回显 + * 1.5.33: 整段由 CONFIG_ESC_EXMC_DIAG 门控,默认不编译。bbDelay/bbReadData/ + * bbDriveData 等位拍辅助函数在上方匿名 namespace 中,continuityDrive 仍复用,保留。 * ======================================================================== */ +#if CONFIG_ESC_EXMC_DIAG void EscExmcBus::diagDump() { /* EtherCAT 同步 ISR 激活期间禁止诊断:本函数会把 EXMC 数据/控制线切成 GPIO, * 并大量 printf,会周期性拆毁运行中的 EtherCAT 总线。 */ @@ -389,6 +400,7 @@ void EscExmcBus::diagDump() { printf("[EXMC-DIAG] === HBI ALIVE ===\r\n"); } } +#endif /* CONFIG_ESC_EXMC_DIAG */ /* ======================================================================== * 通断测试: 网页逐个 pattern 触发,GD32 驱动数据线/控制线输出已知电平, diff --git a/GD32E508_DCU_APP/comm/esc_lan9252.cpp b/GD32E508_DCU_APP/comm/esc_lan9252.cpp index 4cd2424..5c16cca 100644 --- a/GD32E508_DCU_APP/comm/esc_lan9252.cpp +++ b/GD32E508_DCU_APP/comm/esc_lan9252.cpp @@ -14,7 +14,12 @@ * EXMC_A[24:0],即 EXMC_A16=CPU 偏移 bit17(0x20000)、EXMC_A17=bit18(0x40000)。 * LAN9252 手册 Table 9-3:A2=1 选 HBI_DATA_0,A1 选低/高半字。 * - * 所有 ESC 寄存器/PDI 内存访问均通过"写 IDX -> 读写 DATA"完成。 + * 访问模型(1.5.39 按 DS 12.11/表 12-13 修正): + * HBI 索引直接寻址的只有 LAN9252 系统 CSR(BYTE_TEST 0x064 等)与访问端口; + * EtherCAT 核心 CSR(0x0000-0x0FFF)经 ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300) + * 间接访问,过程 RAM(0x1000-0x1FFF)经 PRAM FIFO(0x000-0x03C + 0x308-0x314) 访问。 + * 1.5.37 及之前把索引空间当统一寻址,导致全部核心 CSR 读 0/写丢弃—— + * 主站恒读 AL=INIT 的真正根因(网络侧寄存器诊断证实 EEPROM/核心一直正常)。 */ #include "comm/esc_lan9252.h" #include "comm/esc_exmc_bus.h" @@ -39,6 +44,34 @@ constexpr uint32_t HBI_DATA0_HIGH_OFF = 0x60000U; /* A2=1(EXMC_A17), A1=1(EXMC * 芯片身份探针用 BYTE_TEST 寄存器(0x064),不是 ESC 0x0000(那是类型寄存器=0xC0)。 */ constexpr uint16_t LAN9252_REG_BYTE_TEST = 0x0064U; /**< 字节顺序测试寄存器(默认 0x87654321,LAN9252 手册 16.1.2)。 */ constexpr uint32_t LAN9252_BYTE_TEST_VALUE = 0x87654321U; /**< BYTE_TEST 默认值(小端序)。 */ +constexpr uint16_t ESC_REG_DL_STATUS = 0x0110U; /**< DL Status:bit0 = PDI operational / EEPROM loaded。 */ +constexpr uint16_t ESC_REG_EEP_CSR = 0x0502U; /**< EEPROM Control/Status:bit11=校验错,bit12=加载错(0=OK)。 */ + +/* LAN9252 系统中断寄存器(HBI 索引直接可达,DS 8.3 表 0.1)。 + * IRQ 引脚上电默认全关:IRQ_CFG.IRQ_EN=0 且 INT_EN.ECAT_INT_EN=0, + * 不配这两个寄存器则 PB1 永远收不到中断,SSC 的 PDI_Isr 从不执行。 */ +constexpr uint16_t LAN9252_REG_IRQ_CFG = 0x0054U; /**< 中断配置:bit8=IRQ_EN,bit4=IRQ_POL,bit0=IRQ_TYPE。 */ +constexpr uint16_t LAN9252_REG_INT_STS = 0x0058U; /**< 中断状态(R/WC):bit0=ECAT_INT。 */ +constexpr uint16_t LAN9252_REG_INT_EN = 0x005CU; /**< 中断使能:bit0=ECAT_INT_EN。 */ +constexpr uint32_t IRQ_CFG_IRQ_EN = 1UL << 8; /**< IRQ 引脚输出总闸。 */ +constexpr uint32_t IRQ_CFG_IRQ_POL = 1UL << 4; /**< 1=高有效,0=低有效(开漏时忽略本位)。 */ +constexpr uint32_t IRQ_CFG_IRQ_TYPE = 1UL << 0; /**< 1=推挽,0=开漏(开漏强制低有效)。 */ +constexpr uint32_t INT_EN_ECAT_INT = 1UL << 0; /**< 允许 EtherCAT 事件驱动 IRQ 引脚。 */ + +/* LAN9252 直接映射的 ESC 访问端口(DS 表 12-13;经 HBI 索引直接读写) */ +constexpr uint16_t ECAT_PRAM_RD_DATA = 0x0000U; /**< PRAM 读 FIFO(0x00-0x1C 别名,功能相同,突发用) */ +constexpr uint16_t ECAT_PRAM_WR_DATA = 0x0020U; /**< PRAM 写 FIFO(0x20-0x3C 别名) */ +constexpr uint16_t ECAT_CSR_DATA_REG = 0x0300U; /**< ESC 核心 CSR 间接访问数据寄存器 */ +constexpr uint16_t ECAT_CSR_CMD_REG = 0x0304U; /**< ESC 核心 CSR 间接访问命令寄存器 */ +constexpr uint16_t ECAT_PRAM_RD_ADDR_LEN = 0x0308U; /**< PRAM 读地址/长度 */ +constexpr uint16_t ECAT_PRAM_RD_CMD = 0x030CU; /**< PRAM 读命令/状态 */ +constexpr uint16_t ECAT_PRAM_WR_ADDR_LEN = 0x0310U; /**< PRAM 写地址/长度 */ +constexpr uint16_t ECAT_PRAM_WR_CMD = 0x0314U; /**< PRAM 写命令/状态 */ +constexpr uint32_t CSR_BUSY = 1UL << 31; /**< CSR_CMD:写 1 启动,完成自清 */ +constexpr uint32_t CSR_RNW = 1UL << 30; /**< CSR_CMD:1=读 0=写 */ +constexpr uint32_t PRAM_BUSY = 1UL << 31; /**< PRAM_*_CMD:写 1 启动,完成自清 */ +constexpr uint32_t PRAM_AVAIL = 1UL << 0; /**< PRAM_*_CMD:数据可用(读)/空间可用(写) */ +constexpr uint32_t POLL_MAX = 2000UL; /**< busy/avail 轮询上限(~ms 级,防芯片异常时死等) */ __attribute__((section(".ramfunc"), noinline)) void setHbiIndex(uint16_t addr) @@ -65,10 +98,9 @@ void writeDataLow(uint16_t v) } __attribute__((section(".ramfunc"), noinline)) -uint16_t readReg16(uint16_t addr) +void writeDataHigh(uint16_t v) { - setHbiIndex(addr); - return readDataLow(); + EscExmcBus::write16(HBI_DATA0_HIGH_OFF, v); } __attribute__((section(".ramfunc"), noinline)) @@ -81,14 +113,16 @@ uint32_t readReg32(uint16_t addr) } __attribute__((section(".ramfunc"), noinline)) -void writeReg16(uint16_t addr, uint16_t v) +void writeReg32(uint16_t addr, uint32_t v) { setHbiIndex(addr); - writeDataLow(v); + writeDataLow((uint16_t)(v & 0xFFFFU)); + writeDataHigh((uint16_t)(v >> 16)); } } /* anonymous namespace */ bool EscLan9252::s_busReady = false; +bool EscLan9252::s_coreReady = false; bool EscLan9252::s_resetDone = false; void EscLan9252::hardwareReset() @@ -155,6 +189,7 @@ uint8_t EscLan9252::nrstDrive(uint8_t action) bool EscLan9252::init() { s_busReady = false; /* 1.5.27:s_busReady 语义 = init 完整成功(EXMC+探针+BYTE_TEST) */ + s_coreReady = false; /* 1.5.39:s_coreReady 语义 = EEPROM 加载完成、EtherCAT 核心使能 */ /* 使能 DWT 周期计数器,供 hw_gd32exmc/SSC 执行时间测量 */ CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; @@ -203,9 +238,151 @@ bool EscLan9252::init() } printf("[ECAT][ESC] init: OK, HBI alive (IDX=0x1234, BYTE_TEST=0x87654321)\r\n"); s_busReady = true; + + /* 等待 PDI 就绪、EtherCAT 核心可交互(1.5.37 新增,1.5.39 改经 CSR 间接端口读—— + * 此前直读 HBI 索引,DL_STATUS 属 EtherCAT 核心寄存器,根本不在直接地址空间)。 + * 判据:DL_STATUS(0x0110) bit0 = PDI operational / EEPROM loaded。 + * 网络侧诊断已证实本板 EEPROM 加载与核心均正常,此等待应几乎立即通过; + * 超时意味着 PDI 真的未就绪(EEPROM 内容/I2C/strap 问题),此时 SSC 的 + * AL Event Mask/邮箱 SM 配置会落空,由 initial.cpp 按 isCoreReady() 门控跳过。 */ + uint16_t dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U); + uint32_t waited = 0U; + while ((dlStatus & 0x0001U) == 0U && waited < CONFIG_ESC_HBI_POLL_TIMEOUT_MS) { + SysTickTimer::delayMs(CONFIG_ESC_HBI_POLL_STEP_MS); + waited += CONFIG_ESC_HBI_POLL_STEP_MS; + dlStatus = (uint16_t)csrRead(ESC_REG_DL_STATUS, 2U); + } + if ((dlStatus & 0x0001U) != 0U) { + s_coreReady = true; + printf("[ECAT][ESC] PDI operational, EtherCAT core ready (DL_STATUS=0x%04X, waited %ums)\r\n", + (unsigned)dlStatus, (unsigned)waited); + enableEcatIrqOutput(); + } else { + printf("[ECAT][ESC] init: FAIL, PDI not operational after %ums " + "(DL_STATUS=0x%04X EEP_CSR=0x%04X)\r\n", + (unsigned)CONFIG_ESC_HBI_POLL_TIMEOUT_MS, (unsigned)dlStatus, + (unsigned)csrRead(ESC_REG_EEP_CSR, 2U)); + printf("[ECAT][ESC] hint: 用 tools/ecat/ecat_esc_regs.py 从网络侧读 0x0502 错误位区分 EEPROM/I2C/内容问题\r\n"); + } return true; } +/** + * @brief 使能 LAN9252 的 IRQ 引脚输出,让 EtherCAT 事件真正到达 GD32 的 PB1。 + * @note 1.5.43 新增。IRQ_CFG(0x054)/INT_EN(0x05C) 都是 LAN9252 系统 CSR + * (HBI 索引直接可达,不走 CSR 间接端口),复位默认值均为 0,即 + * IRQ_EN=0(引脚永久去断言) 且 ECAT_INT_EN=0(EtherCAT 事件不上报)。 + * 此前固件从未配置它们,导致 PDI_Isr 一次都没被调用过:SSC 因此 + * 永远等不到 PROCESS_OUTPUT_EVENT、bEcatFirstOutputsReceived 恒 FALSE, + * SAFEOP->OP 被无限挂起(表现为主站 "进 OP 失败 code=0x0000")。 + * 极性必须与 ethercat_sync.cpp 的 EXTI 配置一致:那里是上拉输入 + + * 下降沿触发,故此处选推挽(IRQ_TYPE=1) + 低有效(IRQ_POL=0)。 + * 注意 AL Event Mask(0x204) 由 SSC 在状态迁移时自行维护,不在此处设置。 + */ +void EscLan9252::enableEcatIrqOutput() +{ + /* 先清掉可能悬留的状态位(R/WC:写 1 清零),避免使能瞬间就有一个陈旧电平。 */ + writeReg32(LAN9252_REG_INT_STS, INT_EN_ECAT_INT); + /* 允许 EtherCAT 事件汇入 IRQ。 */ + writeReg32(LAN9252_REG_INT_EN, INT_EN_ECAT_INT); + /* 打开引脚总闸:推挽 + 低有效(IRQ_POL=0 即不置该位),去断言间隔保持 0=禁用。 */ + writeReg32(LAN9252_REG_IRQ_CFG, IRQ_CFG_IRQ_EN | IRQ_CFG_IRQ_TYPE); + + const uint32_t cfg = readReg32(LAN9252_REG_IRQ_CFG); + const uint32_t en = readReg32(LAN9252_REG_INT_EN); + if ((cfg & IRQ_CFG_IRQ_EN) != 0U && (en & INT_EN_ECAT_INT) != 0U) { + printf("[ECAT][ESC] IRQ output enabled (IRQ_CFG=0x%08lX INT_EN=0x%08lX, push-pull active-low)\r\n", + (unsigned long)cfg, (unsigned long)en); + } else { + printf("[ECAT][ESC] WARN: IRQ enable readback failed (IRQ_CFG=0x%08lX INT_EN=0x%08lX)" + " -> PDI_Isr 不会触发,SAFEOP->OP 会卡住\r\n", + (unsigned long)cfg, (unsigned long)en); + } +} + +uint32_t EscLan9252::csrRead(uint16_t addr, uint8_t size) +{ + /* DS 12.11.1:写 CSR_CMD(BUSY|R_nW|SIZE|ADDR) 启动,busy 自清后读 CSR_DATA; + * 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。 */ + uint32_t n = POLL_MAX; + while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {} + writeReg32(ECAT_CSR_CMD_REG, CSR_BUSY | CSR_RNW | ((uint32_t)size << 16) | addr); + n = POLL_MAX; + while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {} + return readReg32(ECAT_CSR_DATA_REG); +} + +void EscLan9252::csrWrite(uint16_t addr, uint32_t data, uint8_t size) +{ + /* DS 12.11.2:先写 CSR_DATA(数据在低位),再写 CSR_CMD(BUSY|SIZE|ADDR,R_nW=0)。 */ + uint32_t n = POLL_MAX; + while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {} + writeReg32(ECAT_CSR_DATA_REG, data); + writeReg32(ECAT_CSR_CMD_REG, CSR_BUSY | ((uint32_t)size << 16) | addr); + n = POLL_MAX; + while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {} +} + +void EscLan9252::pramRead(uint16_t addr, uint8_t *buf, uint16_t len) +{ + /* DS 12.11.3:写 ADDR_LEN 再置 BUSY 启动;引擎按 DWORD 向下对齐填充读 FIFO, + * 首 DWORD 有效字节从 (addr&3) 起(表 12-7),末 DWORD 尾部无效(表 12-8)。 + * 别名口 0x00-0x1C 功能相同,轮换寻址即官方突发用法;每读满一个 DWORD 弹一项。 */ + if (len == 0U) { + return; + } + writeReg32(ECAT_PRAM_RD_ADDR_LEN, ((uint32_t)len << 16) | addr); + writeReg32(ECAT_PRAM_RD_CMD, PRAM_BUSY); + uint16_t done = 0U; + uint16_t alias = 0U; + while (done < len) { + uint32_t n = POLL_MAX; + while ((readReg32(ECAT_PRAM_RD_CMD) & PRAM_AVAIL) == 0U && --n != 0U) {} + if (n == 0U) { + break; /* 芯片异常:避免死等,交出残缺数据 */ + } + const uint32_t dw = readReg32((uint16_t)(ECAT_PRAM_RD_DATA + alias)); + alias = (uint16_t)((alias + 4U) & 0x1FU); + const uint8_t lead = (done == 0U) ? (uint8_t)(addr & 3U) : 0U; + const uint8_t take = (uint8_t)((len - done) < (4U - lead) ? (len - done) : (4U - lead)); + for (uint8_t i = 0U; i < take; ++i) { + buf[done + i] = (uint8_t)(dw >> (8U * (lead + i))); + } + done = (uint16_t)(done + take); + } +} + +void EscLan9252::pramWrite(uint16_t addr, const uint8_t *buf, uint16_t len) +{ + /* DS 12.11.4:写 ADDR_LEN 再置 BUSY 启动;首 DWORD 有效字节从 (addr&3) 起, + * 引擎只取有效字节;别名口 0x20-0x3C 轮换;写满一个 DWORD 推一项。 */ + if (len == 0U) { + return; + } + writeReg32(ECAT_PRAM_WR_ADDR_LEN, ((uint32_t)len << 16) | addr); + writeReg32(ECAT_PRAM_WR_CMD, PRAM_BUSY); + uint16_t done = 0U; + uint16_t alias = 0U; + while (done < len) { + uint32_t n = POLL_MAX; + while ((readReg32(ECAT_PRAM_WR_CMD) & PRAM_AVAIL) == 0U && --n != 0U) {} + if (n == 0U) { + break; /* 芯片异常:放弃剩余字节 */ + } + const uint8_t lead = (done == 0U) ? (uint8_t)((uint16_t)(addr + done) & 3U) : 0U; + const uint8_t take = (uint8_t)((len - done) < (4U - lead) ? (len - done) : (4U - lead)); + uint32_t dw = 0U; + for (uint8_t i = 0U; i < take; ++i) { + dw |= (uint32_t)buf[done + i] << (8U * (lead + i)); + } + writeReg32((uint16_t)(ECAT_PRAM_WR_DATA + alias), dw); + alias = (uint16_t)((alias + 4U) & 0x1FU); + done = (uint16_t)(done + take); + } + uint32_t n = POLL_MAX; + while ((readReg32(ECAT_PRAM_WR_CMD) & PRAM_BUSY) != 0U && --n != 0U) {} +} + uint32_t EscLan9252::readChipId() { /* INIT-ONLY:init 阶段中断未使能,直接读取 BYTE_TEST(32-bit) */ @@ -214,10 +391,11 @@ uint32_t EscLan9252::readChipId() uint16_t EscLan9252::initOnlyReadReg(uint16_t addr) { - return readReg16(addr); + /* 1.5.39:ESC 核心 CSR 经间接端口读(此前直读 HBI 索引,核心寄存器不在直接空间,恒 0) */ + return (uint16_t)csrRead(addr, 2U); } void EscLan9252::initOnlyWriteReg(uint16_t addr, uint16_t v) { - writeReg16(addr, v); + csrWrite(addr, v, 2U); } diff --git a/GD32E508_DCU_APP/comm/esc_lan9252.h b/GD32E508_DCU_APP/comm/esc_lan9252.h index 836e27b..0c5fb42 100644 --- a/GD32E508_DCU_APP/comm/esc_lan9252.h +++ b/GD32E508_DCU_APP/comm/esc_lan9252.h @@ -1,8 +1,14 @@ /** * @file esc_lan9252.h * @brief LAN9252 EtherCAT 从站控制器(ESC)驱动接口。 - * @note 通过 EscExmcBus 16-bit 并行总线,以 HBI Indexed Address Mode 访问 - * LAN9252 寄存器与 PDI 内存:先写 HBI_IDX_0,再读写 HBI_DATA_0。 + * @note 通过 EscExmcBus 16-bit 并行总线,以 HBI Indexed Address Mode 访问 LAN9252。 + * 访问模型(1.5.39 按 DS 12.11 修正,此前误以为整个 16 位索引空间统一可寻址): + * - 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 直接读写; + * - EtherCAT 核心 CSR(0x0000-0x0FFF:AL Status、SyncManager、EEPROM 接口等) + * 不在直接空间,必须经 ECAT_CSR_CMD/CSR_DATA 间接访问(csrRead/csrWrite); + * - 过程 RAM(0x1000-0x1FFF:邮箱/PDO)必须经 PRAM FIFO(pramRead/pramWrite)。 * * 接口调用约束(避免 EXMC 总线争用): * - 运行期 ESC 寄存器/PDI 访问统一走 SSC 移植层 hw_gd32exmc.cpp 的 HW 系列函数 @@ -46,18 +52,49 @@ public: */ static uint32_t readChipId(); - /** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SSC 移植层(HW_*)。 */ + /** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SSC 移植层(HW_*)。 + * @note 1.5.39 起经 CSR_CMD/CSR_DATA 间接端口读 ESC 核心 CSR(此前直读 HBI 索引, + * 而 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 与访问端口 + * (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 上下文)。 */ + static uint32_t csrRead(uint16_t addr, uint8_t size); + + /** @brief ESC 核心 CSR 间接写(ECAT_CSR_CMD/DATA 端口)。约束同 csrRead。 */ + static void csrWrite(uint16_t addr, uint32_t data, uint8_t size); + + /** @brief 过程 RAM(0x1000-0x1FFF)经 PRAM 读 FIFO 读取 len 字节到 buf。 */ + static void pramRead(uint16_t addr, uint8_t *buf, uint16_t len); + + /** @brief 过程 RAM(0x1000-0x1FFF)经 PRAM 写 FIFO 写入 len 字节。 */ + static void pramWrite(uint16_t addr, const uint8_t *buf, uint16_t len); + /** @brief EXMC 总线 + HBI_IDX_0 探针 + BYTE_TEST 全部通过(init 成功)后为 true。 * @note 与 init() 返回值一致,可作运行期查询。 */ static bool isBusReady() { return s_busReady; } + /** @brief EEPROM(SII) 加载完成、EtherCAT 核心使能(1.5.39)。 + * @note false 时核心寄存器对 PDI 读全 0、写静默丢弃,SSC MainInit 会全部 + * 落空(AL Event Mask/邮箱 SM 配置写不进),调用方必须跳过 SSC 初始化。 */ + static bool isCoreReady() { return s_coreReady; } + private: + /** @brief 使能 LAN9252 的 IRQ 引脚输出(IRQ_CFG 0x054 / INT_EN 0x05C)。 + * @note 1.5.43 新增,由 init() 在 PDI/核心就绪后调用。两寄存器复位默认全 0, + * 不配则 EtherCAT 事件永不到达 PB1、SSC 的 PDI_Isr 从不执行, + * bEcatFirstOutputsReceived 恒 FALSE 使 SAFEOP->OP 被无限挂起。 */ + static void enableEcatIrqOutput(); + static bool s_busReady; /**< init 完整成功(EXMC+IDX 探针+BYTE_TEST)后置位。 */ + static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位(1.5.39)。 */ static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */ }; diff --git a/GD32E508_DCU_APP/comm/ethercat_slave.cpp b/GD32E508_DCU_APP/comm/ethercat_slave.cpp index 8f0d221..7793e6c 100644 --- a/GD32E508_DCU_APP/comm/ethercat_slave.cpp +++ b/GD32E508_DCU_APP/comm/ethercat_slave.cpp @@ -63,16 +63,109 @@ void EthercatSlave::init() /* AL Status Code(寄存器 0x0130 高 8 位):非 0 表示从站进入错误状态 * (如 SM Watchdog=0x1B、FMMU 配置错、邮箱协议错),需主站介入。 */ + /* ============================================================ + * bring-up 诊断(1.5.35 引入,1.5.39 重新定性):经 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 故障, + * 交叉验证用 tools/ecat/ecat_esc_regs.py 从网络侧读同样的寄存器。 + * 本段在 EthercatSync::init() 使能 EXTI 之前执行,满足 initOnly* 的 INIT-ONLY 约束。 + * ============================================================ */ + uint16_t dlStatus = 0U; + if (chipIdOk) { + /* (1) DL Status 0x0110 bit0 = PDI operational。 + * ESC 只有认为 PDI 就绪时,才接受 PDI 侧(GD32 经 HBI)写入的 AL Status; + * 该位为 0 时 SSC 的 SetALStatus 被静默丢弃 -> 主站恒读 INIT/code=0。 */ + dlStatus = EscLan9252::initOnlyReadReg(0x0110U); + /* (2) ESC Configuration 0x0140:PDI 类型是否真被 EEPROM 加载为 HBI 16-bit(0x8D)。 + * 读回 0x00/0xFF 说明 EEPROM 的 PDI Control 未生效、ESC 的 PDI 没启用 + * (此时 HBI 读寄存器仍可能照常工作,极易误判为"硬件全通")。 */ + const uint16_t escCfg = EscLan9252::initOnlyReadReg(0x0140U); + /* (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 全部代表正常。 + * 真正的"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 | " + "ESC_CFG(0x140)=0x%04X pdi_type=0x%02X(expect 0x8D) | " + "EEP_CSR(0x502)=0x%04X crc_err=%u load_err=%u ack_err=%u wr_err=%u busy=%u" + " (全 0=正常)\r\n", + (unsigned)dlStatus, (unsigned)(dlStatus & 0x0001U), + (unsigned)escCfg, (unsigned)(escCfg & 0x00FFU), + (unsigned)eepCsr, + (unsigned)((eepCsr >> 11U) & 0x1U), + (unsigned)((eepCsr >> 12U) & 0x1U), + (unsigned)((eepCsr >> 13U) & 0x1U), + (unsigned)((eepCsr >> 14U) & 0x1U), + (unsigned)((eepCsr >> 15U) & 0x1U)); + + /* (4) CSR 端口写回环:用 AL Event Mask(0x0204)做写-读-还原。该寄存器按设计 + * 就是 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 一直正常。 */ + const uint16_t maskSaved = EscLan9252::initOnlyReadReg(0x0204U); + EscLan9252::initOnlyWriteReg(0x0204U, 0x0A50U); + const uint16_t maskRead = EscLan9252::initOnlyReadReg(0x0204U); + EscLan9252::initOnlyWriteReg(0x0204U, maskSaved); /* 还原,交回 SSC 管理 */ + if (maskRead == 0x0A50U) { + LOG_ERR("[ECAT][DIAG] CSR write loopback PASS (0x204 wrote 0x0A50 read back OK)\r\n"); + } else { + LOG_ERR("[ECAT][DIAG] CSR write loopback FAIL: wrote 0x0A50 read 0x%04X" + " -> 先查 CSR_CMD(0x304) busy 是否自清/EXMC 时序,再疑硬件\r\n", + (unsigned)maskRead); + } + } + + /* 是否有主站在线: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); + * 空载时为 0x5503(三口 loop closed、link/comm 全 0)。 */ + const bool anyPhyLink = chipIdOk && ((dlStatus & 0x0070U) != 0U); + if (!chipIdOk) { LOG_ERR("[ECAT][SLAVE] ESC NOT ready, sync ISR disabled (check EXMC wiring/timing)\r\n"); } else if (s_alState < AL_STATE_SAFEOP) { - LOG_ERR("[ECAT][SLAVE] chip id PASS, but AL state=0x%02X(%s) < SAFEOP(0x%02X), code=0x%02X\r\n", - (unsigned)s_alState, alStateName, (unsigned)AL_STATE_SAFEOP, (unsigned)alCode); - LOG_ERR("[ECAT][SLAVE] SYNC0 will not fire, full ESC config requires SSC integration\r\n"); + /* 区分"等主站"与"真故障":两者 AL 都 < SAFEOP,但只有后者需要排查。 + * 1.5.40 及之前一律打 FAIL(partial),而冷启动无主站时必然走到这里, + * 与紧随其后的 "SSC MainInit OK" 自相矛盾,极易被误读为初始化失败。 */ + if (!anyPhyLink) { + LOG_ERR("[ECAT][SLAVE] WAIT_MASTER: no PHY link (DL_STATUS=0x%04X), " + "AL=0x%02X(%s) 由主站驱动,空载停在 INIT 属预期\r\n", + (unsigned)dlStatus, (unsigned)s_alState, alStateName); + } else { + LOG_ERR("[ECAT][SLAVE] chip id PASS, PHY link up (DL_STATUS=0x%04X) but " + "AL state=0x%02X(%s) < SAFEOP(0x%02X), code=0x%02X\r\n", + (unsigned)dlStatus, (unsigned)s_alState, alStateName, + (unsigned)AL_STATE_SAFEOP, (unsigned)alCode); + LOG_ERR("[ECAT][SLAVE] SYNC0 未触发:检查主站是否逐级 write_state" + " (PREOP->config_map->SAFEOP->OP) 并已配置 DC\r\n"); + } } else { ECAT_LOGI("[ECAT][SLAVE] ESC ready, chip id=0x%08lX, AL state=0x%02X(%s), code=0x%02X\r\n", (unsigned long)s_chipId, (unsigned)s_alState, alStateName, (unsigned)alCode); } - LOG_ERR("[ECAT][SLAVE] init: %s\r\n", s_escReady ? "OK" : "FAIL(partial)"); + /* 终判措辞:PDI/EEPROM 就绪但只差主站时报 WAIT_MASTER 而非 FAIL——本函数的 + * s_escReady 要求 AL>=SAFEOP,无主站永远达不到,而 SSC 放行门控实际用的是 + * isBusReady()&&isCoreReady()(见 initial.cpp),两者结论相反不应都叫 FAIL。 */ + const char *verdict; + if (s_escReady) { + verdict = "OK"; + } else if (chipIdOk && EscLan9252::isCoreReady() && !anyPhyLink) { + verdict = "WAIT_MASTER (PDI 就绪,等主站接入)"; + } else { + verdict = "FAIL(partial)"; + } + LOG_ERR("[ECAT][SLAVE] init: %s\r\n", verdict); } diff --git a/GD32E508_DCU_APP/comm/ssc/port/dc_app.c b/GD32E508_DCU_APP/comm/ssc/port/dc_app.c index f96a74a..b42f3c6 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/dc_app.c +++ b/GD32E508_DCU_APP/comm/ssc/port/dc_app.c @@ -20,6 +20,7 @@ #include "dc_app.h" #include +#include /* offsetof:下方布局守卫断言使用 */ /*--------------------------------------------------------------------------- * el9800appl.h 桩 include (SSC 内部需要) @@ -49,6 +50,13 @@ TOBJ6000; static TOBJ7000 s_outputObj = {4, {0, 0, 0, 0}}; static TOBJ6000 s_inputObj = {4, {0, 0, 0, 0}}; +/* 布局守卫(1.5.42):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"); +_Static_assert(offsetof(TOBJ6000, u64Data) == 2, "TOBJ6000 未紧排:检查 OBJ_STRUCT_PACKED_START"); +_Static_assert(sizeof(TOBJ7000) == 34, "TOBJ7000 尺寸应为 2+32"); + /*--------------------------------------------------------------------------- * SYNC 诊断对象 0x2000 (只读, 主站 SDO 轮询) * sub1=SYNC0 计数 sub2=SYNC1 计数 sub3=ESC IRQ 计数 sub4=uptime 秒 @@ -66,6 +74,9 @@ TOBJ2000; static TOBJ2000 s_syncDiag = {4, 0, 0, 0, 0}; +/* 同上:UINT32 首条目必须落在字节 2(未紧排时为 4)。 */ +_Static_assert(offsetof(TOBJ2000, u32Sync0) == 2, "TOBJ2000 未紧排:检查 OBJ_STRUCT_PACKED_START"); + /* EthercatSync 计数器 getter (C++ 侧 extern "C" 导出) */ extern UINT32 EcatDbg_GetSync0(void); extern UINT32 EcatDbg_GetSync1(void); @@ -113,29 +124,57 @@ static TOBJ_PDO_MAPPING sTxPDOMap = {4, { /*--------------------------------------------------------------------------- * Entry Descriptions + * + * 重要(1.5.42 修正): 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 侥幸通过, 正是越界特征)。 + * 另: 可被 PDO 映射的数据子条目必须带 OBJACCESS_RxPDOMAPPING/TxPDOMAPPING, + * 否则主站校验 0x1600/0x1A00 的映射目标时会拒绝(官方 sampleappl.h 同此)。 *---------------------------------------------------------------------------*/ -/* PDO Assign (0x1C12/0x1C13): Sub0=UINT8 count, Sub1..n=UINT16 index */ +/* PDO Assign (0x1C12/0x1C13): ARR, Sub0=UINT8 count, Sub1..n=UINT16 index。 + * ARR 只需 2 项(Sub0 + 通用项), 由 OBJ_GetEntryDesc 复用 [1]。 */ OBJCONST TSDOINFOENTRYDESC OBJMEM asPDOAssignEntryDesc[] = { {DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, {DEFTYPE_UNSIGNED16, 0x10, ACCESS_READ} }; -/* PDO Mapping (0x1600/0x1A00): Sub0=UINT8 count, Sub1..n=UINT32 mapping */ +/* PDO Mapping (0x1600/0x1A00): REC 含 4 个映射条目 -> 必须给 Sub0..Sub4 共 5 项。 */ OBJCONST TSDOINFOENTRYDESC OBJMEM asPDOMappingEntryDesc[] = { - {DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, - {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ} + {DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* Sub0: 条目数 */ + {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* Sub1 */ + {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* Sub2 */ + {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* Sub3 */ + {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ} /* Sub4 */ }; -/* Data object 0x7000/0x6000: Sub0=UINT8 count, Sub1..4=UINT64 */ -OBJCONST TSDOINFOENTRYDESC OBJMEM asDataEntryDesc[] = { +/* 0x7000 Output Data: REC, Sub0..Sub4 共 5 项; 子条目可被 RxPDO 映射。 */ +OBJCONST TSDOINFOENTRYDESC OBJMEM asOutputEntryDesc[] = { {DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, - {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE} + {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING}, + {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING}, + {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING}, + {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING} }; -/* SYNC 诊断 0x2000: Sub0=UINT8 count, Sub1..4=UINT32 (只读) */ +/* 0x6000 Input Data: REC, Sub0..Sub4 共 5 项; 子条目可被 TxPDO 映射, 输入侧只读。 */ +OBJCONST TSDOINFOENTRYDESC OBJMEM asInputEntryDesc[] = { + {DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, + {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}, + {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}, + {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}, + {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING} +}; + +/* SYNC 诊断 0x2000: REC, Sub0..Sub4 共 5 项, 纯只读不参与 PDO 映射。 */ OBJCONST TSDOINFOENTRYDESC OBJMEM asSyncDiagEntryDesc[] = { {DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, + {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, + {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, + {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ} }; @@ -156,29 +195,29 @@ OBJCONST UCHAR OBJMEM aName0x2000[] = "SYNC Diag"; *---------------------------------------------------------------------------*/ TOBJECT OBJMEM ApplicationObjDicList[] = { - /* 0x1C12: RxPDO Assign */ + /* 0x1C12: RxPDO Assign (ARR of UINT16; DEFTYPE 为元素类型) */ {NULL, NULL, 0x1C12, {DEFTYPE_UNSIGNED16, 1 | (OBJCODE_ARR << 8)}, asPDOAssignEntryDesc, aName0x1C12, &sRxPDOassign, NULL, NULL, 0x0000}, - /* 0x1600: RxPDO Mapping */ + /* 0x1600: RxPDO Mapping (REC, 4 条目 -> 描述数组必须 5 项) */ {NULL, NULL, 0x1600, {DEFTYPE_PDOMAPPING, 4 | (OBJCODE_REC << 8)}, asPDOMappingEntryDesc, aName0x1600, &sRxPDOMap, NULL, NULL, 0x0000}, /* 0x7000: Output Data (主站→从站) */ {NULL, NULL, 0x7000, {DEFTYPE_RECORD, 4 | (OBJCODE_REC << 8)}, - asDataEntryDesc, aName0x7000, &s_outputObj, NULL, NULL, 0x0000}, + asOutputEntryDesc, aName0x7000, &s_outputObj, NULL, NULL, 0x0000}, /* 0x1C13: TxPDO Assign */ {NULL, NULL, 0x1C13, {DEFTYPE_UNSIGNED16, 1 | (OBJCODE_ARR << 8)}, asPDOAssignEntryDesc, aName0x1C13, &sTxPDOassign, NULL, NULL, 0x0000}, - /* 0x1A00: TxPDO Mapping */ + /* 0x1A00: TxPDO Mapping (REC, 4 条目 -> 描述数组必须 5 项) */ {NULL, NULL, 0x1A00, {DEFTYPE_PDOMAPPING, 4 | (OBJCODE_REC << 8)}, asPDOMappingEntryDesc, aName0x1A00, &sTxPDOMap, NULL, NULL, 0x0000}, /* 0x6000: Input Data (从站→主站) */ {NULL, NULL, 0x6000, {DEFTYPE_RECORD, 4 | (OBJCODE_REC << 8)}, - asDataEntryDesc, aName0x6000, &s_inputObj, NULL, NULL, 0x0000}, + asInputEntryDesc, aName0x6000, &s_inputObj, NULL, NULL, 0x0000}, /* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路) */ {NULL, NULL, 0x2000, {DEFTYPE_RECORD, 4 | (OBJCODE_REC << 8)}, diff --git a/GD32E508_DCU_APP/comm/ssc/port/ecat_def.h b/GD32E508_DCU_APP/comm/ssc/port/ecat_def.h index a686a6a..507b8bd 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/ecat_def.h +++ b/GD32E508_DCU_APP/comm/ssc/port/ecat_def.h @@ -517,12 +517,21 @@ /*----------------------------------------------------------------------------------------- ------ ------- Struct Packing (ARM AC6: __attribute__((packed)) 不需要前后宏,留空即可) +------ 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 等同样受影响。 +------ AC6/armclang 支持 GCC 风格 __attribute__((packed)),置于 struct 关键字之后 +------ (即 START 位置)对整个结构体生效。 -----------------------------------------------------------------------------------------*/ #ifndef STRUCT_PACKED_START -#define STRUCT_PACKED_START +#define STRUCT_PACKED_START __attribute__((packed)) #endif #ifndef STRUCT_PACKED_END @@ -530,7 +539,7 @@ #endif #ifndef MBX_STRUCT_PACKED_START -#define MBX_STRUCT_PACKED_START +#define MBX_STRUCT_PACKED_START __attribute__((packed)) #endif #ifndef MBX_STRUCT_PACKED_END @@ -538,7 +547,7 @@ #endif #ifndef OBJ_STRUCT_PACKED_START -#define OBJ_STRUCT_PACKED_START +#define OBJ_STRUCT_PACKED_START __attribute__((packed)) #endif #ifndef OBJ_STRUCT_PACKED_END diff --git a/GD32E508_DCU_APP/comm/ssc/port/hw_gd32exmc.cpp b/GD32E508_DCU_APP/comm/ssc/port/hw_gd32exmc.cpp index 001050f..8a3c3ce 100644 --- a/GD32E508_DCU_APP/comm/ssc/port/hw_gd32exmc.cpp +++ b/GD32E508_DCU_APP/comm/ssc/port/hw_gd32exmc.cpp @@ -2,20 +2,20 @@ * @file hw_gd32exmc.cpp * @brief SSC V5.11 硬件 port 层 — GD32E508 + LAN9252 (EXMC 16-bit HBI Indexed Mode) * - * 实现 el9800hw.h 中声明的全部 HW_* 函数,将 SSC 的 ESC 访问请求 - * 转发到 EscExmcBus (EXMC NOR/SRAM Region0, 16-bit 内存映射)。 + * 实现 el9800hw.h 中声明的全部 HW_* 函数,将 SSC 的 ESC 访问请求 + * 转发到 EscLan9252 的间接访问端口(1.5.39 按 DS 12.11 修正访问模型): + * - 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。 * - * HBI Indexed 寻址协议(A2=LAN9252 Pin29←PD12=EXMC_A17, A1=Pin25←PD11=EXMC_A16; - * EXMC 16-bit 模式地址左移 1 位,见 esc_lan9252.cpp 头注释): - * 1. 写 HBI_IDX_0 (EXMC offset 0x00000) = ESC 寄存器字节地址 - * 2. 读/写 HBI_DATA_0 低 word (EXMC offset 0x40000, A2=1/A1=0) - * 3. 读/写 HBI_DATA_0 高 word (EXMC offset 0x60000, A2=1/A1=1) - * - * 并发保护: - * - HW_EscRead/HW_EscWrite 等主循环函数:内部经 EscExmcBus::maskEnter/Exit - * 屏蔽 EXTI0+EXTI1(HBI Indexed 是两步事务,不可被 ISR 打断)。 - * - HW_EscReadIsr/HW_EscWriteIsr (ISR):在 preempt=0 的 SYNC0 ISR 内调用, - * 天然不会被抢占;PDI_Isr/Sync1_Isr(preempt=1) 由 ethercat_sync.cpp 包装器 + * 并发保护(不变): + * - HW_EscRead/HW_EscWrite 等主循环函数:内部经 EscExmcBus::maskEnter/Exit + * 屏蔽 EXTI0+EXTI1(访问端口是多步事务,不可被 ISR 打断)。 + * - HW_EscReadIsr/HW_EscWriteIsr (ISR):在 preempt=0 的 SYNC0 ISR 内调用, + * 天然不会被抢占;PDI_Isr/Sync1_Isr(preempt=1) 由 ethercat_sync.cpp 包装器 * 屏蔽 EXTI0 后调用。 */ @@ -28,126 +28,76 @@ /* 项目硬件驱动 */ #include "comm/esc_exmc_bus.h" +#include "comm/esc_lan9252.h" #include "sys/config.h" #include #include -/*--------------------------------------------------------------------------- - * HBI Indexed Mode EXMC 偏移 (与 esc_lan9252.cpp 一致) - *---------------------------------------------------------------------------*/ namespace { -constexpr uint32_t HBI_IDX0_OFFSET = 0x00000U; /* A2=0, A1=0 */ -constexpr uint32_t HBI_DATA0_LOW_OFF = 0x40000U; /* A2=1(EXMC_A17), A1=0(EXMC_A16) */ -constexpr uint32_t HBI_DATA0_HIGH_OFF = 0x60000U; /* A2=1(EXMC_A17), A1=1(EXMC_A16) */ - /* AL Event Mask 寄存器影子 (ESC 0x0204) */ volatile uint16_t s_alEventMask = 0xFFFFU; /* SSC 看门狗软件定时器 (DWT CYCCNT 基准) */ volatile uint32_t s_timerBase = 0U; -/*--------------------------------------------------------------------------- - * 内部 HBI 访问原语 (无锁,调用者负责并发保护) - *---------------------------------------------------------------------------*/ +constexpr uint16_t ESC_PRAM_BASE = 0x1000U; /**< 过程 RAM 起点(DS 12.11):≥此地址走 PRAM FIFO */ -inline void hbiSetIndex(uint16_t addr) -{ - EscExmcBus::write16(HBI_IDX0_OFFSET, addr); -} - -inline uint16_t hbiReadLow(void) -{ - return EscExmcBus::read16(HBI_DATA0_LOW_OFF); -} - -inline uint16_t hbiReadHigh(void) -{ - return EscExmcBus::read16(HBI_DATA0_HIGH_OFF); -} - -inline void hbiWriteLow(uint16_t v) -{ - EscExmcBus::write16(HBI_DATA0_LOW_OFF, v); -} - -inline void hbiWriteHigh(uint16_t v) -{ - EscExmcBus::write16(HBI_DATA0_HIGH_OFF, v); -} - -/** 从 ESC 地址读取 Len 字节到 pData (按 16-bit word 对齐处理) */ -void hbiRead(void *pData, uint16_t address, uint16_t len) +/** 从 ESC 地址读取 Len 字节到 pData(按地址自动路由 CSR 端口 / PRAM FIFO) */ +void escRead(MEM_ADDR *pData, uint16_t address, uint16_t len) { uint8_t *dst = (uint8_t *)pData; + if (address >= ESC_PRAM_BASE) { + EscLan9252::pramRead(address, dst, len); + return; + } + /* CSR 端口:size 1 任意地址,size 2 需偶地址(DS 表 12-14) */ uint16_t addr = address; uint16_t remaining = len; - - /* 奇数起始地址:读低 byte */ if ((addr & 1U) != 0U && remaining > 0U) { - hbiSetIndex(addr & 0xFFFEU); - uint16_t w = hbiReadLow(); - *dst++ = (uint8_t)((w >> 8) & 0xFFU); + *dst++ = (uint8_t)EscLan9252::csrRead(addr, 1U); addr++; remaining--; } - - /* 对齐的 16-bit word 读取 */ while (remaining >= 2U) { - hbiSetIndex(addr); - uint16_t w = hbiReadLow(); + const uint16_t w = (uint16_t)EscLan9252::csrRead(addr, 2U); dst[0] = (uint8_t)(w & 0xFFU); - dst[1] = (uint8_t)((w >> 8) & 0xFFU); + dst[1] = (uint8_t)(w >> 8); dst += 2; addr += 2; remaining -= 2; } - - /* 尾部奇数字节 */ if (remaining > 0U) { - hbiSetIndex(addr); - uint16_t w = hbiReadLow(); - *dst = (uint8_t)(w & 0xFFU); + *dst = (uint8_t)EscLan9252::csrRead(addr, 1U); } } -/** 向 ESC 地址写入 Len 字节 (按 16-bit word 对齐处理) */ -void hbiWrite(const void *pData, uint16_t address, uint16_t len) +/** 向 ESC 地址写入 Len 字节(按地址自动路由 CSR 端口 / PRAM FIFO) */ +void escWrite(const MEM_ADDR *pData, uint16_t address, uint16_t len) { const uint8_t *src = (const uint8_t *)pData; + if (address >= ESC_PRAM_BASE) { + EscLan9252::pramWrite(address, src, len); + return; + } uint16_t addr = address; uint16_t remaining = len; - - /* 奇数起始地址:RMW 高 byte */ if ((addr & 1U) != 0U && remaining > 0U) { - uint16_t aligned = addr & 0xFFFEU; - hbiSetIndex(aligned); - uint16_t w = hbiReadLow(); - w = (w & 0x00FFU) | ((uint16_t)(*src++) << 8); - hbiSetIndex(aligned); - hbiWriteLow(w); + EscLan9252::csrWrite(addr, *src, 1U); + src++; addr++; remaining--; } - - /* 对齐的 16-bit word 写入 */ while (remaining >= 2U) { - uint16_t w = (uint16_t)src[0] | ((uint16_t)src[1] << 8); - hbiSetIndex(addr); - hbiWriteLow(w); + const uint16_t w = (uint16_t)src[0] | ((uint16_t)src[1] << 8); + EscLan9252::csrWrite(addr, w, 2U); src += 2; addr += 2; remaining -= 2; } - - /* 尾部奇数字节:RMW 低 byte */ if (remaining > 0U) { - hbiSetIndex(addr); - uint16_t w = hbiReadLow(); - w = (w & 0xFF00U) | (uint16_t)(*src); - hbiSetIndex(addr); - hbiWriteLow(w); + EscLan9252::csrWrite(addr, *src, 1U); } } @@ -173,8 +123,7 @@ UINT8 HW_Init(void) s_timerBase = DWT->CYCCNT; /* 读取当前 AL Event Mask */ - hbiSetIndex(ESC_AL_EVENTMASK_OFFSET); - s_alEventMask = hbiReadLow(); + s_alEventMask = (uint16_t)EscLan9252::csrRead(ESC_AL_EVENTMASK_OFFSET, 2U); return 0; /* 0 = success */ } @@ -186,13 +135,12 @@ void HW_Release(void) /** * @brief 读取 AL Event 寄存器 (0x0220),主循环上下文。 - * @note 内部屏蔽 EXTI0+EXTI1 保证 HBI 事务完整。 + * @note 内部屏蔽 EXTI0+EXTI1 保证访问端口事务完整。 */ UINT16 HW_GetALEventRegister(void) { const uint32_t prev = EscExmcBus::maskEnter(); - hbiSetIndex(ESC_AL_EVENT_OFFSET); - const UINT16 v = hbiReadLow(); + const UINT16 v = (UINT16)EscLan9252::csrRead(ESC_AL_EVENT_OFFSET, 2U); EscExmcBus::maskExit(prev); return v; } @@ -202,8 +150,7 @@ UINT16 HW_GetALEventRegister(void) */ UINT16 HW_GetALEventRegister_Isr(void) { - hbiSetIndex(ESC_AL_EVENT_OFFSET); - return hbiReadLow(); + return (UINT16)EscLan9252::csrRead(ESC_AL_EVENT_OFFSET, 2U); } /** @@ -213,8 +160,7 @@ void HW_ResetALEventMask(UINT16 intMask) { const uint32_t prev = EscExmcBus::maskEnter(); s_alEventMask &= ~intMask; - hbiSetIndex(ESC_AL_EVENTMASK_OFFSET); - hbiWriteLow(s_alEventMask); + EscLan9252::csrWrite(ESC_AL_EVENTMASK_OFFSET, s_alEventMask, 2U); EscExmcBus::maskExit(prev); } @@ -225,8 +171,7 @@ void HW_SetALEventMask(UINT16 intMask) { const uint32_t prev = EscExmcBus::maskEnter(); s_alEventMask |= intMask; - hbiSetIndex(ESC_AL_EVENTMASK_OFFSET); - hbiWriteLow(s_alEventMask); + EscLan9252::csrWrite(ESC_AL_EVENTMASK_OFFSET, s_alEventMask, 2U); EscExmcBus::maskExit(prev); } @@ -239,12 +184,9 @@ void HW_SetLed(UINT8 RunLed, UINT8 ErrLed) { const uint32_t prev = EscExmcBus::maskEnter(); /* Run LED override (0x0138): bit0 = LED on */ - hbiSetIndex(ESC_RUN_LED_OVERRIDE); - hbiWriteLow(RunLed ? 0x0001U : 0x0000U); - + EscLan9252::csrWrite(ESC_RUN_LED_OVERRIDE, RunLed ? 0x0001U : 0x0000U, 2U); /* Error LED override (0x0139): bit0 = LED on */ - hbiSetIndex(ESC_ERROR_LED_OVERRIDE); - hbiWriteLow(ErrLed ? 0x0001U : 0x0000U); + EscLan9252::csrWrite(ESC_ERROR_LED_OVERRIDE, ErrLed ? 0x0001U : 0x0000U, 2U); EscExmcBus::maskExit(prev); } @@ -255,14 +197,14 @@ void HW_SetLed(UINT8 RunLed, UINT8 ErrLed) void HW_EscRead(MEM_ADDR *pData, UINT16 Address, UINT16 Len) { const uint32_t prev = EscExmcBus::maskEnter(); - hbiRead(pData, Address, Len); + escRead(pData, Address, Len); EscExmcBus::maskExit(prev); } void HW_EscWrite(MEM_ADDR *pData, UINT16 Address, UINT16 Len) { const uint32_t prev = EscExmcBus::maskEnter(); - hbiWrite(pData, Address, Len); + escWrite(pData, Address, Len); EscExmcBus::maskExit(prev); } @@ -272,12 +214,12 @@ void HW_EscWrite(MEM_ADDR *pData, UINT16 Address, UINT16 Len) void HW_EscReadIsr(MEM_ADDR *pData, UINT16 Address, UINT16 Len) { - hbiRead(pData, Address, Len); + escRead(pData, Address, Len); } void HW_EscWriteIsr(MEM_ADDR *pData, UINT16 Address, UINT16 Len) { - hbiWrite(pData, Address, Len); + escWrite(pData, Address, Len); } /*--------------------------------------------------------------------------- @@ -285,59 +227,49 @@ void HW_EscWriteIsr(MEM_ADDR *pData, UINT16 Address, UINT16 Len) *---------------------------------------------------------------------------*/ /** - * @brief 禁用指定 SM 通道 (清 SM Activate 寄存器 bit0)。 + * @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;旧值从未真正生效过(访问层全空)。 */ void HW_DisableSyncManChannel(UINT8 channel) { - /* SM 寄存器基址 0x0800,每个 SM 占 8 字节,Activate 在 offset +4 (bit0) */ - uint16_t smBase = 0x0800U + (uint16_t)channel * 8U; - uint16_t activateAddr = smBase + 4U; - + const uint16_t activateAddr = (uint16_t)(ESC_SYNCMAN_REG_OFFSET + + (uint16_t)channel * SIZEOF_SM_REGISTER + 6U); const uint32_t prev = EscExmcBus::maskEnter(); - hbiSetIndex(activateAddr); - uint16_t val = hbiReadLow(); - val &= ~0x0001U; /* 清 bit0 = deactivate */ - hbiSetIndex(activateAddr); - hbiWriteLow(val); + uint8_t val = (uint8_t)EscLan9252::csrRead(activateAddr, 1U); + val &= (uint8_t)~0x01U; /* 清 bit0 = deactivate */ + EscLan9252::csrWrite(activateAddr, val, 1U); EscExmcBus::maskExit(prev); } /** - * @brief 使能指定 SM 通道 (置 SM Activate 寄存器 bit0)。 + * @brief 使能指定 SM 通道 (置 SM Activate 寄存器 0x0806+8n bit0)。 */ void HW_EnableSyncManChannel(UINT8 channel) { - uint16_t smBase = 0x0800U + (uint16_t)channel * 8U; - uint16_t activateAddr = smBase + 4U; - + const uint16_t activateAddr = (uint16_t)(ESC_SYNCMAN_REG_OFFSET + + (uint16_t)channel * SIZEOF_SM_REGISTER + 6U); const uint32_t prev = EscExmcBus::maskEnter(); - hbiSetIndex(activateAddr); - uint16_t val = hbiReadLow(); - val |= 0x0001U; - hbiSetIndex(activateAddr); - hbiWriteLow(val); + uint8_t val = (uint8_t)EscLan9252::csrRead(activateAddr, 1U); + val |= 0x01U; + EscLan9252::csrWrite(activateAddr, val, 1U); EscExmcBus::maskExit(prev); } /** * @brief 获取指定 SM 通道的寄存器块指针。 * @note SSC 用此函数读取 SM 配置 (StartAddr, Length, Control, Status, Activate)。 - * 由于 HBI 是间接访问,无法返回真实指针。SSC 在 EL9800_HW 模式下 - * 实际通过 HW_EscRead 读取 SM 寄存器,此函数返回 NULL 并由调用者 - * 使用 HW_EscRead 替代。 - * - * 检查 ecatslv.c 中 HW_GetSyncMan 的使用方式: - * 如果 SSC 直接解引用返回的指针,则需要提供一个本地缓冲。 - * 这里提供一个静态缓冲,每次调用时从 ESC 读入。 + * 访问是间接端口,无法返回真实指针;提供静态缓冲,每次调用时从 ESC 读入。 */ static TSYNCMAN s_smBuf; TSYNCMAN ESCMEM *HW_GetSyncMan(UINT8 channel) { - uint16_t smBase = 0x0800U + (uint16_t)channel * 8U; + const uint16_t smBase = (uint16_t)(ESC_SYNCMAN_REG_OFFSET + + (uint16_t)channel * SIZEOF_SM_REGISTER); const uint32_t prev = EscExmcBus::maskEnter(); - hbiRead(&s_smBuf, smBase, SIZEOF_SM_REGISTER); + escRead((MEM_ADDR *)&s_smBuf, smBase, SIZEOF_SM_REGISTER); EscExmcBus::maskExit(prev); return &s_smBuf; } diff --git a/GD32E508_DCU_APP/device/ADC.cpp b/GD32E508_DCU_APP/device/ADC.cpp index 4a2f7d8..ffd6681 100644 --- a/GD32E508_DCU_APP/device/ADC.cpp +++ b/GD32E508_DCU_APP/device/ADC.cpp @@ -65,14 +65,51 @@ void Adc::initDma() } /** - * @brief 初始化 ADC、DMA 与规则通道扫描配置。 + * @brief 带超时等待 ADC_CTL1 指定位清零。 + * @param[in] bit ADC_CTL1 位(RSTCLB/CLB)。 + * @param[in] timeout_ms 超时毫秒数。 + * @retval bool true=位已清零;false=超时(硬件未响应)。 + * @note 替代库函数 adc_calibration_enable 内部的无超时死等—— + * ADC 硬件异常时库函数会永久挂住 init,且无任何诊断。 + */ +static bool waitAdcCtl1BitClear(uint32_t bit, uint32_t timeout_ms) +{ + const uint32_t start = SysTickTimer::getMs(); + while ((ADC_CTL1(CONFIG_ADC_INSTANCE) & bit) != 0U) { + if ((SysTickTimer::getMs() - start) >= timeout_ms) { + return false; + } + } + return true; +} + +/** + * @brief 带超时执行 ADC 校准(RSTCLB → CLB 两段各等清零)。 + * @retval uint8_t Adc::InitFault 故障码(0=校准完成;1=RSTCLB 超时;2=CLB 超时)。 + */ +static uint8_t runCalibrationWithTimeout() +{ + constexpr uint32_t kCalTimeoutMs = 10U; /* 校准实际仅数十 µs,10ms 裕量极大 */ + ADC_CTL1(CONFIG_ADC_INSTANCE) |= ADC_CTL1_RSTCLB; + if (!waitAdcCtl1BitClear(ADC_CTL1_RSTCLB, kCalTimeoutMs)) { + return Adc::kFaultCalRstTimeout; + } + ADC_CTL1(CONFIG_ADC_INSTANCE) |= ADC_CTL1_CLB; + if (!waitAdcCtl1BitClear(ADC_CTL1_CLB, kCalTimeoutMs)) { + return Adc::kFaultCalTimeout; + } + return Adc::kFaultOk; +} + +/** + * @brief 初始化 ADC、DMA 与规则通道扫描配置,并验证采样链有效。 * @param[in] 无。 * @param[out] 无。 - * @retval 无返回值。 + * @retval uint8_t Adc::InitFault 故障码(0=校准完成且 VREFINT→VDDA 换算在合理区间)。 * @note 按 `VIN_ADC / TEMP / VREFINT` 三通道顺序启用连续扫描与 DMA 采样。 * 参考 GD32E508 文档要求,内部温度与 VREFINT 通道使用长采样时间,以满足 ts_temp 要求。 */ -void Adc::init() +uint8_t Adc::init() { rcu_periph_clock_enable(RCU_ADC0); rcu_periph_clock_enable(RCU_GPIOA); @@ -103,10 +140,17 @@ void Adc::init() adc_enable(CONFIG_ADC_INSTANCE); SysTickTimer::delayMs(CONFIG_ADC_STABILIZE_DELAY_MS); - adc_calibration_enable(CONFIG_ADC_INSTANCE); - SysTickTimer::delayMs(CONFIG_ADC_STABILIZE_DELAY_MS); + const uint8_t calFault = runCalibrationWithTimeout(); + if (calFault != kFaultOk) { + return calFault; + } adc_software_trigger_enable(CONFIG_ADC_INSTANCE, ADC_REGULAR_CHANNEL); - SysTickTimer::delayMs(1U); + SysTickTimer::delayMs(CONFIG_ADC_STABILIZE_DELAY_MS); + + /* 验证采样链:DMA 循环搬运在跑且 VREFINT 换算 VDDA 落在合理区间。 + * calculateVdda 对非法 raw/超范围返回 0.0f。 */ + const float vdda = calculateVdda(readVrefRaw()); + return ((vdda >= kMinValidVdda) && (vdda <= kMaxValidVdda)) ? kFaultOk : kFaultVddaInvalid; } /** diff --git a/GD32E508_DCU_APP/device/CANFD.cpp b/GD32E508_DCU_APP/device/CANFD.cpp index edaaf0d..a6f6186 100644 --- a/GD32E508_DCU_APP/device/CANFD.cpp +++ b/GD32E508_DCU_APP/device/CANFD.cpp @@ -399,19 +399,24 @@ void CanFd::initChannelHardware(uint8_t i) const uint8_t rxLevelAfter = (gpio_input_bit_get(ch.rxPort, ch.rxPin) != RESET) ? 1U : 0U; const uint32_t tecAfterExit = (errAfterExit >> 16) & 0xFFU; const uint32_t recAfterExit = (errAfterExit >> 24) & 0xFFU; - CAN_LOGI("[CAN-DIAG] CH%u init_nom=%u init_fd=%u exit_ok=%u wait=%lu rx_before=%u rx_after=%u stb=%u STAT=0x%08lX ERR=0x%08lX TEC=%lu REC=%lu\r\n", - static_cast(i), - nominalOk ? 1U : 0U, - fdOk ? 1U : 0U, - ((statAfterExit & CAN_STAT_IWS) == 0U) ? 1U : 0U, - static_cast(exitWait), - static_cast(rxLevelBefore), - static_cast(rxLevelAfter), - static_cast(stbLevel), - static_cast(statAfterExit), - static_cast(errAfterExit), - static_cast(tecAfterExit), - static_cast(recAfterExit)); + /* 正常上电静默:只在本端控制器初始化失败(can_init/can_fd_init 失败或卡 init mode) + * 时打印一行诊断。TEC/REC 属总线侧状态,未接从站时浮空噪声会累积,不作为打印条件。 */ + const bool exitedInitMode = ((statAfterExit & CAN_STAT_IWS) == 0U); + if (!nominalOk || !fdOk || !exitedInitMode) { + LOG_ERR("[CAN-DIAG] CH%u init_nom=%u init_fd=%u exit_ok=%u wait=%lu rx_before=%u rx_after=%u stb=%u STAT=0x%08lX ERR=0x%08lX TEC=%lu REC=%lu\r\n", + static_cast(i), + nominalOk ? 1U : 0U, + fdOk ? 1U : 0U, + exitedInitMode ? 1U : 0U, + static_cast(exitWait), + static_cast(rxLevelBefore), + static_cast(rxLevelAfter), + static_cast(stbLevel), + static_cast(statAfterExit), + static_cast(errAfterExit), + static_cast(tecAfterExit), + static_cast(recAfterExit)); + } 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); @@ -782,13 +787,16 @@ void CanFd::init() } /** - * @brief L0 轻量自检:无总线发帧,仅校验使能通道硬件与收发器状态。 + * @brief L0 轻量自检:无总线发帧,仅校验使能通道控制器自身状态(GD32 侧故障)。 + * @note 1.5.35:不再检查 BUS_OFF/TEC/REC——未接从站的通道 RX 浮空噪声会累积 REC, + * 「没接设备」不等于错误;总线侧健康由 0xC1 反馈帧运行期上报。 */ -bool CanFd::selfTestL0() +bool CanFd::selfTestL0(uint8_t *fault_code, uint8_t *fault_channel) { - constexpr uint32_t kTecRecLimit = 16U; bool anyEnabled = false; bool allOk = true; + uint8_t firstCode = kSelfTestOk; + uint8_t firstCh = 0U; for (uint8_t i = 0U; i < static_cast(kChannelCount); ++i) { if (!kChannelEnabled[i]) { @@ -796,38 +804,37 @@ bool CanFd::selfTestL0() } anyEnabled = true; const auto &ch = kChannels[i]; + uint8_t code = kSelfTestOk; if (!s_channelHwOk[i]) { - allOk = false; - continue; + code = kSelfTestHwInit; + } else if ((CAN_STAT(ch.canPeriph) & CAN_STAT_IWS) != 0U) { + /* 仍停在初始化模式 */ + code = kSelfTestStuckInit; + } else if (gpio_output_bit_get(ch.slpPort, ch.slpPin) != RESET) { + /* STB=0 为 Normal;输出寄存器回读确认配置生效 */ + code = kSelfTestStbLevel; } - /* 仍停在初始化模式 */ - if ((CAN_STAT(ch.canPeriph) & CAN_STAT_IWS) != 0U) { + if (code != kSelfTestOk) { allOk = false; - continue; - } - - /* STB=0 为 Normal;高电平视为仍待机 */ - if (gpio_output_bit_get(ch.slpPort, ch.slpPin) != RESET) { - allOk = false; - continue; - } - - const uint32_t errReg = CAN_ERR(ch.canPeriph); - if ((errReg & CAN_ERR_BOERR) != 0U) { - allOk = false; - continue; - } - - const uint32_t tec = (errReg >> 16) & 0xFFU; - const uint32_t rec = (errReg >> 24) & 0xFFU; - if (tec > kTecRecLimit || rec > kTecRecLimit) { - allOk = false; - continue; + if (firstCode == kSelfTestOk) { + firstCode = code; + firstCh = i; + } + LOG_ERR("[CAN] L0 selftest CH%u FAIL code=%u (STAT=0x%08lX ERR=0x%08lX)\r\n", + static_cast(i), static_cast(code), + static_cast(CAN_STAT(ch.canPeriph)), + static_cast(CAN_ERR(ch.canPeriph))); } } + if (fault_code != nullptr) { + *fault_code = firstCode; + } + if (fault_channel != nullptr) { + *fault_channel = firstCh; + } return anyEnabled && allOk; } diff --git a/GD32E508_DCU_APP/device/Timer.cpp b/GD32E508_DCU_APP/device/Timer.cpp index 405d8bf..9514d69 100644 --- a/GD32E508_DCU_APP/device/Timer.cpp +++ b/GD32E508_DCU_APP/device/Timer.cpp @@ -6,6 +6,7 @@ #include "main.h" #include "device/timer.h" #include "sys/task_manager.h" +#include "sys/systick.h" #include "sys/config.h" /** @@ -48,16 +49,38 @@ void SchedulerTimer::configureNvic() } /** - * @brief 初始化用户调度定时器模块。 + * @brief 初始化用户调度定时器模块并验证节拍运行。 * @param[in] 无。 * @param[out] 无。 - * @retval 无返回值。 - * @note 依次完成定时器本体配置与 NVIC 中断配置。 + * @retval uint8_t SchedulerTimer::InitFault 故障码(0=回读一致且 tick_100us 已前进)。 + * @note 依次完成定时器本体配置与 NVIC 中断配置; + * 验证覆盖"中断向量/优先级配错导致调度器静默停摆"这类纯写配置发现不了的故障。 */ -void SchedulerTimer::init() +uint8_t SchedulerTimer::init() { configureTimer(); configureNvic(); + + /* 回读:计数器已使能、PSC/CAR 与配置一致 */ + if ((TIMER_CTL0(CONFIG_SCHED_TIMER_INSTANCE) & TIMER_CTL0_CEN) == 0U) { + return kFaultNotEnabled; + } + if (TIMER_PSC(CONFIG_SCHED_TIMER_INSTANCE) != CONFIG_SCHED_TIMER_PRESCALER) { + return kFaultPscMismatch; + } + if (TIMER_CAR(CONFIG_SCHED_TIMER_INSTANCE) != CONFIG_SCHED_TIMER_PERIOD) { + return kFaultCarMismatch; + } + + /* 有界等待 100us 中断节拍前进(50ms 预算 = 500 个 tick) */ + const uint32_t start = g_scheduler.tick_100us; + const uint32_t deadline = SysTickTimer::getMs() + 50U; + while (g_scheduler.tick_100us == start) { + if ((int32_t)(SysTickTimer::getMs() - deadline) >= 0) { + return kFaultNoTick; + } + } + return kFaultOk; } /** diff --git a/GD32E508_DCU_APP/device/Timer.h b/GD32E508_DCU_APP/device/Timer.h index cab078a..a92c918 100644 --- a/GD32E508_DCU_APP/device/Timer.h +++ b/GD32E508_DCU_APP/device/Timer.h @@ -7,14 +7,23 @@ */ class SchedulerTimer { public: + /** init 故障码(0=成功;经 0xA3 INIT_STATUS fault_codes[] 上报) */ + enum InitFault : uint8_t { + kFaultOk = 0U, /**< 成功 */ + kFaultNotEnabled = 1U, /**< CTL0.CEN 未置位(定时器未使能) */ + kFaultPscMismatch = 2U, /**< PSC 回读与配置不一致 */ + kFaultCarMismatch = 3U, /**< CAR 回读与配置不一致 */ + kFaultNoTick = 4U, /**< 100us 中断节拍未前进(NVIC/向量/优先级异常) */ + }; + /** - * @brief 初始化用户调度定时器及其中断。 + * @brief 初始化用户调度定时器及其中断,并验证节拍真实运行。 * @param[in] 无。 * @param[out] 无。 - * @retval 无返回值。 + * @retval uint8_t InitFault 故障码(0=寄存器回读一致且 tick_100us 中断节拍已前进)。 * @note 当前用于建立 100us 基准节拍,驱动 TaskManager::tick100us()。 */ - static void init(); + static uint8_t init(); /** * @brief TIMER2 中断处理入口。 diff --git a/GD32E508_DCU_APP/device/adc.h b/GD32E508_DCU_APP/device/adc.h index a271394..15fbc65 100644 --- a/GD32E508_DCU_APP/device/adc.h +++ b/GD32E508_DCU_APP/device/adc.h @@ -23,14 +23,22 @@ public: uint8_t valid; /**< 采样是否有效:1=有效,0=存在原始值/换算异常。 */ }; + /** init 故障码(0=成功;经 0xA3 INIT_STATUS fault_codes[] 上报,网页解码文字见 initial.h 注释) */ + enum InitFault : uint8_t { + kFaultOk = 0U, /**< 成功 */ + kFaultCalRstTimeout = 1U, /**< 校准复位(RSTCLB)等待超时,ADC 硬件无响应 */ + kFaultCalTimeout = 2U, /**< 校准(CLB)等待超时 */ + kFaultVddaInvalid = 3U, /**< 校准后 VREFINT→VDDA 采样链无效(DMA/通道/供电异常) */ + }; + /** * @brief 初始化 ADC 硬件、扫描通道与 DMA 循环缓存。 * @param[in] 无。 * @param[out] 无。 - * @retval 无返回值。 + * @retval uint8_t InitFault 故障码(0=校准完成且 VDDA 采样链有效)。 * @note 完成 ADC 三通道扫描、DMA 循环搬运、PA6(VIN_ADC) 模拟输入配置以及内部通道校准。 */ - static void init(); + static uint8_t init(); /** * @brief 读取指定通道在 DMA 缓存中的原始 ADC 值。 diff --git a/GD32E508_DCU_APP/device/canfd.h b/GD32E508_DCU_APP/device/canfd.h index 282bc82..805898a 100644 --- a/GD32E508_DCU_APP/device/canfd.h +++ b/GD32E508_DCU_APP/device/canfd.h @@ -118,12 +118,25 @@ public: static void init(); + /** L0 自检故障原因码(selfTestL0 输出;经 0xA3 fault_codes[] 上报,高 3 位带通道号见 initial.h) */ + enum SelfTestFault : uint8_t { + kSelfTestOk = 0U, /**< 通过 */ + kSelfTestHwInit = 1U, /**< 控制器初始化失败(重试 5 次后仍不成功) */ + kSelfTestStuckInit = 2U, /**< 卡在初始化模式(IWS 未清零) */ + kSelfTestStbLevel = 3U, /**< 收发器 STB 输出未拉低(配置未生效) */ + }; + /** - * @brief L0 轻量自检(不向物理总线发帧)。 + * @brief L0 轻量自检(不向物理总线发帧),仅校验 GD32 侧控制器自身状态。 + * @param[out] fault_code 首个失败通道的 SelfTestFault 原因码;全部通过时为 kSelfTestOk。允许空。 + * @param[out] fault_channel 首个失败通道号 0/1/2;全部通过时无意义。允许空。 * @retval true 所有已使能通道通过;false 无使能通道或任一路失败。 - * @note 检查:硬件 init 结果、退出 init mode、收发器 Normal、无 BUS_OFF、TEC/REC 阈值。 + * @note 检查:硬件 init 结果、退出 init mode、收发器 STB 已置 Normal。 + * 1.5.35 移除 BUS_OFF 与 TEC/REC 检查:它们是总线侧状态, + * 通道未接从站(RX 浮空噪声)时会误报——「没接设备」不等于错误; + * 总线健康经 0xC1 反馈帧(bus_state/TEC/REC)运行期上报,网页 CAN 页可见。 */ - static bool selfTestL0(); + static bool selfTestL0(uint8_t *fault_code = nullptr, uint8_t *fault_channel = nullptr); /** * @brief 查询指定通道是否已使能。 diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_init_status.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_init_status.cpp new file mode 100644 index 0000000..7869dd2 --- /dev/null +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_init_status.cpp @@ -0,0 +1,61 @@ +/** + * @file app_spi_cmd_init_status.cpp + * @brief SPI 0xA3 INIT_STATUS 命令:向 ESP32 上报 APP 初始化状态(1.5.34 新增,1.5.35 扩展故障码)。 + * + * 响应 ACK 25B 布局(与 app_spi_command_defs.h kTypeInitStatus 注释一致): + * [0..3] module_mask LE AppInitializer::ModuleBit 成功位图 + * [4..7] critical_fail_mask LE 失败的 critical 模块位图(0=无 critical 失败) + * [8] reset_cause ResetCauseBit 复位源位图 + * [9] init_state 0=OK 1=degraded 2=critical_fail + * [10..11] reserved (0) + * [12..24] fault_codes[13] 每模块 1B 故障码(0=成功;编码见 sys/initial.h, + * canfd 高 4 位为首个失败通道号) + * 1.5.34 只回前 12B;ESP32 按 resp_len≥25 判定故障码数组是否存在,前后兼容。 + */ + +#include "device/spi/spi_command_handler.h" +#include "device/spi/app_spi_command.h" +#include "device/spi/app_spi_command_defs.h" +#include "sys/initial.h" + +namespace { + +/** @brief 0xA3 响应 payload 长度(12B 基础 + 13B 故障码)。 */ +static constexpr size_t kInitStatusLen = 12U + 13U; + +/** @brief SPI 0xA3 INIT_STATUS 处理器:应答初始化状态 25B。 */ +class SpiInitStatusHandler final : public ISpiCommandHandler { +public: + /** @brief 是否处理该帧类型。 */ + bool canHandle(uint8_t frame_type) const override + { + return frame_type == SpiDefs::kTypeInitStatus; + } + + /** + * @brief 处理 INIT_STATUS:填充初始化状态与模块故障码并回 0x80。 + * @param bus 命令总线 + * @param ctx 请求上下文 + */ + void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override + { + uint8_t payload[kInitStatusLen] = {0U}; + writeU32Le(&payload[0], AppInitializer::moduleMask()); + writeU32Le(&payload[4], AppInitializer::criticalFailMask()); + payload[8] = AppInitializer::resetCause(); + payload[9] = AppInitializer::initState(); + AppInitializer::faultCodes(&payload[12]); + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, payload, sizeof(payload)); + } +}; + +static SpiInitStatusHandler s_handler; + +} // namespace + +/** @brief 注册 0xA3 INIT_STATUS 命令处理器。 */ +void registerSpiInitStatusCommand(AppSpiCommand &bus) +{ + bus.registerHandler(&s_handler); +} diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_ping.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_ping.cpp index 87dde80..61789a4 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_cmd_ping.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_ping.cpp @@ -2,6 +2,7 @@ #include "device/spi/app_spi_command.h" #include "device/spi/app_spi_command_defs.h" #include "sys/config.h" +#include "sys/initial.h" #include "sys/ota_boot_request.h" #include "sys/systick.h" #include "main.h" @@ -68,7 +69,10 @@ void fillGd32AppIdentityPayload(uint8_t *out) out[12] = V2_FEAT_MAP_VER; writeU16Le(&out[13], buildAppFeatureFlags()); - /* [15] reserved = 0 */ + /* [15] init_state(1.5.34 起):0=OK 1=degraded 2=critical_fail。 + * 复用原 reserved 字节——旧 ESP32 忽略该字节、旧 GD32 恒 0(恰与 OK 同义), + * 因此不 bump feat_map_ver 即前后兼容;详情经 0xA3 INIT_STATUS 查询。 */ + out[15] = AppInitializer::initState(); } namespace { diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp index 7cbc382..12f20c4 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp @@ -382,6 +382,7 @@ void AppSpiCommand::configureExti() extern void registerSpiBootRequestCommand(AppSpiCommand &bus); extern void registerSpiOtaWaitCommand(AppSpiCommand &bus); extern void registerSpiAppPingCommand(AppSpiCommand &bus); +extern void registerSpiInitStatusCommand(AppSpiCommand &bus); extern void registerSpiCanBridgeCommand(AppSpiCommand &bus); extern void registerSpiCanBurstCommand(AppSpiCommand &bus); extern void registerSpiCanFeedbackCommand(AppSpiCommand &bus); @@ -407,6 +408,7 @@ void AppSpiCommand::init() registerSpiBootRequestCommand(AppSpiCommandBus); registerSpiOtaWaitCommand(AppSpiCommandBus); registerSpiAppPingCommand(AppSpiCommandBus); + registerSpiInitStatusCommand(AppSpiCommandBus); registerSpiCanBridgeCommand(AppSpiCommandBus); registerSpiCanBurstCommand(AppSpiCommandBus); registerSpiCanFeedbackCommand(AppSpiCommandBus); diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h b/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h index 2142a67..9740fa7 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h +++ b/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h @@ -23,6 +23,15 @@ static constexpr uint8_t kTypeAppPing = 0xA1U; * 0=stop(恢复AF_PP), 1-4=数据pattern(0xFFFF/0x0000/0xAAAA/0x5555), * 5=CS低, 6=RD低, 7=WR低, 8=A1高, 9=A2高。用于 LAN9252 通断测试。 */ static constexpr uint8_t kTypeExmcDiag = 0xA2U; +/** @brief APP 初始化状态查询(ESP32→GD32,1.5.34 新增):无 payload。 + * 响应 ACK 25B(1.5.35 起;1.5.34 仅前 12B): + * [0..3] module_mask LE(AppInitializer::ModuleBit 成功位图) + * [4..7] critical_fail_mask LE(失败的 critical 模块位图,0=无) + * [8] reset_cause(ResetCauseBit 位图) [9] init_state(0=OK 1=degraded 2=critical_fail) + * [10..11] reserved + * [12..24] fault_codes[13](每模块 1B 故障码,0=成功;编码见 sys/initial.h, + * canfd = 原因码|(首个失败通道<<4))。位定义与 sys/initial.h 一致。 */ +static constexpr uint8_t kTypeInitStatus = 0xA3U; static constexpr uint8_t kTypeBootReq = 0xB0U; static constexpr uint8_t kTypeCanSend = 0xC0U; static constexpr uint8_t kTypeCanFeedback = 0xC1U; /**< ESP32 -> GD32 轮询 CAN 反馈快照 */ diff --git a/GD32E508_DCU_APP/device/usart0.cpp b/GD32E508_DCU_APP/device/usart0.cpp index 6cd407d..ddcb094 100644 --- a/GD32E508_DCU_APP/device/usart0.cpp +++ b/GD32E508_DCU_APP/device/usart0.cpp @@ -11,6 +11,7 @@ uint8_t Usart0::s_txBuf[Usart0::kTxBufSize]; volatile uint16_t Usart0::s_txHead = 0U; volatile uint16_t Usart0::s_txTail = 0U; +volatile uint32_t Usart0::s_txDropped = 0U; /** * @brief 计算发送环形缓冲区中的下一个索引。 @@ -35,19 +36,54 @@ bool Usart0::isBufferEmpty() } /** - * @brief 以非阻塞方式向 USART0 发送缓冲写入一个字符。 + * @brief 在临界区内直接轮询 TBE 发出一个字节,腾出一个缓冲槽位。 + * @param[in] 无。 + * @param[out] 无。 + * @retval bool true 表示成功发出一字节(s_txTail 已前进),false 表示等待 TBE 超时。 + * @note 调用者必须已持有临界区:USART0 中断为 preempt=3,已被 BASEPRI 屏蔽, + * 故此处独占 s_txTail,不会与 onInterrupt() 取到同一字节。 + * 走轮询而非等中断,因此在中断上下文里调用 printf 也不会死锁。 + */ +bool Usart0::drainOneByteLocked() +{ + if(isBufferEmpty()) { + return false; + } + uint32_t spin = kTbeSpinMax; + while(usart_flag_get(USART0, USART_FLAG_TBE) == RESET) { + if(--spin == 0U) { + return false; /* USART 未在发送(外设异常/时钟丢失),交由调用者计丢弃 */ + } + } + usart_data_transmit(USART0, s_txBuf[s_txTail]); + s_txTail = nextIndex(s_txTail); + return true; +} + +/** + * @brief 向 USART0 发送缓冲写入一个字符,缓冲满时自旋腾位而非丢弃。 * @param[in] ch 待发送字符。 * @param[out] 无。 - * @retval bool true 表示成功写入发送缓冲,false 表示缓冲已满而被丢弃。 - * @note 当前统一由 TBE 中断从队列中取字节发送;此处只负责入队并使能发送中断。 + * @retval bool true 表示成功写入发送缓冲,false 表示 USART 异常导致该字节被丢弃。 + * @note 正常路径仍是"入队 + 使能 TBE 中断"的非阻塞发送。仅在缓冲写满时 + * (3M 波特率下 init 阶段诊断突发写入会写满)就地轮询发出一字节腾位, + * 以字节级背压换取日志完整——旧版此处静默丢弃且 putChar 忽略返回值, + * 实测使 [ECAT] 诊断出现 "bus eaabled"/"pdi_type=0x8Dexect" 等掉字, + * 曾导致 LAN9252 故障被两次误定性,故日志完整性优先于此处的等待开销。 + * 仅当 USART 外设本身不发送时才真正丢弃并累加 s_txDropped。 */ bool Usart0::putCharNonBlocking(int ch) { uint32_t save = RT_CRITICAL_SAVE(); - const uint16_t next = nextIndex(s_txHead); + uint16_t next = nextIndex(s_txHead); if(next == s_txTail) { - RT_CRITICAL_RESTORE(save); - return false; + /* 缓冲已满:就地发出最老的一字节腾出槽位,失败则说明 USART 真的没在发。 */ + if(!drainOneByteLocked()) { + ++s_txDropped; + RT_CRITICAL_RESTORE(save); + return false; + } + next = nextIndex(s_txHead); } s_txBuf[s_txHead] = static_cast(ch); @@ -110,10 +146,10 @@ void Usart0::configurePins() * @brief 初始化 USART0 外设与中断。 * @param[in] 无。 * @param[out] 无。 - * @retval 无返回值。 + * @retval uint8_t Usart0::InitFault 故障码(0=USART 已使能,CTL0.UEN 回读置位)。 * @note 使用 `CONFIG_USART0_BAUDRATE` 配置串口参数,并使能 USART0 TBE 中断用于队列发送。 */ -void Usart0::init() +uint8_t Usart0::init() { rcu_periph_clock_enable(RCU_USART0); usart_deinit(USART0); @@ -125,6 +161,7 @@ void Usart0::init() usart_transmit_config(USART0, USART_TRANSMIT_ENABLE); usart_enable(USART0); nvic_irq_enable(USART0_IRQn, CONFIG_IRQ_PREEMPT_SPI_USART, 0U); + return ((USART_CTL0(USART0) & USART_CTL0_UEN) != 0U) ? kFaultOk : kFaultNotEnabled; } /* ----- 中断入口 / printf 重定向的 C 包装 --------------------------- */ diff --git a/GD32E508_DCU_APP/device/usart0.h b/GD32E508_DCU_APP/device/usart0.h index f3ce5e0..2cd9b11 100644 --- a/GD32E508_DCU_APP/device/usart0.h +++ b/GD32E508_DCU_APP/device/usart0.h @@ -19,14 +19,20 @@ public: */ static void configurePins(); + /** init 故障码(0=成功;经 0xA3 INIT_STATUS fault_codes[] 上报) */ + enum InitFault : uint8_t { + kFaultOk = 0U, /**< 成功 */ + kFaultNotEnabled = 1U, /**< CTL0.UEN 回读未置位(使能未生效) */ + }; + /** * @brief 初始化 USART0 外设与 NVIC。 * @param[in] 无。 * @param[out] 无。 - * @retval 无返回值。 + * @retval uint8_t InitFault 故障码(0=USART 已使能,CTL0.UEN 回读置位)。 * @note 使用 8N1 格式,收发使能,同步配置 NVIC 以便 TXE 中断驱动发送。 */ - static void init(); + static uint8_t init(); /** * @brief 将字符写入 USART0 发送环形缓冲。 @@ -66,10 +72,36 @@ private: static bool putCharNonBlocking(int ch); - static constexpr uint16_t kTxBufSize = 1024U; + /** + * @brief 在临界区内直接轮询 TBE 并发出一个字节,腾出一个缓冲槽位。 + * @param[in] 无。 + * @param[out] 无。 + * @retval bool true 表示成功发出一字节(tail 已前进),false 表示等待 TBE 超时。 + * @note 调用者必须已持有临界区(USART0 中断 preempt=3 被 BASEPRI 屏蔽), + * 因此这里独占 s_txTail,不会与 onInterrupt() 争用同一字节。 + * 不依赖中断投递,故在任意上下文(含 ISR 内 printf)均可安全使用。 + */ + static bool drainOneByteLocked(); + + /** @brief 3M 波特率下 init 阶段诊断是突发写入,1024 字节会溢出致日志掉字。 */ + static constexpr uint16_t kTxBufSize = 4096U; + /** @brief 等待 TBE 的自旋上限:3M 下单字节约 3.3us,此值远大于之,仅防 USART 失效死等。 */ + static constexpr uint32_t kTbeSpinMax = 200000U; static uint8_t s_txBuf[kTxBufSize]; static volatile uint16_t s_txHead; static volatile uint16_t s_txTail; + static volatile uint32_t s_txDropped; + +public: + /** + * @brief 读取因发送缓冲溢出而被丢弃的字节数。 + * @param[in] 无。 + * @param[out] 无。 + * @retval uint32_t 累计丢弃字节数(0 表示日志完整可信)。 + * @note 非 0 说明串口日志有掉字,诊断结论不可全信——历史上掉字日志曾导致 + * LAN9252 故障两次误定性,故显式暴露该计数供 init 报告核对。 + */ + static uint32_t droppedBytes() { return s_txDropped; } }; #endif /* DEVICE_USART0_H */ \ No newline at end of file diff --git a/GD32E508_DCU_APP/device/watchdog.cpp b/GD32E508_DCU_APP/device/watchdog.cpp index ac1af80..a584a4d 100644 --- a/GD32E508_DCU_APP/device/watchdog.cpp +++ b/GD32E508_DCU_APP/device/watchdog.cpp @@ -11,13 +11,39 @@ #include "sys/config.h" #include "main.h" -void Watchdog::init() +uint8_t Watchdog::init() { + /* FWDGT 的 PSC/RLD 写入经 LSI(IRC40K) 时钟域同步(PUD/RUD 由硬件清零)。 + * BOOT 只检测 FWDGT 复位标志、从不启动看门狗,冷启动时 LSI 未开—— + * 直接配置会让 PUD/RUD 永远等不到清零(库超时返回 ERROR,1.5.34 起暴露为 init 失败; + * 此前静默:FWDGT 实际按复位默认值 DIV4/0xFFF 运行,超时就与配置不符)。 + * 必须先手动开 LSI 并等就绪,再配置。 */ + rcu_osci_on(RCU_IRC40K); + if (rcu_osci_stab_wait(RCU_IRC40K) != SUCCESS) { + return kFaultLsiNotReady; + } + + /* PSC/RLD 配置函数自带 ErrStatus(等待 PUD/RUD 清零带超时); + * 此前返回值被忽略,写入失败会无声变成"看门狗实际未按预期运行"。 */ fwdgt_write_enable(); - fwdgt_prescaler_value_config(CONFIG_FWDGT_PSC); - fwdgt_reload_value_config(CONFIG_FWDGT_RLD); + uint8_t fault = kFaultOk; + if (fwdgt_prescaler_value_config((uint16_t)CONFIG_FWDGT_PSC) != SUCCESS) { + fault = kFaultPsc; + } + if (fwdgt_reload_value_config((uint16_t)CONFIG_FWDGT_RLD) != SUCCESS) { + if (fault == kFaultOk) { + fault = kFaultRld; + } + } fwdgt_write_disable(); fwdgt_enable(); + /* 使能后确认无挂起更新(PUD/RUD 应已清零) */ + if ((FWDGT_STAT & (FWDGT_STAT_PUD | FWDGT_STAT_RUD)) != 0U) { + if (fault == kFaultOk) { + fault = kFaultPending; + } + } + return fault; } void Watchdog::feed() diff --git a/GD32E508_DCU_APP/device/watchdog.h b/GD32E508_DCU_APP/device/watchdog.h index f3ce057..8f3a2e8 100644 --- a/GD32E508_DCU_APP/device/watchdog.h +++ b/GD32E508_DCU_APP/device/watchdog.h @@ -12,8 +12,18 @@ class Watchdog { public: - /** @brief 初始化并启动 FWDGT,超时约 CONFIG_FWDGT_TIMEOUT_MS 毫秒。 */ - static void init(); + /** init 故障码(0=成功;经 0xA3 INIT_STATUS fault_codes[] 上报) */ + enum InitFault : uint8_t { + kFaultOk = 0U, /**< 成功 */ + kFaultPsc = 1U, /**< 预分频写入未被硬件确认(PUD 超时) */ + kFaultRld = 2U, /**< 重载值写入未被硬件确认(RUD 超时) */ + kFaultPending = 3U, /**< 使能后仍有挂起的配置更新(PUD/RUD 未清零) */ + kFaultLsiNotReady = 4U, /**< LSI(IRC40K) 使能后稳定等待超时 */ + }; + + /** @brief 初始化并启动 FWDGT,超时约 CONFIG_FWDGT_TIMEOUT_MS 毫秒。 + * @retval uint8_t InitFault 故障码(0=PSC/RLD 写入均经硬件确认且无挂起更新)。 */ + static uint8_t init(); /** @brief 喂狗(重载计数器),应在主循环中周期调用。 */ static void feed(); diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index 27e88cf..45b275b 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -80,6 +80,11 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true; #define CONFIG_CAN_INIT_LOG_ENABLE 1 /** @brief EtherCAT/EXMC 初始化 INFO 级日志;FAIL 恒保留。 */ #define CONFIG_ECAT_INIT_LOG_ENABLE 1 +/** @brief HBI bring-up 诊断探针(bit-bang 探活 + predrive/回显 + 20s 周期复跑)。 + * @note 硬件层已确认全通(IDX_0=0x1234、BYTE_TEST=0x87654321),默认关闭以清理上电日志, + * 并避免每次上电/每 20s 把 EXMC 引脚切成 GPIO 手动拉一遍。换板/补焊需重新排查浮空时置 1。 + * 网页万用表通断测试 continuityDrive() 不受此开关影响,始终可用。 */ +#define CONFIG_ESC_EXMC_DIAG 0 /** @brief 启动横幅与 main 进度打印。 */ #define CONFIG_APP_BANNER_LOG_ENABLE 1 @@ -115,7 +120,7 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true; * ============================================================ */ constexpr uint32_t APP_FW_VERSION_MAJOR = 1U; constexpr uint32_t APP_FW_VERSION_MINOR = 5U; -constexpr uint32_t APP_FW_VERSION_PATCH = 32U; /* 1.5.32:AL 通路诊断打印(排查主站连接后 AL 卡 INIT)——ECAT_Main 收到主站 AL 控制请求打 [ECAT][AL] evt/req、SetALStatus 写回打 [ECAT][AL] -> 状态+错误码、task_runner 5s 心跳 [ECAT][HB] al/code/evt(仅同步 ISR 激活时,心跳停=主循环卡死);dc_app.c 导出 EcatDbg_ReadAlDiag(0x0130+0x0220 快照)。1.5.31:DC 通路观测同步到网页——SSC OD 新增只读记录 0x2000 SYNC Diag(sub1=sync0/sub2=sync1/sub3=escirq/sub4=uptime 秒,UINT32),APPL_InputMapping 每 PDO 周期从 EthercatSync 拉取快照(ethercat_sync.cpp 导出 EcatDbg_Get* extern C);配套 tools/ecat:/api/sync_diag 经 SDO 读 0x2000 并以主站时间戳算 sync0 速率,ecat.html 运行期 2s 轮询显示,串口 5s 低频打印保留为旁证。1.5.30:DC SYNC0 通路可观测性——EthercatSync 增加 s_sync0Count/s_sync1Count/s_escIrqCount 三个 volatile 计数器(ISR 内自增,SYNC0 路径实测仅 LDR/ADDS/STR 三条指令,仍在 .ramfunc);task_runner 新增 ethercatSyncCounterPrint(),5s 诊断按增量打印 sync0 速率(主站设 1ms 周期时应 ≈1000/s),无脉冲时静默。配套 tools/ecat 支持分布时钟:ecat_tool.py send/monitor 新增 --dc-cycle-us/--dc-shift-us(0=free-run 默认),enter_op() 在 config_map 后、进 SAFEOP 前调 config_dc()+dc_sync();ecat_server.py /api/op/start 接受 dc_cycle_us/dc_shift_us;ecat.html 加 DC SYNC0 周期下拉。此前工具从不调用 config_dc,PDO 恒为 free-run,SYNC0 引脚无脉冲、Sync0_Isr 从未触发,1.5.28 配的 word1=0xCC/word2=5000 一直未被实测覆盖。1.5.29:删除 GD32 侧 EEPROM 烧录路径(EscEeprom/esc_eeprom.h/.cpp、eeprom_image.h 及 task_runner 主循环烧录块)——ESI 镜像改由 EtherCAT 主站侧刷写(tools/ecat/ecat_flash_eeprom.py 经 ESC 0x0500 EEPROM 接口,2026-08-31 实测 162 字全校验通过),固件不再负责烧录;gen_eeprom.py 生成器保留。HBI 复查修复:maskEnter/Exit 补屏蔽 EXTI2(SYNC1)——Sync1_Isr 的 PDO_InputMapping→HW_EscWriteIsr 两步事务(preempt=1)可打断主循环 HW_EscRead/Write 致错址;通断测试模式进入/退出改经 maskEnter/Exit 屏蔽三类 EXTI,防测试期 ISR 访问 GPIO 态总线读垃圾。1.5.28:对照 LAN9252 数据手册/AN1907/官方 EVB XML 修正——EEPROM word1 高字节 0x00→0xCC(Sync/Latch 0151h:SYNC0/1 Map+Output、推挽低有效配 EXTI 下降沿;原 0x00 时 SYNC0 引脚留在 LATCH 输入态,DC 模式过程数据死);word2 0→5000(Sync 脉宽 0982h,x10ns=50us,0=确认模式不可用);eeprom_image.h 尾部补真实 0xFF 字节(原注释行致 C 零填充,烧写全阵列);EXMC ADDSET 2→4 HCLK(DS 表9-4 trdrd/twrwr/twrrd≥13ns);XML ConfigData 扩到 14B 并加 CycleTimeSync1。1.5.27:LAN9252 使用修正——EXMC 16-bit 地址左移 1 位(HBI DATA 偏移 0x20000/0x30000→0x40000/0x60000);gen_eeprom.py 按 ETG.1000.6 重写(CRC-8/word7、身份字 word8-15、CoE=0x0004、分类区 byte128);DCU_ESI.xml ConfigData PDI 0x05→0x8D、RxPdo/TxPdo 互换并改 4×64bit 对齐 dc_app.c;HBI 两步事务互斥守卫(主循环 mask EXTI0/1 + PDI/Sync1 ISR 屏蔽 EXTI0);删除死代码 EthercatSlave::periodicIsr 三缓冲;20s HBI 诊断在同步 ISR 激活后自动停跑 */ +constexpr uint32_t APP_FW_VERSION_PATCH = 43U; /* 1.5.43:修复 SAFEOP->OP 永远进不去(主站报 "进 OP 失败: AL=PREOP code=0x0000",且已边发 processdata 边等 3s 仍无效)——根因是 LAN9252 的 IRQ 引脚从未被使能,PDI_Isr 一次都没执行过。IRQ_CFG(0x054) 与 INT_EN(0x05C) 是 LAN9252 系统 CSR(HBI 索引直接可达,不走 CSR 间接端口),复位默认值均为 0,即 IRQ_EN=0(引脚永久去断言)+ ECAT_INT_EN=0(EtherCAT 事件不汇入 IRQ,DS 8.2.6/8.3.1,ECAT_INT 为 INT_STS/INT_EN 的 bit0);固件此前只配了 EXTI/GPIO/NVIC 侧(PB1 上拉输入+下降沿+preempt=1)与 EEPROM 的 SYNC0/1(0x0151=0xCC),从来没打开芯片侧的中断输出总闸。后果链:PDI_Isr 从不触发 → SSC 收不到 PROCESS_OUTPUT_EVENT → bEcatFirstOutputsReceived 恒 FALSE(该标志只在 PDI_Isr(ecatappl.c:305) 或 free-run 的 MainLoop 轮询分支(ecatappl.c:551, 条件 !bEscIntEnabled)里置位)→ AL_ControlInd 的 SAFEOP_2_OP 分支(ecatslv.c:1993 `nPdOutputSize == 0 || bEcatFirstOutputsReceived`) 因本工程有 32B 输出而永不成立 → 迁移被挂起(bApplEsmPending)后回落,AL Status Code 保持 0x0000(不是被拒绝,故不给 0x001B/0x001A)。注意本工程 SyncType 使 UpdateSyncManagerParameter 置 bEscIntEnabled=TRUE(ecatslv.c:951),因此 MainLoop 的 free-run 轮询兜底分支不执行,中断是唯一通路。EscLan9252::init() 在 DL_STATUS bit0 确认 PDI 就绪后新增 enableEcatIrqOutput():先写 INT_STS 清 ECAT_INT 悬留状态(R/WC),再置 INT_EN.ECAT_INT_EN,最后开 IRQ_CFG 总闸并回读校验,打印 [ECAT][ESC] IRQ output enabled(失败则 WARN 并指明 SAFEOP->OP 会卡)。极性与 ethercat_sync.cpp 保持一致:推挽 IRQ_TYPE=1 + 低有效 IRQ_POL=0,配对上拉输入的下降沿 EXTI;INT_DEAS 保持 0=禁用去断言定时器。AL Event Mask(0x204) 仍由 SSC 在状态迁移时自行维护,不在此处干预。配套修主站工具(tools/ecat):(a) _require_op_ready/enter_op 请求 OP 后改为边 send_processdata/receive_processdata 边 state_check 循环至多 3s(SSC 要求先收到首帧输出才 ack OP),中途回落则重发 OP 请求,失败信息按 AL code 分流排查方向;(b) 修正 state_check 超时单位——pysoem docstring 明确 "Timeout value in us",旧代码各处传 2000 实际只等 2ms(SOEM 默认 EC_TIMEOUTSTATE=2000000=2s),PREOP/SAFEOP 能过纯属从站 ack 快的侥幸,现统一用 EC_TIMEOUT_STATE_US=2_000_000 常量。1.5.42:修复主站 config_map 失败(SdoError "Unsupported access to an object" / "Attempt to read to a write only object",实测 0x1600/0x1A00 的 sub0/3/4 报错而 sub1/2 通过)——两个独立的对象字典 bug,均在从站侧,与主站工具无关。(1) REC 对象的 entry descriptor 数组给少了:OBJ_GetEntryDesc()(objdef.c:554) 与 OBJ_GetEntryOffset()(objdef.c:609) 只对 OBJCODE_ARR 复用 pEntryDesc[1] 描述所有子索引,对 OBJCODE_REC 是逐下标索引 pEntryDesc[Subindex];而 dc_app.c 把 0x1600/0x1A00/0x7000/0x6000/0x2000 全声明为 REC(4|OBJCODE_REC<<8) 却只提供 2 元素描述数组,sub3/sub4 越界读到相邻常量数组的垃圾 DataType/ObjAccess,正好解释"sub0/3/4 报错、sub1/2 侥幸通过"的错误模式(sub1/2 恰好落在合法项上)。现为 0x1600/0x1A00 补满 Sub0..Sub4 共 5 项(asPDOMappingEntryDesc),并把原共用的 asDataEntryDesc 拆成 asOutputEntryDesc/asInputEntryDesc(各 5 项)、asSyncDiagEntryDesc 补到 5 项;同时给数据子条目补 OBJACCESS_RXPDOMAPPING/TXPDOMAPPING(官方 sampleappl.h 的 0x7010/0x6000 同此,缺这两个标志主站校验 0x1600/0x1A00 的映射目标时会拒绝),输入侧 0x6000 由 READWRITE 收紧为 ACCESS_READ。0x1C12/0x1C13 是 ARR 保持 2 项不变。(2) 结构体从未被紧排:ecat_def.h 的 STRUCT_PACKED_START/MBX_STRUCT_PACKED_START/OBJ_STRUCT_PACKED_START 三对宏全部留空,注释还误写"AC6 不需要"——实际这些宏就是 SSC 紧排结构体的唯一手段,SSC 的 SDO 读写按"si0 占 16bit、其后各条目按 BitLength 紧排"算字节偏移(OBJ_GetEntryOffset),留空则按编译器自然对齐。armclang 编译期断言实测:{UINT16;UINT64[4]} 的 UINT64 落在字节 8 而非 2,{UINT16;UINT32 x4} 落在字节 4 而非 2,故 0x7000/0x6000/0x2000 的 SDO 单条目访问全部读写错位内存,SSC 内部 0x1C3x(TSYNCMANPAR)/0x10F1 等同样受影响(PDO 路径走 memcpy 未受影响,故此前回环测试无法暴露)。三对宏改为 __attribute__((packed)),并在 dc_app.c 加 _Static_assert 守卫 offsetof==2 与 sizeof==34 防回归。1.5.41:修正三处会误导诊断的日志缺陷(1.5.39 的 LAN9252 访问模型修复经实测已端到端成立:IDX=0x1234、BYTE_TEST=0x87654321、DL_STATUS bit0=1 且 waited 0ms、PDI_CTRL=0x8D、ESC_CFG bit0=0 Device Emulation 关闭、EEP_CSR 无错误位、CSR 写回环 0x0204 写 0x0A50 读回 PASS、SSC MainInit OK;waited 0ms 同时反证 1.5.37「EEPROM 加载竞态」确不存在,读全 0 纯因走错通路)。(1) [ECAT][DIAG] 的 EEP_CSR 位标签错且语义反:0x0502 的 bit11 是 Checksum Error 而非 loaded(真正的加载状态在 bit12,且 0=OK;见 ETG.1000.4 表 49 与 SSC esc.h ESC_EEPROM_ERROR_CRC/LOAD),旧版打印 "loaded=%u" 取 bit11,正常板子恒显示 loaded=0,字面像「EEPROM 未加载」,正是 1.5.37 那个已撤回结论的诱因;现改为 crc_err/load_err/ack_err/wr_err/busy 分位打印并注明全 0=正常。(2) [ECAT][SLAVE] init 终判在无主站时是假警报:s_escReady 要求 AL>=SAFEOP,而 AL 迁移只由主站写 AL Control(0x0120) 驱动、从站不自跳,故冷启动必然打 FAIL(partial),与紧随其后的 SSC MainInit OK 自相矛盾;现按 DL_STATUS 的 Physical Link 位 bit4/5/6(DS 12.14.19) 判定有无主站,空载报 WAIT_MASTER(实测空载 0x5503 三口 link/comm 全 0,接主站 0x5611),link up 却卡 AL 才报真问题并提示逐级 write_state+配 DC。(3) 串口日志丢字节:Usart0::putCharNonBlocking 在 1024B 环形缓冲满时静默丢弃且 putChar 忽略返回值,3M 波特率下 init 阶段诊断突发写入必溢出,实测掉字产生 "bus eaabled"/"pdi_type=0x8Dexect"/"[EAT][SLAVE]" 等伪影,直接污染判断依据;现 kTxBufSize 1024→4096,缓冲满时在临界区内轮询 TBE 就地发出最老字节腾位(drainOneByteLocked,走轮询不等中断,故 ISR 内 printf 亦不死锁;USART0 为 preempt=3 已被 BASEPRI 屏蔽,独占 s_txTail 无争用),仅 USART 外设不发送时才丢弃并累加 s_txDropped,新增 droppedBytes() 并在 init 报告尾部非 0 时打 WARN 自证日志完整性。1.5.40:清理上电 CAN 启动噪声——[CAN-DIAG] CH%u init_nom/init_fd/exit_ok 三行改为仅在本端控制器初始化失败(can_init/can_fd_init 返回失败,或退出 init mode 后 STAT.IWS 仍置位)时以 LOG_ERR 打印;正常上电(三通道 init_nom=1 init_fd=1 exit_ok=1)完全静默。判据只看 GD32 控制器自身,不含 TEC/REC——总线侧计数在未接从站时会因 RX 浮空噪声累积,与 1.5.36 移除 L0 自检 BUS_OFF/TEC/REC 检查同一原则。1.5.39:修复 LAN9252 访问模型根本性错误——EtherCAT 核心寄存器一直读 0/写丢弃(主站恒读 AL=INIT code=0 的真正根因,1.5.35/1.5.37 两次误判已撤回)。网络侧诊断(新增 tools/ecat/ecat_esc_regs.py,经 Npcap 裸 APRD 帧读 ESC + pysoem 读 SII)实测:EEPROM 加载正常(SII CRC=0x9B 相符)、DL_STATUS=0x5611 pdi_oper=1、ESC_CFG pdi_type=0x8D 已生效、AL Status=0x0001,即 ESC 侧一切正常,故障纯在 GD32 的 HBI 访问层。按 DS 12.11/表 12-13:HBI 索引直接寻址的只有 LAN9252 系统 CSR(BYTE_TEST 0x064 等)与访问端口,EtherCAT 核心 CSR(0x0000-0x0FFF:AL Status/Event、SyncManager、EEPROM 接口)必须经 ECAT_CSR_CMD(0x304)/ECAT_CSR_DATA(0x300) 间接访问,过程 RAM(0x1000-0x1FFF:邮箱/PDO)必须经 PRAM FIFO(数据口 0x000-0x03C + 地址长度/命令 0x308-0x314);旧代码把 16 位索引当统一地址空间直读直写,只有 BYTE_TEST 这类系统 CSR 恰好可达,所以「探活全过、业务全死」,且写路径从未被端到端验证过。EscLan9252 新增 csrRead/csrWrite(BUSY 轮询+size 1/2/4)与 pramRead/pramWrite(ADDR_LEN→BUSY→按 avail 轮询读写 FIFO,首/末 DWORD 按 addr&3 取有效字节,别名口轮换),initOnlyReadReg/WriteReg 改走 CSR 端口;hw_gd32exmc.cpp 的 escRead/escWrite 按地址自动路由 CSR/PRAM,AL Event(0x220)/AL Event Mask(0x204)/LED override/HW_GetSyncMan 全部改经 CSR 端口。附带修复:HW_Enable/DisableSyncManChannel 的 Activate 偏移 +4→+6(+4 是 SM Control,Activate 在 0x0806+8n)。1.5.38:修复看门狗 init 失败(SPI 0xA3 实测 crit_fail=0x1000)——FWDGT 的 PSC/RLD 写入经 LSI(IRC40K) 时钟域同步(PUD/RUD 由硬件清零),而 BOOT 只检测 FWDGT 复位标志、从不启动看门狗,冷启动时 LSI 未开,fwdgt_prescaler/reload_value_config 等 PUD/RUD 永远等不到清零、库超时返回 ERROR;该失败自始存在(旧代码忽略返回值时 FWDGT 实际按复位默认值 DIV4/0xFFF≈0.41s 运行,与配置 3.0s 不符,主循环喂狗快才未暴露)。Watchdog::init() 改为先 rcu_osci_on(RCU_IRC40K)+rcu_osci_stab_wait 再配置,新增故障码 kFaultLsiNotReady=4。1.5.37:修复 EEPROM 加载竞态致 EtherCAT 永不就绪(主站恒读 AL=INIT code=0 的根因)——LAN9252 RST# 释放后内部 EEPROM Loader 经 I2C 读 SII 约 30ms,加载完成前 EtherCAT 核心寄存器对 PDI(HBI) 读全 0、写静默丢弃;1.5.35 的 [ECAT][DIAG] 实测 RST# 释放 +3ms 时 DL_STATUS/ESC_CFG/EEP_CSR 全 0、写回环 FAIL,而 SSC 在 +4ms 就已完成 MainInit,AL Event Mask/邮箱 SM 配置全部落空。EscLan9252::init() 在 BYTE_TEST 通过后新增轮询 DL_STATUS(0x0110) bit0(PDI operational/EEPROM loaded,超时沿用 CONFIG_ESC_HBI_POLL_TIMEOUT_MS=300ms;成功打印等待时长,失败打印 DL_STATUS/EEP_CSR 原始值与排查提示)并新增 isCoreReady();initial.cpp 仅在 busReady&&coreReady 时放行 MainInit,总线通但核心未就绪报新故障码 kFaultEthercatCoreNotReady(=3,SPI 0xA3 fault_codes 同步可见)。订正 1.5.35 条目:写回环实际用 AL Event Mask(0x0204) 写 0x0A50 后还原,非 Station Alias(0x012)——后者在部分 ESC 上对 PDI 只读会造成假 FAIL。1.5.36:模块级故障码(fault_codes)——每模块 1B 原因码(0=成功),SPI 0xA3 INIT_STATUS 响应从 12B 扩到 25B([12..24] 按 ModuleBit 位序):canfd=原因码|(首个失败通道<<4)(1=控制器init失败 2=卡init mode 3=STB配置未生效)、ADC(1=RSTCLB超时 2=CLB超时 3=VDDA无效)、Scheduler(1=CEN 2=PSC 3=CAR 4=节拍未前进)、Watchdog(1=PSC 2=RLD 3=挂起)、EtherCAT(1=EXMC总线未就绪 2=SSC MainInit失败)、nvic/esp32_hold/usart0 各单码;init 报告失败模块名带 #XX 故障码后缀,码值定义集中在 sys/initial.h 注释。重要修正:CAN L0 自检移除 BUS_OFF 与 TEC/REC 检查——总线侧状态在通道未接从站(RX 浮空噪声累积 REC)时误报,「没接从站≠错误」;L0 现仅校验 GD32 控制器自身(init失败/卡init mode/STB输出配置),总线健康仍经 0xC1 反馈帧运行期上报(网页 CAN 页可见);CAN L0 按通道打 LOG_ERR(含 STAT/ERR 寄存器);无使能 CAN 通道(全关)属合法配置不再算失败。1.5.35:AL 恒卡 INIT 的判死诊断。主站侧根因已先修:tools/ecat/ecat_server.py 的 _require_op_ready 旧版只 state_check(SAFEOP) 而从不 write_state、且全程未请求过 PREOP——pysoem 的 state_check 只轮询等待、不发 AL Control,故从站从未被要求迁移(现象=AL INIT/code=0x0000 且无错误码);已改为 PREOP→config_map→[DC]→SAFEOP→OP 每步先 write_state 再 state_check。修完后主站确认真的发出 PREOP 请求,但从站仍恒 INIT/code=0x0000,故在 EthercatSlave::init() 增 [ECAT][DIAG] 一次性判死三点:DL_STATUS(0x110) bit0 PDI operational(为 0 则 ESC 静默丢弃 PDI 侧写入的 AL Status)、ESC_CFG(0x140) PDI 类型是否被 EEPROM 加载为 0x8D(HBI 16-bit)、EEP_CSR(0x502) EEPROM loaded/校验错(SII 校验失败会拒绝离开 INIT 且常不给 code);外加 HBI 写路径回环(0x012 Configured Station Alias 写 0xA55A 读回后还原)——此前所有上电探针验证的都是"读"(bit-bang IDX_0 / BYTE_TEST),而 esc_exmc_bus 的 EXMC 写回显测试只在 idx0_raw!=0x1234 的失败分支才跑、因 IDX 正确恒被跳过,故 HBI 写路径从未被端到端验证;若写坏,ECAT_Main 能读到 AL Control 事件却写不回 AL Status,与实测现象完全一致。注意主站读身份/AL 走 ESC 以太网侧+EEPROM(SII),不经过 GD32 的 HBI/PDI,"主站能读到身份"不能证明 PDI 写通。1.5.34:初始化状态检查补全覆盖——原"盲初始化"模块全部改为带验证:SysTick initMs 检查 SysTick_Config 返回值并验证 1ms 节拍真实前进(失败=FATAL 裸串口报告+停机,与 HXTAL FAIL 同级)、ADC 校准改带超时轮询(库函数 adc_calibration_enable 内部死等,硬件异常会永久挂住)并验证 VREFINT→VDDA 采样链、SchedulerTimer 回读 CEN/PSC/CAR 并验证 100us 中断节拍前进、USART0 回读 CTL0.UEN、Watchdog 检查 fwdgt PSC/RLD 的 ErrStatus 返回值+PUD/RUD 无挂起、NVIC 回读 AIRCR.PRIGROUP、ESP32_EN 回读门控置位;新增复位源采集(RCU_RSTSCK→ResetCauseBit 位图,init 报告打印);AppInitializer 导出 moduleMask/criticalFailMask/initState/resetCause getter;新增 SPI 0xA3 INIT_STATUS 命令(12B:模块位图+critical失败位图+复位源+init_state),身份块[15] 原 reserved 字节改填 init_state(旧 ESP32 忽略、旧 GD32 恒 0 与 OK 同义,不 bump feat_map_ver 前后兼容);CONFIG_SPI_MAX_COMMANDS 12→14。附带修复:system_gd32e50x.c 五处 HXTAL FAIL 裸串口诊断段寄存器 bug(CTL0 误写 STAT0 偏移 0x40013800 且漏开 APB2EN bit14 USART0EN,该诊断从未真正发出)——正确布局 CTL0 @ USART0+0x0C。1.5.33:HBI bring-up 诊断探针 EscExmcBus::diagDump 改由 CONFIG_ESC_EXMC_DIAG 门控(默认 0)——硬件层已确认全通(IDX_0=0x1234、BYTE_TEST=0x87654321),清理上电 [EXMC-DIAG] 噪声并停止 task_runner 的 20s 周期复跑,不再每次上电把 EXMC 引脚切成 GPIO 手动拉一遍;换板/补焊排查浮空时置 1 即恢复。网页万用表通断测试 continuityDrive() 及其复用的 bit-bang 辅助函数不受影响,始终可用。1.5.32:AL 通路诊断打印(排查主站连接后 AL 卡 INIT)——ECAT_Main 收到主站 AL 控制请求打 [ECAT][AL] evt/req、SetALStatus 写回打 [ECAT][AL] -> 状态+错误码、task_runner 5s 心跳 [ECAT][HB] al/code/evt(仅同步 ISR 激活时,心跳停=主循环卡死);dc_app.c 导出 EcatDbg_ReadAlDiag(0x0130+0x0220 快照)。1.5.31:DC 通路观测同步到网页——SSC OD 新增只读记录 0x2000 SYNC Diag(sub1=sync0/sub2=sync1/sub3=escirq/sub4=uptime 秒,UINT32),APPL_InputMapping 每 PDO 周期从 EthercatSync 拉取快照(ethercat_sync.cpp 导出 EcatDbg_Get* extern C);配套 tools/ecat:/api/sync_diag 经 SDO 读 0x2000 并以主站时间戳算 sync0 速率,ecat.html 运行期 2s 轮询显示,串口 5s 低频打印保留为旁证。1.5.30:DC SYNC0 通路可观测性——EthercatSync 增加 s_sync0Count/s_sync1Count/s_escIrqCount 三个 volatile 计数器(ISR 内自增,SYNC0 路径实测仅 LDR/ADDS/STR 三条指令,仍在 .ramfunc);task_runner 新增 ethercatSyncCounterPrint(),5s 诊断按增量打印 sync0 速率(主站设 1ms 周期时应 ≈1000/s),无脉冲时静默。配套 tools/ecat 支持分布时钟:ecat_tool.py send/monitor 新增 --dc-cycle-us/--dc-shift-us(0=free-run 默认),enter_op() 在 config_map 后、进 SAFEOP 前调 config_dc()+dc_sync();ecat_server.py /api/op/start 接受 dc_cycle_us/dc_shift_us;ecat.html 加 DC SYNC0 周期下拉。此前工具从不调用 config_dc,PDO 恒为 free-run,SYNC0 引脚无脉冲、Sync0_Isr 从未触发,1.5.28 配的 word1=0xCC/word2=5000 一直未被实测覆盖。1.5.29:删除 GD32 侧 EEPROM 烧录路径(EscEeprom/esc_eeprom.h/.cpp、eeprom_image.h 及 task_runner 主循环烧录块)——ESI 镜像改由 EtherCAT 主站侧刷写(tools/ecat/ecat_flash_eeprom.py 经 ESC 0x0500 EEPROM 接口,2026-08-31 实测 162 字全校验通过),固件不再负责烧录;gen_eeprom.py 生成器保留。HBI 复查修复:maskEnter/Exit 补屏蔽 EXTI2(SYNC1)——Sync1_Isr 的 PDO_InputMapping→HW_EscWriteIsr 两步事务(preempt=1)可打断主循环 HW_EscRead/Write 致错址;通断测试模式进入/退出改经 maskEnter/Exit 屏蔽三类 EXTI,防测试期 ISR 访问 GPIO 态总线读垃圾。1.5.28:对照 LAN9252 数据手册/AN1907/官方 EVB XML 修正——EEPROM word1 高字节 0x00→0xCC(Sync/Latch 0151h:SYNC0/1 Map+Output、推挽低有效配 EXTI 下降沿;原 0x00 时 SYNC0 引脚留在 LATCH 输入态,DC 模式过程数据死);word2 0→5000(Sync 脉宽 0982h,x10ns=50us,0=确认模式不可用);eeprom_image.h 尾部补真实 0xFF 字节(原注释行致 C 零填充,烧写全阵列);EXMC ADDSET 2→4 HCLK(DS 表9-4 trdrd/twrwr/twrrd≥13ns);XML ConfigData 扩到 14B 并加 CycleTimeSync1。1.5.27:LAN9252 使用修正——EXMC 16-bit 地址左移 1 位(HBI DATA 偏移 0x20000/0x30000→0x40000/0x60000);gen_eeprom.py 按 ETG.1000.6 重写(CRC-8/word7、身份字 word8-15、CoE=0x0004、分类区 byte128);DCU_ESI.xml ConfigData PDI 0x05→0x8D、RxPdo/TxPdo 互换并改 4×64bit 对齐 dc_app.c;HBI 两步事务互斥守卫(主循环 mask EXTI0/1 + PDI/Sync1 ISR 屏蔽 EXTI0);删除死代码 EthercatSlave::periodicIsr 三缓冲;20s HBI 诊断在同步 ISR 激活后自动停跑 */ constexpr uint32_t APP_FW_VERSION_BUILD = 0U; @@ -168,11 +173,11 @@ constexpr uint16_t CONFIG_SPI_LOG_BUF_SIZE = 256U; constexpr uint32_t CONFIG_SPI_RING_TIMEOUT_MS = 50U; /** @brief SPI 命令 handler 注册表容量。 * - * 当前 init 注册 9 个:boot_request / ota_wait / ping / can_bridge / - * can_feedback / id_snapshot / adc_feedback / motor_state / motor_ota。 + * 当前 init 注册 12 个:boot_request / ota_wait / ping / init_status / can_bridge / + * can_burst / can_feedback / id_snapshot / adc_feedback / motor_state / motor_ota / exmc_diag。 * registerHandler 表满时静默丢弃,motor_ota 曾因容量=8 被丢弃(0xD0 回 0xEE)。 - * 取 12 留余量;新增 handler 时须确认本容量足够。 */ -constexpr uint8_t CONFIG_SPI_MAX_COMMANDS = 12U; + * 取 14 留余量;新增 handler 时须确认本容量足够。 */ +constexpr uint8_t CONFIG_SPI_MAX_COMMANDS = 14U; /** @brief 每通道不重复 ID 快照表最大条目数(与 ESP32 CANFD_ID_SNAPSHOT_MAX 对齐)。 */ constexpr uint8_t CONFIG_CANFD_ID_SNAPSHOT_MAX = 16U; /** @brief 快照条目最大数据字节数(CAN FD DLC 上限)。 */ diff --git a/GD32E508_DCU_APP/sys/initial.cpp b/GD32E508_DCU_APP/sys/initial.cpp index 8c403b3..1dfb818 100644 --- a/GD32E508_DCU_APP/sys/initial.cpp +++ b/GD32E508_DCU_APP/sys/initial.cpp @@ -3,9 +3,12 @@ * @brief 应用启动初始化:模块成功置位,结束统一报告结果。 * * 顺序: - * NVIC → SysTick → ESP32_EN 保持 → USART0 → AppSpi(尽早 HELLO 探活)→ + * 复位源采集 → NVIC → SysTick → ESP32_EN 保持 → USART0 → AppSpi(尽早 HELLO 探活)→ * ADC → CANFD → OtaBootRequest → TaskManager → Scheduler → * EtherCAT(+Sync 可选) → Watchdog → report + * + * 1.5.34:各模块 init 从"盲配置"改为带检查(寄存器回读 / 节拍验证 / 校准超时), + * 失败不置位;复位源/模块位图/critical 失败位图经 getter 导出供 SPI 0xA3 上报。 */ #include "sys/initial.h" @@ -30,6 +33,9 @@ void MainLoop(void); } uint32_t AppInitializer::s_mask = 0U; +uint8_t AppInitializer::s_initState = AppInitializer::kInitOk; +uint8_t AppInitializer::s_resetCause = 0U; +uint8_t AppInitializer::s_faultCodes[AppInitializer::kModuleCount] = {0U}; namespace { @@ -62,6 +68,71 @@ void AppInitializer::mark(ModuleBit b) s_mask |= static_cast(b); } +/** @brief ModuleBit(1< 1U) { + bit >>= 1U; + ++idx; + } + return idx; +} + +void AppInitializer::setFault(ModuleBit b, uint8_t code) +{ + const uint8_t idx = moduleBitIndex(static_cast(b)); + if (idx < kModuleCount) { + s_faultCodes[idx] = code; + } +} + +uint8_t AppInitializer::faultCode(uint8_t module_index) +{ + return (module_index < kModuleCount) ? s_faultCodes[module_index] : 0U; +} + +uint8_t AppInitializer::faultCodes(uint8_t *out) +{ + if (out == nullptr) { + return 0U; + } + for (uint8_t i = 0U; i < kModuleCount; ++i) { + out[i] = s_faultCodes[i]; + } + return kModuleCount; +} + +uint32_t AppInitializer::criticalFailMask() +{ + uint32_t fail = 0U; + for (size_t i = 0; i < sizeof(kEntries) / sizeof(kEntries[0]); ++i) { + const InitEntry &e = kEntries[i]; + if (e.critical && (s_mask & e.bit) == 0U) { + fail |= e.bit; + } + } + return fail; +} + +/** + * @brief 采集 RCU_RSTSCK 复位源位图并清硬件标志。 + * @note 必须在 init 最开头调用(早于任何可能读标志的模块); + * 清除不影响 BOOT 的 FWDGT 停留 OTA 判定——新的看门狗复位会由硬件重新置位。 + */ +void AppInitializer::captureResetCause() +{ + uint8_t cause = 0U; + if (rcu_flag_get(RCU_FLAG_PORRST) != RESET) cause |= kRstPor; + if (rcu_flag_get(RCU_FLAG_EPRST) != RESET) cause |= kRstEpr; + if (rcu_flag_get(RCU_FLAG_FWDGTRST) != RESET) cause |= kRstFwdgt; + if (rcu_flag_get(RCU_FLAG_WWDGTRST) != RESET) cause |= kRstWwdgt; + if (rcu_flag_get(RCU_FLAG_SWRST) != RESET) cause |= kRstSw; + if (rcu_flag_get(RCU_FLAG_LPRST) != RESET) cause |= kRstLp; + rcu_all_reset_flag_clear(); + s_resetCause = cause; +} + void AppInitializer::report() { bool critical_ok = true; @@ -89,51 +160,119 @@ void AppInitializer::report() } fail_list[fail_len] = '\0'; } + /* 故障码后缀 #XX(16 进制;canfd 高 4 位为通道号),0=成功不追加。 + * 码值含义见 initial.h 注释与各模块头文件;网页按同一编码解码中文原因。 */ + const uint8_t fc = s_faultCodes[moduleBitIndex(e.bit)]; + if (fc != 0U && fail_len + 3U < sizeof(fail_list)) { + const char *hex = "0123456789ABCDEF"; + fail_list[fail_len++] = '#'; + fail_list[fail_len++] = hex[(fc >> 4) & 0xFU]; + fail_list[fail_len++] = hex[fc & 0xFU]; + fail_list[fail_len] = '\0'; + } } + /* 复位源随 init 报告输出一次(位图多位置位属正常,如 POR 常伴随 EPRST/SWRST) */ + printf("[APP] reset_cause: 0x%02X (por=%d epr=%d fwdgt=%d wwdgt=%d sw=%d lp=%d)\r\n", + (unsigned)s_resetCause, + (s_resetCause & kRstPor) ? 1 : 0, + (s_resetCause & kRstEpr) ? 1 : 0, + (s_resetCause & kRstFwdgt) ? 1 : 0, + (s_resetCause & kRstWwdgt) ? 1 : 0, + (s_resetCause & kRstSw) ? 1 : 0, + (s_resetCause & kRstLp) ? 1 : 0); + if (critical_ok && all_ok) { + s_initState = kInitOk; printf("[APP] init: OK (all modules)\r\n"); } else if (critical_ok) { + s_initState = kInitDegraded; printf("[APP] init: OK (critical), skipped/failed: %s\r\n", fail_list[0] != '\0' ? fail_list : "?"); } else { + s_initState = kInitCriticalFail; printf("[APP] init: FAIL, modules: %s\r\n", fail_list[0] != '\0' ? fail_list : "?"); } + + /* 日志完整性自证:串口 TX 缓冲溢出会静默吞字节,1.5.40 及之前实测使 [ECAT] 诊断 + * 出现 "bus eaabled"/"pdi_type=0x8Dexect" 等掉字,并两次导致 LAN9252 故障误定性。 + * 1.5.41 起缓冲满时自旋腾位,仅 USART 外设异常才丢弃;非 0 即代表上面的日志不可全信。 */ + const uint32_t dropped = Usart0::droppedBytes(); + if (dropped != 0U) { + printf("[APP] WARN: uart dropped %u bytes, 以上日志可能不完整\r\n", + (unsigned)dropped); + } } void AppInitializer::init() { s_mask = 0U; + s_initState = kInitOk; + for (uint8_t i = 0U; i < kModuleCount; ++i) { + s_faultCodes[i] = 0U; + } + captureResetCause(); + + /* NVIC 优先级分组:写入后回读 AIRCR.PRIGROUP 验证(纯寄存器写,回读确认分组生效) */ nvic_priority_group_set(CONFIG_NVIC_PRIORITY_GROUP); - mark(kNvic); + if ((SCB->AIRCR & (0x7UL << 8U)) == (CONFIG_NVIC_PRIORITY_GROUP & (0x7UL << 8U))) { + mark(kNvic); + } else { + setFault(kNvic, kFaultNvicPrigroupMismatch); + } - SysTickTimer::initMs(); - mark(kSysTick); + /* SysTick:内部验证节拍真实前进;失败不返回(FATAL 停机) */ + if (SysTickTimer::initMs()) { + mark(kSysTick); + } rcu_periph_clock_enable(RCU_GPIOC); gpio_bit_set(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN); gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, CONFIG_ESP32_RESET_PIN); - mark(kEsp32Hold); - /* ESP32 EN 自检:日志里能直接看出是上电即拉高,还是被外部钳住 */ - APP_LOGI("[APP] ESP32_EN PC9=%d (1=released)\r\n", - (int)gpio_input_bit_get(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN)); + /* ESP32 EN 自检:回读为 1 才算成功(被外部钳低属硬件异常,标记失败但不阻塞其余模块) */ + const bool esp32_released = + (gpio_input_bit_get(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN) == SET); + if (esp32_released) { + mark(kEsp32Hold); + } else { + setFault(kEsp32Hold, kFaultEsp32HoldClamped); + } + APP_LOGI("[APP] ESP32_EN PC9=%d (1=released)\r\n", esp32_released ? 1 : 0); Usart0::configurePins(); - Usart0::init(); - mark(kUsart0); + const uint8_t usartFault = Usart0::init(); + if (usartFault == Usart0::kFaultOk) { + mark(kUsart0); + } else { + setFault(kUsart0, usartFault); + } - /* SPI 尽早就绪:OTA FINISH 跳转后 ESP32 用 HELLO 探活,不能等 CAN 上电延时 */ + /* SPI 尽早就绪:OTA FINISH 跳转后 ESP32 用 HELLO 探活,不能等 CAN 上电延时。 + * 从机无硬件自证手段(配置即生效),探活依赖 ESP32 侧 HELLO/PING(运行期)。 */ AppSpiCommandBus.init(); mark(kAppSpi); - Adc::init(); - mark(kAdc); + /* ADC:带超时校准 + VREFINT→VDDA 采样链验证 */ + const uint8_t adcFault = Adc::init(); + if (adcFault == Adc::kFaultOk) { + mark(kAdc); + } else { + setFault(kAdc, adcFault); + LOG_ERR("[APP] ADC init FAIL fault=%u (1=RSTCLB timeout 2=CLB timeout 3=VDDA invalid)\r\n", + (unsigned)adcFault); + } CanFd::init(); - /* L0 通过才置 canfd 位 */ - if (CanFd::selfTestL0()) { + /* L0 通过才置 canfd 位;故障码 = 原因 | (首个失败通道 << 4)。 + * 无使能通道(canFault==Ok)属合法配置而非故障,照样置位。 */ + uint8_t canFault = CanFd::kSelfTestOk; + uint8_t canFaultCh = 0U; + (void)CanFd::selfTestL0(&canFault, &canFaultCh); + if (canFault != CanFd::kSelfTestOk) { + setFault(kCanFd, (uint8_t)(canFault | (uint8_t)(canFaultCh << 4U))); + } else { mark(kCanFd); } @@ -143,17 +282,28 @@ void AppInitializer::init() TaskManager::init(); mark(kTaskManager); - SchedulerTimer::init(); - mark(kScheduler); + /* 调度定时器:回读 + 100us 中断节拍前进验证 */ + const uint8_t schedFault = SchedulerTimer::init(); + if (schedFault == SchedulerTimer::kFaultOk) { + mark(kScheduler); + } else { + setFault(kScheduler, schedFault); + LOG_ERR("[APP] SchedulerTimer init FAIL fault=%u (1=CEN 2=PSC 3=CAR 4=no tick)\r\n", + (unsigned)schedFault); + } /* ---- 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) + * 加载完成(DL_STATUS bit0);未加载时 EtherCAT 核心寄存器写静默丢弃, + * SSC 的 AL Event Mask/邮箱 SM 配置会全部落空(主站恒读 AL=INIT 的根因), + * 此时跳过 SSC 并以独立故障码上报。 * EEPROM 校验/烧录延迟到主循环执行,避免阻塞 init(烧录需 ~20s)。 */ EthercatSlave::init(); - if (EscLan9252::isBusReady()) { + if (EscLan9252::isBusReady() && EscLan9252::isCoreReady()) { const uint16_t sscResult = MainInit(); if (sscResult == 0U) { mark(kEthercat); @@ -161,14 +311,26 @@ void AppInitializer::init() mark(kEthercatSync); printf("[ECAT] SSC MainInit OK, sync ISR enabled\r\n"); } else { + setFault(kEthercat, kFaultEthercatSscFail); printf("[ECAT] SSC MainInit FAIL (code=0x%04X)\r\n", (unsigned)sscResult); } + } else if (EscLan9252::isBusReady()) { + setFault(kEthercat, kFaultEthercatCoreNotReady); + printf("[ECAT] EtherCAT core not ready (EEPROM not loaded), SSC skipped\r\n"); } else { + setFault(kEthercat, kFaultEthercatBusNotReady); printf("[ECAT] EXMC bus not ready, SSC skipped\r\n"); } - Watchdog::init(); - mark(kWatchdog); + /* 看门狗放最后:PSC/RLD 写入经硬件确认才置位 */ + const uint8_t wdgFault = Watchdog::init(); + if (wdgFault == Watchdog::kFaultOk) { + mark(kWatchdog); + } else { + setFault(kWatchdog, wdgFault); + LOG_ERR("[APP] Watchdog init FAIL fault=%u (1=PSC 2=RLD 3=pending 4=LSI not ready)\r\n", + (unsigned)wdgFault); + } report(); } diff --git a/GD32E508_DCU_APP/sys/initial.h b/GD32E508_DCU_APP/sys/initial.h index 561749e..ed0bb3d 100644 --- a/GD32E508_DCU_APP/sys/initial.h +++ b/GD32E508_DCU_APP/sys/initial.h @@ -6,10 +6,12 @@ /** * @brief 应用初始化器:按固定顺序启动模块,成功位置 1,末尾汇总。 + * @note 汇总结果(模块位图 / critical 失败位图 / 复位源 / init 状态)经静态 getter 导出, + * 供 SPI 0xA3 INIT_STATUS 命令与身份块 [15] 上报 ESP32。 */ class AppInitializer { public: - /** 启动模块位(成功置 1) */ + /** 启动模块位(成功置 1);与 SPI 0xA3 上报位图共用,改动须同步 ESP32 侧注释 */ enum ModuleBit : uint32_t { kNvic = 1u << 0, kSysTick = 1u << 1, @@ -26,15 +28,92 @@ public: kWatchdog = 1u << 12, }; + /** init 总体状态(身份块 [15] 与 0xA3 payload[9] 共用编码) */ + enum InitState : uint8_t { + kInitOk = 0U, /**< 全部模块成功 */ + kInitDegraded = 1U, /**< critical 全成功,非 critical 有失败/跳过 */ + kInitCriticalFail = 2U, /**< 有 critical 模块失败 */ + }; + + /** 复位源位图(0xA3 payload[8] 编码),多位置位属正常(如 POR 常伴随其它标志) */ + enum ResetCauseBit : uint8_t { + kRstPor = 1u << 0, /**< 上电/掉电复位 */ + kRstEpr = 1u << 1, /**< 外部引脚复位(NRST) */ + kRstFwdgt = 1u << 2, /**< 独立看门狗复位 */ + kRstWwdgt = 1u << 3, /**< 窗口看门狗复位 */ + kRstSw = 1u << 4, /**< 软件复位(含 OTA 跳转) */ + kRstLp = 1u << 5, /**< 低功耗复位 */ + }; + + /** 模块数量(fault code 数组长度,与 ModuleBit 位序一致:bit N → 索引 N) */ + static constexpr uint8_t kModuleCount = 13U; + + /** + * @brief 模块故障码(0xA3 payload[12..24],每模块 1B,0=成功)。 + * + * 各模块码值定义位置: + * - nvic : kFaultNvic*(本类下) + * - systick : kFaultSystick*(本类下;实际失败时 FATAL 停机不上报,仅备查) + * - esp32_hold : kFaultEsp32Hold*(本类下) + * - usart0 : Usart0::InitFault(device/usart0.h) + * - adc : Adc::InitFault(device/adc.h) + * - canfd : CanFd::SelfTestFault(device/canfd.h), + * **编码 = 原因码 | (首个失败通道号 << 4)**,通道 0/1/2 + * - ota_boot_req: 纯软件,恒 0 + * - app_spi : 无自证手段,恒 0 + * - task_manager: 纯软件,恒 0 + * - scheduler : SchedulerTimer::InitFault(device/timer.h) + * - ethercat : kFaultEthercat*(本类下) + * - ethercat_sync: 跟随 ethercat,恒 0 + * - watchdog : Watchdog::InitFault(device/watchdog.h) + */ + enum NvicFault : uint8_t { + kFaultNvicOk = 0U, + kFaultNvicPrigroupMismatch = 1U, /**< AIRCR.PRIGROUP 回读与配置不一致 */ + }; + enum SystickFault : uint8_t { + kFaultSystickOk = 0U, + kFaultSystickConfig = 1U, /**< SysTick_Config 失败(备查,实际 FATAL 停机) */ + kFaultSystickNoTick = 2U, /**< 节拍未前进(备查,实际 FATAL 停机) */ + }; + enum Esp32HoldFault : uint8_t { + kFaultEsp32HoldOk = 0U, + kFaultEsp32HoldClamped = 1U, /**< PC9 输出高后回读仍为 0(被外部钳低) */ + }; + enum EthercatFault : uint8_t { + kFaultEthercatOk = 0U, + kFaultEthercatBusNotReady = 1U, /**< EXMC/HBI 总线探活失败(BYTE_TEST 未通过) */ + kFaultEthercatSscFail = 2U, /**< SSC MainInit 返回非 0 */ + kFaultEthercatCoreNotReady = 3U, /**< EEPROM 未加载完成,EtherCAT 核心未使能(1.5.39) */ + }; + /** * @brief 按固定顺序完成模块初始化(过程只置位,结束统一报告成败/失败模块)。 */ static void init(); + /** @brief 已成功初始化的模块位图。 */ + static uint32_t moduleMask() { return s_mask; } + /** @brief 初始化失败的 critical 模块位图(0=无 critical 失败)。 */ + static uint32_t criticalFailMask(); + /** @brief init 总体状态(InitState 编码)。 */ + static uint8_t initState() { return s_initState; } + /** @brief 本次上电复位源位图(ResetCauseBit 编码,init 开头采集并已清硬件标志)。 */ + static uint8_t resetCause() { return s_resetCause; } + /** @brief 指定模块(ModuleBit 位序索引 0..12)的故障码,0=成功。 */ + static uint8_t faultCode(uint8_t module_index); + /** @brief 将全部模块故障码拷贝到 out(至少 kModuleCount 字节),返回长度。 */ + static uint8_t faultCodes(uint8_t *out); + private: static uint32_t s_mask; + static uint8_t s_initState; + static uint8_t s_resetCause; + static uint8_t s_faultCodes[kModuleCount]; static void mark(ModuleBit b); + static void setFault(ModuleBit b, uint8_t code); + static void captureResetCause(); static void report(); }; diff --git a/GD32E508_DCU_APP/sys/systick.cpp b/GD32E508_DCU_APP/sys/systick.cpp index 2594a91..61d60e1 100644 --- a/GD32E508_DCU_APP/sys/systick.cpp +++ b/GD32E508_DCU_APP/sys/systick.cpp @@ -34,16 +34,55 @@ void SysTickTimer::delayMs(uint32_t ms) } /** - * @brief 初始化 1ms SysTick 时基。 + * @brief SysTick 失效时的致命停机:裸寄存器 USART0(PA9) 输出 FATAL 后挂死。 * @param[in] 无。 * @param[out] 无。 - * @retval 无返回值。 - * @note 先更新 `SystemCoreClock`,再按 1kHz 频率配置 SysTick。 + * @retval 无返回值(不返回)。 + * @note 此时 USART0 驱动尚未初始化,只能裸写寄存器; + * 时钟树已是 180MHz(SystemInit 完成),BAUD=60 对应 3M(与工程 printf 一致)。 + * 注意 system_gd32e50x.c 的 HXTAL FAIL 段有寄存器 bug(CTL0 写到 STAT0 偏移、 + * 漏开 USART0EN),本段使用修正后的地址:APB2EN bit14=USART0EN、CTL0 @ +0x0C。 */ -void SysTickTimer::initMs() +static void fatalSystickHalt() +{ + *((volatile uint32_t *)0x40021018U) |= (1U << 14) | (1U << 2) | (1U << 0); /* USART0EN|PAEN|AFEN */ + { volatile uint32_t _ctl = *((volatile uint32_t *)0x40010804U); /* PA9 → AF_PP */ + _ctl &= ~(0xFU << 4); _ctl |= (0xBU << 4); + *((volatile uint32_t *)0x40010804U) = _ctl; } + *((volatile uint32_t *)0x40013808U) = 60U; /* BAUD: 180MHz/3M */ + *((volatile uint32_t *)0x4001380CU) = (1U << 3) | (1U << 13); /* CTL0: TEN | UEN */ + { const char *_msg = "[APP] init: FATAL systick\r\n"; + for(int i=0; _msg[i]; i++){ + *((volatile uint32_t *)0x40013804U) = _msg[i]; /* DATA */ + while((*((volatile uint32_t *)0x40013800U) & (1U << 7)) == 0U) {} } } /* STAT0.TBE */ + while(1){ } +} + +/** + * @brief 初始化 1ms SysTick 时基并验证中断真实触发。 + * @param[in] 无。 + * @param[out] 无。 + * @retval bool true=节拍已前进;false=配置失败(不返回,直接 FATAL 停机)。 + * @note 先更新 `SystemCoreClock`,再按 1kHz 频率配置 SysTick。 + * 节拍验证失败时系统失去一切超时/延时基础,继续运行等于盲跑, + * 按致命错误处理(裸串口报告 + 挂死,与 HXTAL FAIL 同级)。 + */ +bool SysTickTimer::initMs() { SystemCoreClockUpdate(); - SysTick_Config(SystemCoreClock / 1000U); + if (SysTick_Config(SystemCoreClock / 1000U) != 0U) { + fatalSystickHalt(); + } + /* 有界等待节拍前进:guard 预算约 50ms+,覆盖 1ms 节拍; + * 若无界等待,中断未触发时此处即死循环且无任何诊断。 */ + const uint32_t start = s_ms; + volatile uint32_t guard = 2000000U; + while (s_ms == start) { + if (--guard == 0U) { + fatalSystickHalt(); + } + } + return true; } /* ----- 中断入口 C 包装 ---------------------------------------------- */ diff --git a/GD32E508_DCU_APP/sys/systick.h b/GD32E508_DCU_APP/sys/systick.h index 901b887..f758192 100644 --- a/GD32E508_DCU_APP/sys/systick.h +++ b/GD32E508_DCU_APP/sys/systick.h @@ -13,9 +13,10 @@ public: * @brief 初始化 SysTick 为 1ms 系统节拍。 * @param[in] 无。 * @param[out] 无。 - * @retval 无返回值。 + * @retval bool true=配置成功且中断真实触发;false=配置失败或节拍未前进。 + * @note 失败时系统失去时基(后续 delayMs 不可用),调用方应按致命错误处理。 */ - static void initMs(); + static bool initMs(); /** * @brief 执行阻塞式毫秒延时。 diff --git a/GD32E508_DCU_APP/sys/task_runner.cpp b/GD32E508_DCU_APP/sys/task_runner.cpp index 5dbfca2..ee6d648 100644 --- a/GD32E508_DCU_APP/sys/task_runner.cpp +++ b/GD32E508_DCU_APP/sys/task_runner.cpp @@ -502,7 +502,10 @@ void TaskRunner::runPending() /* === HBI 周期诊断:每 20 秒重新跑一次 EXMC/HBI 探活,方便热插拔/焊接后观察 === * 1.5.27:EtherCAT 同步 ISR 激活后停止(diagDump 会把 EXMC 引脚切成 GPIO, - * 拆毁运行中的总线;诊断的 bit-bang 也与 HBI 事务互斥冲突)。 */ + * 拆毁运行中的总线;诊断的 bit-bang 也与 HBI 事务互斥冲突)。 + * 1.5.33:整块由 CONFIG_ESC_EXMC_DIAG 门控,默认关闭——硬件已确认全通, + * 不再每 20s 拆总线做探针;换板/补焊排查浮空时置 1 恢复。 */ +#if CONFIG_ESC_EXMC_DIAG static uint32_t s_lastDiagSec = 0xFFFFFFFFU; if (!EthercatSync::isActive() && (g_app_uptime_sec >= s_lastDiagSec + 20U || s_lastDiagSec == 0xFFFFFFFFU)) { @@ -511,6 +514,7 @@ void TaskRunner::runPending() (unsigned long)g_app_uptime_sec); EscExmcBus::diagDump(); } +#endif /* 延迟 EEPROM 烧录已删除(1.5.29):ESI 镜像改由 EtherCAT 主站侧刷写 * (tools/ecat/ecat_flash_eeprom.py 经 ESC 0x0500 EEPROM 接口),