Files
chrg/docs/2026-07-17-apps-chrg-deadlock-remediation.md
yhf 5ffc85077e docs: 初始化HCDF MID 405充放电控制器固件仓库
添加完整的项目README文档,包含系统架构、功能说明、目录结构、通信接口、Flash布局、开发编译流程、下载验证和注意事项等内容。
2026-07-18 15:38:54 +08:00

1187 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <rtthread.h>
#include <rtdevice.h>
#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 <north_rx|south_rx|comm|lcd|roll_north|roll_south>
chrg_health_unfreeze <node>
```
冻结只停止对应节点的心跳更新,不关闭中断或破坏内存,用于验证看门狗决策。
- [ ] **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: 无队列永久 pendingheap 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、板测和长期运行验收项。