1187 lines
42 KiB
Markdown
1187 lines
42 KiB
Markdown
# 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: 无队列永久 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、板测和长期运行验收项。
|