diff --git a/.trae/documents/cleanup-residual-low-risk-optimizations.md b/.trae/documents/cleanup-residual-low-risk-optimizations.md new file mode 100644 index 0000000..533181a --- /dev/null +++ b/.trae/documents/cleanup-residual-low-risk-optimizations.md @@ -0,0 +1,256 @@ +# 清理剩余低风险可优化项 Plan + +## Summary + +在上一轮 `purge-legacy-dead-code` spec(已清除 23 项 A 类死代码 + 11 项 B/C/D 类改进)基础上,对 `GD32E508_DCU_APP/` 进行二次扫描,识别并处理 **13 项剩余低风险可优化项**(不改变运行期行为,构建可验证)。 + +覆盖三类: +- **A 类残留死代码**(6 项):死别名、写而不读成员/静态、空头文件、冗余类型别名 +- **过期/缺失注释**(3 项):PRIMASK 过期说明、taskHeartbeat 过期注释、复位前 PRIMASK 缺解释 +- **重复代码提取**(4 项):spinDelay 延时循环、armRingPending 赋值模式、writeU32Le 跨文件去重、adc.cpp namespace 合并 + +## Current State Analysis + +### 已完成(上一轮 spec) +- A1-A14:23 项死代码清除(CAN 兼容轮询链/SPI v1 状态机/EtherCAT 死 API/ADC/USART0/Watchdog/TempSensor 死接口等) +- B1-B6:6 项前瞻性桩加 TODO 标记 +- C1-C4:4 项条件编译优化 +- D1:1 项历史注释清理 +- 构建验证通过(43/43,0 错误,Code=47462) + +### 本轮新发现(经 Grep 交叉验证) +二次扫描发现 13 项上一轮未覆盖的低风险优化项,均不改变运行期行为。 + +## Proposed Changes + +### 第一组:A 类残留死代码清除(6 项) + +#### 1.1 删除死类型别名 `canfd_diag_t` +- **文件**:`GD32E508_DCU_APP/device/canfd.h:211` +- **现状**:`using canfd_diag_t = CanFd::Diag;` — Grep 全项目仅命中声明自身,无任何使用方 +- **改法**:删除第 211 行及相邻空行 +- **风险**:低(纯类型别名删除) + +#### 1.2 删除写而永不读成员 `m_ringTxLen` +- **文件**: + - `GD32E508_DCU_APP/device/spi/app_spi_command.h:119-120`(声明 + 注释) + - `GD32E508_DCU_APP/device/spi/app_spi_command.cpp:407, 712, 745, 859, 867`(5 处写入) +- **现状**:`volatile uint16_t m_ringTxLen = 0U;` 头注释称"供 onCsAssert 装入 DMA",但 `onCsAssert` 实际使用的是 `m_txLen`。在 `requestEsp32Read`(745-746 行)中 `m_ringTxLen` 与 `m_txLen` 被赋同值,`m_ringTxLen` 是冗余写者。Grep 确认 5 处写入、0 处读取。 +- **改法**: + 1. 删除 `app_spi_command.h:119-120`(注释 + 成员声明) + 2. 删除 `app_spi_command.cpp` 中 5 处 `m_ringTxLen = ...;` 赋值语句 +- **风险**:低(仅删除写操作,`m_txLen` 与 `m_ringTxLen` 始终同值,读取路径不变) +- **注意**:745 行 `m_ringTxLen = (uint16_t)built_len;` 的下一行 `m_txLen = (uint16_t)built_len;` 必须保留 + +#### 1.3 删除写而永不读静态 `s_pushCount` +- **文件**:`GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp:257, 282` +- **现状**:`static uint32_t s_pushCount = 0U;` 仅被自增(282 行 `++s_pushCount;`),从未被读取、打印或暴露给诊断接口 +- **改法**:删除 257 行声明与 282 行自增语句(含其注释 `/* 仅保留计数,不打印心跳日志 */`) +- **风险**:低(纯死状态) + +#### 1.4 删除 4 个未使用本地别名 +- **文件**:`GD32E508_DCU_APP/device/spi/app_spi_command.cpp:49, 51, 54, 55` +- **现状**:本地别名块(48-56 行)共 9 个别名,其中 `kTypeCanSend`/`kTypeAppPing`/`kRspAck`/`kRspWait` 4 个在文件内从未被引用(Grep 仅命中声明行)。其余 5 个(`kTypeBootReq`/`kTypeOtaReq`/`kTypeOtaData`/`kTypeOtaCancel`/`kRspError`)仍被使用。 +- **改法**:删除第 49、51、54、55 行 +- **风险**:低(删除 `static constexpr` 未使用符号) + +#### 1.5 删除空头文件 `sys/hardfault.h` +- **文件**:`GD32E508_DCU_APP/sys/hardfault.h`(整文件,仅 3 行 include guard) +- **现状**:A13 清理后该头文件已无任何声明,仅剩 `#ifndef HARDFAULT_H / #define HARDFAULT_H / #endif`。`sys/hardfault.cpp` 不包含此头,全项目无任何 `#include "sys/hardfault.h"` 或 `#include "hardfault.h"`(Grep 确认 0 匹配)。 +- **改法**:删除整个文件 +- **风险**:低(无引用方) +- **注意**:需确认构建脚本(CMakeLists.txt)中是否显式列出该头文件作为安装目标;若仅有 .cpp 参与编译则无影响 + +#### 1.6 删除 TempSensor 冗余类型别名(方案 A) +- **文件**: + - `GD32E508_DCU_APP/device/temp_sensor.h:12`(`using Data = Adc::Sample;`) + - `GD32E508_DCU_APP/device/temp_sensor.h:24`(`using temp_sensor_data_t = TempSensor::Data;`) + - `GD32E508_DCU_APP/sys/task_runner.cpp:62`(消费点 `temp_sensor_data_t sample{};`) +- **现状**:两层 `using` 别名链(`temp_sensor_data_t` → `TempSensor::Data` → `Adc::Sample`),仅 1 处消费(task_runner.cpp:62)。`TempSensor` 类本身保留(`init()` 仍被 `initial.cpp:45` 调用,作为语义门面有价值)。 +- **改法**: + 1. 删除 `temp_sensor.h:12` 的 `using Data = Adc::Sample;` + 2. 删除 `temp_sensor.h:24` 的 `using temp_sensor_data_t = TempSensor::Data;` + 3. 修改 `task_runner.cpp:62` 将 `temp_sensor_data_t sample{};` 改为 `Adc::Sample sample{};` + 4. 确认 `task_runner.cpp` 已 `#include "device/adc.h"`(若未包含则需添加,但 task_runner.cpp 大概率已包含) +- **风险**:低(仅类型别名替换,不改语义) +- **注意**:`TempSensor` 类的 `init()` 声明与 doxygen 注释保留不动 + +--- + +### 第二组:过期/缺失注释修正(3 项) + +#### 2.1 修正 `task_manager.h` PRIMASK 过期注释 +- **文件**:`GD32E508_DCU_APP/sys/task_manager.h:45` +- **现状**:注释 `内部通过保持当前 PRIMASK 状态的临界区保证可由主循环与 ISR 共享调用`,但实现已迁移至 BASEPRI(`RT_CRITICAL_SAVE/RESTORE`) +- **改法**:改为 `内部通过 BASEPRI 临界区(RT_CRITICAL_SAVE/RESTORE)保证可由主循环与 ISR 共享调用` +- **风险**:低(纯注释) + +#### 2.2 修正 `task_runner.cpp` taskHeartbeat 过期注释 +- **文件**:`GD32E508_DCU_APP/sys/task_runner.cpp:225` +- **现状**:`taskHeartbeat` 的 doxygen 注释 `@note 当前仅保留任务入口。` 已过期失真——函数体实际已有实现(227 行:`OtaBootRequest::process(); CanFd::refreshFeedbackSnapshot(); canFeedbackStartRound();`)。相邻的 `taskStatusLed`(216 行)确实空体,其注释准确。 +- **改法**:将 225 行注释改为 `@note 当前任务:推进 OTA Boot 请求、刷新 CAN 反馈快照、启动反馈推送轮次。` +- **风险**:低(纯注释) +- **注意**:`taskStatusLed` 的 216 行注释保持不动(空体桩是 B 类前瞻性桩) + +#### 2.3 补充 `ota_boot_request.cpp` PRIMASK 解释注释 +- **文件**:`GD32E508_DCU_APP/sys/ota_boot_request.cpp:220` +- **现状**:`__disable_irq(); NVIC_SystemReset();` 是全项目唯一使用 PRIMASK 而非 BASEPRI 的位置。这是**合法用法**(系统复位前最终屏蔽全部中断,防止复位过程中断嵌套),但缺乏解释,后续维护者可能误判为违反项目硬约束。 +- **改法**:在 220 行前加注释:`/* 复位前最终关中断:必须用 PRIMASK 全屏蔽(非 BASEPRI),确保 NVIC_SystemReset 过程中无任何中断嵌套 */` +- **风险**:低(纯注释) + +--- + +### 第三组:重复代码提取(4 项) + +#### 3.1 提取 `spinDelay` 延时循环(CANFD.cpp) +- **文件**:`GD32E508_DCU_APP/device/CANFD.cpp` +- **现状**:9 处 `for (volatile uint32_t d = 0; d < NNNNN; ++d) {}` 纯延时循环散落在 `hardResetCan`、`runSelfTestFrame`、`init` 退出 init mode 兜底等多处。魔数 `{40000, 50000, 100000, 200000}` 无命名常量。 +- **改法**: + 1. 在文件顶部匿名 namespace 内新增: + ```cpp + /* 纯软件延时循环(不依赖 SysTick,可在任何中断状态下使用) */ + static inline void spinDelay(uint32_t loops) { + for (volatile uint32_t d = 0; d < loops; ++d) { } + } + ``` + 2. 把以下**纯延时循环**(无 break/条件)替换为 `spinDelay(N);`: + - 94 行:`for (volatile uint32_t dly = 0; dly < 40000U; ++dly) { }` → `spinDelay(40000U);` + - 120 行:`for (volatile uint32_t d = 0; d < 100000U; ++d) {}` → `spinDelay(100000U);` + - 125 行:`for (volatile uint32_t d = 0; d < 50000U; ++d) {}` → `spinDelay(50000U);` + - 130 行:`for (volatile uint32_t d = 0; d < 200000U; ++d) {}` → `spinDelay(200000U);` + - 142 行:需确认是否纯延时(读取后判断) + - 382 行:需确认是否纯延时 + - 384 行:需确认是否纯延时 + 3. **不替换**带有条件 break 的轮询等待循环(如 355 行 `for (...) { if ((CAN_STAT(...) & CAN_STAT_IWS) == 0U) break; }`)——这些不是纯延时,保持原样 +- **风险**:低(纯结构提取,延时循环行为不变) +- **注意**:执行时需逐一 Read 确认 134/142/382/384 行的实际内容,仅替换纯延时循环 + +#### 3.2 提取 `armRingPending` 赋值模式(app_spi_command.cpp) +- **文件**:`GD32E508_DCU_APP/device/spi/app_spi_command.cpp` +- **现状**:7 处重复的"按铃挂起"赋值模式: + ``` + m_ringPending = true; + m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; + ``` + 出现在 516-517, 556-557, 580-581, 604-605, 630-631, 699-700, 748-749 行。 +- **改法**: + 1. 在 `app_spi_command.h` 的 `AppSpiCommand` private 段新增声明:`void armRingPending();` + 2. 在 `app_spi_command.cpp` 新增实现: + ```cpp + void AppSpiCommand::armRingPending() { + m_ringPending = true; + m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; + } + ``` + 3. 把 7 处两行赋值替换为 `armRingPending();` +- **风险**:低(语义等价提取,两个独立赋值无依赖关系,顺序无关) +- **注意**:第 7 处(748-749)原顺序为先 `m_ringDeadline` 后 `m_ringPending`,提取后顺序统一为方法内顺序,不影响语义 + +#### 3.3 `writeU32Le` 跨文件去重 +- **文件**: + - `GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp:53-59`(`static void writeU32Le`) + - `GD32E508_DCU_APP/device/spi/app_spi_cmd_id_snapshot.cpp:33-39`(`inline void writeU32Le`) +- **现状**:两个 SPI 命令处理器的匿名 namespace 各自定义了字节序相同的 `writeU32Le(uint8_t *dst, uint32_t val)`,实现完全一致(仅参数名 value/val 不同)。 +- **改法**: + 1. 在 `GD32E508_DCU_APP/device/spi/app_spi_command_defs.h` 末尾(`SpiDefs` namespace 外或内,视现有结构而定)新增: + ```cpp + inline void writeU32Le(uint8_t *dst, uint32_t val) { + dst[0] = static_cast(val & 0xFFU); + dst[1] = static_cast((val >> 8) & 0xFFU); + dst[2] = static_cast((val >> 16) & 0xFFU); + dst[3] = static_cast((val >> 24) & 0xFFU); + } + ``` + 2. 删除 `app_spi_cmd_can_feedback.cpp:53-59` 的本地 `writeU32Le` 定义 + 3. 删除 `app_spi_cmd_id_snapshot.cpp:33-39` 的本地 `writeU32Le` 定义 + 4. 确认两个 .cpp 已 `#include "device/spi/app_spi_command_defs.h"`(大概率已包含,因它们使用 `SpiDefs::kType*` 常量) +- **风险**:低(纯函数提取,实现完全一致) + +#### 3.4 合并 `adc.cpp` 双重匿名 namespace +- **文件**:`GD32E508_DCU_APP/device/adc.cpp:12-27, 29-35` +- **现状**:同一文件有两个独立 `namespace { ... }` 块: + - 第一个(12-27):含 `kMinValidVdda` 等校验常量与 `isValidAdcRaw`/`isFiniteFloat` + - 第二个(29-35):含 `kDmaPeriph`/`kDmaChannel`/`s_dmaBuffer` 等 DMA 配置 + - 语义上都属于文件内静态,无必要分两块 +- **改法**:合并为单一 `namespace { ... }` 块,用空行 + 注释分隔逻辑分组: + ```cpp + namespace { + /* ---- 校验常量与辅助函数 ---- */ + constexpr float kMinValidVdda = 2.7f; + ... (原第一块内容) + + /* ---- DMA 配置 ---- */ + constexpr uint32_t kDmaPeriph = DMA0; + ... (原第二块内容) + } // namespace + ``` +- **风险**:低(纯结构调整) + +--- + +## Assumptions & Decisions + +### 假设 +1. 上一轮 `purge-legacy-dead-code` spec 的所有修改已合并且构建通过(已确认:Code=47462) +2. `task_runner.cpp` 已 `#include "device/adc.h"`(因 `Adc::collectSample` 在 319 行被调用) +3. 两个 SPI 命令处理器 .cpp 已 `#include "device/spi/app_spi_command_defs.h"`(因使用 `SpiDefs::kType*` 常量) +4. CMake 构建脚本未显式安装 `sys/hardfault.h`(头文件通常不被列为编译目标) + +### 决策 +1. **3.1 spinDelay 仅提取纯延时循环**:带条件 break 的轮询等待循环(如 355 行)保持原样,避免改变语义 +2. **1.6 TempSensor 保留类本身**:仅删冗余别名,`TempSensor::init()` 作为语义门面保留(`initial.cpp:45` 仍调用) +3. **不处理 3 项中风险项**:CANFD.cpp 文件名大小写、init() 重构、volatile 审计——需独立 spec +4. **不主动清理 #include**:即使某头文件因移除类型而变冗余,也不删除 #include(避免过度改动,构建不会因此报错) + +## Verification Steps + +### 1. Grep 验证(清除项零残留) +- `canfd_diag_t` → 0 匹配 +- `m_ringTxLen` → 0 匹配 +- `s_pushCount` → 0 匹配 +- `kTypeCanSend|kTypeAppPing|kRspAck|kRspWait`(在 app_spi_command.cpp 内)→ 0 匹配 +- `#include.*hardfault.h` → 0 匹配 +- `temp_sensor_data_t` → 0 匹配 +- `writeU32Le`(在 app_spi_cmd_can_feedback.cpp 和 app_spi_cmd_id_snapshot.cpp 内)→ 0 匹配(仅 app_spi_command_defs.h 中有定义) + +### 2. Grep 验证(新增项存在) +- `spinDelay` 在 CANFD.cpp 中出现 ≥1 次(定义)+ N 次(调用) +- `armRingPending` 在 app_spi_command.h 中出现 1 次(声明)+ app_spi_command.cpp 中出现 1 次(定义)+ 7 次(调用) +- `writeU32Le` 在 app_spi_command_defs.h 中出现 1 次(定义) +- `BASEPRI 临界区` 在 task_manager.h:45 出现 1 次 +- `推进 OTA Boot 请求` 在 task_runner.cpp 出现 1 次 +- `复位前最终关中断` 在 ota_boot_request.cpp 出现 1 次 + +### 3. 构建验证 +- `cmake --build g:\Robot_v2\DCU\DCU_APP\tmp\1 --clean-first` +- 0 编译错误 +- 0 新增警告(`#warning` 仅来自上一轮 C1 的 EXMC,不计入) +- Program Size: Code 应略小于 47462(因删除了死代码) + +### 4. 活跃代码完整性核查 +- `Adc::collectSample` 仍被 task_runner.cpp 引用 +- `TempSensor::init` 仍被 initial.cpp:45 引用 +- `m_txLen`/`m_ringPending`/`m_ringDeadline` 成员完整保留 +- `SpiDefs::kTypeCanSend` 等常量在 app_spi_command_defs.h 中完整保留(仅本地别名删除) +- `HardFault_Handler`/`HardFault_Handler_C` 在 hardfault.cpp 中完整保留 + +## 任务分组与执行顺序 + +### 可并行执行的三组(文件集不交叠) + +**组 A:device/spi/ 目录(1.2 + 1.3 + 1.4 + 3.2 + 3.3)** +- 文件:`app_spi_command.h/.cpp`、`app_spi_cmd_can_feedback.cpp`、`app_spi_cmd_id_snapshot.cpp`、`app_spi_command_defs.h` +- 注意:3.2(armRingPending)和 1.2(m_ringTxLen)都改 `app_spi_command.h/.cpp`,需顺序执行 + +**组 B:device/ 目录(1.1 + 1.6 + 3.1 + 3.4)** +- 文件:`canfd.h`、`CANFD.cpp`、`temp_sensor.h`、`task_runner.cpp`、`adc.cpp` +- 注意:1.6 改 `temp_sensor.h` + `task_runner.cpp`;3.1 改 `CANFD.cpp` + +**组 C:sys/ 目录(1.5 + 2.1 + 2.2 + 2.3)** +- 文件:`hardfault.h`(删除)、`task_manager.h`、`task_runner.cpp`、`ota_boot_request.cpp` +- 注意:2.2 和 1.6 都改 `task_runner.cpp`,需顺序执行或合并到同一子代理 + +### 建议执行顺序 +1. 先并行执行组 A、组 B、组 C(三组文件集基本不交叠,仅 `task_runner.cpp` 跨组) +2. `task_runner.cpp` 的改动(1.6 改类型 + 2.2 改注释)合并到同一子代理 +3. 最后执行构建验证 diff --git a/.trae/specs/add-ethercat-lan9252-realtime/checklist.md b/.trae/specs/add-ethercat-lan9252-realtime/checklist.md deleted file mode 100644 index f5ec79b..0000000 --- a/.trae/specs/add-ethercat-lan9252-realtime/checklist.md +++ /dev/null @@ -1,82 +0,0 @@ -# Verification Checklist - -## 使能开关(未焊接保护) -- [x] `sys/config.h` 中 `CONFIG_ETHERCAT_ENABLE` 默认为 0 -- [x] `CONFIG_ETHERCAT_ENABLE=0` 时 `comm/` 全部代码不参与编译(用 `#if` 包裹) -- [x] `CONFIG_ETHERCAT_ENABLE=0` 时 `sys/initial.cpp` 不调用 `EthercatSlave::init()` / `EthercatSync::init()` -- [x] `CONFIG_ETHERCAT_ENABLE=0` 时 `sys/task_runner.cpp` 的 `TASK_ETHERCAT_PROCESS` 分支空操作 -- [x] `CONFIG_ETHERCAT_ENABLE=0` 时 EXMC / EXTI0 / EXTI1 / EXTI2 不被配置 -- [x] `CONFIG_ETHERCAT_ENABLE=0` 时编译产物与改造前功能等价(ADC/CAN/SPI 正常) -- [x] 阶段 1 实时架构改造(NVIC/BASEPRI/ISR 瘦身)不受 `CONFIG_ETHERCAT_ENABLE` 影响,始终生效 -- [x] `TASK_SPI_CMD_PROCESS` 与 `taskSpiCmdProcess()` 不受 `CONFIG_ETHERCAT_ENABLE` 限制(阶段1 改造已需要) - -## 阶段 1:实时架构改造 -- [x] `sys/critical.h` 已创建,`RT_CRITICAL_ENTER/EXIT/SAVE/RESTORE` 宏基于 BASEPRI 实现 -- [x] `sys/config.h` 中 `CONFIG_NVIC_PRIORITY_GROUP` 设为 `NVIC_PRIGROUP_PRE4_SUB0` -- [x] `sys/config.h` 中各中断优先级宏已按分层表配置(EtherCAT=0/调度器=1/CAN=2/SPI+EXTI+USART=3) -- [x] `sys/config.h` 中 `CONFIG_MAIN_LOOP_WFI_ENABLE` 默认为 0 -- [x] `device/Timer.cpp` 中 `nvic_priority_group_set` 使用 `CONFIG_NVIC_PRIORITY_GROUP` -- [x] `device/Timer.cpp` 中调度器 NVIC 优先级为 (1, 0) -- [x] `sys/task_manager.cpp` 中 `set()` 和 `takePending()` 使用 BASEPRI 临界区 -- [x] `sys/task_manager.cpp` 中无残留 `__disable_irq/__set_PRIMASK` -- [x] `device/CANFD.cpp` 中 `hasRxFrame()` 和 `consumeRxFrame()` 使用 BASEPRI 临界区 -- [x] `device/CANFD.cpp` 中 CAN RX0 NVIC 优先级为 (2, n) -- [x] `device/CANFD.cpp` 中 `handleRxInterrupt()` 仅做入队+置位(验证未退化) -- [x] `device/app_spi_command.cpp` 中 SPI0 NVIC 优先级为 (3, 0) -- [x] `device/app_spi_command.cpp` 中 EXTI10_15 NVIC 优先级为 (3, 1) -- [x] `device/app_spi_command.cpp` 中 `poll()` 使用 BASEPRI 临界区 -- [x] `device/app_spi_command.cpp` 中 `onCsRelease()` 不在 ISR 内调用 `processCommand()`(移主循环) -- [x] `device/usart0.cpp` 中 USART0 NVIC 优先级为 (3, 0) -- [x] `device/usart0.cpp` 中 `putCharNonBlocking()` 使用 BASEPRI 临界区 -- [x] `main.h` 中 `system_task_t` 增加 `TASK_ETHERCAT_PROCESS` -- [x] `main.h` 中 `system_task_t` 增加 `TASK_SPI_CMD_PROCESS` -- [x] `device/app_spi_command.h` 中增加 `processPendingCommand()` 公有方法声明 -- [x] `device/app_spi_command.cpp` 中 `onCsRelease()` 不再调用 `processCommand()`,改置位 `m_cmdPending` + `TASK_SPI_CMD_PROCESS` -- [x] `device/app_spi_command.cpp` 中 `processPendingCommand()` 清除 `m_cmdPending` 后调用 `processCommand()` -- [x] `main.cpp` 中 WFI 代码块由 `CONFIG_MAIN_LOOP_WFI_ENABLE` 开关控制 -- [x] `main.cpp` 中 WFI 关闭时使用 `__NOP()` 忙等 -- [x] 全项目搜索确认无遗漏的 `__disable_irq` / `__set_PRIMASK`(除 `sys/critical.h` 实现内) - -## 阶段 2:LAN9252 ESC 驱动骨架 -- [x] `comm/esc_exmc_bus.h` 已创建,声明 `EscExmcBus` 类接口 -- [x] `comm/esc_exmc_bus.cpp` 已创建,EXMC NOR/SRAM 16-bit 初始化实现 -- [x] `comm/esc_exmc_bus.cpp` 中 GPIO 配置使用 `config.h` 引脚宏(基于标准映射 PD0/PD1/PD4/PD5/PD7/PD8-PD11/PD14/PD15,PE7-PE15) -- [x] `comm/esc_exmc_bus.cpp` 中 `read16/write16` 通过内存映射地址(0x60000000)访问 -- [x] `comm/esc_lan9252.h` 已创建,声明 `EscLan9252` 类接口 -- [x] `comm/esc_lan9252.cpp` 已创建,实现 `init()` / `selfTest()` / `readReg` / `writeReg` / `readPdi` / `writePdi` -- [x] `comm/esc_lan9252.cpp` 中 `selfTest()` 读取 ESC ID 寄存器并校验 -- [x] `comm/ethercat_slave.h` 已创建,声明 `EthercatSlave` 类与 PDO 双缓冲结构 -- [x] `comm/ethercat_slave.cpp` 已创建,实现周期处理与双缓冲 -- [x] `comm/ethercat_slave.cpp` 中 `periodicIsr()` 标记 `__attribute__((section(".ramfunc")))` -- [x] `comm/ethercat_slave.cpp` 中 `periodicIsr()` 执行:读输入→写输出→翻转指针→置位任务 -- [x] `comm/ethercat_slave.cpp` 中 `getLatestInputs()` 无锁原子读取 -- [x] `comm/ethercat_sync.h` / `comm/ethercat_sync.cpp` 已创建 -- [x] `comm/ethercat_sync.cpp` 中 SYNC0 EXTI(PB0/EXTI0)配置完成,下降沿触发 -- [x] `comm/ethercat_sync.cpp` 中 IRQ EXTI(PB1/EXTI1)配置完成,下降沿触发 -- [x] `comm/ethercat_sync.cpp` 中 SYNC1 EXTI(PB2/EXTI2)配置完成,下降沿触发 -- [x] `comm/ethercat_sync.cpp` 中 EXTI0_IRQn NVIC 优先级 (0, 0)(SYNC0 独占 preempt=0) -- [x] `comm/ethercat_sync.cpp` 中 EXTI1_IRQn NVIC 优先级 (1, 0)(IRQ preempt=1,可被 SYNC0 抢占) -- [x] `comm/ethercat_sync.cpp` 中 EXTI2_IRQn NVIC 优先级 (1, 0)(SYNC1 preempt=1,备用) -- [x] `comm/ethercat_sync.cpp` 中 `onSync0Exti()` 清 EXTI0 标志 → 调用 `EthercatSlave::periodicIsr()` -- [x] `comm/ethercat_sync.cpp` 中 `onEscIrqExti()` 清 EXTI1 标志 → 置位 `TASK_ETHERCAT_PROCESS`(极简) -- [x] `comm/ethercat_sync.cpp` 中 `onSync1Exti()` 清 EXTI2 标志(备用) -- [x] `comm/ethercat_sync.cpp` 中提供 `EXTI0_IRQHandler` / `EXTI1_IRQHandler` / `EXTI2_IRQHandler` C 包装 -- [x] GPIO PB0/PB1/PB2 时钟使能与输入模式配置完成 -- [x] PA0 未被占用,保留给未来 STANDBY 模式唤醒(WKUP_PIN0) -- [x] 降级模式 `CONFIG_ETHERCAT_SYNC_MODE==1` 时 TIMER0 配置可用 - -## 阶段 3:系统集成 -- [x] `sys/initial.cpp` 中 `AppInitializer::init()` 增加 `EthercatSlave::init()` 调用 -- [x] `sys/initial.cpp` 中增加 `EthercatSync::init()` 调用 -- [x] `sys/task_runner.cpp` 中增加 `taskEthercatProcess()` 方法 -- [x] `sys/task_runner.cpp` 中增加 `taskSpiCmdProcess()` 方法 -- [x] `sys/task_runner.cpp` 中 `runPending()` 增加 `TASK_ETHERCAT_PROCESS` 分支 -- [x] `sys/task_runner.cpp` 中 `runPending()` 增加 `TASK_SPI_CMD_PROCESS` 分支 -- [x] `sys/config.h` 中 NVIC 分组、优先级、EXMC 引脚、ESC 中断引脚、SYNC 模式等配置宏已定义 -- [x] 构建配置 `GD32E508_DCU_APP.cproject.yml` 包含 `comm/` 目录 - -## 编译验证 -- [x] 全量编译通过,无 error -- [x] 无新增 warning -- [x] 无遗漏的 `__disable_irq/__set_PRIMASK`(全项目搜索确认) -- [x] 新增文件均在 `comm/` 目录下,不污染现有 `device/` / `sys/` 结构 diff --git a/.trae/specs/add-ethercat-lan9252-realtime/spec.md b/.trae/specs/add-ethercat-lan9252-realtime/spec.md deleted file mode 100644 index c652d6d..0000000 --- a/.trae/specs/add-ethercat-lan9252-realtime/spec.md +++ /dev/null @@ -1,222 +0,0 @@ -# EtherCAT LAN9252 从站实时集成 Spec - -## Why -DCU_APP 后续要加入 EtherCAT 从站通信,要求周期 100us~250us、抖动 <1us(高性能运动控制级)。当前架构使用全局关中断(`__disable_irq`/PRIMASK)临界区、非抢占式协作调度、主循环 WFI 休眠,无法满足 <1us 抖动要求。需要先做实时架构改造,再新增 LAN9252 ESC 驱动骨架,并与现有系统集成。 - -## What Changes - -### Requirement: EtherCAT 使能开关(未焊接硬件保护) -系统 SHALL 提供编译时使能宏 `CONFIG_ETHERCAT_ENABLE`(默认 0=禁用),控制 EtherCAT 相关代码的编译与运行时初始化。未焊接 LAN9252 硬件时,该宏保持 0,系统 SHALL: -1. 编译期:`comm/` 全部代码不参与编译(`#if CONFIG_ETHERCAT_ENABLE` 包裹),不增加任何 Flash/RAM 占用 -2. 运行时:`sys/initial.cpp` 不调用 `EthercatSlave::init()` / `EthercatSync::init()`,EXMC/EXTI0/EXTI1/EXTI2 不被配置 -3. 主循环:`TASK_ETHERCAT_PROCESS` / `TASK_SPI_CMD_PROCESS` 分支不被执行 -4. 阶段 1 实时架构改造(NVIC 分组/BASEPRI 临界区/ISR 瘦身)不受影响,始终生效 - -#### Scenario: 未焊接 LAN9252 时系统正常运行 -- **WHEN** `CONFIG_ETHERCAT_ENABLE` 为 0(默认) -- **THEN** 编译产物与改造前功能等价,EXMC/EXTI0-2 未配置,主循环不调用 EtherCAT 任务,现有 ADC/CAN/SPI 功能正常 - -#### Scenario: 焊接 LAN9252 后启用 -- **WHEN** 用户焊接硬件后将 `CONFIG_ETHERCAT_ENABLE` 改为 1 并重新编译 -- **THEN** `comm/` 代码参与编译,初始化序列调用 EtherCAT,EXTI0-2 配置生效,周期通信启动 - -### 阶段 1:实时架构改造(前置必做,不受使能开关影响) -- **BREAKING**: NVIC 优先级分组从 `NVIC_PRIGROUP_PRE2_SUB2` 改为 `NVIC_PRIGROUP_PRE4_SUB0`,抢占优先级从 2 位变为 4 位 -- **BREAKING**: 全部临界区从 `__disable_irq/__set_PRIMASK`(PRIMASK,屏蔽所有中断)改为 BASEPRI(仅屏蔽 preempt≥1,保留 preempt=0 给 EtherCAT) -- 新增 `sys/critical.h`:统一 BASEPRI 临界区宏 `RT_CRITICAL_ENTER()/RT_CRITICAL_EXIT()` -- ISR 瘦身:SPI CS release 中断与 CAN RX 中断只做"数据入队 + 置位标志",命令解析与处理移到主循环 -- 主循环 WFI 去除:EtherCAT 启用后改忙等,通过 `CONFIG_MAIN_LOOP_WFI_ENABLE` 开关控制 -- 各中断优先级重排,为 EtherCAT 预留 preempt=0 - -### 阶段 2:LAN9252 ESC 驱动骨架(新建 `comm/` 目录) -- 新增 `comm/esc_lan9252.h` / `comm/esc_lan9252.cpp`:LAN9252 寄存器定义、HBI 读写接口、自检(0x87654321) -- 新增 `comm/esc_exmc_bus.h` / `comm/esc_exmc_bus.cpp`:EXMC NOR/SRAM 16-bit 模式初始化、GPIO 配置、读写时序 -- 新增 `comm/ethercat_slave.h` / `comm/ethercat_slave.cpp`:从站周期处理接口、PDO 双缓冲(乒乓)、周期 ISR(放 RAM 执行段) -- 新增 `comm/ethercat_sync.h` / `comm/ethercat_sync.cpp`:SYNC0 周期同步定时器(TIMER0,preempt=0) - -### 阶段 3:系统集成(受 `CONFIG_ETHERCAT_ENABLE` 开关控制) -- `main.h` 任务枚举增加 `TASK_ETHERCAT_PROCESS` / `TASK_SPI_CMD_PROCESS`(始终定义,不受开关影响) -- `sys/initial.cpp` 初始化序列:`#if CONFIG_ETHERCAT_ENABLE` 包裹 `EthercatSlave::init()` / `EthercatSync::init()` 调用 -- `sys/task_runner.cpp` 增加 `taskEthercatProcess()` / `taskSpiCmdProcess()`,`runPending()` 分支受 `CONFIG_ETHERCAT_ENABLE` 保护(无 EtherCAT 时分支空操作) -- `sys/config.h` 增加 LAN9252 / EXMC / SYNC 相关配置项(含引脚定义占位宏),`CONFIG_ETHERCAT_ENABLE` 默认 0 -- 构建配置纳入新文件(`comm/` 目录源文件纳入编译列表,实际编译由 `#if CONFIG_ETHERCAT_ENABLE` 控制) - -## Impact -- Affected specs: 无(首个 spec) -- Affected code: - - `main.cpp` / `main.h`:主循环 WFI、任务枚举 - - `sys/config.h`:NVIC 分组、优先级、LAN9252 配置 - - `sys/critical.h`(新建):BASEPRI 临界区 - - `sys/task_manager.cpp`:临界区换 BASEPRI - - `sys/task_runner.cpp`:增加 EtherCAT 任务 - - `sys/initial.cpp`:增加 EtherCAT 初始化 - - `device/Timer.cpp`:NVIC 分组设置 - - `device/CANFD.cpp`:优先级、临界区、ISR 已基本瘦身 - - `device/app_spi_command.cpp`:优先级、临界区、ISR 瘦身(CS release 处理移主循环) - - `device/usart0.cpp`:优先级、临界区 - - `comm/`(新建全部):ESC 驱动、EXMC 总线、EtherCAT 从站、SYNC 同步 - -## ADDED Requirements - -### Requirement: NVIC 优先级分组为 4 位抢占 0 位子优先级 -系统 SHALL 使用 `NVIC_PRIGROUP_PRE4_SUB0` 分组,提供 4 个抢占优先级层级,为 EtherCAT 预留最高级(preempt=0)。 - -#### Scenario: EtherCAT 中断不被阻塞 -- **WHEN** 任意非 EtherCAT 代码进入临界区 -- **THEN** EtherCAT 中断(preempt=0)仍能立即响应,因为临界区使用 BASEPRI 仅屏蔽 preempt≥1 - -### Requirement: BASEPRI 临界区替代 PRIMASK -系统 SHALL 使用 BASEPRI 实现临界区,阈值为 preempt≥1,确保 EtherCAT 中断(preempt=0)永不被阻塞。所有原有 `__disable_irq/__set_PRIMASK` 临界区 SHALL 替换为 BASEPRI 版本。 - -#### Scenario: 任务标志原子操作不阻塞 EtherCAT -- **WHEN** `TaskManager::takePending()` 或 `TaskManager::set()` 执行期间 -- **THEN** EtherCAT 周期中断仍能抢占执行,BASEPRI 仅屏蔽 preempt≥1 的中断 - -### Requirement: 中断优先级分层 -系统 SHALL 按以下优先级表配置所有中断: - -| 中断 | preempt | sub | 说明 | -|---|---|---|---| -| EtherCAT ESC 中断 / SYNC0 定时器 | 0 | 0 | 最高优先级,永不被阻塞 | -| TIMER2 100us 调度器 | 1 | 0 | 系统调度节拍 | -| CAN0/1/2 RX0 | 2 | 0/1/2 | CANFD 接收 | -| SPI0 / EXTI10_15 / USART0 | 3 | 0/1 | 非实时通信 | - -#### Scenario: EtherCAT 中断抢占 CAN 中断 -- **WHEN** CAN RX 中断正在处理时 EtherCAT 周期中断到达 -- **THEN** EtherCAT 中断立即抢占 CAN 中断执行 - -### Requirement: ISR 瘦身 -SPI CS release 中断和 CAN RX 中断 SHALL 只做"数据入队 + 置位任务标志",命令解析、协议处理、硬件发送等耗时操作 SHALL 移到主循环任务中执行。 - -#### Scenario: SPI CS release 不在 ISR 内做命令处理 -- **WHEN** SPI CS 释放中断触发 -- **THEN** ISR 仅将接收数据入队并置位 `TASK_SPI_LOG`(或新增的解析任务),`processCommand()` 在主循环中执行 - -### Requirement: LAN9252 ESC HBI 驱动 -系统 SHALL 通过 GD32E508 的 EXMC NOR/SRAM 模式提供 LAN9252 的 16-bit 并行 HBI 访问能力,包括:寄存器读、寄存器写、PDI 内存块读写、自检(读 0x87654321 校验)。 - -#### Scenario: ESC 自检通过 -- **WHEN** 系统初始化调用 `EscLan9252::selfTest()` -- **THEN** 通过 EXMC 读取 LAN9252 的 ESC ID 寄存器,返回值校验通过(0x87654321 或对应芯片 ID) - -#### Scenario: 寄存器读写正确 -- **WHEN** 调用 `EscLan9252::writeReg(addr, value)` 后调用 `EscLan9252::readReg(addr)` -- **THEN** 读回值与写入值一致 - -### Requirement: EtherCAT 周期数据双缓冲 -系统 SHALL 使用 ping-pong 双缓冲机制实现 EtherCAT 周期数据交换:周期 ISR 使用一个缓冲区,主循环应用层使用另一个缓冲区,通过原子指针交换实现无锁访问。 - -#### Scenario: 主循环无锁读取最新 PDO -- **WHEN** 周期 ISR 完成一帧输入数据读取并翻转缓冲区指针 -- **THEN** 主循环 `taskEthercatProcess()` 通过原子指针读取到最新一帧完整数据,无需关中断 - -### Requirement: 周期 ISR 放 RAM 执行段 -EtherCAT 周期 ISR SHALL 标记为 RAM 执行段(通过链接脚本 section 属性),避免 Flash wait state 造成抖动,保证 <1us 抖动目标。 - -#### Scenario: 周期 ISR 执行时间稳定 -- **WHEN** 周期 ISR 连续执行多次 -- **THEN** 执行时间波动 <1us,不受 Flash wait state 影响 - -### Requirement: SYNC0 周期同步由 ESC 硬件触发 -系统 SHALL 使用 LAN9252 的 SYNC0 输出引脚(PB0/EXTI0)经 MCU EXTI0_IRQn 独立向量触发周期中断(而非 MCU 内部定时器自计时),由 ESC 硬件同步保证抖动最小。EXTI0_IRQn 配置为 preempt=0,永不被任何其他中断(包括 ESC IRQ)阻塞。TIMER0 作为 DC 未同步时的降级备选(通过 `CONFIG_ETHERCAT_SYNC_MODE` 开关切换)。 - -#### Scenario: SYNC0 引脚触发周期中断 -- **WHEN** LAN9252 在 DC 同步点输出 SYNC0 脉冲 -- **THEN** EXTI0 中断立即触发(独立向量),handler 清标志后调用 `EthercatSlave::periodicIsr()`,抖动 <1us - -#### Scenario: SYNC0 抢占 ESC IRQ -- **WHEN** ESC IRQ 中断(EXTI1,preempt=1)正在处理时 SYNC0 周期中断到达 -- **THEN** SYNC0(EXTI0,preempt=0)立即抢占 IRQ 中断执行 - -#### Scenario: 降级模式用 TIMER0 -- **WHEN** `CONFIG_ETHERCAT_SYNC_MODE` 设为 TIMER 降级模式 -- **THEN** 使用 TIMER0 以固定周期触发,作为 DC 未同步前的过渡 - -### Requirement: ESC 事件中断(独立向量) -系统 SHALL 配置 LAN9252 的 IRQ 输出引脚(PB1/EXTI1)经 MCU EXTI1_IRQn 独立向量触发 ESC 事件中断(用于 AL 状态变化、EEPROM 加载完成、看门狗触发等事件),preempt=1。由于拥有独立向量,IRQ 与 SYNC0 完全分离,可被 SYNC0 抢占。IRQ handler SHALL 极简:仅清 EXTI 标志 + 置位 `TASK_ETHERCAT_PROCESS`。 - -#### Scenario: ESC 事件触发中断 -- **WHEN** LAN9252 产生 ESC 事件(如 AL 状态变化)拉低 IRQ 引脚 -- **THEN** EXTI1 中断(独立向量)触发,handler 置位 `TASK_ETHERCAT_PROCESS`,主循环处理事件 - -#### Scenario: SYNC1 备用中断 -- **WHEN** LAN9252 输出 SYNC1 脉冲(PB2/EXTI2) -- **THEN** EXTI2 中断(独立向量,preempt=1)触发,清标志(备用,暂不处理) - -### Requirement: EtherCAT 应用层任务 -系统 SHALL 在主循环中提供非实时应用层 PDO 映射任务 `TASK_ETHERCAT_PROCESS`,从双缓冲读取最新 PDO 数据供应用层使用。 - -#### Scenario: 应用层消费 PDO 数据 -- **WHEN** 周期 ISR 置位 `TASK_ETHERCAT_PROCESS` 标志 -- **THEN** 主循环 `taskEthercatProcess()` 从双缓冲读取最新 PDO 数据并交给应用层处理 - -## MODIFIED Requirements - -### Requirement: 主循环空闲策略 -原: 空闲时关中断后进 WFI 休眠。 -改: 通过 `CONFIG_MAIN_LOOP_WFI_ENABLE` 开关控制;EtherCAT 启用时默认关闭 WFI 改忙等,避免唤醒延迟抖动。 - -### Requirement: 临界区实现 -原: 使用 `__disable_irq()`/`__set_PRIMASK()`(PRIMASK)全局关中断。 -改: 使用 `__set_BASEPRI()`/`__get_BASEPRI()`(BASEPRI)仅屏蔽 preempt≥1,保留 preempt=0 给 EtherCAT。统一通过 `sys/critical.h` 的 `RT_CRITICAL_ENTER()/RT_CRITICAL_EXIT()` 宏访问。 - -## Assumptions & Decisions - -1. **ESC 型号**: LAN9252(内建 PHY),16-bit 并行 HBI 模式 -2. **MCU 接口**: GD32E508 EXMC NOR/SRAM 模式,16-bit 数据宽度 -3. **通信周期**: 100us~250us,抖动目标 <1us -4. **SYNC0 同步**: 优先由 LAN9252 SYNC0 输出引脚(PB0/EXTI0)经 MCU EXTI0 独立向量触发(ESC 硬件同步,抖动最小);TIMER0 作为降级备选(`CONFIG_ETHERCAT_SYNC_MODE=1`) -4a. **ESC 中断引脚(已确定)**: SYNC0=PB0(EXTI0,preempt=0)、IRQ=PB1(EXTI1,preempt=1)、SYNC1=PB2(EXTI2,preempt=1),三个独立 NVIC 向量,完全分离优先级;**保留 PA0 给未来 STANDBY 模式唤醒(WKUP_PIN0)** -4b. **深度睡眠兼容**: 引脚选择避开 PA0(WKUP),未来支持 STANDBY 最低功耗唤醒;Deep sleep 模式可用任意 EXTI(SYNC0/IRQ/CAN)唤醒 -5. **不包含**: 完整 EtherCAT 协议栈(SSC),后续可挂接到 `ethercat_slave.cpp` 接口 -6. **不包含**: EEPROM/ESI 配置内容、从站应用对象字典 -7. **引脚定义**: 基于 GD32E508 EXMC 标准映射 + 当前空闲引脚规划(见下表),配置项以宏定义在 `config.h`,用户可在原理图确认后调整 -8. **EXMC Region**: 使用 BANK0 Region1(NE1),基址 0x60000000 -9. **FPU 上下文**: 周期 ISR 不使用 FPU,避免惰性保存抖动;若协议栈后续需要 FPU,需评估 -10. **RAM 执行段**: 需要链接脚本支持(新增 `.ramfunc` section),但本次不改链接脚本,先用 `__attribute__((section))` 标记,实际 RAM 放置由后续配置 -11. **使能开关(未焊接保护)**: `CONFIG_ETHERCAT_ENABLE` 默认 0(禁用)。阶段 1 实时架构改造始终生效(不依赖硬件);阶段 2/3 的 EtherCAT 代码全部由 `#if CONFIG_ETHERCAT_ENABLE` 包裹,未焊接时 0→不编译不初始化,系统功能与改造前等价。用户焊接硬件后改为 1 重新编译即可启用 - -## 引脚定义(GD32E508 EXMC 标准映射 + ESC 中断) - -### EXMC 16-bit 数据总线(全部 AF 复用,无当前占用冲突) -| 信号 | MCU 引脚 | EXMC 功能 | 当前占用 | -|---|---|---|---| -| D0 | PD14 | EXMC_D0 | 空闲 | -| D1 | PD15 | EXMC_D1 | 空闲 | -| D2 | PD0 | EXMC_D2 | 空闲 | -| D3 | PD1 | EXMC_D3 | 空闲 | -| D4 | PE7 | EXMC_D4 | 空闲 | -| D5 | PE8 | EXMC_D5 | 空闲 | -| D6 | PE9 | EXMC_D6 | 空闲 | -| D7 | PE10 | EXMC_D7 | 空闲 | -| D8 | PE11 | EXMC_D8 | 空闲 | -| D9 | PE12 | EXMC_D9 | 空闲 | -| D10 | PE13 | EXMC_D10 | 空闲 | -| D11 | PE14 | EXMC_D11 | 空闲 | -| D12 | PE15 | EXMC_D12 | 空闲 | -| D13 | PD8 | EXMC_D13 | 空闲 | -| D14 | PD9 | EXMC_D14 | 空闲 | -| D15 | PD10 | EXMC_D15 | 空闲 | - -### EXMC 控制线 -| 信号 | MCU 引脚 | EXMC 功能 | 用途 | -|---|---|---|---| -| A16 | PD11 | EXMC_A16 | LAN9252 寄存器/数据选择(地址线) | -| NOE | PD4 | EXMC_NOE | 读使能 | -| NWE | PD5 | EXMC_NWE | 写使能 | -| NE1 | PD7 | EXMC_NE1 | 片选(LAN9252) | - -### ESC 中断引脚(已确定:PB0/PB1/PB2,三个独立向量,保留 PA0 给 STANDBY 唤醒) -| 信号 | MCU 引脚 | EXTI | 中断向量 | preempt | 用途 | -|---|---|---|---|---|---| -| LAN9252 SYNC0 | PB0 | EXTI0 | **EXTI0_IRQn(独占)** | 0 | 周期同步触发(DC sync),永不被阻塞 | -| LAN9252 SYNC1 | PB2 | EXTI2 | **EXTI2_IRQn(独占)** | 1 | 次周期同步(备用) | -| LAN9252 IRQ | PB1 | EXTI1 | **EXTI1_IRQn(独占)** | 1 | ESC 事件(AL 状态等) | - -> **引脚选择理由(考虑深度睡眠需求)**: -> - PB0/PB1/PB2 分别对应 EXTI0/1/2,在 GD32E508 上各自拥有**独立的 NVIC 向量**(EXTI0_IRQn/EXTI1_IRQn/EXTI2_IRQn),彻底避免 EXTI5-9 共享 EXTI9_5_IRQn 的风险 -> - SYNC0 独占 preempt=0,IRQ/SYNC1 同级 preempt=1,SYNC0 可抢占 IRQ/SYNC1,满足 <1us 抖动目标 -> - **关键:保留 PA0 给未来 STANDBY 模式唤醒**。GD32E508 STANDBY 模式只有 PA0(WKUP_PIN0)能唤醒,若用于 SYNC0 会永久占用该唯一唤醒引脚。改用 PB0/PB1/PB2 后,未来 STANDBY 低功耗路径保留 -> - PB0/PB1/PB2 在当前代码中均未使用(已确认无占用冲突,仅 PB3/4/5=SPI0, PB9/12/13/14=CAN) -> - 唤醒策略灵活:Deep sleep 模式可用 SYNC0/IRQ/CAN 等任意 EXTI 唤醒(恢复执行);Standby 模式用 PA0 唤醒(复位重启,适合完全断电场景) diff --git a/.trae/specs/add-ethercat-lan9252-realtime/tasks.md b/.trae/specs/add-ethercat-lan9252-realtime/tasks.md deleted file mode 100644 index 5e86b63..0000000 --- a/.trae/specs/add-ethercat-lan9252-realtime/tasks.md +++ /dev/null @@ -1,131 +0,0 @@ -# Tasks - -- [x] Task 1: 新增 `sys/critical.h` BASEPRI 临界区基础设施 - - [x] SubTask 1.1: 定义 `RT_CRITICAL_ENTER()` / `RT_CRITICAL_EXIT()` 宏,基于 `__set_BASEPRI()` / `__get_BASEPRI()`,阈值为 preempt≥1 - - [x] SubTask 1.2: 定义 `RT_CRITICAL_SAVE()` 返回保存值,`RT_CRITICAL_RESTORE(val)` 恢复,用于嵌套场景 - - [x] SubTask 1.3: 确认 GD32E508 (Cortex-M4F/M33) 支持 BASEPRI 寄存器 - -- [x] Task 2: 修改 `sys/config.h` 增加 NVIC 分组、优先级与引脚配置 - - [x] SubTask 2.0: 增加 `CONFIG_ETHERCAT_ENABLE` 宏(默认 0,禁用),作为所有 EtherCAT 相关代码的编译使能开关 - - [x] SubTask 2.1: 增加 `CONFIG_NVIC_PRIORITY_GROUP` 宏设为 `NVIC_PRIGROUP_PRE4_SUB0` - - [x] SubTask 2.2: 增加各中断抢占优先级宏:`CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC=0` / `CONFIG_IRQ_PREEMPT_SCHED=1` / `CONFIG_IRQ_PREEMPT_CAN=2` / `CONFIG_IRQ_PREEMPT_ETHERCAT_ESC=1` / `CONFIG_IRQ_PREEMPT_SPI_USART=3` - - [x] SubTask 2.3: 增加 `CONFIG_MAIN_LOOP_WFI_ENABLE` 开关宏(默认 0,关闭 WFI) - - [x] SubTask 2.4: 增加 `CONFIG_ETHERCAT_SYNC_MODE` 宏(EXTI=0 默认 / TIMER=1 降级) - - [x] SubTask 2.5: 增加 EXMC 引脚配置宏:D0-D15 / A16 / NOE / NWE / NE1 引脚(基于标准映射 PD0/PD1/PD4/PD5/PD7/PD8-PD11/PD14/PD15,PE7-PE15) - - [x] SubTask 2.6: 增加 ESC 中断引脚宏:`CONFIG_ESC_SYNC0_PORT/PIN/EXTI/IRQn`(GPIOB/PB0/EXTI0/EXTI0_IRQn)、`CONFIG_ESC_IRQ_PORT/PIN/EXTI/IRQn`(GPIOB/PB1/EXTI1/EXTI1_IRQn)、`CONFIG_ESC_SYNC1_PORT/PIN/EXTI/IRQn`(GPIOB/PB2/EXTI2/EXTI2_IRQn),三个独立向量(保留 PA0 给 STANDBY 唤醒) - - [x] SubTask 2.7: 增加 EXMC Region 基址宏 `CONFIG_ESC_EXMC_BASE_ADDR`(0x60000000)和 `CONFIG_ESC_EXMC_BANK_NOR_SRAM_REGION1` - -- [x] Task 3: 修改 `device/Timer.cpp` NVIC 分组与调度器优先级 - - [x] SubTask 3.1: `configureNvic()` 中 `nvic_priority_group_set` 改为 `CONFIG_NVIC_PRIORITY_GROUP` - - [x] SubTask 3.2: `CONFIG_SCHED_TIMER_IRQ_PREEMPT_PRIO` 改为 1,`CONFIG_SCHED_TIMER_IRQ_SUB_PRIO` 保持 0 - -- [x] Task 4: 修改 `sys/task_manager.cpp` 临界区换 BASEPRI - - [x] SubTask 4.1: 移除本地 `enterCriticalSection()` / `exitCriticalSection()`(基于 PRIMASK) - - [x] SubTask 4.2: 改用 `sys/critical.h` 的 `RT_CRITICAL_SAVE()/RESTORE()` - - [x] SubTask 4.3: `set()` 和 `takePending()` 使用新临界区宏 - -- [x] Task 5: 修改 `device/CANFD.cpp` 优先级与临界区 - - [x] SubTask 5.1: 移除本地 `enterCriticalSection()` / `exitCriticalSection()`,改用 `sys/critical.h` - - [x] 5.2: `init()` 中 `nvic_irq_enable` CAN RX0 优先级从 (1, n) 改为 (2, n) - - [x] 5.3: 确认 `handleRxInterrupt()` 已只做入队+置位(当前已满足,验证即可) - -- [x] Task 6: 修改 `device/app_spi_command.cpp` 优先级、临界区与 ISR 瘦身 - - [x] SubTask 6.1: `configureSpi0()` 中 SPI0 NVIC 优先级从 (2, 0) 改为 `(CONFIG_IRQ_PREEMPT_SPI_USART, 0)` - - [x] SubTask 6.2: `configureExti()` 中 EXTI10_15 NVIC 优先级从 (1, 1) 改为 `(CONFIG_IRQ_PREEMPT_SPI_USART, 1)` - - [x] SubTask 6.3: `poll()` 中 `__disable_irq/__enable_irq` 改用 `RT_CRITICAL_SAVE/RESTORE` - - [x] SubTask 6.4: ISR 瘦身:`onCsRelease()` 中 `processCommand()` 移出 ISR,改为置位 `TASK_SPI_CMD_PROCESS` 标志由主循环执行 - - [x] SubTask 6.5: `AppSpiCommand` 类增加 `processPendingCommand()` 公有方法供主循环调用,内部调用 `processCommand()` - - [x] SubTask 6.6: 增加 `volatile bool m_cmdPending` 标志,`onCsRelease()` 置位,`processPendingCommand()` 清除 - -- [x] Task 7: 修改 `device/usart0.cpp` 优先级与临界区 - - [x] SubTask 7.1: `init()` 中 USART0 NVIC 优先级从 (2, 0) 改为 (3, 0) - - [x] SubTask 7.2: `putCharNonBlocking()` 中 `__disable_irq/__enable_irq` 改用 `RT_CRITICAL_SAVE/RESTORE` - -- [x] Task 8: 修改 `main.h` 增加任务枚举 - - [x] SubTask 8.1: `system_task_t` 增加 `TASK_ETHERCAT_PROCESS` - - [x] SubTask 8.2: `system_task_t` 增加 `TASK_SPI_CMD_PROCESS`(用于 SPI CS release 后的命令解析,移出 ISR) - -- [x] Task 9: 修改 `main.cpp` 主循环 WFI 策略 - - [x] SubTask 9.1: 用 `#if (CONFIG_MAIN_LOOP_WFI_ENABLE != 0)` 包裹 WFI 代码块 - - [x] SubTask 9.2: WFI 关闭时改为 `__NOP()` 空转忙等 - - [x] SubTask 9.3: WFI 块内的临界区改用 `RT_CRITICAL_SAVE/RESTORE` - -- [x] Task 10: 新建 `comm/esc_exmc_bus.h` EXMC 总线抽象接口 - - [x] SubTask 10.1: 声明 `EscExmcBus` 类:`init()` / `read16(addr)` / `write16(addr, data)` / `readBlock(addr, buf, len)` / `writeBlock(addr, buf, len)` - - [x] SubTask 10.2: 定义 EXMC 寄存器基址宏、Region 选择宏(待引脚确认) - -- [x] Task 11: 新建 `comm/esc_exmc_bus.cpp` EXMC 总线实现 - - [x] SubTask 11.0: 文件整体用 `#if CONFIG_ETHERCAT_ENABLE` 包裹实现(未使能时编译为空) - - [x] SubTask 11.1: `init()`:配置 EXMC NOR/SRAM Region(GPIO 时钟、EXMC 时钟、时序参数) - - [x] SubTask 11.2: GPIO 配置:D0-D15 / A16 / NOE / NWE / NE1(引脚基于标准映射) - - [x] SubTask 11.3: EXMC 时序参数:按 LAN9252 HBI 时序要求配置(read/write wait state) - - [x] SubTask 11.4: `read16/write16`:通过内存映射地址(0x60000000)直接读写 - - [x] SubTask 11.5: `readBlock/writeBlock`:循环调用 read16/write16 - -- [x] Task 12: 新建 `comm/esc_lan9252.h` LAN9252 ESC 驱动接口 - - [x] SubTask 12.1: 定义 LAN9252 关键寄存器地址宏(ESC ID、PDI 控制、中断使能等) - - [x] SubTask 12.2: 声明 `EscLan9252` 类:`init()` / `selfTest()` / `readReg(addr)` / `writeReg(addr, val)` / `readPdi(offset, buf, len)` / `writePdi(offset, buf, len)` - -- [x] Task 13: 新建 `comm/esc_lan9252.cpp` LAN9252 ESC 驱动实现 - - [x] SubTask 13.0: 文件整体用 `#if CONFIG_ETHERCAT_ENABLE` 包裹实现(未使能时编译为空) - - [x] SubTask 13.1: `init()`:调用 `EscExmcBus::init()`,初始化 ESC 基本寄存器 - - [x] SubTask 13.2: `selfTest()`:读 ESC ID 寄存器校验(0x87654321 或芯片 ID) - - [x] SubTask 13.3: `readReg/writeReg`:封装 EXMC 读写 + 地址映射 - - [x] SubTask 13.4: `readPdi/writePdi`:PDI 内存块读写 - -- [x] Task 14: 新建 `comm/ethercat_slave.h` 从站周期接口与双缓冲 - - [x] SubTask 14.1: 定义 PDO 输入/输出缓冲结构体(大小可配置) - - [x] SubTask 14.2: 声明 `EthercatSlave` 类:`init()` / `periodicIsr()` / `getLatestInputs()` / `setOutputs()` / `process()` - - [x] SubTask 14.3: 声明 ping-pong 双缓冲成员:两个输入缓冲 + 原子指针索引 - -- [x] Task 15: 新建 `comm/ethercat_slave.cpp` 从站周期实现 - - [x] SubTask 15.0: 文件整体用 `#if CONFIG_ETHERCAT_ENABLE` 包裹实现(未使能时编译为空) - - [x] SubTask 15.1: `init()`:调用 `EscLan9252::init()` + `selfTest()` + 初始化双缓冲 - - [x] SubTask 15.2: `periodicIsr()`:标 `__attribute__((section(".ramfunc")))`,读输入 PDO → 写输出 PDO → 翻转缓冲指针 → 置位 `TASK_ETHERCAT_PROCESS` - - [x] SubTask 15.3: `getLatestInputs()`:原子读取缓冲指针返回最新输入数据指针(无锁) - - [x] SubTask 15.4: `setOutputs()`:写入输出缓冲供下次 ISR 发送 - - [x] SubTask 15.5: `process()`:应用层 PDO 映射入口(非实时,留接口) - -- [x] Task 16: 新建 `comm/ethercat_sync.h` / `comm/ethercat_sync.cpp` SYNC0 同步(EXTI 独立向量 / TIMER0 降级) - - [x] SubTask 16.0: `.cpp` 文件整体用 `#if CONFIG_ETHERCAT_ENABLE` 包裹实现(未使能时编译为空,EXTI0/1/2 不配置) - - [x] SubTask 16.1: 声明 `EthercatSync` 类:`init()` / `onSync0Exti()` / `onEscIrqExti()` / `onSync1Exti()` / `onTimerIrq()` - - [x] SubTask 16.2: `init()`:配置 SYNC0 EXTI(PB0/EXTI0,下降沿,NVIC EXTI0_IRQn preempt=0,sub=0) - - [x] SubTask 16.3: `init()`:配置 IRQ EXTI(PB1/EXTI1,下降沿,NVIC EXTI1_IRQn preempt=1,sub=0) - - [x] SubTask 16.4: `init()`:配置 SYNC1 EXTI(PB2/EXTI2,下降沿,NVIC EXTI2_IRQn preempt=1,sub=0) - - [x] SubTask 16.5: `init()`:GPIO PB0/PB1/PB2 时钟使能与输入模式配置(GPIOB) - - [x] SubTask 16.6: 降级模式(`CONFIG_ETHERCAT_SYNC_MODE==1`)时配置 TIMER0 周期 + NVIC preempt=0 - - [x] SubTask 16.7: `onSync0Exti()`:清 EXTI0 标志 → 调用 `EthercatSlave::periodicIsr()` - - [x] SubTask 16.8: `onEscIrqExti()`:清 EXTI1 标志 → 置位 `TASK_ETHERCAT_PROCESS` - - [x] SubTask 16.9: `onSync1Exti()`:清 EXTI2 标志(备用,暂不处理) - - [x] SubTask 16.10: 提供 `EXTI0_IRQHandler` / `EXTI1_IRQHandler` / `EXTI2_IRQHandler` C 包装(及降级模式 `TIMER0_IRQHandler`) - -- [x] Task 17: 修改 `sys/initial.cpp` 集成 EtherCAT 初始化(受使能开关保护) - - [x] SubTask 17.1: `AppInitializer::init()` 中在 `SchedulerTimer::init()` 之后,用 `#if CONFIG_ETHERCAT_ENABLE` 包裹 `#include "comm/ethercat_slave.h"` 与 `comm/ethercat_sync.h"` - - [x] SubTask 17.2: 在 `#if CONFIG_ETHERCAT_ENABLE` 块内调用 `EthercatSlave::init()` - - [x] SubTask 17.3: 在 `EthercatSlave::init()` 之后(同块内)调用 `EthercatSync::init()` - -- [x] Task 18: 修改 `sys/task_runner.cpp` 增加 EtherCAT 与 SPI 命令处理任务 - - [x] SubTask 18.1: 增加 `taskEthercatProcess()` 私有方法:用 `#if CONFIG_ETHERCAT_ENABLE` 包裹,内部调用 `EthercatSlave::getLatestInputs()` + `EthercatSlave::process()` - - [x] SubTask 18.2: 增加 `taskSpiCmdProcess()` 私有方法:调用 `AppSpiCommandBus.processPendingCommand()`(此功能独立于 EtherCAT,不受 `CONFIG_ETHERCAT_ENABLE` 限制,阶段1 ISR 瘦身已需要) - - [x] SubTask 18.3: `runPending()` 中增加 `TASK_ETHERCAT_PROCESS` 分支调用(受 `#if CONFIG_ETHERCAT_ENABLE` 保护) - - [x] SubTask 18.4: `runPending()` 中增加 `TASK_SPI_CMD_PROCESS` 分支调用(无条件,阶段1 改造已需要) - -- [x] Task 19: 构建配置纳入新文件 - - [x] SubTask 19.1: 确认 `GD32E508_DCU_APP.cproject.yml` 中 source 路径包含 `comm/` 目录 - - [x] SubTask 19.2: 确认 include 路径包含 `comm/` 目录 - -- [x] Task 20: 编译验证 - - [x] SubTask 20.1: 全量编译通过,无 error - - [x] SubTask 20.2: 检查无新增 warning - - [x] SubTask 20.3: 检查无遗漏的 `__disable_irq/__set_PRIMASK`(应全部改为 BASEPRI) - -# Task Dependencies -- Task 1 → Task 4, 5, 6, 7, 9(临界区宏是改造前提) -- Task 2 → Task 3, 5, 6, 7, 16(config 优先级与引脚宏) -- Task 8 → Task 6.4, 18(任务枚举先定义,才能在 ISR/task_runner 中引用) -- Task 10, 12 → Task 11, 13(EXMC 接口定义在实现前) -- Task 13, 14 → Task 15(ESC 驱动 + 从站接口在周期实现前) -- Task 14, 16 → Task 15(双缓冲 + 同步中断在周期实现前) -- Task 1-9(阶段1) 可与 Task 10-16(阶段2) 部分并行:阶段1 改造不依赖 comm/ 代码 -- Task 17, 18(阶段3) 依赖阶段1+阶段2 全部完成 -- Task 19 → Task 20(构建配置在编译验证前) diff --git a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml index 4bd7f59..3c83793 100644 --- a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml +++ b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml @@ -25,7 +25,7 @@ project: - file: ./device/adc.h - file: ./device/adc.cpp - file: ./device/canfd.h - - file: ./device/CANFD.cpp + - file: ./device/canfd.cpp - file: ./device/timer.h - file: ./device/timer.cpp - file: ./device/usart0.h diff --git a/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp b/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp index c690ff7..d3c0b94 100644 --- a/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp +++ b/GD32E508_DCU_APP/comm/esc_exmc_bus.cpp @@ -60,7 +60,7 @@ void EscExmcBus::init() norsram_init.asyn_wait = DISABLE; norsram_init.nwait_polarity = EXMC_NWAIT_POLARITY_LOW; norsram_init.databus_width = EXMC_NOR_DATABUS_WIDTH_16B; - /* 时序:按 LAN9252 HBI 要求配置 read/write wait state + /* TODO(spec): 时序:按 LAN9252 HBI 要求配置 read/write wait state * LAN9252 HBI 最小 read/write cycle = 100ns。 * AHB=180MHz(RCU_MODIFY_UP 强制 /1),1 HCLK = 5.6ns。 * 6 HCLK = 33ns → 不满足,需 ≥ 18 HCLK 才能达到 100ns。 @@ -71,7 +71,9 @@ void EscExmcBus::init() exmc_norsram_init(&norsram_init); exmc_norsram_enable(CONFIG_ESC_EXMC_BANK_NOR_SRAM_REGION1); #else - /* EXMC 外设库未安装:仅占位,启用 EtherCAT 时需先安装 EXMC 组件。 */ + /* EXMC 外设库未安装(gd32e50x_exmc.h 缺失):EtherCAT ESC EXMC 总线初始化为空, + * 运行期 selfTest 必失败,EtherCAT 自动禁用。启用 ESC 需先安装 EXMC 组件。 */ +#warning "EXMC peripheral library not included; EtherCAT ESC EXMC bus init is a no-op" #endif } diff --git a/GD32E508_DCU_APP/comm/esc_lan9252.cpp b/GD32E508_DCU_APP/comm/esc_lan9252.cpp index adade4e..9c207c8 100644 --- a/GD32E508_DCU_APP/comm/esc_lan9252.cpp +++ b/GD32E508_DCU_APP/comm/esc_lan9252.cpp @@ -28,7 +28,7 @@ void EscLan9252::init() EscExmcBus::init(); /* ============================================================ - * TODO: 完整 ESC 寄存器配置(需 SSC 或等价协议栈代码,本 spec 不实现) + * TODO(spec): 完整 ESC 寄存器配置(需 SSC 或等价协议栈代码,本 spec 不实现) * ============================================================ * 当前仅完成 EXMC 总线初始化与芯片 ID 读取。要使 SYNC0 实际触发,还需配置: * @@ -58,11 +58,6 @@ void EscLan9252::init() * ============================================================ */ } -bool EscLan9252::selfTest() -{ - return (readChipId() == LAN9252_CHIP_ID_VALUE); -} - uint32_t EscLan9252::readChipId() { /* INIT-ONLY:init 阶段中断未使能,但仍走 initOnly 路径保持一致性 */ @@ -77,11 +72,6 @@ uint16_t EscLan9252::readReg(uint16_t addr) return EscExmcBus::read16((uint32_t)addr << 1U); } -void EscLan9252::writeReg(uint16_t addr, uint16_t val) -{ - EscExmcBus::write16((uint32_t)addr << 1U, val); -} - __attribute__((section(".ramfunc"), noinline)) void EscLan9252::readPdi(uint16_t offset, uint8_t *buf, size_t len) { @@ -121,24 +111,8 @@ void EscLan9252::writePdi(uint16_t offset, const uint8_t *buf, size_t len) } /* ---- initOnly* 接口:INIT-ONLY,仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用 ---- - * 运行期禁止调用;需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 - * 注:不再使用 PRIMASK 关中断(违反项目硬约束"Critical sections MUST use BASEPRI")。 */ + * 运行期禁止调用;需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */ uint16_t EscLan9252::initOnlyReadReg(uint16_t addr) { return readReg(addr); } - -void EscLan9252::initOnlyWriteReg(uint16_t addr, uint16_t val) -{ - writeReg(addr, val); -} - -void EscLan9252::initOnlyReadPdi(uint16_t offset, uint8_t *buf, size_t len) -{ - readPdi(offset, buf, len); -} - -void EscLan9252::initOnlyWritePdi(uint16_t offset, const uint8_t *buf, size_t len) -{ - writePdi(offset, buf, len); -} diff --git a/GD32E508_DCU_APP/comm/esc_lan9252.h b/GD32E508_DCU_APP/comm/esc_lan9252.h index 3b79f67..55bda91 100644 --- a/GD32E508_DCU_APP/comm/esc_lan9252.h +++ b/GD32E508_DCU_APP/comm/esc_lan9252.h @@ -4,8 +4,8 @@ * @note 通过 EscExmcBus 16-bit 并行总线访问 LAN9252 寄存器与 PDI 内存。 * * 接口调用约束(避免 EXMC 总线争用): - * - readReg/writeReg/readPdi/writePdi:ISR-only,仅供 preempt=0 的 periodicIsr() 调用,无锁。 - * - initOnlyReadReg/initOnlyWriteReg/initOnlyReadPdi/initOnlyWritePdi:INIT-ONLY,仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 + * - readReg/readPdi/writePdi:ISR-only,仅供 preempt=0 的 periodicIsr() 调用,无锁。 + * - initOnlyReadReg:INIT-ONLY,仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 * * PDI offset 语义:offset 参数为 SyncManager 内存区相对偏移,输入 PDO(TxPDO)与输出 PDO(RxPDO)必须使用不同 offset,参见 CONFIG_ESC_PDI_INPUT_OFFSET / CONFIG_ESC_PDI_OUTPUT_OFFSET。 */ @@ -24,12 +24,6 @@ public: /** @brief 初始化 EXMC 总线与 ESC 基本寄存器。 */ static void init(); - /** - * @brief ESC 自检:读取芯片 ID 寄存器校验。 - * @return bool true 表示 ID 匹配(0x87654321),false 表示失败。 - */ - static bool selfTest(); - /** * @brief 读取 ESC 芯片 ID(INIT-ONLY,供 init 打印诊断用)。 * @return uint32_t 32-bit 芯片 ID(期望值 0x87654321)。 @@ -39,9 +33,6 @@ public: /** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */ static uint16_t readReg(uint16_t addr); - /** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */ - static void writeReg(uint16_t addr, uint16_t val); - /** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */ static void readPdi(uint16_t offset, uint8_t *buf, size_t len); @@ -50,15 +41,6 @@ public: /** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */ static uint16_t initOnlyReadReg(uint16_t addr); - - /** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */ - static void initOnlyWriteReg(uint16_t addr, uint16_t val); - - /** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */ - static void initOnlyReadPdi(uint16_t offset, uint8_t *buf, size_t len); - - /** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */ - static void initOnlyWritePdi(uint16_t offset, const uint8_t *buf, size_t len); }; #endif /* COMM_ESC_LAN9252_H */ diff --git a/GD32E508_DCU_APP/comm/ethercat_slave.cpp b/GD32E508_DCU_APP/comm/ethercat_slave.cpp index 8590724..8d76ce4 100644 --- a/GD32E508_DCU_APP/comm/ethercat_slave.cpp +++ b/GD32E508_DCU_APP/comm/ethercat_slave.cpp @@ -113,8 +113,9 @@ void EthercatSlave::process() * 运行期 PDO 交换仅通过 getLatestInputs()/setOutputs() 三缓冲进行;ESC 寄存器访问仅限 init 阶段。 * * 框架级桥接:从三缓冲取最新输入到 s_appInputs,把 s_appOutputs 推回三缓冲。 - * TODO: 应用对象字典映射(具体输入字节对应哪个应用变量,如字节 0-3=温度浮点), - * 由后续应用层 spec 定义;应用层通过 getApplicationInputs()/setApplicationOutputs() 访问快照。 */ + * TODO(spec): 应用对象字典映射(具体输入字节对应哪个应用变量,如字节 0-3=温度浮点) + * 待后续应用层 spec 定义; + * 应用层通过 getApplicationInputs()/setApplicationOutputs() 访问快照。 */ const PdoBuffer *latest = getLatestInputs(); if (latest != nullptr) { s_appInputs = *latest; diff --git a/GD32E508_DCU_APP/comm/ethercat_slave.h b/GD32E508_DCU_APP/comm/ethercat_slave.h index f969bc0..0113faf 100644 --- a/GD32E508_DCU_APP/comm/ethercat_slave.h +++ b/GD32E508_DCU_APP/comm/ethercat_slave.h @@ -19,11 +19,7 @@ constexpr size_t CONFIG_ETHERCAT_PDO_SIZE = 32U; /** @brief EtherCAT AL 状态码(EtherCAT 协议定义,ESC AL Status 寄存器低 4 位)。 */ -constexpr uint8_t AL_STATE_INIT = 0x01U; /**< INIT 状态。 */ -constexpr uint8_t AL_STATE_PREOP = 0x02U; /**< PREOP 状态。 */ -constexpr uint8_t AL_STATE_BOOT = 0x03U; /**< BOOT 状态。 */ constexpr uint8_t AL_STATE_SAFEOP = 0x04U; /**< SAFEOP 状态(SYNC0 可触发下限)。 */ -constexpr uint8_t AL_STATE_OP = 0x08U; /**< OP 状态。 */ /** * @brief PDO 数据缓冲(输入/输出共用结构)。 @@ -75,14 +71,16 @@ public: * @brief 获取应用层输入 PDO 快照(主循环调用)。 * @return const PdoBuffer* 最近一次 process() 拷贝的输入快照只读指针。 * @note 返回的指针在下次 process() 调用前有效;应用层应在 process() 后立即读取。 - */ + * @note TODO(app-layer): 应用层 PDO 字典映射 spec 启用前无外部调用方; + * 保留接口契约,待未来 spec 实现后由应用层调用。 */ static const PdoBuffer* getApplicationInputs(); /** * @brief 设置应用层输出 PDO 快照(主循环调用)。 * @param[in] outputs 输出数据指针(内容会被拷贝到内部快照)。 * @note 下次 process() 调用时,内部快照通过 setOutputs() 推入三缓冲。 - */ + * @note TODO(app-layer): 应用层 PDO 字典映射 spec 启用前无外部调用方; + * 保留接口契约,待未来 spec 实现后由应用层调用。 */ static void setApplicationOutputs(const PdoBuffer *outputs); /** @@ -107,7 +105,9 @@ public: /** @brief 读取 ESC 芯片 ID(供诊断打印;未就绪时为上次读到的值)。 */ static uint32_t getChipId() { return s_chipId; } - /** @brief 读取 ESC AL 状态码(供诊断打印与 SSC 集成;未就绪时为上次读到的值)。 */ + /** @brief 读取 ESC AL 状态码(供诊断打印与 SSC 集成;未就绪时为上次读到的值)。 + * @note TODO(app-layer): 当前仅 init 内部用作 SAFEOP 守卫,外部诊断未启用; + * SSC 集成后由应用层查询。 */ static uint8_t getAlState() { return s_alState; } private: diff --git a/GD32E508_DCU_APP/comm/ethercat_sync.cpp b/GD32E508_DCU_APP/comm/ethercat_sync.cpp index 425dbf5..5a09836 100644 --- a/GD32E508_DCU_APP/comm/ethercat_sync.cpp +++ b/GD32E508_DCU_APP/comm/ethercat_sync.cpp @@ -36,20 +36,6 @@ void EthercatSync::init() exti_interrupt_enable(CONFIG_ESC_SYNC0_EXTI); exti_interrupt_flag_clear(CONFIG_ESC_SYNC0_EXTI); nvic_irq_enable(CONFIG_ESC_SYNC0_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC, 0U); - - /* ESC IRQ:PB1 -> EXTI1,下降沿,preempt=1 */ - gpio_exti_source_select(CONFIG_ESC_IRQ_EXTI_SOURCE_PORT, CONFIG_ESC_IRQ_EXTI_SOURCE_PIN); - exti_init(CONFIG_ESC_IRQ_EXTI, EXTI_INTERRUPT, EXTI_TRIG_FALLING); - exti_interrupt_enable(CONFIG_ESC_IRQ_EXTI); - exti_interrupt_flag_clear(CONFIG_ESC_IRQ_EXTI); - nvic_irq_enable(CONFIG_ESC_IRQ_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_ESC, 0U); - - /* SYNC1:PB2 -> EXTI2,下降沿,preempt=1(备用) */ - gpio_exti_source_select(CONFIG_ESC_SYNC1_EXTI_SOURCE_PORT, CONFIG_ESC_SYNC1_EXTI_SOURCE_PIN); - exti_init(CONFIG_ESC_SYNC1_EXTI, EXTI_INTERRUPT, EXTI_TRIG_FALLING); - exti_interrupt_enable(CONFIG_ESC_SYNC1_EXTI); - exti_interrupt_flag_clear(CONFIG_ESC_SYNC1_EXTI); - nvic_irq_enable(CONFIG_ESC_SYNC1_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC1, 0U); #else /* ---- 降级模式:TIMER0 内部定时 ---- */ rcu_periph_clock_enable(RCU_TIMER0); @@ -66,21 +52,20 @@ void EthercatSync::init() timer_interrupt_enable(TIMER0, TIMER_INT_UP); nvic_irq_enable(TIMER0_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC, 0U); timer_enable(TIMER0); +#endif - /* ESC IRQ:PB1 -> EXTI1,下降沿,preempt=1 */ + /* 公共:ESC IRQ (PB1->EXTI1) + SYNC1 (PB2->EXTI2) */ gpio_exti_source_select(CONFIG_ESC_IRQ_EXTI_SOURCE_PORT, CONFIG_ESC_IRQ_EXTI_SOURCE_PIN); exti_init(CONFIG_ESC_IRQ_EXTI, EXTI_INTERRUPT, EXTI_TRIG_FALLING); exti_interrupt_enable(CONFIG_ESC_IRQ_EXTI); exti_interrupt_flag_clear(CONFIG_ESC_IRQ_EXTI); nvic_irq_enable(CONFIG_ESC_IRQ_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_ESC, 0U); - /* SYNC1:PB2 -> EXTI2,下降沿,preempt=1(备用) */ gpio_exti_source_select(CONFIG_ESC_SYNC1_EXTI_SOURCE_PORT, CONFIG_ESC_SYNC1_EXTI_SOURCE_PIN); exti_init(CONFIG_ESC_SYNC1_EXTI, EXTI_INTERRUPT, EXTI_TRIG_FALLING); exti_interrupt_enable(CONFIG_ESC_SYNC1_EXTI); exti_interrupt_flag_clear(CONFIG_ESC_SYNC1_EXTI); nvic_irq_enable(CONFIG_ESC_SYNC1_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC1, 0U); -#endif } __attribute__((section(".ramfunc"), noinline)) @@ -106,7 +91,8 @@ void EthercatSync::onSync1Exti() { if (exti_interrupt_flag_get(CONFIG_ESC_SYNC1_EXTI) != RESET) { exti_interrupt_flag_clear(CONFIG_ESC_SYNC1_EXTI); - /* SYNC1 备用,暂不处理 */ + /* 预留:SYNC1 通道当前未承载业务,仅清 EXTI 标志防止中断挂起; + * 启用 SYNC1 业务时在此处添加处理逻辑。 */ } } @@ -117,6 +103,8 @@ void EthercatSync::onTimerIrq() timer_interrupt_flag_clear(TIMER0, TIMER_INT_FLAG_UP); EthercatSlave::periodicIsr(); } +#else + /* EXTI 模式下 TIMER0 不使用,本函数为占位;CONFIG_ETHERCAT_SYNC_MODE=1 时激活。 */ #endif } diff --git a/GD32E508_DCU_APP/device/ADC.cpp b/GD32E508_DCU_APP/device/ADC.cpp index 0def0f4..4a2f7d8 100644 --- a/GD32E508_DCU_APP/device/ADC.cpp +++ b/GD32E508_DCU_APP/device/ADC.cpp @@ -10,6 +10,7 @@ #include namespace { +/* ---- 校验常量与辅助函数 ---- */ constexpr float kMinValidVdda = 2.7f; constexpr float kMaxValidVdda = 3.6f; constexpr float kMinValidTemperature = -40.0f; @@ -24,16 +25,14 @@ static bool isFiniteFloat(float value) { return isfinite(value) != 0; } -} // namespace -namespace { +/* ---- DMA 配置 ---- */ constexpr uint32_t kDmaPeriph = DMA0; constexpr dma_channel_enum kDmaChannel = DMA_CH0; constexpr uint32_t kChannelCount = 3U; constexpr uint32_t kVinGpio = GPIOA; constexpr uint32_t kVinPin = GPIO_PIN_6; volatile uint16_t s_dmaBuffer[kChannelCount] = {0U, 0U, 0U}; -volatile uint8_t s_dmaReady = 0U; } // namespace /** @@ -108,30 +107,6 @@ void Adc::init() SysTickTimer::delayMs(CONFIG_ADC_STABILIZE_DELAY_MS); adc_software_trigger_enable(CONFIG_ADC_INSTANCE, ADC_REGULAR_CHANNEL); SysTickTimer::delayMs(1U); - s_dmaReady = 1U; -} - -/** - * @brief 查询 ADC DMA 采样是否已经就绪。 - * @param[in] 无。 - * @param[out] 无。 - * @retval bool true 表示 DMA 已启动并完成初始稳定等待,false 表示尚未就绪。 - */ -bool Adc::dmaReady() -{ - return s_dmaReady != 0U; -} - -/** - * @brief 获取 ADC DMA 原始缓冲区首地址。 - * @param[in] 无。 - * @param[out] 无。 - * @retval const uint16_t* 返回 DMA 原始缓冲区首地址。 - * @note 缓冲顺序固定为 `VIN_ADC / TEMP / VREFINT`。 - */ -const uint16_t *Adc::dmaBuffer() -{ - return (const uint16_t *)s_dmaBuffer; } /** diff --git a/GD32E508_DCU_APP/device/CANFD.cpp b/GD32E508_DCU_APP/device/CANFD.cpp index 0a9746b..9ccec9a 100644 --- a/GD32E508_DCU_APP/device/CANFD.cpp +++ b/GD32E508_DCU_APP/device/CANFD.cpp @@ -12,6 +12,12 @@ #include namespace { + +/* 纯软件延时循环(不依赖 SysTick,可在任何中断状态下使用) */ +static inline void spinDelay(uint32_t loops) { + for (volatile uint32_t d = 0; d < loops; ++d) { } +} + struct CanChannelConfig { uint32_t canPeriph; rcu_periph_enum canRcu; @@ -91,7 +97,7 @@ static CanSelfTestResult runSelfTestFrame(uint8_t channelIndex, uint32_t extId, result.mailbox = can_message_transmit(canPeriph, &tx_msg); result.txReq = (result.mailbox != CAN_NOMAILBOX); - for (volatile uint32_t dly = 0; dly < 40000U; ++dly) { } + spinDelay(40000U); if (result.txReq) { result.txState = static_cast(can_transmit_states(canPeriph, result.mailbox)); @@ -117,21 +123,21 @@ static void hardResetCan(uint32_t canPeriph, rcu_periph_enum canRcu) { /* 1. 触发 SWRST 软件复位 */ CAN_CTL(canPeriph) |= CAN_CTL_SWRST; - for (volatile uint32_t d = 0; d < 100000U; ++d) {} + spinDelay(100000U); /* 2. 确认 SWRST 已被硬件自动清零;若未清零则强制写 0 并等待 */ if (CAN_CTL(canPeriph) & CAN_CTL_SWRST) { CAN_CTL(canPeriph) &= ~CAN_CTL_SWRST; - for (volatile uint32_t d = 0; d < 50000U; ++d) {} + spinDelay(50000U); } /* 3. 关 RCU 时钟,等待外设域完全放电 */ rcu_periph_clock_disable(canRcu); - for (volatile uint32_t d = 0; d < 200000U; ++d) {} /* ~1.1ms @180MHz */ + spinDelay(200000U); /* ~1.1ms @180MHz */ /* 4. 重新开时钟,等待外设域上电稳定 */ rcu_periph_clock_enable(canRcu); - for (volatile uint32_t d = 0; d < 200000U; ++d) {} /* ~1.1ms @180MHz */ + spinDelay(200000U); /* ~1.1ms @180MHz */ /* 5. can_deinit() 走标准固件库复位流程 */ can_deinit(canPeriph); @@ -303,123 +309,110 @@ bool CanFd::initFdMode(uint32_t canPeriph) #endif } -/** - * @brief 初始化三路 CANFD 控制器、过滤器、中断与软件队列。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 初始化后默认将三路 TCAN1057A 收发器置为 Normal,并使能 FIFO0 的非空/满/overfull 中断统计。 - */ -void CanFd::init() +/** @brief 打印 SYS/APB1 时钟与目标波特率。 */ +void CanFd::printClockInfo() { - s_diag = {}; - s_nextConsumeChannel = 0U; - s_mode = Mode::kModeAllPass; - for (size_t i = 0; i < kChannelCount; ++i) { - s_channelStats[i] = {}; - s_feedback[i] = {}; - } - s_feedbackSeq = 0U; - - initGpio(); - printf("[CAN-CLOCK] SYS=%luHz APB1=%luHz nominal=1Mbps data=2Mbps\r\n", static_cast(SystemCoreClock), static_cast(rcu_clock_freq_get(CK_APB1))); +} - for(size_t i = 0; i < kChannelCount; ++i) { - const auto &ch = kChannels[i]; - if (!kChannelEnabled[i]) { - printf("[CAN] CH%u DISABLED, skip init\r\n", static_cast(i)); - continue; - } - rcu_periph_clock_enable(ch.canRcu); - gpio_bit_reset(ch.slpPort, ch.slpPin); /* STB=0 -> Normal */ - const uint8_t rxLevelBefore = (gpio_input_bit_get(ch.rxPort, ch.rxPin) != RESET) ? 1U : 0U; - const uint8_t stbLevel = (gpio_output_bit_get(ch.slpPort, ch.slpPin) != RESET) ? 1U : 0U; +/** @brief 单通道硬件初始化(hardResetCan+重试+退出init mode+中断使能+RX队列清零)。 */ +void CanFd::initChannelHardware(uint8_t i) +{ + const auto &ch = kChannels[i]; + if (!kChannelEnabled[i]) { + printf("[CAN] CH%u DISABLED, skip init\r\n", static_cast(i)); + return; + } + rcu_periph_clock_enable(ch.canRcu); + gpio_bit_reset(ch.slpPort, ch.slpPin); /* STB=0 -> Normal */ + const uint8_t rxLevelBefore = (gpio_input_bit_get(ch.rxPort, ch.rxPin) != RESET) ? 1U : 0U; + const uint8_t stbLevel = (gpio_output_bit_get(ch.slpPort, ch.slpPin) != RESET) ? 1U : 0U; - /* 强制复位 + 重试机制:使用 hardResetCan 完整电源周期解决 CAN1 卡死问题 */ - bool nominalOk = false; - bool fdOk = false; - for (uint8_t retry = 0U; retry < 5U; ++retry) { - hardResetCan(ch.canPeriph, ch.canRcu); + /* 强制复位 + 重试机制:使用 hardResetCan 完整电源周期解决 CAN1 卡死问题 */ + bool nominalOk = false; + bool fdOk = false; + for (uint8_t retry = 0U; retry < 5U; ++retry) { + hardResetCan(ch.canPeriph, ch.canRcu); - nominalOk = initNominalTiming(ch.canPeriph); - fdOk = initFdMode(ch.canPeriph); - if (nominalOk && fdOk) { - break; /* 初始化成功,无需重试 */ - } - - /* can_init 失败后外设可能卡在 init mode,先强制退出让下次重试从干净状态开始 */ - CAN_CTL(ch.canPeriph) &= ~CAN_CTL_IWMOD; - for (volatile uint32_t d = 0; d < 50000U; ++d) { - if ((CAN_STAT(ch.canPeriph) & CAN_STAT_IWS) == 0U) break; - } - - printf("[CAN-WARN] CH%u init failed (nom=%u fd=%u STAT=0x%08lX), retry %u/5\r\n", - static_cast(i), - nominalOk ? 1U : 0U, fdOk ? 1U : 0U, - static_cast(CAN_STAT(ch.canPeriph)), - static_cast(retry + 1U)); + nominalOk = initNominalTiming(ch.canPeriph); + fdOk = initFdMode(ch.canPeriph); + if (nominalOk && fdOk) { + break; /* 初始化成功,无需重试 */ } - /* 滤波器配置延迟到循环结束后统一执行: - * CAN1 的 hardResetCan 含 SWRST,会清零 CAN0/CAN1 共享滤波器寄存器, - * 若在循环内配 CAN0 filter 后再做 CAN1 hardReset,CAN0 filter 会被破坏。 */ - - /* 退出 init mode,确保进入 NORMAL 工作模式 */ + /* can_init 失败后外设可能卡在 init mode,先强制退出让下次重试从干净状态开始 */ CAN_CTL(ch.canPeriph) &= ~CAN_CTL_IWMOD; - uint32_t exitWait = 0U; - for (; exitWait < 200000U; ++exitWait) { - if ((CAN_STAT(ch.canPeriph) & CAN_STAT_IWS) == 0U) { - break; - } + for (volatile uint32_t d = 0; d < 50000U; ++d) { + if ((CAN_STAT(ch.canPeriph) & CAN_STAT_IWS) == 0U) break; } - /* 兜底:若仍然卡在 init mode,做一次额外的 SWRST + 强制退出 */ - if (CAN_STAT(ch.canPeriph) & CAN_STAT_IWS) { - CAN_CTL(ch.canPeriph) |= CAN_CTL_SWRST; - for (volatile uint32_t d = 0; d < 50000U; ++d) {} - CAN_CTL(ch.canPeriph) &= ~(CAN_CTL_SWRST | CAN_CTL_IWMOD); - for (volatile uint32_t d = 0; d < 200000U; ++d) { - if ((CAN_STAT(ch.canPeriph) & CAN_STAT_IWS) == 0U) break; - } - printf("[CAN-WARN] CH%u needed extra SWRST to exit init mode (STAT=0x%08lX)\r\n", - static_cast(i), - static_cast(CAN_STAT(ch.canPeriph))); - } - - const uint32_t statAfterExit = CAN_STAT(ch.canPeriph); - const uint32_t errAfterExit = CAN_ERR(ch.canPeriph); - 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; - printf("[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", + printf("[CAN-WARN] CH%u init failed (nom=%u fd=%u STAT=0x%08lX), retry %u/5\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_TSTAT(ch.canPeriph) = CAN_TSTAT_MTF0 | CAN_TSTAT_MTF1 | CAN_TSTAT_MTF2; - can_interrupt_enable(ch.canPeriph, CAN_INT_TME | CAN_INT_RFNE0 | CAN_INT_RFF0 | CAN_INT_RFO0); - nvic_irq_enable(ch.txIrq, CONFIG_IRQ_PREEMPT_CAN, static_cast(i)); - nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN, static_cast(i)); - - s_rxQueues[i].head = 0U; - s_rxQueues[i].tail = 0U; - s_rxQueues[i].count = 0U; + nominalOk ? 1U : 0U, fdOk ? 1U : 0U, + static_cast(CAN_STAT(ch.canPeriph)), + static_cast(retry + 1U)); } - s_nextConsumeChannel = 0U; + /* 滤波器配置延迟到循环结束后统一执行: + * CAN1 的 hardResetCan 含 SWRST,会清零 CAN0/CAN1 共享滤波器寄存器, + * 若在循环内配 CAN0 filter 后再做 CAN1 hardReset,CAN0 filter 会被破坏。 */ + /* 退出 init mode,确保进入 NORMAL 工作模式 */ + CAN_CTL(ch.canPeriph) &= ~CAN_CTL_IWMOD; + uint32_t exitWait = 0U; + for (; exitWait < 200000U; ++exitWait) { + if ((CAN_STAT(ch.canPeriph) & CAN_STAT_IWS) == 0U) { + break; + } + } + + /* 兜底:若仍然卡在 init mode,做一次额外的 SWRST + 强制退出 */ + if (CAN_STAT(ch.canPeriph) & CAN_STAT_IWS) { + CAN_CTL(ch.canPeriph) |= CAN_CTL_SWRST; + spinDelay(50000U); + CAN_CTL(ch.canPeriph) &= ~(CAN_CTL_SWRST | CAN_CTL_IWMOD); + for (volatile uint32_t d = 0; d < 200000U; ++d) { + if ((CAN_STAT(ch.canPeriph) & CAN_STAT_IWS) == 0U) break; + } + printf("[CAN-WARN] CH%u needed extra SWRST to exit init mode (STAT=0x%08lX)\r\n", + static_cast(i), + static_cast(CAN_STAT(ch.canPeriph))); + } + + const uint32_t statAfterExit = CAN_STAT(ch.canPeriph); + const uint32_t errAfterExit = CAN_ERR(ch.canPeriph); + 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; + printf("[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_TSTAT(ch.canPeriph) = CAN_TSTAT_MTF0 | CAN_TSTAT_MTF1 | CAN_TSTAT_MTF2; + can_interrupt_enable(ch.canPeriph, CAN_INT_TME | CAN_INT_RFNE0 | CAN_INT_RFF0 | CAN_INT_RFO0); + nvic_irq_enable(ch.txIrq, CONFIG_IRQ_PREEMPT_CAN, static_cast(i)); + nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN, static_cast(i)); + + s_rxQueues[i].head = 0U; + s_rxQueues[i].tail = 0U; + s_rxQueues[i].count = 0U; +} + +/** @brief 统一配置三路滤波器(HBC1F + per-channel filter)。 */ +void CanFd::configureAllFilters() +{ /* ===== 统一配置滤波器(必须在所有 hardResetCan 完成之后) ===== * CAN1 hardResetCan 的 SWRST 会清零 CAN0/CAN1 共享滤波器寄存器域。 * 若在循环内先配 CAN0 filter、后做 CAN1 hardReset,CAN0 的 filter 0 会被 @@ -433,7 +426,11 @@ void CanFd::init() printf("[CAN] filters configured post-init: HBC1F=%lu FW=0x%08lX\r\n", static_cast((CAN_FCTL(CAN0) >> 8) & 0x3FU), static_cast(CAN_FW(CAN0))); +} +/** @brief 打印初始化结果汇总(BT/DBT/滤波器状态)。 */ +void CanFd::printInitResult() +{ /* ===== 初始化结果汇总 ===== */ printf("=== CAN INIT RESULT ===\r\n"); for (size_t i = 0; i < kChannelCount; ++i) { @@ -462,7 +459,11 @@ void CanFd::init() static_cast(fw2), static_cast((fw2 >> 2) & 1U)); } +} +/** @brief 打印初始化后错误计数器快照(TEC/REC/BOFF/EPV/EW)。 */ +void CanFd::printErrorState() +{ /* ===== 初始化后错误计数器快照(不发帧,避免启动时累积 TEC)===== */ printf("=== CAN INIT ERR STATE ===\r\n"); for (size_t i = 0; i < kChannelCount; ++i) { @@ -479,7 +480,11 @@ void CanFd::init() (err & CAN_ERR_PERR) ? 1U : 0U, /* EPV bit1 */ (err & CAN_ERR_WERR) ? 1U : 0U); /* EWARN bit0 */ } +} +/** @brief 物理总线自测(受 CONFIG_CAN_STARTUP_BUS_TEST_ENABLE 控制)。 */ +void CanFd::runSelfTest() +{ /* ===== Self-test (NORMAL mode, 发到物理总线测 ACK) ===== */ #if (CONFIG_CAN_STARTUP_BUS_TEST_ENABLE != 0) printf("=== CAN SELF-TEST (normal mode) ===\r\n"); @@ -514,8 +519,11 @@ void CanFd::init() #else printf("=== CAN SELF-TEST SKIPPED (CONFIG_CAN_STARTUP_BUS_TEST_ENABLE=0) ===\r\n"); #endif +} - +/** @brief 内部回环测试(受 CONFIG_CAN_STARTUP_LOOPBACK_TEST_ENABLE 控制)。 */ +void CanFd::runLoopbackTest() +{ /* ===== Loopback 回环测试 (内部 TX→RX,不走物理总线) ===== */ #if (CONFIG_CAN_STARTUP_LOOPBACK_TEST_ENABLE != 0) printf("=== CAN LOOPBACK (internal) ===\r\n"); @@ -658,7 +666,11 @@ void CanFd::init() #else printf("=== CAN LOOPBACK SKIPPED (CONFIG_CAN_STARTUP_LOOPBACK_TEST_ENABLE=0) ===\r\n"); #endif +} +/** @brief 全寄存器快照转储(CTL/STAT/BT/DBT/ERR/INTEN/FCTL/FW/RFL0)。 */ +void CanFd::dumpRegisters() +{ /* ===== 初始化后全寄存器快照:诊断 CAN0/CAN1 不收帧 ===== */ printf("=== POST-INIT FULL REG DUMP ===\r\n"); for (size_t i = 0; i < kChannelCount; ++i) { @@ -702,6 +714,40 @@ void CanFd::init() printf("===============================\r\n"); } +/** + * @brief 初始化三路 CANFD 控制器、过滤器、中断与软件队列。 + * @param[in] 无。 + * @param[out] 无。 + * @retval 无返回值。 + * @note 初始化后默认将三路 TCAN1057A 收发器置为 Normal,并使能 FIFO0 的非空/满/overfull 中断统计。 + */ +void CanFd::init() +{ + s_diag = {}; + s_nextConsumeChannel = 0U; + s_mode = Mode::kModeAllPass; + for (size_t i = 0; i < kChannelCount; ++i) { + s_channelStats[i] = {}; + s_feedback[i] = {}; + } + s_feedbackSeq = 0U; + + initGpio(); + printClockInfo(); + + for (uint8_t i = 0; i < kChannelCount; ++i) { + initChannelHardware(i); + } + s_nextConsumeChannel = 0U; + + configureAllFilters(); + printInitResult(); + printErrorState(); + runSelfTest(); + runLoopbackTest(); + dumpRegisters(); +} + /** * @brief 切换 CANFD 接收过滤模式。 * @param[in] mode 目标过滤模式。 @@ -725,18 +771,6 @@ CanFd::Mode CanFd::mode() return s_mode; } -/** - * @brief 执行兼容保留的 CAN 轮询入口。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 当前三路接收均由 RX0 中断驱动,该函数仅保留旧调用兼容性。 - */ -void CanFd::poll() -{ - /* 兼容保留:三通道接收已由 RX0 中断驱动 */ -} - /** * @brief 查询三路 CAN 通道是否存在待消费接收报文。 * @param[in] 无。 @@ -963,34 +997,6 @@ const CanFd::Diag *CanFd::diag() return &s_diag; } -/** - * @brief 将三路 TCAN1057A 收发器置为 Normal 模式。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 对所有通道统一将 SLP/STB 控制脚拉低。 - */ -void CanFd::setNormalMode() -{ - for(size_t i = 0; i < kChannelCount; ++i) { - gpio_bit_reset(kChannels[i].slpPort, kChannels[i].slpPin); - } -} - -/** - * @brief 将三路 TCAN1057A 收发器置为 Silent 模式。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 对所有通道统一将 SLP/STB 控制脚拉高。 - */ -void CanFd::setSilentMode() -{ - for(size_t i = 0; i < kChannelCount; ++i) { - gpio_bit_set(kChannels[i].slpPort, kChannels[i].slpPin); - } -} - /** * @brief 透传式发送:按接收帧的原格式(标准/扩展、CAN-FD/经典、BRS/ESI、DLC)原样转发到目标 CAN 通道。 * @param[in] canPeriph 目标 CAN 外设实例。 @@ -1184,18 +1190,6 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex) } } -/** - * @brief CAN0 兼容中断处理入口。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 兼容保留入口,内部统一转调 `handleRxInterrupt(0U)`。 - */ -void CanFd::onRx0Interrupt() -{ - handleRxInterrupt(0U); -} - /** * @brief 处理指定通道的发送完成中断。 * @param[in] channelIndex 目标 CAN 通道索引(0/1/2)。 @@ -1334,98 +1328,6 @@ void CanFd::refreshFeedbackSnapshot() } } -/** - * @brief 运行时 BUS_OFF / 严重错误被动状态软件恢复。 - * @note 由 1Hz 心跳任务调用。若某通道持续 2 秒以上处于 BUS_OFF 或 TEC>96, - * 通过收发器静默-唤醒切换让总线获得 128×11 个连续隐性位,协助硬件 - * auto_bus_off_recovery 退出 BUS_OFF。本函数不执行 CAN 控制器 hardReset, - * 避免破坏 CAN0/CAN1 共享的 filter 配置;若切换后仍不健康,说明物理层 - * 问题未消除,需要排查终端电阻/ACK/波特率/接线。 - */ -void CanFd::recoverBusOffIfNeeded() -{ - static uint32_t s_unhealthySinceSeq[kChannelCount] = {0U, 0U, 0U}; - static bool s_recoveryInProgress[kChannelCount] = {false, false, false}; - - for (size_t i = 0; i < kChannelCount; ++i) { - if (!kChannelEnabled[i]) { - continue; - } - - const auto &ch = kChannels[i]; - const uint32_t errReg = CAN_ERR(ch.canPeriph); - const bool busOff = (errReg & CAN_ERR_BOERR) != 0U; - const uint32_t tec = (errReg >> 16) & 0xFFU; - const bool healthy = !busOff && (tec <= 96U); - - if (healthy) { - if (s_unhealthySinceSeq[i] != 0U) { - printf("[CAN-RECOVER] CH%u healthy again (TEC=%lu) seq=%lu\r\n", - static_cast(i), - static_cast(tec), - static_cast(s_feedbackSeq)); - s_unhealthySinceSeq[i] = 0U; - } - s_recoveryInProgress[i] = false; - continue; - } - - if (s_unhealthySinceSeq[i] == 0U) { - s_unhealthySinceSeq[i] = s_feedbackSeq; - } - - /* 仅在连续 2 秒不健康后才干预,避免对短暂波动过度反应 */ - const uint32_t elapsed = s_feedbackSeq - s_unhealthySinceSeq[i]; - if (elapsed < 2U) { - continue; - } - - if (s_recoveryInProgress[i]) { - /* 上一轮恢复后仍在下一周期观察到不健康:物理层问题未解决,不要刷屏 */ - static uint32_t s_lastStillBadPrintSeq[kChannelCount] = {0U, 0U, 0U}; - if ((s_feedbackSeq - s_lastStillBadPrintSeq[i]) >= 10U) { - printf("[CAN-RECOVER] CH%u still unhealthy after toggle (TEC=%lu bus_off=%u) seq=%lu; check physical layer\r\n", - static_cast(i), - static_cast(tec), - busOff ? 1U : 0U, - static_cast(s_feedbackSeq)); - s_lastStillBadPrintSeq[i] = s_feedbackSeq; - } - s_recoveryInProgress[i] = false; /* 允许下一轮再次尝试 */ - continue; - } - - printf("[CAN-RECOVER] CH%u attempting recovery (TEC=%lu bus_off=%u elapsed=%lus)\r\n", - static_cast(i), - static_cast(tec), - busOff ? 1U : 0U, - static_cast(elapsed)); - - /* 步骤:让收发器进入 standby 再切回 normal,强制总线出现连续隐性位, - * 帮助硬件 ABOR 完成 128×11 个连续隐性位的恢复序列。 */ - gpio_bit_set(ch.slpPort, ch.slpPin); - SysTickTimer::delayMs(20U); - gpio_bit_reset(ch.slpPort, ch.slpPin); - SysTickTimer::delayMs(10U); - - const uint32_t errAfterToggle = CAN_ERR(ch.canPeriph); - const bool stillBad = ((errAfterToggle & CAN_ERR_BOERR) != 0U) || - (((errAfterToggle >> 16) & 0xFFU) > 96U); - if (stillBad) { - printf("[CAN-RECOVER] CH%u toggle done but still unhealthy (TEC=%lu bus_off=%u); need physical-layer fix\r\n", - static_cast(i), - static_cast((errAfterToggle >> 16) & 0xFFU), - ((errAfterToggle & CAN_ERR_BOERR) != 0U) ? 1U : 0U); - } else { - printf("[CAN-RECOVER] CH%u recovered by transceiver toggle\r\n", - static_cast(i)); - } - - s_recoveryInProgress[i] = true; - } -} - - /** * @brief 拷贝指定通道的最新反馈快照。 * @param[in] channelIndex 目标 CAN 通道号(0/1/2)。 diff --git a/GD32E508_DCU_APP/device/adc.h b/GD32E508_DCU_APP/device/adc.h index 59a888a..a271394 100644 --- a/GD32E508_DCU_APP/device/adc.h +++ b/GD32E508_DCU_APP/device/adc.h @@ -32,23 +32,6 @@ public: */ static void init(); - /** - * @brief 查询 ADC DMA 缓存是否已完成首次有效填充。 - * @param[in] 无。 - * @param[out] 无。 - * @retval bool true 表示 DMA 循环缓存已可用,false 表示仍未准备好。 - */ - static bool dmaReady(); - - /** - * @brief 获取 ADC DMA 原始循环缓存只读指针。 - * @param[in] 无。 - * @param[out] 无。 - * @retval const uint16_t* 返回长度为 \p kChannelCount 的原始缓存指针。 - * @note 缓冲区顺序为:VIN_ADC → 温度通道 → VREF 通道。 - */ - static const uint16_t *dmaBuffer(); - /** * @brief 读取指定通道在 DMA 缓存中的原始 ADC 值。 * @param[in] channel 目标 ADC 通道号。 @@ -128,6 +111,4 @@ private: static void initDma(); }; -using adc_sample_t = Adc::Sample; - #endif /* DEVICE_ADC_H */ \ No newline at end of file diff --git a/GD32E508_DCU_APP/device/canfd.h b/GD32E508_DCU_APP/device/canfd.h index 7e7f2f1..4835801 100644 --- a/GD32E508_DCU_APP/device/canfd.h +++ b/GD32E508_DCU_APP/device/canfd.h @@ -132,7 +132,6 @@ public: /** @brief 查询当前 CANFD 接收过滤模式。 */ static Mode mode(); - static void poll(); static bool hasRxFrame(); static bool consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel = nullptr); static CanTxResult transmitFrame(uint8_t channelIndex, uint32_t ext_id, @@ -152,13 +151,6 @@ public: */ static void refreshFeedbackSnapshot(); - /** - * @brief 保留的手动 BUS_OFF 恢复诊断入口。 - * @note 正常周期任务不调用;控制器已启用硬件自动 BUS_OFF 恢复,避免在 - * 发送重试期间切换收发器 S 脚而人为制造额外位错误。 - */ - static void recoverBusOffIfNeeded(); - /** * @brief 拷贝指定通道的最新反馈快照到调用方缓冲。 * @param[in] channelIndex 目标 CAN 通道号(0/1/2)。 @@ -175,9 +167,6 @@ public: */ static bool copyIdSnapshotTable(uint8_t channelIndex, IdSnapshotTable *out); - static void setNormalMode(); - static void setSilentMode(); - static void onRx0Interrupt(); static void handleRxInterrupt(uint8_t channelIndex); static void handleTxInterrupt(uint8_t channelIndex); @@ -209,6 +198,15 @@ private: static bool initNominalTiming(uint32_t canPeriph); static bool initFdMode(uint32_t canPeriph); + static void printClockInfo(); + static void initChannelHardware(uint8_t i); + static void configureAllFilters(); + static void printInitResult(); + static void printErrorState(); + static void runSelfTest(); + static void runLoopbackTest(); + static void dumpRegisters(); + static Diag s_diag; static RxQueue s_rxQueues[3]; static uint8_t s_nextConsumeChannel; @@ -219,6 +217,4 @@ private: static IdSnapshotTable s_idSnapshot[3]; }; -using canfd_diag_t = CanFd::Diag; - #endif /* DEVICE_CANFD_H */ diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_bridge.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_bridge.cpp index 1dd7fe0..eeeaef6 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_bridge.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_bridge.cpp @@ -32,12 +32,7 @@ void postCanSendLog(AppSpiCommand &bus, const char *result, result ? result : "UNKNOWN"); bus.postLog(log_msg); #else - (void)bus; - (void)result; - (void)can_ch; - (void)can_id; - (void)can_data; - (void)can_dlc; + (void)bus; (void)result; (void)can_ch; (void)can_id; (void)can_data; (void)can_dlc; #endif } diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp index e94d076..9998db8 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_feedback.cpp @@ -50,14 +50,6 @@ static constexpr uint16_t kFeedbackPayloadLen = 123U; /* kTxBufSize=128 - over static constexpr uint8_t kPageEntries = 8U; static constexpr uint16_t kFixedHeaderLen = 28U; /* ch(1)+page(1)+total(1)+cnt(1)+mode(1)+seq(4)+rx(4)+tx(4)+drop(4)+err(4)+tec(1)+rec(1)+bus_state(1) */ -static void writeU32Le(uint8_t *dst, uint32_t value) -{ - dst[0] = static_cast(value & 0xFFU); - dst[1] = static_cast((value >> 8) & 0xFFU); - dst[2] = static_cast((value >> 16) & 0xFFU); - dst[3] = static_cast((value >> 24) & 0xFFU); -} - /** * @brief 构建一页 CAN 反馈 payload(与 ESP32 canFdPollFeedback 解析格式一致)。 * @param[in] channel 目标通道 0/1/2。 @@ -254,7 +246,6 @@ void canFeedbackPushStep() { static uint8_t s_channel = 0U; static uint8_t s_page = 0U; - static uint32_t s_pushCount = 0U; /* 已收到 0xB0 进入 Bootloader 请求,停止主动按铃避免干扰 OTA 流程。 * 复位前主循环仍在运行,但不再下拉 SPIready,让 ESP32 的 @@ -273,19 +264,12 @@ void canFeedbackPushStep() return; } - /* ESP32 尚未完成上一帧处理(未释放 SPIready),等待 EXTI6 上升沿。 - * 防止 GD32 在 ESP32 回到 IDLE 前按铃导致下降沿被 ISR 丢弃。 */ - if (!AppSpiCommandBus.canRingNow()) { - return; - } - uint8_t payload[kFeedbackPayloadLen] = {}; uint16_t payload_len = buildFeedbackPayload(s_channel, s_page, payload, sizeof(payload)); if (payload_len > 0U) { (void)AppSpiCommandBus.requestEsp32Read(SpiDefs::kTypeCanFeedback, payload, payload_len); - ++s_pushCount; /* 仅保留计数,不打印心跳日志 */ } /* 推进到下一页/下一通道,循环遍历 3 通道 × 2 页 */ diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_id_snapshot.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_id_snapshot.cpp index cdd9f49..0fd59f7 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_cmd_id_snapshot.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_id_snapshot.cpp @@ -30,14 +30,6 @@ namespace { constexpr uint8_t kPageEntries = 8U; constexpr uint16_t kMaxPayloadLen = 123U; /* kTxBufSize=128 - overhead=5 */ -inline void writeU32Le(uint8_t *dst, uint32_t val) -{ - dst[0] = static_cast(val & 0xFFU); - dst[1] = static_cast((val >> 8) & 0xFFU); - dst[2] = static_cast((val >> 16) & 0xFFU); - dst[3] = static_cast((val >> 24) & 0xFFU); -} - class SpiIdSnapshotHandler final : public ISpiCommandHandler { public: bool canHandle(uint8_t frame_type) const override diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp index 8d60c48..17050e5 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp @@ -1,955 +1,921 @@ -/** - * @file app_spi_command.cpp - * @brief APP 运行期 SPI 链路与命令路由实现。 - * - * 本模块现在只负责: - * - SPI0 从机链路初始化(GPIO/SPI/DMA/EXTI) - * - CS、DMA、中断驱动的帧接收与发送 - * - 公共帧格式解析与应答构建 - * - 业务命令的注册与分发 - * - * 具体命令处理逻辑已经拆到独立的 app_spi_cmd_*.cpp, - * 通过 registerHandler() 注册进来,后续新增 SPI 业务时 - * 只需要新增命令处理器,不再继续膨胀本文件。 - * - * 握手架构(v2 DATA_READY,PB6<->ESP32 GPIO10 开漏单线): - * - PB6 低 = 有待发数据(m_txLen>0 或 m_ringPending),高 = 队列空。 - * - 命令处理在 onCsRelease() EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame), - * 完成后由 updateDataReady() 依据待发数据驱动 PB6,不再延迟拉高。 - * - GD32 可通过 requestEsp32Read() 设 m_ringPending 并由 updateDataReady() - * 拉低 PB6 主动按铃,请求 ESP32 来读。 - */ - -#include "device/spi/app_spi_command.h" -#include "device/spi/spi_command_handler.h" -#include "device/spi/app_spi_command_defs.h" -#include "spi_link_v2.h" -#include "sys/ota_boot_request.h" - -extern "C" { -#include "gd32e50x_exti.h" -} -#include "gd32e50x_gpio.h" -#include "gd32e50x_misc.h" -#include "gd32e50x_rcu.h" -#include "gd32e50x_dma.h" -#include "gd32e50x_spi.h" -#include "sys/config.h" -#include "sys/critical.h" -#include "sys/task_manager.h" -#include "sys/systick.h" -#include "main.h" -#include -#include - -/* ============================================================ - * 本地别名:保持当前实现文件内部风格不变 - * ============================================================ */ -static constexpr uint8_t kTypeBootReq = SpiDefs::kTypeBootReq; -static constexpr uint8_t kTypeCanSend = SpiDefs::kTypeCanSend; -static constexpr uint8_t kTypeOtaReq = SpiDefs::kTypeOtaReq; -static constexpr uint8_t kTypeAppPing = SpiDefs::kTypeAppPing; -static constexpr uint8_t kTypeOtaData = SpiDefs::kTypeOtaData; -static constexpr uint8_t kTypeOtaCancel = SpiDefs::kTypeOtaCancel; -static constexpr uint8_t kRspAck = SpiDefs::kRspAck; -static constexpr uint8_t kRspWait = SpiDefs::kRspWait; -static constexpr uint8_t kRspError = SpiDefs::kRspError; - -/* DMA 全双工配置(与 BOOT SpiOta 一致): - * DMA0_CH1: SPI0_RX DMA0_CH2: SPI0_TX - * RX 走 DMA 后禁用 RBNE 逐字节中断,避免 256B 事务 RX-OVR/SOF 丢失。 */ -#define SPI_TX_DMA_PERIPH DMA0 -#define SPI_TX_DMA_CH DMA_CH2 -#define SPI_RX_DMA_PERIPH DMA0 -#define SPI_RX_DMA_CH DMA_CH1 - -static void startSpiTxDma(const uint8_t *buf, uint16_t len) -{ - DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; - DMA_INTC(SPI_TX_DMA_PERIPH) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE, SPI_TX_DMA_CH); - DMA_CHMADDR(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = (uint32_t)buf; - DMA_CHCNT(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = len; - DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) |= DMA_CHXCTL_CHEN; -} - -/** @brief 启动 DMA 接收 SPI0_DATA → m_rxBuf(容量 kRxBufSize)。 - * 每次 CS 空闲期预装,下一笔 CS 上升沿时由 stopSpiDmas() 读 CHCNT 反算实际长度。 */ -static void startSpiRxDma(uint8_t *rx_buf, uint16_t rx_cap) -{ - DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; - DMA_INTC(SPI_RX_DMA_PERIPH) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE, SPI_RX_DMA_CH); - DMA_CHMADDR(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) = (uint32_t)rx_buf; - DMA_CHCNT(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) = rx_cap; - DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) |= DMA_CHXCTL_CHEN; -} - -/** @brief 清空 SPI 接收通路残留(含 OVR),对齐 BOOT drainSpiRxHw。 */ -static void drainSpiRxHw(void) -{ - while (spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE) != RESET) { - (void)spi_i2s_data_receive(SPI0); - } - if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { - (void)spi_i2s_data_receive(SPI0); - (void)SPI_STAT(SPI0); - } -} - -/** @brief 停止双 DMA;返回本 CS 实际 RX 字节数(必须在 disable 前读 CHCNT)。 - * 对齐 BOOT stopSpiDmas:先读 remaining 再 disable,否则计数器被清。 */ -static uint16_t stopSpiDmas(uint16_t rx_cap) -{ - const uint16_t remaining = (uint16_t)DMA_CHCNT(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); - DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; - DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; - spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); - spi_dma_disable(SPI0, SPI_DMA_RECEIVE); - uint16_t got = 0U; - if (remaining <= rx_cap) { - got = (uint16_t)(rx_cap - remaining); - } - return got; -} - -static bool isAllBytesValue(const uint8_t *data, uint16_t len, uint8_t value) -{ - if (!data || len == 0U) { - return false; - } - for (uint16_t i = 0U; i < len; ++i) { - if (data[i] != value) { - return false; - } - } - return true; -} - -static bool shouldIgnoreTransaction(const uint8_t *data, uint16_t len) -{ - if (!data || len == 0U) { - return true; - } - - if (isAllBytesValue(data, len, 0xFFU) || isAllBytesValue(data, len, 0x00U)) { - return true; - } - - /* v2 协议帧最小长度为 V2_OVERHEAD(20B)。 - * 短于该长度的事务只能是启动切换期的残包,直接忽略。 - * (0xB0 进 BOOT 现已走标准 v2 帧 handler,不再有裸字节通道。) */ - return (len < V2_OVERHEAD); -} - -static bool isFlashRelatedFrame(uint8_t frame_type) -{ - switch (frame_type) { - case kTypeOtaReq: - case kTypeOtaData: - case kTypeOtaCancel: - case kTypeBootReq: - return true; - default: - return false; - } -} - -/* GD32 APP 侧同步状态机(ISR 与主循环只读/原子写,避免临界区开销)。 */ -static volatile uint8_t g_localCredit = 8U; -static volatile uint8_t g_remoteCredit = 8U; - -/* SPI Link v2 会话/序号状态机(ISR 写、主循环读写,均原子,无需临界区)。 */ -static volatile uint32_t g_sessionId = 0; /* v2 会话 ID(0=未建立) */ -static volatile uint8_t g_txSeq = 0; /* GD32 发送序号(事件/响应帧) */ -static volatile uint8_t g_expectedRxSeq = 0; /* 期望收到的 ESP32 请求序号 */ -static volatile bool g_v2Active = false; /* v2 session 是否激活 */ -/* 当前请求的 TX_SEQ,写入响应 ACK_SEQ,供 ESP32 做响应归属匹配(延迟一拍)。 */ -static volatile uint8_t g_pendingAckSeq = 0; - -/* 响应缓存:用于重复请求重发同一响应(避免业务 handler 重复执行)。 */ -static uint8_t s_lastReqSeq = 0xFF; -static uint8_t s_lastRespBuf[256]; -static uint16_t s_lastRespLen = 0; -static bool s_hasCachedResp = false; - -/* ============================================================ - * v2 帧构建 / 帧解析(委托 spi_link_v2.h,三工程共用) - * ============================================================ */ - -/* 构建响应帧;上层接口(type/payload)保持不变,handler 无需改动。 - * 内部调用 v2_build_frame,FLAGS 默认 V2_FLAG_RESPONSE,session/seq/credit - * 从 v2 状态全局变量取。session 未建立时 session_id 填 0。 - * ACK_SEQ = g_pendingAckSeq(对端请求的 TX_SEQ),不能写死 0,否则 ESP32 - * otaFindFrame 在 m_v2Active 时会把合法延迟响应当成 stale 丢弃。 */ -static bool spi_build_frame(uint8_t type, const uint8_t *payload, uint16_t payload_len, - uint8_t *out, uint16_t *out_len) -{ - if (!out || !out_len) { return false; } - if ((uint32_t)V2_OVERHEAD + payload_len > AppSpiCommand::kTxBufSize) { return false; } - - const uint32_t sess = g_v2Active ? g_sessionId : 0U; - size_t built = 0U; - const bool ok = v2_build_frame(type, V2_FLAG_RESPONSE, sess, - g_txSeq, g_pendingAckSeq, (uint8_t)g_localCredit, - payload, payload_len, out, &built); - if (!ok) { return false; } - *out_len = (uint16_t)built; - return true; -} - -/* 解析 v2 帧;委托 v2_parse_frame,扫描 SOF 并校验 VERSION/CRC32。 - * 旧的 0xA5 CRC8 帧兼容路径已移除(v2 不再向后兼容 v1)。 */ -static bool spi_try_parse_frame(const uint8_t *frame, uint16_t frame_len, - uint8_t *type, const uint8_t **payload, uint16_t *payload_len, - v2_header_t *out_hdr = nullptr) -{ - if (!frame || !type || !payload || !payload_len) { return false; } - - v2_header_t hdr; - const uint8_t *pload = nullptr; - const bool ok = v2_parse_frame(frame, frame_len, &hdr, &pload); - if (!ok) { return false; } - - g_remoteCredit = hdr.credit; - *type = hdr.type; - *payload = pload; - *payload_len = hdr.payload_len; - if (out_hdr) { *out_hdr = hdr; } - return true; -} - -/* ============================================================ - * 调试日志辅助 - * ============================================================ */ - -void AppSpiCommand::postLog(const char *msg) -{ -#if (CONFIG_SPI_APP_LOG_ENABLE != 0) - if (!msg) return; - snprintf(m_logBuf, kLogBufSize, "[SPI-APP] %s", msg); - m_logPending = true; -#else - (void)msg; -#endif -} - -void AppSpiCommand::postLogFmt(const char *fmt, uint8_t val) -{ -#if (CONFIG_SPI_APP_LOG_ENABLE != 0) - if (!fmt) return; - snprintf(m_logBuf, kLogBufSize, fmt, val); - m_logPending = true; -#else - (void)fmt; - (void)val; -#endif -} - -void AppSpiCommand::buildResponse(uint8_t *tx_buf, uint16_t tx_buf_size, - uint16_t *tx_len, uint8_t rsp_type, - const uint8_t *payload, uint16_t payload_len) -{ - if (!tx_buf || !tx_len || tx_buf_size == 0U) { - return; - } - - uint16_t built_len = 0U; - const bool ok = spi_build_frame(rsp_type, payload, payload_len, tx_buf, &built_len); - if (ok) { - *tx_len = built_len; - } -} - -/* ============================================================ - * 硬件配置 - * ============================================================ */ - -void AppSpiCommand::configureGpio() -{ - rcu_periph_clock_enable(RCU_GPIOA); - rcu_periph_clock_enable(RCU_GPIOB); - rcu_periph_clock_enable(RCU_AF); - - gpio_pin_remap_config(GPIO_SPI0_REMAP, ENABLE); - gpio_pin_remap_config(GPIO_SWJ_SWDPENABLE_REMAP, ENABLE); - - gpio_init(GPIOB, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_3 | GPIO_PIN_5); - gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4); - gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15); - - /* PB6 复用为 DATA_READY 握手线(开漏输出,线与)。 - * 开漏模式:写 BRR = 下拉驱动低;写 BSRR = 高阻释放,靠外部/对端上拉拉高。 - * 语义:低 = 有待发数据,高 = 队列空;默认释放(HIGH)。 */ - gpio_init(CONFIG_SPI_READY_PORT, GPIO_MODE_OUT_OD, GPIO_OSPEED_50MHZ, CONFIG_SPI_READY_PIN); - gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); -} - -void AppSpiCommand::setSpiReady(bool ready) -{ - /* 开漏线与:ready=true 释放(BSRR,高阻,靠上拉拉高); - * ready=false 下拉(BRR,驱动低)。 */ - if (ready) { - gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); - } else { - gpio_bit_reset(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); - } -} - -/* v2 DATA_READY: PB6 低 = 有待发数据,高 = 队列空。 - * L5:同步 g_localCredit(单缓冲 0/1)。 */ -void AppSpiCommand::updateDataReady() -{ - const bool has_data = (m_txLen > 0U) || m_ringPending; - setSpiReady(!has_data); - g_localCredit = has_data ? 0U : 1U; -} - -void AppSpiCommand::configureSpi0() -{ - rcu_periph_clock_enable(RCU_SPI0); - spi_i2s_deinit(SPI0); - (void)spi_i2s_data_receive(SPI0); /* 清空 SPI_DATA 残留值 */ - - spi_parameter_struct spi_init_struct; - spi_struct_para_init(&spi_init_struct); - spi_init_struct.device_mode = SPI_SLAVE; - spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX; - spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; - spi_init_struct.nss = SPI_NSS_SOFT; - spi_init_struct.endian = SPI_ENDIAN_MSB; - spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE; - spi_init_struct.prescale = SPI_PSC_8; - spi_init(SPI0, &spi_init_struct); - - /* 内部 NSS 常低:SPI 从机始终就绪,不依赖 EXTI ISR 切换 internal NSS。 - * PA15 仅作 EXTI 事务边界信号,用于 onCsAssert/onCsRelease 控制 DMA 装载。 */ - spi_nss_internal_low(SPI0); - (void)spi_i2s_data_receive(SPI0); - spi_i2s_data_transmit(SPI0, 0xFFU); - /* RX 改走 DMA0_CH1:禁用 RBNE 逐字节中断,避免 256B 事务 RX-OVR/SOF 丢失。 */ - spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_RBNE); - spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_ERR); - spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_TBE); - nvic_irq_enable(SPI0_IRQn, 2U, 0U); - spi_enable(SPI0); -} - -void AppSpiCommand::configureDma() -{ - rcu_periph_clock_enable(RCU_DMA0); - - /* TX: memory → SPI0_DATA */ - dma_deinit(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); - dma_parameter_struct dma_cfg; - dma_struct_para_init(&dma_cfg); - dma_cfg.periph_addr = (uint32_t)(SPI0 + 0x0CU); /* SPI0_DATA 寄存器 */ - dma_cfg.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; - dma_cfg.memory_addr = (uint32_t)m_txBuf; - dma_cfg.memory_width = DMA_MEMORY_WIDTH_8BIT; - dma_cfg.number = 0U; - dma_cfg.priority = DMA_PRIORITY_HIGH; - dma_cfg.periph_inc = DMA_PERIPH_INCREASE_DISABLE; - dma_cfg.memory_inc = DMA_MEMORY_INCREASE_ENABLE; - dma_cfg.direction = DMA_MEMORY_TO_PERIPHERAL; - dma_init(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH, &dma_cfg); - dma_circulation_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); - dma_channel_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); - - /* RX: SPI0_DATA → m_rxBuf(替代旧 RBNE 逐字节累积)。 - * 与 BOOT SpiOta 对齐:DMA0_CH1 固定映射 SPI0_RX。 */ - dma_deinit(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); - dma_struct_para_init(&dma_cfg); - dma_cfg.periph_addr = (uint32_t)(SPI0 + 0x0CU); - dma_cfg.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; - dma_cfg.memory_addr = (uint32_t)m_rxBuf; - dma_cfg.memory_width = DMA_MEMORY_WIDTH_8BIT; - dma_cfg.number = kRxBufSize; - dma_cfg.priority = DMA_PRIORITY_HIGH; - dma_cfg.periph_inc = DMA_PERIPH_INCREASE_DISABLE; - dma_cfg.memory_inc = DMA_MEMORY_INCREASE_ENABLE; - dma_cfg.direction = DMA_PERIPHERAL_TO_MEMORY; - dma_init(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH, &dma_cfg); - dma_circulation_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); - dma_channel_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); -} - -void AppSpiCommand::configureExti() -{ - gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15); - exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH); - exti_interrupt_enable(EXTI_15); - exti_interrupt_flag_clear(EXTI_15); - nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U); -} - - - -/* ============================================================ - * 默认命令注册入口(由各 app_spi_cmd_*.cpp 提供) - * ============================================================ */ -extern void registerSpiBootRequestCommand(AppSpiCommand &bus); -extern void registerSpiOtaWaitCommand(AppSpiCommand &bus); -extern void registerSpiAppPingCommand(AppSpiCommand &bus); -extern void registerSpiCanBridgeCommand(AppSpiCommand &bus); -extern void registerSpiCanFeedbackCommand(AppSpiCommand &bus); -extern void registerSpiIdSnapshotCommand(AppSpiCommand &bus); -AppSpiCommand AppSpiCommandBus; -void AppSpiCommand::init() -{ - m_csActive = false; - m_rxLen = 0U; - m_txLen = 0U; - m_txPos = 0U; - m_logPending = false; - m_txActiveThisCs = false; - m_commandCount = 0U; - m_ringPending = false; - m_ringDeadline = 0U; - m_ringTxLen = 0U; - m_linkState.reset(); - memset(m_txBuf, 0xFF, sizeof(m_txBuf)); - memset(m_logBuf, 0, sizeof(m_logBuf)); - memset(m_commands, 0, sizeof(m_commands)); - - registerSpiBootRequestCommand(AppSpiCommandBus); - registerSpiOtaWaitCommand(AppSpiCommandBus); - registerSpiAppPingCommand(AppSpiCommandBus); - registerSpiCanBridgeCommand(AppSpiCommandBus); - registerSpiCanFeedbackCommand(AppSpiCommandBus); - registerSpiIdSnapshotCommand(AppSpiCommandBus); - - configureGpio(); - configureDma(); - configureSpi0(); - configureExti(); - /* 首笔 CS 到来前预装 DMA,避免 EXTI 晚于首 SCK。 - * 必须在 configureSpi0/configureDma 之后调用。 */ - armSpiDmasForNextCs(); -} - -/** @brief 在 CS 空闲期预装下一笔全双工 DMA。 - * - * 必须在首个 SCK 之前完成:EXTI CS 下降相对 ESP32 时钟经常偏晚, - * 若在 onCsAssert 里才 start DMA,整笔事务会收 0 字节 → 永不 HELLO_ACK → MISO 全 0xFF。 - * - * TX 始终按 kTxBufSize 装载:响应帧在头部,其余 0xFF。 - * 若只 DMA 实际帧长(~26B),主机打 96 时钟时 TX underrun,后续 MISO 不定。 - * - * 对齐 BOOT SpiOta::armSpiDmasForNextCs。 - */ -void AppSpiCommand::armSpiDmasForNextCs() -{ - DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; - DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; - spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); - spi_dma_disable(SPI0, SPI_DMA_RECEIVE); - drainSpiRxHw(); - - /* 预填 TX:有挂起响应用响应帧(头部)+ 0xFF 填充到满缓冲 */ - if (m_txLen > 0U && m_txLen <= kTxBufSize) { - if (m_txLen < kTxBufSize) { - memset(&m_txBuf[m_txLen], 0xFF, (size_t)(kTxBufSize - m_txLen)); - } - } else { - m_txLen = 0U; - memset(m_txBuf, 0xFF, kTxBufSize); - } - - /* RX 缓冲清零(便于诊断时区分有效字节与残留) */ - memset(m_rxBuf, 0x00, kRxBufSize); - m_rxLen = 0U; - - /* 预装 SPI_DATA,避免 DMA 启动前首拍 TBE 吐旧值 */ - spi_i2s_data_transmit(SPI0, m_txBuf[0]); - - /* 先使能 SPI DMA 请求,再开 DMA 通道,保证首字节即可搬运 */ - spi_dma_enable(SPI0, SPI_DMA_RECEIVE); - spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); - startSpiRxDma(m_rxBuf, kRxBufSize); - /* TX 永远满缓冲长度,与主机 96~256B 事务匹配,避免 underrun */ - startSpiTxDma(m_txBuf, kTxBufSize); -} - -/* ============================================================ - * 命令路由 - * ============================================================ */ - -void AppSpiCommand::registerHandler(ISpiCommandHandler *handler) -{ - if (!handler || m_commandCount >= kMaxCommands) { - return; - } - m_commands[m_commandCount++] = handler; -} - -void AppSpiCommand::processCommand() -{ - if (m_rxLen == 0U) { - return; - } - - /* 0xB0 已受理、正在写 OTA 配置页 / 即将复位:不再处理业务帧,避免打断 Flash。 */ - if (OtaBootRequest::isSpiFrozenForBoot() || OtaBootRequest::isRebootPending()) { - m_txLen = 0U; - m_txPos = 0U; - updateDataReady(); - return; - } - - /* 尝试按 v2 协议帧解析;同时取回完整帧头用于 session/seq 校验 */ - uint8_t frame_type = 0U; - const uint8_t *payload = nullptr; - uint16_t payload_len = 0U; - v2_header_t hdr; - - if (!spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr)) { - /* 无法解析为合法 v2 帧:构建 ERROR 响应 */ - postLogFmt("[SPI-APP] 非v2帧 0x%02X -> ERROR", m_rxBuf[0]); - uint16_t tx_len = 0U; - uint8_t err = 0xEEU; - spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); - m_txLen = tx_len; - m_txPos = 0U; - - if (m_logPending) { - TaskManager::set(1UL << TASK_SPI_LOG); - } - if (m_txLen > 0U) { - m_ringPending = true; - m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; - } - updateDataReady(); - return; - } - - if (CONFIG_SPI_APP_LOG_ENABLE != 0 && !isFlashRelatedFrame(frame_type)) { - postLogFmt("[SPI-APP] 收到非刷写 SPI type=0x%02X", frame_type); - } - - /* 响应归属:ACK_SEQ 必须回显请求 TX_SEQ(延迟一拍 + ESP32 otaFindFrame 匹配)。 */ - g_pendingAckSeq = hdr.tx_seq; - - /* ---------- HELLO 建链路处理(在 session 校验之前) ---------- */ - if (frame_type == V2_TYPE_LINK_HELLO) { - /* 接受 ESP32 提供的 session_id,重置本端序号状态机 */ - g_sessionId = hdr.session_id; - g_txSeq = 0; - g_expectedRxSeq = 0; - g_v2Active = true; - s_hasCachedResp = false; - - /* 构建 HELLO_ACK 响应帧(回显 session_id + 角色 + credit_max) */ - size_t built_len = 0U; - v2_build_hello_ack_frame(g_sessionId, V2_ROLE_APP, (uint8_t)g_localCredit, - 0, hdr.tx_seq, m_txBuf, &built_len); - m_txLen = (uint16_t)built_len; - m_txPos = 0U; - - /* 缓存 HELLO_ACK 便于重复 HELLO 重发 */ - s_lastReqSeq = hdr.tx_seq; - memcpy(s_lastRespBuf, m_txBuf, m_txLen); - s_lastRespLen = m_txLen; - s_hasCachedResp = true; - - if (m_logPending) { - TaskManager::set(1UL << TASK_SPI_LOG); - } - if (m_txLen > 0U) { - m_ringPending = true; - m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; - } - updateDataReady(); - return; - } - - /* ---------- SESSION / SEQ 校验(仅在 v2 session 激活后生效) ---------- */ - if (g_v2Active) { - /* session_id 不匹配:构建 LINK_ERROR(V2_ERR_SESSION),跳过 handler */ - if (hdr.session_id != g_sessionId) { - postLogFmt("[SPI-APP] session 不匹配 0x%02X -> LINK_ERROR", - (uint8_t)(hdr.session_id & 0xFFU)); - size_t built_len = 0U; - v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq, - (uint8_t)g_localCredit, V2_ERR_SESSION, - (uint8_t)g_expectedRxSeq, m_txBuf, &built_len); - m_txLen = (uint16_t)built_len; - m_txPos = 0U; - - if (m_logPending) { - TaskManager::set(1UL << TASK_SPI_LOG); - } - if (m_txLen > 0U) { - m_ringPending = true; - m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; - } - updateDataReady(); - return; - } - - /* 序号回绕距离判定 */ - const uint8_t dist = v2_seq_distance(hdr.tx_seq, g_expectedRxSeq); - if (dist == 0U) { - /* 正常:序号匹配,推进期望序号 */ - g_expectedRxSeq = (uint8_t)(g_expectedRxSeq + 1U); - } else if (dist >= 128U) { - /* 重复请求:重发缓存的响应帧,跳过 handler */ - if (s_hasCachedResp && hdr.tx_seq == s_lastReqSeq) { - postLogFmt("[SPI-APP] 重复请求 seq=0x%02X -> 重发缓存", hdr.tx_seq); - memcpy(m_txBuf, s_lastRespBuf, s_lastRespLen); - m_txLen = s_lastRespLen; - m_txPos = 0U; - - if (m_logPending) { - TaskManager::set(1UL << TASK_SPI_LOG); - } - if (m_txLen > 0U) { - m_ringPending = true; - m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; - } - updateDataReady(); - return; - } - /* 无缓存可用:按正常请求处理 */ - } else { - /* dist 1-127:序号跳变。对齐期望序号后: - * - 普通业务:回 LINK_ERROR 并丢弃(避免副作用) - * - 0xB0 ENTER_BOOT:控制面命令,仍必须执行,否则永远进不了 BOOT */ - postLogFmt("[SPI-APP] SEQ jump rx=0x%02X", hdr.tx_seq); - g_expectedRxSeq = (uint8_t)(hdr.tx_seq + 1U); - - if (frame_type != kTypeBootReq) { - size_t built_len = 0U; - v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq, - (uint8_t)g_localCredit, V2_ERR_SEQ_JUMP, - (uint8_t)g_expectedRxSeq, m_txBuf, &built_len); - m_txLen = (uint16_t)built_len; - m_txPos = 0U; - - if (m_logPending) { - TaskManager::set(1UL << TASK_SPI_LOG); - } - if (m_txLen > 0U) { - m_ringPending = true; - m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; - } - updateDataReady(); - return; - } - postLog("SEQ jump but BOOT_REQ still handled"); - /* fall through → 业务分发 */ - } - } - - /* ---------- 业务命令分发 ---------- */ - uint16_t tx_len = 0U; - bool handled = false; - - SpiCommandContext ctx{}; - ctx.frame_type = frame_type; - ctx.payload = payload; - ctx.payload_len = payload_len; - ctx.tx_buf = m_txBuf; - ctx.tx_buf_size = kTxBufSize; - ctx.tx_len = &tx_len; - - for (uint8_t i = 0U; i < m_commandCount; ++i) { - ISpiCommandHandler *handler = m_commands[i]; - if (handler && handler->canHandle(frame_type)) { - handler->handle(AppSpiCommandBus, ctx); - handled = true; - break; - } - } - - if (!handled) { - postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type); - uint8_t err = 0xEEU; - spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); - } - - if (tx_len == 0U) { - /* 兜底:避免残留上一次应答帧 */ - uint8_t err = 0xEEU; - spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); - } - - m_txLen = tx_len; - m_txPos = 0U; - - /* 缓存本次响应,便于重复请求时重发(避免 handler 重复执行副作用) */ - if (g_v2Active) { - s_lastReqSeq = hdr.tx_seq; - memcpy(s_lastRespBuf, m_txBuf, m_txLen); - s_lastRespLen = m_txLen; - s_hasCachedResp = true; - /* 推进本端发送序号(响应帧占用一个 tx_seq 槽位) */ - g_txSeq = (uint8_t)(g_txSeq + 1U); - } - - if (m_logPending) { - /* 置位 TASK_SPI_LOG,唤醒主循环立刻打印日志 */ - TaskManager::set(1UL << TASK_SPI_LOG); - } - - /* 应答帧已就绪:由 updateDataReady() 依据 m_txLen 驱动 PB6(有待发=低)。 - * 命令处理已在 ISR 内同步完成,无需 m_cmdPending 推迟机制。 */ - - /* 有响应待发时,设置按铃标志阻止主循环 canFeedbackPushStep() 覆盖 m_txBuf。 - * ESP32 下次 CS assert 时 DMA 输出响应,onCsRelease 清除 m_ringPending。 - * 50ms 超时后 cancelRing() 清除,恢复 CAN 反馈推送。 */ - if (m_txLen > 0U) { - m_ringPending = true; - m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; - } - updateDataReady(); -} - -/* ============================================================ - * GD32 主动按铃:设 m_ringPending + updateDataReady() 拉低 PB6 请求 ESP32 来读 - * ============================================================ */ - -void AppSpiCommand::cancelRing() -{ - m_ringPending = false; - m_ringTxLen = 0U; - /* 清除待发响应,防止超时后残留帧在下一次 CS assert 时被错误输出 */ - m_txLen = 0U; - m_txPos = 0U; - /* 超时后允许重试,不依赖 ESP32 releaseReady 上升沿 */ - m_linkState.onRingTimeout(); - /* 释放 SPIready(高阻),避免永久占用线。 - * cancelRing 已清 m_txLen 和 m_ringPending,updateDataReady() 会释放为高。 */ - updateDataReady(); -} - -bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len) -{ - /* 若上一次按铃尚未被 ESP32 读走,拒绝重复按铃 */ - if (m_ringPending) { - return false; - } - - /* 构建 v2 EVENT 帧到 m_txBuf(ESP32 下次读事务时通过 DMA 输出到 MISO)。 - * 主动按铃帧使用 V2_FLAG_EVENT,session/seq 从 v2 状态全局变量取; - * session 未建立时 session_id 填 0。 */ - const uint32_t sess = g_v2Active ? g_sessionId : 0U; - /* L5:单缓冲 — 装载前 credit 报 1,装载后 updateDataReady 置 0 */ - g_localCredit = 1U; - size_t built_len = 0U; - if (!v2_build_frame(frame_type, V2_FLAG_EVENT, sess, - g_txSeq, 0U, (uint8_t)g_localCredit, - payload, payload_len, m_txBuf, &built_len)) { - return false; - } - /* EVENT 帧占用一个发送序号槽位 */ - if (g_v2Active) { - g_txSeq = (uint8_t)(g_txSeq + 1U); - } - - m_ringTxLen = (uint16_t)built_len; - m_txLen = (uint16_t)built_len; - m_txPos = 0U; - m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; - m_ringPending = true; - /* 消费完成 flag,等 ESP32 releaseReady 触发 EXTI6 上升沿 */ - m_linkState.onRingStart(); - - /* v2 DATA_READY: 已设 m_ringPending=true 且 m_txLen>0, - * updateDataReady() 据此拉低 PB6,并置 g_localCredit=0。 */ - updateDataReady(); - return true; -} - -/* ============================================================ - * 主循环轮询:打印延迟日志 + 按铃超时看护 - * ============================================================ */ - -void AppSpiCommand::poll() -{ - /* 打印 ISR 推迟的日志 */ - char log_copy[kLogBufSize] = {}; - bool has_log = false; - - uint32_t save = RT_CRITICAL_SAVE(); - if (m_logPending) { - memcpy(log_copy, m_logBuf, sizeof(log_copy)); - m_logPending = false; - has_log = true; - } - RT_CRITICAL_RESTORE(save); - - if (has_log) { - printf("%s\r\n", log_copy); - } - - /* 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。 - * 防止 ESP32 不响应时永久占用 SPIready 线导致双方死锁。 */ - if (m_ringPending) { - uint32_t save2 = RT_CRITICAL_SAVE(); - if (m_ringPending) { - if ((int32_t)(SysTickTimer::getMs() - m_ringDeadline) >= 0) { - printf("[SPI-APP] 按铃超时取消 (ESP32 未响应, 释放 SPIready 重试)\r\n"); - cancelRing(); - } - } - RT_CRITICAL_RESTORE(save2); - } -} - -/* ============================================================ - * CS 边沿处理 - * ============================================================ */ - -void AppSpiCommand::onCsAssert() -{ - if (m_csActive) { - return; - } - m_csActive = true; - /* CS 下降:DMA 已在空闲期由 armSpiDmasForNextCs() 预装,此处只标记忙碌。 - * v2 DATA_READY: CS 本身即互斥,PB6 不再表示「忙碌」,不在 CS 下降沿拉低。 - * 切勿在这里 stop/restart DMA,否则赶不上 ESP32 已发出的 SCK(对齐 BOOT)。 */ - m_txPos = 0U; - m_txActiveThisCs = false; -} - -void AppSpiCommand::onCsRelease() -{ - if (!m_csActive) { - /* EXTI 丢失下降沿时:仍尝试收尾并 re-arm,避免永久失步(对齐 BOOT) */ - m_rxLen = stopSpiDmas(kRxBufSize); - drainSpiRxHw(); - if (m_txLen > 0U) { - m_txLen = 0U; - m_txPos = 0U; - memset(m_txBuf, 0xFF, sizeof(m_txBuf)); - } - if (m_rxLen >= V2_OVERHEAD) { - processCommand(); - } - armSpiDmasForNextCs(); - updateDataReady(); - return; - } - m_csActive = false; - - /* 停 DMA 并取本笔 RX 长度(先读 CHCNT 再 disable,否则计数器被清) */ - m_rxLen = stopSpiDmas(kRxBufSize); - drainSpiRxHw(); - - /* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧, - * 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。 - * - * 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 v2 协议帧(如 PING/OTA/0xB0)。 - * 若 GD32 正在按铃时 ESP32 发协议帧,会因 m_ringPending=true 被误吞, - * 导致命令无法处理。检测到 v2 SOF 时取消按铃并走正常命令处理。 */ - if (m_ringPending) { - /* 判断本次收到的数据是否为 v2 协议帧(扫描 SOF0 SOF1)。 - * 取代旧 containsSyncSof,直接用 V2_SOF0/V2_SOF1。 - * 0xB0 进 BOOT 现已走标准 v2 帧 handler,不再单独检测裸字节。 */ - bool is_v2_frame = false; - if (m_rxLen >= 2U) { - const uint16_t last = (m_rxLen < 16U) ? (uint16_t)(m_rxLen - 1U) : 16U; - for (uint16_t i = 0U; i < last; ++i) { - if (m_rxBuf[i] == V2_SOF0 && m_rxBuf[i + 1U] == V2_SOF1) { - is_v2_frame = true; - break; - } - } - } - if (is_v2_frame) { - /* ESP32 发的是 v2 协议帧(如 PING/OTA/0xB0 等),不是按铃响应。 - * 取消按铃,清空 TX 让 processCommand() 重新装载对应应答。 - * processCommand() 末尾由 updateDataReady() 依据新 m_txLen 驱动 PB6。 */ - m_ringPending = false; - m_ringTxLen = 0U; - m_txLen = 0U; - m_txPos = 0U; - memset(m_txBuf, 0xFF, sizeof(m_txBuf)); - /* 继续走 shouldIgnoreTransaction + processCommand(),不 return */ - } else { - /* 正常按铃响应:请求帧已被读走,取消按铃状态 */ - m_ringPending = false; - m_ringTxLen = 0U; - /* 请求帧已被读走,清空 TX 防止下次事务重复输出 */ - m_txLen = 0U; - m_txPos = 0U; - memset(m_txBuf, 0xFF, sizeof(m_txBuf)); - /* v2 DATA_READY: 已清 m_txLen=0 与 m_ringPending=false, - * updateDataReady() 释放为高;ESP32 完成 CS 即视为已读走确认。 */ - m_linkState.onEsp32Complete(); - /* 重新预装下一笔 DMA,保证下一 CS 首字节即可收发 */ - armSpiDmasForNextCs(); - updateDataReady(); - return; - } - } - - if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) { - m_rxLen = 0U; - if (m_txActiveThisCs) { - m_txLen = 0U; - m_txPos = 0U; - memset(m_txBuf, 0xFF, sizeof(m_txBuf)); - } - m_txActiveThisCs = false; - /* v2 DATA_READY: shouldIgnoreTransaction 路径已清 m_txLen=0, - * updateDataReady() 释放为高,避免 PB6 卡 LOW。 */ - armSpiDmasForNextCs(); - updateDataReady(); - return; - } - - m_txLen = 0U; - m_txPos = 0U; - m_txActiveThisCs = false; - memset(m_txBuf, 0xFF, sizeof(m_txBuf)); - - /* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式), - * 消除主循环延迟。处理完成后 processCommand() 末尾再 re-arm DMA + 释放 SPIready。 */ - processCommand(); - - /* 处理完立即预装下一笔,保证下一 CS 首字节即可收发 */ - armSpiDmasForNextCs(); - updateDataReady(); -} - -/* ============================================================ - * SPI0 / EXTI 中断处理 - * ============================================================ */ - -void AppSpiCommand::onInterrupt() -{ - /* RX 已改走 DMA0_CH1:RBNE 逐字节中断已禁用,本 ISR 只保留错误标志清理。 - * CS 边沿处理全部在 onCsExtiInterrupt 的 EXTI15 ISR 里完成。 */ - if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { - /* RXORERR 理论上不应再出现(RX 走 DMA),但兜底清理避免标志卡死。 - * 读 SPI_DATA + SPI_STAT 清除 OVR 标志。 */ - (void)spi_i2s_data_receive(SPI0); - (void)SPI_STAT(SPI0); - } - if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { - spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); - } -} - -void AppSpiCommand::onCsExtiInterrupt() -{ - if (exti_interrupt_flag_get(EXTI_15) != RESET) { - exti_interrupt_flag_clear(EXTI_15); - if (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) { - onCsAssert(); - } else { - onCsRelease(); - } - } -} - -extern "C" void SPI0_IRQHandler(void) -{ - AppSpiCommandBus.onInterrupt(); -} - -extern "C" void EXTI10_15_IRQHandler(void) -{ - AppSpiCommandBus.onCsExtiInterrupt(); -} +/** + * @file app_spi_command.cpp + * @brief APP 运行期 SPI 链路与命令路由实现。 + * + * 本模块现在只负责: + * - SPI0 从机链路初始化(GPIO/SPI/DMA/EXTI) + * - CS、DMA、中断驱动的帧接收与发送 + * - 公共帧格式解析与应答构建 + * - 业务命令的注册与分发 + * + * 具体命令处理逻辑已经拆到独立的 app_spi_cmd_*.cpp, + * 通过 registerHandler() 注册进来,后续新增 SPI 业务时 + * 只需要新增命令处理器,不再继续膨胀本文件。 + * + * 握手架构(v2 DATA_READY,PB6<->ESP32 GPIO10 开漏单线): + * - PB6 低 = 有待发数据(m_txLen>0 或 m_ringPending),高 = 队列空。 + * - 命令处理在 onCsRelease() EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame), + * 完成后由 updateDataReady() 依据待发数据驱动 PB6,不再延迟拉高。 + * - GD32 可通过 requestEsp32Read() 设 m_ringPending 并由 updateDataReady() + * 拉低 PB6 主动按铃,请求 ESP32 来读。 + */ + +#include "device/spi/app_spi_command.h" +#include "device/spi/spi_command_handler.h" +#include "device/spi/app_spi_command_defs.h" +#include "spi_link_v2.h" +#include "sys/ota_boot_request.h" + +extern "C" { +#include "gd32e50x_exti.h" +} +#include "gd32e50x_gpio.h" +#include "gd32e50x_misc.h" +#include "gd32e50x_rcu.h" +#include "gd32e50x_dma.h" +#include "gd32e50x_spi.h" +#include "sys/config.h" +#include "sys/critical.h" +#include "sys/task_manager.h" +#include "sys/systick.h" +#include "main.h" +#include +#include + +/* ============================================================ + * 本地别名:保持当前实现文件内部风格不变 + * ============================================================ */ +static constexpr uint8_t kTypeBootReq = SpiDefs::kTypeBootReq; +static constexpr uint8_t kTypeOtaReq = SpiDefs::kTypeOtaReq; +static constexpr uint8_t kTypeOtaData = SpiDefs::kTypeOtaData; +static constexpr uint8_t kTypeOtaCancel = SpiDefs::kTypeOtaCancel; +static constexpr uint8_t kRspError = SpiDefs::kRspError; + +/* DMA 全双工配置(与 BOOT SpiOta 一致): + * DMA0_CH1: SPI0_RX DMA0_CH2: SPI0_TX + * RX 走 DMA 后禁用 RBNE 逐字节中断,避免 256B 事务 RX-OVR/SOF 丢失。 */ +#define SPI_TX_DMA_PERIPH DMA0 +#define SPI_TX_DMA_CH DMA_CH2 +#define SPI_RX_DMA_PERIPH DMA0 +#define SPI_RX_DMA_CH DMA_CH1 + +static void startSpiTxDma(const uint8_t *buf, uint16_t len) +{ + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + DMA_INTC(SPI_TX_DMA_PERIPH) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE, SPI_TX_DMA_CH); + DMA_CHMADDR(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = (uint32_t)buf; + DMA_CHCNT(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) = len; + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) |= DMA_CHXCTL_CHEN; +} + +/** @brief 启动 DMA 接收 SPI0_DATA → m_rxBuf(容量 kRxBufSize)。 + * 每次 CS 空闲期预装,下一笔 CS 上升沿时由 stopSpiDmas() 读 CHCNT 反算实际长度。 */ +static void startSpiRxDma(uint8_t *rx_buf, uint16_t rx_cap) +{ + DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + DMA_INTC(SPI_RX_DMA_PERIPH) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE, SPI_RX_DMA_CH); + DMA_CHMADDR(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) = (uint32_t)rx_buf; + DMA_CHCNT(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) = rx_cap; + DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) |= DMA_CHXCTL_CHEN; +} + +/** @brief 清空 SPI 接收通路残留(含 OVR),对齐 BOOT drainSpiRxHw。 */ +static void drainSpiRxHw(void) +{ + while (spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE) != RESET) { + (void)spi_i2s_data_receive(SPI0); + } + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { + (void)spi_i2s_data_receive(SPI0); + (void)SPI_STAT(SPI0); + } +} + +/** @brief 停止双 DMA;返回本 CS 实际 RX 字节数(必须在 disable 前读 CHCNT)。 + * 对齐 BOOT stopSpiDmas:先读 remaining 再 disable,否则计数器被清。 */ +static uint16_t stopSpiDmas(uint16_t rx_cap) +{ + const uint16_t remaining = (uint16_t)DMA_CHCNT(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); + spi_dma_disable(SPI0, SPI_DMA_RECEIVE); + uint16_t got = 0U; + if (remaining <= rx_cap) { + got = (uint16_t)(rx_cap - remaining); + } + return got; +} + +static bool isAllBytesValue(const uint8_t *data, uint16_t len, uint8_t value) +{ + if (!data || len == 0U) { + return false; + } + for (uint16_t i = 0U; i < len; ++i) { + if (data[i] != value) { + return false; + } + } + return true; +} + +static bool shouldIgnoreTransaction(const uint8_t *data, uint16_t len) +{ + if (!data || len == 0U) { + return true; + } + + if (isAllBytesValue(data, len, 0xFFU) || isAllBytesValue(data, len, 0x00U)) { + return true; + } + + /* v2 协议帧最小长度为 V2_OVERHEAD(20B)。 + * 短于该长度的事务只能是启动切换期的残包,直接忽略。 + * (0xB0 进 BOOT 现已走标准 v2 帧 handler,不再有裸字节通道。) */ + return (len < V2_OVERHEAD); +} + +static bool isFlashRelatedFrame(uint8_t frame_type) +{ + switch (frame_type) { + case kTypeOtaReq: + case kTypeOtaData: + case kTypeOtaCancel: + case kTypeBootReq: + return true; + default: + return false; + } +} + +/* GD32 APP 侧同步状态机(ISR 与主循环只读/原子写,避免临界区开销)。 */ +static volatile uint8_t g_localCredit = 8U; +static volatile uint8_t g_remoteCredit = 8U; + +/* SPI Link v2 会话/序号状态机(ISR 写、主循环读写,均原子,无需临界区)。 */ +static volatile uint32_t g_sessionId = 0; /* v2 会话 ID(0=未建立) */ +static volatile uint8_t g_txSeq = 0; /* GD32 发送序号(事件/响应帧) */ +static volatile uint8_t g_expectedRxSeq = 0; /* 期望收到的 ESP32 请求序号 */ +static volatile bool g_v2Active = false; /* v2 session 是否激活 */ +/* 当前请求的 TX_SEQ,写入响应 ACK_SEQ,供 ESP32 做响应归属匹配(延迟一拍)。 */ +static volatile uint8_t g_pendingAckSeq = 0; + +/* 响应缓存:用于重复请求重发同一响应(避免业务 handler 重复执行)。 */ +static uint8_t s_lastReqSeq = 0xFF; +static uint8_t s_lastRespBuf[256]; +static uint16_t s_lastRespLen = 0; +static bool s_hasCachedResp = false; + +/* ============================================================ + * v2 帧构建 / 帧解析(委托 spi_link_v2.h,三工程共用) + * ============================================================ */ + +/* 构建响应帧;上层接口(type/payload)保持不变,handler 无需改动。 + * 内部调用 v2_build_frame,FLAGS 默认 V2_FLAG_RESPONSE,session/seq/credit + * 从 v2 状态全局变量取。session 未建立时 session_id 填 0。 + * ACK_SEQ = g_pendingAckSeq(对端请求的 TX_SEQ),不能写死 0,否则 ESP32 + * otaFindFrame 在 m_v2Active 时会把合法延迟响应当成 stale 丢弃。 */ +static bool spi_build_frame(uint8_t type, const uint8_t *payload, uint16_t payload_len, + uint8_t *out, uint16_t *out_len) +{ + if (!out || !out_len) { return false; } + if ((uint32_t)V2_OVERHEAD + payload_len > AppSpiCommand::kTxBufSize) { return false; } + + const uint32_t sess = g_v2Active ? g_sessionId : 0U; + size_t built = 0U; + const bool ok = v2_build_frame(type, V2_FLAG_RESPONSE, sess, + g_txSeq, g_pendingAckSeq, (uint8_t)g_localCredit, + payload, payload_len, out, &built); + if (!ok) { return false; } + *out_len = (uint16_t)built; + return true; +} + +/* 解析 v2 帧;委托 v2_parse_frame,扫描 SOF 并校验 VERSION/CRC32。 + * 旧的 0xA5 CRC8 帧兼容路径已移除(v2 不再向后兼容 v1)。 */ +static bool spi_try_parse_frame(const uint8_t *frame, uint16_t frame_len, + uint8_t *type, const uint8_t **payload, uint16_t *payload_len, + v2_header_t *out_hdr = nullptr) +{ + if (!frame || !type || !payload || !payload_len) { return false; } + + v2_header_t hdr; + const uint8_t *pload = nullptr; + const bool ok = v2_parse_frame(frame, frame_len, &hdr, &pload); + if (!ok) { return false; } + + g_remoteCredit = hdr.credit; + *type = hdr.type; + *payload = pload; + *payload_len = hdr.payload_len; + if (out_hdr) { *out_hdr = hdr; } + return true; +} + +/* ============================================================ + * 调试日志辅助 + * ============================================================ */ + +void AppSpiCommand::postLog(const char *msg) +{ +#if (CONFIG_SPI_APP_LOG_ENABLE != 0) + if (!msg) return; + snprintf(m_logBuf, kLogBufSize, "[SPI-APP] %s", msg); + m_logPending = true; +#else + (void)msg; +#endif +} + +void AppSpiCommand::postLogFmt(const char *fmt, uint8_t val) +{ +#if (CONFIG_SPI_APP_LOG_ENABLE != 0) + if (!fmt) return; + snprintf(m_logBuf, kLogBufSize, fmt, val); + m_logPending = true; +#else + (void)fmt; + (void)val; +#endif +} + +void AppSpiCommand::buildResponse(uint8_t *tx_buf, uint16_t tx_buf_size, + uint16_t *tx_len, uint8_t rsp_type, + const uint8_t *payload, uint16_t payload_len) +{ + if (!tx_buf || !tx_len || tx_buf_size == 0U) { + return; + } + + uint16_t built_len = 0U; + const bool ok = spi_build_frame(rsp_type, payload, payload_len, tx_buf, &built_len); + if (ok) { + *tx_len = built_len; + } +} + +/* ============================================================ + * 硬件配置 + * ============================================================ */ + +void AppSpiCommand::configureGpio() +{ + rcu_periph_clock_enable(RCU_GPIOA); + rcu_periph_clock_enable(RCU_GPIOB); + rcu_periph_clock_enable(RCU_AF); + + gpio_pin_remap_config(GPIO_SPI0_REMAP, ENABLE); + gpio_pin_remap_config(GPIO_SWJ_SWDPENABLE_REMAP, ENABLE); + + gpio_init(GPIOB, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_3 | GPIO_PIN_5); + gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4); + gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15); + + /* PB6 复用为 DATA_READY 握手线(开漏输出,线与)。 + * 开漏模式:写 BRR = 下拉驱动低;写 BSRR = 高阻释放,靠外部/对端上拉拉高。 + * 语义:低 = 有待发数据,高 = 队列空;默认释放(HIGH)。 */ + gpio_init(CONFIG_SPI_READY_PORT, GPIO_MODE_OUT_OD, GPIO_OSPEED_50MHZ, CONFIG_SPI_READY_PIN); + gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); +} + +void AppSpiCommand::setSpiReady(bool ready) +{ + /* 开漏线与:ready=true 释放(BSRR,高阻,靠上拉拉高); + * ready=false 下拉(BRR,驱动低)。 */ + if (ready) { + gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); + } else { + gpio_bit_reset(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN); + } +} + +/* v2 DATA_READY: PB6 低 = 有待发数据,高 = 队列空。 + * L5:同步 g_localCredit(单缓冲 0/1)。 */ +void AppSpiCommand::updateDataReady() +{ + const bool has_data = (m_txLen > 0U) || m_ringPending; + setSpiReady(!has_data); + g_localCredit = has_data ? 0U : 1U; +} + +void AppSpiCommand::configureSpi0() +{ + rcu_periph_clock_enable(RCU_SPI0); + spi_i2s_deinit(SPI0); + (void)spi_i2s_data_receive(SPI0); /* 清空 SPI_DATA 残留值 */ + + spi_parameter_struct spi_init_struct; + spi_struct_para_init(&spi_init_struct); + spi_init_struct.device_mode = SPI_SLAVE; + spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX; + spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; + spi_init_struct.nss = SPI_NSS_SOFT; + spi_init_struct.endian = SPI_ENDIAN_MSB; + spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE; + spi_init_struct.prescale = SPI_PSC_8; + spi_init(SPI0, &spi_init_struct); + + /* 内部 NSS 常低:SPI 从机始终就绪,不依赖 EXTI ISR 切换 internal NSS。 + * PA15 仅作 EXTI 事务边界信号,用于 onCsAssert/onCsRelease 控制 DMA 装载。 */ + spi_nss_internal_low(SPI0); + (void)spi_i2s_data_receive(SPI0); + spi_i2s_data_transmit(SPI0, 0xFFU); + /* RX 改走 DMA0_CH1:禁用 RBNE 逐字节中断,避免 256B 事务 RX-OVR/SOF 丢失。 */ + spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_RBNE); + spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_ERR); + spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_TBE); + nvic_irq_enable(SPI0_IRQn, 2U, 0U); + spi_enable(SPI0); +} + +void AppSpiCommand::configureDma() +{ + rcu_periph_clock_enable(RCU_DMA0); + + /* TX: memory → SPI0_DATA */ + dma_deinit(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); + dma_parameter_struct dma_cfg; + dma_struct_para_init(&dma_cfg); + dma_cfg.periph_addr = (uint32_t)(SPI0 + 0x0CU); /* SPI0_DATA 寄存器 */ + dma_cfg.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; + dma_cfg.memory_addr = (uint32_t)m_txBuf; + dma_cfg.memory_width = DMA_MEMORY_WIDTH_8BIT; + dma_cfg.number = 0U; + dma_cfg.priority = DMA_PRIORITY_HIGH; + dma_cfg.periph_inc = DMA_PERIPH_INCREASE_DISABLE; + dma_cfg.memory_inc = DMA_MEMORY_INCREASE_ENABLE; + dma_cfg.direction = DMA_MEMORY_TO_PERIPHERAL; + dma_init(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH, &dma_cfg); + dma_circulation_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); + dma_channel_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); + + /* RX: SPI0_DATA → m_rxBuf(替代旧 RBNE 逐字节累积)。 + * 与 BOOT SpiOta 对齐:DMA0_CH1 固定映射 SPI0_RX。 */ + dma_deinit(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); + dma_struct_para_init(&dma_cfg); + dma_cfg.periph_addr = (uint32_t)(SPI0 + 0x0CU); + dma_cfg.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; + dma_cfg.memory_addr = (uint32_t)m_rxBuf; + dma_cfg.memory_width = DMA_MEMORY_WIDTH_8BIT; + dma_cfg.number = kRxBufSize; + dma_cfg.priority = DMA_PRIORITY_HIGH; + dma_cfg.periph_inc = DMA_PERIPH_INCREASE_DISABLE; + dma_cfg.memory_inc = DMA_MEMORY_INCREASE_ENABLE; + dma_cfg.direction = DMA_PERIPHERAL_TO_MEMORY; + dma_init(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH, &dma_cfg); + dma_circulation_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); + dma_channel_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); +} + +void AppSpiCommand::configureExti() +{ + gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOA, GPIO_PIN_SOURCE_15); + exti_init(EXTI_15, EXTI_INTERRUPT, EXTI_TRIG_BOTH); + exti_interrupt_enable(EXTI_15); + exti_interrupt_flag_clear(EXTI_15); + nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U); +} + + + +/* ============================================================ + * 默认命令注册入口(由各 app_spi_cmd_*.cpp 提供) + * ============================================================ */ +extern void registerSpiBootRequestCommand(AppSpiCommand &bus); +extern void registerSpiOtaWaitCommand(AppSpiCommand &bus); +extern void registerSpiAppPingCommand(AppSpiCommand &bus); +extern void registerSpiCanBridgeCommand(AppSpiCommand &bus); +extern void registerSpiCanFeedbackCommand(AppSpiCommand &bus); +extern void registerSpiIdSnapshotCommand(AppSpiCommand &bus); +AppSpiCommand AppSpiCommandBus; +void AppSpiCommand::init() +{ + m_csActive = false; + m_rxLen = 0U; + m_txLen = 0U; + m_logPending = false; + m_txActiveThisCs = false; + m_commandCount = 0U; + m_ringPending = false; + m_ringDeadline = 0U; + memset(m_txBuf, 0xFF, sizeof(m_txBuf)); + memset(m_logBuf, 0, sizeof(m_logBuf)); + memset(m_commands, 0, sizeof(m_commands)); + + registerSpiBootRequestCommand(AppSpiCommandBus); + registerSpiOtaWaitCommand(AppSpiCommandBus); + registerSpiAppPingCommand(AppSpiCommandBus); + registerSpiCanBridgeCommand(AppSpiCommandBus); + registerSpiCanFeedbackCommand(AppSpiCommandBus); + registerSpiIdSnapshotCommand(AppSpiCommandBus); + + configureGpio(); + configureDma(); + configureSpi0(); + configureExti(); + /* 首笔 CS 到来前预装 DMA,避免 EXTI 晚于首 SCK。 + * 必须在 configureSpi0/configureDma 之后调用。 */ + armSpiDmasForNextCs(); +} + +/** @brief 在 CS 空闲期预装下一笔全双工 DMA。 + * + * 必须在首个 SCK 之前完成:EXTI CS 下降相对 ESP32 时钟经常偏晚, + * 若在 onCsAssert 里才 start DMA,整笔事务会收 0 字节 → 永不 HELLO_ACK → MISO 全 0xFF。 + * + * TX 始终按 kTxBufSize 装载:响应帧在头部,其余 0xFF。 + * 若只 DMA 实际帧长(~26B),主机打 96 时钟时 TX underrun,后续 MISO 不定。 + * + * 对齐 BOOT SpiOta::armSpiDmasForNextCs。 + */ +void AppSpiCommand::armSpiDmasForNextCs() +{ + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); + spi_dma_disable(SPI0, SPI_DMA_RECEIVE); + drainSpiRxHw(); + + /* 预填 TX:有挂起响应用响应帧(头部)+ 0xFF 填充到满缓冲 */ + if (m_txLen > 0U && m_txLen <= kTxBufSize) { + if (m_txLen < kTxBufSize) { + memset(&m_txBuf[m_txLen], 0xFF, (size_t)(kTxBufSize - m_txLen)); + } + } else { + m_txLen = 0U; + memset(m_txBuf, 0xFF, kTxBufSize); + } + + /* RX 缓冲清零(便于诊断时区分有效字节与残留) */ + memset(m_rxBuf, 0x00, kRxBufSize); + m_rxLen = 0U; + + /* 预装 SPI_DATA,避免 DMA 启动前首拍 TBE 吐旧值 */ + spi_i2s_data_transmit(SPI0, m_txBuf[0]); + + /* 先使能 SPI DMA 请求,再开 DMA 通道,保证首字节即可搬运 */ + spi_dma_enable(SPI0, SPI_DMA_RECEIVE); + spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); + startSpiRxDma(m_rxBuf, kRxBufSize); + /* TX 永远满缓冲长度,与主机 96~256B 事务匹配,避免 underrun */ + startSpiTxDma(m_txBuf, kTxBufSize); +} + +/* ============================================================ + * 命令路由 + * ============================================================ */ + +void AppSpiCommand::registerHandler(ISpiCommandHandler *handler) +{ + if (!handler || m_commandCount >= kMaxCommands) { + return; + } + m_commands[m_commandCount++] = handler; +} + +void AppSpiCommand::processCommand() +{ + if (m_rxLen == 0U) { + return; + } + + /* 0xB0 已受理、正在写 OTA 配置页 / 即将复位:不再处理业务帧,避免打断 Flash。 */ + if (OtaBootRequest::isSpiFrozenForBoot() || OtaBootRequest::isRebootPending()) { + m_txLen = 0U; + updateDataReady(); + return; + } + + /* 尝试按 v2 协议帧解析;同时取回完整帧头用于 session/seq 校验 */ + uint8_t frame_type = 0U; + const uint8_t *payload = nullptr; + uint16_t payload_len = 0U; + v2_header_t hdr; + + if (!spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len, &hdr)) { + /* 无法解析为合法 v2 帧:构建 ERROR 响应 */ + postLogFmt("[SPI-APP] 非v2帧 0x%02X -> ERROR", m_rxBuf[0]); + uint16_t tx_len = 0U; + uint8_t err = 0xEEU; + spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); + m_txLen = tx_len; + + if (m_logPending) { + TaskManager::set(1UL << TASK_SPI_LOG); + } + if (m_txLen > 0U) { + armRingPending(); + } + updateDataReady(); + return; + } + + if (CONFIG_SPI_APP_LOG_ENABLE != 0 && !isFlashRelatedFrame(frame_type)) { + postLogFmt("[SPI-APP] 收到非刷写 SPI type=0x%02X", frame_type); + } + + /* 响应归属:ACK_SEQ 必须回显请求 TX_SEQ(延迟一拍 + ESP32 otaFindFrame 匹配)。 */ + g_pendingAckSeq = hdr.tx_seq; + + /* ---------- HELLO 建链路处理(在 session 校验之前) ---------- */ + if (frame_type == V2_TYPE_LINK_HELLO) { + /* 接受 ESP32 提供的 session_id,重置本端序号状态机 */ + g_sessionId = hdr.session_id; + g_txSeq = 0; + g_expectedRxSeq = 0; + g_v2Active = true; + s_hasCachedResp = false; + + /* 构建 HELLO_ACK 响应帧(回显 session_id + 角色 + credit_max) */ + size_t built_len = 0U; + v2_build_hello_ack_frame(g_sessionId, V2_ROLE_APP, (uint8_t)g_localCredit, + 0, hdr.tx_seq, m_txBuf, &built_len); + m_txLen = (uint16_t)built_len; + + /* 缓存 HELLO_ACK 便于重复 HELLO 重发 */ + s_lastReqSeq = hdr.tx_seq; + memcpy(s_lastRespBuf, m_txBuf, m_txLen); + s_lastRespLen = m_txLen; + s_hasCachedResp = true; + + if (m_logPending) { + TaskManager::set(1UL << TASK_SPI_LOG); + } + if (m_txLen > 0U) { + armRingPending(); + } + updateDataReady(); + return; + } + + /* ---------- SESSION / SEQ 校验(仅在 v2 session 激活后生效) ---------- */ + if (g_v2Active) { + /* session_id 不匹配:构建 LINK_ERROR(V2_ERR_SESSION),跳过 handler */ + if (hdr.session_id != g_sessionId) { + postLogFmt("[SPI-APP] session 不匹配 0x%02X -> LINK_ERROR", + (uint8_t)(hdr.session_id & 0xFFU)); + size_t built_len = 0U; + v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq, + (uint8_t)g_localCredit, V2_ERR_SESSION, + (uint8_t)g_expectedRxSeq, m_txBuf, &built_len); + m_txLen = (uint16_t)built_len; + + if (m_logPending) { + TaskManager::set(1UL << TASK_SPI_LOG); + } + if (m_txLen > 0U) { + armRingPending(); + } + updateDataReady(); + return; + } + + /* 序号回绕距离判定 */ + const uint8_t dist = v2_seq_distance(hdr.tx_seq, g_expectedRxSeq); + if (dist == 0U) { + /* 正常:序号匹配,推进期望序号 */ + g_expectedRxSeq = (uint8_t)(g_expectedRxSeq + 1U); + } else if (dist >= 128U) { + /* 重复请求:重发缓存的响应帧,跳过 handler */ + if (s_hasCachedResp && hdr.tx_seq == s_lastReqSeq) { + postLogFmt("[SPI-APP] 重复请求 seq=0x%02X -> 重发缓存", hdr.tx_seq); + memcpy(m_txBuf, s_lastRespBuf, s_lastRespLen); + m_txLen = s_lastRespLen; + + if (m_logPending) { + TaskManager::set(1UL << TASK_SPI_LOG); + } + if (m_txLen > 0U) { + armRingPending(); + } + updateDataReady(); + return; + } + /* 无缓存可用:按正常请求处理 */ + } else { + /* dist 1-127:序号跳变。对齐期望序号后: + * - 普通业务:回 LINK_ERROR 并丢弃(避免副作用) + * - 0xB0 ENTER_BOOT:控制面命令,仍必须执行,否则永远进不了 BOOT */ + postLogFmt("[SPI-APP] SEQ jump rx=0x%02X", hdr.tx_seq); + g_expectedRxSeq = (uint8_t)(hdr.tx_seq + 1U); + + if (frame_type != kTypeBootReq) { + size_t built_len = 0U; + v2_build_error_frame(g_sessionId, g_txSeq, hdr.tx_seq, + (uint8_t)g_localCredit, V2_ERR_SEQ_JUMP, + (uint8_t)g_expectedRxSeq, m_txBuf, &built_len); + m_txLen = (uint16_t)built_len; + + if (m_logPending) { + TaskManager::set(1UL << TASK_SPI_LOG); + } + if (m_txLen > 0U) { + armRingPending(); + } + updateDataReady(); + return; + } + postLog("SEQ jump but BOOT_REQ still handled"); + /* fall through → 业务分发 */ + } + } + + /* ---------- 业务命令分发 ---------- */ + uint16_t tx_len = 0U; + bool handled = false; + + SpiCommandContext ctx{}; + ctx.frame_type = frame_type; + ctx.payload = payload; + ctx.payload_len = payload_len; + ctx.tx_buf = m_txBuf; + ctx.tx_buf_size = kTxBufSize; + ctx.tx_len = &tx_len; + + for (uint8_t i = 0U; i < m_commandCount; ++i) { + ISpiCommandHandler *handler = m_commands[i]; + if (handler && handler->canHandle(frame_type)) { + handler->handle(AppSpiCommandBus, ctx); + handled = true; + break; + } + } + + if (!handled) { + postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type); + uint8_t err = 0xEEU; + spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); + } + + if (tx_len == 0U) { + /* 兜底:避免残留上一次应答帧 */ + uint8_t err = 0xEEU; + spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); + } + + m_txLen = tx_len; + + /* 缓存本次响应,便于重复请求时重发(避免 handler 重复执行副作用) */ + if (g_v2Active) { + s_lastReqSeq = hdr.tx_seq; + memcpy(s_lastRespBuf, m_txBuf, m_txLen); + s_lastRespLen = m_txLen; + s_hasCachedResp = true; + /* 推进本端发送序号(响应帧占用一个 tx_seq 槽位) */ + g_txSeq = (uint8_t)(g_txSeq + 1U); + } + + if (m_logPending) { + /* 置位 TASK_SPI_LOG,唤醒主循环立刻打印日志 */ + TaskManager::set(1UL << TASK_SPI_LOG); + } + + /* 应答帧已就绪:由 updateDataReady() 依据 m_txLen 驱动 PB6(有待发=低)。 + * 命令处理已在 ISR 内同步完成,无需 m_cmdPending 推迟机制。 */ + + /* 有响应待发时,设置按铃标志阻止主循环 canFeedbackPushStep() 覆盖 m_txBuf。 + * ESP32 下次 CS assert 时 DMA 输出响应,onCsRelease 清除 m_ringPending。 + * 50ms 超时后 cancelRing() 清除,恢复 CAN 反馈推送。 */ + if (m_txLen > 0U) { + armRingPending(); + } + updateDataReady(); +} + +/* ============================================================ + * GD32 主动按铃:设 m_ringPending + updateDataReady() 拉低 PB6 请求 ESP32 来读 + * ============================================================ */ + +void AppSpiCommand::armRingPending() +{ + m_ringPending = true; + m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs; +} + +void AppSpiCommand::cancelRing() +{ + m_ringPending = false; + /* 清除待发响应,防止超时后残留帧在下一次 CS assert 时被错误输出 */ + m_txLen = 0U; + /* 释放 SPIready(高阻),避免永久占用线。 + * cancelRing 已清 m_txLen 和 m_ringPending,updateDataReady() 会释放为高。 */ + updateDataReady(); +} + +bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len) +{ + /* 若上一次按铃尚未被 ESP32 读走,拒绝重复按铃 */ + if (m_ringPending) { + return false; + } + + /* 构建 v2 EVENT 帧到 m_txBuf(ESP32 下次读事务时通过 DMA 输出到 MISO)。 + * 主动按铃帧使用 V2_FLAG_EVENT,session/seq 从 v2 状态全局变量取; + * session 未建立时 session_id 填 0。 */ + const uint32_t sess = g_v2Active ? g_sessionId : 0U; + /* L5:单缓冲 — 装载前 credit 报 1,装载后 updateDataReady 置 0 */ + g_localCredit = 1U; + size_t built_len = 0U; + if (!v2_build_frame(frame_type, V2_FLAG_EVENT, sess, + g_txSeq, 0U, (uint8_t)g_localCredit, + payload, payload_len, m_txBuf, &built_len)) { + return false; + } + /* EVENT 帧占用一个发送序号槽位 */ + if (g_v2Active) { + g_txSeq = (uint8_t)(g_txSeq + 1U); + } + + m_txLen = (uint16_t)built_len; + armRingPending(); + + /* v2 DATA_READY: 已设 m_ringPending=true 且 m_txLen>0, + * updateDataReady() 据此拉低 PB6,并置 g_localCredit=0。 */ + updateDataReady(); + return true; +} + +/* ============================================================ + * 主循环轮询:打印延迟日志 + 按铃超时看护 + * ============================================================ */ + +void AppSpiCommand::poll() +{ + /* 打印 ISR 推迟的日志 */ + char log_copy[kLogBufSize] = {}; + bool has_log = false; + + uint32_t save = RT_CRITICAL_SAVE(); + if (m_logPending) { + memcpy(log_copy, m_logBuf, sizeof(log_copy)); + m_logPending = false; + has_log = true; + } + RT_CRITICAL_RESTORE(save); + + if (has_log) { + printf("%s\r\n", log_copy); + } + + /* 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。 + * 防止 ESP32 不响应时永久占用 SPIready 线导致双方死锁。 */ + if (m_ringPending) { + uint32_t save2 = RT_CRITICAL_SAVE(); + if (m_ringPending) { + if ((int32_t)(SysTickTimer::getMs() - m_ringDeadline) >= 0) { + printf("[SPI-APP] 按铃超时取消 (ESP32 未响应, 释放 SPIready 重试)\r\n"); + cancelRing(); + } + } + RT_CRITICAL_RESTORE(save2); + } +} + +/* ============================================================ + * CS 边沿处理 + * ============================================================ */ + +void AppSpiCommand::onCsAssert() +{ + if (m_csActive) { + return; + } + m_csActive = true; + /* CS 下降:DMA 已在空闲期由 armSpiDmasForNextCs() 预装,此处只标记忙碌。 + * v2 DATA_READY: CS 本身即互斥,PB6 不再表示「忙碌」,不在 CS 下降沿拉低。 + * 切勿在这里 stop/restart DMA,否则赶不上 ESP32 已发出的 SCK(对齐 BOOT)。 */ + m_txActiveThisCs = false; +} + +void AppSpiCommand::onCsRelease() +{ + if (!m_csActive) { + /* EXTI 丢失下降沿时:仍尝试收尾并 re-arm,避免永久失步(对齐 BOOT) */ + m_rxLen = stopSpiDmas(kRxBufSize); + drainSpiRxHw(); + if (m_txLen > 0U) { + m_txLen = 0U; + memset(m_txBuf, 0xFF, sizeof(m_txBuf)); + } + if (m_rxLen >= V2_OVERHEAD) { + processCommand(); + } + armSpiDmasForNextCs(); + updateDataReady(); + return; + } + m_csActive = false; + + /* 停 DMA 并取本笔 RX 长度(先读 CHCNT 再 disable,否则计数器被清) */ + m_rxLen = stopSpiDmas(kRxBufSize); + drainSpiRxHw(); + + /* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧, + * 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。 + * + * 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 v2 协议帧(如 PING/OTA/0xB0)。 + * 若 GD32 正在按铃时 ESP32 发协议帧,会因 m_ringPending=true 被误吞, + * 导致命令无法处理。检测到 v2 SOF 时取消按铃并走正常命令处理。 */ + if (m_ringPending) { + /* 判断本次收到的数据是否为 v2 协议帧(扫描 SOF0 SOF1)。 + * 取代旧 containsSyncSof,直接用 V2_SOF0/V2_SOF1。 + * 0xB0 进 BOOT 现已走标准 v2 帧 handler,不再单独检测裸字节。 */ + bool is_v2_frame = false; + if (m_rxLen >= 2U) { + const uint16_t last = (m_rxLen < 16U) ? (uint16_t)(m_rxLen - 1U) : 16U; + for (uint16_t i = 0U; i < last; ++i) { + if (m_rxBuf[i] == V2_SOF0 && m_rxBuf[i + 1U] == V2_SOF1) { + is_v2_frame = true; + break; + } + } + } + if (is_v2_frame) { + /* ESP32 发的是 v2 协议帧(如 PING/OTA/0xB0 等),不是按铃响应。 + * 取消按铃,清空 TX 让 processCommand() 重新装载对应应答。 + * processCommand() 末尾由 updateDataReady() 依据新 m_txLen 驱动 PB6。 */ + m_ringPending = false; + m_txLen = 0U; + memset(m_txBuf, 0xFF, sizeof(m_txBuf)); + /* 继续走 shouldIgnoreTransaction + processCommand(),不 return */ + } else { + /* 正常按铃响应:请求帧已被读走,取消按铃状态 */ + m_ringPending = false; + /* 请求帧已被读走,清空 TX 防止下次事务重复输出 */ + m_txLen = 0U; + memset(m_txBuf, 0xFF, sizeof(m_txBuf)); + /* 重新预装下一笔 DMA,保证下一 CS 首字节即可收发 */ + armSpiDmasForNextCs(); + updateDataReady(); + return; + } + } + + if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) { + m_rxLen = 0U; + if (m_txActiveThisCs) { + m_txLen = 0U; + memset(m_txBuf, 0xFF, sizeof(m_txBuf)); + } + m_txActiveThisCs = false; + /* v2 DATA_READY: shouldIgnoreTransaction 路径已清 m_txLen=0, + * updateDataReady() 释放为高,避免 PB6 卡 LOW。 */ + armSpiDmasForNextCs(); + updateDataReady(); + return; + } + + m_txLen = 0U; + m_txActiveThisCs = false; + memset(m_txBuf, 0xFF, sizeof(m_txBuf)); + + /* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式), + * 消除主循环延迟。处理完成后 processCommand() 末尾再 re-arm DMA + 释放 SPIready。 */ + processCommand(); + + /* 处理完立即预装下一笔,保证下一 CS 首字节即可收发 */ + armSpiDmasForNextCs(); + updateDataReady(); +} + +/* ============================================================ + * SPI0 / EXTI 中断处理 + * ============================================================ */ + +void AppSpiCommand::onInterrupt() +{ + /* RX 已改走 DMA0_CH1:RBNE 逐字节中断已禁用,本 ISR 只保留错误标志清理。 + * CS 边沿处理全部在 onCsExtiInterrupt 的 EXTI15 ISR 里完成。 */ + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { + /* RXORERR 理论上不应再出现(RX 走 DMA),但兜底清理避免标志卡死。 + * 读 SPI_DATA + SPI_STAT 清除 OVR 标志。 */ + (void)spi_i2s_data_receive(SPI0); + (void)SPI_STAT(SPI0); + } + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { + spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); + } +} + +void AppSpiCommand::onCsExtiInterrupt() +{ + if (exti_interrupt_flag_get(EXTI_15) != RESET) { + exti_interrupt_flag_clear(EXTI_15); + if (gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) { + onCsAssert(); + } else { + onCsRelease(); + } + } +} + +extern "C" void SPI0_IRQHandler(void) +{ + AppSpiCommandBus.onInterrupt(); +} + +extern "C" void EXTI10_15_IRQHandler(void) +{ + AppSpiCommandBus.onCsExtiInterrupt(); +} diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.h b/GD32E508_DCU_APP/device/spi/app_spi_command.h index 2e0adea..306eb84 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.h +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.h @@ -5,39 +5,6 @@ class ISpiCommandHandler; -/** - * @brief GD32 侧 SPI 链路状态机:封装按铃状态。 - * - * v2 DATA_READY 语义:PB6 低 = 有待发数据,高 = 队列空。 - * ESP32 不再操作 PB6,无独立「完成」信号;按铃完成由 ESP32 - * 的 CS 读事务本身确认(onCsRelease 按铃响应路径调 onEsp32Complete)。 - * - * canRingNow() 总是返回 true,由 requestEsp32Read() 的 m_ringPending - * 检查控制重复按铃。 - */ -class SpiLinkState { -public: - /** 是否允许发起新按铃。 - * v2 DATA_READY: ESP32 不再操作 PB6,无独立完成信号; - * 总是允许,由 requestEsp32Read() 的 m_ringPending 检查控制重复。 */ - bool canRingNow() const { return true; } - - /** 发起按铃时调用(v2 下仅保留兼容写法,无实际门禁作用)。 */ - void onRingStart() { m_esp32Ready = false; } - - /** ESP32 CS 读事务完成确认(onCsRelease 按铃响应路径调用)。 */ - void onEsp32Complete() { m_esp32Ready = true; } - - /** 按铃超时取消时调用(允许重试,不依赖 EXTI)。 */ - void onRingTimeout() { m_esp32Ready = true; } - - /** 初始化(首帧允许)。 */ - void reset() { m_esp32Ready = true; } - -private: - volatile bool m_esp32Ready = true; -}; - /** * @brief APP 运行期 SPI 链路与命令路由模块。 * @@ -114,11 +81,6 @@ public: */ bool requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len); - /** @brief L4 别名:主动事件入队(当前等同 requestEsp32Read)。 */ - bool enqueueEvent(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len) { - return requestEsp32Read(frame_type, payload, payload_len); - } - /** @brief 查询当前是否有未读按铃(ESP32 尚未来读)。 */ bool isRingPending() const { return m_ringPending; } @@ -126,11 +88,6 @@ public: * 0xB0 进 BOOT 等路径需公开调用,避免 0xC1 残留盖住 ACK。 */ void cancelRing(); - /** @brief 查询 ESP32 是否已完成上一帧处理(SPIready 上升沿触发)。 - * 初始为 true 允许第一次按铃;每次 requestEsp32Read 后置 false, - * ESP32 releaseReady 产生上升沿后由 EXTI 置 true。 */ - bool canRingNow() const { return m_linkState.canRingNow(); } - static constexpr uint16_t kRxBufSize = 256U; static constexpr uint16_t kTxBufSize = 256U; static constexpr uint16_t kLogBufSize = 256U; @@ -143,8 +100,7 @@ private: volatile bool m_csActive = false; /** 本笔 CS 实际收到的字节数(CS 上升沿时由 RX DMA CHCNT 反算得到)。 */ volatile uint16_t m_rxLen = 0U; - uint16_t m_txLen = 0U; - volatile uint8_t m_txPos = 0U; + volatile uint16_t m_txLen = 0U; volatile bool m_logPending = false; bool m_txActiveThisCs = false; /** RX DMA 接收缓冲(替代旧 RBNE 逐字节累积的 m_rxBuf)。 @@ -159,11 +115,6 @@ private: volatile bool m_ringPending = false; /** 按铃超时截止时刻(ms,基于 SysTickTimer::getMs)。 */ volatile uint32_t m_ringDeadline = 0U; - /** 按铃期间暂存的请求帧长度(供 onCsAssert 装入 DMA)。 */ - volatile uint16_t m_ringTxLen = 0U; - - /** SPI 链路状态机:封装 ESP32 完成信号。 */ - SpiLinkState m_linkState; void configureGpio(); void configureSpi0(); @@ -173,6 +124,10 @@ private: void onCsRelease(); void processCommand(); + /** @brief 设置 m_ringPending=true 并启动超时看护截止时刻。 + * 统一 7 处「m_ringPending=true; m_ringDeadline=now+timeout;」赋值模式。 */ + void armRingPending(); + /** @brief 在 CS 空闲期预装下一笔全双工 DMA(TX + RX)。 * 必须在首个 SCK 之前完成,否则 EXTI CS 下降相对 ESP32 时钟偏晚, * 整笔事务会收 0 字节。对齐 BOOT SpiOta::armSpiDmasForNextCs。 */ 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 af0894d..020b7d5 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h +++ b/GD32E508_DCU_APP/device/spi/app_spi_command_defs.h @@ -1,4 +1,4 @@ -#ifndef DEVICE_APP_SPI_COMMAND_DEFS_H +#ifndef DEVICE_APP_SPI_COMMAND_DEFS_H #define DEVICE_APP_SPI_COMMAND_DEFS_H #include @@ -14,10 +14,6 @@ namespace SpiDefs { -/* 链路层帧类型(v2) */ -static constexpr uint8_t kTypeLinkHello = 0x00U; -static constexpr uint8_t kTypeLinkError = 0x7FU; - /* 帧类型 */ static constexpr uint8_t kTypeOtaReq = 0x01U; static constexpr uint8_t kTypeOtaData = 0x02U; @@ -36,4 +32,12 @@ static constexpr uint8_t kRspError = 0x82U; } // namespace SpiDefs +/** @brief 将 32 位值按小端写入 4 字节缓冲(供 CAN 反馈/ID 快照等 payload 复用)。 */ +inline void writeU32Le(uint8_t *dst, uint32_t val) { + dst[0] = static_cast(val & 0xFFU); + dst[1] = static_cast((val >> 8) & 0xFFU); + dst[2] = static_cast((val >> 16) & 0xFFU); + dst[3] = static_cast((val >> 24) & 0xFFU); +} + #endif /* DEVICE_APP_SPI_COMMAND_DEFS_H */ diff --git a/GD32E508_DCU_APP/device/spi/spi_command_handler.h b/GD32E508_DCU_APP/device/spi/spi_command_handler.h index 0cfa43f..4ab36a9 100644 --- a/GD32E508_DCU_APP/device/spi/spi_command_handler.h +++ b/GD32E508_DCU_APP/device/spi/spi_command_handler.h @@ -1,4 +1,4 @@ -#ifndef DEVICE_SPI_COMMAND_HANDLER_H +#ifndef DEVICE_SPI_COMMAND_HANDLER_H #define DEVICE_SPI_COMMAND_HANDLER_H #include @@ -16,10 +16,10 @@ struct SpiCommandContext { class ISpiCommandHandler { public: - ~ISpiCommandHandler() {} - virtual bool canHandle(uint8_t frame_type) const = 0; virtual void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) = 0; +protected: + ~ISpiCommandHandler() = default; /* 接口基类,不允许通过基类指针 delete;裸机不退出。 */ }; #endif /* DEVICE_SPI_COMMAND_HANDLER_H */ diff --git a/GD32E508_DCU_APP/device/temp_sensor.cpp b/GD32E508_DCU_APP/device/temp_sensor.cpp index b49b30b..d653427 100644 --- a/GD32E508_DCU_APP/device/temp_sensor.cpp +++ b/GD32E508_DCU_APP/device/temp_sensor.cpp @@ -16,16 +16,4 @@ void TempSensor::init() { Adc::init(); -} - -/** - * @brief 获取一组温度传感相关采样结果。 - * @param[in] data 指向输出采样结构体的指针。 - * @param[out] data 输出温度、VREF 与 VIN 相关采样结果;允许为空。 - * @retval 无返回值。 - * @note 当前内部直接转调 `Adc::collectSample()`。 - */ -void TempSensor::sample(Data *data) -{ - Adc::collectSample(data); } \ No newline at end of file diff --git a/GD32E508_DCU_APP/device/temp_sensor.h b/GD32E508_DCU_APP/device/temp_sensor.h index 7c308e2..1e82ee0 100644 --- a/GD32E508_DCU_APP/device/temp_sensor.h +++ b/GD32E508_DCU_APP/device/temp_sensor.h @@ -9,8 +9,6 @@ */ class TempSensor { public: - using Data = Adc::Sample; - /** * @brief 初始化温度采样依赖资源。 * @param[in] 无。 @@ -19,17 +17,6 @@ public: * @note 当前通过调用 Adc::init() 完成内部通道准备。 */ static void init(); - - /** - * @brief 采集一次温度相关样本。 - * @param[in] 无。 - * @param[out] data 指向输出 Data 结构体的指针。 - * @retval 无返回值。 - * @note 内部调用 Adc::collectSample() 填充完整 ADC 结果。 - */ - static void sample(Data *data); }; -using temp_sensor_data_t = TempSensor::Data; - #endif /* DEVICE_TEMP_SENSOR_H */ \ No newline at end of file diff --git a/GD32E508_DCU_APP/device/usart0.cpp b/GD32E508_DCU_APP/device/usart0.cpp index e6b4702..6cd407d 100644 --- a/GD32E508_DCU_APP/device/usart0.cpp +++ b/GD32E508_DCU_APP/device/usart0.cpp @@ -11,7 +11,6 @@ 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_dropCount = 0U; /** * @brief 计算发送环形缓冲区中的下一个索引。 @@ -24,17 +23,6 @@ uint16_t Usart0::nextIndex(uint16_t idx) return (idx + 1U) % kTxBufSize; } -/** - * @brief 判断 USART0 发送缓冲区是否已满。 - * @param[in] 无。 - * @param[out] 无。 - * @retval bool true 表示发送缓冲区已满,false 表示仍有可写空间。 - */ -bool Usart0::isBufferFull() -{ - return nextIndex(s_txHead) == s_txTail; -} - /** * @brief 判断 USART0 发送缓冲区是否为空。 * @param[in] 无。 @@ -59,7 +47,6 @@ bool Usart0::putCharNonBlocking(int ch) const uint16_t next = nextIndex(s_txHead); if(next == s_txTail) { RT_CRITICAL_RESTORE(save); - s_dropCount++; return false; } diff --git a/GD32E508_DCU_APP/device/usart0.h b/GD32E508_DCU_APP/device/usart0.h index 0f74736..f3ce5e0 100644 --- a/GD32E508_DCU_APP/device/usart0.h +++ b/GD32E508_DCU_APP/device/usart0.h @@ -56,14 +56,6 @@ private: */ static uint16_t nextIndex(uint16_t idx); - /** - * @brief 判断环形发送缓冲是否已满。 - * @param[in] 无。 - * @param[out] 无。 - * @retval bool true 表示已满,false 表示可继续写入。 - */ - static bool isBufferFull(); - /** * @brief 判断环形发送缓冲是否为空。 * @param[in] 无。 @@ -75,7 +67,6 @@ private: static bool putCharNonBlocking(int ch); static constexpr uint16_t kTxBufSize = 1024U; - static volatile uint32_t s_dropCount; static uint8_t s_txBuf[kTxBufSize]; static volatile uint16_t s_txHead; static volatile uint16_t s_txTail; diff --git a/GD32E508_DCU_APP/device/watchdog.cpp b/GD32E508_DCU_APP/device/watchdog.cpp index 182a626..ac1af80 100644 --- a/GD32E508_DCU_APP/device/watchdog.cpp +++ b/GD32E508_DCU_APP/device/watchdog.cpp @@ -24,8 +24,3 @@ void Watchdog::feed() { fwdgt_counter_reload(); } - -void Watchdog::abandon() -{ - /* 不喂狗即可,FWDGT 超时后硬件自动复位。 */ -} diff --git a/GD32E508_DCU_APP/device/watchdog.h b/GD32E508_DCU_APP/device/watchdog.h index eede2a9..f3ce057 100644 --- a/GD32E508_DCU_APP/device/watchdog.h +++ b/GD32E508_DCU_APP/device/watchdog.h @@ -1,4 +1,4 @@ -/** +/** * @file watchdog.h * @brief APP 独立看门狗模块(FWDGT)。 * @@ -17,9 +17,6 @@ public: /** @brief 喂狗(重载计数器),应在主循环中周期调用。 */ static void feed(); - - /** @brief 停止喂狗,让看门狗自然超时触发复位跳转 BOOT。 */ - static void abandon(); }; #endif /* DEVICE_WATCHDOG_H */ diff --git a/GD32E508_DCU_APP/main.h b/GD32E508_DCU_APP/main.h index 3db432e..ef2c0b5 100644 --- a/GD32E508_DCU_APP/main.h +++ b/GD32E508_DCU_APP/main.h @@ -15,7 +15,6 @@ enum system_task_t { TASK_ADC_VREF_SAMPLE = 0, /**< 采样 VIN/VREF 原始值任务。 */ TASK_ADC_TEMP_SAMPLE, /**< 采样内部温度通道原始值任务。 */ TASK_ADC_PROCESS, /**< 对 ADC 原始值进行电压与温度计算任务。 */ - TASK_CAN_RX_POLL, /**< 兼容保留的 CANFD 接收轮询任务。 */ TASK_CAN_RX_PARSE, /**< 解析并消费已接收 CANFD 帧任务。 */ TASK_CAN_TX_SEND, /**< CANFD 周期发送任务,每 1ms 每通道发送一帧。 */ TASK_STATUS_LED, /**< 状态灯维护任务。 */ diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index 4dc9dcf..4732b91 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -1,4 +1,4 @@ -/** +/** * @file config.h * @brief 应用级集中配置项定义。 * @note 用于统一管理当前项目中可调的外设、时序、通信与算法常量。 @@ -16,9 +16,9 @@ constexpr const char *APP_BANNER_TEXT = "GD32E508 DCU task scheduler start"; * APP 固件版本号 * ============================================================ */ constexpr uint32_t APP_FW_VERSION_MAJOR = 1U; -constexpr uint32_t APP_FW_VERSION_MINOR = 2U; -constexpr uint32_t APP_FW_VERSION_PATCH = 3U; -constexpr uint32_t APP_FW_VERSION_BUILD = 2U; +constexpr uint32_t APP_FW_VERSION_MINOR = 3U; +constexpr uint32_t APP_FW_VERSION_PATCH = 0U; +constexpr uint32_t APP_FW_VERSION_BUILD = 0U; /** @brief USART0 默认波特率。 */ diff --git a/GD32E508_DCU_APP/sys/hardfault.cpp b/GD32E508_DCU_APP/sys/hardfault.cpp index b2b2c19..f308b11 100644 --- a/GD32E508_DCU_APP/sys/hardfault.cpp +++ b/GD32E508_DCU_APP/sys/hardfault.cpp @@ -92,7 +92,6 @@ static uint32_t detectApb2Clock(void) { } extern "C" void HardFault_Handler_C(uint32_t *stack_frame, uint32_t lr_value) { - (void)lr_value; /* 确保 USART0 时钟可用 */ volatile uint32_t *rcu_apb2en = (volatile uint32_t *)0x40021018U; @@ -116,6 +115,8 @@ extern "C" void HardFault_Handler_C(uint32_t *stack_frame, uint32_t lr_value) { *usart_ctl0 = (1U << 3) | (1U << 13); /* TE | USARTEN */ rawPuts("\r\n!!! HardFault !!!\r\n"); + rawPuts("EXC_RETURN="); rawHex32(lr_value); rawPuts("\r\n"); + /* EXC_RETURN 解析:bit3=0(MSP)/1(PSP), bit2=0(handler mode)/1(thread mode) */ if (stack_frame) { uint32_t pc = stack_frame[6]; diff --git a/GD32E508_DCU_APP/sys/hardfault.h b/GD32E508_DCU_APP/sys/hardfault.h deleted file mode 100644 index 9166c22..0000000 --- a/GD32E508_DCU_APP/sys/hardfault.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef HARDFAULT_H -#define HARDFAULT_H - -void HardFault_Diagnostic(void); - -#endif diff --git a/GD32E508_DCU_APP/sys/ota_boot_request.cpp b/GD32E508_DCU_APP/sys/ota_boot_request.cpp index c346b88..e9db0a1 100644 --- a/GD32E508_DCU_APP/sys/ota_boot_request.cpp +++ b/GD32E508_DCU_APP/sys/ota_boot_request.cpp @@ -217,6 +217,7 @@ void OtaBootRequest::process() * BOOT 起机后再由 HELLO role=boot 确认。 */ /* SysTick 中断仍开,delayMs 可用;SPI 相关 IRQ 已关。 */ SysTickTimer::delayMs(kRebootDelayMs); + /* 复位前最终关中断:必须用 PRIMASK 全屏蔽(非 BASEPRI),确保 NVIC_SystemReset 过程中无任何中断嵌套 */ __disable_irq(); NVIC_SystemReset(); while(1) { diff --git a/GD32E508_DCU_APP/sys/task_manager.h b/GD32E508_DCU_APP/sys/task_manager.h index 0c6b22f..b91c553 100644 --- a/GD32E508_DCU_APP/sys/task_manager.h +++ b/GD32E508_DCU_APP/sys/task_manager.h @@ -42,7 +42,7 @@ public: * @param[in] taskMask 待置位的任务位图掩码。 * @param[out] 无。 * @retval 无返回值。 - * @note 内部通过保持当前 PRIMASK 状态的临界区保证可由主循环与 ISR 共享调用。 + * @note 内部通过 BASEPRI 临界区(RT_CRITICAL_SAVE/RESTORE)保证可由主循环与 ISR 共享调用。 */ static void set(uint32_t taskMask); diff --git a/GD32E508_DCU_APP/sys/task_runner.cpp b/GD32E508_DCU_APP/sys/task_runner.cpp index 6b7e15c..bbc6993 100644 --- a/GD32E508_DCU_APP/sys/task_runner.cpp +++ b/GD32E508_DCU_APP/sys/task_runner.cpp @@ -59,7 +59,7 @@ static float medianFilter(const float *buffer) } // namespace struct TaskRunner::Runtime { - temp_sensor_data_t sample{}; + Adc::Sample sample{}; float temperature_samples[kAdcFilterWindow] = {0.0f}; float vdda_samples[kAdcFilterWindow] = {0.0f}; float vin_adc_samples[kAdcFilterWindow] = {0.0f}; @@ -134,15 +134,6 @@ void TaskRunner::taskAdcProcess() s.valid = sampleValid ? 1U : 0U; } -/** - * @brief 执行兼容保留的 CAN 接收轮询任务。 - * @param[in] 无。 - * @param[out] 无。 - * @retval 无返回值。 - * @note 当前三路 CANFD 接收已由 RX0 中断驱动,该任务仅保留接口兼容性。 - */ -void TaskRunner::taskCanRxPoll() { CanFd::poll(); } - /** * @brief 执行 CAN 周期发送任务:每 1ms 在每个使能通道上发送一帧 CANFD 测试帧。 * @param[in] 无。 @@ -231,7 +222,7 @@ void TaskRunner::taskStatusLed() { /* 预留 */ } * @param[in] 无。 * @param[out] 无。 * @retval 无返回值。 - * @note 当前仅保留任务入口。 + * @note 当前任务:推进 OTA Boot 请求、刷新 CAN 反馈快照、启动反馈推送轮次。 */ void TaskRunner::taskHeartbeat() { OtaBootRequest::process(); CanFd::refreshFeedbackSnapshot(); canFeedbackStartRound(); } @@ -382,7 +373,6 @@ void TaskRunner::runPending() if(pending & (1UL << TASK_ADC_VREF_SAMPLE)) taskAdcVrefSample(); if(pending & (1UL << TASK_ADC_TEMP_SAMPLE)) taskAdcTempSample(); if(pending & (1UL << TASK_ADC_PROCESS)) taskAdcProcess(); - if(pending & (1UL << TASK_CAN_RX_POLL)) taskCanRxPoll(); if(pending & (1UL << TASK_CAN_RX_PARSE)) taskCanRxParse(); if(pending & (1UL << TASK_CAN_TX_SEND)) taskCanTxSend(); if(pending & (1UL << TASK_STATUS_LED)) taskStatusLed(); diff --git a/GD32E508_DCU_APP/sys/task_runner.h b/GD32E508_DCU_APP/sys/task_runner.h index e7c361f..4169dbe 100644 --- a/GD32E508_DCU_APP/sys/task_runner.h +++ b/GD32E508_DCU_APP/sys/task_runner.h @@ -35,8 +35,6 @@ private: static void taskAdcTempSample(); /** @brief 根据采样值计算工程量。 */ static void taskAdcProcess(); - /** @brief 兼容保留的 CAN 轮询任务。 */ - static void taskCanRxPoll(); /** @brief 解析并消费接收帧。 */ static void taskCanRxParse(); /** @brief CAN 周期发送任务;每 1ms 每通道发送一帧测试报文。 */ diff --git a/README.md b/README.md index c678bda..a59902b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GD32E508_DCU_APP README +# GD32E508_DCU_APP README 本 README 用来介绍 `/workzone/ROBOT_V2/DCU/DCU_APP/GD32E508_DCU_APP` 目录下各个代码文件的作用、分层关系和阅读顺序。 @@ -317,7 +317,7 @@ ### `device/canfd.h` **作用:** - 定义 CANFD 模块对外接口 -- 定义诊断结构 `canfd_diag_t` +- 定义诊断结构 `CanFd::Diag` - 定义状态发送载荷结构 `canfd_status_payload_t` **主要接口:** @@ -331,7 +331,7 @@ --- -### `device/CANFD.cpp` +### `device/canfd.cpp` **作用:** - 初始化 CANFD 工作模式 - RX0 中断批量搬空硬件 FIFO0 @@ -488,14 +488,14 @@ main.cpp │ ├─ device/usart0.cpp │ ├─ device/temp_sensor.cpp │ │ └─ device/ADC.cpp - │ ├─ device/CANFD.cpp + │ ├─ device/canfd.cpp │ ├─ sys/task_manager.cpp │ └─ device/Timer.cpp │ └─ TIMER2_IRQHandler -> task_manager_tick_100us() │ └─ sys/task_runner.cpp ├─ device/ADC.cpp - ├─ device/CANFD.cpp + ├─ device/canfd.cpp └─ sys/task_manager.cpp ``` @@ -523,7 +523,7 @@ main.cpp ### 建议优先修改的位置 如果是改业务逻辑,优先看: - `sys/task_runner.cpp` -- `device/CANFD.cpp` +- `device/canfd.cpp` - `device/ADC.cpp` - `sys/task_manager.cpp`