[Quick Summary (TL;DR) - For Global Developers]
- Symptom: 하드웨어 버튼을 한 번만 눌렀지만 인터럽트(EXTI)가 여러번 호출되어 시스템의 오작동 또는 카운터 중복 발생.
- Cause: 기계적 접점의 탄성으로 인해 수 밀리초(ms) 동안 신호가 진동하는 기계적 채터링(Contact Bounce) 현상이 마이크로컨트롤러(MCU)의 엣지 트리거 인터럽트 회로에 인가됨.
- Solution: 인터럽트 서비스 루틴(ISR) 진입 시 타이머 클럭 시스템 카운터(HAL_GetTick 등)를 활용하여 이전 트리거 시점과의 시간 차(정량적 임계값, 예: 50ms)를 확인하고, 임계값 이하인 경우 예외로 간주하여 연산을 차단하는 필터링 소프트웨어 기법 구현.
하드웨어 버튼 채터링(Contact Bounce) 노이즈와 외부 인터럽트(EXTI) 다중 중첩 실행 증상
임베디드 시스템 설계 과정에서 물리적인 Push Button 또는 스위치 인터페이스를 구현할 때, 마이크로컨트롤러(MCU)의 외부 인터럽트(External Interrupt, EXTI) 핀을 엣지 트리거(Rising/Falling Edge Trigger) 모드로 설정하여 이벤트를 처리하는 방식이 많이 사용됩니다. 그러나 하드웨어 디바운싱 회로(RC Low Pass Filter 등)가 누락되거나 부적절하게 설계된 상태에서 소프트웨어 인터럽트 핸들러만 매핑할 경우, 시스템 오작동이 발생하기도 합니다.
대표적인 증상은 사용자가 버튼을 '한 번'만 압착했음에도 불구하고, 내부 카운터 변수가 여러번 증가하거나 이와 연동된 상태 머신(State Machine)이 여러 단계를 건너뛰어 데드락 상태에 빠지는 현상입니다. 디버깅 환경에서 전역 변수를 모니터링하면 인터럽트 서비스 루틴(Interrupt Service Routine, ISR)이 싱글 이벤트로 처리되지 않고 고속으로 다중 중첩 실행(Multiple ISR Execution)되는 것을 확인할 수 있습니다. 개발자 포럼이나 Stack Overflow 등에서 주로 EXTI Multiple Triggering, Button Bounce Interrupt Issue, GPIO Debounce Bug in ISR 등의 키워드로 검색되는 대표적인 하드웨어-소프트웨어 인터페이스 예외 상황입니다.
기계적 접점 진동 과도 현상(Mechanical Chattering)과 NVIC 스케줄링 메커니즘 분석
이러한 오작동의 원인은 스위치 내부의 기계적 접점이 닫히거나 열리는 순간 발생하는 물리적 탄성 때문입니다. 금속 접점이 완전히 결합되기 전, 수 마이크로초($\mu s$)에서 수 밀리초($ms$) 사이의 극히 짧은 시간 동안 미세한 온/오프(High/Low) 전압 진동 구간이 발생하는데, 이를 채터링(Chattering) 또는 컨택트 바운스(Contact Bounce)라고 합니다.
MCU의 중첩 인터럽트 벡터 컨트롤러(Nested Vectored Interrupt Controller, NVIC) 및 EXTI 주변장치는 주변 시스템 클럭 파이프라인의 속도(수십~수백 MHz)로 동작하므로, 나노초($ns$) 단위의 고속 전압 변화도 유효한 엣지 신호(Edge Transition)로 인식합니다.
- 스위치가 눌리는 순간 바운스 노이즈가 발생합니다.
- EXTI 라인의 에지 검출 레지스터(Edge Detect Register)에 입력 전압이 임계 레벨(V_IH, V_IL)을 상호 교차 통과할 때마다 하드웨어 보류 플래그 비트(Pending Register Bit)가 세팅됩니다.
- 해당 코어의 코드가 한 주기 수행을 완료하기도 전에 NVIC가 연속적인 인터럽트 요청을 받아들여 동일한 ISR 벡터 주소로 지속적인 컨텍스트 스위칭(Context Switching)을 수행합니다.
무한 인터럽트 진입을 유발하는 잘못된 외부 인터럽트 핸들러 C 코드 예시 (Bad Case)
아래 코드는 스위치 입력단 노이즈에 대한 방어 코드가 전혀 고려되지 않은 사례입니다. 입력 엣지가 검출되는 즉시 무조건 인터럽트 카운터를 증가시키기 때문에, 단일 입력 시 수회에서 수십 회의 카운트 누적이 발생할 수 있습니다.
#include "stm32f4xx_hal.h"
/* GLOBAL VARIABLES */
volatile uint32_t g_button_push_count = 0;
/**
* @brief EXTI line detection callbacks.
* @param GPIO_Pin: Specifies the pins connected to the EXTI line.
* @retval None
* @warning This implementation fails to clear mechanical chattering noise.
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
/* BAD: Execute business logic directly without validation */
/* Multiple bounces will trigger this block sequentially within few milliseconds */
g_button_push_count++;
}
}
고속 시스템 틱 카운터(HAL_GetTick) 기반 비차단식(Non-blocking) 소프트웨어 디바운싱 C 코드 (Good Case)
하드웨어 수정 없이 소프트웨어적으로 채터링 노이즈에 의한 중복 인터럽트를 방어하기 위해서는 시간 단위의 가상 윈도우 필터(Time-window Filter)를 ISR 내부 또는 직후 단계에 구축해야 합니다. 인터럽트 핸들러 내에서 단순 지연 함수(HAL_Delay 등)를 사용하는 것은 시스템 전체를 멈추게 하므로 절대 금지되며, 시스템의 독립형 타임스탬프 카운터를 대조하는 비차단(Non-blocking) 시간 비교 연산을 적용해야 합니다.
#include "stm32f4xx_hal.h"
/* DEFINE CONSTANTS FOR DEBOUNCING */
#define DEBOUNCE_THRESHOLD_MS 50U /* 50ms time window to suppress contact bounce */
/* GLOBAL VARIABLES */
volatile uint32_t g_secure_push_count = 0;
volatile uint32_t g_last_interrupt_time = 0;
/**
* @brief Advanced EXTI line detection callback with software debouncing algorithm.
* @param GPIO_Pin: Specifies the pins connected to the EXTI line.
* @retval None
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
/* Capture current system uptime ticker in milliseconds */
uint32_t current_time = HAL_GetTick();
/* Validate the elapsed time since the last valid edge event */
if ((current_time - g_last_interrupt_time) >= DEBOUNCE_THRESHOLD_MS)
{
/* Accept as a valid human-driven button press event */
g_secure_push_count++;
/* Update the last valid timestamp entry */
g_last_interrupt_time = current_time;
}
else
{
/* Reject this event as it falls within the mechanical bounce window */
/* Ignore noise and return immediately to optimize CPU duty cycle */
}
}
}
핵심 수정 포인트 설명
- 독립적 하드웨어 타임스탬프 참조: HAL_GetTick() 내장 타이머 기반 시스템 틱(SysTick 참조 변수)을 호출하여 인터럽트가 발생한 고유 시점의 타임스탬프를 획득합니다. 마이크로초 단위까지 정밀 제어가 필요한 경우 범용 타이머의 카운터 레지스터(TIMx->CNT)로 대체할 수 있습니다.
- 차단 없는 시간 경과 검증(Delta Time Check): (current_time - g_last_interrupt_time) 연산을 적용하여 직전 실행 완료 시점 대비 현재 시점까지 경과된 시간을 판별합니다. 설정된 DEBOUNCE_THRESHOLD_MS(50ms) 시간 이내에 들어오는 모든 부가적인 하위 에지 트리거 신호는 하드웨어 바운싱 노이즈로 인지되어 연산 로직을 타지 않고 즉시 리턴(Early Return) 처리됩니다.
- 오버플로우 방어(Rollover Protection): 부호 없는 정수(Unsigned 32-bit Integer) 연산 특성상 시스템 틱 변수가 최대치(0xFFFFFFFF)에 도달한 뒤 0으로 복귀(Rollover)하는 시점에도 빼기 연산 규칙에 의해 언더플로우가 자동 보정되므로 상시 안전하게 연산 주기를 유지할 수 있습니다.
외부 인터럽트(EXTI) 노이즈 디버깅 및 트러블슈팅 가이드 (Debugging Tips)
- 오실로스코프를 활용한 전압 파형의 과도 응답 계측: 소프트웨어 디바운싱 임계값(Threshold Time)을 하드웨어 명세 없이 임의로 설정하는 것은 위험합니다. 혼선이 발생할 때 오실로스코프(Oscilloscope) 프로브를 해당 GPIO 입력 핀에 물린 상태로 스위치를 눌러, 전압이 진동하는 구간의 완전한 안정화 시간(Settling Time)을 측정하십시오. 일반적으로 실측된 최대 채터링 시간의 1.5배~2배를 소프트웨어 윈도우 마스크 상수로 고정하는 것이 양산 관점에서 안전합니다.
- SFR 디버그 뷰를 통한 Pending Register 강제 클리어 확인: 인터럽트 처리가 종료되는 시점에 물리적인 잔류 노이즈가 극심할 경우, ISR을 빠져나오자마자 보류 레지스터가 즉시 다시 세팅되는 현상이 발생할 수 있습니다. MCU 칩셋 공급사 명세(Datasheet)에 명시된 EXTI 보류 레지스터(예: STM32의 경우 EXTI->PR)에 '1'을 명시적으로 라이트하여 해당 하드웨어 보류 플래그가 완벽히 소거되었는지 IDE의 SFR(Special Function Register) View를 활용해 런타임 단계에서 확인해야 합니다.
- 소프트웨어 카운팅 이벤트 추적 디버깅: 디버거 프로브(J-Link, ST-LINK) 연동 상태에서 가짜 인터럽트 횟수와 유효 횟수를 개별 전역 변수로 할당(예: g_total_raw_interrupts vs g_secure_push_count)한 뒤 실시간 감시 창(Live Watch)에 등록하십시오. 버튼 1회 압착 시 Raw 카운터가 비정상적으로 튀는 와중에도 Secure 카운터가 1만 증가하는지 교차 검증함으로써 디바운싱 알고리즘의 유효성을 확인할 수 있습니다.
'Troubleshooting' 카테고리의 다른 글
| [GPIO 설정] 플로팅(Floating) 노이즈로 인한 핀 오동작과 내부 풀업/풀다운 저항 제어 (0) | 2026.06.19 |
|---|---|
| [MCU 설계] Peripheral 기능 오동작을 유발하는 클록(Clock) 및 GPIO 초기화 순서 오류 해결법 (0) | 2026.06.18 |
| [MCU 설계] 인터럽트 서비스 루틴(ISR) 내 delay/printf 사용 시 발생하는 지연 문제와 대책 (0) | 2026.06.15 |
| [임베디드 C] 인터럽트(ISR)와 메인 루프 간 전역 변수 오염 해결 (Critical Section 설정) (0) | 2026.06.14 |
| [C언어 빌드] extern 전역 변수 중복 정의(Multiple Definition) 및 참조 에러 해결 (0) | 2026.06.13 |