Files
ASER/Doc/SD日志方案.md
2026-04-04 14:49:37 +08:00

11 KiB
Raw Blame History

ARES SD 日志方案

1. 结论

对当前项目,最可行的一期方案是:

  • SDMMC1 + FatFs 挂载 SD 卡。
  • 新增一个独立的 logTask 作为唯一写卡任务。
  • navTaskmonitorTaskimuTask 等任务只负责把日志记录写入 RAM 队列,绝不直接 f_write()
  • 日志格式采用“事件 + 低频快照”的文本 CSV一期先保证稳定可读不追求全量高频原始数据。

这个方案最适合当前仓库,因为:

  • 现有高频闭环在 App/app_tasks.cnavTask 中以 20ms 周期运行,不能被 SD 写卡阻塞。
  • 当前日志只有 printf -> USB CDC,没有 FatFs/SDMMC 基础设施。
  • 当前系统没有 RTC日志只能基于 HAL_GetTick() 记录相对时间,文件名也不能依赖真实日期。
  • CMakeLists.txt 已经自动收集 App/*.c,新增 App/log/*.c 不需要手工改源文件列表。

2. 结合当前项目的约束

2.1 现有可直接复用的数据源

当前代码里已经有足够多的“运行状态”可用于日志:

  • 黑板快照:App/Contract/robot_blackboard.h
  • 走廊观测:CorridorObs_t,定义在 App/preproc/corridor_msgs.h
  • EKF 状态:CorridorState_t,定义在 App/preproc/corridor_msgs.h
  • 赛道状态机输出:GlobalNavOutput_t,定义在 App/nav/global_nav.h
  • 安全状态机输出:SegFsmOutput_t,定义在 App/nav/segment_fsm.h

这意味着日志不需要额外造一套状态模型,直接复用现有结构即可。

2.2 当前不适合的做法

不建议直接把现在的 printf 改成“同时写 USB 和 SD”

  • printf 现在走 App/retarget.c_write(),底层是 USB CDC。
  • 如果把 _write() 直接改成写文件,会让所有任务都可能进入 FatFs线程模型会变复杂。
  • 文本格式化和写卡延迟都可能拖慢闭环任务。

不建议一期就做“每 20ms 全量原始数据落盘”:

  • 对排障来说,状态转移、关键变量、故障瞬间比全量采样更有价值。
  • 高频写卡会更容易遇到 H7 的缓存一致性、FatFs 抖动、掉电损坏等问题。

3. 推荐总体架构

3.1 模块划分

建议新增目录:

  • App/log/sd_log.h
  • App/log/sd_log.c
  • App/log/sd_log_port.cApp/log/sd_log_fs.c

职责划分:

  • sd_log.c日志队列、记录封装、CSV 格式化、刷盘策略。
  • sd_log_fs.cFatFs mount/open/write/sync/rotate。
  • app_tasks.c:在现有任务周期内调用 SDLog_TryPush...()

3.2 任务模型

新增一个低优先级 logTask

  • 优先级建议低于 navTaskcanTxTask
  • 周期不固定,阻塞等待日志队列
  • 它是系统里唯一允许调用 f_mount/f_open/f_write/f_sync/f_close 的任务

生产者任务:

  • navTask:产出导航快照、状态切换、故障事件
  • monitorTask:产出 CAN 在线状态、底盘诊断、里程计健康事件
  • 启动阶段:产出 boot 配置、固件版本、参数摘要

核心原则:

  • 生产者只入队,不阻塞
  • 队列满时允许丢弃低优先级快照,但不能卡住控制任务
  • 重要事件单独计数,后续在日志里补写 drop_count

3.3 内存缓冲

一期建议用固定长度消息队列,而不是动态内存:

  • 例如 osMessageQueue,每条记录 192B 或 256B
  • 队列深度 32 到 64 条
  • 总 RAM 占用约 8KB 到 16KB可控

推荐记录结构:

typedef enum {
    LOG_REC_BOOT = 0,
    LOG_REC_HEALTH,
    LOG_REC_NAV_SNAPSHOT,
    LOG_REC_STAGE_EVENT,
    LOG_REC_FAULT,
} LogRecordType_t;

typedef struct {
    uint32_t tick_ms;
    uint16_t type;
    uint16_t len;
    char text[224];
} LogRecord_t;

这里故意用“预格式化文本入队”,因为:

  • 一期更简单,便于直接在 SD 卡上看内容
  • logTask 只做批量写盘,不做复杂格式化
  • 后续如果需要高频日志,再把 text 改成二进制 payload

4. 推荐日志内容

4.1 一期必须记录的内容

建议只做三类:

  1. 启动信息
  2. 周期性状态快照
  3. 异常/状态切换事件

4.2 启动信息

系统启动后创建文件,先写以下内容:

  • 固件名、编译日期时间
  • USE_GLOBAL_NAV 当前值
  • 关键参数摘要
  • SD 卡 mount 是否成功
  • 日志格式版本号

示例:

# ARES log v1
# build=Apr 03 2026 12:00:00
# mode=global_nav
# param.ctrl_v=0.20
# param.safe_front_stop=0.10

4.3 周期性状态快照

建议由 navTask 每 100ms 记录一次,不要每 20ms 都记。

推荐字段:

  • tick_ms
  • stage / stage_name
  • seg_state
  • request_corridor
  • override_v / override_w
  • safe_v / safe_w
  • odom_vx / odom_wz
  • e_y / e_th / s / conf
  • d_front / d_back
  • d_lf / d_lr / d_rf / d_rr
  • valid_mask
  • chassis_online / chassis_diag

推荐 CSV 行示例:

12340,SNAP,GNAV_CORRIDOR_TRACK,CORRIDOR,1,0.000,0.000,0.180,-0.120,0.182,0.01,-0.03,1.42,0.92,0.31,1.85,0.12,0.11,0.10,0.10,0x3F,1,0x00000000

4.4 事件日志

以下情况必须立即记录,而不是等下一个 100ms 快照:

  • GlobalNav 阶段切换
  • SegFsm 状态切换
  • 进入 GNAV_ERROR
  • 进入 SEG_STATE_ESTOP
  • 底盘离线 / 恢复在线
  • SD 卡 mount 失败 / 写入失败 / 队列溢出

推荐示例:

12520,EVENT,GNAV_STAGE,GNAV_REACQUIRE->GNAV_CORRIDOR_TRACK
20840,EVENT,SEG_FSM,CORRIDOR->E-STOP,conf=0.18
20900,FAULT,CHASSIS_OFFLINE,diag=0x00000004

5. 文件组织与命名

5.1 文件命名

因为当前没有 RTC不建议用日期命名。

推荐方式:

  • 目录:/ARESLOG/
  • 文件名:BOOT0001.CSVBOOT0002.CSV...

创建流程:

  • 启动时扫描 /ARESLOG
  • 找到最大序号
  • 新建下一个序号文件

5.2 文件轮转

建议单文件大小限制 2MB 到 4MB

  • 超过阈值自动新建下一个文件
  • 文件轮转只在 logTask 内执行

按 100ms 一条快照估算:

  • 一条快照约 120B 到 220B
  • 约 1.2KB/s 到 2.2KB/s
  • 1 小时约 4MB 到 8MB

所以 4MB 一卷是合理的。

6. 刷盘策略

6.1 建议策略

不要每写一行就 f_sync(),也不要一直不刷。

推荐折中:

  • RAM 累积 512B 或 1024B 后批量 f_write()
  • 每 1000ms 做一次 f_sync()
  • 重大故障事件后立即 f_sync() 一次

这样能兼顾三件事:

  • 降低 FatFs 抖动
  • 降低 SD 卡磨损
  • 掉电时最多丢失最近约 1 秒日志

6.2 关闭策略

如果系统有明确“任务结束/人工停机”流程,可在停机前调用:

  • SDLog_Flush()
  • SDLog_Close()

如果没有,就依赖周期性 f_sync()

7. SDMMC1 硬件与驱动建议

7.1 一期推荐配置

建议先用最保守配置把链路跑通:

  • SDMMC1
  • 1-bit 总线模式先 bring-up
  • FatFs 先用默认配置
  • 先不用 DMA 优化

原因:

  • H7 上 SDMMC + DCache + DMA 的一致性问题很多
  • 一期日志吞吐量很低1-bit 也足够
  • 先把 mount/open/write/read/sync 做稳定,比先追求 4-bit 吞吐更重要

稳定后再考虑:

  • 切到 4-bit 模式
  • 启用 DMA
  • 做 32 字节对齐缓冲区和 cache clean/invalidate

7.2 引脚建议

若 PCB 已引出标准 SDMMC1 引脚,可优先使用:

  • PC8 D0
  • PC9 D1
  • PC10 D2
  • PC11 D3
  • PC12 CK
  • PD2 CMD

从当前 ARES.ioc 看,这组引脚还没有被现有业务占用,但仍需要你结合原理图再确认一次。

7.3 热插拔建议

一期不建议做复杂热插拔。

建议:

  • 上电时检测并 mount
  • 运行中默认认为卡一直在
  • 如果 f_write() 连续失败,则置 sd_offline 标志并停止写卡
  • 需要重新插卡时,用按键命令或调试命令手动 remount

这样实现最小、风险最低。

8. 代码落点建议

8.1 CubeMX / 自动生成部分

需要改:

  • ARES.ioc:开启 SDMMC1
  • ARES.ioc:开启 FATFS
  • 生成对应 sdmmc.cfatfs.c、中间件文件

8.2 手写业务代码

建议新增:

  • App/log/sd_log.c
  • App/log/sd_log.h

建议修改:

  • Core/Src/freertos.c
    • 增加 logTask
  • App/app_tasks.c
    • AppTasks_Init() 中初始化日志模块
    • AppTasks_RunNavTask_Impl() 中每 100ms 打一次导航快照
    • AppTasks_RunMonitorTask() 中记录底盘在线/离线变化
  • App/nav/global_nav.c
    • 在阶段切换点调用事件日志接口,或者暴露状态变化让 navTask 记录
  • App/nav/segment_fsm.c
    • E-STOP/STOP/APPROACH 切换时记录事件,或者暴露状态变化给上层记录

8.3 更推荐的接入方式

为了少改导航核心,我更推荐:

  • navTask 内缓存“上一次 stage / seg_state
  • 若本周期发现变化,就从 app_tasks.c 发事件日志

这样可以减少对 global_nav.csegment_fsm.c 的侵入。

9. 一期接口建议

建议暴露以下最小 API

bool SDLog_Init(void);
bool SDLog_Start(void);
void SDLog_Task(void *argument);

void SDLog_TryPushBoot(void);
void SDLog_TryPushHealth(uint32_t tick_ms, bool chassis_online, uint32_t diag);
void SDLog_TryPushNavSnapshot(uint32_t tick_ms,
                              const RobotBlackboard_t *board,
                              const CorridorObs_t *obs,
                              const CorridorState_t *state,
                              const GlobalNavOutput_t *gnav,
                              const SegFsmOutput_t *fsm);
void SDLog_TryPushEvent(const char *tag, const char *msg);

接口原则:

  • 全部 TryPush
  • 失败直接返回,不阻塞调用方
  • 是否丢包由内部计数

10. 推荐实施顺序

建议按下面顺序推进,不要一步到位:

  1. 在 CubeMX 打开 SDMMC1 + FatFs,完成最小 mount/write/read 测试。
  2. 新建 logTask,只做固定字符串写卡,验证线程模型。
  3. 接入启动日志和健康日志。
  4. 接入 navTask 的 100ms 快照。
  5. 接入 stageseg_state 变化事件。
  6. 最后再考虑 4-bit、DMA、热插拔、二进制高频日志。

11. 我对这个项目的最终建议

如果你的目标是“记录系统运行状况,便于复盘问题”,我建议你的一期目标定成:

  • 能稳定挂载 SD 卡
  • 能连续写 30 分钟以上不影响导航
  • 能在一份 CSV 里看见导航阶段、安全状态、EKF 置信度、关键距离、底盘在线状态
  • 掉电最多损失 1 秒左右日志

不要把一期目标定成“全量原始数据黑匣子”。

对当前仓库,最小且正确的方案就是:

  • SDMMC1 + FatFs
  • 单写者 logTask
  • 100ms 状态快照 + 即时事件日志
  • CSV 文件 + 周期性 f_sync()

这套方案实现量小,调试成本低,而且已经足够覆盖你现在最关心的“系统运行状况记录”。