Files
ASER-NAV/Doc/SD日志方案.md

392 lines
11 KiB
Markdown
Raw Permalink Normal View History

# 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()`
这套方案实现量小,调试成本低,而且已经足够覆盖你现在最关心的“系统运行状况记录”。