# ARES SD 日志方案 ## 1. 结论 对当前项目,最可行的一期方案是: - 用 `SDMMC1 + FatFs` 挂载 SD 卡。 - 新增一个独立的 `logTask` 作为唯一写卡任务。 - `navTask`、`monitorTask`、`imuTask` 等任务只负责把日志记录写入 RAM 队列,绝不直接 `f_write()`。 - 日志格式采用“事件 + 低频快照”的文本 CSV,一期先保证稳定可读,不追求全量高频原始数据。 这个方案最适合当前仓库,因为: - 现有高频闭环在 `App/app_tasks.c` 的 `navTask` 中以 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.c` 或 `App/log/sd_log_fs.c` 职责划分: - `sd_log.c`:日志队列、记录封装、CSV 格式化、刷盘策略。 - `sd_log_fs.c`:FatFs mount/open/write/sync/rotate。 - `app_tasks.c`:在现有任务周期内调用 `SDLog_TryPush...()`。 ### 3.2 任务模型 新增一个低优先级 `logTask`: - 优先级建议低于 `navTask` 和 `canTxTask` - 周期不固定,阻塞等待日志队列 - 它是系统里唯一允许调用 `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,可控 推荐记录结构: ```c 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 是否成功 - 日志格式版本号 示例: ```text # 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 行示例: ```text 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 失败 / 写入失败 / 队列溢出 推荐示例: ```text 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.CSV`、`BOOT0002.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.c`、`fatfs.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.c` 和 `segment_fsm.c` 的侵入。 ## 9. 一期接口建议 建议暴露以下最小 API: ```c 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. 接入 `stage` 与 `seg_state` 变化事件。 6. 最后再考虑 4-bit、DMA、热插拔、二进制高频日志。 ## 11. 我对这个项目的最终建议 如果你的目标是“记录系统运行状况,便于复盘问题”,我建议你的一期目标定成: - 能稳定挂载 SD 卡 - 能连续写 30 分钟以上不影响导航 - 能在一份 CSV 里看见:导航阶段、安全状态、EKF 置信度、关键距离、底盘在线状态 - 掉电最多损失 1 秒左右日志 不要把一期目标定成“全量原始数据黑匣子”。 对当前仓库,最小且正确的方案就是: - `SDMMC1 + FatFs` - 单写者 `logTask` - 100ms 状态快照 + 即时事件日志 - CSV 文件 + 周期性 `f_sync()` 这套方案实现量小,调试成本低,而且已经足够覆盖你现在最关心的“系统运行状况记录”。