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

392 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()`
这套方案实现量小,调试成本低,而且已经足够覆盖你现在最关心的“系统运行状况记录”。