11 KiB
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.hApp/log/sd_log.cApp/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,可控
推荐记录结构:
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 一期必须记录的内容
建议只做三类:
- 启动信息
- 周期性状态快照
- 异常/状态切换事件
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_msstage/stage_nameseg_staterequest_corridoroverride_v/override_wsafe_v/safe_wodom_vx/odom_wze_y/e_th/s/confd_front/d_backd_lf/d_lr/d_rf/d_rrvalid_maskchassis_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.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 引脚,可优先使用:
PC8D0PC9D1PC10D2PC11D3PC12CKPD2CMD
从当前 ARES.ioc 看,这组引脚还没有被现有业务占用,但仍需要你结合原理图再确认一次。
7.3 热插拔建议
一期不建议做复杂热插拔。
建议:
- 上电时检测并 mount
- 运行中默认认为卡一直在
- 如果
f_write()连续失败,则置sd_offline标志并停止写卡 - 需要重新插卡时,用按键命令或调试命令手动 remount
这样实现最小、风险最低。
8. 代码落点建议
8.1 CubeMX / 自动生成部分
需要改:
ARES.ioc:开启SDMMC1ARES.ioc:开启FATFS- 生成对应
sdmmc.c、fatfs.c、中间件文件
8.2 手写业务代码
建议新增:
App/log/sd_log.cApp/log/sd_log.h
建议修改:
Core/Src/freertos.c- 增加
logTask
- 增加
App/app_tasks.cAppTasks_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:
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. 推荐实施顺序
建议按下面顺序推进,不要一步到位:
- 在 CubeMX 打开
SDMMC1 + FatFs,完成最小 mount/write/read 测试。 - 新建
logTask,只做固定字符串写卡,验证线程模型。 - 接入启动日志和健康日志。
- 接入
navTask的 100ms 快照。 - 接入
stage与seg_state变化事件。 - 最后再考虑 4-bit、DMA、热插拔、二进制高频日志。
11. 我对这个项目的最终建议
如果你的目标是“记录系统运行状况,便于复盘问题”,我建议你的一期目标定成:
- 能稳定挂载 SD 卡
- 能连续写 30 分钟以上不影响导航
- 能在一份 CSV 里看见:导航阶段、安全状态、EKF 置信度、关键距离、底盘在线状态
- 掉电最多损失 1 秒左右日志
不要把一期目标定成“全量原始数据黑匣子”。
对当前仓库,最小且正确的方案就是:
SDMMC1 + FatFs- 单写者
logTask - 100ms 状态快照 + 即时事件日志
- CSV 文件 + 周期性
f_sync()
这套方案实现量小,调试成本低,而且已经足够覆盖你现在最关心的“系统运行状况记录”。