Files
STM32F4-Base/App/log/sys_log_mcu.c
T

331 lines
11 KiB
C
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.
/*
* 模块名称:System Log (MCU 后端) — 运行/自检记录落盘
* 模块功能:FatFS /log 目录日志文件(run.log/selftest.log)单写者落盘;启动前静态待发数组 + FreeRTOS 消息队列,
* defaultTask(唯一写者)排空落盘;容量满删除重建;上电/重启分类
* 适用平台:STM32F407ZGTx
* 作者:王建锋
* 创建日期:2026-09-14
* 修改记录:
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/运行与自检记录设计说明.md §4.4)
* v1.1 2026-09-21 王建锋 消息队列改为内核启动后由 syslog_drain 惰性创建
* (修复 osKernelStart 前创建 FreeRTOS 内核对象导致的启动失败)
*/
/* 头文件包含区 */
#include <stdio.h>
#include <string.h>
#define DBG_TAG "[SYSLOG]"
#include "dbg_log.h"
#include "sys_log.h"
#include "ff.h"
#include "cmsis_os.h"
#include "sys_clock.h"
/* 静态变量定义区 */
static const char *const s_paths[SYSLOG_COUNT] = { "/log/run.log", "/log/selftest.log" };
static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_MAX };
#define SYSLOG_QUEUE_MAX 8u
/* 待发条目:desc 为调用方静态串(Flash 字面量),只存指针不拷贝 */
typedef struct {
uint8_t id;
const char *desc;
} syslog_entry_t;
static syslog_entry_t s_preboot[SYSLOG_QUEUE_MAX]; /* 启动前待发数组(RTOS 前单生产者/后单消费者,免锁) */
static volatile uint8_t s_preboot_cnt; /* 数组条目数 */
static osMessageQueueId_t s_queue; /* 运行期消息队列(syslog_drain 首次调用时惰性创建) */
static const osMessageQueueAttr_t s_queue_attr = { .name = "syslog" };
static volatile uint8_t s_fs_ready; /* 卷挂载 + /log 目录就绪后置位 */
/* 函数定义区 */
/*
* 函数功能:追加一条记录(格式化 64B + FA_OPEN_APPEND;容量满先删除重建)
* 入口参数:id - 日志 ID syslog_id_t
* desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char *
* 返回值:0 - 成功;-1 - 写失败 int
* 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载
*/
static int s_append(syslog_id_t id, const char *desc) {
FIL f;
FRESULT fr;
UINT bw = 0u;
uint8_t rec[SYSLOG_REC_SIZE];
char ts[SYSLOG_TS_LEN + 1u];
uint32_t epoch;
size_t dl;
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) {
return -1;
}
if (syslog_count(id) >= s_caps[id]) {
fr = f_unlink(s_paths[id]);
DBG_INFO("cap full, recreate %s", s_paths[id]);
if (fr != FR_OK && fr != FR_NO_FILE) {
return -1;
}
}
memset(rec, 0u, sizeof(rec));
if (sys_clock_get_str(ts, sizeof(ts)) == NULL) {
memcpy(ts, "1970-01-01 08:00:00", SYSLOG_TS_LEN);
}
ts[SYSLOG_TS_LEN] = '\0';
epoch = sys_clock_get();
if (epoch == UINT32_MAX) {
epoch = 0u;
}
memcpy(rec, ts, SYSLOG_TS_LEN);
rec[SYSLOG_TS_LEN] = ' ';
(void)snprintf((char *)(rec + SYSLOG_EPOCH_OFFS), SYSLOG_EPOCH_LEN + 1u, "%010u", (unsigned int)epoch);
rec[SYSLOG_DESC_OFFS - 1u] = ' ';
dl = strlen(desc);
if (dl > SYSLOG_DESC_MAX) {
dl = SYSLOG_DESC_MAX;
}
memcpy(rec + SYSLOG_DESC_OFFS, desc, dl);
fr = f_open(&f, s_paths[id], FA_OPEN_APPEND);
if (fr != FR_OK) {
DBG_ERROR("open %s fail (%d)", s_paths[id], (int)fr);
return -1;
}
fr = f_write(&f, rec, SYSLOG_REC_SIZE, &bw);
(void)f_close(&f);
if (fr != FR_OK || bw != SYSLOG_REC_SIZE) {
DBG_ERROR("write %s fail (%d)", s_paths[id], (int)fr);
return -1;
}
return 0;
}
/*
* 函数功能:读取第 idx 条原始记录(64B)
* 入口参数:id - 日志 ID syslog_id_t
* idx - 记录序号(0 起) uint32_t
* p_rec- 输出缓冲(SYSLOG_REC_SIZE 字节,不可为 NULL) uint8_t *
* 返回值:0 - 成功;-1 - 越界/读失败 int
*/
static int s_read_raw(syslog_id_t id, uint32_t idx, uint8_t *p_rec) {
FIL f;
FRESULT fr;
UINT br = 0u;
if (p_rec == NULL || idx >= syslog_count(id)) {
return -1;
}
fr = f_open(&f, s_paths[id], FA_READ);
if (fr != FR_OK) {
return -1;
}
fr = f_lseek(&f, (FSIZE_t)idx * SYSLOG_REC_SIZE);
if (fr == FR_OK) {
fr = f_read(&f, p_rec, SYSLOG_REC_SIZE, &br);
}
(void)f_close(&f);
return (fr == FR_OK && br == SYSLOG_REC_SIZE) ? 0 : -1;
}
/*
* 函数功能:日志引擎初始化(内核启动前安全空操作)
* 入口参数:无
* 返回值:0 - 成功 int
* 限定条件:app_main_init() 起始处调用(osKernelStart 之前);
* 不得调用任何 FreeRTOS API(内核对象须在内核启动后才可创建)
* 函数说明:启动前投递走 s_preboot 静态数组(syslog_post 按内核状态自动分流);
* FreeRTOS 消息队列由 syslog_drain 在运行期(defaultTask 上下文)惰性创建。
* 历史缺陷:此处曾 osMessageQueueNew 创建队列,早于 osKernelInitialize
* 提前拉起内核,其后任务创建在未完成初始化的内核状态上执行,导致启动失败
*/
int syslog_mcu_init(void) {
return 0;
}
/*
* 函数功能:卷挂载成功后的一次性处理(建 /log 目录 + 写"上电/重启"分类记录)
* 入口参数:无
* 返回值:无
* 限定条件:defaultTask f_mount/f_mkfs 成功且置位 SYS_EVT_STORAGE_READY 前调用一次
*/
void syslog_mcu_mounted(void) {
FILINFO finfo;
uint32_t last = 0u;
const char *desc = "power on";
if (f_stat("/log", &finfo) != FR_OK) {
FRESULT fr = f_mkdir("/log");
if (fr != FR_OK && fr != FR_EXIST) {
DBG_ERROR("mkdir /log fail (%d)", (int)fr);
return;
}
}
s_fs_ready = 1u;
/* 上电/重启分类:末条 epoch 距 now < SYSLOG_REBOOT_WINDOW_S → 重启,否则上电 */
if (syslog_last_epoch(SYSLOG_RUN, &last) == 0 && sys_clock_get() != UINT32_MAX) {
if ((uint32_t)(sys_clock_get() - last) < SYSLOG_REBOOT_WINDOW_S) {
desc = "reboot";
}
}
(void)s_append(SYSLOG_RUN, desc);
DBG_INFO("runlog boot entry: %s", desc);
}
/*
* 函数功能:排空待发日志并落盘(启动前数组 + 运行期队列,FIFO);首次调用时惰性创建运行期队列
* 入口参数:无
* 返回值:无
* 限定条件:仅 defaultTask(唯一写者)调用;卷未就绪时直接返回(条目滞留待重试)
*/
void syslog_drain(void) {
syslog_entry_t e;
/* 运行期惰性创建消息队列:本函数仅由 defaultTask(调度器运行中)调用,
保证内核对象创建发生在 osKernelStart 之后 */
if (s_queue == NULL) {
s_queue = osMessageQueueNew(SYSLOG_QUEUE_MAX, sizeof(syslog_entry_t), &s_queue_attr);
if (s_queue == NULL) {
DBG_ERROR("queue create fail");
}
}
if (!s_fs_ready || s_queue == NULL) {
return;
}
while (s_preboot_cnt > 0u) {
e = s_preboot[0];
if (s_append((syslog_id_t)e.id, e.desc) != 0) {
break; /* 落盘失败:保留剩余条目,下轮重试 */
}
memmove(&s_preboot[0], &s_preboot[1], sizeof(syslog_entry_t) * (size_t)(s_preboot_cnt - 1u));
s_preboot_cnt--;
}
for (;;) {
if (osMessageQueueGet(s_queue, &e, 0u, 0u) != osOK) {
break; /* 队列空 */
}
if (s_append((syslog_id_t)e.id, e.desc) != 0) {
DBG_ERROR("drop entry id=%d", (int)e.id);
break; /* 落盘失败:停止排空,避免连续丢弃 */
}
}
}
/*
* 函数功能:投递一条日志(任意上下文;非阻塞,队列满则丢弃)
* 入口参数:id - 日志 ID syslog_id_t
* desc - 描述串(UTF-8,静态存储期,超长截断) const char *
* 返回值:无
* 限定条件:FreeRTOS 启动前走静态待发数组;desc 必须为静态/常量串
*/
void syslog_post(syslog_id_t id, const char *desc) {
syslog_entry_t e;
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) {
return;
}
e.id = (uint8_t)id;
e.desc = desc;
if (s_queue != NULL && osKernelGetState() == osKernelRunning) {
if (osMessageQueuePut(s_queue, &e, 0u, 0u) != osOK) {
DBG_ERROR("queue full, drop: %s", desc);
}
} else {
uint8_t n = s_preboot_cnt;
if (n < SYSLOG_QUEUE_MAX) {
s_preboot[n] = e;
s_preboot_cnt = n + 1u;
} else {
DBG_ERROR("preboot full, drop: %s", desc);
}
}
}
/*
* 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1))
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:条数;文件缺失/未挂载返回 0 uint32_t
*/
uint32_t syslog_count(syslog_id_t id) {
FILINFO finfo;
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
return 0u;
}
if (f_stat(s_paths[id], &finfo) != FR_OK) {
return 0u;
}
return (uint32_t)finfo.fsize / SYSLOG_REC_SIZE;
}
/*
* 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾)
* 入口参数:id - 日志 ID syslog_id_t
* idx - 记录序号(0 起) uint32_t
* p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节,不可为 NULL) char *
* ts_len - p_ts 容量 size_t
* p_desc - 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节,不可为 NULL) char *
* desc_len - p_desc 容量 size_t
* 返回值:0 - 成功;-1 - 越界/读失败 int
*/
int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len) {
uint8_t rec[SYSLOG_REC_SIZE];
size_t dl = 0u;
if (p_ts == NULL || p_desc == NULL || ts_len <= SYSLOG_TS_LEN || desc_len <= SYSLOG_DESC_MAX) {
return -1;
}
if (s_read_raw(id, idx, rec) != 0) {
return -1;
}
memcpy(p_ts, rec, SYSLOG_TS_LEN);
p_ts[SYSLOG_TS_LEN] = '\0';
while (dl < SYSLOG_DESC_MAX && rec[SYSLOG_DESC_OFFS + dl] != '\0') {
dl++;
}
if (dl >= desc_len) {
dl = desc_len - 1u;
}
memcpy(p_desc, rec + SYSLOG_DESC_OFFS, dl);
p_desc[dl] = '\0';
return 0;
}
/*
* 函数功能:读取末条记录的 epoch 秒(上电/重启分类用)
* 入口参数:id - 日志 ID syslog_id_t
* p_epoch- 输出缓冲(不可为 NULL;无记录时置 0) uint32_t *
* 返回值:0 - 成功读到末条;-1 - 文件缺失/空 int
*/
int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch) {
uint8_t rec[SYSLOG_REC_SIZE];
uint32_t v = 0u;
uint32_t i;
if (syslog_count(id) == 0u) {
if (p_epoch != NULL) {
*p_epoch = 0u;
}
return -1;
}
if (s_read_raw(id, syslog_count(id) - 1u, rec) != 0) {
if (p_epoch != NULL) {
*p_epoch = 0u;
}
return -1;
}
for (i = 0u; i < SYSLOG_EPOCH_LEN; i++) {
char c = (char)rec[SYSLOG_EPOCH_OFFS + i];
if (c >= '0' && c <= '9') {
v = v * 10u + (uint32_t)(c - '0');
}
}
if (p_epoch != NULL) {
*p_epoch = v;
}
return 0;
}