← Embedded C 강의 목록으로
🔌
응용
응용 · 선수: 20강

21. 디바운싱·유한 상태 머신(FSM) 패턴

주변장치 사용법을 익혔으니 이제 펌웨어를 짜는 패턴으로 넘어갑니다. 거의 모든 프로젝트에 등장하는 두 가지 — 디바운싱과 유한 상태 머신(FSM)입니다. 기계식 버튼은 누르는 순간 접점이 수 ms 튀어(채터링) 한 번이 여러 번으로 읽힙니다. 디바운싱은 떨림을 걸러 진짜 입력만 남기고, FSM은 "상태+입력→다음 상태" 규칙으로 동작을 깔끔하게 표현합니다. 하드웨어 비의존 순수 로직이라 gcc로 정확히 검증합니다.

디바운싱채터링FSM상태머신적분기모듈
소요 시간
약 1.5시간
난이도
📊 중급
선수 조건
🎯 20강
결과물
적분기(연속 샘플 카운트) 디바운서를 모듈로 만들고, 디바운스된 확정 에지로 enum+switch FSM을 구동해 입력 정제와 동작 결정을 분리할 수 있습니다.

이 강의에서 배우는 것

  • 1버튼 채터링(바운스)의 원인과 증상을 설명한다
  • 2적분기(연속 샘플 카운트) 방식의 디바운서를 구현한다
  • 3디바운서를 모듈로 분리해 재사용·테스트 가능하게 만든다
  • 4유한 상태 머신의 구성요소(상태·입력·전이)를 이해한다
  • 5디바운스된 확정 에지로 FSM을 구동한다

소개

기계식 버튼의 금속 접점은 닿는 순간 미세하게 튕겨, 한 번 누름이 전기적으로 0→1→0→1…처럼 수 ms간 진동합니다. 이 신호를 그대로 읽으면 여러 번으로 카운트됩니다. 이번 편은 하드웨어 비의존 순수 로직 — 디바운서를 모듈(debounce.c/.h)로 분리하고 그 위에 LED 모드 FSM을 얹습니다.

핵심 개념

1) 적분기 디바운서

일정 주기(예: SysTick 1~5ms)로 핀을 샘플링하고, 안정 상태와 다른 값이 THRESHOLD번 연속될 때만 상태를 바꿉니다. 중간에 한 번이라도 원래 값으로 돌아오면 카운터가 0으로 리셋되어 글리치가 걸러집니다(논블로킹·견고).

c
uint8_t debounce_update(debounce_t *db, uint8_t raw) {
    uint8_t r = raw ? 1u : 0u;
    if (r == db->stable) { db->count = 0u; return 0u; }  /* 떨림 없음 */
    if (++db->count >= DEBOUNCE_THRESHOLD) {             /* N번 연속 */
        db->stable = r; db->count = 0u; return 1u;       /* 확정 변경 */
    }
    return 0u;
}

2) 유한 상태 머신(FSM)

현재 상태입력(눌림 확정)다음 상태
OFFpressSLOW
SLOWpressFAST
FASTpressOFF

C에서는 enum + switch로 구현합니다. 상태가 명시적이라 디버깅·확장이 쉽습니다.

💡

FSM은 디바운서가 확정한 "눌림 에지"만 입력으로 받습니다. 입력 정제(디바운스)와 동작 결정(FSM)을 분리하면 코드가 깔끔합니다. 보드에서는 SysTick 같은 주기 인터럽트에서 raw = (GPIOA->IDR >> pin) & 1 을 읽어 넣기만 하면 됩니다.

핵심 예제

c
/* 노이즈 낀 raw 스트림 → 디바운스 → 눌림 확정 에지마다 OFF→SLOW→FAST 순환 */
typedef enum { MODE_OFF=0, MODE_SLOW=1, MODE_FAST=2 } led_mode_t;
for (i = 0; i < n; i++) {
    if (debounce_update(&db, raw[i]) && debounce_state(&db) == 1u) {
        mode = (led_mode_t)((mode + 1) % 3);   /* 확정 에지에서만 전진 */
    }
}

검증: 짧은 글리치(1샘플)는 무시되고 진짜 누름(THRESHOLD 연속)에서만 모드가 바뀝니다. 빌드: gcc pc_test.c debounce.c -o pc_test.

자주 하는 실수

Q. 디바운스했는데도 가끔 두 번 눌린 것처럼 동작해요.

A. THRESHOLD가 너무 작거나 샘플 주기가 너무 빠릅니다. 보통 샘플 주기 × THRESHOLD가 10~30ms가 되도록 잡습니다(예: 5ms × 3 = 15ms).

Q. 버튼을 누르고 있는 동안 모드가 계속 바뀌어요.

A. "눌림 상태"가 아니라 "눌림으로 바뀐 에지"에서만 동작해야 합니다. debounce_update의 반환값(변경됨)과 새 상태가 1인지를 함께 확인하세요.

Q. FSM을 if-else 더미로 짰더니 상태가 꼬여요.

A. 상태를 enum으로 명시하고 switch로 전이를 한곳에 모으세요. 상태가 코드 곳곳에 흩어지면 추적이 어렵습니다.

정리

  • 채터링은 기계 접점 떨림으로 한 번 누름이 여러 번으로 읽힌다
  • 적분기 디바운서는 같은 값이 N번 연속될 때만 상태를 확정해 글리치를 거른다
  • 디바운서를 모듈로 분리하면 어느 칩에서도 재사용·테스트할 수 있다
  • FSM은 상태·입력·전이로 동작을 표현하며 enum+switch로 구현한다
  • 입력 정제(디바운스)와 동작 결정(FSM)을 분리하면 코드가 깔끔하다

과제

  1. THRESHOLD를 2/5로 바꿔 가며 글리치 거름이 어떻게 달라지는지 관찰
  2. 길게 누르면(long press) 별도 동작을 하는 상태를 FSM에 추가
  3. 두 버튼을 각각 디바운스해 독립적으로 FSM을 구동
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드(과제·정답 포함)는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗