[임베디드 OS 개발 프로젝트] 6. 인터럽트

Sijin·2025년 6월 17일

인터럽트

  • 사람이나 외부 시스템 같은 외부와 상호작용은 인터럽트를 통해 처리된다
    • 스마트폰 화면의 터치, 볼륨 버튼으로 음량 조절 등
  • UART를 예시로보면 아래 순서대로 동작한다
    • UART 하드웨어는 인터럽트를 발생시킴
    • 이를 처리하기 위해, UART 하드웨어와 인터럽트 컨트롤러를 연결시킴
    • 인터럽트 컨트롤러는 arm 코어로 인터럽트를 보냄
    • 코어는 익셉션 핸들러를 호출
      • 펌웨어에서 cpsr의 IRQ 혹은 FIQ 마스크가 꺼져있어야 함
    • 익셉션 핸들러에서 적절한 인터럽트 핸들러를 호출

인터럽트 컨트롤러

  • RealViewPB에는 Generic Interrupt Controller(GIC)라는 인터럽트 컨트롤러 하드웨어가 있다
  • 각 GIC는 각각 고유한 방법으로 인터럽트를 처리한다, 그래도 기본적인 기능은 비슷하다
  • 우리는 그 기본적, 공통의 기능을 사용해보자
  • 우선 GIC의 레지스터 구조체를 만들어보자
# hal/rvpb/Interrupt.h

typedef union CpuControl_t
 {
     uint32_t all;
     struct {
         uint32_t Enable:1;          // 0
         uint32_t reserved:31;
     } bits;
 } CpuControl_t;
 
 typedef union PriorityMask_t
 {
     uint32_t all;
     struct {
         uint32_t Reserved:4;        // 3:0
         uint32_t Prioritymask:4;    // 7:4
         uint32_t reserved:24;
     } bits;
 } PriorityMask_t;
 
 ...
 
 typedef struct GicCput_t
 {
     CpuControl_t       cpucontrol;        //0x000
     PriorityMask_t     prioritymask;      //0x004
     BinaryPoint_t      binarypoint;       //0x008
     InterruptAck_t     interruptack;      //0x00C
     EndOfInterrupt_t   endofinterrupt;    //0x010
     RunningInterrupt_t runninginterrupt;  //0x014
     HighestPendInter_t highestpendinter;  //0x018
 } GicCput_t;
 
 typedef struct GicDist_t
 {
     DistributorCtrl_t   distributorctrl;    //0x000
     ControllerType_t    controllertype;     //0x004
     uint32_t            reserved0[62];      //0x008-0x0FC
     uint32_t            reserved1;          //0x100
     uint32_t            setenable1;         //0x104
     uint32_t            setenable2;         //0x108
     uint32_t            reserved2[29];      //0x10C-0x17C
     uint32_t            reserved3;          //0x180
     uint32_t            clearenable1;       //0x184
     uint32_t            clearenable2;       //0x188
 } GicDist_t;
 
 #define GIC_CPU_BASE  0x1E000000  //CPU interface
 #define GIC_DIST_BASE 0x1E001000  //distributor
  • GIC는 레지스터를 크게 CPU Interface registers와 Distrubutor registers로 구분한다
  • 레지스터 구조체와 베이스 주소를 선언했으니, 구조체 포인터를 만들어 GIC 레지스터를 제어할 수 있도록 하자
# hal/rvpb/Regs.c

#include "Interrupt.h"

volatile PL011_t* Uart = (PL011_t*)UART_BASE_ADDRESS0;
volatile GicCput_t* GicCpu = (GicCput_t*)GIC_CPU_BASE;
volatile GicDist_t* GicDist = (GicDist_t*)GIC_DIST_BASE;
  • 다음으로 아래 5가지 공용 API를 만들어보자
    • 초기화 함수
    • 인터럽트 활성화/비활성화 함수
    • 인터럽트 핸들러 등록 함수
    • 인터럽트 핸들러 호출 함수
  • 공용 API, 여러 플랫폼들이 공용으로 사용할 API이므로 HAL에 해당한다
#ifndef HAL_HALINTERRUPT_H_
#define HAL_HALINTERRUPT_H_

#define INTERRUPT_HANDLER_NUM 255

typedef void (*InterHdlr_fptr)(void);

void Hal_interrupt_init(void);
void Hal_interrupt_enable(uint32_t interrupt_num);
void Hal_interrupt_disable(uint32_t interrupt_num);
void Hal_interrupt_register_handler(InterHdlr_fptr handler, uin32_t interrupt_num);
void Hal_interrupt_run_handler(void);


#endif /* HAL_HALINTERRUPT_H_ */

init

void Hal_interrupt_init(void)
{
    GicCpu->cpucontrol.bits.Enable = 1;
    GicCpu->prioritymask.bits.Prioritymask = GIC_PRIORITY_MASK_NONE;
    GicDist->distributorctrl.bits.Enable = 1;

    for(uint32_t i=0; i<INTERRUPT_HANDLER_NUM; i++)
    {
        sHandlers[i] = NULL;
    }

    enable_irq();
}
  • init 부분 코드는 위와 같다
    • Enable
      • GicCpu와 GicDist 레지스터에서 인터럽트 컨트롤러를 Enable 시킨다
    • Priority Mask resiger
      • Priority mask register는 arm infocenter 문서에 따르면 32비트 중 [4:7] 부분만 사용한다
      • 해당 부분은 0으로 설정하면 모든 인터럽트를 mask 처리한다
      • 해당 부분을 0xf로 설정하면 인터럽트 우선순위가 0x0부터 0xe까지인 인터럽트를 허용한다
    • 이후 인터럽트 handler를 초기화하고
    • cspr에 접근해서 IRQ를 켠다

interrupt enable/disable

  • Hal_interrupt_enable(), Hal_interrupt_disable() 함수는 개별 인터럽트를 켜고 끄는 함수이다
void Hal_interrupt_enable(uint32_t interrupt_num)
{
    if(interrupt_num < GIC_IRQ_START || interrupt_num > GIC_IRQ_END)
    {
        return;
    }

    uint32_t bit_num = interrupt_num - GIC_IRQ_START;

    if(bit_num < GIC_IRQ_START)
    {
        SET_BIT(GicDist->setenable1, bit_num);
    }
    else
    {
        bit_num -= GIC_IRQ_START;
        SET_BIT(GicDist->setenable2, bit_num);
    }
}

void Hal_interrupt_disable(uint32_t interrupt_num)
{
    if(interrupt_num < GIC_IRQ_START || interrupt_num > GIC_IRQ_END)
    {
        return;
    }

    uint32_t bit_num = interrupt_num - GIC_IRQ_START;

    if(bit_num < GIC_IRQ_START)
    {
        CLR_BIT(GicDist->setenable1, bit_num);
    }
    else
    {
        bit_num -= GIC_IRQ_START;
        CLR_BIT(GicDist->setenable2, bit_num);
    }
}
  • 우리의 GIC는 64개의 인터럽트를 관리할 수 있다
    • 각 인터럽트의 enable/disable을 기록하기 위해서는 64bit가 필요하다
    • 이를 위해, 32bit 레지스터 2개 setenable1, setenable2를 사용한다
  • IRQ의 시작번호는 32이므로, IRQ 인터럽트는 32~95까지의 ID를 갖는다
  • 위 코드는 32~63 IRQ는 setenable1에, 64~95 IRQ는 setenable2에 기록한다

interrupt handler 등록/호출

void Hal_interrupt_register_handler(InterHdlr_fptr handler, uint32_t interrupt_num)
{
    sHandlers[interrupt_num] = handler;
}

void Hal_interrupt_run_handler(void)
{
    uint32_t interrupt_num = GicCpu->interruptack.bits.InterruptID;

    if(sHandlers[interrupt_num] != NULL)
    {
        sHandlers[interrupt_num]();
    }

    GicCpu->endofinterrupt.bits.InterruptID = interrupt_num;
}
  • interrupt handler 등록은 단순히 함수포인터를 배열에 저장하는 것이다
  • interrupt handler 호출은 아래와 같이 동작한다
    • 하드웨어에서 대기 중인 인터럽트 IRQ 번호는 interrupt acknowledge 레지스터에 저장된다
    • 우리는 여기서 interrupt ID를 읽고 등록했던 interrupt handler를 호출한다
    • 이후 end of interrupt 레지스터에 처리한 IRQ ID를 입력해, 인터럽트 컨트롤러에 인터럽트 처리가 끝났음을 알린다

[참고] volatile

  • volatile 키워드는 컴파일러가 해당 변수를 최적화에서 제외한다
  • 최적화에서 제외되면서 해당 변수를 사용하면 항상 메모리에 접근하게 된다
    • 일반적인 경우, 변수를 사용하면 컴파일러는 해당 값을 메모리에서 레지스터로 가져오고 연산이 끝난 뒤 필요할 때 레지스터의 값을 메모리로 commit 한다
    • volatile이 적용된 경우, 변수를 사용하면 메모리에서 읽고 연산한 뒤 바로 메모리로 commit해 동기화 시킨다
  • 아래와 같은 코드를 예시로 보자
    • volatile이 적용되지 않으면 컴파일러가 코드를 최적화하면서 1~2번째 줄이 삭제될 수도 있다
    • volatile이 적용되면 최적화를 수행하지 않고 모든 메모리 쓰기를 지정한대로 수행한다
*(volatile unsigned int *)0x8C0F = 0x8001;
*(volatile unsigned int *)0x8C0F = 0x8002;
*(volatile unsigned int *)0x8C0F = 0x8003;
  • 하드웨어 레지스터에 값을 쓰는 프로그램의 경우, 각각의 쓰기가 하드웨어에 명령을 전달하는 것 이므로 최적화로 인한 오동작이 발생할 수 있다

[참고] 다시보는 IRQ, FIQ

  • IRQ (Interrupt Request)
    • 일반적인 인터럽트
  • FIQ (Fast Interrupt Request)
    • 별도의 뱅크드 레지스터를 사용해 인터럽트 진입, 복귀 시 레지스터 저장, 복원이 필요없어 속도가 빠름
    • FIQ가 발생하면 IRQ는 자동으로 마스킹되어 처리되지 않는다
  • 우리 책에서는 IRQ만 사용할 예정이다

UART 입력과 인터럽트 연결

  • GIC 설정이 끝났으니 하드웨어 초기화 단계에서 interrupt가 초기화 되도록 수정하자
# Main.c

static void Hw_init(void)
{
    Hal_interrupt_init();
    Hal_uart_init();
}
  • UART는 이후에 인터럽트를 발생시킬 하드웨어이다
  • UART같은 인터럽트를 사용할 HW의 초기화보다 인터럽트의 초기화가 먼저 이루어져야 한다

UART에 인터럽트 연결

  • 시스템에서 인터럽트를 사용할 준비가 되었으니, 하드웨어에서 인터럽트를 발생시키면 된다
  • UART에 입력 인터럽트를 설정해보자
# hal/rvpb/Uart.c

static void interrupt_handler(void)
{
    uint8_t ch = Hal_uart_get_char();
    Hal_uart_put_char(ch);
}

void Hal_uart_init(void)
{
	// 생략
    
    // input interrupt 활성화
    Uart->uartimsc.bits.RXIM = 1;

    Hal_interrupt_enable(UART_INTERRUPT0);
    Hal_interrupt_register_handler(interrupt_handler, UART_INTERRUPT0);
}
  • UART가 초기화될 때 interrupt를 enable하고 handler를 등록하였다
    • UART 입력이 발생하면 코어에 의해 interrupt_handler 함수가 실행될 것이다
  • handler에서는 UART 하드웨어에서 읽은 값을 다시 UART 출력으로 보낸다

IRQ 익셉션 벡터 연결

  • 지금까지 GIC를 정의하고, UART의 인터럽트 핸들러를 등록하였다
    • 아직 인터럽트 핸들러가 호출되는 부분은 없다
  • ARM 코어는 인터럽트를 받으면 IRQ 익셉션을 발생시킨다
  • 이때 동작모드가 IRQ로 바뀌면서 익셉션 벡터 테이블의 IRQ 익셉션 벡터로 점프한다
    • 이 동작은 인터럽트의 종류(IRQ ID)에 상관없이 무조건 실행된다
  • 따라서 IRQ 익셉션 벡터와 인터럽트 컨트롤러의 핸들러를 연결해주어야 한다
# boot/Handler.c

#include "stdbool.h"
#include "stdint.h"
#include "HalInterrupt.h"

__attribute__ ((interrupt ("IRQ"))) void Irq_Handler(void)
{
    Hal_interrupt_run_handler();
}

__attribute__ ((interrupt ("FIQ"))) void Fiq_Handler(void)
{
    while(true); //dummy
}
  • 익셉션 핸들러를 정의하고, interrupt handler를 실행하도록 작성하였다
    • 우리 책에서 FIQ는 사용하지 않을 것이므로 이부분은 dummy로 만들었다
  • 만든 익셉션 핸들러는 이제 익셉션 벡터 테이블에 등록되어야 한다
# boot/Entry.S

    vector_start:
        LDR     PC, reset_handler_addr
        LDR     PC, undef_handler_addr
        LDR     PC, svc_handler_addr
        LDR     PC, pftch_abt_handler_addr
        LDR     PC, data_abt_handler_addr
        B       .
        LDR     PC, irq_handler_addr
        LDR     PC, fiq_handler_addr

        reset_handler_addr:     .word   reset_handler
        undef_handler_addr:     .word   dummy_handler
        svc_handler_addr:       .word   dummy_handler
        pftch_abt_handler_addr: .word   dummy_handler
        data_abt_handler_addr:  .word   dummy_handler
        irq_handler_addr:       .word   Irq_Handler
        fiq_handler_addr:       .word   Fiq_Handler
  • irq_handler_addr, fiq_handler_addr을 기존 dummy_handler에서 위에서 작성한 익셉션 핸들러로 연결시켜주었다

[참고] gcc 확장 기능 (__attribute__)

  • __attribute__ 키워드는 gcc 컴파일러의 확장 기능을 사용한다는 지시어이다
  • 우리가 사용한 __attribute__ ((interrupt ("IRQ")))는 arm용 gcc 전용 확장 기능이다
  • 이는 IRQ, FIQ 핸들러에 진입하는 코드, 나가는 코드를 자동으로 만들어준다
LR 레지스터
  • LR 레지스터는 arm 아키텍처에서 함수호출 / 인터럽트 발생 시 복귀 주소를 저장한다
  • 복귀 주소는 함수 호출, 인터럽트 발생의 경우 각각 다르다
    • 함수 호출의 경우 다음 명령어로 복귀해야 한다
    • 인터럽트의 경우 인터럽트가 발생한 시점의 코드로 복귀해야 한다
    • arm에서는 LR 레지스터의 기본동작을 함수호출로 기준하고 있다
    • 따라서 인터럽트 발생 시에는 LR 레지스터에서 4를 빼서 현재 시점으로 복귀해주어야 한다
  • 예를 들어 아래와 같은 코드가 있고, a++에서 interrupt가 발생했을 때
    • interrupt 처리 이후 다시 a++ 동작인 0x1000으로 돌아와야 한다
    • 그러나 LR 레지스터에는 0x1004가 저장된다
    • 따라서 인터럽트 발생 시점의 instruction인 a++를 건너뛰지 않도록, LR 레지스터에서 4를 빼주어야 한다
// main.c
int main(void) {
    int a = 0;
    a++;            // [0x1000] <-- 여기서 IRQ 발생했다고 가정
    a += 2;         // [0x1004]
    while(1);
}
  • 이를 역 어셈블해서 확인해보면, __attribute__가 있는 경우는 아래와 같이 함수 첫 부분에서 lr 레지스터에서 4를 빼는걸 볼 수 있다
    • 일반 함수에는 저런 instruction이 없다
arm-none-eabi-objdump -D build/Handler.o
...
00000000 <Irq_Handler>:
   0:   e24ee004        sub     lr, lr, #4
   4:   e92d581f        push    {r0, r1, r2, r3, r4, fp, ip, lr}
   8:   e28db01c        add     fp, sp, #28
   c:   e3001000        movw    r1, #0

0개의 댓글