C 디바운서 구현
120분 안팎
학습 목표
HAL 입력에 20ms 안정 구간과 눌림 에지 검출을 적용합니다.
개념
Python 상태를 C 객체로 옮깁니다
브라우저에서 확인한 시간 차 규칙을 Button 구조체로 옮깁니다. 후보·안정값을 지역 변수로 매번 초기화하면 여러 호출에 걸친 유지 시간을 기억할 수 없습니다. Button은 stable, candidate, since_ms, last_ms를 소유하고 호출자는 같은 객체의 주소를 계속 전달합니다. 필터 내부에서 핀을 읽지는 않습니다. HAL이 성공적으로 얻은 레벨과 시각을 인자로 주면 장치 접근과 상태 계산을 각각 시험할 수 있습니다. 제공한 button_poll.c는 hal_gpio_read(0)의 성공 상태를 확인하고 hal_now_ms와 레벨을 button_update에 전달합니다. test_button_hal.c는 입력 bit0과 19/20ms 및 해제 상태를 8개 조건으로 검증합니다. 애플리케이션은 button_poll.h를 포함하고 환경 주입 함수는 테스트 쪽에만 둡니다.
button_init은 안정값과 후보값을 해제 0으로 정하고 두 시각을 주어진 now로 맞춥니다. 레벨 1을 처음 받은 호출은 candidate만 변경합니다. 부팅할 때 버튼을 계속 누르고 있다면 20ms 뒤 PRESS 한 번이 인정됩니다. 최초 레벨을 곧 stable에 넣는 초기화는 부팅 눌림을 이벤트 없이 흡수하는 다른 정책입니다. 어느 쪽이든 가능하지만 이 실습은 문서와 테스트에 정한 정책을 유지합니다.
API의 네 결과를 구별합니다
button_update는 변화 없음 0, 눌림 확정 1, 해제 확정 2, 잘못된 입력 -1을 반환합니다. 레벨 1과 반환값 1은 같은 숫자라도 서로 다른 의미입니다. 출력 event=1은 눌림 에지이며 stable=1은 유지 상태입니다. 애플리케이션에서 if(event)로 토글하면 해제 2와 오류 -1에도 반응합니다. 상수 BUTTON_PRESS와 정확히 비교하는 코드를 사용합니다.
NULL 포인터, 레벨 2, 허용하지 않는 시계 순서를 검사한 뒤 필드를 갱신합니다. 유효하지 않은 호출은 상태를 보존해야 다음 정상 호출이 오류에 의해 오염되지 않습니다. tests는 반환값뿐 아니라 last_ms와 stable 등 저장 상태를 함께 확인합니다. 구조체 전체를 memcmp로 비교하면 패딩 표현까지 비교할 수 있으므로 의미 있는 필드를 비교하는 방식을 씁니다. 실패 보존 계약을 자료 의미로 설명합니다.
호출자는 원시 입력이 성공한 뒤에만 필터를 호출합니다. hal_gpio_read의 상태 반환이 실패면 out에 남아 있는 숫자를 새 관측처럼 쓰지 않습니다. 이번 독립 필터에는 GPIO 연결이 없으므로 HAL 실패는 다음 레슨의 컨트롤러 시험에서 넣습니다. 함수 인자 검사가 있다고 장치 읽기 실패까지 알아서 해결되는 것은 아닙니다. 각 계층이 어떤 오류를 관측할 수 있는지 분리합니다.
후보 시작 시각과 안정값을 갱신합니다
level이 candidate와 다르면 candidate를 교체하고 since_ms를 now로 설정합니다. 이후 stable과 candidate가 다르고 경과 시간이 20 이상일 때만 stable을 candidate로 갱신합니다. 결과는 새 stable이 1이면 PRESS, 0이면 RELEASE입니다. 유지 상태에서 stable과 candidate가 같다면 시간이 아무리 길어져도 이벤트가 없습니다. 임계 시간 검사와 에지 검사 둘 중 하나를 빼면 짧은 튐이나 긴 누름에서 오류가 드러납니다.
실습 starter는 함수와 검증 조건이 있지만 시간 대기 조건을 잘못 만들어 후보 변화를 즉시 확정합니다. 컴파일은 성공하고 candidate at zero 등의 테스트가 실패합니다. button.c의 TODO에서 차이 비교를 완성합니다. 테스트 파일을 먼저 읽어 초기 눌림, 튐, 해제, 재눌림의 기대값을 시간표에 적습니다. solution을 펼치기 전 어떤 필드가 각 행에서 바뀌어야 하는지 설명합니다.
해제의 튐도 후보 시간을 다시 시작합니다. demo에서 101ms 해제 후보, 104ms 눌림 후보, 108ms 해제 후보를 주면 RELEASE는 128ms입니다. 해제는 즉시 적용하고 눌림만 기다리는 구현은 이 계약과 다릅니다. 해제 튐이 안정값을 너무 빨리 0으로 만들면 접점이 다시 1로 튈 때 새로운 눌림을 만들 위험이 있습니다. 양쪽 전이를 같은 기준으로 거른다는 선택을 시험합니다.
32비트 시계 차를 제한된 계약으로 사용합니다
hal_now_ms는 uint32_t이므로 최대값 다음에는 0으로 순환할 수 있습니다. now가 since보다 작다는 이유로 모두 역행이라고 판단하면 정상 순환을 거부합니다. 부호 없는 차이를 uint32_t로 계산하면 한 번 순환한 경과 시간을 표현할 수 있습니다. 테스트는 UINT32_MAX-9에서 후보를 시작해 now=9에서 19ms, now=10에서 20ms인 경우를 검증합니다. 덧셈으로 절대 마감 시각을 만들고 단순 크기 비교하지 않습니다.
시간 순서를 아무 제약 없이 추론할 수는 없습니다. 이번 계약은 연속 호출 간격이 INT32_MAX ms 이하라는 조건입니다. last_ms에서 now까지의 unsigned 차이가 이 상한을 넘으면 시간 오류로 거부합니다. 짧은 역행은 큰 unsigned 차이로 나타나므로 이 조건에 걸립니다. 매우 긴 공백과 역행을 완벽히 구분하는 시스템은 아니며, 여러 순환을 건너뛴 경우도 보증하지 않습니다. reset된 시계에는 객체도 다시 초기화합니다.
같은 시각에 같은 레벨을 재조회하는 호출은 허용합니다. 시간이 더 지나지 않았으므로 새 후보에 추가 유지 시간이 생기지 않습니다. Python 입력 계약은 행 시각을 엄격히 증가시켰지만 C 함수는 HAL 재시도를 위해 동일 시각을 허용합니다. 두 API의 입력 차이를 문서에 명시합니다. 브라우저 파서를 그대로 복사해 동일 시각을 오류로 처리하면 다음 레슨의 실패 뒤 재시도 정책과 충돌합니다.
경계와 저장 상태를 함께 검사합니다
독립 test_button.c는 28개 조건을 검사합니다. 19ms에서는 NONE, 20ms에서는 PRESS, 유지에서는 NONE, 안정 해제에서는 RELEASE, 다음 눌림에서는 PRESS입니다. 단순히 이벤트 총수가 2인지 보는 대신 각 경계에서 반환값을 확인합니다. 두 번 잘못 발생하고 두 번 놓치는 구현도 총수만 같을 수 있기 때문입니다. 마지막 요약이 0 failures여야 하며 개별 FAIL 이름을 원인 분류에 사용합니다.
FAIL 19ms boundary라면 임계를 너무 짧게 적용하거나 후보 시간을 초기화하지 않은 경우를 봅니다. FAIL held라면 stable을 갱신하지 않거나 안정값과 후보값 비교를 빠뜨렸는지 확인합니다. FAIL wrap 20ms라면 signed 비교나 now가 작은 경우의 무조건 거부를 살펴봅니다. FAIL invalid unchanged는 입력 검사보다 필드 갱신을 먼저 했는지 점검합니다. 이름별로 의심할 조건이 다르다는 점을 익힙니다.
컴파일러가 unused parameter를 경고하면 필요한 시각이나 인자를 계산에서 빠뜨렸는지 확인합니다. -Werror 때문에 경고도 실패이며 기존 실행 파일을 직접 다시 실행한 결과로 새 소스가 통과했다고 판단하지 않습니다. Undefined symbols 또는 undefined reference가 button_update를 가리키면 Makefile에 button.c가 링크되는지 확인합니다. 헤더 선언이 보이는 것과 함수 정의가 실행 파일에 연결된 것은 별개입니다.
필터의 한계까지 제출합니다
button_update에는 sleep, busy-wait, 동적 메모리 할당이 없습니다. 호출 한 번은 현재 상태를 갱신하고 반환하며 시간이 흐를 때까지 내부에서 기다리지 않습니다. 이 구조는 뒤의 메인 루프에 넣기 위한 조건입니다. 인터럽트 처리 시간을 측정한 결과나 RTOS에서 동시 접근을 보호한 결과는 아닙니다. Button 객체는 이번 시험에서 단일 실행 흐름이 소유합니다. 다른 실행 흐름의 공유는 별도 동기화가 필요합니다.
fixture는 행 사이 레벨 유지 모델이므로 실제 파형을 연속 측정한 증거로 설명하지 않습니다. 후보가 시작된 뒤 관측 실패가 있었을 때 시간을 어떻게 인정할지 제품 정책은 추가 검토가 필요합니다. 이번 함수는 성공 관측만 받아 알려진 후보를 유지하며 관측되지 않은 변화를 만들어 내지 않습니다. 샘플링 간격과 실제 버튼 특성을 모르면 20ms만으로 모든 노이즈를 제거한다고 말할 수 없습니다.
제출물에는 수정한 button.c, make test 결과, demo에서 stable과 event가 다른 행 두 개, 부팅 눌림·시계 순환·유효하지 않은 레벨 정책 설명을 넣습니다. 새 시간표를 만들어 since_ms가 갱신된 순간과 stable이 바뀐 순간을 표시합니다. 다음 누적 미션에는 이 완성 필터를 그대로 가져가고 기록 모드만 확장합니다. 필터 규칙과 사용자의 기록 요청 정책을 한 함수에 뒤섞지 않은 것이 인계의 핵심입니다.
따라하기
starter의 기능 실패 확인
독립 C 디바운서 starter에서 실행하고 후보가 너무 빨리 확정되는 FAIL 이름을 확인합니다.
make test시간 차 조건 구현
button.c의 TODO에 stable과 candidate의 차이 및 uint32_t 경과 20ms 기준을 반영합니다.
make -s demo상태와 이벤트 관찰
solution에서 직접 실행한 결과입니다. stable=1이 계속 유지되어도 event=0인 행을 설명합니다.
./demo실행 결과
t=0 level=1 stable=0 event=0 t=3 level=0 stable=0 event=0 t=8 level=1 stable=0 event=0 t=27 level=1 stable=0 event=0 t=28 level=1 stable=1 event=1 t=100 level=1 stable=1 event=0 t=101 level=0 stable=1 event=0 t=104 level=1 stable=1 event=0 t=108 level=0 stable=1 event=0 t=128 level=0 stable=0 event=2 t=150 level=1 stable=0 event=0 t=170 level=1 stable=1 event=1
단위 경계 재검사
수정한 구현의 경계·순환·오류 보존 결과를 확인합니다.
make -s test실행 결과
button: 28 checks, 0 failures button HAL: 8 checks, 0 failures
확인 문제
실습
button.c의 즉시 확정 TODO를 20ms 안정 조건으로 고칩니다. button.h의 네 이벤트 상태와 부팅 눌림 정책을 유지합니다. 19/20ms·긴 누름·해제 튐·재눌림·uint32_t 순환·역행·잘못된 레벨 검사와 button_poll.c의 HAL 연결 8개 검사를 모두 통과하고 demo에서 stable과 event의 차이를 설명합니다.
실행 명령
make test
기대 결과
button: 28 checks, 0 failures 및 button HAL: 8 checks, 0 failures
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 버튼을 한 번 눌렀는데 여러 번 감지되는 이유를 설명해 주시면 됩니다.