ISR (Interrupt Service Routine)은 쉽게 말해 "인터럽트가 발생했을 때 CPU가 하던 일을 멈추고 부리나케 달려가서 실행하는 특수한 C언어 함수"입니다. 다른 말로 인터럽트 핸들러(Interrupt Handler)라고도 부릅니다.
앞서 우리가 배운 NVIC이 인터럽트의 순서를 정해주는 '교통경찰'이라면, ISR은 경찰의 호출을 받고 현장에 직접 출동해서 문제를 해결하는 '구급대원(실제 실행되는 코드)'이라고 볼 수 있습니다.
NVIC(Nested Vectored Interrupt Controller) is a ARM Cortex-M processor
ARM Cortex-M 프로세서에서 인터럽트는 시스템의 흐름을 바꾸는 예외(Exception)의 일종입니다.
Nested Vectored Interrupt Controller(중첩 벡터 인터럽트 컨트롤러)의 약자로, ARM Cortex-M 프로세서(지금 다루고 계신 TM4C123 같은 MCU) 내부에 찰싹 달라붙어 있는 *"인터럽트 전담 교통경찰"입니다.
CPU가 연산에 집중할 수 있도록, 외부 센서나 타이머 등에서 쏟아지는 수십 개의 인터럽트 요청을 대신 받아주고 순서를 정해주는 아주 똑똑한 하드웨어 장치입니다.
새치기(Preemption) 허용: 우선순위(Priority) 개념이 적용되어 있습니다. 낮은 우선순위의 인터럽트(예: 버튼 입력)를 처리하고 있는데, 갑자기 매우 중요한 높은 우선순위의 인터럽트(예: 시스템 치명적 오류 또는 고속 통신)가 발생하면, 하던 일을 멈추고 높은 우선순위의 작업을 먼저 처리(중첩)합니다.
처리가 끝나면 다시 원래 하던 낮은 우선순위의 작업으로 돌아갑니다.
주소록(Vector Table) 보유: 일반적인 구형 CPU는 인터럽트가 발생하면 먼저 "누가 날 불렀지?" 하고 소프트웨어적으로 찾아야 했습니다.
하지만 NVIC은 벡터 테이블이라는 메모리 주소록을 가지고 있어서, 인터럽트 번호만 들어오면 그 즉시 실행해야 할 함수(ISR, Interrupt Service Routine)의 시작 주소로 CPU를 바로 점프(Vectoring)시킵니다. 덕분에 반응 속도(Latency)가 엄청나게 빠릅니다.
레지스터를 통한 섬세한 관리: 수많은 하드웨어 핀이나 타이머를 켜고 끌 수 있습니다.
Enable/Disable: 특정 인터럽트를 활성화하거나 무시합니다.
Pending: 인터럽트가 발생했지만 더 높은 우선순위 작업 때문에 기다리고 있는 '대기 상태'를 관리합니다.

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

이 표는 위에서 설명한 ISER(켜는 스위치)를 TM4C123 보드에서 어떻게 맵핑해두었는지 보여줍니다. 코드에서는 NVIC_EN0_R, NVIC_EN1_R이라는 이름으로 씁니다.
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을 씁니다.
인터럽트가 발생하고 처리되는 과정은 하드웨어적으로 매우 정교하게 최적화되어 있습니다.
중첩 인터럽트 (Nested Interrupt): 우선순위가 낮은 ISR을 실행 중에 우선순위가 더 높은 인터럽트가 발생하면, 현재 ISR을 잠시 멈추고 높은 우선순위의 ISR을 먼저 실행합니다. (선점, Preemption)
테일 체이닝 (Tail Chaining): 하나의 ISR이 끝나는 시점에 이미 대기 중인(Pending) 다른 인터럽트가 있다면, 레지스터를 복구(Unstacking)했다가 다시 저장(Stacking)하는 낭비를 하지 않습니다. 즉시 다음 ISR로 진입하여 처리 속도를 최적화합니다.
지연 도착 (Late Arrival): 낮은 우선순위 인터럽트를 처리하기 위해 레지스터를 스택에 저장하는 도중, 높은 우선순위 인터럽트가 발생하면, 하드웨어는 즉시 높은 우선순위 ISR로 목표를 바꿔 실행합니다. (낮은 우선순위는 나중에 처리됨)

멀티스레딩이나 인터럽트 환경에서는 여러 실행 흐름(스레드 또는 ISR)이 공유 자원(전역 변수, I/O 포트 등)에 접근할 때 문제가 발생할 수 있습니다.
제공해주신 예시는 '비친화적(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)을 보장해야 합니다.
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) 같은 동기화 도구를 사용하여 한 번에 하나의 스레드만 접근하도록 해야 합니다.