기술 가이드

MCU 32비트 틱 오버플로: 49.7일 뒤에도 동작하는 타임아웃 만들기

STM32·RTOS의 순환 틱을 경과 시간으로 비교하는 방법과 12개 C 경계값 테스트를 설명합니다. 저전력·틱 단위·관측 간격의 한계도 확인합니다.

목차

49.7일 뒤에만 실패하는 타임아웃을 재현해 보기

대상은 STM32 등 MCU에서 C로 센서 폴링, 통신 응답 대기, 상태 머신을 만드는 펌웨어 개발자입니다. 1ms마다 증가하는 32비트 무부호 카운터는 약 49.7103일마다 0으로 돌아옵니다. 장기 가동 시험에서만 나타나는 즉시 타임아웃이나 멈춤은 이 경계와 관련될 수 있습니다.

시작 시각에 제한 시간을 더한 뒤 now >= deadline으로 비교하면 경계를 넘는 구간의 순서를 제대로 판단하지 못합니다. 시작 값을 저장하고 현재 값에서 시작 값을 뺀 경과 틱을 제한 시간과 비교하세요. 단, 틱이 계속 진행하고 실제 경과 시간이 카운터 한 주기보다 짧다는 전제가 필요합니다.

이 글의 예제는 MCU 없이 호스트에서 경계값을 직접 주입합니다. 오래 켜 두지 않아도 실패 조건을 재현할 수 있습니다.

덧셈으로 만든 마감 시각이 틀리는 경우

시작 값이 0xFFFFFFF0, 제한 시간이 32틱이라고 합시다. 32비트 마감 값은 0x00000010이 됩니다. 아직 0틱밖에 지나지 않았는데도 큰 수인 0xFFFFFFF00x10보다 크므로 단순 대소 비교는 즉시 만료로 오판합니다.

현재 값 실제 경과 틱 32비트 차이 32틱 제한 판정
0xFFFFFFF0 0 0 대기
0xFFFFFFFF 15 15 대기
0x0000000F 31 31 대기
0x00000010 32 32 만료
0x00000011 33 33 만료

uint32_t로 변환한 차이는 2의 32제곱을 기준으로 순환합니다. C11 위원회 초안의 무부호 정수 연산 규칙과 무부호 변환 규칙에 근거한 동작입니다. 부호 있는 정수의 오버플로를 같은 방식으로 취급하면 안 됩니다. C11 초안 N1570, 6.2.5 및 6.3.1.3

복사해서 실행할 수 있는 C 예제

아래 전체 코드를 tick_timeout.c로 저장하세요. 0틱 제한은 즉시 만료로 정의했습니다. 이 예제의 프로젝트 정책은 제한 시간을 0x7FFFFFFF 이하로 정하는 것입니다. 이 제한만으로 장기간 폴링 중단까지 해결되는 것은 아닙니다.

#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>

static bool expired(uint32_t now, uint32_t start, uint32_t limit)
{
return (uint32_t)(now - start) >= limit;
}

struct test_case {
uint32_t start, now, limit;
bool expected;
};

int main(void)
{
const struct test_case cases[] = {
{100U, 100U, 32U, false},
{100U, 131U, 32U, false},
{100U, 132U, 32U, true},
{0xFFFFFFF0U, 0xFFFFFFF0U, 32U, false},
{0xFFFFFFF0U, 0xFFFFFFFFU, 32U, false},
{0xFFFFFFF0U, 0x0000000FU, 32U, false},
{0xFFFFFFF0U, 0x00000010U, 32U, true},
{0xFFFFFFF0U, 0x00000011U, 32U, true},
{0xFFFFFFFFU, 0U, 1U, true},
{123U, 123U, 0U, true},
{0x80000010U, 0x0000000EU, 0x7FFFFFFFU, false},
{0x80000010U, 0x0000000FU, 0x7FFFFFFFU, true}
};
const size_t count = sizeof cases / sizeof cases[0];
for (size_t i = 0; i < count; ++i) {
assert(cases[i].limit <= 0x7FFFFFFFU);
assert(expired(cases[i].now, cases[i].start,
cases[i].limit) == cases[i].expected);
}
uint32_t start = 0xFFFFFFF0U;
uint32_t deadline = start + 32U;
assert(start >= deadline); /* The naive check expires too early. */
assert(!expired(start, start, 32U));
printf("PASS: %zu boundary cases; naive comparison reproduced\n", count);
return 0;
}

GCC가 있는 Linux 또는 Windows의 MSYS2 MinGW 터미널에서 빌드합니다. assert 검증이 실행되도록 -DNDEBUG는 사용하지 마세요.

gcc -std=c11 -Wall -Wextra -Werror tick_timeout.c -o tick_timeout
./tick_timeout

Windows PowerShell에서 실행 파일 이름은 tick_timeout.exe, 실행 명령은 ./tick_timeout.exe입니다. 성공하면 다음 한 줄을 출력합니다.

PASS: 12 boundary cases; naive comparison reproduced

이 예제는 작성 시 Windows의 MinGW GCC로 실제 컴파일·실행하여 위 결과를 확인했습니다. 보드의 인터럽트, 저전력 모드, 드라이버 동작까지 검증한 결과는 아닙니다.

STM32 상태 머신에 붙이는 위치

통신 요청을 보낼 때 시작 틱을 한 번 저장하고, 메인 루프에서 상태를 갱신합니다. 대기 중 매번 시작 틱을 다시 저장하면 타임아웃이 끝나지 않습니다. 다음은 위 expired()를 사용하는 연결 예시이며, HAL_GetTick() 선언은 사용하는 STM32 HAL 헤더에서 가져옵니다.

static uint32_t started_ms;
static bool waiting;

void begin_response_wait(void)
{
started_ms = HAL_GetTick();
waiting = true;
}

void poll_response_timeout(void)
{
if (waiting && expired(HAL_GetTick(), started_ms, 250U)) {
waiting = false;
/* Change state to timeout; decide retry policy elsewhere. */
}
}

실제 수신 완료 경로에서도 waiting = false로 전환해야 합니다. 수신과 시간 초과가 동시에 관측되면 어느 쪽을 우선할지는 프로토콜 요구에 따라 상태 머신에서 정하세요. ISR과 태스크가 상태를 공유한다면 큐, 임계 구역 등 별도의 동기화가 필요합니다. 틱 산술식은 공유 상태의 원자성을 보장하지 않습니다.

ST의 STM32F4 HAL 기본 구현은 밀리초 틱을 반환하며, HAL_Delay() 내부에서도 시작 값과 현재 값의 차이를 사용합니다. 저전력 처리에서 HAL_SuspendTick()을 호출하면 기본 SysTick 증가가 중단되므로 벽시계 기준 제한 시간과 달라질 수 있습니다. 프로젝트가 timebase 함수를 재정의했다면 그 구현부터 확인하세요. 확인한 STM32F4 HAL 소스

RTOS에서는 틱과 밀리초를 구분하기

osKernelGetTickCount()의 결과는 RTOS 틱입니다. osKernelGetTickFreq()가 1000일 때만 1틱이 1ms에 해당합니다. 예를 들어 100Hz 커널에서 250ms는 25틱입니다. 다른 주파수에서는 64비트 중간값으로 올림 변환하고, 결과가 사용할 카운터 및 정책 범위 안에 있는지 검사하세요. 단순히 변수 이름을 ms로 바꾸는 것으로는 단위가 바뀌지 않습니다. Arm CMSIS-RTOS2 커널 틱 API

일반 작업 태스크의 대기는 해당 RTOS의 대기·타임아웃 API를 우선 검토하세요. 바쁜 대기 루프를 만들면 다른 작업과 저전력 진입에 영향을 줄 수 있습니다. 틱이 중단되는 전원 모드에서는 RTC 등 계속 동작하는 시간원을 사용할지, 깨어난 뒤 경과 시간을 보정할지 설계해야 합니다.

현장에서 확인할 네 가지

  1. 카운터 폭과 단위: 이 글은 32비트 무부호 카운터용입니다. 16비트 타이머, 64비트 시간원, 마이크로초 타이머에 그대로 대입하지 마세요.
  2. 관측 간격: 시작 이후 실제 경과 시간이 2^32틱 이상이면 낮은 32비트만으로 몇 바퀴 지났는지 알 수 없습니다. 주기적으로 확인하고, 만료 상태를 확인한 뒤에는 상태 머신에 그 결과를 유지하세요.
  3. 정지와 재시작: 디버거 정지, 인터럽트 차단, 저전력, 리셋은 틱과 실제 시간의 관계를 바꿀 수 있습니다. 부팅 전후 틱 값을 연결해 빼지 마세요.
  4. 기간과 순서의 구별: 경과 기간 계산과 임의의 두 타임스탬프 중 누가 먼저인지 판단하는 문제는 다릅니다. 후자에는 보통 반 주기 이내라는 추가 전제가 필요합니다. 이 예제는 기간 판정 함수입니다.

운영 코드의 틱을 강제로 덮어쓰는 대신 시간 읽기 함수를 주입 가능한 경계로 두고, 위 표의 값들을 테스트 입력으로 사용하면 장기 가동 오류를 짧은 실행으로 점검할 수 있습니다.

공식 문서 확인일: 2026-09-17. 본문의 테스트 프로그램과 상태 머신 연결 예시는 이 가이드를 위해 작성한 코드입니다.

관련 글

처리 중입니다...

잠시만 기다려주세요.