본문 바로가기
임베디드 기초

채터링(Chattering) 완벽 정리: 원인, 문제점, 디바운싱 실무 가이드

by 버그없는토마토 2026. 1. 7.

채터링(Chattering) 완벽 정리: 원인, 문제점, 디바운싱 실무 가이드

채터링이란? 디바운싱이란?

 


들어가며

자동차 ECU는 버튼, 스위치, 센서 신호를 수백 밀리, 보통은 10미리초마다 읽죠.
운전자가 버튼을 한 번 누르면, ECU는 하나의 신호를 받아야 합니다.
그런데 현실은 달라요.
메커니컬 버튼을 누르는 순간, 접점(Contact)이 떨리는데 이럴경우 신호가 0-1-0-1-1-0-0-1 처럼 요동칩니다.
이게 바로 채터링입니다.
채터링은 단순해 보이지만, 자동차 소프트웨어에서는 심각한 문제를 만들죠.
버튼 한 번 누르기가 여러 번 누르기로 인식될 수 있다.
CAN 메시지 중복 수신. 센서 오류 판정. 이런 문제들이 쌓이면 차량 제어의 안정성을 떨어뜨리게 됩니다
이 글에서는 채터링의 원인, 차량 시스템에 미치는 영향, 그리고 실무에서 검증된 디바운싱 기법들을 정리해보겠습니다



1. 채터링(Chattering)이란?

1.1 정의

채터링 = 메커니컬 신호의 급격한 상태 변화로 인한 불안정한 진동

메커니컬 버튼 누르기:

시간 (ms)
↓
0      : 누르기 전 (신호 = 0)

10     : 접점 접촉 시작
15     : 접점 떨림 (0-1-0-1-1-0-0-1...) ← 채터링!
20     : 
25     : 
30     : 
35     : 접점 안정화 (신호 = 1)

↓

ECU가 받는 신호:
시간    신호
0ms    0
10ms   0
12ms   1
14ms   0  ← 채터링
16ms   1
18ms   1
20ms   0  ← 채터링
22ms   0
24ms   1  ← 채터링
26ms   1
28ms   1
30ms   1

실제로는 수 밀리초 ~ 수십 밀리초 동안 신호가 불안정하다.

1.2 채터링이 발생하는 이유

물리적 원인:

  1. 메커니컬 접점의 탄성: 버튼을 누를 때 스프링처럼 움직임
  2. 접점 표면 마찰: 접점이 미끄러지면서 떨림
  3. 전기적 방전: 접점 분리 순간 스파크로 인한 불안정성
  4. 진동: 차량 진동이 버튼/센서를 미세하게 흔듦

시간 스케일:

버튼 종류                채터링 시간
─────────────────────────────────────
기계식 푸시 버튼         5 ~ 30ms
자동차 버튼              2 ~ 10ms
릴레이 접점             1 ~ 5ms
센서 신호 (CAN, LIN)    1ms 이하 (네트워크 레벨)

1.3 채터링이 미치는 영향

자동차 시스템에서 채터링은 다양한 문제를 야기한다:

버튼 입력한 번 누르기 → 여러 번으로 인식창문 조작 오류, 시트 위치 변경 실패
CAN 통신프레임 손상, 중복 수신엔진 제어 오류, 진단 오류 축적
센서 신호한계값(Threshold) 근처에서 진동잘못된 오류 판정 (DTC 발생)
상태 머신상태 전환 불안정제어 로직 예측 불가
CPU 부하과도한 인터럽트 처리실시간성 저하

2. 채터링의 실무 사례

2.1 Case 1: 파워 윈도우 제어

상황:
운전자가 파워 윈도우 UP 버튼을 한 번 누른다.

기대 동작:
UP 신호 수신 → 1회 인식 → 창문 전부 올라감

채터링 없는 신호:
시간 (ms)  신호    해석
0          0       누르기 전
10         1       UP 버튼 눌림 ← 인식
20         1
30         1
40         1
50         0       손가락 뗌

결과: ✅ 창문 1회 올라감


채터링 있는 신호:
시간 (ms)  신호    해석
0          0       누르기 전
10         0
12         1       ← 인식 #1 (올라감 시작)
14         0       ← 인식 #2 (올라감 중단)
16         1       ← 인식 #3 (올라감 재시작)
18         1
20         0       ← 인식 #4
22         1       ← 인식 #5
24         1
26         1
28         1
30         1
40         1
50         0       손가락 뗌

결과: ❌ 창문 움직임이 끊김. 여러 번 인식되어 제어 불안정

2.2 Case 2: CAN 메시지 손상

상황:
CAN 트랜시버에 입력되는 신호가 채터링을 겪는다.

채터링이 없을 때:
CAN 메시지: 0x123 [8] 12 34 56 78 9A BC DE F0
         ─────────── 안정적 ───────────
수신 결과:  올바른 메시지 1개


채터링이 있을 때:
CAN 메시지: 0x123 [8] 12 34 56 78 | 채터링 | 9A BC DE F0
         ─────────── 안정적 ─────┬──────────┬──────── 안정적 ─
                                 └ 신호 손상
수신 결과:  CRC 오류 → 메시지 폐기 또는 중복 수신

2.3 Case 3: 온도 센서 오류 판정

상황:
엔진 온도 센서가 경계값(Threshold) 근처에서 진동한다.

설정: 엔진 온도 > 95°C이면 "과열" DTC 발생

센서 신호 (채터링):
시간    온도     상태         DTC 발생?
─────────────────────────────────────
0ms    94.5°C   정상         X
5ms    94.8°C   정상         X
10ms   95.1°C   과열         DTC 설정 #1
15ms   94.9°C   정상         X DTC 해제
20ms   95.3°C   과열         DTC 설정 #2
25ms   94.7°C   정상         X DTC 해제
30ms   95.2°C   과열         DTC 설정 #3
...

결과:  DTC가 계속 set/clear 반복
      진단 시스템은 불안정한 센서로 판정
      운전자에게 불필요한 경고음

3. 디바운싱 기법 (SW 방식)

3.1 타이머 기반 디바운싱 (가장 일반적)

원리:
신호가 변화한 후, 일정 시간(Debounce Time) 동안 신호가 안정적인지 확인하고, 안정적일 때만 신호 변화로 인정한다.

#define DEBOUNCE_TIME_MS 20  // 20ms 안정화 필요

uint8_t button_state = 0;
uint32_t debounce_timer = 0;

void Button_ReadInput(void) {
    static uint8_t debounced_state = 0;
    uint8_t raw_input = GPIO_ReadPin(BUTTON_PIN);

    // 신호 변화 감지
    if (raw_input != button_state) {
        button_state = raw_input;
        debounce_timer = 0;  // 타이머 초기화
    }

    // 타이머 증가
    debounce_timer++;

    // 충분히 안정적이면 상태 갱신
    if (debounce_timer >= DEBOUNCE_TIME_MS / 10) {  // 10ms 주기라고 가정
        if (raw_input != debounced_state) {
            debounced_state = raw_input;

            if (debounced_state == 1) {
                Button_OnPressed();  // 버튼 누름 이벤트
            } else {
                Button_OnReleased();  // 버튼 뗌 이벤트
            }
        }
    }
}

동작 흐름:

Raw Signal (채터링):
┌─┐
│ │ 1  1  0  0  1  1  1  0  1  1
└─┘ ├──┼──┼──┼──┼──┼──┼──┼──┼──┤
     0  5  10 15 20 25 30 35 40 45 ms

타이머 시작 후:
(신호 변화마다 타이머 리셋)

Debounce Timer:
    ├──┼──┼──┼──┼  Reset! ├──┼──┼──┼──┼  Reset! ├──┼──...
    0  5  10 15 20           0  5  10 15 20           0

20ms 안정화:
    ├──┼──┼──┼──┼──┐ (20ms 도달!)
    0  5  10 15 20 25
                    ▼ State change 확인
                    신호 변화로 인정!

3.2 히스터시스 (Hysteresis) 방식

온도 센서처럼 연속값을 받을 때 유용하다.

#define THRESHOLD_UP   95.0f   // 올라갈 때의 경계값
#define THRESHOLD_DOWN 93.0f   // 내려갈 때의 경계값 (더 낮음)

uint8_t engine_overheating = 0;

void Temperature_Check(float current_temp) {
    if (engine_overheating == 0) {
        // 정상 상태: THRESHOLD_UP를 넘어야 과열로 전환
        if (current_temp > THRESHOLD_UP) {
            engine_overheating = 1;
            SetDTC(P0125_ENGINE_OVERTEMP);
        }
    } else {
        // 과열 상태: THRESHOLD_DOWN까지 내려야 정상으로 전환
        if (current_temp < THRESHOLD_DOWN) {
            engine_overheating = 0;
            ClearDTC(P0125_ENGINE_OVERTEMP);
        }
    }
}

효과:

온도 변화:
94.5 → 95.2 → 95.1 → 95.3 → 95.5 (채터링)

히스터시스 없이:
95.0°C 기준
94.5: 정상
95.2: 과열 ← DTC 설정
95.1: 정상 ← DTC 해제
95.3: 과열 ← DTC 설정 (불안정!)

히스터시스 있이:
Up:93°C, Down:95°C
94.5: 정상
95.2: 과열 ← DTC 설정
95.1: 여전히 > 93°C → 과열 유지! (안정!)
95.3: 과열 유지
95.5: 과열 유지
...
92.8: 93°C 이하 → 정상으로 전환

3.3 다중 샘플링 (Majority Voting)

여러 번 샘플링해서 대다수 의견으로 결정한다.

#define SAMPLE_COUNT 5

uint8_t Button_IsPressedDebounced(void) {
    static uint8_t sample_buffer[SAMPLE_COUNT] = {0};
    static uint8_t sample_index = 0;

    uint8_t count_pressed = 0;

    // 샘플 저장
    sample_buffer[sample_index] = GPIO_ReadPin(BUTTON_PIN);
    sample_index = (sample_index + 1) % SAMPLE_COUNT;

    // 5개 샘플 중 누가 눌렸는지 개수 세기
    for (int i = 0; i < SAMPLE_COUNT; i++) {
        count_pressed += sample_buffer[i];
    }

    // 3개 이상이 1이면 → 버튼이 눌렸다고 판정
    return (count_pressed >= SAMPLE_COUNT / 2) ? 1 : 0;
}

동작:

Raw Signal:
시간    신호
0ms    1
2ms    0  ← 채터링
4ms    1
6ms    0  ← 채터링
8ms    1

다중 샘플링 (5개 중 3개 이상):
1 + 0 + 1 + 0 + 1 = 3개
→ 3 >= 3 ? YES
→ 버튼이 눌렸다고 판정! ✓

4. 디바운싱 기법 (HW 방식)

4.1 RC 필터 (저역 통과 필터)

회로:

버튼 ──[100kΩ]──┬── → GPIO
                │
            [100nF]
                │
               GND

시간 상수 τ = R × C = 100k × 100n = 10ms

효과:

Raw Signal (채터링):
1 ┌─┐
  │ │
0 └─┘ 1 0 1 0 1 ← 채터링
  ┌──────────────

Filtered Signal (RC):
1     ╱──────╲
     ╱        ╲
0  ─┘          ╲
  ├─ τ ┤ ├─ τ ┤
  (10ms) (10ms)
  ← 부드러워짐, 채터링 제거

4.2 슈미트 트리거 (Schmitt Trigger)

회로:

버튼 ────┬──[op-amp]──┬── → GPIO
         │            │
         └─[100nF]────┴─ Feedback

특성: 두 개의 경계값 (Upper, Lower)

동작:

입력 신호:
0.5V ─────────╱────────╲─────╱─────
       ↑              ↓
    Lower Threshold Upper Threshold
       ↓              ↑
출력 신호 (0 또는 5V):
5V  ────────────────────────

0V  ────────────────────────

채터링 제거됨! ✓

5. 자동차 실무 디바운싱 구현

5.1 멀티코어 환경에서의 고려사항

// Core 0 (10ms 주기)
void ButtonInput_ReadTask(void) {
    uint8_t raw = GPIO_ReadPin(BUTTON_PIN);

    // 공유 변수 보호 (뮤텍스 또는 원자적 연산)
    SPINLOCK_ACQUIRE(&button_lock);
    button_raw_data = raw;
    SPINLOCK_RELEASE(&button_lock);
}

// Core 1 (5ms 주기)
void ButtonInput_DebounceTask(void) {
    static uint32_t timer = 0;
    uint8_t raw;

    // 데이터 읽기 (뮤텍스 사용)
    SPINLOCK_ACQUIRE(&button_lock);
    raw = button_raw_data;
    SPINLOCK_RELEASE(&button_lock);

    // 디바운싱 로직
    ...
}

5.2 CAN 메시지 수신 필터링

void CanRx_ProcessEngineStatus(const CanMessage_t* msg) {
    static CanMessage_t prev_msg = {0};
    static uint32_t debounce_count = 0;

    // 같은 메시지가 3회 연속 수신되면 유효
    if (msg->Id == prev_msg.Id &&
        memcmp(msg->Data, prev_msg.Data, 8) == 0) {
        debounce_count++;
    } else {
        debounce_count = 0;
    }

    if (debounce_count >= 3) {
        // 메시지 처리 (채터링 제거됨)
        EngineStatus_Update(msg);
        debounce_count = 0;
    }

    prev_msg = *msg;
}

5.3 센서 신호 필터링 (AUTOSAR 방식)

// AUTOSAR에서는 RTE (Run-Time Environment)로 신호 필터링 제공

// *.arxml 설정 예시:
/*
<PortInterface>
    <Port Name="EngineTemp">
        <Type>AnalogInput</Type>
        <Filter>
            <Type>Debounce</Type>
            <Time>20ms</Time>
            <Hysteresis>
                <HighThreshold>95.0</HighThreshold>
                <LowThreshold>93.0</LowThreshold>
            </Hysteresis>
        </Filter>
    </Port>
</PortInterface>
*/

// 코드에서는 자동으로 필터링됨:
void EngineControl_MainFunction(void) {
    float temp = Rte_Read_EngineTemp_value();  // 이미 필터링됨!

    if (temp > 95.0f) {
        EngineControl_Derate();  // 안정적인 값만 수신
    }
}

6. 성능 영향 분석

6.1 응답 시간 vs 안정성 트레이드오프

Debounce Time vs 응답 시간:

Debounce Time = 0ms (없음):
├─  응답 즉시 (0ms)
├─  채터링으로 인한 오류
└─  신뢰성 낮음

Debounce Time = 10ms:
├─  응답 지연 (10ms)
├─  채터링 제거
└─  신뢰성 개선

Debounce Time = 50ms:
├─  응답 지연 (50ms)
├─  완전한 채터링 제거
├─  신뢰성 최고
└─  운전자가 지연 느낄 수 있음


추천 Debounce Time:
┌──────────────┬─────────────────┐
│ 신호 종류    │ Debounce Time   │
├──────────────┼─────────────────┤
│ 버튼         │ 20 ~ 50ms       │
│ 센서 (아날로그) │ 10 ~ 20ms    │
│ 센서 (디지털)│ 5 ~ 10ms        │
│ CAN 메시지   │ 5 ~ 10ms        │
└──────────────┴─────────────────┘

6.2 CPU 부하

타이머 기반 디바운싱:
- 메모리: O(1) (타이머 변수만)
- CPU: O(1) (샘플 + 비교 + 타이머 증가)
- 주기: 10ms마다 호출
- 부하: ~ 0.1% (1000줄의 코드 중 1줄)

다중 샘플링:
- 메모리: O(N) (버퍼 크기만큼)
- CPU: O(N) (루프로 개수 세기)
- 주기: 2ms마다 호출 (더 빈번)
- 부하: ~ 0.5% (더 높음)

HW RC 필터:
- CPU: 0% (하드웨어에서 처리)
- 메모리: 0%
- 주기: 없음 (항상 동작)
- 부하: 없음 (추천!)

7. 자주 하는 실수

7.1 Debounce Time을 너무 길게 설정

- 잘못된 예:
#define DEBOUNCE_TIME_MS 500  // 500ms는 너무 길다!

문제:
- 운전자가 버튼을 누른 후 0.5초 기다려야 반응
- 사용자 경험 저하
- 연속 입력 불가능 (예: 음량 조절)

- 올바른 예:
#define DEBOUNCE_TIME_MS 20  // 20ms

기준:
- 버튼: 20 ~ 50ms
- 센서: 10 ~ 20ms

7.2 멀티스레드 환경에서 동기화 없음

- 잘못된 예:
uint8_t raw_signal = 0;  // 공유 변수

void Task_A(void) {  // Core 0
    raw_signal = GPIO_ReadPin(PIN);  // 쓰기
}

void Task_B(void) {  // Core 1
    if (raw_signal) {  // 읽기
        // ...
    }
}

문제:
- Race condition 발생 가능
- 불안정한 값 읽음

- 올바른 예:
SPINLOCK_t signal_lock = SPINLOCK_INIT;
uint8_t raw_signal = 0;

void Task_A(void) {
    SPINLOCK_ACQUIRE(&signal_lock);
    raw_signal = GPIO_ReadPin(PIN);
    SPINLOCK_RELEASE(&signal_lock);
}

void Task_B(void) {
    SPINLOCK_ACQUIRE(&signal_lock);
    if (raw_signal) {
        // ...
    }
    SPINLOCK_RELEASE(&signal_lock);
}

7.3 경계값 설정을 하나로 고정

- 잘못된 예 (센서 신호):
#define TEMP_THRESHOLD 95.0f

문제:
- 94.9°C ↔ 95.1°C 진동 시 불안정
- 앞서 본 온도 센서 사례

- 올바른 예 (히스터시스):
#define TEMP_THRESHOLD_UP   95.0f
#define TEMP_THRESHOLD_DOWN 93.0f

효과:
- 한 번 상태 변경되면 큰 폭으로 변해야 다시 바뀜
- 안정성 향상

7.4 CAN 메시지 중복 필터링 없음

- 잘못된 예:
void CanRx_EngineData(const CanMessage_t* msg) {
    // 모든 수신 메시지 즉시 처리
    EngineRPM = msg->Data[0];
    EngineTemp = msg->Data[1];
}

문제:
- 채터링으로 인한 중복 메시지 → 신호 불안정
- 불필요한 연산 증가

- 올바른 예:
void CanRx_EngineData(const CanMessage_t* msg) {
    static CanMessage_t prev_msg = {0};

    // 이전 메시지와 다르면 처리
    if (memcmp(msg->Data, prev_msg.Data, 8) != 0) {
        EngineRPM = msg->Data[0];
        EngineTemp = msg->Data[1];
        prev_msg = *msg;
    }
    // 같으면 무시
}

8. 핵심 정리

채터링(Chattering):
- 메커니컬 신호의 불안정한 진동
- 버튼, 스위치, 센서에서 발생
- 자동차 시스템의 신뢰성 저하

원인:
- 메커니컬 접점의 탄성
- 표면 마찰 및 스파크
- 주변 진동

디바운싱 (SW 방식):
- 타이머 기반 (20 ~ 50ms)
- 히스터시스 (경계값 2개)
- 다중 샘플링 (Majority Voting)
- CAN 메시지 필터링

디바운싱 (HW 방식):
- RC 필터 (저역 통과)
- 슈미트 트리거 (이상적)

응답 시간 vs 안정성:
- Debounce Time이 길수록 안정적
- 너무 길면 사용자 경험 저하
- 신호 종류에 따라 10 ~ 50ms 권장

멀티코어 환경:
- 동기화 필수 (뮤텍스, 스핀락)
- Race condition 방지

AUTOSAR:
- RTE에서 자동 필터링 설정 가능
- *.arxml로 설정