[Quick Summary - For Global Developers]
- Symptom: 인터럽트 진입 후 메인 루프(Main Loop)가 완전히 멈추거나, 워치독 타이머 리셋(Watchdog Reset)이 발생하며, 특정 실시간 하드웨어 입력 인터럽트가 누락(Interrupt Missing)됨.
- Cause: 인터럽트 서비스 루틴(ISR) 내부에서 블로킹 방식의 delay 함수나 폴링 기반의 I/O 작업인 printf를 호출하여 상위/동일 우선순위 인터럽트 마스크 및 CPU 자원 독점이 발생함.
- Solution: ISR 내부의 무거운 오버헤드 코드를 전면 제거하고, 최소한의 정적 플래그(Volatile Flag) 세팅 또는 링 버퍼(Ring Buffer) 데이터 적재 후 메인 루프나 RTOS 태스크에서 처리하도록 구조를 분리함.
인터럽트 서비스 루틴(ISR) 내 블로킹 오버헤드로 인한 시스템 다운 및 Interrupt Latency 발생 증상
임베디드 펌웨어 개발 과정에서 하드웨어 인터럽트가 정상적으로 트리거되지만, 디버깅을 위해 인터럽트 서비스 루틴(ISR) 내부에 printf()를 추가하거나 타이밍 조절을 위해 HAL_Delay() 같은 대기 함수를 호출하는 순간 시스템이 비정상적으로 오동작하는 경우가 있습니다.
수많은 엔지니어들이 Stack Overflow나 커뮤니티에 제보하는 증상은 다음과 같습니다. ISR 내부에서 블로킹 연산이 실행되는 순간, 시스템의 실시간성(Real-time Determinism)이 깨지며 Interrupt Latency(인터럽트 지연 시간)가 임계값을 초과합니다. 이로 인해 마이크로컨트롤러(MCU)는 하드웨어 연쇄 반응을 놓치고, 최악의 경우 HardFault_Handler 무한 루프 벡터로 진입하거나, 먹통 현상으로 인해 워치독 타이머 리셋(IWDG Reset)이 발생합니다. 특히 RTOS 환경에서는 커널 스케줄러가 일시 중단되는 시스템 데드락(System Deadlock) 현상으로 이어집니다.
마이크로컨트롤러 중첩 인터럽트(NVIC) 제어 및 I/O 버퍼 기반 인터럽트 지연(Latency)의 원인 분석
이 에러의 원인은 MCU의 인터럽트 컨트롤러(예: ARM Cortex-M의 NVIC) 메커니즘과 C 표준 입출력 함수의 하드웨어 의존적 아키텍처에 있습니다.
첫째, NVIC(Nested Vectored Interrupt Controller)는 인터럽트가 실행되는 동안 동일하거나 낮은 우선순위(Priority)의 다른 인터럽트 예외 요청을 강제로 보류(Pending) 상태로 둡니다. 만약 ISR 내부에서 HAL_Delay()를 호출하면, 이 함수는 대개 시스템 틱 타이머(SysTick) 레지스터의 카운트 변수가 변경될 때까지 루프를 돌며 대기하고 있습니다. 시스템 틱 인터럽트의 우선순위가 현재 실행 중인 ISR의 우선순위보다 낮거나 같다면, 카운트 변수가 업데이트되지 않으므로 ISR 내부에서 영원히 빠져나오지 못하는 무한 블로킹에 빠지게 됩니다.
둘째, printf() 함수는 직렬 통신(UART/USART) 하드웨어의 전송 데이터 레지스터(DR/TDR)에 바이트를 쓰고, 물리적인 전송이 완료될 때까지 상태 레지스터(SR/ISR)의 TXE(Transmit Data Register Empty) 또는 TC(Transmission Complete) 플래그 비트가 1이 될 때까지 CPU가 폴링(Polling)하며 대기하는 동기식 블로킹 매커니즘으로 동작합니다. 115200 bps 속도로 소량의 문자열만 출력하더라도 수 밀리초(ms) 이상의 시간이 소요되며, 이는 CPU 클럭 수만 번의 기계어 사이클을 낭비하며, 다른 하드웨어 인터럽트의 응답 성능을 치명적으로 떨어뜨립니다.
시스템 데드락 및 Interrupt Latency를 유발하는 잘못된 ISR C 코드 예시 (Bad Case)
아래 코드는 타이머 인터럽트 또는 외부 이벤트 인터럽트 발생 시, ISR 내부에서 무거운 블로킹 함수와 동기식 입출력을 처리하여 시스템 마비를 발생시키는 패턴입니다.
#include "stm32f4xx_hal.h"
#include <stdio.h>
extern UART_HandleTypeDef huart2;
/* Bad Case: Executing blocking functions inside Interrupt Service Routine */
void EXTI0_IRQHandler(void)
{
/* Clear external interrupt pending bit */
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
/* CRITICAL BUG: printf utilizes blocking polling method via UART */
printf("External Interrupt 0 Triggered!\r\n");
/* CRITICAL BUG: HAL_Delay relies on SysTick. If SysTick priority <= EXTI0 priority, infinite loop occurs */
HAL_Delay(10);
/* Heavy business logic embedded in ISR */
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
콘텍스트 분리 및 비블로킹 데이터 적재를 구현한 방어적 C 코드 (Good Case)
올바른 설계는 ISR 내부의 실행 시간을 최소화(Minimal Footprint)하고, 실제 연산 및 출력 처리는 메인 루프(Main Background Thread) 또는 RTOS 태스크 콘텍스트에서 처리하는 것입니다. 인터럽트 내에서는 정적 플래그 변수만 신속히 업데이트하거나 링 버퍼(Ring Buffer) 구조에 데이터를 넣은 뒤 즉시 인터럽트 마스크를 해제해야 합니다.
#include "stm32f4xx_hal.h"
/* Volatile qualifier prevents compiler register optimization */
volatile uint8_t g_exti0_event_flag = 0;
/* Good Case: Minimal Footprint ISR with Context Separation */
void EXTI0_IRQHandler(void)
{
/* Clear external interrupt pending bit immediately */
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
/* Fast assignment to minimize interrupt latency */
g_exti0_event_flag = 1;
}
/* Background Main Routine Execution Context */
int main(void)
{
HAL_Init();
SystemClock_Config();
while (1)
{
/* Check the flag synchronously in main loop */
if (g_exti0_event_flag)
{
/* Reset flag in critical section if required */
g_exti0_event_flag = 0;
/* Safe execution of heavy I/O operations outside ISR */
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_UART_Transmit(&huart2, (uint8_t*)"Event Processed\r\n", 17, 100);
}
}
}
핵심 수정 포인트 설명
- volatile 한정자 명시: 전역 플래그 변수 g_exti0_event_flag에 volatile 키워드를 할당하여, 컴파일러가 매 루프 진입 시 레지스터 캐싱을 배제하고 메모리 주소(SRAM)에서 최신 플래그 상태를 직접 읽도록 보장합니다.
- 콘텍스트 완전 분리(Context Separation): 무거운 하드웨어 제어 기능 및 통신 패킷 전송 로직을 ISR 외부의 main 루프로 이관하여 하드웨어 인터럽트 점유 시간을 몇 사이클 이내로 단축시켰습니다.
- Interrupt Latency 해소: 인터럽트가 실행되자마자 플래그만 세팅하고 즉시 리턴하므로, NVIC 스케줄링에 영향을 주지 않아 시스템 전체의 응답성이 확보됩니다.
인터럽트 지연(Interrupt Latency) 디버깅 및 트러블슈팅 가이드 (Debugging Tips)
시스템 멈춤 현상이나 인터럽트 누락이 발생했을 때, 하드웨어 프로브 및 통합 개발 환경(IDE)을 활용하여 실무에서 즉각적으로 원인을 추적하는 기법입니다.
- 디버거 상태 레지스터(NVIC ISPR/ICPR) 분석: 시스템이 중단되었을 때 하드웨어 디버거(J-Link, ST-LINK)의 런타임 실행을 일시 중지(Suspend) 시키십시오. IDE의 Peripheral View 또는 레지스터 검증 창을 통해 NVIC_ISPR(Interrupt Set-Pending Register)를 점검하여 특정 하드웨어 인터럽트 번호가 장시간 보류 상태로 밀려 있는지 확인해야 합니다. 특정 인터럽트 비트가 계속 켜져 있다면, 상위 레벨의 어떤 인터럽트 루틴이 코어를 독점하고 있는 것입니다.
- GPIO 토글 기반 실행 시간 실측(Scope Profiling): 코드 분석으로 타이밍 측정이 어려울 경우, ISR 진입 직후 특정 하드웨어 GPIO 핀을 High로 만들고, ISR 탈출 직전에 Low로 떨어뜨리도록 테스트 코드를 작성하십시오. 그 후 오실로스코프(Oscilloscope)나 로직 분석기(Logic Analyzer)를 해당 핀에 연결하여 인터럽트 서비스 루틴의 실제 물리적 실행 시간(Width)을 측정하십시오. 마이크로초(us) 단위를 넘어 밀리초(ms) 단위까지 핀이 High 상태를 유지한다면 해당 ISR 내부의 로직은 리팩토링 대상입니다.
'Troubleshooting' 카테고리의 다른 글
| [MCU 설계] Peripheral 기능 오동작을 유발하는 클록(Clock) 및 GPIO 초기화 순서 오류 해결법 (0) | 2026.06.18 |
|---|---|
| [GPIO 디바운싱] 버튼 채터링(Contact Bounce)으로 인한 외부 인터럽트(EXTI) 중복 실행 소프트웨어 방어적 설계법 (0) | 2026.06.17 |
| [임베디드 C] 인터럽트(ISR)와 메인 루프 간 전역 변수 오염 해결 (Critical Section 설정) (0) | 2026.06.14 |
| [C언어 빌드] extern 전역 변수 중복 정의(Multiple Definition) 및 참조 에러 해결 (0) | 2026.06.13 |
| [임베디드 빌드] 링커 스크립트(.ld) 메모리 초과 에러(Flash/RAM Overflow) 해결 가이드 (0) | 2026.06.12 |