Powrót do strony głównej

Poziomy logowania w embedded: debugowanie bez wodospadu

Artykuł opisuje implementację poziomów logowania w systemach embedded z interfejsem CLI. Omawiane są makra LOG_*, facility enum, serializatory dla diagnostyki. Zalety nad printf: dynamiczna konfiguracja verbosity bez rekompilacji.

Debugowanie firmware: poziomy logowania i CLI
Advertisement 728x90

Poziomy logowania dla efektywnego debugowania systemów wbudowanych

Debugowanie za pomocą printf często prowadzi do przepełnienia konsoli logami, zwłaszcza w złożonych systemach. Poziomy logowania rozwiązują ten problem, umożliwiając dynamiczne zarządzanie szczegółowością komunikatów (verbosity) za pośrednictwem interfejsu CLI w czasie rzeczywistym. To standardowa praktyka wywodząca się jeszcze z czasów Unix SysLog, gdzie dla każdego modułu definiuje się osobne poziomy: critical, error, info, debug, paranoid.

Polecenia takie jak ll usb debug aktywują debugowanie dla modułu USB, a ll pll info ustawia poziom informacyjny dla PLL. Podczas obliczania współczynników PLL na poziomie info wyświetlany jest wyłącznie wynik końcowy, natomiast na poziomie debug widoczne są poszczególne kroki algorytmu iteracyjnego.

Implementacja interfejsu CLI oraz makr logowania

Oprogramowanie budowane jest z modułów posiadających unikalne identyfikatory facility_t. Interfejs CLI (shell/TUI) obsługuje polecenia LogLevel (ll), umożliwiające konfigurację szczegółowości logów dla poszczególnych modułów.

Google AdInline article slot

W kodzie źródłowym umieszcza się makra takie jak LOG_ERROR, LOG_WARNING itp., które działają podobnie do printf, ale przyjmują dodatkowy parametr 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
}

Dyrektywa HAS_STREAM pozwala wyłączyć logi w wersji produkcyjnej bez modyfikacji kodu źródłowego – makra są wówczas kompilowane do pustych instrukcji.

Definiowanie identyfikatorów facility i mapowanie na ciągi znaków

Facility to typ wyliczeniowy (enum) z unikalnymi ID dla modułów (np. SYS=1, GPIO, SPI itd.), które można warunkowo kompilować:

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;

Tablica FacilityInfo_t konwertuje identyfikatory na czytelne nazwy używane w logach:

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

Serializatory do diagnostyki struktur

Do interpretacji stałych i struktur niezbędne są serializatory umieszczane w plikach xxx_diag.c. Konwertują one wartości binarne na czytelne dla człowieka ciągi znaków:

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);
        // ... dodatkowe pola
    }
    return text;
}

Interfejs CLI wywołuje te funkcje podczas inicjalizacji oraz generowania logów, aby wyświetlać aktualne konfiguracje.

Google AdInline article slot

Standardowe poziomy log_level_t

Definicja poziomów jako typu enum z ujemnymi wartościami ułatwia ich sortowanie:

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): Maksymalna szczegółowość, ryzyko „lawiny” logów.
  • DEBUG (-3): Informacje debugowe, dane tymczasowe.
  • NOTICE (-1): Istotne zdarzenia wymagające uwagi.
  • INFO (0): Standardowe działanie systemu.
  • WARNING (1): Nieoczekiwane zdarzenia, zwiastuny błędów.
  • ERROR (2): Błędy wymagające naprawy.
  • CRITICAL (3): Krytyczne awarie, zatrzymanie systemu.
  • COVERAGE (4): Śledzenie ścieżek wykonania kodu.

Kluczowe wnioski

  • Poziomy logowania integruje się poprzez CLI, co umożliwia konfigurację w czasie rzeczywistym bez konieczności rekompilacji.
  • Typ facility_t oraz serializatory zapewniają czytelność logów, wyświetlając nazwy modułów i parametrów.
  • Kompilacja warunkowa (#ifdef HAS_STREAM) minimalizuje narzut wydajnościowy w wersji produkcyjnej.
  • Prawidłowe rozmieszczenie makr LOG_XXX jest kluczowe: dla błędów używamy ERROR, a do debugowania – DEBUG.
  • Serializatory automatyzują diagnostykę struktur, przyspieszając migrację starszego kodu (legacy) z wykorzystaniem sztucznej inteligencji.

— Editorial Team

Advertisement 728x90

Czytaj dalej