移植EasyLogger到STM32
一、EasyLogger简介
EasyLogger是一款超轻量级的C日志库,由国人Armink开源,专为嵌入式系统设计。它的特点:
-
代码极少:核心代码只有几百行
-
资源占用低:ROM和RAM消耗都很小
-
支持多种输出方式:串口、RTT、文件、Flash等
-
支持日志级别过滤:ASSERT / ERROR / WARN / INFO / DEBUG / VERBOSE 六个级别
-
支持标签过滤:按模块名过滤日志
项目地址:https://github.com/armink/EasyLogger
二、源码结构
把EasyLogger源码放到工程的 Middlewares/easylogger/ 目录下,结构如下:
Middlewares/easylogger/ ├── inc/ │ ├── elog.h # 库头文件,对外接口 │ └── elog_cfg.h # ★ 配置文件,需要修改 ├── src/ │ ├── elog.c # 核心实现(不改) │ ├── elog_utils.c # 工具函数(不改) │ ├── elog_async.c # 异步输出(不用可关闭) │ └── elog_buf.c # 缓冲输出(不用可关闭) └── port/ └── elog_port.c # ★ 移植层,需要修改
实际需要改的只有两个文件:elog_cfg.h(配置)和 elog_port.c(移植层)。
三、移植步骤
3.1 配置 elog_cfg.h
这是EasyLogger的配置文件,通过宏开关控制功能。以下是适合STM32裸机/FreeRTOS的推荐配置:
/*---------------------------------------------------------------------------*/
/* enable log output. */
#define ELOG_OUTPUT_ENABLE
/* setting static output log level. range: from ELOG_LVL_ASSERT to ELOG_LVL_VERBOSE */
#define ELOG_OUTPUT_LVL ELOG_LVL_VERBOSE
/* enable assert check */
#define ELOG_ASSERT_ENABLE
/* buffer size for every line's log */
#define ELOG_LINE_BUF_SIZE 1024
/* output line number max length */
#define ELOG_LINE_NUM_MAX_LEN 5
/* output filter's tag max length */
#define ELOG_FILTER_TAG_MAX_LEN 30
/* output filter's keyword max length */
#define ELOG_FILTER_KW_MAX_LEN 16
/* output filter's tag level max num */
#define ELOG_FILTER_TAG_LVL_MAX_NUM 5
/* output newline sign */
#define ELOG_NEWLINE_SIGN "\n"
/*---------------------------------------------------------------------------*/
/* enable log color —— ——————————————————————————————关闭,串口助手会显示乱码 */
// #define ELOG_COLOR_ENABLE
/*---------------------------------------------------------------------------*/
/* enable log fmt */
#define ELOG_FMT_USING_FUNC
#define ELOG_FMT_USING_DIR
#define ELOG_FMT_USING_LINE
/*---------------------------------------------------------------------------*/
/* enable asynchronous output mode —— ————————————关闭,需要pthread,裸机没有 */
// #define ELOG_ASYNC_OUTPUT_ENABLE
// #define ELOG_ASYNC_OUTPUT_USING_PTHREAD
/*---------------------------------------------------------------------------*/
/* enable buffered output mode —— —————————关闭,需要手动flush,否则日志不输出 */
// #define ELOG_BUF_OUTPUT_ENABLE
三个必须关闭的开关及原因:
| 宏开关 | 为什么要关 |
|---|---|
ELOG_ASYNC_OUTPUT_ENABLE |
异步模式依赖 pthread.h(POSIX线程库),STM32裸机/FreeRTOS没有这个库 |
ELOG_BUF_OUTPUT_ENABLE |
缓冲模式会把日志存在内存里,需要手动调用flush函数才会输出,否则日志永远在缓冲区里看不到 |
ELOG_COLOR_ENABLE |
ANSI颜色码(\033[31m等)在普通串口助手里会显示为乱码 |
注意:这三个宏要用注释掉的方式关闭,不能写成
#define ELOG_ASYNC_OUTPUT_ENABLE 0。 因为EasyLogger内部用的是#ifdef判断(只检查宏是否被定义),赋值为0仍然算"已定义"。
3.2 填充 elog_port.c(移植层)
这个文件是EasyLogger和硬件之间的桥梁,库本体通过调用这里的函数来完成实际操作:
#include <elog.h>
#include "usart.h" // 为了使用 huart1
extern UART_HandleTypeDef huart1;
/**
* EasyLogger port initialize
*/
ElogErrCode elog_port_init(void) {
ElogErrCode result = ELOG_NO_ERR;
return result;
}
/**
* EasyLogger port deinitialize
*/
void elog_port_deinit(void) {
}
/**
* ★ 最关键的函数:日志实际从哪里输出
* EasyLogger格式化好日志后,调用这个函数把字符串发出去
*/
void elog_port_output(const char *log, size_t size) {
HAL_UART_Transmit(&huart1, (uint8_t *)log, size, 0xFFFF);
}
/**
* 输出加锁 —— 用了FreeRTOS建议加互斥锁,裸机留空即可
*/
void elog_port_output_lock(void) {
// 如果用FreeRTOS:
// osMutexWait(mutex_id, osWaitForever);
}
/**
* 输出解锁
*/
void elog_port_output_unlock(void) {
// 如果用FreeRTOS:
// osMutexRelease(mutex_id);
}
/**
* 获取当前时间 —— 格式化日志时会调用,显示在 [方括号] 里
*/
static char time_buf[16];
const char *elog_port_get_time(void) {
snprintf(time_buf, sizeof(time_buf), "%lu", HAL_GetTick());
return time_buf;
}
/**
* 获取当前进程名 —— 裸机没有进程概念,返回空即可
*/
const char *elog_port_get_p_info(void) {
return "";
}
/**
* 获取当前线程名 —— 裸机留空,FreeRTOS可返回当前任务名
*/
const char *elog_port_get_t_info(void) {
return "";
}
重点解释 elog_port_output:
你可能以为重定向了 printf(通过 fputc)就够了,其实不够。EasyLogger内部不走 printf,它自己用 vsnprintf 格式化好字符串后,直接调用 elog_port_output 把原始数据传出来。所以你必须在这个函数里手动调用 HAL_UART_Transmit 把数据发出去。
elog_i("app", "msg")
↓
内部格式化(不经过printf)
↓
elog_port_output(buf, len) ← 你的移植层,必须实现
↓
HAL_UART_Transmit() → 串口发出
如果你想用SEGGER RTT输出(比串口快得多),把
elog_port_output里换成:SEGGER_RTT_Write(0, log, size);
3.3 初始化和使用
在 main.c 中:
#include "elog.h"
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
/* ---- EasyLogger初始化 ---- */
elog_init();
// 设置各级别日志的输出格式
elog_set_fmt(ELOG_LVL_ASSERT, ELOG_FMT_ALL); // 断言:输出所有信息
elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_ALL); // 错误:输出所有信息(方便定位问题)
elog_set_fmt(ELOG_LVL_WARN, ELOG_FMT_ALL); // 警告:输出所有信息
elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME); // 信息:级别+标签+时间
elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME); // 调试:级别+标签+时间
elog_set_fmt(ELOG_LVL_VERBOSE,ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME); // 详细:级别+标签+时间
elog_start();
/* ---- 使用日志 ---- */
elog_i("app", "系统启动完成");
elog_d("sensor", "温度: %.1f°C", 25.6);
elog_e("uart", "串口接收超时");
elog_w("battery", "电量低: %d%%", 15);
// ... FreeRTOS初始化等
}
四、输出格式说明
elog_set_fmt 的第二个参数控制该级别的日志带哪些信息,可以自由组合:
| 格式宏 | 含义 | 输出效果 |
|---|---|---|
ELOG_FMT_LVL |
日志级别 | I/ E/ W/ |
ELOG_FMT_TAG |
标签(模块名) | app sensor |
ELOG_FMT_TIME |
时间戳 | [1234] |
ELOG_FMT_FUNC |
函数名 | main |
ELOG_FMT_DIR |
文件路径 | ../Core/Src/main.c |
ELOG_FMT_LINE |
行号 | :42 |
ELOG_FMT_ALL |
以上全部 | 啥都输出 |
设计建议:
-
ERROR/WARN 用
ELOG_FMT_ALL:出错时需要完整信息来定位问题 -
INFO/DEBUG 用精简格式:日常日志太多太长会刷屏
五、日志级别
从高到低六个级别:
| 级别 | 宏 | 用途 | 输出前缀 |
|---|---|---|---|
| ASSERT | elog_assert() |
断言失败 | A/ |
| ERROR | elog_e() |
错误 | E/ |
| WARN | elog_w() |
警告 | W/ |
| INFO | elog_i() |
一般信息 | I/ |
| DEBUG | elog_d() |
调试信息 | D/ |
| VERBOSE | elog_v() |
详细信息 | V/ |
通过 elog_set_filter_lvl() 可以在运行时动态调整输出级别:
elog_set_filter_lvl(ELOG_LVL_INFO); // 只输出INFO及以上级别,DEBUG和VERBOSE被过滤
六、实际输出效果
I/app [1234] 系统启动完成 D/sensor [1235] 温度: 25.6°C E/uart [1236] (../Core/Src/main.c:42 main)串口接收超时 W/battery [1237] (../Core/Src/main.c:43 main)电量低: 15%
如果你的 elog_port_get_time() 返回空字符串,实际输出会像这样:
I/app [] 系统启动完成 E/uart [] (../Core/Src/main.c:42 main)串口接收超时
可以看到:
-
INFO和DEBUG只有 级别+标签+时间(精简)
-
ERROR和WARN额外带了 文件名+行号+函数名(方便定位)
输出格式逐段解读
以这条日志为例:
W/app [ ] (../Core/Src/main.c:114 main)电量低:
对照来看每一部分的含义:
W → 日志级别(ELOG_FMT_LVL)
A=ASSERT E=ERROR W=WARN I=INFO D=DEBUG V=VERBOSE
/app → 标签(ELOG_FMT_TAG)
你在代码里写 elog_w("app", ...) 的第一个参数
标签后面会自动填充空格,对齐到固定宽度
[ ] → 时间 进程 线程(ELOG_FMT_TIME | ELOG_FMT_P_INFO | ELOG_FMT_T_INFO)
内容来自 elog_port_get_time()、elog_port_get_p_info()、elog_port_get_t_info()
你返回了空字符串,所以方括号里是空的
如果 elog_port_get_time() 返回 "1234",这里就显示 [1234]
( → 括号开始,包裹文件/行号/函数信息
../Core/Src/ → 文件目录(ELOG_FMT_DIR)
main.c → 文件名
:114 → 行号(ELOG_FMT_LINE)
对应代码里的 __LINE__
main) → 函数名(ELOG_FMT_FUNC)
对应代码里的 __FUNCTION__
右括号结束
电量低: → 你传入的日志内容(第二个参数)
注意:[] 里的内容取决于 elog_port_get_time() 等三个函数的返回值。() 里的内容取决于 elog_set_fmt 设置了哪些格式宏。两者是独立控制的:
| 位置 | 控制方 | 内容来源 |
|---|---|---|
[] 方括号 |
elog_port.c 里的三个函数 |
你自己实现,可以返回时间、任务名等 |
() 圆括号 |
elog_cfg.h 里的格式宏 |
编译器自动填充的 __FILE__ __LINE__ __FUNCTION__ |
七、常见问题
Q1:编译报错 cannot open source input file "pthread.h"
原因:ELOG_ASYNC_OUTPUT_ENABLE 或 ELOG_ASYNC_OUTPUT_USING_PTHREAD 被定义了。
解决:在 elog_cfg.h 中注释掉这两个宏。
Q2:编译通过但串口没有日志输出
原因:ELOG_BUF_OUTPUT_ENABLE 被定义了。缓冲模式下日志存到内存里但没有flush,所以看不到。
解决:注释掉 ELOG_BUF_OUTPUT_ENABLE。
Q3:elog_port_get_time 等函数返回空导致程序卡死
原因:elog_port_get_time()、elog_port_get_p_info()、elog_port_get_t_info() 函数体里没有 return 语句,返回了未定义的值。
解决:确保每个函数都有 return,至少返回空字符串 ""。
Q4:串口输出乱码
原因:ELOG_COLOR_ENABLE 被定义了,ANSI颜色码在普通串口助手里显示为乱码。
解决:注释掉 ELOG_COLOR_ENABLE,或使用支持ANSI颜色的串口助手(如MobaXterm、VSCode串口监视器)。
Q5:为什么重定向了printf还是没有日志输出?
原因:EasyLogger不走 printf,它内部用 vsnprintf 格式化后直接调用 elog_port_output。
解决:在 elog_port_output 里必须手动调用 HAL_UART_Transmit(或 SEGGER_RTT_Write)。
八、总结
EasyLogger的移植本质就一件事:告诉它怎么从你的硬件上把字符发出去。
你需要做的: 1. 拷贝源码到工程 2. 改 elog_cfg.h —— 关掉三个不兼容的开关 3. 改 elog_port.c —— 填 elog_port_output 函数 4. main.c 里初始化,开始用
更多推荐

所有评论(0)