본문으로 건너뛰기
Practical RTOS Internals · 30/53

ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work

· Hawk · 4분 읽기

#한 줄 요약

“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 APIISR API
Block 가능O (timeout)X (timeout 인자 없음)
Critical sectionportENTER_CRITICALportENTER_CRITICAL_FROM_ISR
Wake 결과자동 yieldpxHigherPriorityTaskWoken 반환
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에서만 호출할 수 있습니다.

FreeRTOSConfig.h
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 // = 0xBF
#define configKERNEL_INTERRUPT_PRIORITY 255 // = 0xFF (lowest)

Cortex-M priority bands separated by configMAX_SYSCALL_INTERRUPT_PRIORITY

⚠️ 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

  1. 1Practical RTOS Internals — 실시간 커널 내부 분석 시리즈 소개
  2. 2RTOS가 필요한 이유 — 일반 OS와의 결정적 차이
  3. 3Task와 Thread 개념 — TCB·상태 머신·생명 주기 분석
  4. 4실시간 스케줄링 알고리즘 비교 — RR·Priority·EDF·RMS
  5. 5Preemption과 Cooperation — 강제 전환 vs 자발 양보
  6. 6인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API
  7. 7동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
  8. 8Semaphore 개념 분해 — Counting·Binary·P/V 연산
  9. 9Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance
  10. 10큐와 메시지 패싱 — Producer-Consumer·Ring Buffer·전달 의미
  11. 11실시간성 분석 — Latency·Jitter·Deadline·WCET·RMA
  12. 12Ready List 자료구조 분석 — Linked List·Bitmap·O(1) Scheduler
  13. 13Blocked List 자료구조 — Timeout 정렬·Delta List·Two-List Scheme
  14. 14Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
  15. 15Context Switch 원리 분석 — 레지스터 저장·복원·Stack Frame
  16. 16ARM Cortex-M Context Switch — PendSV·MSP/PSP 어셈블리 추적
  17. 17ARM Cortex-A Context Switch — Mode 전환·SVC·Banked Registers
  18. 18RISC-V Context Switch 분석 — ECALL·mret·CSR
  19. 19RTOS Tick과 타이머 — SysTick·Generic Timer·configTICK_RATE_HZ
  20. 20Tickless 모드 구현 — Idle Tick Suppression·Sleep·Wake 보정
  21. 21Scheduler Latency 측정 기법 — GPIO Toggle·DWT·ftrace·cyclictest
  22. 22RTOS Tracing과 Observability — Tracealyzer·SystemView·ITM/ETM
  23. 23Critical Section 구현 비교 — IRQ Disable·BASEPRI·Spinlock
  24. 24Semaphore 내부 구현 추적 — Counter·Wait List·ISR-Safe Variant
  25. 25Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
  26. 26Priority Inversion 문제 — Mars Pathfinder 사례·Bounded vs Unbounded
  27. 27Priority Inheritance 구현 — Inherit·Disinherit·Chain
  28. 28Priority Ceiling Protocol — Immediate vs Original 비교
  29. 29Queue 내부 구현 추적 — Ring Buffer·2 Wait Lists·Atomic Send/Receive
  30. 30Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier
  31. 31ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work
  32. 32Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout
  33. 33Stream Buffer와 Message Buffer — FreeRTOS 10의 Lock-Free SPSC
  34. 34실시간 메모리 요구사항 — Determinism·Fragmentation·WCET
  35. 35FreeRTOS Heap_1~5 분석 — 5종 Allocator의 구조와 트레이드오프
  36. 36TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
  37. 37Static Allocation — 컴파일 타임으로 동적 위험 제거하기
  38. 38Memory Pool — Fixed-Size Block Allocator의 단순함과 강력함
  39. 39Stack Overflow 탐지 — Canary·MPU·Watermark 3중 방어
  40. 40SMP RTOS 설계 — Ready List·Affinity·IPI·Load Balancing
  41. 41SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV
  42. 42Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API
  43. 43RTOS System Call — SVC·ECALL·User/Kernel 분리·FreeRTOS-MPU
  44. 44TrustZone과 TF-M — Secure/Non-Secure·NSC Veneer·PSA
  45. 45AMP와 OpenAMP — Heterogeneous SoC·RPMsg·remoteproc
  46. 46C++ in RTOS — RAII·std::thread·ETL·Coroutine
  47. 47FreeRTOS 소스 분석 — tasks.c·queue.c·port.c 추적
  48. 48Zephyr 커널 분석 — k_thread·k_sem·Driver Model
  49. 49RT-Thread 분석 — Object 모델·Components·Smart·Studio
  50. 50RTOS 포팅 가이드 — 새 아키텍처에 옮기는 절차
  51. 51RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
  52. 52Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
  53. 53PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL