임베디드 시스템 디버깅을 위한 효율적인 로깅 레벨 활용
printf 기반 디버깅은 복잡한 시스템일수록 콘솔을 로그로 순식간에 채워버립니다. 로깅 레벨은 런타임 중 CLI를 통해 동적으로 출력 수준을 제어할 수 있어 이 문제를 해결합니다. 이는 유닉스 SysLog 시대부터 표준으로 자리 잡은 방식으로, 각 모듈에 critical, error, info, debug, paranoid 등의 레벨을 할당해 관리합니다.
명령어 ll usb debug는 USB 모듈의 디버깅을 활성화하고, ll pll info는 PLL 모듈을 정보 출력 모드로 설정합니다. PLL 계수를 계산할 때 info 레벨은 최종 결과만 출력하지만, debug 레벨은 반복 알고리즘의 각 단계를 상세히 보여줍니다.
CLI 인터페이스 및 로깅 매크로 구현
애플리케이션은 고유한 facility_t ID를 가진 모듈들로 구성됩니다. CLI(셸/TUI)는 LogLevel(ll) 명령어를 처리하여 모듈별 상세 출력 수준을 구성합니다.
코드에서는 LOG_ERROR, LOG_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 정의 및 문자열 매핑
facility는 각 모듈에 고유 ID를 할당하는 열거형(enum)입니다(SYS=1, GPIO, SPI 등). 하드웨어 기능에 따라 조건부 컴파일됩니다.
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는 초기화 및 로깅 과정에서 이러한 함수를 호출해 구성 상태를 덤프합니다.
표준 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_tID와 시리얼라이저를 활용하면 로그에 명확한 모듈명과 파라미터 값이 표시되어 가독성이 크게 향상됩니다.- 조건부 컴파일(
#ifdef HAS_STREAM)을 적용하면 프로덕션 빌드에서의 오버헤드를 최소화할 수 있습니다. LOG_XXX매크로의 전략적 배치가 중요합니다. 오류에는ERROR를, 개발 단계 추적에는DEBUG를 사용하세요.- 시리얼라이저는 구조체 진단을 자동화하며, AI 도구와 결합하면 레거시 코드 마이그레이션 속도를 획기적으로 높여줍니다.
— Editorial Team
아직 댓글이 없습니다.