返回首页

嵌入式中的日志级别:无瀑布调试

文章描述了嵌入式系统中带有 CLI 接口的日志级别实现。涵盖 LOG_* 宏、facility 枚举、用于诊断的序列化器。与 printf 相比的优势:无需重新编译即可进行动态详细程度设置。

固件调试:日志级别和 CLI
Advertisement 728x90

嵌入式系统高效调试的日志级别指南

在复杂的嵌入式系统中,使用 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) 命令,从而按需配置各模块的日志详细程度。

Google AdInline article slot

在代码中,你可以使用 LOG_ERRORLOG_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 等)。它会根据硬件特性进行条件编译:

Google AdInline article slot
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 会在初始化和记录日志时调用这些函数,从而快速导出当前的配置状态。

Google AdInline article slot

标准 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

Advertisement 728x90

继续阅读