Devin.KR

짧은 인터럽트와 이벤트 큐

130분 안팎

학습 목표

가상 ISR은 시각과 이벤트만 큐에 넣고 측정·출력은 루프에서 수행합니다.

개념

짧은 인터럽트와 이벤트 큐의 계약을 구현합니다

인터럽트에서 센서 읽기와 출력까지 수행하면 그동안 다른 이벤트 처리에 영향을 줄 수 있습니다. 이번 목표는 센서 기록기의 무거운 작업을 메인 소비 경로로 옮기는 것입니다. virtual_isr는 예정 시각 하나를 큐에 넣습니다. 타이머가 준비한 시각을 전달받는 함수이며 실제 CPU 예외 진입이나 하드웨어 플래그 처리를 모사하지 않습니다. 함수 호출 경계를 통해 역할을 분리합니다.

큐는 uint32_t due 배열 8개, head, count, dropped로 구성됩니다. head는 다음에 꺼낼 위치이고 count는 유효 항목 수입니다. 삽입 위치는 head와 count의 합을 8로 나눈 나머지입니다. pop은 head를 한 칸 옮기고 count를 줄입니다. 이 구성은 빈 상태와 가득 찬 상태를 count로 구분하므로 배열 한 칸을 비워 두지 않고 8개 전부 사용합니다.

FIFO는 먼저 넣은 항목부터 꺼내는 순서입니다. 예정 100·200·300을 넣었다면 소비도 같은 순서여야 합니다. 잘못된 인덱스 계산은 첫 바퀴에서는 정상으로 보이다가 head가 끝을 지나갈 때 드러납니다. 테스트는 0~7을 넣고 모두 꺼낸 뒤 8~15를 다시 넣어 순서를 확인합니다. 같은 배열 칸의 재사용을 검사해야 정적인 첫 삽입 성공에 그치지 않습니다.

가득 찼을 때 정책은 새 이벤트를 버리고 dropped를 증가시키는 것입니다. 기존 8개의 예정 시각과 head·count는 그대로 유지합니다. 덮어쓰기 정책도 가능하지만 이 프로젝트에서는 오래 기다린 작업의 순서를 보존합니다. 버린 시각은 다음 poll에서 다시 생성하지 않습니다. 이 선택은 손실을 숨기지 않으면서 ISR이 소비를 기다리는 상황을 피합니다.

빈 pop은 0을 반환하고 호출자가 준 due 값을 바꾸지 않습니다. 데이터 시각 0도 순환 이후 정상 값일 수 있으므로 반환 상태와 due를 분리합니다. 시각이 0이면 빈 큐라고 처리하면 순환 테스트가 틀립니다. push 성공도 1이고 거부는 0입니다. 오류 출력은 소비 이후 상위 루프가 맡으며 큐 함수 자체는 문자열을 출력하지 않습니다.

virtual_isr가 호출할 수 있는 것은 events_push뿐입니다. 센서 raw 유효성 판정, buffer_push, printf, 파일 접근, sleep, 동적 할당은 이 경로에 넣지 않습니다. 큐가 가득 차도 즉시 반환하고 다음 이벤트를 기다리지 않습니다. 이 경로의 작업량은 배열 한 칸 복사와 계수 갱신으로 제한됩니다. 짧다는 설명은 실제 MCU에서 몇 마이크로초인지 측정했다는 의미가 아닙니다.

순차 실행 PC 모델에서는 ISR 함수와 소비 함수가 동시에 실행되지 않습니다. 실제 인터럽트가 pop 도중 count 변경에 끼어들면 공유 데이터가 손상될 수 있습니다. volatile만 붙여도 복합 갱신이 원자적이 되는 것은 아닙니다. 보드 이식 때는 원자 연산의 지원 범위나 짧은 임계 구역을 검토해야 합니다. 이번 모듈의 테스트 통과를 동시성 안전성 증명으로 제출하지 않습니다.

timed_consume은 이벤트를 한 개 꺼낸 다음 현재 모드를 확인합니다. 정지 상태이면 버리고 저장하지 않습니다. 유효하지 않은 raw와 가득 찬 버퍼도 이벤트는 소비하되 새 샘플은 남기지 않습니다. 기존 기록은 그대로 유지합니다. 이전 module의 controller_step은 회귀 검사용으로 보존하지만 새 경로와 함께 호출하면 호출당 저장과 주기 저장이 섞여 건수가 늘어납니다.

샘플 시각은 실제 hal_now_ms를 사용합니다. 예정 1100 이벤트를 1125에 소비하면 샘플 timestamp는 1125입니다. 예정 시각은 큐 안에서 일정 판단에 쓰이고 필요하면 상위 로그에 함께 남깁니다. 1100으로 저장하면 과거에 측정한 것처럼 보입니다. 이 레슨은 시각 두 종류의 의미를 분리하며 과거 센서 값 보관 기능을 추가하지 않습니다.

버튼 처리는 이벤트 유무와 관계없이 메인 반복마다 수행합니다. 100ms 수집 이벤트가 없다고 버튼 폴링을 생략하면 20ms 안정 판단도 수집 속도에 묶입니다. timed_button이 먼저 모드를 확정하고 다음에 타이머 및 소비 경로를 수행하는 순서를 사용합니다. 정지 확정된 같은 반복에서 이미 큐에 있던 이벤트도 저장하지 않습니다. 사용자 상태와 일정 순서를 README에서 명시합니다.

지연된 이벤트는 소비 시점의 모드로 판단한다는 정책을 채택했습니다. 정지 때 만들어졌으나 소비 전에 시작되면 저장될 수 있습니다. 원래 발생 시점의 모드가 필요하다면 이벤트에 모드 세대 번호를 담거나 전환 때 큐를 정리해야 합니다. 현재 실습은 그 보장을 요구하지 않습니다. 요구하지 않은 성질을 된다고 주장하는 대신 정책이 필요한 사례를 설명하는 것이 리뷰의 일부입니다.

테스트는 ISR 호출 직후 Buffer 길이가 변하지 않는지 확인합니다. 그 뒤 메인 소비를 호출하면 새 샘플이 한 개 생깁니다. 이 비교가 성공하면 측정·저장을 다른 경로로 옮긴 구조를 보여 줍니다. 모든 데이터가 사라진다면 raw 범위, recording 값, 큐 상태, Buffer 길이를 순서대로 확인합니다. 큐 count가 줄었는데 length가 그대로라면 저장 조건에서 생략한 경우를 먼저 살펴봅니다.

starter는 full에서 반환하지만 손실 계수를 올리지 않습니다. FAIL 문구에서 q.dropped가 기대와 다른 조건을 찾아 수정합니다. 테스트가 단순 종료 코드만 실패한다면 앞쪽 출력에서 실제 실패식을 찾습니다. 컴파일이 되지 않는다면 큐 로직이 아니라 함수 선언이나 헤더 포함 문제부터 고칩니다. 계수는 uint32_t이므로 장기 운용에서는 계수 순환 전 보고 또는 더 넓은 통계 정책도 검토합니다.

완성한 뒤 큐 8개 성공·9번째 거부·FIFO 순서·재사용·빈 pop 출력 보존을 근거로 남깁니다. 용량 8이면 몇 ms 지연을 허용하는지는 생산 속도와 소비 속도에 따라 달라집니다. 100ms마다 한 이벤트라는 가정에서도 800ms를 모든 상황의 안전 기준으로 단정하지 않습니다. 타이머 따라잡기는 한 poll에서 여러 개를 생성할 수 있습니다. 출력 비용 관찰과 실물 파형 측정은 더 읽기에서 확장합니다.

따라하기

새 테스트의 실패식 확인

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의 full 경로에서 손실을 계수합니다. ISR은 큐 삽입만 수행하고 소비는 메인에서 합니다. 실제 동시성 검증으로 주장하지 않습니다.

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

실행 명령

make test

기대 결과

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

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

더 읽기

면접 질문

  • 인터럽트에서 출력과 센서 처리를 분리하는 이유와 큐가 가득 찼을 때의 정책을 설명해 주세요.