嵌入式系统高效调试的日志级别指南
在复杂的嵌入式系统中,使用 printf 进行调试往往会迅速淹没控制台。日志级别(Logging Levels)通过允许在运行时通过命令行接口(CLI)动态控制输出详细程度,完美解决了这一问题。自 Unix SysLog 时代以来,这已成为行业标准实践:每个模块都会被分配不同的日志级别,如 critical(严重)、error(错误)、info(信息)、debug(调试)和 paranoid(详尽)。
例如,执行 ll usb debug 命令即可开启 USB 模块的调试日志,而 ll pll info 则将 PLL 模块设置为信息模式。在计算 PLL 系数时,info 级别仅输出最终结果,而 debug 级别则会完整展示迭代算法的每一步。
实现 CLI 接口与日志宏
应用程序由多个模块构成,每个模块都拥有唯一的 facility_t 标识符。CLI(命令行/文本用户界面)负责解析 LogLevel (ll) 命令,从而按需配置各模块的日志详细程度。
在代码中,你可以使用 LOG_ERROR、LOG_WARNING 等宏。它们的功能与 printf 类似,但额外包含了一个设施(facility)参数:
void log_write(log_level_t level, facility_t facility, const char* format, ...) {
#ifdef HAS_STREAM
bool res = log_write_prefix(level, facility);
if(res) {
va_list va;
va_start(va, format);
cli_vprintf(format, va);
va_end(va);
log_write_end();
}
#endif
}
void LOG_WARNING(facility_t facility, const char* format, ...) {
#ifdef HAS_STREAM
if(log_write_prefix(LOG_LEVEL_WARNING, facility)) {
va_list va;
va_start(va, format);
cli_vprintf(format, va);
va_end(va);
log_write_end();
}
#endif
}
void LOG_DEBUG(facility_t facility, const char* format, ...) {
#ifdef HAS_STREAM
if(log_write_prefix(LOG_LEVEL_DEBUG, facility)) {
va_list va;
va_start(va, format);
cli_vprintf(format, va);
va_end(va);
log_write_end();
}
#endif
}
HAS_STREAM 宏指令允许在发布版本中直接剥离日志代码,无需修改源码——这些宏在编译时会自动优化为空操作。
定义设施标识与字符串映射
facility_t 是一个枚举类型,用于为各个模块分配唯一 ID(如 SYS=1、GPIO、SPI 等)。它会根据硬件特性进行条件编译:
typedef enum {
SYS= 1 ,
UNKNOWN_FACILITY = 2,
#ifdef HAS_ACC
LG_ACC,
#endif
#ifdef HAS_GPIO
GPIO,
#endif
#ifdef HAS_SPI
SPI,
#endif
ALL_FACILITY, /*must be last in enum*/
} facility_t;
FacilityInfo_t 数组将这些 ID 映射为人类可读的字符串,以便在日志输出时清晰显示:
typedef struct{
facility_t facility;
char* name;
}FacilityInfo_t;
#ifdef HAS_MPU
#define MPU_FACILITY_DIAG { .facility = LG_MPU, .name = "Mpu" },
#else
#define MPU_FACILITY_DIAG
#endif
static const FacilityInfo_t FacilityInfo[] = {
// ...
EEPROM_FACILITY_DIAG
MPU_FACILITY_DIAG
// ...
};
用于结构体诊断的序列化器
为了直观地解析常量和数据结构,你需要在独立的 xxx_diag.c 文件中编写序列化器。它们负责将原始二进制值转换为可读字符串:
const char* DacLevel2Str(uint8_t code){
const char *name="?";
switch(code){
case DAC_LEV_CTRL_INTERNALY: name="internally"; break;
case DAC_LEV_CTRL_LOW: name="low"; break;
case DAC_LEV_CTRL_MEDIUM: name="medium"; break;
case DAC_LEV_CTRL_HIGH: name="high"; break;
default: name="??"; break;
}
return name;
}
const char* SpiConfigToStr(const SpiConfig_t* const Config) {
strcpy(text,"");
if(Config) {
sprintf(text, "SPI%u", Config->num);
snprintf(text, sizeof(text), "%sRate:%u Hz,", text, Config->bit_rate_hz);
// ... additional fields
}
return text;
}
CLI 会在初始化和记录日志时调用这些函数,从而快速导出当前的配置状态。
标准 log_level_t 日志级别
日志级别通过枚举定义,采用负值以确保正确的排序逻辑:
typedef enum {
LOG_LEVEL_UNKNOWN = -5,
LOG_LEVEL_PARANOID = -4,
LOG_LEVEL_DEBUG = -3,
LOG_LEVEL_PROTECTED = -2,
LOG_LEVEL_NOTICE = -1,
LOG_LEVEL_INFO = 0,
LOG_LEVEL_WARNING = 1,
LOG_LEVEL_ERROR = 2,
LOG_LEVEL_CRITICAL = 3,
LOG_LEVEL_COVERAGE = 4,
LOG_LEVEL_DISABLE = 5,
LOG_LEVEL_LAST = LOG_LEVEL_DISABLE
} log_level_t;
- PARANOID (-4):最高详细程度,极易导致日志泛滥。
- DEBUG (-3):调试细节与临时状态数据。
- NOTICE (-1):值得关注的显著事件。
- INFO (0):系统常规运行状态。
- WARNING (1):异常行为或潜在故障的前兆。
- ERROR (2):需要立即修复的故障。
- CRITICAL (3):可能导致系统停机的严重故障。
- COVERAGE (4):用于代码覆盖率分析的执行路径追踪。
核心要点
- 日志级别与 CLI 无缝集成,支持运行时动态调整,无需重新编译。
facility_t标识符与序列化器确保日志具备清晰的模块名称和参数值,可读性极强。- 条件编译(
#ifdef HAS_STREAM)可大幅降低生产环境构建的性能开销。 - 合理放置
LOG_XXX宏至关重要:故障排查用ERROR,开发追踪用DEBUG。 - 序列化器能自动完成结构体诊断,结合 AI 工具可显著加速遗留代码的迁移与重构。
— Editorial Team
暂无评论。