WDT로 무인 시스템 정지에 대응하는 방법
WDT(WatchDog Timer)의 감시·타임아웃·복구 구조와 하드웨어·소프트웨어 구현, 임베디드 시스템 설계 시 주의점을 정리한다.
2026-08-14 · 최초 발행 2025-12-28
멈춘 시스템을 기다리지 않게 만드는 감시 타이머
임베디드 장치와 산업 제어 시스템은 사람이 즉시 재시작할 수 없는 상태로 운전되는 경우가 많다. 무한 루프, 데드락, 하드웨어 정지로 정상 처리가 중단되면 WDT(WatchDog Timer)가 Kick 신호의 중단을 감지하고 사전에 정한 복구 동작을 실행한다.
WDT는 단순히 시간을 재는 장치가 아니다. 시스템이 실제로 정상 상태를 통과했는지를 확인하고, 그 증거가 사라졌을 때 리셋이나 인터럽트로 복구 경로를 연다. 마이크로컨트롤러와 센서 노드부터 PLC, 라우터, ECU, ADAS, 생명 유지 장치, 위성, 드론, 스마트 홈 및 웨어러블 장치까지 적용 대상이 넓다.
Kick이 끊길 때 타임아웃이 일어나는 구조
WDT는 시작 시 카운터에 Timeout 값을 설정하고, 클럭에 맞춰 이를 감소시킨다. 시스템은 정상 처리를 완료했다는 의미로 주기적인 Kick 또는 Reset 신호를 보내 카운터를 다시 채운다. 신호가 도착하지 않아 카운터가 0이 되면 WDT는 시스템 리셋, 인터럽트 또는 설정된 복구 조치를 수행한다.
감시 정책을 결정하는 신호와 복구 수단
Kick은 Pet, Refresh, Strobe, Reset이라고도 부른다. 하드웨어에서는 특정 레지스터에 정해진 값을 기록하고, 소프트웨어 구현에서는 카운터를 초기 Timeout 값으로 되돌리는 방식이 사용된다.
// 하드웨어 WDT Kick 예시 (레지스터 기반)
#define WDT_KICK_REG (*((volatile uint32_t*)0x40000004))
#define WDT_KICK_KEY 0x5A // Magic Number
void wdt_kick(void) {
WDT_KICK_REG = WDT_KICK_KEY; // 특정 값 쓰기로 Kick
}
// 소프트웨어 WDT Kick 예시
volatile uint32_t wdt_counter;
void wdt_kick(void) {
wdt_counter = WDT_TIMEOUT_VALUE; // 카운터 리셋
}
Kick은 Timeout의 50-75% 간격으로 보내는 방식을 권장한다. Timeout이 10초라면 Kick 주기는 5-7.5초다.
Timeout은 Kick 없이 허용할 최대 시간이다. 최장 작업 시간보다 길어야 하지만, 과도하게 길면 오류 감지가 늦어지고 너무 짧으면 False Positive가 생긴다.
// Timeout 설정 예시
#define WDT_TIMEOUT_MS 10000 // 10초
void wdt_init(uint32_t timeout_ms) {
// 클럭 주파수: 1MHz
// Timeout = (timeout_ms * 1000) / 클럭 주기
uint32_t timeout_count = timeout_ms * 1000;
WDT_TIMEOUT_REG = timeout_count;
WDT_ENABLE_REG = 1;
}
타임아웃 이후의 조치는 시스템 특성에 맞춰 선택한다. 전체 리셋은 CPU, 메모리, 주변장치를 초기화하는 가장 강한 복구 방식이다.
void wdt_timeout_handler(void) {
// 로그 기록 (선택적, 시간 제약 있음)
log_error("WDT Timeout - System Reset");
// 하드웨어 리셋 트리거
trigger_system_reset();
}
인터럽트 기반 처리에서는 상태를 수집한 뒤 소프트 복구를 먼저 시도할 수 있다. Two-Stage WDT는 1차 인터럽트 뒤 2차 리셋으로 이어지는 구성이다.
void wdt_timeout_isr(void) {
// 디버깅 정보 수집
save_crash_dump();
// 복구 시도
if (soft_recovery_possible()) {
perform_soft_recovery();
wdt_kick(); // 복구 성공 시 Kick
} else {
trigger_system_reset(); // 실패 시 리셋
}
}
NMI는 다른 인터럽트에 막히지 않는 최우선 인터럽트이므로, 긴급 종료 작업과 데이터 보존 뒤 강제 리셋을 수행하는 데 사용할 수 있다.
void NMI_Handler(void) {
// 인터럽트 비활성화 상태에서도 실행
// 긴급 종료 작업
flush_critical_data();
// 강제 리셋
NVIC_SystemReset();
}
복구가 반드시 재부팅일 필요는 없다. 안전 모드 전환, 외부 알람, 백업 시스템 활성화처럼 시스템이 요구하는 사용자 정의 동작을 둘 수 있다.
void wdt_custom_action(void) {
// 안전 모드 전환
enter_safe_mode();
// 외부 알람
trigger_external_alarm();
// 백업 시스템 활성화
activate_backup_system();
}
활성화와 리셋 상태를 분리해 다루기
Enable은 WDT의 시작과 중지를 제어한다. 일부 시스템은 보안상 한 번 활성화한 WDT를 소프트웨어로 비활성화할 수 없게 한다. Window WDT는 허용된 시간 창 안에서만 Kick을 받으므로, 지나치게 빠른 Kick도 오류 신호로 볼 수 있다.
// WDT 활성화
void wdt_enable(void) {
WDT_CTRL_REG |= WDT_ENABLE_BIT;
}
// WDT 비활성화 (디버깅용)
void wdt_disable(void) {
WDT_CTRL_REG &= ~WDT_ENABLE_BIT;
}
// 일부 시스템: 활성화 후 비활성화 불가
// (보안 목적, 소프트웨어가 WDT 무력화 방지)
WDT 자체의 상태를 초기화하는 일과 CPU 및 주변장치를 다시 시작하는 일은 구별해야 한다.
// WDT 초기화
void wdt_reset(void) {
WDT_CTRL_REG = 0; // WDT 비활성화
WDT_COUNTER_REG = 0; // 카운터 초기화
WDT_STATUS_REG = 0; // 상태 클리어
}
// 시스템 리셋
void system_reset(void) {
// ARM Cortex-M 예시
NVIC_SystemReset();
}
클럭 독립성이 복구 가능 범위를 정한다
WDT 카운터는 내부 RC Oscillator, 외부 Crystal, 시스템 클럭 분주 등으로 구동할 수 있다. 내부 RC Oscillator는 독립 동작과 저전력 특성이 있지만 정확도가 낮고, 외부 Crystal은 고정밀이지만 외부 부품이 필요하다. 시스템 클럭 분주 방식은 정확하지만 시스템 클럭에 의존한다.
시스템 클럭이 멈춰도 감시를 계속하려면 독립 클럭 소스가 필요하다. Low Power Oscillator를 쓰면 저전력 모드에서도 WDT를 유지할 수 있다.
// 클럭 소스 선택 예시
void wdt_set_clock_source(WDT_ClockSource source) {
switch(source) {
case WDT_CLOCK_LSI: // Low Speed Internal (32kHz)
WDT_CLK_REG = 0x01;
break;
case WDT_CLOCK_LSE: // Low Speed External (32.768kHz)
WDT_CLK_REG = 0x02;
break;
case WDT_CLOCK_PCLK: // Peripheral Clock / Prescaler
WDT_CLK_REG = 0x03;
break;
}
}
단순 루프 감시를 넘어 태스크 상태를 확인하기
하트비트 방식은 메인 루프가 정상 작업을 마쳤을 때 플래그를 세우고, WDT 태스크가 그 플래그를 확인한 뒤 Kick을 보내는 구조다. 하트비트가 갱신되지 않으면 Kick도 발생하지 않아 타임아웃으로 이어진다.
volatile bool heartbeat_flag = false;
// 메인 루프
while(1) {
process_task();
heartbeat_flag = true; // 정상 동작 표시
}
// WDT 태스크 (주기적 실행)
void wdt_task(void) {
if(heartbeat_flag) {
wdt_kick();
heartbeat_flag = false;
}
// heartbeat_flag가 false면 Kick 없음 → Timeout
}
여러 태스크가 함께 동작하는 시스템에서는 각 태스크의 마지막 실행 시각을 기록하고, 모두 살아 있는 경우에만 Kick을 보내는 편이 낫다.
#define NUM_TASKS 5
volatile uint32_t task_timestamps[NUM_TASKS];
void task_checkin(int task_id) {
task_timestamps[task_id] = get_current_time();
}
void wdt_supervisor(void) {
uint32_t current_time = get_current_time();
bool all_tasks_alive = true;
for(int i = 0; i < NUM_TASKS; i++) {
if(current_time - task_timestamps[i] > TASK_TIMEOUT[i]) {
all_tasks_alive = false;
break;
}
}
if(all_tasks_alive) {
wdt_kick();
}
// 하나라도 타임아웃되면 Kick 없음 → WDT Timeout
}
하드웨어와 소프트웨어로 구현하는 감시 경로
독립 타이머 IC는 CPU와 별도의 감시 경로를 제공한다.
// 예시: MAX6369 External WDT
#define WDT_PIN GPIO_PIN_5
void wdt_init_external(void) {
// WDT 핀을 출력으로 설정
GPIO_Config(WDT_PIN, OUTPUT);
}
void wdt_kick_external(void) {
// 펄스 생성 (Low → High → Low)
GPIO_Write(WDT_PIN, LOW);
delay_us(10);
GPIO_Write(WDT_PIN, HIGH);
delay_us(10);
GPIO_Write(WDT_PIN, LOW);
}
마이크로컨트롤러 내장 WDT는 CPU와 독립적으로 동작하고 저전력 모드에서도 동작할 수 있으며, 일부 모델은 소프트웨어 비활성화를 허용하지 않는다.
// STM32 Independent Watchdog (IWDG) 예시
void iwdg_init(uint32_t timeout_ms) {
// 클럭: LSI 40kHz
// Prescaler: /32 → 1.25kHz
// Reload = timeout_ms * 1.25
uint32_t reload_value = (timeout_ms * 125) / 100;
IWDG->KR = 0xCCCC; // WDT 시작
IWDG->KR = 0x5555; // 레지스터 쓰기 허용
IWDG->PR = IWDG_PRESCALER_32;
IWDG->RLR = reload_value;
IWDG->KR = 0xAAAA; // Kick
}
void iwdg_kick(void) {
IWDG->KR = 0xAAAA; // Reload
}
FPGA에서는 카운터와 타임아웃 출력을 직접 설계할 수 있다.
// Verilog WDT 예시
module watchdog_timer #(
parameter TIMEOUT = 1000000 // 클럭 사이클
)(
input wire clk,
input wire reset_n,
input wire kick,
output reg timeout
);
reg [31:0] counter;
always @(posedge clk or negedge reset_n) begin
if(!reset_n) begin
counter <= TIMEOUT;
timeout <= 1'b0;
end else if(kick) begin
counter <= TIMEOUT;
timeout <= 1'b0;
end else if(counter == 0) begin
timeout <= 1'b1; // Timeout 발생
end else begin
counter <= counter - 1;
end
end
endmodule
소프트웨어 WDT는 RTOS 태스크나 타이머 인터럽트로 구성할 수 있다. 하드웨어 감시와 달리 태스크별 상태를 세밀하게 반영할 수 있다.
// FreeRTOS 예시
void vWatchdogTask(void *pvParameters) {
const TickType_t xDelay = pdMS_TO_TICKS(1000); // 1초
for(;;) {
// 모든 태스크의 상태 확인
if(check_all_tasks_alive()) {
hardware_wdt_kick();
} else {
// 문제 있는 태스크 처리
handle_task_failure();
}
vTaskDelay(xDelay);
}
}
volatile uint32_t wdt_counter = WDT_TIMEOUT;
// 타이머 인터럽트 (예: 1ms마다)
void TIM2_IRQHandler(void) {
if(wdt_counter > 0) {
wdt_counter--;
} else {
// Timeout 처리
wdt_timeout_action();
}
TIM2->SR &= ~TIM_SR_UIF; // 인터럽트 플래그 클리어
}
void wdt_kick_software(void) {
wdt_counter = WDT_TIMEOUT;
}
Window WDT는 50ms ~ 100ms 사이에만 Kick을 허용하도록 구성할 수 있다. 무한 루프에서 Kick만 반복하고 실제 작업은 하지 않는 상태를 감지하는 데 쓴다.
// 너무 빠른 Kick 방지
void wwdg_init(void) {
// 윈도우: 50ms ~ 100ms 사이에만 Kick 허용
WWDG->CFR = 0x7F; // 윈도우 값
WWDG->CR = 0xFF; // 카운터 초기값
WWDG->CR |= WWDG_CR_WDGA; // 활성화
}
void wwdg_kick(void) {
// 윈도우 내에서만 Kick
if(WWDG->CR < 0xBF) { // 윈도우 체크
WWDG->CR = 0xFF;
}
}
운전 환경별 WDT 사용 모습
배터리 구동 온도 센서는 통신에 성공했을 때 Kick을 보내고, 통신 모듈 오류로 무한 대기에 빠지면 타임아웃 뒤 재시작할 수 있다.
// 배터리 구동 온도 센서
void main(void) {
wdt_init(60000); // 60초 타임아웃
while(1) {
// 센서 읽기
float temperature = read_temperature();
// 데이터 전송
if(transmit_data(temperature) == SUCCESS) {
wdt_kick(); // 정상 동작 확인
}
// 저전력 슬립 (50초)
enter_sleep_mode(50000);
}
}
산업용 PLC에서는 제어 루프의 실행 시간을 확인해 정상 범위일 때만 WDT를 갱신한다.
// 주기적 제어 루프
void plc_control_loop(void) {
wdt_init(500); // 500ms 타임아웃
while(1) {
uint32_t start_time = get_tick();
// 입력 읽기
read_digital_inputs();
// 제어 로직 실행
execute_ladder_logic();
// 출력 쓰기
write_digital_outputs();
// 통신 처리
process_modbus();
// 실행 시간 체크
uint32_t elapsed = get_tick() - start_time;
if(elapsed < 450) { // 정상 실행 시간 내
wdt_kick();
} else {
// 과부하 감지 - Kick 없이 리셋 유도
log_error("Control loop timeout");
}
delay_until_next_cycle();
}
}
자동차 ECU는 연료 분사, 점화 타이밍, CAN 통신처럼 중요한 태스크의 실행 시점을 감시한 후 모든 태스크가 정상일 때만 Kick을 보낸다.
// 엔진 제어 유닛
#define CRITICAL_TASKS 3
typedef struct {
uint32_t last_execution;
uint32_t max_interval;
} TaskMonitor;
TaskMonitor task_monitors[CRITICAL_TASKS] = {
{0, 10}, // 연료 분사: 10ms
{0, 20}, // 점화 타이밍: 20ms
{0, 100} // CAN 통신: 100ms
};
void wdt_supervisor(void) {
uint32_t current_time = get_tick();
bool all_ok = true;
for(int i = 0; i < CRITICAL_TASKS; i++) {
if(current_time - task_monitors[i].last_execution >
task_monitors[i].max_interval) {
all_ok = false;
break;
}
}
if(all_ok) {
wdt_kick();
}
}
void fuel_injection_task(void) {
// 연료 분사 제어
control_fuel_injection();
task_monitors[0].last_execution = get_tick();
}
인공호흡기처럼 안전 모드 전환이 필요한 장비에서는 1차 WDT를 인터럽트로, 2차 WDT를 리셋으로 두는 방식을 사용할 수 있다.
// 인공호흡기
void ventilator_control(void) {
// Two-Stage WDT 사용
wdt_init_primary(1000); // 1차: 1초 → 인터럽트
wdt_init_secondary(2000); // 2차: 2초 → 리셋
while(1) {
// 호흡 주기 제어
if(inspiratory_phase()) {
deliver_air();
} else {
allow_exhalation();
}
// 센서 모니터링
monitor_patient_vitals();
// 알람 체크
check_alarm_conditions();
wdt_kick_primary();
wdt_kick_secondary();
}
}
void wdt_primary_timeout_handler(void) {
// 소프트 복구 시도
log_event("Primary WDT timeout");
// 안전 모드 전환
enter_fail_safe_mode();
// 의료진에게 알람
trigger_audible_alarm();
// 2차 WDT는 계속 카운트 (복구 실패 시 리셋)
}
Timeout과 Kick을 배치할 때 확인할 점
Timeout은 측정한 최장 작업 시간에 안전 마진을 반영하고, 최소·최대 제약을 적용해 정할 수 있다.
// 적절한 Timeout 계산
uint32_t calculate_wdt_timeout(void) {
// 최장 작업 시간 측정
uint32_t max_task_time = measure_max_execution_time();
// 안전 마진 추가 (50%)
uint32_t timeout = max_task_time * 1.5;
// 최소/최대 제약 적용
if(timeout < MIN_TIMEOUT) timeout = MIN_TIMEOUT;
if(timeout > MAX_TIMEOUT) timeout = MAX_TIMEOUT;
return timeout;
}
Kick을 루프 시작에만 두면 루프가 형식적으로 반복되는 동안 실제 작업 정지를 놓칠 수 있다. 작업 성공을 확인한 뒤 Kick을 보내야 한다.
// 잘못된 예: 루프 시작에만 Kick
while(1) {
wdt_kick(); // 잘못: 루프만 돌면 Kick
process_task(); // 여기서 정지해도 감지 못함
}
// 올바른 예: 작업 완료 후 Kick
while(1) {
if(process_task() == SUCCESS) {
wdt_kick(); // 올바름: 작업 성공 시만 Kick
}
}
소프트웨어 WDT, RTOS WDT, 하드웨어 WDT를 계층으로 두면 Soft Recovery, Task Reset, System Reset을 서로 다른 타임아웃에 연결할 수 있다.
디버깅에서는 브레이크포인트로 인한 WDT 리셋을 고려해야 한다. 빌드 유형에 따라 비활성화하거나 긴 Timeout을 적용할 수 있다.
void wdt_init_with_debug(void) {
#ifdef DEBUG
// 디버깅 시 WDT 비활성화 또는 긴 Timeout
wdt_init(VERY_LONG_TIMEOUT);
#else
// 릴리스 빌드에서 정상 Timeout
wdt_init(NORMAL_TIMEOUT);
#endif
}
WDT는 클럭과 주변장치 초기화, 중요 서비스 시작이 끝난 뒤 활성화해야 초기 부팅 과정에서 불필요한 타임아웃을 피할 수 있다.
void system_init(void) {
// 하드웨어 초기화
init_clocks();
init_peripherals();
// 중요 서비스 시작
start_critical_services();
// 모든 초기화 완료 후 WDT 활성화
wdt_enable();
}
WDT만으로 감지할 수 없는 상태
Kick 로직 자체가 문제없이 실행되는 버그는 WDT가 가리지 못할 수 있다. 시스템 클럭 자체가 정지하면 그 클럭에 의존하는 WDT도 동작하지 않으며, 정상적으로 긴 작업을 수행하는 경우 False Positive가 발생할 수 있다. 디버깅 중 브레이크포인트에서 멈추는 상황도 리셋을 유발할 수 있다.
내부 WDT에는 빠른 응답을 맡기고, 독립 클럭을 쓰는 외부 WDT에는 긴 Timeout을 맡기는 방식으로 보완할 수 있다. 또한 시스템 상태 검증에 통과했을 때만 Kick을 보내야 한다.
// 외부 독립 WDT 추가
void dual_wdt_system(void) {
// 내부 WDT: 빠른 응답
internal_wdt_init(5000);
// 외부 WDT: 독립 클럭, 긴 Timeout
external_wdt_init(30000);
}
// Kick 로직 검증
void verified_wdt_kick(void) {
// 시스템 상태 검증
if(verify_system_health()) {
wdt_kick();
}
}
WDT는 무인 시스템에서 가용성과 안전성을 지키는 마지막 복구 경로다. 하드웨어 WDT는 CPU와 독립적으로 동작해 시스템 전체 정지에 대응할 수 있고, 소프트웨어 WDT는 태스크 상태를 더 세밀하게 확인한다. Timeout 값, Kick의 위치, 클럭 소스, 복구 동작, 다층 감시 구조를 함께 설계해야 감시 타이머가 실제 오류 상황에서도 제 역할을 한다.