본문 바로가기

임베디드

[RTOS 개발하기] 임베디드 OS 개발 프로젝트ch.6

https://www.yes24.com/Product/Goods/84909414

 

임베디드 OS 개발 프로젝트 - 예스24

나만의 임베디드 운영체제를 만들어 보자.이 책은 펌웨어 개발 과정을 실시간 운영체제(RTOS)를 만들어 가며 설명한다. 임베디드 운영체제를 개발 환경 구성에서 시작해 최종적으로 RTOS를 만드는

www.yes24.com

공부 목적으로 이만우님의 저서 "임베디드 OS 개발 프로젝트"를 따라가며, RTOS "Navilos"를 개발하는 포스트입니다. 모든 내용은 "임베디드 OS 개발 프로젝트"에 포함되어 있습니다.

개발 목적

임베디드 시스템에 많이 사용되는 RTOS를 직접 개발해 보며 다음과 같은 지식을 학습하고자 합니다.
 - RTOS의 핵심 개념에 대해 학습한다.
 - 운영체제 핵심 기능을 설계해 보며 학습한다.
 - 펌웨어에 대한 진입장벽을 낮춘다.
 -  ARM 아키텍처에 대해 학습한다.
 - 하드웨어에 대한 지식을 학습한다.(펌웨어가 어떻게 하드웨어를 제어하는지)


이번 챕터는 인터럽트에 관한 내용입니다.

 

인터럽트란?

mpu에서 프로그램을 실행하고있을 때, 입출력하드웨어 등의 장치에 예외상황이 발생하여 처리가 필요할 경우에 마이크로프로세서에게 알려 처리할 수 있도록 하는 것을 말합니다. 임베디드 시스템에서의 인터럽트도 동일하게 작동합니다.

 

인터럽트를 처리하려면 인터럽트 컨트롤러를 어떻게 사용해야 하는지 알아야 합니다. 인터럽트 컨트롤러를 초기화하고 사용하는 코드를 작성해야 합니다. 이후 인터럽트를 발생시키는 하드웨어와 인터럽트 컨트롤러를 연결해야 합니다. 현재 UART 하드웨어만 사용하고 있으니 UART 하드웨어와 인터럽트 컨트롤러를 연결해야 합니다.

  1. UART 하드웨어가 인터럽트 컨트롤러로 인터럽트 신호를 보냄
  2. 인터럽트 컨트롤러는 ARM 코어로 인터럽트를 보냄
  3. 펌웨어에서 cpsr(current program status register)의 IRQ 혹은 FIQ 마스크를 끄면 IRQ나 FIQ가 발생했을 때 코어가 자동으로 익셉션 핸들러를 호출

이를 구현하기 위해 우리가 할 일은 다음과 같습니다.

  1. 인터럽트 컨트롤러 초기화 및 코드 작성
  2. 하드웨어와 인터럽트 컨트롤러 연결
  3. 익셉션 핸들러 작성

 

이번 챕터에서의 목표는

//무한 루프로 종료하지 않는 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개)

Set Enable1, Set Enable2

 

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는 직접 어셈블리어를 사용해야 합니다.

여기엔 두 가지 방법이 있습니다.

  1. Entry.S처럼 어셈블리어 소스 파일 만들고 완전히 어셈블리어로 사용하는 방법
  2. 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 오프셋의 코드입니다.

 

익셉션 핸들러를 만들었으니 익셉션 벡터 테이블에서 연결해주면 됩니다.

더미 핸들러에서 Irq_Handler, Fiq_Handler로 변경

이제 빌드 후 테스트하겠습니다.

테스트 결과

빌드 후 테스트를 진행한 결과입니다. 가장 아랫줄에 “Interrupt works well, This is echo data!!!!”는 QEMU 실행 후 키보드로 입력한 내용입니다.

현재까지 tree

이번 챕터에선 디렉터리를 새로 만들지 않았기 때문에 Makefile은 수정하지 않아도 됩니다.

 


참고

RealViewPB 데이터시트 https://developer.arm.com/documentation/dui0417/d/?lang=en

 

Documentation – Arm Developer

 

developer.arm.com

저자 이만우님 깃허브 https://github.com/navilera/Navilos

 

GitHub - navilera/Navilos: RTOS for various embedded platform

RTOS for various embedded platform. Contribute to navilera/Navilos development by creating an account on GitHub.

github.com