获取更多技术资讯?点击页面右上角【注册】,加入开发者社区吧
在嵌入式开发或 C 语言编程中,打印调试输出是我们最常用的调试手段。
很多初学者习惯直接使用 printf 函数打印信息,但随着项目代码量增加,这种方式的弊端逐渐显现:
- 难以管理:想屏蔽调试信息时,只能一行行注释或删除。
- 缺乏分级:无法区分普通日志、警告(Warning)和错误(Error)。
- 信息杂乱:无法快速定位打印信息所在的文件和行号。
虽然市面上有如 EasyLogger 这样功能强大的开源日志库,但对于很多中小型项目而言,我们只需要一个轻量级、可移植、功能够用的日志模块。
本文将分享如何参考RT-Thread的设计思路,手写一个支持分级控制、颜色显示、自动打印文件行号的轻量级 Log 模块,并演示如何在Linux和APM32单片机上移植使用。
1. 核心功能需求
我们需要实现的 Log 模块主要包含以下功能:
- 全局开关:一键开启或关闭所有调试信息。
- 分级过滤:支持 Error, Warning, Info, Debug 四个级别,可设置只显示特定级别以上的信息(类似 Linux 内核)。
- 颜色输出:根据日志级别显示不同颜色(ANSI 转义序列),一目了然。
- 自动元数据:自动打印文件名、函数名或行号。
2. 代码实现原理
核心逻辑在于利用 C 语言的宏定义(Macro)。通过宏包装 printf,我们可以在编译阶段就决定是否生成打印代码,从而节省 Flash 空间。

2.1 核心宏定义分析
我们定义了一个核心宏 dbg_log_line:
#define dbg_log_line(lvl, color_n, fmt, ...) \
do { \
_DBG_LOG_HDR(lvl, color_n); \
printf(fmt, ##__VA_ARGS__); \
_DBG_LOG_X_END; \
} while (0)
- _DBG_LOG_HDR:负责打印日志前缀(如
[E/main.c:20])和设置终端颜色。
- #
#__VA_ARGS__:这是 C99 标准允许的可变参数宏,用于处理
printf 的格式化参数。
- do…while(0):标准的宏定义写法,确保宏在
if...else 语句中能被正确展开,避免语法错误。
2.2 完整代码 (log.h)
你也可以直接复制以下代码**:
#ifndef __LOG_H__
#define __LOG_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdio.h>
/* ================= 配置区域 ================= */
/* 日志输出总开关:注释掉此行则关闭所有打印 */
#define DBG_ENABLE
/* 颜色输出开关:注释掉此行则不显示颜色(适用于不支持颜色的串口工具) */
#define DBG_COLOR
/* 日志打印级别定义 */
#define DBG_ERROR 0
#define DBG_WARNING 1
#define DBG_INFO 2
#define DBG_LOG 3
/* 当前显示的日志级别:级别越高,输出的信息越多 */
#define DBG_LEVEL DBG_LOG
/* =========================================== */
#ifdef DBG_ENABLE
/* ANSI 颜色代码 (Foreground) */
/* BLACK 30, RED 31, GREEN 32, YELLOW 33, BLUE 34, PURPLE 35, CYAN 36, WHITE 37 */
#ifdef DBG_COLOR
#define _DBG_COLOR(n) printf("\033["#n"m")
#define _DBG_LOG_HDR(lvl_name, color_n) \
printf("\033["#color_n"m[" lvl_name "/" "<%s:%d>" "] ", __FILE__, __LINE__)
#define _DBG_LOG_X_END \
printf("\033[0m\n")
#else
#define _DBG_COLOR(n)
#define _DBG_LOG_HDR(lvl_name, color_n) \
printf("[" lvl_name "/" "%s:%d" "] ", __FILE__, __LINE__)
#define _DBG_LOG_X_END \
printf("\n")
#endif /* DBG_COLOR */
#define dbg_log_line(lvl, color_n, fmt, ...) \
do { \
_DBG_LOG_HDR(lvl, color_n); \
printf(fmt, ##__VA_ARGS__); \
_DBG_LOG_X_END; \
} while (0)
#else
#define dbg_log_line(lvl, color_n, fmt, ...)
#endif /* DBG_ENABLE */
/*
* 对外接口 API
* 根据 DBG_LEVEL 的值,编译器会自动优化掉不需要的代码
*/
/* Debug 级别 (绿色) */
#if (DBG_LEVEL >= DBG_LOG)
#define LOG_D(fmt, ...) dbg_log_line("D", 32, fmt, ##__VA_ARGS__)
#else
#define LOG_D(...)
#endif
/* Info 级别 (默认色/绿色) */
#if (DBG_LEVEL >= DBG_INFO)
#define LOG_I(fmt, ...) dbg_log_line("I", 32, fmt, ##__VA_ARGS__)
#else
#define LOG_I(...)
#endif
/* Warning 级别 (黄色) */
#if (DBG_LEVEL >= DBG_WARNING)
#define LOG_W(fmt, ...) dbg_log_line("W", 33, fmt, ##__VA_ARGS__)
#else
#define LOG_W(...)
#endif
/* Error 级别 (红色) */
#if (DBG_LEVEL >= DBG_ERROR)
#define LOG_E(fmt, ...) dbg_log_line("E", 31, fmt, ##__VA_ARGS__)
#else
#define LOG_E(...)
#endif
#ifdef __cplusplus
}
#endif
#endif /* __LOG_H__ */
3. 环境测试:Linux 下的颜色输出
由于 Linux 终端原生支持 ANSI 颜色转义码,我们先在 Ubuntu 环境下验证效果。
测试代码 (main.c):
#include <stdio.h>
#include "log.h"
int main(void)
{
LOG_D("***********************log test start***********************");
LOG_D("This is a Debug message");
LOG_I("This is an Info message");
LOG_W("This is a Warning message");
LOG_E("This is an Error message");
LOG_D("***********************log test end***********************");
return 0;
}
运行效果:
可以看到,不同级别的日志显示了不同的颜色。

4. 实战移植:在 APM32单片机上使用
在 MCU 上使用 printf 需要硬件串口(UART)的支持,并进行重定向。以下以 APM32F407ZGT6(兼容 STM32F4)为例。
4.1 串口初始化与 printf 重定向
首先,你需要初始化一个串口(如 USART1)。初始化代码参考官方 SDK 即可。
为了让 printf 输出到串口,我们需要重写 C 库中的 fputc 函数。
基于 HAL 库/标准库的重定向代码:
/* 重定向 printf 到 USART1 */
int fputc(int ch, FILE* f)
{
/* 发送换行符时,自动补充回车符,适应部分串口工具 */
if (ch == '\n')
{
while (USART_ReadStatusFlag(USART1, USART_FLAG_TXBE) == RESET);
USART_TxData(USART1, '\r');
}
/* 等待发送缓冲区为空 */
while (USART_ReadStatusFlag(USART1, USART_FLAG_TXBE) == RESET);
/* 发送数据 */
USART_TxData(USART1, (uint8_t)ch);
return (ch);
}
4.2 方案一:配合 Keil MicroLIB 库(推荐)
这是最简单的方法。MicroLIB 是 Keil MDK 为嵌入式应用高度优化的 C 库。
- 打开 Keil 工程配置(Magic Wand 图标)。
- 进入 Target 选项卡。
- 勾选 Use MicroLIB。
勾选后,上述 fputc 重定向即可生效。
4.3 方案二:不使用 MicroLIB(标准 C 库)
如果你不想使用 MicroLIB,则需要禁用“半主机模式”(Semihosting),否则程序可能会卡死在初始化阶段。
你需要添加以下代码来配置标准库:
#if 1 /* 如果不勾选 MicroLIB,请开启此宏 */
/* 告知连接器不从 C 库链接使用半主机的函数 */
#pragma import(__use_no_semihosting)
/* 标准库需要的支持数据类型 */
struct __FILE
{
int handle;
};
FILE __stdout;
/* 定义 _sys_exit() 以避免使用半主机模式 */
void _sys_exit(int x)
{
x = x;
}
/* 依然需要 fputc 重定向 */
int fputc(int ch, FILE* f)
{
/* ... (同上文 fputc 实现) ... */
}
#endif
4.4 最终效果
在 main 函数中,我们添加在 ubuntu 下测试的那几行打印测试代码,然后打开 MobaXterm 终端软件,可以看到如下打印效果:

可以看到有不同颜色的打印效果,如果不想打印某些信息的级别,修改打印日志输出级别的宏定义即可。
注意:如果是使用常用的串口助手测试工具的话,可能不支持输出信息带颜色的,而且反而会看到一些不需要的颜色编码,这个时候我们可以把颜色输出的宏定义关闭就可以不用输出颜色了。
总结
通过简单的宏定义封装,我们实现了一个功能完备的轻量级日志库。相比直接使用 printf,它具备以下优势:
- 代码整洁:调试代码规范统一。
- 性能可控:发布版本可通过修改
DBG_LEVEL 瞬间屏蔽低级日志,减少 CPU 占用。
- 定位精准:报错时直接定位到文件和行号,无需全局搜索字符串。
希望这个小模块能成为你嵌入式开发工具箱中的利器!
