Logging Levels for Effective Embedded System Debugging
Printf debugging quickly floods the console with logs, especially in complex systems. Logging levels solve this by allowing dynamic verbosity control via CLI at runtime. This has been a standard practice since the Unix SysLog era, where each module is assigned levels: critical, error, info, debug, paranoid.
Commands like ll usb debug enable debugging for the USB module, while ll pll info sets the PLL module to informational mode. When calculating PLL coefficients, the info level outputs only the final result, whereas debug reveals each step of the iterative algorithm.
Implementing the CLI Interface and Logging Macros
The application is built from modules, each with a unique facility_t ID. The CLI (shell/TUI) processes LogLevel (ll) commands to configure verbosity per module.
In the code, you place macros like LOG_ERROR, LOG_WARNING, etc., which function similarly to printf but include a facility parameter:
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
}
The HAS_STREAM directive strips logging from release builds without requiring code changes—the macros simply compile down to nothing.
Defining Facilities and String Mapping
A facility is an enum assigning unique IDs to modules (SYS=1, GPIO, SPI, etc.), conditionally compiled based on hardware features:
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;
The FacilityInfo_t array maps these IDs to human-readable strings for log output:
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
// ...
};
Serializers for Structure Diagnostics
To interpret constants and data structures, you need serializers in dedicated xxx_diag.c files. They convert raw binary values into readable strings:
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;
}
The CLI invokes these functions during initialization and logging to dump configuration states.
Standard log_level_t Levels
Log levels are defined as an enum using negative values to ensure proper sorting:
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): Maximum verbosity, with a high risk of log flooding.
- DEBUG (-3): Debugging details and temporary state data.
- NOTICE (-1): Notable events that warrant attention.
- INFO (0): Standard system operation.
- WARNING (1): Unexpected behavior or potential precursors to failures.
- ERROR (2): Faults requiring immediate fixes.
- CRITICAL (3): Severe failures that may halt the system.
- COVERAGE (4): Execution path tracing for code coverage analysis.
Key Takeaways
- Logging levels integrate seamlessly with the CLI for runtime tuning without recompilation.
facility_tIDs and serializers ensure logs remain readable with clear module names and parameter values.- Conditional compilation (
#ifdef HAS_STREAM) minimizes overhead in production builds. - Strategic placement of
LOG_XXXmacros is crucial: useERRORfor faults andDEBUGfor development tracing. - Serializers automate structure diagnostics, significantly accelerating legacy code migration when paired with AI tools.
— Editorial Team
No comments yet.