전자회로 실무 / 펌웨어

인터럽트(interrupt)에서는 발생 사실만 기록하고, 오래 걸리는 처리는 메인 루프(main loop)로 넘깁니다

인터럽트(interrupt) 안에서 기다리거나 많은 데이터를 출력하면 다른 작업이 늦어질 수 있습니다. 인터럽트에서는 필요한 정보만 저장하고, 시간이 걸리는 처리는 메인 루프(main loop)에서 이어서 하도록 나눕니다.

먼저 알아둘 용어

ISR(Interrupt Service Routine)·메인 루프(main loop) — ISR은 인터럽트가 발생할 때 실행되는 처리 함수입니다. 메인 루프는 입력 확인과 출력 갱신 등을 반복하는 프로그램(program)의 기본 실행 부분입니다. ISR을 짧게 끝내면 다른 작업이 오래 지연되는 것을 줄일 수 있습니다.

이벤트(event)·플래그(flag)·큐(queue) — 이벤트는 버튼(button) 입력처럼 프로그램에서 처리해야 하는 변화나 동작입니다. 플래그는 이벤트가 있었다는 참·거짓 표시입니다. 큐는 여러 이벤트를 순서대로 담는 저장 공간입니다. 이벤트의 수와 순서가 필요하면 플래그 하나로 대체할 수 없습니다.

원자성·임계 구역·volatile — 원자성은 다른 실행 흐름이 중간 상태를 끼어들어 보지 못하게 하는 성질입니다. 임계 구역은 공유 상태를 짧게 보호하는 구간입니다. volatile 선언만으로 여러 단계의 처리나 다중 코어(core) 동기화가 원자적으로 되지는 않습니다.

RTOS(Real-Time Operating System) — 실시간 운영체제로, 작업과 인터럽트 사이 전달을 위한 큐·알림 기능 등을 제공합니다. ISR에서는 그 환경이 허용한 전용 기능을 사용해야 합니다.

인터럽트가 이벤트를 큐에 넣고 메인 루프가 나중에 꺼내 처리하는 순서입니다. 재생 속도와 움직임의 크기는 설명을 위한 것입니다.
정지 그림 확대
인터럽트가 이벤트를 큐에 넣고 메인 루프가 나중에 꺼내 처리하는 순서입니다. 재생 속도와 움직임의 크기는 설명을 위한 것입니다. · 그림을 클릭하면 확대됩니다.

요청을 받아 처리한 뒤 원래 프로그램으로 돌아옵니다

폴링(polling)은 프로그램이 입력 상태를 반복해서 읽는 방식입니다. 인터럽트에서는 입력의 변화나 주변회로의 사건이 처리 요청을 만들고, CPU가 허용 조건과 우선순위에 따라 그 요청을 받아 ISR을 실행합니다. 버튼 핀을 입력으로 설정하는 것만으로 인터럽트까지 자동으로 켜지는 것은 아닙니다. 감지할 에지와 요청 경로, 인터럽트 허용 설정을 확인해야 합니다.

예를 들어 Cortex-M0+에서는 인터럽트 진입 때 실행을 재개하는 데 필요한 상태를 스택(stack)에 저장합니다. 스택은 실행 중 잠시 보관할 정보를 정해진 순서로 쌓고 꺼내는 메모리 영역입니다. 기본 하드웨어 저장 묶음은 R0–R3, R12, LR, PC, xPSR의 8워드입니다. 레지스터(register)는 CPU가 계산이나 실행 상태를 보관하는 작은 저장 공간이며, 이 예시의 한 워드는 32비트입니다. 저장된 PC(Program Counter)는 재개할 명령 위치, LR(Link Register)은 복귀 정보를 담습니다. xPSR은 프로그램 상태 레지스터(Program Status Register)의 여러 상태 정보를 모은 값입니다. 다른 CPU에도 같은 묶음이 그대로 적용되는 것은 아닙니다.

ISR에서는 장치가 요구하는 방식으로 사건을 처리하고 원인 플래그를 해제합니다. 이후 예외 복귀(exception return) 과정이 저장된 상태를 복원해 원래 프로그램을 이어 갑니다. 주변장치의 사건 플래그와 메인 루프에 전달할 소프트웨어 플래그는 역할이 다릅니다. 전자를 지웠다고 후자에 담아야 할 사건 수나 데이터까지 없어져도 되는 것은 아닙니다.

이 과정은 잠깐 끼어들어 실행하는 처리입니다. ISR에서 오래 기다리면 저장된 원래 프로그램도 그만큼 늦게 재개됩니다. 기계식 버튼의 접점 바운스는 여러 요청을 만들 수 있으므로, 인터럽트를 쓴다고 해서 한 번 누르는 동작이 한 번의 사건이 되는 것은 아닙니다. 먼저 무엇을 보존할지 정한 뒤 전달 방식을 선택합니다.

STM32G071 예시에서는 EXTI와 NVIC를 나누어 봅니다

풀업된 active-low 버튼을 누르면 입력이 낮아질 수 있습니다. 이때 확인할 흐름은 GPIO 입력 에지 → EXTI 검출과 요청 → NVIC의 허용·대기 상태 → CPU 예외 진입입니다. 폴링은 입력 레벨을 반복해서 읽는 방식이고, 인터럽트는 설정한 에지가 요청 경로를 통해 처리를 요구하는 방식입니다. GPIO 입력만 설정했다고 전체 경로가 켜지는 것은 아닙니다.

ST의 STM32G0 HAL GPIO 설명은 에지·외부 인터럽트 모드 설정과 해당 NVIC IRQ의 우선순위·허용 설정을 구별합니다. 같은 고정 커밋의 EXTI 설정 구현에서는 GPIO port 선택, 상승·하강 trigger와 interrupt mask를 따로 다룹니다. 실제 핀·IRQ 매핑과 초기화 순서는 사용하는 칩과 보드 설정으로 확인합니다.

EXTI 처리기는 상승·하강 pending을 각각 확인하고 해당 pending을 해제한 뒤 등록된 callback을 호출합니다. 주변장치 pending, NVIC의 대기 상태, 애플리케이션 이벤트 플래그는 같은 것이 아닙니다. 필요한 사건을 아래의 큐·카운터 등에 안전하게 남기는 절차가 여전히 필요합니다.

입력이 한 번 낮아진다는 설명 도식은 기계식 버튼의 바운스 시험이 아닙니다. 하강 에지를 택했다 해도 한 번 누를 때 요청이 한 번만 발생한다고 보장하지 않습니다. 위의 Cortex-M0+ 상태 저장·복귀 설명과 함께 보되, 실제 debounce·응답 지연·우선순위는 보드에서 별도로 검증합니다. 이 절에서는 제조사 소스를 대조했으며 실제 STM32G071 실행 시험은 하지 않았습니다.

먼저 보존해야 할 이벤트의 정보를 정합니다

버튼을 누른 뒤 1초 동안 표시를 바꾸려는 경우, ISR에서 1초를 기다리는 대신 이벤트 표시를 남기고 바로 돌아옵니다. 메인 루프에서 표시를 시작하고, 경과 시간이 끝나면 표시를 종료합니다. 하지만 참·거짓 플래그 하나는 처리 전에 두 번 발생한 이벤트를 하나로 합친다는 한계가 있습니다.

ISR이 필요한 최소 정보만 저장하고 메인 루프가 안전하게 가져간 뒤 실제 작업을 수행하도록 구성합니다. “한 번 이상 발생했습니다”만 필요할 때는 플래그를 사용할 수 있습니다. 모든 펄스 수가 필요하면 카운터, 순서나 개별 데이터가 필요하면 큐를 검토합니다. 저장 공간을 초과했을 때 버림·오류 표시·누적 횟수 기록 중 무엇을 할지도 정합니다.

AVR 같은 단일 코어 MCU(Microcontroller Unit)에서는 플래그를 읽고 지우는 사이에 ISR이 다시 실행되는 경합을 고려합니다. 기존 인터럽트 상태를 보존하면서 짧은 임계 구역 안에서 플래그를 읽고 지웁니다. 오래 걸리는 작업은 그 구역을 벗어난 뒤 수행합니다. 공유 변수에 붙이는 volatile은 컴파일러가 변수 접근을 임의로 생략하거나 이전 값으로 대체하지 않도록 지정하는 데 쓰입니다. 그러나 여러 단계의 작업이 중간 개입 없이 끝나는 원자성(atomicity)이나 여러 코어 사이의 동기화까지 보장하지는 않습니다. RTOS·다중 코어에서는 해당 환경의 ISR용 큐·알림 등 허용된 수단을 따릅니다.

ISR 안에서는 지연 함수, 시간 함수, 출력 함수가 메인 루프에서 호출할 때와 같은 방식으로 동작한다고 가정하지 않습니다. 다른 인터럽트에 의존하는 함수는 대기에서 빠져나오지 못할 수 있습니다. 사용하는 플랫폼에서 허용하는 기능과 중첩·우선순위 규칙을 확인하고, 긴 작업은 공유 상태를 보호하는 구간 밖에서 수행합니다.

확인 시험은 단일 이벤트, 빠른 연속 이벤트, 메인 루프가 바쁜 동안의 이벤트로 나누어 진행합니다. 플래그를 쓴다면 이벤트가 합쳐지는 것이 의도에 맞는지, 카운터·큐를 쓴다면 손실과 넘침을 알아낼 수 있는지 확인합니다. ISR에서 빠져나왔다고 해서 필요한 일을 모두 처리한 것은 아닙니다.

작업 순서를 다시 확인합니다

인터럽트에서는 발생 사실만 기록하고, 오래 걸리는 처리는 메인 루프로 넘깁니다 — 설명용 도해
인터럽트에서는 발생 사실만 기록하고, 오래 걸리는 처리는 메인 루프로 넘깁니다 — 설명용 도해 · 클릭하면 확대됩니다.

printf를 넣었을 때만 멈춤이 사라진다면

로그를 넣자 ISR 플래그 대기가 끝났다는 관측만으로 원인을 확정하지 않습니다. 먼저 같은 소스의 최적화 옵션별 생성 어셈블리에서 플래그 재적재가 어떻게 달라졌는지 비교합니다. 호출은 실행 시간·코드 배치도 바꾸므로 타이밍 문제나 다른 결함이 가려졌을 수 있습니다. printf를 언제나 컴파일러 배리어로 취급하거나 로그 추가 자체를 수정 완료로 기록하지 않습니다. GCC volatile 설명은 비-volatile 메모리와의 순서까지 보장하지 않는다고 명시합니다.

플래그의 read-and-clear 사이에 새 이벤트가 들어오는지, 여러 발생을 하나로 합쳐도 되는지, 페이로드가 먼저 준비되어야 하는지를 별도로 검토합니다. Zephyr에서는 atomic API나 ISR에서 허용되는 인계 수단을 고릅니다. k_sem_give()도 최대 count에 도달하면 더 늘어나지 않으므로 무제한 이벤트 보존 수단은 아닙니다. DMA 캐시 소유권이나 다중 필드 전달을 volatile 하나로 해결하지 않습니다.

저장된 Harshith Kolur의 사례는 질문의 출처입니다. 해당 사례의 전체 소스·빌드 옵션·실제 어셈블리가 없어 버그 원인을 재현한 결과로 쓰지 않습니다.

English summary. Logging can alter code generation and timing. Compare generated accesses before diagnosing the cause. Volatile does not supply atomic read-and-clear, payload ordering or DMA cache ownership, and a bounded semaphore can still merge excess events.