채터링(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 채터링이 발생하는 이유
물리적 원인:
- 메커니컬 접점의 탄성: 버튼을 누를 때 스프링처럼 움직임
- 접점 표면 마찰: 접점이 미끄러지면서 떨림
- 전기적 방전: 접점 분리 순간 스파크로 인한 불안정성
- 진동: 차량 진동이 버튼/센서를 미세하게 흔듦
시간 스케일:
버튼 종류 채터링 시간
─────────────────────────────────────
기계식 푸시 버튼 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 ~ 20ms7.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로 설정'임베디드 기초' 카테고리의 다른 글
| 임베디드 시스템 완벽 정리: 일상 속 숨겨진 컴퓨터들의 세계 (1) | 2026.01.21 |
|---|---|
| 히스테리시스(Hysteresis) 완벽 정리: 불안정한 경계값을 안정화시키는 기법 (0) | 2026.01.08 |
| 포인터와 배열 - 입문자가 반드시 알아야 할 관계 (0) | 2025.12.29 |
| 자동차 개발자가 반드시 알아야 할 ROM, RAM, Flash, EEPROM, NVM의 완벽한 차이 (1) | 2025.12.22 |
| 실무자가 반드시 알아야 할 Static, Extern, Global 변수의 완벽한 차이 (3) | 2025.12.19 |