From 5ffc85077ee7c4ebd52178021d9ac99eed223cd6 Mon Sep 17 00:00:00 2001 From: yhf Date: Sat, 18 Jul 2026 15:38:54 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=88=9D=E5=A7=8B=E5=8C=96HCDF=20MID?= =?UTF-8?q?=20405=E5=85=85=E6=94=BE=E7=94=B5=E6=8E=A7=E5=88=B6=E5=99=A8?= =?UTF-8?q?=E5=9B=BA=E4=BB=B6=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加完整的项目README文档,包含系统架构、功能说明、目录结构、通信接口、Flash布局、开发编译流程、下载验证和注意事项等内容。 --- README.md | 156 +++ ...26-07-17-apps-chrg-deadlock-remediation.md | 1186 +++++++++++++++++ 2 files changed, 1342 insertions(+) create mode 100644 README.md create mode 100644 docs/2026-07-17-apps-chrg-deadlock-remediation.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7acad0 --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +# HCDF MID 405 充放电控制器固件 + +本仓库是基于 **STM32F405RG** 的四通道双向电源/电子负载控制固件。主程序运行在 **RT-Thread 5.2.2** 上,负责连接上位机、HMI、快充协议板和源载功率板,完成通道切换、源/载控制、继电器管理、状态采集、校准和固件升级。仓库同时包含独立 Bootloader,用于应用固件校验、更新和出厂固件恢复。 + +> 本项目直接控制电源与电子负载硬件。调试前应确认限压、限流、散热、急停和继电器默认状态;修改通道切换或输出控制逻辑后,先在断电或受限功率条件下验证。 + +## 系统概览 + +```mermaid +flowchart LR + PC["上位机 / 测试系统"] -->|"UART1 · Modbus RTU"| CTRL["STM32F405 控制板"] + HMI["HMI 屏幕"] <-->|"UART2"| CTRL + CTRL <-->|"UART3 · 多路复用"| NORTH["4 路快充协议板"] + CTRL <-->|"UART6 · 多路复用"| SOUTH["4 路源载功率板"] + CTRL --> RELAY["继电器与通道选择"] + CTRL --> FLASH["参数 / 日志 / 升级分区"] + USB["USB CDC 调试终端"] <-->|"MSH / 日志"| CTRL +``` + +主程序把每个物理通道的控制状态集中保存在北向对象中,并从南向功率板刷新电压、电流、功率、故障和在线状态。上位机或 HMI 写入控制参数后,轮询线程将变化转换为协议板、功率板和继电器操作;采集结果再映射回 Modbus 寄存器和屏幕数据。 + +## 主要功能 + +- 四通道 Source/Sink 控制及通信通道复用。 +- 电源侧快充协议设置,代码中包含 FCP、SCP、QC2.0/QC3.0、AFC、PD、UFCS 等协议编号;实际支持范围以 `docs/` 中的协议表和目标板固件为准。 +- 电子负载恒压、恒流、恒功率、恒阻和开环模式,以及短路、OCP、动态测试控制。 +- 上位机 Modbus RTU 服务,地址默认为 `0x01`,实现 `0x01/0x02/0x03/0x04/0x05/0x06/0x0F/0x10`,并提供 `0x20` 升级和 `0x21` 校准扩展功能。 +- FAL 片内 Flash 管理、看门狗、硬件定时器、USB CDC 虚拟串口、FinSH/MSH 和 ULog。 +- 带 CRC、包头校验、AES 解密、断电恢复和出厂分区回退的 Bootloader 升级流程。 + +## 目录结构 + +| 路径 | 说明 | +| --- | --- | +| `apps/chrg/` | RT-Thread 主固件、SCons 配置和 Keil 工程 | +| `apps/chrg/applications/bsp/` | GPIO、继电器、LED、串口封装、FAL、定时器、看门狗和 USB VCOM | +| `apps/chrg/applications/thread/` | 接收、轮询、Modbus 和 LCD 六个常驻业务线程 | +| `apps/chrg/applications/utils/` | Source/Sink 控制、功率板寄存器、协议编解码和通用工具 | +| `apps/chrg/board/` | 时钟、STM32 HAL、CubeMX 配置、驱动端口和链接脚本 | +| `apps/boot/` | 裸机 HAL Bootloader、YModem、固件校验/搬运和 Keil 工程 | +| `libs/` | CMSIS、STM32F4 HAL 和共享驱动 | +| `rt-thread/` | 随仓库维护的 RT-Thread 5.2.2 源码,不应随意做产品级改动 | +| `docs/` | 原理图、引脚表、通信协议、HMI 和调试资料 | +| `bin_pack/` | 升级包及 YModem 相关工具资料 | + +## 通信接口与线程 + +主程序中的接口配置如下: + +| 接口 | 波特率 | 作用 | +| --- | ---: | --- | +| UART1 | 115200, 8N1 | `thr.comm`:上位机 Modbus RTU 服务 | +| UART2 | 921600 | `thr.lcd`:HMI 收发;RT-Thread 初始控制台配置也指向 UART2 | +| UART3 | 230400 | `thr.nor`:快充协议板接收,`thr.rollnor` 负责轮询发送 | +| UART6 | 115200 | `thr.sou`:功率板接收,`thr.rollsou` 负责轮询和批量写寄存器 | +| USB CDC | USB FS | 环境初始化后将 RT-Thread 控制台切换到 `vcom` | + +六个业务线程由 `chrg_thread.c` 统一创建。接收线程通过邮箱传递已解析消息,两个轮询线程通过事件触发,公共数据由互斥锁保护。修改线程优先级、栈大小或 IPC 类型时,应同时检查实时性和内存占用。 + +Bootloader 与主程序的串口用途不同:UART1 以 115200 接收升级数据,UART2 以 115200 输出启动和升级日志。 + +## Flash 布局与启动流程 + +STM32F405 片内 1 MiB Flash 使用三固件分区方案: + +| 分区 | 地址范围 | 大小 | 用途 | +| --- | --- | ---: | --- | +| `boot` | `0x08000000` - `0x0800FFFF` | 64 KiB | Bootloader | +| `param` | `0x08010000` - `0x0801FFFF` | 64 KiB | 参数 | +| `log` | `0x08020000` - `0x0803FFFF` | 128 KiB | 日志预留区 | +| `app` | `0x08040000` - `0x0807FFFF` | 256 KiB | 当前运行固件 | +| `download` | `0x08080000` - `0x080BFFFF` | 256 KiB | 待更新固件 | +| `factory` | `0x080C0000` - `0x080FFFFF` | 256 KiB | 出厂恢复固件 | + +上电后 Bootloader 等待主机约 5 秒,检查升级请求和各分区固件状态;无升级任务且 `app` 校验通过时,设置向量表并跳转到 `0x08040000`。升级过程依次完成包头检查、擦除、写入、CRC/完整性校验和状态落盘,异常时可尝试从 `download` 或 `factory` 恢复。分区地址在 `apps/boot/application/include/app_config.h`、`apps/chrg/board/ports/fal_cfg.h` 和主程序链接脚本中必须保持一致。 + +## 开发环境 + +推荐环境: + +- Windows 10/11; +- Keil MDK 5 和对应 STM32F4 Device Pack; +- Python 3、SCons,以及 ARM Compiler 5 或 `arm-none-eabi-gcc`; +- RT-Thread Env(需要图形化配置时使用)。 + +仓库不附带编译器,`apps/chrg/rtconfig.py` 中的默认 GCC 路径为 `/usr/bin`。Windows 开发机通常需要通过 `RTT_CC` 和 `RTT_EXEC_PATH` 指定实际工具链位置。 + +## 编译主程序 + +### Keil MDK + +直接打开 `apps/chrg/chrg.uvprojx`,选择 `chrg` Target 后 Build。也可从仓库根目录执行: + +```powershell +& 'C:\Keil_v5\UV4\UV4.exe' -b '.\apps\chrg\chrg.uvprojx' +``` + +源文件或 Kconfig 发生变化后,可重新生成 Keil 工程: + +```powershell +Set-Location .\apps\chrg +$env:RTT_CC = 'keil' +$env:RTT_EXEC_PATH = 'C:\Keil_v5' +scons --target=mdk5 +``` + +### SCons + +```powershell +Set-Location .\apps\chrg +$env:RTT_CC = 'gcc' +$env:RTT_EXEC_PATH = 'C:\ArmGNU\bin' # 修改为本机路径 +scons -j4 +``` + +成功后生成 `chrg.elf` 和 `rtthread.bin`。常用辅助命令: + +```powershell +scons --menuconfig # 修改 RT-Thread 功能配置 +scons -c # 清理 SCons 输出 +scons --target=mdk5 # 重新生成 Keil MDK5 工程 +``` + +配置入口包括 `apps/chrg/.config`、`defconfig`、`rtconfig.h` 和 `Kconfig`。修改配置后应检查生成文件差异,避免 `.config`、`defconfig` 与 `rtconfig.h` 长期不一致。 + +## 编译 Bootloader + +Bootloader 当前使用独立 Keil 工程: + +```powershell +& 'C:\Keil_v5\UV4\UV4.exe' -b '.\apps\boot\boot.uvprojx' +``` + +确认链接地址为 `0x08000000`、空间不超过 64 KiB,并在烧写前核对主程序仍链接到 `0x08040000`。不要把 `Objects/`、`build/`、`.axf`、`.bin`、`.hex` 或 `.map` 文件提交到 Git。 + +## 下载、调试与验证 + +1. 首次装机先烧写 Bootloader,再写入与 `app` 分区匹配的主固件或升级包。 +2. 连接 USB CDC 后打开串口终端,可通过 MSH 查看线程、设备和内存状态;主程序提示符为 `chrg `。 +3. 分别验证四个通道的在线状态、通道切换、源/载输出、继电器动作和测量回读。 +4. 使用上位机验证 Modbus 读写、非法地址/功能码响应以及多寄存器写入的生效时序。 +5. 升级相关修改至少覆盖正常升级、CRC 错误、中途断电、无有效 App 和 Factory 恢复场景。 + +仓库没有产品级自动化测试或覆盖率门槛。每次修改至少应完成受影响 Target 的全量编译,并在 STM32F405 实机记录固件版本、板卡版本、串口日志和测试结果。`rt-thread/tools/testcases/` 仅用于 RT-Thread 构建工具,不代表业务固件测试。 + +## 配置与安全注意事项 + +- Bootloader 的分区、包头、CRC、加密参数必须与升级包工具保持一致;任意一侧单独修改都会导致升级失败。 +- 当前 Bootloader 源码包含开发阶段的静态 AES 配置。量产前应替换为受控密钥方案,禁止在公开日志、README 或提交信息中泄露生产密钥。 +- 改动 Modbus 寄存器映射时同步更新 `chrg_regs.h`、上位机、HMI 和 `docs/` 协议文件,并说明兼容策略。 +- `docs/` 中可能同时存在多个版本的协议资料,调试前应核对日期、板卡版本和 Source/Sink 方向。 +- 仓库根目录未提供明确的开源许可证;对外分发源码、固件或第三方组件前,请先确认项目授权和各依赖许可证。 + +## 参与开发 + +编码规范、测试要求以及提交/合并请求约定见 [AGENTS.md](AGENTS.md)。提交前请检查 `git status`,只包含本次修改所需的源码和文档;协议、链接脚本、Flash 分区及升级兼容性变化必须在评审说明中单独列出。 diff --git a/docs/2026-07-17-apps-chrg-deadlock-remediation.md b/docs/2026-07-17-apps-chrg-deadlock-remediation.md new file mode 100644 index 0000000..e7fa109 --- /dev/null +++ b/docs/2026-07-17-apps-chrg-deadlock-remediation.md @@ -0,0 +1,1186 @@ +# apps/chrg 长期运行锁死修复 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 消除 `apps/chrg` 长期运行后的帧组装越界、内存泄漏、南向状态机永久重试和共享状态竞态,并让内部 IWDG 只在全部关键业务线程健康时喂狗。 + +**Architecture:** 先用可观测指标固定故障证据,再将串口分帧、南向命令队列、请求应答关联和线程健康监控拆成边界清晰的模块。业务线程不再通过共享单槽状态传递命令;所有事务必须有队列、截止时间、重试上限和响应匹配,最终由主线程根据六个关键线程的心跳集中决定是否刷新 IWDG。 + +**Tech Stack:** STM32F405RG、RT-Thread 5.2.2、C99、SCons/Keil MDK、RT-Thread UTest、FinSH/MSH、FAL、STM32 HAL IWDG。 + +--- + +## 1. 执行前提 + +当前工作区相对 `HEAD` 存在大量实质性未提交改动,`git diff --ignore-space-at-eol --stat -- apps/chrg` 显示约 5752 行新增、812 行删除。南向批处理逻辑和北向线程第二个喂狗点均位于这些当前改动中。 + +实施本计划前必须满足: + +- 当前工作区由代码所有者创建可恢复的检查点;执行者不得自行回退、暂存或提交不属于本计划的改动。 +- 在包含当前工作区业务改动的基线上实施,不能直接从 `HEAD` 重新开始,否则会遗漏正在现场运行的逻辑。 +- 每次提交只暂存本任务明确列出的文件,禁止使用 `git add -A`、`git add .` 或全仓格式化。 +- 保持现有文件换行格式,不进行 LF/CRLF 批量转换。 +- 必须在具备 SCons + Arm GCC 或 Keil MDK 的构建机上执行编译;当前 Codex 环境没有 `scons` 和 `arm-none-eabi-gcc`。 + +建议的执行顺序严格如下: + +1. 保存故障现场和构建基线。 +2. 建立测试入口。 +3. 修复帧组装内存安全问题。 +4. 重构南向命令队列和有限重试。 +5. 增加请求应答关联。 +6. 收紧互斥量范围和线程初始化。 +7. 引入线程健康监控并调整喂狗。 +8. 完成故障注入和 72 小时稳定性验证。 + +## 2. 文件规划 + +### 新建文件 + +| 文件 | 职责 | +| --- | --- | +| `apps/chrg/applications/Kconfig` | 项目诊断、测试和故障注入配置 | +| `apps/chrg/applications/utils/chrg_frame.h` | 通用分帧状态和所有权接口 | +| `apps/chrg/applications/utils/chrg_frame.c` | 分帧追加、超时、释放和消息移交实现 | +| `apps/chrg/applications/thread/chrg_sou_queue.h` | 南向固定长度命令队列、批次和事务定义 | +| `apps/chrg/applications/thread/chrg_sou_queue.c` | 原子批量入队、出队、批次取消和队列统计 | +| `apps/chrg/applications/utils/chrg_health.h` | 关键线程心跳、错误计数和喂狗判定接口 | +| `apps/chrg/applications/utils/chrg_health.c` | 心跳状态、MSH 健康快照和故障注入实现 | +| `apps/chrg/applications/tests/SConscript` | 仅测试配置下编译 UTest 用例 | +| `apps/chrg/applications/tests/test_chrg_frame.c` | 分包、超时、越界和所有权测试 | +| `apps/chrg/applications/tests/test_chrg_sou_queue.c` | 队列容量、批次原子性、重试和取消测试 | +| `apps/chrg/applications/tests/test_chrg_health.c` | 心跳时限、启动宽限和喂狗判定测试 | +| `docs/debug/apps-chrg-soak-test.md` | 板上故障注入、长期运行和验收记录模板 | + +### 修改文件 + +| 文件 | 修改内容 | +| --- | --- | +| `apps/chrg/Kconfig` | 引入 `applications/Kconfig` | +| `apps/chrg/applications/utils/chrg_def.h` | 邮箱消息增加来源、通道、接收时刻和事务字段 | +| `apps/chrg/applications/thread/chrg_north.c` | 接入安全分帧、检查 RingBuffer 写入、记录 RX 心跳 | +| `apps/chrg/applications/thread/chrg_south.c` | 接入安全分帧、检查 RingBuffer 写入、记录 RX 心跳 | +| `apps/chrg/applications/thread/chrg_roll_nor.c` | 响应关联、缩短锁范围、移除直接喂狗、记录心跳 | +| `apps/chrg/applications/thread/chrg_roll_sou.c` | 使用命令队列、有限重试、响应匹配和自动轮询恢复 | +| `apps/chrg/applications/thread/chrg_roll_sou.h` | 删除共享批处理数组和单槽命令字段,导出提交接口 | +| `apps/chrg/applications/thread/chrg_comm.c` | 构造原子南向批次、检查提交结果、记录线程心跳 | +| `apps/chrg/applications/thread/chrg_lcd.c` | 使用队列提交命令、处理队列满和记录线程心跳 | +| `apps/chrg/applications/utils/chrg_sink.c` | 将分散的 `batch_add` 调用改为一次性批次提交 | +| `apps/chrg/applications/utils/chrg_eload.c` | 提供南向响应匹配和命令编码接口 | +| `apps/chrg/applications/bsp/chrg_tty.c` | 有界获取 TTY 锁、消除事件丢失窗口、返回明确错误 | +| `apps/chrg/applications/bsp/chrg_tty.h` | 锁超时常量和错误语义 | +| `apps/chrg/applications/thread/chrg_thread.c` | 两阶段创建/启动线程、失败回滚和邮箱所有权修正 | +| `apps/chrg/applications/thread/chrg_thread.h` | 线程准备/启动接口和健康标识 | +| `apps/chrg/applications/bsp/chrg_wdt.c` | 记录启动状态、返回喂狗错误、输出复位原因 | +| `apps/chrg/applications/bsp/chrg_wdt.h` | `chrg_wdt_feed()` 改为有返回值并增加 ready 查询 | +| `apps/chrg/applications/main.c` | 集中健康检查和条件喂狗 | +| `apps/chrg/defconfig` | 保留生产健康监控,默认关闭 UTest 和故障注入 | +| `README.md` | 补充线程健康、恢复策略和稳定性验证方法 | + +## 3. 验收标准 + +全部完成必须同时满足: + +- 分帧数据分 1、2、3、N 次到达时,不越界、不泄漏、不重复等待已收齐的数据。 +- 丢半帧后即使不再收到新数据,100 ms 超时仍能释放该帧全部资源。 +- RingBuffer 写不完整时有计数、丢弃当前帧并重新同步,不把截断数据继续交给解析器。 +- 南向单条和批量命令由固定队列传递,多生产者不能交叉覆盖 `ch/reg/value`。 +- 南向事务最多重试 3 次;失败后取消当前批次、记录错误并恢复自动轮询。 +- 延迟旧响应不能推进当前事务;写响应必须匹配地址、功能码、寄存器和值。 +- 业务互斥量不再永久等待;任何获取失败必须返回错误并计数。 +- IWDG 仅在全部六个关键业务线程心跳新鲜且初始化成功时刷新。 +- 暂停任一关键线程后,在最后一次喂狗约 3 秒后发生 IWDG 复位,并在重启日志中看到 IWDG reset cause。 +- 连续 72 小时压力运行中,空闲堆最低值稳定、队列无永久 pending、线程栈余量不持续下降、无异常复位。 +- 主应用和 Bootloader 的链接地址与 Flash 分区不发生变化。 + +--- + +### Task 1: 固定当前基线和现场证据 + +**Files:** +- Create during execution: `docs/debug/apps-chrg-baseline.md` +- Do not modify application source in this task. + +- [ ] **Step 1: 记录工作区基线** + +Run: + +```powershell +git status --short --branch +git diff --ignore-space-at-eol --stat -- apps/chrg +git log -5 --oneline --decorate +``` + +Expected: 输出被保存到 `docs/debug/apps-chrg-baseline.md`,并明确当前业务改动属于哪个检查点。不得执行 reset、checkout 或自动清理。 + +- [ ] **Step 2: 完成修复前构建** + +Run: + +```powershell +scons -C apps/chrg -j4 +``` + +Expected: `chrg.elf` 和 `rtthread.bin` 生成成功。记录 text/data/bss、镜像大小和所有 warning;若失败,先修复构建环境,不开始功能修改。 + +- [ ] **Step 3: 保存故障现场快照** + +在问题出现但 Shell 尚可用时依次执行: + +```text +list_thread +list_mutex +list_mailbox +list_event +free +list_device +``` + +Expected: 记录每个线程状态、栈最大使用、互斥量 owner/suspend、邮箱 entry 数和 heap total/used/max-used。 + +- [ ] **Step 4: 记录故障特征** + +在基线文档中固定以下字段: + +```markdown +- 首次异常运行时长: +- 异常时仍工作的接口:USB Shell / Modbus / LCD / North / South +- 最后 100 行日志: +- `com batch timeout` 是否连续出现:是/否 +- `free` 的 total/used/max-used: +- `thr.rollnor` 和 `thr.rollsou` 状态: +- `thr.nor` 和 `thr.sou` 状态: +- 最近一次复位原因:POR / PIN / SOFTWARE / IWDG / UNKNOWN +``` + +Expected: 不留空字段;无法取得的数据填写 `无法取得:<原因>`。 + +- [ ] **Step 5: 提交仅包含基线文档的检查点** + +```powershell +git add docs/debug/apps-chrg-baseline.md +git commit -m "docs: capture chrg long-run failure baseline" +``` + +Expected: 提交中只有基线文档。 + +--- + +### Task 2: 建立项目级 UTest 测试入口 + +**Files:** +- Create: `apps/chrg/applications/Kconfig` +- Create: `apps/chrg/applications/tests/SConscript` +- Modify: `apps/chrg/Kconfig` +- Modify: `apps/chrg/defconfig` + +- [ ] **Step 1: 在项目 Kconfig 中引入应用配置** + +在 `apps/chrg/Kconfig` 的 RT-Thread 与 board 配置之后加入: + +```kconfig +rsource "applications/Kconfig" +``` + +- [ ] **Step 2: 定义测试和故障注入选项** + +创建 `apps/chrg/applications/Kconfig`: + +```kconfig +menu "Charger application options" + +config CHRG_USING_HEALTH_MONITOR + bool "Enable charger thread health monitor" + default y + +config CHRG_USING_SELFTEST + bool "Build charger UTest cases" + select RT_USING_UTEST + default n + +config CHRG_USING_FAULT_INJECTION + bool "Enable charger fault injection commands" + depends on CHRG_USING_HEALTH_MONITOR + default n + +endmenu +``` + +- [ ] **Step 3: 创建测试构建入口** + +创建 `apps/chrg/applications/tests/SConscript`: + +```python +from building import * + +cwd = GetCurrentDir() +src = Glob('*.c') +group = DefineGroup( + 'Applications/tests', + src, + depend=['CHRG_USING_SELFTEST'], + CPPPATH=[cwd] +) + +Return('group') +``` + +- [ ] **Step 4: 保持生产配置关闭测试功能** + +在 `apps/chrg/defconfig` 中加入: + +```text +CONFIG_CHRG_USING_HEALTH_MONITOR=y +# CONFIG_CHRG_USING_SELFTEST is not set +# CONFIG_CHRG_USING_FAULT_INJECTION is not set +``` + +- [ ] **Step 5: 生成测试配置并构建** + +在测试构建配置中启用 `CHRG_USING_SELFTEST`,然后运行: + +```powershell +scons -C apps/chrg --menuconfig +scons -C apps/chrg -j4 +``` + +Expected: `rtconfig.h` 中存在 `RT_USING_UTEST` 和 `CHRG_USING_SELFTEST`;主应用构建成功。 + +- [ ] **Step 6: 提交测试入口** + +```powershell +git add apps/chrg/Kconfig apps/chrg/applications/Kconfig apps/chrg/applications/tests/SConscript apps/chrg/defconfig +git commit -m "test: add chrg application utest configuration" +``` + +--- + +### Task 3: 用测试锁定分帧越界和泄漏 + +**Files:** +- Create: `apps/chrg/applications/utils/chrg_frame.h` +- Create: `apps/chrg/applications/utils/chrg_frame.c` +- Create: `apps/chrg/applications/tests/test_chrg_frame.c` +- Modify: `apps/chrg/applications/utils/chrg_def.h` + +- [ ] **Step 1: 定义分帧所有权接口** + +`chrg_frame.h` 必须导出以下完整接口: + +```c +#ifndef __CHRG_FRAME_H__ +#define __CHRG_FRAME_H__ + +#include +#include +#include "chrg_def.h" + +enum chrg_frame_status { + CHRG_FRAME_ERROR = -1, + CHRG_FRAME_WAIT = 0, + CHRG_FRAME_COMPLETE = 1, +}; + +int chrg_frame_begin(struct pickup_info_t *frame, + const rt_uint8_t *head, + rt_size_t head_len, + rt_size_t frame_len, + rt_uint8_t origin, + rt_uint8_t channel); +int chrg_frame_append(struct pickup_info_t *frame, + struct rt_ringbuffer *rb); +rt_bool_t chrg_frame_expired(const struct pickup_info_t *frame, + rt_tick_t now, + rt_tick_t timeout); +struct mb_msg_t *chrg_frame_take(struct pickup_info_t *frame); +void chrg_frame_abort(struct pickup_info_t *frame); + +#endif +``` + +扩展 `struct mb_msg_t`: + +```c +struct mb_msg_t { + rt_uint16_t length; + rt_uint8_t *payload; + rt_uint8_t origin; + rt_uint8_t channel; + rt_uint32_t transaction; + rt_tick_t rx_tick; +}; +``` + +- [ ] **Step 2: 先写失败用例:三段分包不能越界** + +`test_chrg_frame.c` 至少包含: + +```c +static void test_frame_three_chunks(void) +{ + struct pickup_info_t frame = {0}; + struct rt_ringbuffer rb; + rt_uint8_t rb_mem[32]; + const rt_uint8_t head[3] = {21, 0, 1}; + const rt_uint8_t body_a[5] = {1, 2, 3, 4, 5}; + const rt_uint8_t body_b[7] = {6, 7, 8, 9, 10, 11, 12}; + const rt_uint8_t body_c[6] = {13, 14, 15, 16, 17, 18}; + + rt_ringbuffer_init(&rb, rb_mem, sizeof(rb_mem)); + uassert_int_equal(chrg_frame_begin(&frame, head, 3, 21, 1, 0), RT_EOK); + + rt_ringbuffer_put(&rb, body_a, sizeof(body_a)); + uassert_int_equal(chrg_frame_append(&frame, &rb), CHRG_FRAME_WAIT); + uassert_int_equal(frame.length, 8); + + rt_ringbuffer_put(&rb, body_b, sizeof(body_b)); + uassert_int_equal(chrg_frame_append(&frame, &rb), CHRG_FRAME_WAIT); + uassert_int_equal(frame.length, 15); + + rt_ringbuffer_put(&rb, body_c, sizeof(body_c)); + uassert_int_equal(chrg_frame_append(&frame, &rb), CHRG_FRAME_COMPLETE); + uassert_int_equal(frame.length, 21); + + chrg_frame_abort(&frame); +} +``` + +- [ ] **Step 3: 增加所有权和超时失败用例** + +必须覆盖: + +```c +static void test_frame_timeout_wrap_safe(void); +static void test_frame_abort_frees_all_state(void); +static void test_frame_take_transfers_message_ownership(void); +static void test_frame_append_never_exceeds_frame_length(void); +static void test_frame_rejects_header_longer_than_frame(void); +static void test_frame_rejects_frame_larger_than_size_buf_tty(void); +``` + +所有用例通过以下入口导出: + +```c +static void test_frame_all(void) +{ + UTEST_UNIT_RUN(test_frame_three_chunks); + UTEST_UNIT_RUN(test_frame_timeout_wrap_safe); + UTEST_UNIT_RUN(test_frame_abort_frees_all_state); + UTEST_UNIT_RUN(test_frame_take_transfers_message_ownership); + UTEST_UNIT_RUN(test_frame_append_never_exceeds_frame_length); + UTEST_UNIT_RUN(test_frame_rejects_header_longer_than_frame); + UTEST_UNIT_RUN(test_frame_rejects_frame_larger_than_size_buf_tty); +} + +UTEST_TC_EXPORT(test_frame_all, "chrg.frame", RT_NULL, RT_NULL, 1000); +``` + +- [ ] **Step 4: 运行测试确认当前实现失败** + +Run on target: + +```text +utest_run chrg.frame +``` + +Expected: 因接口尚无实现或三段分包长度错误而 FAIL。保存失败输出。 + +- [ ] **Step 5: 实现严格的剩余长度和所有权规则** + +`chrg_frame_append()` 的核心必须是: + +```c +int chrg_frame_append(struct pickup_info_t *frame, + struct rt_ringbuffer *rb) +{ + rt_size_t remaining; + rt_size_t available; + rt_size_t copied; + + if (frame == RT_NULL || rb == RT_NULL || frame->pMB == RT_NULL || + frame->pMB->payload == RT_NULL || frame->length > frame->len_msg) { + return CHRG_FRAME_ERROR; + } + + remaining = (rt_size_t)frame->len_msg - frame->length; + if (remaining == 0) { + return CHRG_FRAME_COMPLETE; + } + + available = rt_ringbuffer_data_len(rb); + copied = available < remaining ? available : remaining; + if (copied > 0) { + copied = rt_ringbuffer_get(rb, + &frame->pMB->payload[frame->length], copied); + frame->length += copied; + frame->last_tick = rt_tick_get(); + } + + return frame->length == frame->len_msg + ? CHRG_FRAME_COMPLETE + : CHRG_FRAME_WAIT; +} +``` + +`chrg_frame_expired()` 必须使用无符号差值,不能使用 `now >= last + timeout`: + +```c +return frame->findhead && + (rt_tick_t)(now - frame->last_tick) >= timeout; +``` + +`chrg_frame_abort()` 必须释放 payload、消息对象并清空 `length/findhead/len_msg/pMB/last_tick`。`chrg_frame_take()` 必须只转移消息指针,随后清空 frame 状态但不能释放消息。 + +- [ ] **Step 6: 运行测试确认全部通过** + +```text +utest_run chrg.frame +``` + +Expected: 7 个分帧用例全部 PASS,测试前后 `free` 的 used/max-used 没有持续增长。 + +- [ ] **Step 7: 提交分帧核心和测试** + +```powershell +git add apps/chrg/applications/utils/chrg_frame.h apps/chrg/applications/utils/chrg_frame.c apps/chrg/applications/utils/chrg_def.h apps/chrg/applications/tests/test_chrg_frame.c +git commit -m "fix: make chrg frame assembly memory safe" +``` + +--- + +### Task 4: 将安全分帧接入 North/South 接收线程 + +**Files:** +- Modify: `apps/chrg/applications/thread/chrg_north.c` +- Modify: `apps/chrg/applications/thread/chrg_south.c` +- Modify: `apps/chrg/applications/thread/chrg_thread.c` +- Modify: `apps/chrg/applications/thread/chrg_thread.h` + +- [ ] **Step 1: 在无新数据时也执行分帧超时** + +North 和 South 循环必须先检查: + +```c +if (chrg_frame_expired(&ctx->frame, rt_tick_get(), + rt_tick_from_millisecond(100))) { + chrg_frame_abort(&ctx->frame); + LOG_W("partial frame timeout"); +} +``` + +该检查必须位于 `len <= 0` 的 `continue` 之前。 + +- [ ] **Step 2: 检查 RingBuffer 写入结果** + +替换忽略返回值的 `rt_ringbuffer_put()`: + +```c +written = rt_ringbuffer_put(ctx->rb, ctx->rx_buf, len); +if (written != (rt_size_t)len) { + rt_ringbuffer_reset(ctx->rb); + chrg_frame_abort(&ctx->frame); + LOG_W("receive ringbuffer overflow: %d/%d", written, len); + continue; +} +``` + +- [ ] **Step 3: 用 begin/append/take 替换重复分包逻辑** + +完成帧头识别后调用 `chrg_frame_begin()`;每轮调用 `chrg_frame_append()`;返回 COMPLETE 时调用: + +```c +struct mb_msg_t *msg = chrg_frame_take(&ctx->frame); +if (msg != RT_NULL) { + chrg_thread_mb_send_owned(target, msg); +} +``` + +禁止在 North/South 中直接计算 `len_msg - HEAD_SIZE` 后向 payload 续写。 + +- [ ] **Step 4: 明确邮箱消息所有权** + +新增: + +```c +rt_err_t chrg_thread_mb_send_owned(eIDX_THRS idx, struct mb_msg_t *msg); +``` + +语义固定为:无论发送成功、邮箱为空、邮箱满或其他错误,调用结束后调用者都不再拥有 `msg`。发送失败时函数负责释放 payload 和 msg;发送成功时接收方负责释放。 + +- [ ] **Step 5: 增加分包板上测试** + +通过 UART 测试工具发送: + +1. 完整帧一次发送。 +2. 头 3 字节 + 剩余数据两段发送。 +3. 每字节间隔 1 ms。 +4. 发送半帧后静默 500 ms。 +5. 连续发送 10000 个 CRC 错误帧和截断帧。 + +Expected: 接收线程持续运行;`free` 的 used 值回到稳定区间;frame timeout/overflow 计数按注入增长;无 HardFault 和互斥量异常。 + +- [ ] **Step 6: 构建和提交** + +```powershell +scons -C apps/chrg -j4 +git diff --check -- apps/chrg/applications/thread/chrg_north.c apps/chrg/applications/thread/chrg_south.c apps/chrg/applications/thread/chrg_thread.c apps/chrg/applications/thread/chrg_thread.h +git add apps/chrg/applications/thread/chrg_north.c apps/chrg/applications/thread/chrg_south.c apps/chrg/applications/thread/chrg_thread.c apps/chrg/applications/thread/chrg_thread.h +git commit -m "fix: harden north and south frame receive paths" +``` + +--- + +### Task 5: 用固定队列替换南向共享单槽和无锁批处理 + +**Files:** +- Create: `apps/chrg/applications/thread/chrg_sou_queue.h` +- Create: `apps/chrg/applications/thread/chrg_sou_queue.c` +- Create: `apps/chrg/applications/tests/test_chrg_sou_queue.c` +- Modify: `apps/chrg/applications/thread/chrg_roll_sou.h` + +- [ ] **Step 1: 定义命令和批次模型** + +```c +#define CHRG_SOU_QUEUE_DEPTH 32 +#define CHRG_SOU_RETRY_MAX 3 + +enum chrg_sou_cmd_source { + CHRG_SOU_CMD_AUTO, + CHRG_SOU_CMD_LCD, + CHRG_SOU_CMD_MODBUS, + CHRG_SOU_CMD_INTERNAL, +}; + +struct chrg_sou_cmd { + rt_uint32_t sequence; + rt_uint32_t batch; + rt_uint8_t source; + rt_uint8_t channel; + rt_uint8_t function; + rt_uint8_t attempt; + rt_uint16_t reg; + rt_uint16_t value; + rt_bool_t batch_last; +}; + +struct chrg_sou_queue { + struct chrg_sou_cmd items[CHRG_SOU_QUEUE_DEPTH]; + rt_uint8_t head; + rt_uint8_t tail; + rt_uint8_t count; + rt_uint32_t next_sequence; + rt_uint32_t next_batch; + rt_mutex_t lock; +}; +``` + +导出 `init`、`push`、`push_batch`、`pop`、`drop_batch`、`count`。`push_batch` 必须先检查剩余槽位,空间不足时整批拒绝,不允许部分入队。 + +- [ ] **Step 2: 先写失败的队列测试** + +覆盖: + +```c +static void test_queue_fifo(void); +static void test_queue_batch_is_atomic(void); +static void test_queue_full_rejects_without_overwrite(void); +static void test_queue_drop_batch_keeps_other_batches(void); +static void test_queue_sequence_is_monotonic(void); +static void test_queue_concurrent_producers(void); +``` + +导出: + +```c +UTEST_TC_EXPORT(test_sou_queue_all, "chrg.sou_queue", RT_NULL, RT_NULL, 2000); +``` + +- [ ] **Step 3: 运行测试确认失败** + +```text +utest_run chrg.sou_queue +``` + +Expected: 队列接口尚未实现导致 FAIL。 + +- [ ] **Step 4: 实现固定队列** + +所有读写必须在队列自身 mutex 内完成,mutex 获取超时固定为 100 ms。所有接口返回 `RT_EOK`、`-RT_EFULL`、`-RT_ETIMEOUT` 或 `-RT_EINVAL`,调用者必须处理返回值。 + +禁止: + +- 在锁外修改 head/tail/count。 +- 通过全局 `pROLL->ch/reg/value` 传递命令。 +- 队列满时覆盖旧命令。 +- 为每条命令动态分配内存。 + +- [ ] **Step 5: 运行测试确认通过** + +```text +utest_run chrg.sou_queue +``` + +Expected: 6 个队列用例全部 PASS,队列 count 始终在 0..32 范围内。 + +- [ ] **Step 6: 提交队列模块** + +```powershell +git add apps/chrg/applications/thread/chrg_sou_queue.h apps/chrg/applications/thread/chrg_sou_queue.c apps/chrg/applications/thread/chrg_roll_sou.h apps/chrg/applications/tests/test_chrg_sou_queue.c +git commit -m "feat: add bounded south command queue" +``` + +--- + +### Task 6: 将所有南向命令生产者迁移到队列 + +**Files:** +- Modify: `apps/chrg/applications/thread/chrg_roll_sou.c` +- Modify: `apps/chrg/applications/thread/chrg_roll_sou.h` +- Modify: `apps/chrg/applications/thread/chrg_comm.c` +- Modify: `apps/chrg/applications/thread/chrg_lcd.c` +- Modify: `apps/chrg/applications/utils/chrg_sink.c` + +- [ ] **Step 1: 用返回错误的提交接口替换 void 接口** + +导出: + +```c +int chrg_sou_submit(const struct chrg_sou_cmd *cmd); +int chrg_sou_submit_batch(const struct chrg_sou_cmd *cmds, + rt_size_t count); +``` + +删除 `g_sou_com_batch`、`chrg_sou_com_batch_add_reg()`、`chrg_sou_com_sou_set()` 和 `chrg_set_sou_reg_com()`。`chrg_set_sou_reg()` 改为构造单条命令并返回提交结果。 + +- [ ] **Step 2: 将 Sink 状态切换改为一次原子批次** + +`chrg_sink_work_init()`、`chrg_sink_change_ON()`、`chrg_sink_SHOUT_ON()` 等函数先在栈上构造完整命令数组,再一次调用 `chrg_sou_submit_batch()`。 + +只有提交成功后才更新 `last_state[idx]`: + +```c +ret = chrg_sou_submit_batch(cmds, cmd_count); +if (ret == RT_EOK) { + last_state[idx] = work_mode; +} else { + LOG_E("south batch enqueue failed: %d", ret); +} +``` + +- [ ] **Step 3: 修正 `last_state` 初始化** + +不能使用 `{100}` 期望四个元素都为 100。显式初始化: + +```c +static rt_uint8_t last_state[TOTAL_SOU_CHS] = { + 100, 100, 100, 100 +}; +``` + +- [ ] **Step 4: 迁移 LCD 和 Modbus 调用者** + +每个调用点必须检查返回值: + +```c +ret = chrg_set_sou_reg(channel, reg, value); +if (ret != RT_EOK) { + LOG_E("south command enqueue failed: %d", ret); + return ret; +} +``` + +删除 LCD 中连续 10 次覆盖同一单槽命令的循环,单条设置只入队一次。 + +- [ ] **Step 5: 删除 `RUN_COM` 对共享批处理数组的依赖** + +正常模式下 `thr.rollsou` 每轮先非阻塞检查命令队列:有命令则执行一个事务,无命令才执行自动轮询。`run` 仅保留升级/校准等互斥模式,不再代表单条命令是否存在。 + +- [ ] **Step 6: 构建并验证多生产者顺序** + +同时从 LCD 和 Modbus 连续提交不同通道的 1000 条命令。 + +Expected: 执行顺序与 sequence 一致;`ch/reg/value` 不交叉;queue full 返回错误但不破坏已有命令;自动轮询在队列清空后恢复。 + +- [ ] **Step 7: 提交生产者迁移** + +```powershell +git add apps/chrg/applications/thread/chrg_roll_sou.c apps/chrg/applications/thread/chrg_roll_sou.h apps/chrg/applications/thread/chrg_comm.c apps/chrg/applications/thread/chrg_lcd.c apps/chrg/applications/utils/chrg_sink.c +git commit -m "refactor: serialize south commands through bounded queue" +``` + +--- + +### Task 7: 增加南向请求应答匹配和有限重试 + +**Files:** +- Modify: `apps/chrg/applications/utils/chrg_eload.c` +- Modify: `apps/chrg/applications/utils/chrg_eload.h` +- Modify: `apps/chrg/applications/thread/chrg_roll_sou.c` +- Modify: `apps/chrg/applications/tests/test_chrg_sou_queue.c` + +- [ ] **Step 1: 定义响应匹配接口** + +```c +rt_bool_t chrg_eload_response_matches( + const struct chrg_sou_cmd *cmd, + const rt_uint8_t *frame, + rt_size_t length); +``` + +匹配规则: + +- 地址必须等于 `ID_ELOAD`。 +- CRC 必须通过。 +- 写单寄存器 `0x06` 必须回显相同寄存器和值。 +- 读寄存器必须匹配功能码和预期字节数。 +- Modbus 异常响应必须作为事务失败,不得推进批次。 + +- [ ] **Step 2: 增加延迟旧响应测试** + +测试必须证明:事务 A 超时后,A 的延迟响应不能让事务 B 完成;通道和寄存器不匹配的合法 CRC 帧必须被丢弃并计数。 + +- [ ] **Step 3: 实现单事务截止时间** + +`thr.rollsou` 对每条命令执行: + +```text +send -> wait matching response until 50 ms deadline + matched: complete command + unrelated: free message and continue waiting within same deadline + timeout/error: attempt++ + attempt < 3: wait 20 ms and retry + attempt == 3: drop remaining commands in same batch, report error, resume queue/auto poll +``` + +不能在 CRC 错误或超时后无限保持当前命令。 + +- [ ] **Step 4: 保证消息在所有路径释放** + +匹配成功、无关帧、CRC 错误、异常响应和超时后收到的延迟帧都必须有明确释放路径。用 `free` 的 max-used 和消息分配/释放计数验证收支一致。 + +- [ ] **Step 5: 板上断线恢复测试** + +1. 正常执行一个 6 条命令批次。 +2. 第 3 条命令前断开南向设备。 +3. 确认最多重试 3 次后取消该批次。 +4. 恢复设备。 +5. 确认自动轮询恢复,后续新命令可执行。 + +Expected: 不再连续无限打印 `com batch timeout`;queue count 最终回到 0;业务无需整机复位即可恢复。 + +- [ ] **Step 6: 提交事务匹配和有限重试** + +```powershell +git add apps/chrg/applications/utils/chrg_eload.c apps/chrg/applications/utils/chrg_eload.h apps/chrg/applications/thread/chrg_roll_sou.c apps/chrg/applications/tests/test_chrg_sou_queue.c +git commit -m "fix: bound south retries and match responses" +``` + +--- + +### Task 8: 增加 North 响应元数据和过期帧过滤 + +**Files:** +- Modify: `apps/chrg/applications/utils/chrg_def.h` +- Modify: `apps/chrg/applications/thread/chrg_north.c` +- Modify: `apps/chrg/applications/thread/chrg_roll_nor.c` + +- [ ] **Step 1: 在帧开始时快照通道和接收时刻** + +调用 `chrg_frame_begin()` 时把当前物理通道写入消息 `channel`,完成时保留首字节接收时刻 `rx_tick`。通道不能在消息消费时从全局 `chrgnorth.idx` 重新读取。 + +- [ ] **Step 2: 为每次 North 请求记录事务开始信息** + +在发送前记录: + +```c +active.transaction++; +active.channel = idx; +active.started_at = rt_tick_get(); +active.expected_id = pSW->id; +``` + +- [ ] **Step 3: 等待窗口内过滤不匹配消息** + +North 响应必须满足: + +- `msg->channel == active.channel`。 +- `(rt_tick_t)(msg->rx_tick - active.started_at) < RT_TICK_MAX / 2`。 +- 协议 ID、长度和 CRC 符合当前状态步骤。 + +不匹配消息释放后继续等待,直到 200 ms 总截止时间;不能因为一条旧帧提前结束当前事务。 + +- [ ] **Step 4: 不再用 `pROLL->ch` 解析自动轮询响应** + +自动轮询响应使用消息携带的 channel 或 `active.channel`。`pROLL->ch` 只允许表示人工插入命令目标,不得作为自动轮询当前通道。 + +- [ ] **Step 5: 四通道延迟响应测试** + +人为延迟 CH1 响应,轮询切到 CH2 后再注入 CH1 响应。 + +Expected: CH1 延迟帧被丢弃或归入 CH1,不修改 CH2 状态;CH2 当前事务不会被错误推进。 + +- [ ] **Step 6: 提交 North 事务关联** + +```powershell +git add apps/chrg/applications/utils/chrg_def.h apps/chrg/applications/thread/chrg_north.c apps/chrg/applications/thread/chrg_roll_nor.c +git commit -m "fix: correlate north responses with active transaction" +``` + +--- + +### Task 9: 收紧互斥量范围并修正线程启动顺序 + +**Files:** +- Modify: `apps/chrg/applications/bsp/chrg_tty.c` +- Modify: `apps/chrg/applications/bsp/chrg_tty.h` +- Modify: `apps/chrg/applications/thread/chrg_comm.c` +- Modify: `apps/chrg/applications/thread/chrg_roll_nor.c` +- Modify: `apps/chrg/applications/thread/chrg_thread.c` +- Modify: `apps/chrg/applications/thread/chrg_thread.h` + +- [ ] **Step 1: 为锁定义有界等待** + +```c +#define CHRG_STATE_LOCK_TIMEOUT_MS 100 +#define CHRG_TTY_LOCK_TIMEOUT_MS 250 +``` + +所有 `rt_mutex_take(..., RT_WAITING_FOREVER)` 必须替换为 `rt_tick_from_millisecond()` 转换后的有界值,并检查返回结果。 + +- [ ] **Step 2: 禁止持有状态锁执行 I/O 或延时** + +检查锁内调用,确保锁内只做结构体快照、状态提交或索引更新。以下操作必须在释放状态锁后执行: + +- `chrg_tty_send()` / `chrg_tty_recv()`。 +- `rt_event_recv()` / `rt_mb_recv()`。 +- `rt_thread_mdelay()`。 +- `rt_kprintf()` / `LOG_*()`。 +- Source/Sink/Eload 实际报文发送。 + +- [ ] **Step 3: 缩短 Modbus 参数生效临界区** + +`chrg_comm.c` 的 `SYS_REG_PARAM_EFFECT` 路径在锁内只完成实际结构体更新和动作描述生成;释放锁后再提交南向批次或触发后续状态机。任何错误路径都通过单一 cleanup 出口释放锁。 + +- [ ] **Step 4: 消除 TTY 事件丢失窗口** + +当前“先读为空、再 RESET event、再等待”可能清除刚到达的 RX 事件。调整为: + +1. 清事件。 +2. 再次读取设备。 +3. 第二次仍为空才等待事件。 + +接收等待仍有 `timeout/byte_tmo` 上限;发送锁获取超过 250 ms 返回 `-RT_ETIMEOUT` 并计数。 + +- [ ] **Step 5: 线程初始化改为两阶段** + +第一阶段为六个线程全部创建 mutex/mailbox/event;第二阶段才创建和启动线程。禁止创建一个线程后立即启动、再继续准备后续线程。 + +拆分接口: + +```c +static int chrg_thread_prepare(eIDX_THRS idx); +static int chrg_thread_start(eIDX_THRS idx); +static void chrg_thread_cleanup(eIDX_THRS idx); +``` + +任一准备或启动失败必须返回错误,清理已创建但未启动的对象,并输出具体失败线程和错误码。`rt_thread_create()` 返回 NULL 时不能继续返回 `RT_EOK`。健康错误计数在 Task 10 接入。 + +- [ ] **Step 6: 锁超时和启动失败测试** + +测试构建中故意占用状态 mutex 500 ms,确认调用者在 100 ms 返回错误而不是永久等待。故意让一个线程分配失败,确认初始化报告具体线程且不会访问尚未创建的后续 mutex。 + +- [ ] **Step 7: 构建和提交** + +```powershell +scons -C apps/chrg -j4 +git diff --check -- apps/chrg/applications/bsp/chrg_tty.c apps/chrg/applications/bsp/chrg_tty.h apps/chrg/applications/thread/chrg_comm.c apps/chrg/applications/thread/chrg_roll_nor.c apps/chrg/applications/thread/chrg_thread.c apps/chrg/applications/thread/chrg_thread.h +git add apps/chrg/applications/bsp/chrg_tty.c apps/chrg/applications/bsp/chrg_tty.h apps/chrg/applications/thread/chrg_comm.c apps/chrg/applications/thread/chrg_roll_nor.c apps/chrg/applications/thread/chrg_thread.c apps/chrg/applications/thread/chrg_thread.h +git commit -m "fix: bound chrg locks and start threads in two phases" +``` + +--- + +### Task 10: 增加关键线程健康监控 + +**Files:** +- Create: `apps/chrg/applications/utils/chrg_health.h` +- Create: `apps/chrg/applications/utils/chrg_health.c` +- Create: `apps/chrg/applications/tests/test_chrg_health.c` +- Modify: all six thread entry files under `apps/chrg/applications/thread/` +- Modify: `apps/chrg/applications/bsp/chrg_tty.c` +- Modify: `apps/chrg/applications/thread/chrg_thread.c` + +- [ ] **Step 1: 定义六个关键健康节点** + +```c +enum chrg_health_node { + CHRG_HEALTH_NORTH_RX, + CHRG_HEALTH_SOUTH_RX, + CHRG_HEALTH_COMM, + CHRG_HEALTH_LCD, + CHRG_HEALTH_ROLL_NORTH, + CHRG_HEALTH_ROLL_SOUTH, + CHRG_HEALTH_NODE_COUNT +}; + +enum chrg_health_error { + CHRG_HEALTH_ERR_FRAME_TIMEOUT, + CHRG_HEALTH_ERR_RING_OVERFLOW, + CHRG_HEALTH_ERR_SOU_QUEUE_FULL, + CHRG_HEALTH_ERR_SOU_RETRY_DROP, + CHRG_HEALTH_ERR_MAILBOX_DROP, + CHRG_HEALTH_ERR_MUTEX_TIMEOUT, + CHRG_HEALTH_ERR_THREAD_INIT, + CHRG_HEALTH_ERR_WDT_FEED, + CHRG_HEALTH_ERR_COUNT +}; + +#define CHRG_HEALTH_STARTUP_GRACE_MS 10000 +#define CHRG_HEALTH_STALE_MS 2000 + +void chrg_health_init(void); +void chrg_health_beat(enum chrg_health_node node); +void chrg_health_error(enum chrg_health_error error); +rt_bool_t chrg_health_all_fresh(rt_tick_t now); +rt_uint32_t chrg_health_stale_mask(rt_tick_t now); +``` + +- [ ] **Step 2: 写健康判定失败测试** + +覆盖:全部新鲜、单线程过期、tick 回绕、启动宽限、初始化失败、故障注入冻结和错误计数饱和。 + +```c +UTEST_TC_EXPORT(test_health_all, "chrg.health", RT_NULL, RT_NULL, 1000); +``` + +- [ ] **Step 3: 实现无阻塞心跳更新** + +心跳只写入对齐的 `rt_tick_t` 和循环计数。用短临界区保护快照,不允许在健康模块中分配内存、等待 mutex 或输出日志。 + +- [ ] **Step 4: 在正确的进度点打心跳** + +- RX 线程:每次 `chrg_tty_recv()` 返回并完成超时/数据处理后。 +- Comm:每次接收超时或完成一帧解析后。 +- LCD:每次接收超时或完成一帧解析后。 +- Roll North:每次事务完成、超时处理或空闲轮询后。 +- Roll South:每次队列事务完成/失败或自动轮询后。 + +不能在进入可能永久阻塞的调用之前打心跳。 + +- [ ] **Step 5: 将前序任务的错误路径接入统一计数** + +将 Task 4、6、7、9 中已经具有明确错误返回的路径分别映射到 `FRAME_TIMEOUT`、`RING_OVERFLOW`、`SOU_QUEUE_FULL`、`SOU_RETRY_DROP`、`MAILBOX_DROP`、`MUTEX_TIMEOUT` 和 `THREAD_INIT`。计数失败不能改变原错误路径,也不能在中断中输出日志。 + +- [ ] **Step 6: 导出 `chrg_health` MSH 命令** + +输出固定字段:node、age_ms、loop_count、stale、last_error、frame_timeout、ring_overflow、queue_full、retry_drop、mailbox_drop、mutex_timeout、heap_used/max-used。 + +- [ ] **Step 7: 增加测试构建故障注入** + +仅在 `CHRG_USING_FAULT_INJECTION` 下导出: + +```text +chrg_health_freeze +chrg_health_unfreeze +``` + +冻结只停止对应节点的心跳更新,不关闭中断或破坏内存,用于验证看门狗决策。 + +- [ ] **Step 8: 运行健康测试** + +```text +utest_run chrg.health +chrg_health +``` + +Expected: UTest 全部 PASS;正常运行时 stale mask 为 0。 + +- [ ] **Step 9: 提交健康模块** + +```powershell +git add apps/chrg/applications/utils/chrg_health.h apps/chrg/applications/utils/chrg_health.c apps/chrg/applications/tests/test_chrg_health.c apps/chrg/applications/thread/chrg_north.c apps/chrg/applications/thread/chrg_south.c apps/chrg/applications/thread/chrg_comm.c apps/chrg/applications/thread/chrg_lcd.c apps/chrg/applications/thread/chrg_roll_nor.c apps/chrg/applications/thread/chrg_roll_sou.c apps/chrg/applications/bsp/chrg_tty.c apps/chrg/applications/thread/chrg_thread.c +git commit -m "feat: monitor chrg business thread health" +``` + +--- + +### Task 11: 改为集中条件喂狗并记录复位原因 + +**Files:** +- Modify: `apps/chrg/applications/bsp/chrg_wdt.c` +- Modify: `apps/chrg/applications/bsp/chrg_wdt.h` +- Modify: `apps/chrg/applications/main.c` +- Modify: `apps/chrg/applications/thread/chrg_roll_nor.c` +- Modify: `apps/chrg/applications/utils/chrg_health.c` + +- [ ] **Step 1: 让 WDT API 可验证** + +接口改为: + +```c +int chrg_wdt_init(void); +rt_err_t chrg_wdt_feed(void); +rt_bool_t chrg_wdt_is_ready(void); +rt_uint32_t chrg_wdt_reset_cause(void); +``` + +`chrg_wdt_feed()` 必须检查 `dev` 和 ready 状态并返回 `rt_device_control()` 结果,不能静默忽略失败。 + +- [ ] **Step 2: 启动时读取并清除 RCC reset flags** + +记录至少:Power-on、Pin、Software、IWDG、WWDG、Brownout。先保存标志,再调用 RCC clear reset flags。启动日志输出一次上次复位原因。 + +- [ ] **Step 3: 删除所有分散喂狗点** + +删除 `chrg_roll_nor_thread_entry()` 内的 `chrg_wdt_feed()`。全仓搜索必须只剩主线程调用: + +```powershell +rg -n "chrg_wdt_feed\(" apps/chrg +``` + +Expected: 一个实现、一个声明、一个主线程调用。 + +- [ ] **Step 4: 主线程根据健康状态喂狗** + +```c +while (1) { + rt_thread_mdelay(250); + if (chrg_wdt_is_ready() && chrg_health_all_fresh(rt_tick_get())) { + if (chrg_wdt_feed() != RT_EOK) { + chrg_health_error(CHRG_HEALTH_ERR_WDT_FEED); + } + } +} +``` + +健康过期后只记录一次 stale mask,随后停止喂狗,等待 IWDG 硬复位。禁止业务线程自行恢复喂狗。 + +- [ ] **Step 5: 验证单线程停滞触发复位** + +测试构建执行: + +```text +chrg_health_freeze roll_south +``` + +Expected: 2 秒后 health 判定 stale,停止刷新 IWDG;最后一次刷新约 3 秒后复位;重启日志显示 IWDG reset cause。 + +分别对六个节点执行一次,均必须复位。 + +- [ ] **Step 6: 验证正常长操作不会误复位** + +执行正常 Modbus、LCD、North/South 轮询、队列批量操作和 USB 断连。Expected: 所有节点 age 小于 2 秒,IWDG 不误触发。 + +- [ ] **Step 7: 确认外部看门狗边界** + +当前代码和 GPIO 定义中没有外部 WDI 驱动。本计划只修正 STM32 内部 IWDG。硬件若确有外部看门狗,必须由硬件负责人从原理图确认 WDI 引脚、有效沿、最短/最长喂狗窗口和复位连接后单独立项;在这些参数未确认前不得复用任意 GPIO 模拟外部喂狗。 + +- [ ] **Step 8: 构建和提交** + +```powershell +scons -C apps/chrg -j4 +git diff --check -- apps/chrg/applications/bsp/chrg_wdt.c apps/chrg/applications/bsp/chrg_wdt.h apps/chrg/applications/main.c apps/chrg/applications/thread/chrg_roll_nor.c apps/chrg/applications/utils/chrg_health.c +git add apps/chrg/applications/bsp/chrg_wdt.c apps/chrg/applications/bsp/chrg_wdt.h apps/chrg/applications/main.c apps/chrg/applications/thread/chrg_roll_nor.c apps/chrg/applications/utils/chrg_health.c +git commit -m "fix: feed iwdg only when all chrg threads are healthy" +``` + +--- + +### Task 12: 完成故障注入和 72 小时稳定性验证 + +**Files:** +- Create: `docs/debug/apps-chrg-soak-test.md` +- Modify: `README.md` + +- [ ] **Step 1: 建立测试记录表** + +记录项必须包含:固件 commit、编译器版本、镜像大小、板卡编号、外设固件版本、开始/结束时间、复位次数、reset cause、heap used/max-used、各线程最小剩余栈、各错误计数和最终结论。 + +- [ ] **Step 2: 执行 2 小时通信压力测试** + +并发执行: + +- UART1 持续 Modbus 读写,覆盖四通道。 +- LCD 持续状态刷新和人工设置。 +- North/South 四通道自动轮询。 +- 每分钟注入一次 CRC 错误、截断帧和延迟帧。 + +Expected: 无队列永久 pending;heap used 不呈单调增长;frame abort 后分配/释放计数平衡。 + +- [ ] **Step 3: 执行南向断线恢复矩阵** + +分别在单条命令、批次第 1 条、第 3 条和最后 1 条时断开南向设备。每种情况持续 10 秒后恢复。 + +Expected: 每个失败事务最多重试 3 次;当前批次取消;设备恢复后自动轮询和新命令恢复。 + +- [ ] **Step 4: 执行看门狗故障矩阵** + +逐一冻结六个健康节点并确认 IWDG 复位。随后关闭故障注入配置重新构建生产固件,确认生产镜像不导出 freeze 命令。 + +- [ ] **Step 5: 执行 72 小时稳定运行** + +每小时采集: + +```text +chrg_health +list_thread +list_mutex +list_mailbox +free +``` + +通过条件: + +- 无未解释复位。 +- heap used 在热身后保持有界波动,不单调增长。 +- max-used 可增长到稳定峰值,但剩余 heap 始终高于基线定义的安全下限。 +- 六个线程栈余量不低于各自栈的 20%。 +- queue count 能回到 0,任何批次都能完成或在 3 次重试后取消。 +- mutex 没有长期 owner/waiter 不变化。 + +- [ ] **Step 6: 更新 README** + +写明: + +- `chrg_health` 输出解释。 +- 内部 IWDG 的集中喂狗策略。 +- 南向队列深度、重试上限和批次失败语义。 +- 分帧 timeout/overflow 计数含义。 +- 外部看门狗当前不在软件实现范围内。 + +- [ ] **Step 7: 最终构建、检查和提交** + +```powershell +scons -C apps/chrg -c +scons -C apps/chrg -j4 +git diff --check -- README.md docs/debug/apps-chrg-soak-test.md +git status --short +git add README.md docs/debug/apps-chrg-soak-test.md +git commit -m "docs: document chrg health and soak verification" +``` + +Expected: 主应用构建成功、镜像不超过 256 KiB、文档记录完整、提交中没有 IDE 状态或无关换行变化。 + +--- + +## 4. 回退策略 + +每个任务必须是独立提交,回退只能按提交逆序进行: + +1. 文档和测试记录可单独回退,不影响固件行为。 +2. 看门狗健康策略回退时,必须同时恢复唯一喂狗点,禁止出现无喂狗或多个无条件喂狗点。 +3. 南向事务匹配必须与命令队列一起回退,不能保留新消费者配旧共享批处理数组。 +4. 分帧核心与 North/South 接入必须一起回退,避免消息所有权语义不一致。 +5. 禁止通过恢复 `RT_WAITING_FOREVER` 或无限重试作为临时绕过方案。 + +如某阶段板测失败,先保留失败日志和对应 commit,回退该阶段后重新验证上一阶段仍稳定,再根据单一失败假设继续。 + +## 5. 计划自检 + +- 覆盖了已识别的分帧越界、超时泄漏、payload 分配失败泄漏和 RingBuffer 静默丢数据。 +- 覆盖了南向共享单槽、无锁批处理、无限重试和旧响应错配。 +- 覆盖了 North 通道错配和延迟响应问题。 +- 覆盖了永久 mutex 等待、锁内 I/O 和线程依赖对象尚未创建即启动的问题。 +- 覆盖了 IWDG 多点无条件喂狗、启动失败不可见和复位原因缺失。 +- 明确区分内部 IWDG 与尚无软件证据的外部看门狗。 +- 所有生产行为变更均有对应 UTest、板测和长期运行验收项。