인터럽트
- 사람이나 외부 시스템 같은 외부와 상호작용은 인터럽트를 통해 처리된다
- 스마트폰 화면의 터치, 볼륨 버튼으로 음량 조절 등
- 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