|
|
发表于 15 小时前
|
查看: 2 |
回复: 0
在嵌入式项目开发中,日志功能是调试和问题追踪的利器。然而,有些问题具有偶发性,可能在开发者实时查看时并未发生。如果上位机或终端的缓冲区容量有限,关键的异常日志信息便可能丢失。因此,将日志持久化保存到文件系统中至关重要。RT-Thread软件包仓库中的ULOG_FILE包虽能实现此功能,但其更新停滞,且会将所有日志混存于单一文件,无法按标签进行区分存储。幸运的是,当前RT-Thread的ulog核心组件已内置了文件后端功能,通过合理配置即可满足分类存储的需求。
使用场景与目标
在实际的嵌入式应用(如AGV小车)中,某些状态信息(如运动数据、电池电量)需要持续记录。这些信息若不加处理地打印到控制台,会严重干扰开发者查看关键信息或输入MSH命令。因此,我们期望的日志系统应具备以下能力:
- 将不同类别的日志(如系统日志、运动日志)分别保存到独立的文件中。
- 能够按需控制控制台是否显示特定类别的日志信息。
实现步骤详解
0. 前提配置
- 在RT-Thread Settings中启用ULOG组件,并选择异步日志模式。
- 确保系统已挂载可读写的文件系统(如LittleFS、SPIFFS),用于存储日志文件。
- 在ULOG组件配置中,勾选启用“文件日志后端”功能。

- 务必提前测试文件系统的基本操作(创建、读写、删除)是否正常,这是所有文件日志管理功能的基础。
1. 定义日志文件配置表
首先,创建一个配置表,用于定义不同日志后端的存储路径、文件名、文件大小、数量及缓冲区大小。
BUFF_SIZE:写入缓冲区大小。设置过大会导致数据积压延迟写入;设置过小则会频繁触发写入操作,可能引发ULOG异步线程堆栈溢出。需要根据实际业务日志频率和系统资源权衡。
ROOT_PATH:日志文件存储的根目录。示例中使用/flash/log是考虑到ROMFS通常挂载为只读根目录,而/flash下的文件系统(如FATFS、LittleFS)才具备读写权限。
struct _log_file {
const char *name;
const char *dir_path;
rt_size_t max_num;
rt_size_t max_size;
rt_size_t buf_size;
};
#define ROOT_PATH "/flash/log"
#define FILE_SIZE (512 * 1024) // 单个日志文件最大512KB
#define BUFF_SIZE 512 // 写入缓冲区大小
// 日志文件配置表
static struct _log_file table[] = {
{"sys", ROOT_PATH, 10, FILE_SIZE, BUFF_SIZE}, // 系统日志,最多10个文件
{"motion", ROOT_PATH, 5, FILE_SIZE, BUFF_SIZE}, // 运动日志,最多5个文件
};
// 为配置表条目定义索引
#define sys_id 0
#define motion_id 1
2. 初始化多个文件后端
为每种日志类别初始化独立的文件后端。注意:调用ulog_file_backend_init初始化后,必须调用ulog_file_backend_enable使其生效。
// 系统日志后端
static struct ulog_backend sys_log_backend;
static struct ulog_file_be sys_log_file;
void sys_log_file_backend_init(void) {
struct ulog_file_be *file_be = &sys_log_file;
uint8_t id = sys_id;
file_be->parent = sys_log_backend;
// 设置过滤函数(后续步骤实现)
ulog_backend_filter_t filter = sys_log_file_backend_filter;
ulog_file_backend_init(file_be,
table[id].name,
table[id].dir_path,
table[id].max_num,
table[id].max_size,
table[id].buf_size);
ulog_backend_set_filter(&file_be->parent, filter); // 绑定过滤规则
ulog_file_backend_enable(file_be); // 启用后端
}
// 运动日志后端
static struct ulog_backend motion_log_backend;
static struct ulog_file_be motion_log_file;
void motion_log_file_backend_init(void) {
struct ulog_file_be *file_be = &motion_log_file;
uint8_t id = motion_id;
file_be->parent = motion_log_backend;
ulog_backend_filter_t filter = motion_log_file_backend_filter;
ulog_file_backend_init(file_be,
table[id].name,
table[id].dir_path,
table[id].max_num,
table[id].max_size,
table[id].buf_size);
ulog_backend_set_filter(&file_be->parent, filter);
ulog_file_backend_enable(file_be);
}
3. 功能测试
完成初始化后,可以先进行基础测试,确认日志文件被正确创建。

- 使用
cat命令可以查看文件内容,此时两个文件内容可能相同。

4. 实现日志分类过滤
上一步实现了日志的多文件存储,但内容并未区分。接下来通过自定义过滤函数实现按标签分类。
-
编写过滤函数
过滤函数决定了哪些日志可以进入该后端。例如,系统日志文件需要排除所有带MOVE标签的日志。
#define MOTION_TAG "MOVE"
static rt_bool_t sys_log_file_backend_filter(struct ulog_backend *backend,
rt_uint32_t level,
const char *tag,
rt_bool_t is_raw,
const char *log,
rt_size_t len) {
// 排除标签为“MOVE”的日志,不存入系统日志文件
if (rt_strncmp(tag, MOTION_TAG, sizeof(MOTION_TAG)) == 0)
return RT_FALSE;
else
return RT_TRUE;
}
运动日志文件则只需记录带MOVE标签的日志。
static rt_bool_t motion_log_file_backend_filter(struct ulog_backend *backend,
rt_uint32_t level,
const char *tag,
rt_bool_t is_raw,
const char *log,
rt_size_t len) {
// 只允许标签为“MOVE”的日志存入运动日志文件
if (rt_strncmp(tag, MOTION_TAG, sizeof(MOTION_TAG)) == 0)
return RT_TRUE;
else
return RT_FALSE;
}
-
在应用代码中使用特定标签输出
// 定义运动日志输出的宏
#define LOG_MV(...) ulog_i(MOTION_TAG, __VA_ARGS__)
void motion_log_thread_entry(void *parameter) {
float set_angle = 0, get_angle = 0, value = 0;
int set_speed = 0, get_speed = 0;
while (1) {
set_angle += 0.1;
get_angle -= 0.1;
set_speed += 1;
get_speed -= 1;
value += 3.14;
// 使用MOTION_TAG标签输出运动日志
LOG_MV("%f %f %d %d %f",
set_angle, get_angle,
set_speed, get_speed,
value);
rt_thread_mdelay(500);
}
}
-
验证结果
现在,两个日志文件将保存不同的内容,且文件大小会根据各自接收的日志量增长。

sys.log 包含除MOVE外的所有日志。

motion.log 仅包含MOVE标签的日志。

5. 动态控制文件后端
考虑到Flash等存储介质的写寿命,或需要临时删除日志文件时(文件打开状态下无法删除),需要能够动态启用/禁用文件后端。
-
控制日志是否写入文件
static void log_file_backend_control(uint8_t argc, char **argv) {
const char *operator = argv[1];
const char *flag = argv[2];
if (argc < 3) {
rt_kprintf("Usage: ulog_be_ctrl [sys/motion] [enable/disable]\n");
return;
}
if (!rt_strcmp(operator, table[sys_id].name)) {
if (!rt_strcmp(flag, "disable")) {
ulog_file_backend_disable(&sys_log_file);
rt_kprintf("The file backend %s is disabled\n", operator);
} else if (!rt_strcmp(flag, "enable")) {
ulog_file_backend_enable(&sys_log_file);
rt_kprintf("The file backend %s is enabled\n", operator);
}
} else if (!rt_strcmp(operator, table[motion_id].name)) {
// ... 类似地处理 motion 后端 ...
} else {
rt_kprintf("Failed to find the file backend: %s\n", operator);
}
}
MSH_CMD_EXPORT_ALIAS(log_file_backend_control, ulog_be_ctrl, control ulog file backend);
-
卸载后端以删除文件
static void log_file_backend_deinit(uint8_t argc, char **argv) {
const char *operator = argv[1];
if (argc < 2) {
rt_kprintf("Usage: ulog_be_deinit [sys/motion]\n");
return;
}
if (!rt_strcmp(operator, "motion")) {
ulog_file_backend_deinit(&motion_log_file);
ulog_file_backend_disable(&motion_log_file);
rt_kprintf("The file backend %s is deinited\n", operator);
} else if (!rt_strcmp(operator, "sys")) {
// ... 类似地处理 sys 后端 ...
}
}
MSH_CMD_EXPORT_ALIAS(log_file_backend_deinit, ulog_be_deinit, Deinit ulog file backend);
6. 控制台输出过滤
为避免运动日志等高频信息刷屏,影响控制台使用,需要对其在控制台的显示进行过滤。
-
修改控制台后端(console_be.c)
在控制台后端初始化代码中,为其设置一个弱定义的过滤函数,并允许运行时设置。
// 在 console_be.c 中增加
RT_WEAK rt_bool_t ulog_console_backend_filter(struct ulog_backend *backend,
rt_uint32_t level,
const char *tag,
rt_bool_t is_raw,
const char *log,
rt_size_t len) {
return RT_TRUE; // 默认允许所有日志输出到控制台
}
int ulog_console_backend_init(void) {
ulog_init();
console.output = ulog_console_backend_output;
ulog_backend_register(&console, "console", RT_TRUE);
// 初始化时挂钩过滤函数
ulog_backend_set_filter(&console, ulog_console_backend_filter);
return 0;
}
-
实现控制台过滤函数
在应用代码中实现该函数,例如过滤掉运动日志。
// 在应用程序中实现强符号函数,覆盖弱定义
rt_bool_t ulog_console_backend_filter(struct ulog_backend *backend,
rt_uint32_t level,
const char *tag,
rt_bool_t is_raw,
const char *log,
rt_size_t len) {
// 过滤掉标签为“MOVE”的日志,不显示在控制台
if (rt_strncmp(tag, MOTION_TAG, sizeof(MOTION_TAG)) == 0)
return RT_FALSE;
else
return RT_TRUE;
}
-
增加MSH命令动态控制过滤
为了方便调试时临时查看被过滤的日志,可以增加命令动态开关控制台过滤。
#ifdef RT_USING_MSH
static void ulog_console_backend_filter_set(uint8_t argc, char **argv) {
if (argc < 2) {
rt_kprintf("Usage: console_filter [enable/disable]\n");
return;
}
const char *operator = argv[1];
if (!rt_strcmp(operator, "enable")) {
// 启用过滤(即不显示MOVE日志)
ulog_backend_set_filter(&console, ulog_console_backend_filter);
rt_kprintf("Console filter enabled.\n");
} else if (!rt_strcmp(operator, "disable")) {
// 禁用过滤(显示所有日志)
ulog_backend_set_filter(&console, RT_NULL);
rt_kprintf("Console filter disabled.\n");
}
}
MSH_CMD_EXPORT_ALIAS(ulog_console_backend_filter_set, console_filter, console filter control);
#endif
总结与扩展
本文详细介绍了在RT-Thread中利用ULOG组件原生文件后端,实现多日志文件分类存储、动态控制以及控制台显示过滤的完整方案。该方案解决了嵌入式开发中日志管理的关键痛点,提升了网络与系统调试效率。此实现源于社区实践,开发者可根据自身项目需求,调整过滤逻辑和存储策略,构建更健壮的日志系统。
|
上一篇:开源工具助力邮件地址有效性批量检测与数据清洗下一篇:12月15日安全动态:Windows LNK漏洞、VSCode恶意扩展与固件逆向分析
|