Devin.KR

타이머 HAL과 시간 순환

120분 안팎

학습 목표

uint32_t 차이 계산으로 타이머 순환을 지나도 수집 주기를 유지합니다.

개념

타이머 HAL과 시간 순환의 계약을 구현합니다

32비트 밀리초 카운터는 최댓값 다음에 0으로 돌아갑니다. 기록기를 오래 켜 두었을 때 now가 작아졌다는 이유만으로 시간이 역행했다고 판단하면 수집이 멈춥니다. 이번 레슨은 uint32_t의 차이를 일정 계산에 사용합니다. 실습에서 순환 직전 값을 직접 주입하므로 장시간 기다리지 않아도 버그를 재현합니다. 벽시계 날짜와 단조 증가하는 경과 시간은 구분합니다.

events.h의 Timer는 anchor와 last를 가집니다. anchor는 마지막 예정 시각이며 last는 직전 poll 관찰 시각입니다. 초기화는 두 값을 같은 시작 시각으로 놓습니다. timer_poll은 새로운 시각을 받아 100ms 예정 이벤트를 생성합니다. hal_now_ms는 앞 모듈과 같이 fixture 시각을 읽기만 합니다. 시각을 자동으로 증가시키는 함수라고 생각하면 같은 값을 읽는 반복문에서 빠져나오지 못합니다.

차이 식은 (uint32_t)(now - anchor)입니다. unsigned 결과는 2의 32승을 법으로 돌아가므로 시작 4294967246에서 현재 50이면 100ms 차이입니다. 비교식 now가 anchor 이상이라는 조건을 먼저 넣으면 이 사례를 거부합니다. 덧셈으로 다음 마감을 만든 뒤 일반 대소 비교하는 구현도 같은 문제를 갖습니다. 차이를 구하는 두 피연산자는 선언부터 uint32_t로 유지합니다.

이 계산은 무제한의 시간을 복원하지 않습니다. 두 관찰 사이 실제 경과가 카운터 한 바퀴 이상이면 같은 숫자에서 여러 가능성이 생깁니다. 이번 poll 계약은 연속 호출 간격 0~1000ms이며 뒤로 이동한 입력과 큰 간격을 거부합니다. 실제 시간 역행과 큰 정방향 간격을 모두 정확히 구분한다는 주장은 하지 않습니다. 작은 전진 간격이 주입된다는 외부 전제가 필요합니다.

last 차이가 1000을 넘으면 -1을 반환하고 last와 anchor 및 큐를 보존합니다. 1000은 허용하고 1001은 거부합니다. 이 제한은 잘못된 fixture가 거대한 따라잡기 반복을 만들지 않도록 한 설계입니다. 장치에서 오랫동안 잠드는 동작을 추가하면 그 요구에 맞게 제한과 누락 정책을 다시 정해야 합니다. 무시하고 정상 반환하면 누락 사실을 상위 상태가 알 수 없습니다.

now가 350이고 anchor가 0이면 예정 100·200·300 세 개가 필요합니다. while 안에서 anchor에 정확히 100을 더하고 해당 예정 시각을 큐에 넘깁니다. anchor를 now로 바꾸면 다음 예정이 450으로 이동하여 기준선을 잃습니다. if 한 번만 실행하면 지난 예정 이벤트를 다음 poll로 미루게 됩니다. 따라잡기 정책의 이유와 최대 작업량을 함께 검토합니다.

정상 poll 한 번에 생기는 이벤트 수는 기존 예정에서 남은 99ms와 최대 관찰 간격 1000ms를 고려해도 최대 10개입니다. 큐의 용량은 8이므로 이 허용 범위에서도 손실이 생길 수 있습니다. 타이머는 큐가 거부하더라도 anchor를 전진시켜 같은 이벤트를 재발행하지 않습니다. 거부 건수는 큐에서 집계합니다. 생성 일정과 저장 성공을 혼동하지 않는 것이 핵심입니다.

순환 테스트는 anchor를 UINT32_MAX-49로 초기화합니다. now 49에서는 99ms이므로 이벤트가 없고 now 50에서 첫 이벤트가 생깁니다. 예정 값도 50으로 돌아갑니다. 테스트에 실제 경과 99와 100을 적으면 계산 의미가 분명해집니다. UINT32_MAX의 십진 표현을 손으로 다시 입력하는 대신 표준 헤더 상수를 쓰면 카운터 폭을 코드에서 확인할 수 있습니다.

컴파일러 경고는 테스트 실패와 다른 층입니다. 비교에서 부호가 섞였다는 진단이 보이면 임시로 캐스트를 늘리기 전에 선언을 점검합니다. -Werror는 경고도 빌드 실패로 취급합니다. 함수 선언과 정의가 다르면 events.h와 events.c의 매개변수를 맞춥니다. 링크의 undefined symbol은 시간 계산이 틀렸다는 뜻이 아니라 해당 정의가 빌드 명령에 포함되지 않았다는 단서입니다.

경과 시간 계산 뒤에 signed int로 바꾸면 큰 값의 해석이 달라질 수 있습니다. 이번 구현은 signed 차이 판정을 필요로 하지 않습니다. timer_poll의 간격을 uint32_t로 보관한 뒤 작은 상수 1000u와 비교합니다. 짧은 시간만 측정한다는 조건을 문서화한 unsigned 차이 계약이므로 모든 wrap 코드를 같은 식으로 대체할 수 있다고 일반화하지 않습니다.

실습 starter는 now가 anchor 이상인지 먼저 확인합니다. 일반 구간과 일부 경계는 통과하지만 순환 테스트가 실패합니다. 이 실패는 테스트가 실행되었다는 증거이며 컴파일 환경 오류와 다릅니다. FAIL line 뒤의 조건을 읽어 50ms에서 q.count가 왜 0인지 살펴봅니다. 기대값을 0으로 바꾸면 요구를 제거할 뿐입니다. 비교를 차이 기준으로 고친 뒤 같은 테스트를 다시 실행합니다.

반복해서 같은 now를 주면 추가 이벤트가 생기지 않아야 합니다. 이미 anchor가 최신 예정까지 전진했으므로 차이가 100 미만입니다. 타이머 초기화 함수를 매 poll마다 호출하면 anchor도 현재로 이동하여 이벤트가 영원히 생기지 않습니다. 초기화는 기록기 부팅 시 한 번 하고 모드 토글과 별개로 유지합니다. 정지 상태의 이벤트를 처리하는 정책은 소비 경로에서 다룹니다.

예정 시각을 지난 뒤 만들어진 이벤트는 과거 센서 값을 보유하지 않습니다. timer_poll은 시각 정보만 생성합니다. 현재 raw를 세 번 저장하면 같은 시점의 값을 세 번 복사한 것이며 과거 100·200·300ms 측정을 복원한 것이 아닙니다. 실제 제품은 누락 집계나 DMA 버퍼 등 별도 정책이 필요합니다. 이번 모델에서 샘플의 timestamp는 실제 소비 시각으로 남깁니다.

완료 증거에는 99/100ms, 350ms 따라잡기, 순환 49/50ms, 뒤로 한 칸, 간격 1001ms의 결과를 적습니다. 정상뿐 아니라 거부 후 상태가 보존되는지도 확인합니다. 부팅 기준선과 last의 역할을 따로 설명하면 다음 통신 모듈에서도 시간 초과 계약을 재사용할 수 있습니다. 레지스터 분주 계산은 서재 장에서 확장하고 여기서는 시각 폭과 일정 유지 조건을 검증합니다.

따라하기

새 테스트의 실패식 확인

starter 압축을 풀어 Makefile 위치에서 실행합니다. FAIL line 뒤의 조건을 읽고 events.c의 TODO를 수정합니다. 이 단계는 실패 지점을 관찰하므로 환경별 출력은 직접 기록합니다.

make -s events-test

완성 경로 재현

solution 압축의 독립 폴더에서 새 검사를 실행합니다. 테스트 소스의 조건과 출력 건수를 함께 확인합니다.

make -s events-test

실행 결과

timer-events: 91 cases, 0 failures

누적 회귀와 제출 근거

수정한 starter에서 전체 회귀를 실행합니다. README의 관찰 목록 중 이 레슨 경계 사례를 적고 정상 종료 코드를 기록합니다.

make test

확인 문제

실습

events.c의 timer_poll 비교를 고쳐 순환에서도 100ms 예정선을 유지합니다. 다른 완성 함수와 test_events.c를 보존합니다.

시작 코드·테스트 내려받기

실행 명령

make test

기대 결과

이전 회귀 및 timer-events 검사 0 failures, 종료 코드 0

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • uint32_t 카운터 순환을 처리할 때 어떤 시간 간격 전제가 필요한가요?