42 KiB
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。
建议的执行顺序严格如下:
- 保存故障现场和构建基线。
- 建立测试入口。
- 修复帧组装内存安全问题。
- 重构南向命令队列和有限重试。
- 增加请求应答关联。
- 收紧互斥量范围和线程初始化。
- 引入线程健康监控并调整喂狗。
- 完成故障注入和 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:
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:
scons -C apps/chrg -j4
Expected: chrg.elf 和 rtthread.bin 生成成功。记录 text/data/bss、镜像大小和所有 warning;若失败,先修复构建环境,不开始功能修改。
- Step 3: 保存故障现场快照
在问题出现但 Shell 尚可用时依次执行:
list_thread
list_mutex
list_mailbox
list_event
free
list_device
Expected: 记录每个线程状态、栈最大使用、互斥量 owner/suspend、邮箱 entry 数和 heap total/used/max-used。
- Step 4: 记录故障特征
在基线文档中固定以下字段:
- 首次异常运行时长:
- 异常时仍工作的接口: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: 提交仅包含基线文档的检查点
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 配置之后加入:
rsource "applications/Kconfig"
- Step 2: 定义测试和故障注入选项
创建 apps/chrg/applications/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:
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 中加入:
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,然后运行:
scons -C apps/chrg --menuconfig
scons -C apps/chrg -j4
Expected: rtconfig.h 中存在 RT_USING_UTEST 和 CHRG_USING_SELFTEST;主应用构建成功。
- Step 6: 提交测试入口
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 必须导出以下完整接口:
#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:
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 至少包含:
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: 增加所有权和超时失败用例
必须覆盖:
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);
所有用例通过以下入口导出:
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:
utest_run chrg.frame
Expected: 因接口尚无实现或三段分包长度错误而 FAIL。保存失败输出。
- Step 5: 实现严格的剩余长度和所有权规则
chrg_frame_append() 的核心必须是:
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:
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: 运行测试确认全部通过
utest_run chrg.frame
Expected: 7 个分帧用例全部 PASS,测试前后 free 的 used/max-used 没有持续增长。
- Step 7: 提交分帧核心和测试
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 循环必须先检查:
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():
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 时调用:
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: 明确邮箱消息所有权
新增:
rt_err_t chrg_thread_mb_send_owned(eIDX_THRS idx, struct mb_msg_t *msg);
语义固定为:无论发送成功、邮箱为空、邮箱满或其他错误,调用结束后调用者都不再拥有 msg。发送失败时函数负责释放 payload 和 msg;发送成功时接收方负责释放。
- Step 5: 增加分包板上测试
通过 UART 测试工具发送:
- 完整帧一次发送。
- 头 3 字节 + 剩余数据两段发送。
- 每字节间隔 1 ms。
- 发送半帧后静默 500 ms。
- 连续发送 10000 个 CRC 错误帧和截断帧。
Expected: 接收线程持续运行;free 的 used 值回到稳定区间;frame timeout/overflow 计数按注入增长;无 HardFault 和互斥量异常。
- Step 6: 构建和提交
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: 定义命令和批次模型
#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: 先写失败的队列测试
覆盖:
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);
导出:
UTEST_TC_EXPORT(test_sou_queue_all, "chrg.sou_queue", RT_NULL, RT_NULL, 2000);
- Step 3: 运行测试确认失败
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: 运行测试确认通过
utest_run chrg.sou_queue
Expected: 6 个队列用例全部 PASS,队列 count 始终在 0..32 范围内。
- Step 6: 提交队列模块
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 接口
导出:
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]:
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。显式初始化:
static rt_uint8_t last_state[TOTAL_SOU_CHS] = {
100, 100, 100, 100
};
- Step 4: 迁移 LCD 和 Modbus 调用者
每个调用点必须检查返回值:
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: 提交生产者迁移
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: 定义响应匹配接口
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 对每条命令执行:
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: 板上断线恢复测试
- 正常执行一个 6 条命令批次。
- 第 3 条命令前断开南向设备。
- 确认最多重试 3 次后取消该批次。
- 恢复设备。
- 确认自动轮询恢复,后续新命令可执行。
Expected: 不再连续无限打印 com batch timeout;queue count 最终回到 0;业务无需整机复位即可恢复。
- Step 6: 提交事务匹配和有限重试
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 请求记录事务开始信息
在发送前记录:
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 事务关联
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: 为锁定义有界等待
#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 事件。调整为:
- 清事件。
- 再次读取设备。
- 第二次仍为空才等待事件。
接收等待仍有 timeout/byte_tmo 上限;发送锁获取超过 250 ms 返回 -RT_ETIMEOUT 并计数。
- Step 5: 线程初始化改为两阶段
第一阶段为六个线程全部创建 mutex/mailbox/event;第二阶段才创建和启动线程。禁止创建一个线程后立即启动、再继续准备后续线程。
拆分接口:
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: 构建和提交
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: 定义六个关键健康节点
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 回绕、启动宽限、初始化失败、故障注入冻结和错误计数饱和。
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_healthMSH 命令
输出固定字段: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 下导出:
chrg_health_freeze <north_rx|south_rx|comm|lcd|roll_north|roll_south>
chrg_health_unfreeze <node>
冻结只停止对应节点的心跳更新,不关闭中断或破坏内存,用于验证看门狗决策。
- Step 8: 运行健康测试
utest_run chrg.health
chrg_health
Expected: UTest 全部 PASS;正常运行时 stale mask 为 0。
- Step 9: 提交健康模块
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 可验证
接口改为:
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()。全仓搜索必须只剩主线程调用:
rg -n "chrg_wdt_feed\(" apps/chrg
Expected: 一个实现、一个声明、一个主线程调用。
- Step 4: 主线程根据健康状态喂狗
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: 验证单线程停滞触发复位
测试构建执行:
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: 构建和提交
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 小时稳定运行
每小时采集:
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: 最终构建、检查和提交
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. 回退策略
每个任务必须是独立提交,回退只能按提交逆序进行:
- 文档和测试记录可单独回退,不影响固件行为。
- 看门狗健康策略回退时,必须同时恢复唯一喂狗点,禁止出现无喂狗或多个无条件喂狗点。
- 南向事务匹配必须与命令队列一起回退,不能保留新消费者配旧共享批处理数组。
- 分帧核心与 North/South 接入必须一起回退,避免消息所有权语义不一致。
- 禁止通过恢复
RT_WAITING_FOREVER或无限重试作为临时绕过方案。
如某阶段板测失败,先保留失败日志和对应 commit,回退该阶段后重新验证上一阶段仍稳定,再根据单一失败假设继续。
5. 计划自检
- 覆盖了已识别的分帧越界、超时泄漏、payload 分配失败泄漏和 RingBuffer 静默丢数据。
- 覆盖了南向共享单槽、无锁批处理、无限重试和旧响应错配。
- 覆盖了 North 通道错配和延迟响应问题。
- 覆盖了永久 mutex 等待、锁内 I/O 和线程依赖对象尚未创建即启动的问题。
- 覆盖了 IWDG 多点无条件喂狗、启动失败不可见和复位原因缺失。
- 明确区分内部 IWDG 与尚无软件证据的外部看门狗。
- 所有生产行为变更均有对应 UTest、板测和长期运行验收项。