ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work
#한 줄 요약
“FromISR API는 block이 불가능하고 yield를 명시해야 합니다” — task API와 분리해 안전성을 확보합니다.
#API 명명 규칙
/* Task context */xQueueSend(q, &item, xTicksToWait);xSemaphoreTake(sem, xTicksToWait);
/* ISR context */xQueueSendFromISR(q, &item, &xHigherPriorityTaskWoken);xSemaphoreGiveFromISR(sem, &xHigherPriorityTaskWoken);두 API의 차이를 정리하면 다음과 같습니다.
| 항목 | Task API | ISR API |
|---|---|---|
| Block 가능 | O (timeout) | X (timeout 인자 없음) |
| Critical section | portENTER_CRITICAL | portENTER_CRITICAL_FROM_ISR |
| Wake 결과 | 자동 yield | pxHigherPriorityTaskWoken 반환 |
| Reschedule | 함수 내부 | 호출자가 명시 |
#pxHigherPriorityTaskWoken은 왜 필요한가
ISR이 어떤 task를 wake했을 때, 깨어난 task의 priority가 interrupted task보다 높으면 ISR 종료 후 context switch가 발생합니다. 그러나 ISR 자체는 task switch를 직접 호출할 수 없습니다. 대신 pending 비트만 set해 두고, ISR exit 시점에 실제 switch가 처리됩니다.
void uart_rx_isr(void) { uint8_t byte = UART->RDR; BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(rx_queue, &byte, &xHigherPriorityTaskWoken);
/* ISR 끝에서 — 더 높은 priority task wake됐다면 yield */ portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}portYIELD_FROM_ISR은 Cortex-M에서 SCB->ICSR = PENDSVSET을 통해 PendSV를 호출합니다.
#portENTER_CRITICAL_FROM_ISR
uint32_t saved = portSET_INTERRUPT_MASK_FROM_ISR();{ /* ISR-safe critical — IRQ priority temp boost */}portCLEAR_INTERRUPT_MASK_FROM_ISR(saved);ISR 내부에서 더 높은 priority ISR을 차단할 때는 BASEPRI를 설정합니다. 이때 기존 BASEPRI 값을 save/restore하는 것이 핵심입니다.
static inline uint32_t portSET_INTERRUPT_MASK_FROM_ISR(void) { uint32_t saved_basepri; __asm volatile ( "mrs %0, basepri \n" "mov r0, %1 \n" "msr basepri, r0 \n" : "=r"(saved_basepri) : "i"(configMAX_SYSCALL_INTERRUPT_PRIORITY) : "r0" ); return saved_basepri;}#ISR 내부에서 Block을 금지하는 이유
void some_isr(void) { xSemaphoreTake(sem, 100); // ✗ 컴파일 에러}이유를 정리하면 다음과 같습니다.
- ISR은 task가 아니므로 TCB가 없고 block list에도 들어갈 수 없습니다.
- Block이 발생하면 다른 ISR이나 task가 무한 대기에 빠져 deadlock으로 이어집니다.
- ISR 길이가 곧 system response time을 결정합니다.
규칙은 단순합니다. ISR은 비동기 신호 송신만 담당하고, 실제 처리는 task에 위임해야 합니다.
#Deferred Interrupt Pattern
/* Deferred task — high priority */void deferred_handler_task(void *p) { for (;;) { xSemaphoreTake(uart_rx_sem, portMAX_DELAY);
/* ISR 대신 여기서 무거운 처리 */ process_uart_packet(); }}
/* ISR — 짧게 */void uart_irq(void) { BaseType_t pxHP = pdFALSE;
xSemaphoreGiveFromISR(uart_rx_sem, &pxHP); portYIELD_FROM_ISR(pxHP);}장점은 세 가지입니다.
- ISR 길이를 수 µs 수준으로 최소화할 수 있습니다.
- Task context에서 blocking API를 자유롭게 사용할 수 있습니다.
- Priority 조정으로 latency를 제어할 수 있습니다.
#Timer Service (Daemon) Task
/* FreeRTOS internal — config로 활성화 */void prvTimerTask(void *p) { for (;;) { /* Wait for command */ xQueueReceive(xTimerQueue, &cmd, portMAX_DELAY);
switch (cmd.type) { case PEND_FUNC_CALL: cmd.func(cmd.arg1, cmd.arg2); // ISR에서 요청한 함수 실행 break; case TIMER_EXPIRED: cmd.timer->callback(); break; } }}ISR이 복잡한 작업을 수행해야 한다면 다음과 같이 위임합니다.
void heavy_isr(void) { BaseType_t pxHP = pdFALSE;
xTimerPendFunctionCallFromISR(do_heavy_work, arg1, arg2, &pxHP); portYIELD_FROM_ISR(pxHP);}이렇게 하면 daemon task가 do_heavy_work(arg1, arg2)를 대신 실행합니다.
#ISR Priority와 configMAX_SYSCALL_INTERRUPT_PRIORITY
FreeRTOS API는 낮은 priority ISR에서만 호출할 수 있습니다.
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 // = 0xBF#define configKERNEL_INTERRUPT_PRIORITY 255 // = 0xFF (lowest)⚠️ Cortex-M에서 priority는 수치가 클수록 실제 우선순위가 낮아집니다. 헷갈리기 쉬운 부분입니다.
#taskYIELD_FROM_ISR (Cortex-M)
#define portYIELD_FROM_ISR(x) do { \ if (x != pdFALSE) { \ portYIELD(); /* SCB->ICSR = PENDSVSET */ \ } \} while (0)또는 일부 포트:
portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);xHigherPriorityTaskWoken == pdFALSE이면 그대로 ISR이 종료되고 같은 task로 복귀합니다.
xHigherPriorityTaskWoken == pdTRUE이면 ISR 종료 직후 PendSV가 발생하고 context switch가 일어납니다.
#ISR과 Task 간 공유 변수의 Atomic Operation
ISR이 task와 공유 변수를 읽고 쓸 때는 barrier가 필요합니다.
volatile uint32_t shared_counter;
void task(void *p) { portENTER_CRITICAL(); uint32_t v = shared_counter; portEXIT_CRITICAL(); /* use v */}
void isr(void) { shared_counter++; // 32-bit atomic on M-class}Cortex-M에서 32-bit alignment에 32-bit access는 atomic입니다. 그러나 64-bit 변수는 split read가 발생하므로 critical section이 반드시 필요합니다.
#자주 하는 실수
⚠️ ISR에서 Task API를 호출하는 경우
void isr(void) { xQueueSend(q, ...); // ✗ task API}이 코드는 block이 가능한 API를 ISR에서 부르는 형태입니다. 컴파일은 통과하지만 런타임에 hard fault나 데이터 corruption이 발생합니다. 항상 *FromISR 변형을 사용해야 합니다.
⚠️ pxHigherPriorityTaskWoken을 누락하는 경우
void isr(void) { BaseType_t pxHP = pdFALSE; xSemaphoreGiveFromISR(sem, &pxHP); /* portYIELD_FROM_ISR 안 호출 */}Wake가 발생해도 yield가 일어나지 않으므로 high-priority task는 다음 스케줄링 시점까지 기다려야 합니다. 그만큼 latency가 늘어납니다.
⚠️ ISR Priority가 configMAX_SYSCALL_INTERRUPT_PRIORITY보다 높은 경우
NVIC_SetPriority(UART1_IRQn, 0); // Highest — FreeRTOS API 금지 영역이 영역에서 xQueueSendFromISR을 호출하면 kernel data가 손상됩니다. 항상 낮은 priority로 설정해야 합니다.
⚠️ Critical section 안에서 nested ISR이 BASEPRI를 직접 변경하는 경우
낮은 priority IRQ 안에서 portENTER_CRITICAL_FROM_ISR()을 호출한 뒤 BASEPRI를 직접 바꾸면 stack이 깨집니다.
#정리
- ISR API는 non-blocking이며 yield를 명시적으로 호출해야 합니다.
pxHigherPriorityTaskWoken을 통해 task wake 결과를 전달합니다.portYIELD_FROM_ISR은 PendSV를 pending시켜 ISR 종료 후 switch를 일으킵니다.- Deferred handler를 사용해 ISR을 짧게 유지하고 무거운 처리는 task에 맡깁니다.
configMAX_SYSCALL_INTERRUPT_PRIORITY보다 우선순위가 높은 IRQ에서는 FreeRTOS API를 호출하면 안 됩니다.
다음 편에서는 deadlock 패턴의 발견과 예방을 다룹니다.
#관련 항목
Practical RTOS Internals · 31 of 53
- 1Practical RTOS Internals — 실시간 커널 내부 분석 시리즈 소개
- 2RTOS가 필요한 이유 — 일반 OS와의 결정적 차이
- 3Task와 Thread 개념 — TCB·상태 머신·생명 주기 분석
- 4실시간 스케줄링 알고리즘 비교 — RR·Priority·EDF·RMS
- 5Preemption과 Cooperation — 강제 전환 vs 자발 양보
- 6인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API
- 7동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
- 8Semaphore 개념 분해 — Counting·Binary·P/V 연산
- 9Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance
- 10큐와 메시지 패싱 — Producer-Consumer·Ring Buffer·전달 의미
- 11실시간성 분석 — Latency·Jitter·Deadline·WCET·RMA
- 12Ready List 자료구조 분석 — Linked List·Bitmap·O(1) Scheduler
- 13Blocked List 자료구조 — Timeout 정렬·Delta List·Two-List Scheme
- 14Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
- 15Context Switch 원리 분석 — 레지스터 저장·복원·Stack Frame
- 16ARM Cortex-M Context Switch — PendSV·MSP/PSP 어셈블리 추적
- 17ARM Cortex-A Context Switch — Mode 전환·SVC·Banked Registers
- 18RISC-V Context Switch 분석 — ECALL·mret·CSR
- 19RTOS Tick과 타이머 — SysTick·Generic Timer·configTICK_RATE_HZ
- 20Tickless 모드 구현 — Idle Tick Suppression·Sleep·Wake 보정
- 21Scheduler Latency 측정 기법 — GPIO Toggle·DWT·ftrace·cyclictest
- 22RTOS Tracing과 Observability — Tracealyzer·SystemView·ITM/ETM
- 23Critical Section 구현 비교 — IRQ Disable·BASEPRI·Spinlock
- 24Semaphore 내부 구현 추적 — Counter·Wait List·ISR-Safe Variant
- 25Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
- 26Priority Inversion 문제 — Mars Pathfinder 사례·Bounded vs Unbounded
- 27Priority Inheritance 구현 — Inherit·Disinherit·Chain
- 28Priority Ceiling Protocol — Immediate vs Original 비교
- 29Queue 내부 구현 추적 — Ring Buffer·2 Wait Lists·Atomic Send/Receive
- 30Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier
- 31ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work
- 32Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout
- 33Stream Buffer와 Message Buffer — FreeRTOS 10의 Lock-Free SPSC
- 34실시간 메모리 요구사항 — Determinism·Fragmentation·WCET
- 35FreeRTOS Heap_1~5 분석 — 5종 Allocator의 구조와 트레이드오프
- 36TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
- 37Static Allocation — 컴파일 타임으로 동적 위험 제거하기
- 38Memory Pool — Fixed-Size Block Allocator의 단순함과 강력함
- 39Stack Overflow 탐지 — Canary·MPU·Watermark 3중 방어
- 40SMP RTOS 설계 — Ready List·Affinity·IPI·Load Balancing
- 41SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV
- 42Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API
- 43RTOS System Call — SVC·ECALL·User/Kernel 분리·FreeRTOS-MPU
- 44TrustZone과 TF-M — Secure/Non-Secure·NSC Veneer·PSA
- 45AMP와 OpenAMP — Heterogeneous SoC·RPMsg·remoteproc
- 46C++ in RTOS — RAII·std::thread·ETL·Coroutine
- 47FreeRTOS 소스 분석 — tasks.c·queue.c·port.c 추적
- 48Zephyr 커널 분석 — k_thread·k_sem·Driver Model
- 49RT-Thread 분석 — Object 모델·Components·Smart·Studio
- 50RTOS 포팅 가이드 — 새 아키텍처에 옮기는 절차
- 51RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
- 52Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
- 53PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL
관련 글
인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API
ISR은 task가 아니므로 context도 따로 관리됩니다. Long work는 deferred task로 넘기고, FromISR API 패턴을 씁니다.
Semaphore 내부 구현 추적 — Counter·Wait List·ISR-Safe Variant
FreeRTOS semaphore = Queue wrapper. Counter + priority-sorted wait list.
PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL
2024년 9월 Linux 6.12 mainline에 합류한 PREEMPT_RT의 핵심 변경을 정리하고, Xenomai 4·EVL과 함께 RTOS와의 선택 기준을 비교합니다. threaded IRQ·sleeping spinlock·cyclictest까지 한 지도에 모읍니다.
이 글을 참조하는 글 (6)
- Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API— Practical RTOS Internals
- SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV— Practical RTOS Internals
- Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout— Practical RTOS Internals
- Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier— Practical RTOS Internals
- ARM Cortex-M Context Switch — PendSV·MSP/PSP 어셈블리 추적— Practical RTOS Internals
- 인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API— Practical RTOS Internals