一、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_ENABLEELOG_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 里初始化,开始用

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐