Volver al inicio

Niveles de log en embebidos: depuración sin cascada

El artículo describe la implementación de niveles de log en sistemas embebidos con interfaz CLI. Cubre macros LOG_*, enum de facility, serializadores para diagnósticos. Ventajas sobre printf: configuración dinámica de verbosidad sin recompilación.

Depuración de firmware: niveles de log y CLI
Advertisement 728x90

Niveles de registro para una depuración eficaz de sistemas embebidos

La depuración con printf satura rápidamente la consola con registros, especialmente en sistemas complejos. Los niveles de registro resuelven este problema al permitir un control dinámico de la verbosidad mediante la CLI en tiempo de ejecución. Esta ha sido una práctica estándar desde la era de Unix SysLog, donde a cada módulo se le asignan niveles: crítico, error, información, depuración y paranoico.

Comandos como ll usb debug habilitan la depuración para el módulo USB, mientras que ll pll info configura el módulo PLL en modo informativo. Al calcular los coeficientes del PLL, el nivel info muestra únicamente el resultado final, mientras que debug revela cada paso del algoritmo iterativo.

Implementación de la interfaz CLI y macros de registro

La aplicación se estructura en módulos, cada uno con un identificador único facility_t. La CLI (shell/TUI) procesa los comandos LogLevel (ll) para configurar la verbosidad por módulo.

Google AdInline article slot

En el código, se utilizan macros como LOG_ERROR, LOG_WARNING, etc., que funcionan de manera similar a printf, pero incluyen un parámetro de facilidad:

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
}

La directiva HAS_STREAM elimina por completo el registro en las compilaciones de lanzamiento sin necesidad de modificar el código; las macros simplemente se resuelven a nada durante la compilación.

Definición de facilidades y mapeo de cadenas

Una facilidad es un enum que asigna identificadores únicos a los módulos (SYS=1, GPIO, SPI, etc.), compilado condicionalmente según las características del hardware:

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;

El arreglo FacilityInfo_t mapea estos identificadores a cadenas legibles para la salida de los registros:

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
    // ...
};

Serializadores para diagnósticos de estructuras

Para interpretar constantes y estructuras de datos, se requieren serializadores en archivos dedicados xxx_diag.c. Estos convierten valores binarios sin procesar en cadenas legibles:

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;
}

La CLI invoca estas funciones durante la inicialización y el registro para volcar los estados de configuración.

Google AdInline article slot

Niveles estándar log_level_t

Los niveles de registro se definen como un enum utilizando valores negativos para garantizar un ordenamiento correcto:

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): Verbosidad máxima, con un alto riesgo de saturar el registro.
  • DEBUG (-3): Detalles de depuración y datos de estado temporales.
  • NOTICE (-1): Eventos notables que requieren atención.
  • INFO (0): Operación estándar del sistema.
  • WARNING (1): Comportamiento inesperado o posibles precursores de fallos.
  • ERROR (2): Fallos que requieren corrección inmediata.
  • CRITICAL (3): Fallos graves que pueden detener el sistema.
  • COVERAGE (4): Rastreo de rutas de ejecución para análisis de cobertura de código.

Conclusiones clave

  • Los niveles de registro se integran perfectamente con la CLI para ajustes en tiempo de ejecución sin necesidad de recompilar.
  • Los identificadores facility_t y los serializadores garantizan que los registros sean legibles, con nombres claros de módulos y valores de parámetros.
  • La compilación condicional (#ifdef HAS_STREAM) minimiza la sobrecarga en las compilaciones de producción.
  • La colocación estratégica de las macros LOG_XXX es fundamental: usa ERROR para fallos y DEBUG para el rastreo durante el desarrollo.
  • Los serializadores automatizan el diagnóstico de estructuras, acelerando significativamente la migración de código heredado cuando se combinan con herramientas de IA.

— Editorial Team

Advertisement 728x90

Leer después