Protokollebenen für effektives Debugging von Embedded-Systemen
Printf-Debugging überflutet die Konsole schnell mit Logs, besonders in komplexen Systemen. Protokollebenen lösen dieses Problem, indem sie eine dynamische Steuerung der Ausführlichkeit über die CLI zur Laufzeit ermöglichen. Dies ist seit der Unix-SysLog-Ära Standardpraxis, bei der jedem Modul Ebenen zugewiesen werden: kritisch, Fehler, Info, Debug, paranoid.
Befehle wie ll usb debug aktivieren das Debugging für das USB-Modul, während ll pll info das PLL-Modul auf den Informationsmodus setzt. Bei der Berechnung von PLL-Koeffizienten gibt die info-Ebene nur das Endergebnis aus, wohingegen debug jeden Schritt des iterativen Algorithmus offenlegt.
Implementierung der CLI-Schnittstelle und Logging-Makros
Die Anwendung besteht aus Modulen, die jeweils eine eindeutige facility_t-ID besitzen. Die CLI (Shell/TUI) verarbeitet LogLevel-Befehle (ll), um die Ausführlichkeit pro Modul zu konfigurieren.
Im Code platziert man Makros wie LOG_ERROR, LOG_WARNING usw., die ähnlich wie printf funktionieren, aber einen Facility-Parameter enthalten:
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
}
Die HAS_STREAM-Direktive entfernt das Logging aus Release-Builds, ohne dass Codeänderungen nötig sind – die Makros werden einfach zu nichts kompiliert.
Definition von Facilities und String-Mapping
Eine Facility ist ein Enum, das Modulen eindeutige IDs zuweist (SYS=1, GPIO, SPI usw.), bedingt kompiliert basierend auf 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;
Das FacilityInfo_t-Array ordnet diese IDs menschenlesbaren Strings für die Log-Ausgabe zu:
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
// ...
};
Serialisierer für Strukturdiagnosen
Um Konstanten und Datenstrukturen zu interpretieren, benötigen Sie Serialisierer in dedizierten xxx_diag.c-Dateien. Sie wandeln rohe Binärwerte in lesbare Zeichenketten um:
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;
}
Die CLI ruft diese Funktionen während der Initialisierung und beim Logging auf, um Konfigurationszustände auszugeben.
Standard-log_level_t-Ebenen
Die Protokollebenen sind als Enum mit negativen Werten definiert, um eine korrekte Sortierung zu gewährleisten:
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): Maximale Ausführlichkeit mit hohem Risiko einer Log-Überflutung.
- DEBUG (-3): Debugging-Details und temporäre Zustandsdaten.
- NOTICE (-1): Bemerkenswerte Ereignisse, die Aufmerksamkeit erfordern.
- INFO (0): Standard-Systembetrieb.
- WARNING (1): Unerwartetes Verhalten oder potenzielle Vorboten von Fehlern.
- ERROR (2): Fehler, die sofortige Korrekturen erfordern.
- CRITICAL (3): Schwere Ausfälle, die das System zum Stillstand bringen können.
- COVERAGE (4): Ausführungspfadverfolgung zur Analyse der Codeabdeckung.
Wichtige Erkenntnisse
- Protokollebenen integrieren sich nahtlos in die CLI zur Laufzeitanpassung ohne Neukompilierung.
facility_t-IDs und Serialisierer sorgen dafür, dass Logs mit klaren Modulnamen und Parameterwerten lesbar bleiben.- Bedingte Kompilierung (
#ifdef HAS_STREAM) minimiert den Overhead in Produktions-Builds. - Strategische Platzierung von
LOG_XXX-Makros ist entscheidend: Verwenden SieERRORfür Fehler undDEBUGfür die Entwicklungsnachverfolgung. - Serialisierer automatisieren die Strukturdiagnose und beschleunigen die Migration von Legacy-Code erheblich, wenn sie mit KI-Tools kombiniert werden.
— Editorial Team
Noch keine Kommentare.