Interrupts & NVIC, SysTick Timer

Seungyun Lee·2026년 2월 11일

RTOS

목록 보기
5/14

ISR = Interrup Handler (Interrupt Service Routine)

ISR (Interrupt Service Routine)은 쉽게 말해 "인터럽트가 발생했을 때 CPU가 하던 일을 멈추고 부리나케 달려가서 실행하는 특수한 C언어 함수"입니다. 다른 말로 인터럽트 핸들러(Interrupt Handler)라고도 부릅니다.

앞서 우리가 배운 NVIC이 인터럽트의 순서를 정해주는 '교통경찰'이라면, ISR은 경찰의 호출을 받고 현장에 직접 출동해서 문제를 해결하는 '구급대원(실제 실행되는 코드)'이라고 볼 수 있습니다.

1. Interrupts

NVIC(Nested Vectored Interrupt Controller) is a ARM Cortex-M processor
ARM Cortex-M 프로세서에서 인터럽트는 시스템의 흐름을 바꾸는 예외(Exception)의 일종입니다.

예외(Exception)의 종류:

  • Reset: 시스템 전원이 켜지거나 리셋 버튼이 눌렸을 때 발생하는 가장 높은 우선순위의 예외입니다.
  • Software Interrupts: 프로그램 코드 내에서 특정 명령(예: SVC)을 통해 의도적으로 발생시키는 인터럽트입니다. 주로 OS의 시스템 호출(System Call)에 사용됩니다.
  • Hardware Interrupts: 주변 장치(타이머, GPIO, UART 등)가 CPU의 처리를 요청할 때 보냅니다.

벡터 테이블 (Vector Table):

  • 각 예외는 고유한 번호를 가지며, 메모리의 특정 위치에 32비트 주소(Vector)가 저장되어 있습니다.
  • 이 주소는 해당 예외를 처리할 ISR(Interrupt Service Routine)의 시작 주소를 가리킵니다.
  • 벡터 테이블은 메모리의 가장 앞부분(주로 0x00000000 번지)인 ROM 영역에 저장됩니다. (0번지는 초기 스택 포인터, 4번지는 리셋 핸들러 주소입니다.)

2. NVIC (Nested Vectored Interrupt Controller):

Nested Vectored Interrupt Controller(중첩 벡터 인터럽트 컨트롤러)의 약자로, ARM Cortex-M 프로세서(지금 다루고 계신 TM4C123 같은 MCU) 내부에 찰싹 달라붙어 있는 *"인터럽트 전담 교통경찰"입니다.

CPU가 연산에 집중할 수 있도록, 외부 센서나 타이머 등에서 쏟아지는 수십 개의 인터럽트 요청을 대신 받아주고 순서를 정해주는 아주 똑똑한 하드웨어 장치입니다.

1. Nested (중첩)

새치기(Preemption) 허용: 우선순위(Priority) 개념이 적용되어 있습니다. 낮은 우선순위의 인터럽트(예: 버튼 입력)를 처리하고 있는데, 갑자기 매우 중요한 높은 우선순위의 인터럽트(예: 시스템 치명적 오류 또는 고속 통신)가 발생하면, 하던 일을 멈추고 높은 우선순위의 작업을 먼저 처리(중첩)합니다.

처리가 끝나면 다시 원래 하던 낮은 우선순위의 작업으로 돌아갑니다.

2. Vectored (벡터 기반)

주소록(Vector Table) 보유: 일반적인 구형 CPU는 인터럽트가 발생하면 먼저 "누가 날 불렀지?" 하고 소프트웨어적으로 찾아야 했습니다.

하지만 NVIC은 벡터 테이블이라는 메모리 주소록을 가지고 있어서, 인터럽트 번호만 들어오면 그 즉시 실행해야 할 함수(ISR, Interrupt Service Routine)의 시작 주소로 CPU를 바로 점프(Vectoring)시킵니다. 덕분에 반응 속도(Latency)가 엄청나게 빠릅니다.

3. Interrupt Controller (제어기)

레지스터를 통한 섬세한 관리: 수많은 하드웨어 핀이나 타이머를 켜고 끌 수 있습니다.

Enable/Disable: 특정 인터럽트를 활성화하거나 무시합니다.

Pending: 인터럽트가 발생했지만 더 높은 우선순위 작업 때문에 기다리고 있는 '대기 상태'를 관리합니다.

NVIC 레지스터 (NVIC Registers):

이 표는 앞서 언급하신 내용 중 Priority Registers (IP, 우선순위 설정)를 보여줍니다

  • Enable Registers (ISER/ICER): 인터럽트를 활성화(Set-Enable)하거나 비활성화(Clear-Enable)하는 레지스터입니다. 비트를 1로 설정하면 인터럽트가 켜집니다.

이 표는 위에서 설명한 ISER(켜는 스위치)를 TM4C123 보드에서 어떻게 맵핑해두었는지 보여줍니다. 코드에서는 NVIC_EN0_R, NVIC_EN1_R이라는 이름으로 씁니다.

  • Pending Registers (ISPR/ICPR): 인터럽트가 발생했지만 아직 처리되지 않은 상태(Pending)를 설정하거나 해제합니다.
  • Priority Registers (IP): 각 인터럽트의 우선순위 레벨을 설정합니다.
ISER (Interrupt Set-Enable Register): 인터럽트를 켜는(Enable) 레지스터입니다. 원하는 자리에 1을 쓰면 켜집니다 (0을 쓰면 아무 일도 안 일어남).

ICER (Interrupt Clear-Enable Register): 인터럽트를 끄는(Disable) 레지스터입니다. 끄고 싶은 자리에 1을 쓰면 꺼집니다.

ISPR (Interrupt Set-Pending Register): 원래 인터럽트는 하드웨어가 신호를 줘야 발생하지만, 소프트웨어적으로 **강제로 인터럽트를 발생(대기 상태로 만듦)**시킬 때 여기에 1을 씁니다.

ICPR (Interrupt Clear-Pending Register): 인터럽트가 발생해서 대기 중(Pending)인 상태를 강제로 취소할 때 여기에 1을 씁니다.

3. Interrupt Handling

인터럽트가 발생하고 처리되는 과정은 하드웨어적으로 매우 정교하게 최적화되어 있습니다.

진입(Entry) 및 스택킹(Stacking):

  • 인터럽트가 발생하면 CPU는 현재 실행 중인 상태를 저장하기 위해 8개의 레지스터(xPSR, PC, LR,R12, R3, R2, R1, R0)를 스택에 자동으로 푸시(Push)합니다.
  • 이는 ISR이 실행되면서 기존 레지스터 값을 덮어쓰는 것을 방지하기 위함입니다.
  • 일반 함수와 달리, ISR에서 복귀할 때는 BX LR 명령어를 사용하지만, 이때 LR 값은 복귀 주소가 아닌 특별한 값(EXC_RETURN)을 가집니다.
  • 0xFFFFFFF9: 스레드 모드(Thread Mode)로 복귀하며, 메인 스택 포인터(MSP)를 사용합니다. (일반적인 경우)
  • 0xFFFFFFF1: 핸들러 모드(Handler Mode)로 복귀하며, MSP를 사용합니다. (인터럽트가 중첩되어 다른 ISR로 돌아갈 때)
  • 0xFFFFFFFD: 스레드 모드로 복귀하며, 프로세스 스택 포인터(PSP)를 사용합니다. (주로 OS 환경)
  • 이 값에 따라 CPU는 스택에서 8개의 레지스터를 팝(Pop)하여 이전 상태를 복구하고, IPSR(Interrupt Program Status Register)을 초기화하여 인터럽트 처리 상태를 종료합니다.

고급 처리 기능:

  1. 중첩 인터럽트 (Nested Interrupt): 우선순위가 낮은 ISR을 실행 중에 우선순위가 더 높은 인터럽트가 발생하면, 현재 ISR을 잠시 멈추고 높은 우선순위의 ISR을 먼저 실행합니다. (선점, Preemption)

  2. 테일 체이닝 (Tail Chaining): 하나의 ISR이 끝나는 시점에 이미 대기 중인(Pending) 다른 인터럽트가 있다면, 레지스터를 복구(Unstacking)했다가 다시 저장(Stacking)하는 낭비를 하지 않습니다. 즉시 다음 ISR로 진입하여 처리 속도를 최적화합니다.

  3. 지연 도착 (Late Arrival): 낮은 우선순위 인터럽트를 처리하기 위해 레지스터를 스택에 저장하는 도중, 높은 우선순위 인터럽트가 발생하면, 하드웨어는 즉시 높은 우선순위 ISR로 목표를 바꿔 실행합니다. (낮은 우선순위는 나중에 처리됨)

3. SysTick Timer

  • Cortex-M 프로세서에 내장된 24비트 다운 카운터 타이머입니다.
  • 용도: 주기적인 인터럽트를 발생시켜 RTOS의 스케줄러를 동작시키거나(Time Slicing), 정확한 시간 지연(Delay)을 만드는 데 사용됩니다.
  • 주요 레지스터:
    STCSR (Control and Status): 타이머 활성화 및 인터럽트 설정.
    STRVR (Reload Value): 타이머가 0이 되었을 때 다시 로드될 초기값 설정.
    STCVR (Current Value): 현재 카운터 값.

4. Critical Sections & Concurrency

멀티스레딩이나 인터럽트 환경에서는 여러 실행 흐름(스레드 또는 ISR)이 공유 자원(전역 변수, I/O 포트 등)에 접근할 때 문제가 발생할 수 있습니다.

  • 경쟁 상태 (Race Condition):
    두 개 이상의 스레드가 공유 자원을 동시에 수정하려고 할 때, 실행 순서(Timing)에 따라 결과가 달라지거나 잘못된 값이 저장되는 현상입니다.
    이러한 문제가 발생할 수 있는 코드 영역을 임계 영역(Critical Section)이라고 합니다.

사례 1: 포트 초기화 문제 (P4DIR 예시)

제공해주신 예시는 '비친화적(Unfriendly)' 코드로 인해 발생하는 문제입니다. P4DIR 레지스터가 공유 자원입니다.

  • Thread-1: P4DIR = 0x0F;를 실행하려고 함.
    내부 동작: P4DIR 값을 읽고, 하위 4비트를 1로 바꾼 뒤, 다시 씀.

  • 문제 상황: Thread-1이 값을 쓰기 직전에 Thread-2로 문맥 전환(Context Switch)이 발생했다고 가정합시다.

  • Thread-2: P4DIR = 0x70;을 실행.
    Thread-2는 아직 Thread-1이 값을 쓰지 않았으므로 초기값을 읽어서 비트 6-4를 1로 설정하고 저장합니다. (P4DIR은 이제 0x70)

  • Thread-1 복귀: Thread-1은 멈췄던 지점부터 실행합니다. 아까 계산해둔 값(0x0F)을 P4DIR에 덮어씁니다.

  • 결과: P4DIR은 0x0F가 되어버립니다. Thread-2가 설정한 0x70 설정은 사라집니다.

  • 해결책: P4DIR |= 0x0F;와 같이 비트 단위 연산을 사용하거나, 해당 연산 중에 인터럽트를 비활성화하여 원자성(Atomicity)을 보장해야 합니다.

사례 2: 전역 변수 문제 (LD-ADD-ST)

Count++와 같은 간단한 C 코드도 어셈블리어로는 3단계로 나뉩니다.

LD (Load): 메모리에서 변수 값을 레지스터(R0)로 읽어옴.
ADD: 레지스터 값을 1 증가시킴.
ST (Store): 증가된 값을 다시 메모리에 저장함.

문제 상황:
Thread A가 LD를 수행하여 Count값(예: 10)을 가져온 직후, 인터럽트가 발생하여 Thread B가 실행됩니다.
Thread B도 Count를 읽습니다. 아직 A가 저장을 안 했으므로 여전히 10입니다.
Thread B가 Count를 11로 증가시키고 저장합니다.
다시 Thread A로 돌아옵니다. A는 아까 읽은 10을 가지고 11로 만든 뒤 저장합니다.
결과: 두 번 증가했으므로 12가 되어야 하는데, 11이 됩니다. 데이터 불일치가 발생합니다.

해결책:
데이터를 수정하는 동안에는 인터럽트를 끄거나(Disable Interrupts), 세마포어(Semaphore) 같은 동기화 도구를 사용하여 한 번에 하나의 스레드만 접근하도록 해야 합니다.

profile
RTL, FPGA Engineer

0개의 댓글