컴퓨팅 시스템은 인터럽트를 통해 외부의 존재와 상호작용한다. 여기서 외부의 존재란 사람이 될 수도 있고 다른 컴퓨팅 시스템이 될 수도 있다. 예를 들어보면, 키보드의 자판을 누르면 키보드 내부에서 인터럽트가 발생하고 키보드의 펌웨어가 이를 처리한다. 그리고 연결된 데스크탑으로 신호를 보내는데 이를 수신한 데스크탑 측에서도 다시 인터럽트가 발생한다. 그럼 데스크탑의 운영체제에서 이를 처리하고 키보드 입력 동작을 수행하게 된다.
해당 포스팅에서는 인터럽트를 처리하는 과정과 이를 활용하여 UART 입력 인터럽트를 처리하는 방법을 학습한다.
ARM 코어의 CPSR 레지스터에서 6, 7번째 비트 즉 I와 F 필드는 Interrupt와 Fast Interrupt의 활성화 여부를 의미한다. 각각의 필드들이 0일 때가 Enable 상태로 Interrupt나 Fast Interrupt가 허용된 상태이다.
MCU 내부에는 Interrupt Controller가 존재한다. 그리고 Interrupt Controller에는 여러개의 pin들이 존재하는데, 이들은 여러 장치들과 연결되어 있다. 특정 pin에 전기 신호를 주면 Interrupt Controller가 Interrupt가 발생한 것을 인지하게 되고 core에게 이를 알려준다. 만약 CPSR에서 인터럽트가 허용된 상태라면 core는 IRQ 혹은 FIQ 모드로 변경되고 Exception Vector Table을 따라 Exception Handler로 점프한다. Exception Handler 내부에서는 발생한 인터럽트의 종류에 따라 적절한 Interrupt Handler (Interrupt Service Routine : ISR)를 호출하여 인터럽트를 처리할 수 있다.
이는 인터럽트가 발생했을 때 수행하는 동작을 나타낸 그림이다. 편의를 위해 FIQ는 제외하였다. 해당 상황에서는 두 종류의 인터럽트가 발생하였는데, IRQ Exception Handler가 실행되는 것 까지는 동일하다. 하지만 IRQ Exception Handler 내부에서 인터럽트의 종류를 파악하고 이에 알맞는 Interrupt Handler가 호출된다는 점에서 차이가 있다. 이를 통해서 IRQ Exception Handler 내부에는 발생한 Interrupt의 종류를 파악하고 이에 따라 Interrupt Handler를 호출하는 코드가 작성될 것이라는 것을 알 수 있다.
realview-pb-a8은 Generic Interrupt Controller 라는 이름의 Interrupt Controller HW를 사용하고 있다. GIC는 해당 HW만의 방식으로 인터럽트를 처리하는데, 다른 Interrupt Controller들도 그들만의 처리 방식으로 인터럽트를 처리한다.
GIC는 일종의 Interrupt Controller HW 이므로 이를 사용하기 위해서는 UART와 마찬가지로 HAL을 구현해주어야 한다.
HAL 구현시 사용할 기타 라이브러리 함수들을 작성한다.
/* armcpu.h */
#ifndef LIB_ARMCPU_H_
#define LIB_ARMCPU_H_
void enable_irq(void);
void enable_fiq(void);
void disable_irq(void);
void disable_fiq(void);
#endif /* LIB_ARMCPU_H_ */
/* armcpu.c */
#include "armcpu.h"
void enable_irq(void)
{
__asm__ ("PUSH {r0, r1}");
__asm__ ("MRS r0, cpsr");
__asm__ ("BIC r1, r0, #0x80");
__asm__ ("MSR cpsr, r1");
__asm__ ("POP {r0, r1}");
}
void enable_fiq(void)
{
__asm__ ("PUSH {r0, r1}");
__asm__ ("MRS r0, cpsr");
__asm__ ("BIC r1, r0, #0x40");
__asm__ ("MSR cpsr, r1");
__asm__ ("POP {r0, r1}");
}
void disable_irq(void)
{
__asm__ ("PUSH {r0, r1}");
__asm__ ("MRS r0, cpsr");
__asm__ ("ORR r1, r0, #0x80");
__asm__ ("MSR cpsr, r1");
__asm__ ("POP {r0, r1}");
}
void disable_fiq(void)
{
__asm__ ("PUSH {r0, r1}");
__asm__ ("MRS r0, cpsr");
__asm__ ("ORR r1, r0, #0x40");
__asm__ ("MSR cpsr, r1");
__asm__ ("POP {r0, r1}");
}
CPSR을 제어하기 위한 코드로, lib 디렉터리 내부에 작성한다. CPSR 레지스터를 제어하기 위해서는 어셈블리어를 사용해야 한다. ARM사에서 제공하는 ARMCC는 컴파일러의 빌트인 변수로 cpsr에 접근할 수 있지만 기타 컴파일러는 불가능하기 때문에 어셈블리어를 사용해야 한다.
구현 방법으로는 두 가지가 있는데, 어셈블리어 소스 파일을 직접 작성하는 방법과 C언어에서 인라인 어셈블리어를 사용하는 방법이 있다. 이 때 인라인 어셈블리어를 사용하면 스택에 레지스터를 백업 및 복구하는 코드와 리턴 처리하는 코드를 컴파일러가 자동으로 만들어준다 는 장점이 있다.
해당 코드들은 CPSR 레지스터의 6, 7번 비트인 I와 F를 변경하기 위한 코드이다. enable 함수에서는 해당 필드를 0으로 바꾸기 위해 BIC 명령을 사용하였고 disable 함수에서는 해당 필드를 1로 바꾸기 위해 ORR 명령을 사용하였다.
#ifndef INCLUDE_MEMIO_H_
#define INCLUDE_MEMIO_H_
#define SET_BIT(p,n) ((p) |= (1 << (n)))
#define CLR_BIT(p,n) ((p) &= ~(1 << (n)))
#endif /* INCLUDE_MEMIO_H_ */
특정 비트를 쉽게 set 하거나 clear 하기 위한 코드로, include 디렉터리 내부에 작성한다.
#ifndef HAL_HALINTERRUPT_H_
#define HAL_HALINTERRUPT_H_
#include "stdint.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, uint32_t interrupt_num);
void Hal_interrupt_run_handler(void);
#endif /* HAL_HALINTERRUPT_H_ */
Interrupt를 사용하기 위한 공용 인터페이스로 hal 디렉터리 내부에 작성한다.
함수 포인터를 사용하여서 인터럽트 핸들러 함수 포인터 자료형을 선언하였다. 이는 인터럽트를 등록할 때 인터럽트 번호와 함께 전달받을 수 있다.
rvpb에서 사용하는 GIC 레지스터들을 추상화하고 사용하는 코드들로, hal 디렉터리 내부 rvpb 디렉터리 하위에 작성한다.
#ifndef HAL_RVPB_INTERRUPT_H_
#define HAL_RVPB_INTERRUPT_H_
#include "stdint.h"
typedef union CpuControl_t
{
uint32_t all;
struct {
uint32_t Enable:1; // 0
uint32_t reserved:31;
} bits;
} CpuControl_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
#define GIC_PRIORITY_MASK_NONE 0xF
#define GIC_IRQ_START 32
#define GIC_IRQ_END 95
#endif /* HAL_RVPB_INTERRUPT_H_ */
GIC는 레지스터를 크게 두 그룹으로 구분한다.
여기서도 마찬가지로 데이터 시트를 보고 각 레지스터의 offset에 맞게 구조체를 구성해야 한다.
#include "stdint.h"
#include "Uart.h"
#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;
rvpb 플랫폼에서 사용하는 여러 HW의 레지스터 구조체 포인터를 정의하는 파일이다.
#include "stdint.h"
#include "memio.h"
#include "Interrupt.h"
#include "HalInterrupt.h"
#include "armcpu.h"
extern volatile GicCput_t* GicCpu;
extern volatile GicDist_t* GicDist;
// interrupt handler function pointers
static InterHdlr_fptr sHandlers[INTERRUPT_HANDLER_NUM]; // 255
void Hal_interrupt_init(void)
{
GicCpu->cpucontrol.bits.Enable = 1;
// use all interrupts
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();
}
void Hal_interrupt_enable(uint32_t interrupt_num)
{
if ((interrupt_num < GIC_IRQ_START) || (GIC_IRQ_END < interrupt_num))
{
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) || (GIC_IRQ_END < interrupt_num))
{
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);
}
}
void Hal_interrupt_register_handler(InterHdlr_fptr handler, uint32_t interrupt_num)
{
sHandlers[interrupt_num] = handler;
}
// IRQ Exception Handler
void Hal_interrupt_run_handler(void)
{
uint32_t interrupt_num = GicCpu->interruptack.bits.InterruptID;
if (sHandlers[interrupt_num] != NULL)
{
// run Interrupt Handler
sHandlers[interrupt_num]();
}
GicCpu->endofinterrupt.bits.InterruptID = interrupt_num;
}
HalInterrupt.h에서 정의한 공용 인터페이스를 구현하기 위한 소스 파일이다.
static InterHdlr_fptr sHandlers[INTERRUPT_HANDLER_NUM]; // 255
Interrupt Handler 함수 포인터들을 저장하는 배열을 정의하였다. 해당 포스팅에서는 255의 크기를 가지는 배열을 선언하였는데, 메모리가 부족한 플랫폼에서 코드를 작성하는 경우에는 좋지 못한 방법이다. 인터럽트 핸들러 개수는 사전에 예측할 수 있으므로 필요한 만큼 메모리를 할당하여 사용하는 것이 바람직하다.
void Hal_interrupt_init(void)
{
GicCpu->cpucontrol.bits.Enable = 1;
// use all interrupts
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();
}
CPU Interface register와 Distributor register의 interrupt를 활성화한다.
그리고 Priority mask register 레지스터에 접근하여 0x0 ~ 0xE 사이의 우선순위를 가지는 인터럽트를 허용한다. 이 때 우선순위의 기본 값은 0이므로 실질적으로는 모든 인터럽트들이 허용되는 것이다.
이후에는 Interrupt Handler 함수 포인터 배열을 NULL로 초기화하고 인라인 어셈블리어를 사용하여 작성한 enable_irq()를 호출한다. 이를 통해서 CPSR 레지스터를 조작할 수 있다.
void Hal_interrupt_enable(uint32_t interrupt_num)
{
if ((interrupt_num < GIC_IRQ_START) || (GIC_IRQ_END < interrupt_num))
{
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) || (GIC_IRQ_END < interrupt_num))
{
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);
}
}
개별 인터럽트를 활성화/비활성화 시킬 때 사용하는 함수이다. Hal_interrupt_enable() 함수와 Hal_interrupt_disable() 함수의 흐름은 동일하다. 우선 인자로 전달받은 인터럽트 번호를 확인하여 오동작을 방지한다. 이후에는 Set-enable1, Set-enable2 레지스터에 접근하여 개별 인터럽트의 활성화 여부를 조작한다.
GIC는 총 64개의 인터럽트를 관리할 수 있다. 따라서 32비트 레지스터 2개를 통해 각각의 인터럽트들을 관리한다. Set-enable1 레지스터는 32번 ~ 63번을 관리하고 Set-enable2 레지스터는 64번 ~ 95번 인터럽트를 관리한다. 예외 처리 이후의 코드들은 이 64개의 개별 인터럽트를 활성화 혹은 비활성화 하기 위한 코드이다.
void Hal_interrupt_register_handler(InterHdlr_fptr handler, uint32_t interrupt_num)
{
sHandlers[interrupt_num] = handler;
}
인터럽트를 등록할 때 사용하는 함수이다. 인자로 인터럽트 핸들러 함수 포인터와 인터럽트 번호를 전달받는다. 그리고 전달받은 인터럽트 번호를 인덱스로 사용하여 핸들러 함수 포인터를 저장한다.
// IRQ Exception Handler
void Hal_interrupt_run_handler(void)
{
uint32_t interrupt_num = GicCpu->interruptack.bits.InterruptID;
if (sHandlers[interrupt_num] != NULL)
{
// run Interrupt Handler
sHandlers[interrupt_num]();
}
GicCpu->endofinterrupt.bits.InterruptID = interrupt_num;
}
Interrupt Acknowledge 레지스터에서 대기중인 인터럽트 번호를 읽어온다. 인터럽트를 등록할 때 인터럽트 번호를 인덱스로 핸들러를 등록했기 때문에 이렇게 읽어온 인터럽트 번호를 통해 알맞은 핸들러를 실행할 수 있다. 함수 핸들러를 실행하고 나면 End of interrupt 레지스터에 인터럽트 번호를 써서 인터럽트 처리가 끝났음을 알려준다.
이는 IRQ Exception 발생 시 호출되어야 하는 IRQ Exception Handler 라고 볼 수 있다.
GIC를 사용하기 위한 HAL은 모두 작성하였으나, 이것 만으로는 인터럽트를 처리할 수 없다. IRQ Exception Handler와 Exception Vector Table을 연결하지 않았기 때문이다.
#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);
}
boot 디렉터리 하위에 Handler.c 소스를 작성하였다. __attribute__
는 GCC의 컴파일러 확장 기능을 사용하겠다는 지시어이다. 여러가지 확장 기능 중 __attribute__ ((interrupt ("IRQ")))
와 __attribute__ ((interrupt ("FIQ")))
는 ARM용 GCC의 전용 확장 기능이다. 이들은 IRQ와 FIQ Exception의 핸들러에 진입하는 코드와 복귀하는 코드를 자동으로 생성 해준다. 그리고 함수 내부에서는 HalInterrupt에서 작성한 Hal_interrupt_run_handler()
를 호출하였다.
#include "ARMv7AR.h"
#include "MemoryMap.h"
.text
.code 32
.global vector_start
.global vector_end
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
reset_handler:
...
.end
Entry.S에서 작성한 Exception Vector Table을 작성하는 코드를 수정하여 위 Handler.c에서 작성한 함수의 심볼을 연결해준다. 이후에는 IRQ Exception이 발생하면 Handler.c의 IRQ_Handler()
함수가 실행되고 내부에서 Hal_interrupt_run_handler()
함수가 실행되어 인터럽트를 처리할 수 있다.
이제는 Interrupt를 처리하기 위한 준비가 모두 완료되었다. 해당 파트에서는 UART 입력 인터럽트를 처리하는 과정을 다룬다.
/* Uart.h */
#define UART_INTERRUPT0 44
/* Uart.c */
#include "Uart.h"
#include "HalUart.h"
#include "HalInterrupt.h"
extern volatile PL011_t* Uart;
static void interrupt_handler(void);
void Hal_uart_init(void) {
// enable UART
Uart->uartcr.bits.UARTEN = 0;
Uart->uartcr.bits.TXE = 1; // enable uart output
Uart->uartcr.bits.RXE - 1; // enable uart input
Uart->uartcr.bits.UARTEN = 1;
// enable input interrupt
Uart->uartimsc.bits.RXIM = 1;
// register UART interrupt handler
Hal_interrupt_enable(UART_INTERRUPT0);
Hal_interrupt_register_handler(interrupt_handler, UART_INTERRUPT0);
}
void Hal_uart_put_char(uint8_t ch) {
...
}
uint8_t Hal_uart_get_char(void) {
...
}
static void interrupt_handler(void)
{
uint8_t ch = Hal_uart_get_char();
Hal_uart_put_char(ch);
}
interrupt_handler()
함수를 구현하였는데, 해당 핸들러에서는 문자 하나를 입력받아 다시 출력하는 에코 동작을 수행한다. 그리고 Hal_uart_init()
함수에서 Hal_interrupt 인터페이스를 통해 인터럽트를 활성화하고 핸들러를 등록하였다.
#include "stdint.h"
#include "HalUart.h"
#include "HalInterrupt.h"
#include "stdio.h"
#include "stdlib.h"
#include "stdbool.h"
static void Hw_init(void);
void main(void) {
Hw_init();
while(true); // infinity loop
}
static void Hw_init(void) {
Hal_interrupt_init();
Hal_uart_init();
}
main() 함수에서는 HW를 초기화 하는 동작 이외에는 무한 루프를 돌면서 아무 동작도 수행하지 않는다. HW를 초기화 할 때는 인터럽트 HW를 먼저 초기화해야 이후 UART HW를 초기화하면서 정상적으로 UART 인터럽트를 등록할 수 있다.
프로그램을 실행하면 아무 동작도 수행하지 않는다. 이 때 키보드를 눌러서 문자를 입력하면 인터럽트가 발생하고, UART 인터럽트 핸들러가 호출되어 문자를 입력받고 다시 이를 터미널로 출력한다. 위는 "Hello World! This is UART Interrtup Test ..." 라는 문장을 입력한 결과이다.