본문 바로가기

Firmware/RTOS

임베디드 OS 개발 프로젝트 10(TASK)

.
├── boot
│   ├── Entry.S
│   ├── Handler.c
│   └── Main.c
├── hal
│   ├── HalInterrupt.h
│   ├── HalTimer.h
│   ├── HalUart.h
│   └── rvpb
│       ├── Interrupt.c
│       ├── Interrupt.h
│       ├── Regs.c
│       ├── Timer.c
│       ├── Timer.h
│       ├── Uart.c
│       └── Uart.h
├── include
│   ├── ARMv7AR.h
│   ├── memio.h
│   ├── MemoryMap.h
│   ├── stdarg.h
│   └── stdint.h
├── kernel
│   ├── task.c
│   └── task.h
├── lib
│   ├── armcpu.c
│   ├── armcpu.h
│   ├── stdio.c
│   ├── stdio.h
│   ├── stdlib.c
│   └── stdlib.h
├── Makefile
└── navilos.ld

 

 

 

본격적으로 RTOS를 만들기 위해 책에서는 따로 KERNEL이란 파일을 만들어 주었다.

 

Task란 운영체제에서 프로그램 그 자체를 봐도 된다. 태스크가 바뀐다는건 동작하는 프로그램이 바뀐다는 말이다.

 

이처럼 Task가 바뀌는 것을 Switching 되었다라고 표현한다. 

 

Task가 Switching 할 때 사용중이던 Task의 정보를 기록해 놓아야 하는데 이때

 

Task의 현재 상태 정보를 Context 라고 한다.

 

1. 메모리 구조

 

이전에 직접 짰던 Stack 구조이다

 

아래 그림과 같이 Task stack은 0x00800000 로 시작하는 걸 알 수 있다.

 

 

실제 코드

 

#ifndef KERNEL_TASK_H_
#define KERNEL_TASK_H_

#include "MemoryMap.h"  // TASK_STACK_SIZE 

#define NOT_ENOUGH_TASK_NUM 0xFFFFFFFF

#define USR_TASK_STACK_SIZE 0x100000     //1MB
#define MAX_TASK_NUM       (TASK_STACK_SIZE / USR_TASK_STACK_SIZE)    

// TASK_STACK_SIZE = (GLOBAL_ADDR_START - TASK_STACK_START) = 0x04800000 - 0x00800000


typedef struct KernelTaskContext_t
{
    uint32_t spsr;
    uint32_t r0_r12[13];
    uint32_t pc;
} KernelTaskContext_t;

typedef struct KernelTcb_t
{
    uint32_t sp;
    uint8_t* stack_base;
}KernelTcb_t;

typedef void (*KernelTaskFunc_t) (void);

void Kernel_task_init(void);
uint32_t Kernel_task_create(KernelTaskFunc_t startFunc);

#endif /* KERNEL_TASK_H_ */

 

그림에서 Code region이 코드에 주석 처리 되어있는 GLOBAL_ADDR_START 이다.

 

task의 context는 아래의 정보를 가지고 있다.

 

1. 현재 TASK가 실행되고 있는 동작모드(SPSR 레지스터) 

2. R0 ~ R12 까지 레지스터

3. Program Counter

 

정의 된 USR_TASK_STACK_SIZE 는 1MB 이고

 

Task Stack size는 64MB 이므로 최대 64개의 TASK를 구현 할 수 있다.

 

 

Kernel_task_init()

static KernelTcb_t sTask_list[MAX_TASK_NUM];
static uint32_t sAllocated_tcb_index;

void Kernel_task_init(void)
{
    sAllocated_tcb_index = 0;

    for(uint32_t i = 0; i<MAX_TASK_NUM; i++)
    {
        sTask_list[i].stack_base = (uint8_t*)(TASK_STACK_START + (i*USR_TASK_STACK_SIZE));         //STACK Base calculate
        sTask_list[i].sp         = (uint32_t)sTask_list[i].stack_base + USR_TASK_STACK_SIZE - 4;   //STACK allocate

        sTask_list[i].sp -= sizeof(KernelTaskContext_t);                         // size of Context
        KernelTaskContext_t* ctx = (KernelTaskContext_t*)sTask_list[i].sp;       // stack에 Context 저장
        ctx ->pc = 0;
        ctx ->spsr = ARM_MODE_BIT_SYS;
    }
}


Kernel_task_init 함수는 말 그대로 초기화 하는 함수이다.

 

앞서 task.h 에서 선언한거 같이 MAX_TASK_NUM은 64개이고

 

for 문을 통하여 할당 된 stack 시작점, SP 레지스터, PC 레지스터, SPSR 레지스터를 모두 지정해준다.

 

위 코드를 통해 초기화한 Task Stack 구조는 다음 그림과 같다.(책 140p 참조)

 

위 그림이 이해 되었다면 GDB에서 할당된 Task_list 역시 각각 1MB씩 메모리가 할당 되었음을 알 수 있다.

 

 

 

초기화가 되었으므로 이제 Task를 만들어 보자

 

Kernel_Task_create

uint32_t Kernel_task_create(KernelTaskFunc_t startFunc)
{
    KernelTcb_t* new_tcb = &sTask_list[sAllocated_tcb_index++];

    if (sAllocated_tcb_index > MAX_TASK_NUM)
    {
        return NOT_ENOUGH_TASK_NUM;
    }

    KernelTaskContext_t* ctx = (KernelTaskContext_t*)new_tcb->sp;
    ctx->pc = (uint32_t)startFunc;

    return (sAllocated_tcb_index - 1);   //29line에서 ++해줬으므로 다시 -1 해준다.
}

 

new_tcb는 task control block으로 Task list에서 사용하지 않은 task control block 객체를 하나 가지고 온다.

 

그 다음 sAllocated_tcb_index++하여 다음 create 호출 시 사용하지 않은 task를 가지고 올 수 있게 한다.

 

KernelTaskContext_t* ctx = (KernelTaskContext_t*)new_tcb->sp;

 

이 코드는 ctx를 변수라 생각하지말고 그냥 메모리 주소라 생각하면 이해하기가 쉽다.

 

Task Context 를 기록하는 메모리 주소(ctx)에 new_tcb ->sp 를 가지고온다.

 

위 그림에서 보면 new_tcb -> sp는 Task Context를 나타내고 있다.

 

따라서 ctx 에 현재 만들어진 task의 context를 가지고온다.

 

ctx->pc = (uint32_t)startFunc;

 

가지고 온 task context에 등록된 함수 주소를 가지고 온다.

 

따라서 task가 실행하게 되면 등록된 함수를 실행 하게 된다.

 

main.c 를 따라가면서 흐름을 보자.

 

int main(void)
{
    Hw_init();

    uint32_t length;
    putstr("Hello world!\n");
    Kernel_task_init();
    Kernel_init();

    printf_test();
   while(true);
}

static void Kernel_init(void)
{
    uint32_t taskId;

    taskId = Kernel_task_create(User_task0);
    if (NOT_ENOUGH_TASK_NUM == taskId)
    {
        putstr("Task0 creation fail\n");
    }

    taskId = Kernel_task_create(User_task1);
    if (NOT_ENOUGH_TASK_NUM == taskId)
    {
        putstr("Task1 creation fail\n");
    }

    taskId = Kernel_task_create(User_task2);
    if (NOT_ENOUGH_TASK_NUM == taskId)
    {
        putstr("Task2 creation fail\n");
    }
}


void User_task0(void)
{
    debug_printf("User Task #0\n");

    while(true);
}

void User_task1(void)
{
    debug_printf("User Task #1\n");

    while(true);
}

void User_task2(void)
{
    debug_printf("User Task #2\n");

    while(true);
}

 

main 함수에 보면 Kernel_init 함수를 호출하여 총 4개의 task를 만든다.

(User_task0, User_task1, User_task2, User_task3, User_task4)

 

4개의 함수를 만들었지만 현재 스케줄러는 만들지 않았으므로 while(true)만 돈다.

 

여기서 위에서 언급한 ctx 주소를 출력해 보면 3개의 Task가 정상적으로 등록 되어 있음을 알 수 있다.

(GDB에서 ctx 가 전역변수로 선언되지 않았으므로 단순하게 ctx 출력하면 없다고 에러가 뜬다. 따라서 create 함수 내부에서 ctx 메모리 주소를 확인 후 외부에서 확인 할 때 메모리 주소를 직접 출력하여 확인한다.)

 

Task 생성 후 메모리 할당.

 

Kernel init 함수 종료 후 GDB를 통해 Task가 정상적으로 할당 된걸 확인 할 수 있다.

 

1번째 ctx 메모리를 살펴보면

 

SYS 모드(0x1f), 레지스터R0~R12가 할당 된걸 확인 할 수 있고

 

Program Counter Register가 0x18c 즉 User_task0 에 정상적으로 할당 된걸 알 수 있다.

 

나머지 ctx 역시 동일하게 sys모드로 되어 있으며 다만 코딩 한대로 User_task1, User_task2로 

 

Program Counter만 다르게 들어가 있다.