一、前言:为什么在嵌入式系统中需要宏Log?
在嵌入式开发中,调试手段往往受限(没有屏幕、无法轻易连接调试器)。Log输出成为了最常用、最有效的调试方式。但直接使用 printf
会遇到以下问题:
- 性能开销大:格式处理、数据传输(如通过UART)非常耗时,可能改变实时程序的行为。
- 代码膨胀:调试完成后需要手动删除或注释掉大量打印语句,费时费力。
- 信息不足:普通的
printf("Value: %d", x)
无法自动提供文件名、行号、函数名等关键上下文信息。
解决方案就是使用宏(Macro)来构建一个强大的Log系统。它可以在编译期灵活地控制Log的开启/关闭、丰富信息,并且几乎无运行时开销。
二、基础入门:从最简单的宏Log开始
让我们从一个最基础的宏开始,解决手动添加前缀的问题。
1// 基础版本 - 添加固定前缀
2#define LOG(msg) printf("[LOG] %s\n", msg)
3
4void main() {
5 LOG("System Started"); // 展开为: printf("[LOG] %s\n", "System Started")
6 int error = 5;
7 LOG("Error occurred!");
8}
输出:
1[LOG] System Started
2[LOG] Error occurred!
优点:无需重复写前缀 [LOG]
。
三、进阶技巧:获取编译环境信息
C标准预定义了一些宏,可以帮助我们获取宝贵的调试信息:
__FILE__
: 当前源文件名__LINE__
: 当前行号__func__
: 当前函数名 (C99标准)__DATE__
,__TIME__
: 编译日期和时间
1// 进阶版本 - 带文件名和行号
2#define LOG(msg) printf("[%s, line %d] %s\n", __FILE__, __LINE__, msg)
3
4void main() {
5 LOG("Sensor reading failed");
6}
输出:
1[main.c, line 25] Sensor reading failed
现在你能精准定位到哪一行代码出了問題!
四、可变参数宏:打造万能格式化Log
基础的宏只能输出字符串。为了支持像 printf
一样的格式化输出(如 %d
, %f
),我们需要使用可变参数宏(...
和 __VA_ARGS__
)。
1// 万能版本 - 支持格式化输出
2#define LOG(format, ...) printf("[%s, %s, line %d] " format "\n", __FILE__, __func__, __LINE__, ##__VA_ARGS__)
3
4void main() {
5 int temperature = 25;
6 int humidity = 60;
7 const char* sensor_name = "DHT11";
8
9 LOG("System Booted");
10 LOG("%s - Temp: %d°C, Humidity: %d%%", sensor_name, temperature, humidity);
11}
输出:
1[main.c, main, line 35] System Booted
2[main.c, main, line 36] DHT11 - Temp: 25°C, Humidity: 60%
代码解释:
...
: 表示宏接受可变数量的参数。__VA_ARGS__
: 代表...
部分的所有参数。##__VA_ARGS__
:##
的作用是当可变参数为空时,自动去掉前面的逗号,
,防止语法错误。这是GCC的扩展,在大多数嵌入式编译器中都支持。
五、核心实战:分级Log与编译控制
一个专业的Log系统必须有等级划分(如Error, Warning, Info, Debug)和编译开关。我们可以使用宏条件编译来实现。
1. 定义Log等级
1typedef enum {
2 LOG_LEVEL_ERROR,
3 LOG_LEVEL_WARNING,
4 LOG_LEVEL_INFO,
5 LOG_LEVEL_DEBUG,
6 LOG_LEVEL_NONE // 用于关闭所有Log
7} log_level_t;
8
9// 编译时配置:设置当前所需的Log级别
10#define CURRENT_LOG_LEVEL LOG_LEVEL_DEBUG
2. 创建分级Log宏
1// 根据 CURRENT_LOG_LEVEL 决定是否编译对应的Log宏
2#if CURRENT_LOG_LEVEL >= LOG_LEVEL_ERROR
3 #define LOG_ERROR(format, ...) printf("[ERROR] " format "\n", ##__VA_ARGS__)
4#else
5 #define LOG_ERROR(format, ...) // 定义为空,编译器会直接移除该行代码
6#endif
7
8#if CURRENT_LOG_LEVEL >= LOG_LEVEL_WARNING
9 #define LOG_WARNING(format, ...) printf("[WARN] " format "\n", ##__VA_ARGS__)
10#else
11 #define LOG_WARNING(format, ...)
12#endif
13
14#if CURRENT_LOG_LEVEL >= LOG_LEVEL_INFO
15 #define LOG_INFO(format, ...) printf("[INFO] " format "\n", ##__VA_ARGS__)
16#else
17 #define LOG_INFO(format, ...)
18#endif
19
20#if CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG
21 #define LOG_DEBUG(format, ...) printf("[DEBUG] [%s, line %d] " format "\n", __FILE__, __LINE__, ##__VA_ARGS__)
22#else
23 #define LOG_DEBUG(format, ...)
24#endif
3. 使用示例
1void read_sensor() {
2 int value = read_adc();
3 LOG_DEBUG("ADC raw value: %d", value); // 这行只有在DEBUG级别下才会被编译进程序
4
5 if (value == -1) {
6 LOG_ERROR("Sensor communication failed!");
7 return;
8 }
9
10 if (value > 900) {
11 LOG_WARNING("Value is approaching maximum: %d", value);
12 }
13
14 LOG_INFO("Sensor read successfully: %d", value);
15}
最终效果:
- 在开发阶段,将
CURRENT_LOG_LEVEL
设为LOG_LEVEL_DEBUG
,所有Log信息都会输出。 - 在发布阶段,将其设为
LOG_LEVEL_ERROR
或LOG_LEVEL_NONE
。所有低于ERROR级别的Log(DEBUG, INFO, WARNING)会在编译时被完全移除,不占任何代码空间,也不产生任何运行时开销!
六、高级技巧:自动添加换行与颜色(可选)
1. 自动换行
可以修改宏,自动在末尾添加换行符 \n
,让调用更简洁。
1// 在格式字符串后自动添加换行,用户无需再写 \n
2#define LOG_INFO(format, ...) printf("[INFO] " format "\n", ##__VA_ARGS__)
3
4// 使用
5LOG_INFO("Temperature is %d", temp); // 输出后会自动换行
2. 终端颜色(如果终端支持)
通过添加ANSI转义序列,让不同级别的Log在支持颜色的终端(如Tera Term, PuTTY)上以不同颜色显示,更易阅读。
1// ANSI 颜色代码
2#define ANSI_COLOR_RED "\x1b[31m"
3#define ANSI_COLOR_YELLOW "\x1b[33m"
4#define ANSI_COLOR_GREEN "\x1b[32m"
5#define ANSI_COLOR_BLUE "\x1b[34m"
6#define ANSI_COLOR_RESET "\x1b[0m"
7
8// 带颜色的Log宏
9#define LOG_ERROR(format, ...) printf(ANSI_COLOR_RED "[ERROR] " format ANSI_COLOR_RESET "\n", ##__VA_ARGS__)
10#define LOG_WARNING(format, ...) printf(ANSI_COLOR_YELLOW "[WARN] " format ANSI_COLOR_RESET "\n", ##__VA_ARGS__)
11#define LOG_INFO(format, ...) printf(ANSI_COLOR_GREEN "[INFO] " format ANSI_COLOR_RESET "\n", ##__VA_ARGS__)
12#define LOG_DEBUG(format, ...) printf(ANSI_COLOR_BLUE "[DEBUG] [%s, line %d] " format ANSI_COLOR_RESET "\n", __FILE__, __LINE__, ##__VA_ARGS__)
七、总结与最佳实践
- 使用可变参数宏:
#define LOG(format, ...)
来支持格式化输出。 - 利用内置宏:
__FILE__
,__LINE__
,__func__
来自动添加上下文,快速定位问题。 - 实现分级系统:使用
#if CURRENT_LOG_LEVEL >= ...
条件编译来灵活控制Log的开启/关闭。这是最重要的优化。 - 发布时关闭Debug Log:将
CURRENT_LOG_LEVEL
设为LOG_LEVEL_ERROR
或LOG_LEVEL_NONE
,可以极大减少代码体积,避免性能损耗,并保护调试信息。 - 集中管理:将所有的Log宏定义放在一个头文件(如
debug_log.h
)中,整个项目都包含它,方便统一管理。