https://www.yes24.com/Product/Goods/84909414
공부 목적으로 이만우님의 저서 "임베디드 OS 개발 프로젝트"를 따라가며, RTOS "Navilos"를 개발하는 포스트입니다. 모든 내용은 "임베디드 OS 개발 프로젝트"에 포함되어 있습니다.
개발 목적
임베디드 시스템에 많이 사용되는 RTOS를 직접 개발해 보며 다음과 같은 지식을 학습하고자 합니다.
- RTOS의 핵심 개념에 대해 학습한다.
- 운영체제 핵심 기능을 설계해 보며 학습한다.
- 펌웨어에 대한 진입장벽을 낮춘다.
- ARM 아키텍처에 대해 학습한다.
- 하드웨어에 대한 지식을 학습한다.(펌웨어가 어떻게 하드웨어를 제어하는지)
이번 챕터는 인터럽트에 관한 내용입니다.
인터럽트란?
mpu에서 프로그램을 실행하고있을 때, 입출력하드웨어 등의 장치에 예외상황이 발생하여 처리가 필요할 경우에 마이크로프로세서에게 알려 처리할 수 있도록 하는 것을 말합니다. 임베디드 시스템에서의 인터럽트도 동일하게 작동합니다.
인터럽트를 처리하려면 인터럽트 컨트롤러를 어떻게 사용해야 하는지 알아야 합니다. 인터럽트 컨트롤러를 초기화하고 사용하는 코드를 작성해야 합니다. 이후 인터럽트를 발생시키는 하드웨어와 인터럽트 컨트롤러를 연결해야 합니다. 현재 UART 하드웨어만 사용하고 있으니 UART 하드웨어와 인터럽트 컨트롤러를 연결해야 합니다.
- UART 하드웨어가 인터럽트 컨트롤러로 인터럽트 신호를 보냄
- 인터럽트 컨트롤러는 ARM 코어로 인터럽트를 보냄
- 펌웨어에서 cpsr(current program status register)의 IRQ 혹은 FIQ 마스크를 끄면 IRQ나 FIQ가 발생했을 때 코어가 자동으로 익셉션 핸들러를 호출
이를 구현하기 위해 우리가 할 일은 다음과 같습니다.
- 인터럽트 컨트롤러 초기화 및 코드 작성
- 하드웨어와 인터럽트 컨트롤러 연결
- 익셉션 핸들러 작성
이번 챕터에서의 목표는
//무한 루프로 종료하지 않는 main()함수
void main(void)
{
Hw_init();
uint32_t i = 100;
while(i--){
Hal_uart_put_char('N');
}
Hal_uart_put_char('\n');
putstr("Hello World\n");
Printf_test();
while(true)
}
이렇게 무한 루프를 돌고 있는 와중에 키보드 입력에 반응하도록 만드는 것입니다.
인터럽트 컨트롤러
RealViewPB의 인터럽트 컨트롤러 하드웨어인 Generic Interrupt Controller(GIC)를 사용할 것입니다. 먼저 GIC의 레지스터 구조체를 만들어야 합니다. GIC는 RealViewPB에 포함된 인터럽트 컨트롤러 이므로 hal/rvpb 디렉터리에 PL011과 같은 레벨로 구조체를 작성해야 합니다.
//hal/rvpb/Interrupt.h
#ifndef HAL_RVPB_INTERRUPT_H_
#define 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 union BinaryPoint_t
{
uint32_t all;
struct {
uint32_t Binarypoint:3; // 2:0
uint32_t reserved:29;
} bits;
} BinaryPoint_t;
typedef union InterruptAck_t
{
uint32_t all;
struct {
uint32_t InterruptID:10; // 9:0
uint32_t CPUsourceID:3; // 12:10
uint32_t reserved:19;
} bits;
} InterruptAck_t;
typedef union EndOfInterrupt_t
{
uint32_t all;
struct {
uint32_t InterruptID:10; // 9:0
uint32_t CPUsourceID:3; // 12:10
uint32_t reserved:19;
} bits;
} EndOfInterrupt_t;
typedef union RunningInterrupt_t
{
uint32_t all;
struct {
uint32_t Reserved:4; // 3:0
uint32_t Priority:4; // 7:4
uint32_t reserved:24;
} bits;
} RunningInterrupt_t;
typedef union HighestPendInter_t
{
uint32_t all;
struct {
uint32_t InterruptID:10; // 9:0
uint32_t CPUsourceID:3; // 12:10
uint32_t reserved:19;
} bits;
} HighestPendInter_t;
typedef union DistributorCtrl_t
{
uint32_t all;
struct {
uint32_t Enable:1; // 0
uint32_t reserved:31;
} bits;
} DistributorCtrl_t;
typedef union ControllerType_t
{
uint32_t all;
struct {
uint32_t IDlinesnumber:5; // 4:0
uint32_t CPUnumber:3; // 7:5
uint32_t reserved:24;
} bits;
} ControllerType_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는 레지스터를 크게 다음 두 그룹으로 구분합니다.
- CPU Interface registers
- Distributor registers
실제 GIC 코드는 위에 작성한 레지스터 보다 훨씬 많은 레지스터를 갖고 있지만, 대부분 사용하지 않기에 적당한 수준에서 추상화할 레지스터 일부만 구현하였습니다.
이제 레지스터 구조를 선언했으니 실제 인스턴스를 선언하기 위해 hal/rvpb/Regs.c 파일을 수정해야 합니다.
//hal/rvpb/Regs.c
#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;
이제 공용 API를 설계하겠습니다.
//hal/HalInterrupt.h
#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, uint32_t interrupt_num);
void Hal_interrupt_run_handler(void);
#endif /* HAL_HALINTERRUPT_H_ */
ARM은 모든 인터럽트를 IRQ와 FIQ 핸들러로 처리하므로 IRQ와 FIQ 핸들러에서 개별 인터럽트의 핸들러를 구분해야 합니다. → 개별 인터럽트 핸들러 구분하여 실행하는 함수
Hal_interrupt_run_handler()
본격적인 코드입니다.
//hal/rvpb/Interrupt.c
extern volatile GicCput_t* GicCpu;
extern volatile GicDist_t* GicDist;
static InterHdlr_fptr sHandlers[INTERRUPT_HANDLER_NUM];
void Hal_interrupt_init(void){
GicCpu->cpucontrols.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();
}
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;
}
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;
}
- 7,8 레지스터 제어 인스턴스 선언
- 10, 인터럽트 핸들러 저장해둔 변수
- INTERRUPT_HANDLER_NUM 헤더 파일 4번째 줄에서 255로 설정함 → 함수 포인터 255개를 저장할 수 있는 배열
- 12-24, Hal_interrupt_init()
- Enable → 스위치 켜는 동작만함
- PriorityMask → ARM 인포센터 참고, 4-7번 비트만 사용 → 인터럽트를 막도록 마스크하는 레지스터, 우린 인터럽트 우선순위를 설정할 것은 안니니 일단 모든 인터럽트 허용
- enable_irq() → irq 켜는 함수, 아직 작성 안함
- 26-64, 개별 인터럽트를 켜고 끄는 함수들
- SET_BIT, CLR_BIT은 오프셋 비트를 1로 설정하거나 0으로 클리어하는 매크로
- 66-81, 인터럽트 핸들러 처리 방법
#define SET_BIT(p, n) ((p) |= (1 << (n)))
#define CLR_BIT(p, n) ((p) &= ~(1 << (n)))
이렇게 구현할 수 있습니다.
GIC는 인터럽트 64개를 관리할 수 있기에 Set Enable1, 2로 반씩 나누어 주었습니다. (32비트 2개)
cspr의 IRQ, FIQ 마스크를 끄고 켜는 코드
//lib/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 */
cpsr을 사용하기 위해서는 어셈블리어를 사용할 수 밖에 없습니다. ARMCC는 컴파일러 빌트인 변수로 cspr을 접근할 수 있지만, GCC는 직접 어셈블리어를 사용해야 합니다.
여기엔 두 가지 방법이 있습니다.
- Entry.S처럼 어셈블리어 소스 파일 만들고 완전히 어셈블리어로 사용하는 방법
- C언어 소스 파일 만들고 C 언어 함수에서 인라인 어셈블리어를 사용하는 방법
→ 두 방법 중 어느것도 상관없지만, 여기서는 인라인 어셈블리어를 사용하겠습니다.
//lib/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 disable_irq(void)
{
__asm__ ("PUSH {r0, r1}");
__asm__ ("MRS r0, cpsr");
__asm__ ("BIC r1, r0, #0x40");
__asm__ ("MSR cpsr, r1");
__asm__ ("POP {r0, r1}");
}
void enable_fiq(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}");
}
enable_irq()를 어셈블리어 코드로 작성하면 어떻게 작성할까?
enable_irq()함수를 빌드 후 오브젝트 파일을 역 어셈블하면 알 수 있습니다.
00000000 <enable_irq>:
0: e52db004 push {fp}
4: e28db000 add fp, sp, #0
... 중략
20: e49db004 pop {fp}
24: e12fff1e bx lr
UART 입력과 인터럽트
GIC 설정은 했지만, 이것만으로 인터럽트를 활용할수는 없습니다. 왜냐하면 인터럽트를 발생시키는 하드웨어와 연결되어 있지 않기 때문입니다. 가장 먼저 작업할 하드웨어는 UART입니다.
먼저 하드웨어 의존적인 UART 코드가 있는 Uart.c 파일의 Hal_uart_init() 함수를 수정합니다.
//hal/rvpb/Uart.c
#include "stdint.h"
#include "Uart.h"
#include "HalUart.h"
#include "HalInterrupt.h"
extern volatile PL011_t* Uart;
void Hal_uart_init(void){
//Enable UART
Uart->uartcr.bits.UARTEN = 0;
Uart->uartcr.bits.TXE = 1;
Uart->uartcr.bits.RXE = 1;
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){
while(Uart->uartfr.bits.TXFF);
Uart->uartdr.all = (ch & 0xFF);
}
uint8_t Hal_uart_get_char(void){
uint8_t data;
while(Uart->uartfr.bits.RXFE);
data = Uart->uartdr.all;
//Check for an error flag
if(data & 0xFFFFFF00){
//Clear the error
Uart->uartrsr.all = 0xFF;
return 0;
}
return (uint8_t)(data & 0xFF);
}
static void interrupt_handler(void)
{
uint8_t ch = Hal_uart_get_char();
Hal_uart_put_char(ch);
}
init에 IRQ ID 44번을 켜고, interrupt_handler()함수와 interrupt_handler()함수를 핸들러와 연결하는 코드를 추가했습니다. interrupt_handler()는 이전에 main()에서 100번 한정으로 입력을 할 수 있도록 했던 에코 코드입니다. 지금은 인터럽트로 처리하기 때문에 제한이 없습니다.
여기까지 UART와 인터럽트를 연결하는 작업을 하여습니다. 이제 인터럽트와 UART가 독립되어 있지 않고 연결되었으므로 초기화 순서를 맞춰야 합니다. Main.c 파일에서 하드웨어 초기화 코드를 수정하겠습니다.
//Main.c의 Hw_init()
static void Hw_init(void)
{
Hal_interrupt_init();
Hal_uart_init();
}
여기서 중요한 것은 순서입니다. 인터럽트 컨트롤러를 UART 보다 먼저 호출해야 hal_uart_init() 함수 인터럽트 관련 함수를 호출하므로 인터럽트 컨트롤러를 먼저 초기화해야 합니다.
지금껏 작업한 내용 정리
- main() 함수를 무한 루프로 종료하지 않게 변경
- 인터럽트 컨트롤러 초기화
- cspr의 IRQ 마스크 해제
- UART 인터럽트 핸들러를 인터럽트 컨트롤러에 등록
- 인터럽트 컨트롤러와 UART 하드웨어 초기화 순서 조정
해당 프로젝트에선 IRQ만 사용합니다.
IRQ 익셉션 벡터 연결
이제 마지막으로 한 작업만 남았는데 익셉션 벡터 테이블의 IRQ 익셉션 벡터와 인터럽트 컨트롤러의 인터럽트 핸들러를 연결하는 작업입니다.
먼저 익셉션 핸들러부터 만들겠습니다. boot/Handler.c파일을 만들어 줍니다.
//boot/Handler.c
#include "stdbool.h"
#include "stdint.g"
#include "HalInterrupt.h"
__attribute__ ((interrupt ("IRQ"))) void Irq_Handler(void)
{
Hal_interrupt_run_handler();
}
__attribute__ ((interrupt ("FIQ"))) void Fiq_Handler(void)
{
while(true);
}
IRQ 익셉션 핸들러와 FIQ 익셉션 핸들러만 작성하였습니다. (FIQ 익셉션 핸들러는 더미)
__attribute__ 는 GCC의 컴파일러 확장 기능을 사용하겠다는 지시어입니다.
__attrubute__ ((interrupt ("IRQ")) 와 __attrubute__ ((interrupt ("FIQ")) 는 ARM용 GCC 전용 확장 기능
→ IRQ와 FIQ 핸들러에 진입과 나가는 코드를 자동으로 만들어줍니다.(A.1절 참고)
IRQ_Handler에서 중요한 것은 0번 오프셋과 가장 마지막 0x14 오프셋의 코드입니다.
익셉션 핸들러를 만들었으니 익셉션 벡터 테이블에서 연결해주면 됩니다.
이제 빌드 후 테스트하겠습니다.
빌드 후 테스트를 진행한 결과입니다. 가장 아랫줄에 “Interrupt works well, This is echo data!!!!”는 QEMU 실행 후 키보드로 입력한 내용입니다.
이번 챕터에선 디렉터리를 새로 만들지 않았기 때문에 Makefile은 수정하지 않아도 됩니다.
참고
RealViewPB 데이터시트 https://developer.arm.com/documentation/dui0417/d/?lang=en
저자 이만우님 깃허브 https://github.com/navilera/Navilos
'임베디드' 카테고리의 다른 글
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.8 (0) | 2024.11.11 |
---|---|
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.7 (1) | 2024.11.11 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.5 (6) | 2024.11.04 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.4 (3) | 2024.10.31 |
[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.3 (1) | 2024.10.19 |