내용 요약
현상: 시스템 구동 약 49.7일 후, 틱 카운터 초기화로 인해 주기적 태스크가 무한 대기 상태(Lockup)에 빠짐.
원인: 32비트 무부호 정수(uint32_t)의 오버플로우(0으로 리셋) 발생 시, 잘못된 대소 비교 조건문으로 인해 시간 계산 오류가 발생함.
해결: 미래의 절대 시간 대신 현재 시간 - 이전 시간 $\ge$ 지연 시간 형태의 무부호 정수 차이 연산으로 조건문을 변경함.
시스템 틱 카운터 오버플로우(System Tick Timer Rollover) 발생 증상
임베디드 시스템 설계 시 장기 신뢰성 테스트(Long-term Reliability Test) 단계나 실제 양산 필드(Field)에서 약 49.7일이 경과하는 시점에 하드웨어가 비정상적으로 락업(Lockup)되거나 통신이 두절되는 결함이 발생하기도 합니다.
해외 포럼이나 Stack Overflow 등에서 주로 "system freezes after 49 days", "HAL_GetTick() rollover issue", "lwIP timeout lockup on timer overflow"와 같은 검색 쿼리로 유입되는 고질적인 버그입니다. Watchdog Reset이 내장되어 있지 않거나, 메인 루프 자체는 돌고 있어 워치독은 리프레시되지만 내부 소프트웨어 스케줄러 및 센서 폴링(Polling) 루틴만 영구 동결되는 기이한 형태로 증상이 발현됩니다.
32비트 무부호 정수 타이머의 오버플로우(Unsigned Integer Overflow) 근본 원인 분석
대부분의 ARM Cortex-M MCU 환경(STM32 CubeHAL, FreeRTOS 등)은 1ms 주기로 하드웨어 타이머(SysTick) 인터럽트를 발생시켜 시스템 전역 틱 변수(uint32_t g_systick_counter)를 1씩 증가시킵니다.32비트 변수가 가질 수 있는 최대 정수 범위는 아래와 같습니다.$$2^{32} - 1 = 4,294,967,295 \text{ ms}$$이를 일(Day) 단위로 환산하면 다음과 같은 물리적 한계(Threshold)가 존재하게 됩니다. $$\frac{4,294,967,295 \text{ ms}}{1,000 \text{ ms} \times 60 \text{ sec} \times 60 \text{ min} \times 24 \text{ hours}} \approx 49.7102 \text{ days}$$정확히 구동 후 49일 17시간 2분 47초가 지나면, 레지스터 내부의 비트가 상위로 캐리(Carry)되어 사라지고 0x00000000으로 복귀(Rollover)합니다. 이때, 타이머 만료 여부를 판단하기 위해 미래의 절대 만료 시점(Timeout Target)을 계산하여 현재 틱과 직접 대소 비교를 수행하는 코드가 있다면, 조건식의 논리 기하학이 무너지면서 시스템은 영구적으로 만료 조건을 충족하지 못하는 락에 걸리게 됩니다.
무한 루프 및 타임아웃 장애를 유발하는 잘못된 타이머 비교 코드 (Bad Case)
아래 코드는 현재 틱에 지연 시간을 더해 미래의 절대 목표 시간을 산출한 뒤, 현재 시간이 그 시점을 넘었는지 직관적으로 대소 비교하는 전형적인 버그 코드 구조입니다.
#include <stdint.h>
#include <stdbool.h>
/* Global system tick counter updated by ISR every 1ms */
extern volatile uint32_t g_systick_counter;
/**
* @brief Bad example of checking timeout susceptible to rollover bug.
* @param start_tick: The tick when the operation started
* @param timeout_ms: Duration of timeout in milliseconds
* @return true if timed out, false otherwise
*/
bool Is_Timeout_Bad(uint32_t start_tick, uint32_t timeout_ms)
{
/* BAD: Calculating absolute future time can overflow if near 49.7 days */
uint32_t timeout_target = start_tick + timeout_ms;
/* * If start_tick = 0xFFFFFFF0 and timeout_ms = 30, timeout_target becomes 0x0000000E.
* When g_systick_counter is 0xFFFFFFF5, the condition below (0xFFFFFFF5 >= 0x0000000E)
* evaluates to TRUE immediately, causing a premature timeout failure.
* Conversely, if the check uses (<), it will stay inside an infinite loop.
*/
if (g_systick_counter >= timeout_target)
{
return true;
}
return false;
}
오버플로우를 자동 보정하는 방어적 델타 시간 계산 C 코드 (Good Case)
문제를 완전히 해결하려면 C 언어 명세(ANSI C Standard)에 정의된 무부호 정수 연산의 모듈로 특성(Unsigned Modular Arithmetic Overflow)을 활용해야 합니다. 미래 값을 산출하지 않고, 상시 현재 시간 - 시작 시간의 차이(Delta)를 구한 뒤 기간(Duration)과 비교하면 링 버퍼처럼 오버플로우가 발생해도 하드웨어 보정 연산이 자동으로 수행됩니다.
#include <stdint.h>
#include <stdbool.h>
/* Global system tick counter updated by ISR every 1ms */
extern volatile uint32_t g_systick_counter;
/**
* @brief Robust and defensive timeout check immune to timer rollover.
* @param start_tick: The tick when the operation started
* @param timeout_ms: Duration of timeout in milliseconds
* @return true if timed out, false otherwise
*/
bool Is_Timeout_Good(uint32_t start_tick, uint32_t timeout_ms)
{
/* * GOOD: Unsigned subtraction automatically handles rollover through modular math.
* * Mathematical Verification:
* Let start_tick = 0xFFFFFFF0 (just before rollover)
* Let current g_systick_counter wrapped around to 0x00000005 (after rollover)
* * Delta calculation: 0x00000005 - 0xFFFFFFF0
* In 32-bit unsigned arithmetic:
* 0x00000005 + (0x0000000F + 1) = 0x15 (21 ms elapsed)
* * The delta remains accurate and perfectly tracked despite the overflow.
*/
if ((g_systick_counter - start_tick) >= timeout_ms)
{
return true; /* Timeout condition met reliably */
}
return false; /* Still within valid time frame */
}
핵심 수정 포인트 설명
- 결합 법칙의 격리 (Delta Math): 미래의 타깃 값(start_tick + timeout_ms)을 연산 컴파일 과정에서 배제하고, g_systick_counter - start_tick 형태의 역산(Subtraction) 구조로 변경했습니다.
- 2의 보수 부호 없는 연산 특성 보호: uint32_t형 공간 내에서 작은 값에서 오버플로우 이전의 큰 값을 빼면 가상 비트 차용(Borrow) 현상이 발생하여, 별도의 조건문 분기 처리 없이도 정확한 양수의 경과 시간(Delta Time) 밸류가 복원됩니다.
타이머 오버플로우 디버깅 및 트러블슈팅 가이드 (Debugging Tips)
- 레지스터 강제 주입을 통한 오버플로우 시뮬레이션(Tick Injection): 구동 후 49일을 실제로 켜두고 대기하는 것은 비효율적입니다. 타깃 시스템을 디버거(J-Link / ST-Link)와 연동한 상태에서 브레이크포인트(Breakpoint)를 잡고, IDE의 Expressions 윈도우 또는 Memory View를 통해 전역 틱 카운터 변수(g_systick_counter)의 값을 오버플로우 직전 단계인 0xFFFFFFF0으로 강제 변조(Value Injection)한 후 소프트웨어의 스케줄러 분기 동작을 크로스 체킹해야 합니다.
- 컴파일러 최적화 방지 (volatile 한정자 검증): 타이머 함수 내부에서 틱을 판독할 때, 컴파일러가 레지스터 최적화(Compiler Optimization -O2/-O3)를 수행하여 메모리에서 값을 매번 읽지 않고 CPU 범용 레지스터 캐시에 잡아두면 무한 루프에 빠집니다. 시스템 틱 카운터 원시 변수 선언부에 반드시 volatile 한정자가 명시되어 바이너리가 생성되었는지 빌드 출력 맵 파일(MAP File) 및 디스어셈블리(Disassembly) 코드를 추적 검증하십시오.