기계식 스위치가 접점에서 여러 번 튀는 현상(bounce) 을 걸러내는 처리.
왜 생기는가
버튼을 누르면 금속 접점이 물리적으로 진동한다
이상적: ─────┐
└─────────
실제: ─────┐ ┌┐ ┌─┐
└─┘└─┘ └──── ← 1~20ms 동안 수십 번 튄다
금속이 부딪히며 튀는 물리 현상이라 소프트웨어로 없앨 수 없고, 걸러내야 한다.
무엇이 문제인가
버튼 한 번 눌렀는데
→ 인터럽트가 20번 발생
→ 카운터가 20 증가
→ 메뉴가 20칸 이동
인터럽트 방식일수록 심각하다. 폴링은 주기가 길면 자연스럽게 걸러지기도 하지만, 인터럽트는 모든 에지를 잡는다.
소프트웨어 해결 ① — 시간 기반 (가장 흔함)
#define DEBOUNCE_MS 30
volatile uint32_t last_tick = 0;
void EXTI0_IRQHandler(void) {
uint32_t now = HAL_GetTick();
if (now - last_tick > DEBOUNCE_MS) {
last_tick = now;
button_pressed = 1; // 유효한 입력으로 처리
}
EXTI->PR = EXTI_PR_PR0; // 플래그는 항상 지운다
}
첫 에지를 받아들이고 이후 30ms를 무시한다. 응답이 즉각적이면서 바운스를 걸러 낸다.
now - last_tick으로 비교하는 이유 — 부호 없는 뺄셈은 오버플로가 나도 올바른 차이를 준다.now > last_tick + 30으로 쓰면 49일 뒤 틱 오버플로에서 오작동한다.
소프트웨어 해결 ② — 샘플링 + 안정성 확인
// 1ms 타이머 인터럽트에서 호출
void debounce_poll(void) {
static uint8_t history = 0;
history = (history << 1) | read_pin();
if (history == 0xFF) state = PRESSED; // 8회 연속 High
if (history == 0x00) state = RELEASED; // 8회 연속 Low
}
"연속 N회 같은 값이어야 인정" 하는 방식이다. 시프트 레지스터 하나로 구현되어 매우 가볍고, 노이즈에도 강하다. 여러 버튼을 다룰 때 특히 깔끔하다.
소프트웨어 해결 ③ — 상태 머신
IDLE → (에지 감지) → DEBOUNCING → (타이머 만료 후 재확인) → PRESSED
- (값이 다르면) → IDLE
길게 누르기, 더블 클릭까지 함께 처리해야 하면 상태 머신이 적합하다.
하드웨어 해결
-
① RC 필터 — 저항 + 커패시터로 신호를 둔하게 만든다
- R=10kΩ, C=100nF → 시정수 1ms
- 가장 싸고 간단하지만 에지가 완만해져 슈미트 트리거 입력이 필요
-
② 슈미트 트리거 — 히스테리시스로 중간 전압에서의 떨림 방지
- 많은 MCU의 GPIO 입력에 내장되어 있다
-
③ SR 래치 — SPDT(3단자) 스위치에 사용. 완벽하지만 부품이 늘어난다
실무에서는 RC 필터 + 소프트웨어 디바운싱을 함께 쓰는 경우가 많다. 하드웨어로 큰 노이즈를 줄이고 소프트웨어로 마무리한다.
시간 상수 선택
너무 짧으면(< 10ms) → 바운스를 못 거른다
- 너무 길면(> 100ms) — → 사용자가 반응이 느리다고 느낀다 일반적으로 20~50ms
스위치 종류마다 다르다. 데이터시트에 바운스 시간이 명시되어 있으면 그것의 2배 정도로 잡는다. 택트 스위치는 보통 5~10ms, 릴레이는 더 길다.
릴리스도 디바운싱해야 한다
누를 때만 처리하고 뗄 때를 무시하는 실수가 흔하다. 뗄 때도 똑같이 튀므로, 릴리스 에지를 감지하는 로직이 있다면 동일하게 걸러야 한다.
인터럽트 vs 폴링 — 디바운싱 관점
인터럽트 : 즉각 반응. 대신 바운스마다 진입해 오버헤드
- 첫 에지 후 인터럽트를 잠시 끄고 타이머로 재활성화하는 기법도 있다
폴링(주기 10~20ms) : 자연스럽게 디바운싱된다. 코드가 단순
- 사람이 누르는 버튼에는 폴링이 더 적합한 경우가 많다
버튼처럼 사람이 조작하는 입력은 10ms 지연이 문제되지 않으므로 타이머 폴링이 오히려 좋은 선택이다. 이 판단을 말할 수 있으면 좋다.