본문 바로가기

임베디드

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

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

 

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

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

www.yes24.com

 

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

개발 목적

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


 

GUI에서 버튼을 눌렀을 때 이벤트가 발생하고 이벤트 핸들러는 버튼이 눌렸을 때 필요한 동작을 실행합니다. RTOS에서의 이벤트도 이와 비슷한 일을 합니다. 어디선가 이벤트가 발생하고 태스크는 이벤트를 받아서 해당 이벤트에 맞는 일을 하는 것입니다.

 

만약 임베디드 시스템에 있는 버튼을 누른다면 다음과 같은 과정으로 이벤트를 처리합니다.

  1. 시스템 내부적으로 버튼에 연결된 스위치에서 전기가 연결됩니다.
  2. 컨트롤러는 해당 전기 신호를 인식합니다.
  3. 이제부터 물리적 전기 신호를 소프트웨어적으로 인터럽트 처리합니다. → ARM이라면 IRQ 혹은 FIQ가 발생하는 것입니다.
  4. IRQ나 FIQ 핸들러에서 인터럽트 컨트롤러의 레지스터를 읽어 어떤 인터럽트인지 확인합니다.
  5. 해당 인터럽트 핸들러를 호출하여 처리합니다.

해당 프로젝트에서는 RTOS 커널이 태스크를 관리하므로 더 유연하게 동작하려면 인터럽트 핸들러의 구체적인 기능을 태스크로 옮기는 것이 더 좋습니다.

이때, 인터럽트와 태스크 간의 연결 매체가 필요한데 이때 사용하는 것이 이벤트입니다.

이벤트 플래그

이벤트를 비트맵으로 만들면 각각의 이벤트를 명확하게 구분할 수 있고, 이벤트를 구분하는 코드를 간단하게 구현할 수 있습니다. 각각의 이벤트 값을 겹치지 않는 비트 위치에 할당합니다. 특정 비트 위치에 독립된 이벤트를 할당해서 이벤트가 있다 없다를 표시하는 방식입니다. 이것은 마치 해당 비트 위치에 깃발을 올렸다 내렸다를 표시하는 것과 같아서 이벤트 플래그 라고 합니다.

이벤트 플래그

위 비트맵을 2진수 숫자로 표현하면, 10000….010입니다. 16진수로 표현하면 0x80000002 입니다. 1번 비트와 31번 비트에 해당하는 이벤트 두 개가 현재 발생해서 처리 대기 중(pending)이란 의미입니다. 이를 처리하는 커널 기능을 만들겠습니다.

 

kernel 디렉터리에 event.c와 event.h 파일을 만들겠습니다.

//kernel/event.h
#ifndef KERNEL_EVENT_H_
#define KERNEL_EVENT_H_

typedef enum KernelEventFalg_t{
        KernelEventFlag_UartIn          = 0x00000001,
        KernelEventFlag_Reserved01      = 0x00000002,
        KernelEventFlag_Reserved02      = 0x00000004,
        KernelEventFlag_Reserved03      = 0x00000008,
        KernelEventFlag_Reserved04      = 0x00000010,
        KernelEventFlag_Reserved05      = 0x00000020,
        KernelEventFlag_Reserved06      = 0x00000040,
        KernelEventFlag_Reserved07      = 0x00000080,
        KernelEventFlag_Reserved08      = 0x00000100,
        KernelEventFlag_Reserved09      = 0x00000200,
        KernelEventFlag_Reserved10      = 0x00000400,
        KernelEventFlag_Reserved11      = 0x00000800,
        KernelEventFlag_Reserved12      = 0x00001000,
        KernelEventFlag_Reserved13      = 0x00002000,
        KernelEventFlag_Reserved14      = 0x00004000,
        KernelEventFlag_Reserved15      = 0x00008000,
        KernelEventFlag_Reserved16      = 0x00010000,
        KernelEventFlag_Reserved17      = 0x00020000,
        KernelEventFlag_Reserved18      = 0x00040000,
        KernelEventFlag_Reserved19      = 0x00080000,
        KernelEventFlag_Reserved20      = 0x00100000,
        KernelEventFlag_Reserved21      = 0x00200000,
        KernelEventFlag_Reserved22      = 0x00400000,
        KernelEventFlag_Reserved23      = 0x00800000,
        KernelEventFlag_Reesrved24      = 0x01000000,
        KernelEventFlag_Reserved25      = 0x02000000,
        KernelEventFlag_Reserved26      = 0x04000000,
        KernelEventFlag_Reserved27      = 0x08000000,
        KernelEventFlag_Reserved28      = 0x10000000,
        KernelEventFlag_Reserved29      = 0x20000000,
        KernelEventFlag_Reserved30      = 0x40000000,
        KernelEventFlag_Reserved31      = 0x80000000,

        KernelEventFlag_Empty           = 0x00000000,
}KernelEvetFlag_t;

void Kernel_event_flag_set(KernelEventFlag_t event);
void Kernel_event_flag_clear(KernelEventFlag_t event);
bool Kernel_event_flag_check(KernelEventFlag_t event);

#endif /* KERNEL_EVENT_H_ */

32비트로는 이벤트 플래그 32개를 표시할 수 있으므로, 32개의 이벤트 플래그 자리를 잡아두었습니다. 우선은 UartIn 이벤트만 정의해 두었습니다.

//kernel/event.c
#include "stdint.h"
#include "stdbool.h"

#include "stdio.h"
#include "event.h"

static uint32_t sEventFlag;

void Kernel_event_flag_init(void){
        sEventFlag = 0;
}

void Kernel_event_flag_set(KernelEventFlag_t event){
        sEventFlag |= (uint32_t)event;
}

void Kernel_event_flag_clear(KernelEventFlag_t event){
        sEventFlag &= ~((uint32_t)event);
}

bool Kernel_event_flag_check(KernelEventFlag_t event){
        if(sEventFlag & (uint32_t)event){
                Kernel_event_flag_clear(event);
                return true;
        }
        return false;
}

각 함수의 역할은

  • Kernel_event_flag_init()은 sEventFlag를 0으로 초기화하는 역할을 합니다.
  • Kernel_event_flag_set()과 Kernel_event_flag_clear()은 각각 sEventFlag의 특정 비트를 1로 바꾸거나 0으로 바꾸는 역할을 합니다.
  • Kernel_event_flag_check()는 파라미터로 전달된 이벤트가 1로 세팅되어 있는지 확인합니다. 만약 1이라면 sEventFlag에서 해당 비트를 제거하고, true 를 리턴해 호출자에게 해당 이벤트가 대기 중임을 알립니다.

다음으로는 태스크에서 커널 API를 통해 이벤트를 처리할 수 있도록 함수를 더 추가하겠습니다.

//kernel/Kernel.c에 api 추가
void Kernel_send_events(uint32_t event_list){
        for (uint32_t i = 0; i < 32; i++){
                if((event_list >> i) & 1){
                        KernelEventFlag_t sending_event = KernelEventFlag_Empty;
                        sending_event = (KernelEventFlag_t)SET_BIT(sending_event, i);
                        Kernel_event_flag_set(sending_event);
                }
        }
}

KernelEventFlag_t Kernel_wait_events(uint32_t waiting_list)
{
    for (uint32_t i = 0 ; i < 32 ; i++)
    {
        if ((waiting_list >> i) & 1)
        {
            KernelEventFlag_t waiting_event = KernelEventFlag_Empty;
            waiting_event = (KernelEventFlag_t)SET_BIT(waiting_event, i);

            if (Kernel_event_flag_check(waiting_event))
            {
                return waiting_event;
            }
        }
    }

    return KernelEventFlag_Empty;
}

Kernel_send_events()는 이벤트를 보내는 함수입니다. 누가 받아서 처리하는 지는 모릅니다. 그래야 다른 코드와의 커플링을 최소화할 수 있습니다.

Kernel_wait_events()는 기다리는 함수입니다. 마찬가지로 누가 보냈는지 모릅니다.

인터럽트와 이벤트

해당 프로젝트는 QEMU 환경이기에 많은 인터럽트를 사용할 수 없습니다. 그리고 프로젝트 내에서도 UART와 타이머만 사용할 수 있습니다. UART의 기능을 이벤트 핸들러로 옮기는 작업을 하겠습니다.

먼저 UART 인터럽트 핸들러를 수정하겠습니다.

//uart.c 수정
static void interrupt_handler(void)
{
    uint8_t ch = Hal_uart_get_char();
    Hal_uart_put_char(ch);

    Kernel_send_events(KernelEventFlag_UartIn);
}

테스트를 위해 Main.c를 수정하겠습니다.

//User_task0수정
void User_task0(void){
        debug_printf("User Task #0\\n");
        uint32_t local = 0;

        debug_printf("User Task#0 SP=0x%x\\n", &local);

        while(true)
    {
        KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_UartIn);
        switch(handle_event)
        {
        case KernelEventFlag_UartIn:
            debug_printf("\\nEvent handled by Task0\\n");
            break;
        }
        Kernel_yield();
    }
}

빌드 후 테스트하니 다음과 같습니다.

테스트 결과

키보드를 누르니 Event handled by Task0을 출력합니다.

 

지금까지 UART 입력에 KernelEventFalg_UartIn 이벤트를 연결했고, Task0에서 이 이벤트를 받았다는 것을 알려주는 출력을 하여 이벤트를 처리하였습니다.


사용자 정의 이벤트

이번엔 사용하지 않는 이벤트 플래그 하나에 이름을 붙히고 Task0에서 보내겠습니다. 그리고 Task1에서 그 이벤트를 받아보겠습니다.

 

우선 Reserved01 이벤를 CmdIn으로 변경하겠습니다.

이벤트 변수명 변경

그리고 Main.c를 변경하겠습니다.

//Main.c Task0 Task1 변경
void User_task0(void)
{
    uint32_t local = 0;

    debug_printf("User Task #0 SP=0x%x\\n", &local);

    while(true)
    {
        KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_UartIn);
        switch(handle_event)
        {
        case KernelEventFlag_UartIn:
            debug_printf("\\nEvent handled by Task0\\n");
                        Kernel_send_events(KernelEventFlag_CmdIn);
            break;
        }
        Kernel_yield();
    }
}

void User_task1(void)
{
    uint32_t local = 0;

    debug_printf("User Task #1 SP=0x%x\\n", &local);

    while(true)
    {
        KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_CmdIn);
        switch(handle_event)
        {
        case KernelEventFlag_CmdIn:
            debug_printf("\\nEvent handled by Task1\\n");
            break;
        }
        Kernel_yield();
    }
}

Task0에서 UartIn 인터럽트를 처리하면 CmdIn 인터럽트를 발생시키고, Task1에서 CmdIn을 처리합니다. 한 번 실행해보겠습니다.

테스트 결과

예상했던대로 처리되는 것을 확인할 수 있었습니다.


여러 이벤트 플래그를 동시에 보내고 처리하기

이벤트 플래그를 설계할 때 비트맵을 사용한 가장 큰 이유는 이벤트 플래그를 동시에 여러개 보내고 받을 수 있게끔 코딩하기 위해서 입니다.

 

이벤트 플래그를 하나 더 만들겠습니다.

이벤트 플래그 이름 변경

앞서 이야기하였지만, 이벤트 플래그의 이름은 딱히 의미는 없습니다. 그냥 CmdOut으로 정한 것 뿐입니다.

이제 동시에 여러 이벤트를 처리할 수 있도록 UART 핸들러를 수정하겠습니다.

//hal/rvpb/Uart.c의 intterupt_handler() 수정
static void interrupt_handler(void)
{
        uint8_t ch = Hal_uart_get_char();
        Hal_uart_put_char(ch);

        Kernel_send_events(KernelEventFlag_UartIn|KernelEventFlag_CmdIn);

        if(ch == 'X'){
                Kernel_send_events(KernelEventFlag_CmdOut);
        }
}

Uart인터럽트가 발생하면, or 연산을 통해 KernelEventFlag_UartIn과 KernelEventFlag_CmdIn을 동시에 보냅니다. 그리고 ‘X’가 입력되었을 때만 KernelEventFlag_CmdOut 이벤트를 추가로 발생시킵니다.

이제 다시 Main.c에 Task0을 수정하겠습니다.

void User_task0(void)
{
    uint32_t local = 0;

    debug_printf("User Task #0 SP=0x%x\\n", &local);

    while(true)
    {
        KernelEventFlag_t handle_event = Kernel_wait_events(KernelEventFlag_UartIn|KernelEventFlag_CmdOut);
        switch(handle_event)
        {
        case KernelEventFlag_UartIn:
            debug_printf("\\nEvent handled by Task0\\n");
            break;
        case KernelEventFlag_CmdOut:
            debug_printf("\\nCmdOut Event by Task0\\n");
            break;
        }
        Kernel_yield();
    }
}

이제 다시 빌드 후 실행해보겠습니다.

테스트 결과

a b c d를 눌렀을 땐 KernelEventFalg_UartIn과 KernelEventFlag_CmdIn이 동시에 보내지고, X를 눌렀을 땐 여기에 KernelEventFlag_CmdOut까지 함께 보내지는 것을 확인할 수 있습니다.

GUI에서 버튼을 눌렀을 때 이벤트가 발생하고 이벤트 핸들러는 버튼이 눌렸을 때 필요한 동작을 실행합니다. RTOS에서의 이벤트도 이와 비슷한 일을 합니다. 어디선가 이벤트가 발생하고 태스크는 이벤트를 받아서 해당 이벤트에 맞는 일을 하는 것입니다.


마치며

이벤트 기능을 만들어 보았습니다. 이벤트 플래그를 비트맵으로 만들어 각 태스크가 이벤트를 보내고 받도록 하였습니다. 이벤트는 태스크 간 정보 전달뿐 아니라 인터럽트 핸들러에서 정보를 전달할 때도 유용하게 쓸 수 있습니다.

 


참고

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