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

42 KiB
Raw Permalink Blame History

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 -Agit add . 或全仓格式化。
  • 保持现有文件换行格式,不进行 LF/CRLF 批量转换。
  • 必须在具备 SCons + Arm GCC 或 Keil MDK 的构建机上执行编译;当前 Codex 环境没有 sconsarm-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:

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.elfrtthread.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_UTESTCHRG_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_tickchrg_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 <= 0continue 之前。

  • 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 测试工具发送:

  1. 完整帧一次发送。
  2. 头 3 字节 + 剩余数据两段发送。
  3. 每字节间隔 1 ms。
  4. 发送半帧后静默 500 ms。
  5. 连续发送 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;
};

导出 initpushpush_batchpopdrop_batchcountpush_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_batchchrg_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: 板上断线恢复测试
  1. 正常执行一个 6 条命令批次。
  2. 第 3 条命令前断开南向设备。
  3. 确认最多重试 3 次后取消该批次。
  4. 恢复设备。
  5. 确认自动轮询恢复,后续新命令可执行。

Expected: 不再连续无限打印 com batch timeoutqueue 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.channelpROLL->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.cSYS_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;第二阶段才创建和启动线程。禁止创建一个线程后立即启动、再继续准备后续线程。

拆分接口:

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_TIMEOUTRING_OVERFLOWSOU_QUEUE_FULLSOU_RETRY_DROPMAILBOX_DROPMUTEX_TIMEOUTTHREAD_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 下导出:

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: 无队列永久 pendingheap 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. 回退策略

每个任务必须是独立提交,回退只能按提交逆序进行:

  1. 文档和测试记录可单独回退,不影响固件行为。
  2. 看门狗健康策略回退时,必须同时恢复唯一喂狗点,禁止出现无喂狗或多个无条件喂狗点。
  3. 南向事务匹配必须与命令队列一起回退,不能保留新消费者配旧共享批处理数组。
  4. 分帧核心与 North/South 接入必须一起回退,避免消息所有权语义不一致。
  5. 禁止通过恢复 RT_WAITING_FOREVER 或无限重试作为临时绕过方案。

如某阶段板测失败,先保留失败日志和对应 commit,回退该阶段后重新验证上一阶段仍稳定,再根据单一失败假设继续。

5. 计划自检

  • 覆盖了已识别的分帧越界、超时泄漏、payload 分配失败泄漏和 RingBuffer 静默丢数据。
  • 覆盖了南向共享单槽、无锁批处理、无限重试和旧响应错配。
  • 覆盖了 North 通道错配和延迟响应问题。
  • 覆盖了永久 mutex 等待、锁内 I/O 和线程依赖对象尚未创建即启动的问题。
  • 覆盖了 IWDG 多点无条件喂狗、启动失败不可见和复位原因缺失。
  • 明确区分内部 IWDG 与尚无软件证据的外部看门狗。
  • 所有生产行为变更均有对应 UTest、板测和长期运行验收项。