Troubleshooting

[MCU 설계] 인터럽트 서비스 루틴(ISR) 내 delay/printf 사용 시 발생하는 지연 문제와 대책

임베디드 친구 2026. 6. 15. 21:05
반응형

[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_flagvolatile 키워드를 할당하여, 컴파일러가 매 루프 진입 시 레지스터 캐싱을 배제하고 메모리 주소(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 내부의 로직은 리팩토링 대상입니다.
반응형