본문 바로가기

Firmware/RTOS

임베디드 OS 개발 프로젝트 12(Context Switching - 2)

.
├── 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
│   ├── Kernel.c
│   ├── Kernel.h
│   ├── task.c
│   └── task.h
├── lib
│   ├── armcpu.c
│   ├── armcpu.h
│   ├── stdio.c
│   ├── stdio.h
│   ├── stdlib.c
│   └── stdlib.h
├── Makefile
└── navilos.ld

 

이전 작업에서 Context Switching 함수까지 만들었으므로 이제 Kernel을 시작하면 된다.

 

main

int main(void)
{
    Hw_init();

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

   // printf_test();
    //Timer_test();
    //length = getstr();


   while(true);
}

간단하다. 

 

Kernel_task_init 에서 task 메모리를 초기화 해주고 Kernel_init에서 task를 만든후 마지막에 Kernel Start를 호출한다.

 

Kernel_task_init은 바뀐게 없으므로 생략한다.

 

Kernel_init

static void Kernel_init(void)
{
    uint32_t taskId;

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

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

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

    Kernel_start();

}

이전 코드에서 바뀐건 없다.

마지막 Kernel_start를 추가하여 Kernel을 시작한다.

 

Kernel_start

void Kernel_start(void)
{
    Kernel_task_start();
}


void Kernel_task_start(void)
{
    sNext_tcb = &sTask_list[sCurrent_tcb_index];
    Restore_context();
}

Kernel_start 함수를 통해 Kernel_task_start를 호출한다.

Kernel_task_start에서는 sNext_tcb에 실행할 Task 를 가지고 온다음

 

Restore_context를 통해 task를 실행한다.

 

실행하게 될 task 코드는 간단하다.

void User_task0(void)
{
    uint32_t local = 0;

    while(true)
    {
        debug_printf("User Task #0\n", &local);
        delay(1000);
        Kernel_yield();
    }
}

void User_task1(void)
{
      int32_t local = 0;

    while(true)
    {
        debug_printf("User Task #1\n", &local);
        delay(1000);
        Kernel_yield();
    }

}

void User_task2(void)
{
    int32_t local = 0;

    while(true)
    {
        debug_printf("User Task #2\n", &local);
        delay(1000);
        Kernel_yield();
    }

}

while문을 통해  terminal 에서 자신이 몇번 째 Task인지 출력한다.

 

local 인자가 붙는데 출력하기 위한 %x 넣는걸 깜빡했다.

 

어차피 gdb를 통해 볼 수 있으므로 컴파일 후 진행함.

 

Kernel_yield

void Kernel_yield(void)
{
    Kernel_task_scheduler();
}


void Kernel_task_scheduler(void)
{
    sCurrent_tcb = &sTask_list[sCurrent_tcb_index];
    sNext_tcb = Scheduler_round_robin_algorithm();

    Kernel_task_context_switching();
}

__attribute__ ((naked)) void Kernel_task_context_switching(void)
{
    __asm__ ("B Save_context");
    __asm__ ("B Restore_context");
}

 Kernel_yield를 통해 Kernel_task_scheduler를 호출한다.

 

Kernel_task_shceduler에서 round_robin_algorithm을 통해 다음에 실행 할 Task를 정한 후

 

Context switching을 한다.

 

Round robin algorithm은 간단하므로 여기서는 디버깅하지 않고 task가 switching 하는 부분을 제대로 분석해보자.

 

Task가 Switching 하기 위해서는 우선 Task의 정보를 Save를 해야한다.

 

Save_context

static __attribute__ ((naked)) void Save_context(void)
{
    // save current task context into the current task stack
    __asm__ ("PUSH {lr}");  //__attribute__ 가 없으면 lr 이 현재 Task의 lr 이 아닌 Save_context의 lr이 들어가게 된다.
    __asm__ ("PUSH {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}");
    __asm__ ("MRS   r0, cpsr");  //current program status register
    __asm__ ("PUSH {r0}");
    // save current task stack pointer into the current TCB
    __asm__ ("LDR   r0, =sCurrent_tcb");  //r0에 sCurrent_tcb의 값을 복사한다.
    __asm__ ("LDR   r0, [r0]");    //r0에 sCurrent_tcb의 Stack pointer 값을 복사한다.
    __asm__ ("STMIA r0, {sp}");    //sCurrent_tcb의 stack ponter 값을 갱신한다.
}

__asm__("push {lr}") 부터 __asm__("push {r0}") 까지는 stack에 레지스터 값을 push 하는 코드이므로 어렵지 않다.

 

gdb registers

실제 레지스터 값과

 

stack memory

stack 메모리를 확인해보면 Stack에 정상적으로 push 된걸 알 수 있다.

 

마지막 cpsr 값까지 push 한 stack의 값은 0x8fffa0인데 sCurrent_tcb -> sp 값은 0x8fffc0 이지만

 

Task 레지스터를 push 하고 난 다음 sp 값은 0x8fffa0 이다 (스택이 Top에서 bottom으로 증가)

 

나중에 load를 하기 위해서는 sCurrent_tcb -> sp 값을 업데이트 해줘야 한다.

 

현 sp 값을 읽어 오기 위해 sCurrent_tcb 주소를 load 한다. (LDR r0, =sCurrent_tcb)

 

Kerneltcb 구조체의 1번째 member는 sp 이므로 따로 계산 없이 sCurrent_tcb가 나타내는 값을 호출한다.(LDR r0, [r0])

 

Task의 sp 값을 갱신한다. (STMIA r0, {sp})

 

sp 갱신 후 gdb를 통해 갱신 확인

sp가 갱신 됨으로 써 사용중인 Task back up이 완료 되었다.

 

Restore_context

static __attribute__ ((naked)) void Restore_context(void)
{
    // restore next task stack pointer from the next TCB
    __asm__ ("LDR   r0, =sNext_tcb"); // 다음에 실행할 Task의 tcb를 r0에 load한다.
    __asm__ ("LDR   r0, [r0]");  //r0의 stack pointer를 load한다.
    __asm__ ("LDMIA r0!, {sp}"); //불러 온 stack pointer를 범용 sp에 load 한다.
    // restore next task context from the next task stack
    __asm__ ("POP  {r0}");  // sNext_tcb에 저장 된 r0(cpsr)을 복구 한다.
    __asm__ ("MSR   cpsr, r0"); // cpsr을 복구 한다.
    __asm__ ("POP  {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}"); //r0 ~ r12 까지 register를 복구한다.
    __asm__ ("POP  {pc}");  //pc register를 복구한다.
}

 

Next Tcb의 KernelTcb의 구조체를 호출한다. (LDR r0, =sNext_tcb)

 

이전 Page에 언급 했듯이 Stack은 FILO 이므로 save 했던 거와 반대 순서로 Load한다.

 

Stack을 load한다.(LDMIA r0!, {sp})

->순서상 Stack 복구 후 다음 Stack은 r0 가 들어가 있다.그러므로  LDMIA  Base Register에 ! 붙여 pointer가 다음 stack을 나타내게 한다.

 

r0를 load한다.(POP {r0})

-이하 생략-

 

Terminal에서 Context가 Switching 하는 log.