# 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、板测和长期运行验收项。