Computer Science
탄탄한 기반 실력을 위한
전공과 이론 지식 모음
Today I Learned!
배웠으면 기록을 해야지
TIL 사진
Flutter 사진
Flutter로 모바일까지
거꾸로캠퍼스 코딩랩 Flutter 앱개발 강사
스파르타코딩클럽 즉문즉답 튜터
카카오테크캠퍼스 3기 학습코치
프로필 사진
박성민
임베디드 세계에
발을 들인 박치기 공룡
임베디드 사진
EMBEDDED SYSTEM
임베디드 SW와 HW, 이론부터 실전까지
ALGORITHM
알고리즘 해결 전략 기록
🎓
중앙대학교 소프트웨어학부
텔레칩스 차량용 임베디드 스쿨 3기
애플 개발자 아카데미 1기
깃허브 사진
GitHub
프로젝트 모아보기
Instagram
인스타그램 사진

Embedded System/FreeRTOS

[FreeRTOS] 커널의 시작과 첫번째 Task의 실행

sm_amoled 2025. 11. 21. 19:53

이전 글들에서 FreeRTOS가 어떻게 Task를 만들어 관리하는지Task간의 전환 과정, 그리고 Real-Time 을 만족하기 위한 스케줄링 전략에 대해 알아보았다. 이번 글에서는 그동안 공부한 내용들을 바탕으로 어떻게 RTOS에서 자동으로 스케줄링을 알잘딱하게 수행하는지에 대해서 알아보자.

제목에서부터 알 수 있듯이, Task 간의 스케줄링은 커널이 담당한다. FreeRTOS에서 커널은 간단하게 보면 다음의 것들을 담당한다.

  • Task 간의 스케줄링
  • 인터럽트 기반의 시간 측정
  • Queue, 세마포어, 뮤텍스 등을 활용한 동기화

커널이라는 이름이 리눅스 같은 곳에서 많이 나왔었기에, 거창해보이고 어려워보일 수는 있지만, 결국 이놈은 하드웨어 자원(CPU나 메모리)을 관리하고 Task들에게 할당하는 SW 프로그램이다. 우리가 작성한 hello world를 출력하는 프로그램과 다를 바가 없다. 단지 조금 대단한 녀석일 뿐이다. 어떻게 보면 프로그램이 부팅된 뒤 처음부터 끝까지 무한루프를 돌면서 실행되는 모듈 함수 중 하나라고 볼 수도 있겠다.

그렇다면 이 커널이 어떻게 시작되어서 스케줄링을 처리하는지에 대해서 한 번 찾아보자.

main 함수를 살펴보면, osKernelStart() 라는 이름의 함수를 호출하는 것을 볼 수 있다. 이 함수의 호출과 함께 커널이 시작된다.

osKernelStart() 함수를 통해 이로어지는 일은 단순하게 말하자면 다음 3가지이다.

  • 아무것도 안하는 Idle Task를 하나 만든다.
  • 하드웨어 및 시스템 설정을 초기화한다.
  • Supervisor Call을 통해 첫 번째 Task로 실행 흐름을 전환한다.

내부 코드를 살펴보면서 각각의 동작을 살펴보자.

1. osKernelStart 함수 호출

함수를 실행하면 vTaskStartScheduler()를 호출하여 RTOS의 커널을 시작한다.

/**
* @brief  Start the RTOS Kernel with executing the specified thread.
* @param  thread_def    thread definition referenced with \ref osThread.
* @param  argument      pointer that is passed to the thread function as start argument.
* @retval status code that indicates the execution status of the function
* @note   MUST REMAIN UNCHANGED: \b osKernelStart shall be consistent in every CMSIS-RTOS.
*/
osStatus osKernelStart (void)
{
  vTaskStartScheduler();

  return osOK;
}

이 os 레이어 자체가 추상화를 위해 한 번 감싸주는 역할만 하기 때문에, 이렇게 이름만 바꿔주는 코드의 모양새가 되었다. 다른 보드 또는 시스템 환경에서는 여기에서 추가적인 작업을 해주기에 매크로가 아닌 함수로 따로 빼뒀을 것이다.

2. vTaskStartScheduler 함수 호출

/**
 * task. h
 *
 * Starts the real time kernel tick processing.  After calling the kernel
 * has control over which tasks are executed and when.
 */
void vTaskStartScheduler( void ) PRIVILEGED_FUNCTION;

vTaskStartScheduler 함수가 아까 앞에서 말했던 3가지 동작을 수행하는 실질적인 코드이다.

  • 아무것도 안하는 Idle Task를 하나 만든다.
  • 하드웨어 및 시스템 설정을 초기화한다.
  • Supervisor Call을 통해 첫 번째 Task로 실행 흐름을 전환한다.

함수가 호출되 Real-Time 커널의 Tick을 활성화하고, 커널에게 스케줄링에 대한 권한을 넘기는 역할을 한다. 그 말은 이 함수가 호출된 이후에는, 어떤 태스크가 언제 실행될지를 커널이 제어하게 된다.

우리가 사용하는 Cortex-M에서는 MPU를 사용하지 않으므로, MPU 환경에서 사용되는 레퍼인 PRIVILEGED_FUNCTION 은 무시해도 된다.

vTaskStartScheduler 의 유효한 부분만 간추린 코드이다. 여기에서 각 블럭을 한 번 살펴보자!

void vTaskStartScheduler( void )
{
    BaseType_t xReturn;

    /* The Idle task is being created using dynamically allocated RAM. */
    xReturn = xTaskCreate(    prvIdleTask,
                            configIDLE_TASK_NAME,
                            configMINIMAL_STACK_SIZE,
                            ( void * ) NULL,
                            portPRIVILEGE_BIT, 
                            &xIdleTaskHandle );

    if( xReturn == pdPASS )
    {
      // xPortStartScheduler()를 호출한 동안 Timer Tick 인터럽트가 발생하여 
      // 작업 수행을 방해하지 않도록 한다
        portDISABLE_INTERRUPTS();

        // 타이머와 스케줄러에 대해 기본값을 넣어주고
        xNextTaskUnblockTime = portMAX_DELAY;
        xSchedulerRunning = pdTRUE;
        xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

        // 아래 두 코드는 Define 되어있으나, 비어있는 코드 (나중에 필요하면 넣어줘야함)
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

        /* xPortStartScheduler 함수 호출을 통해 스케줄러 실행 시작*/
        if( xPortStartScheduler() != pdFALSE )
        {
            /* Should not reach here as if the scheduler is running the
            function will not return. */
        }
    }
}

2-1. Idle Task 생성

BaseType_t xReturn;

    /* The Idle task is being created using dynamically allocated RAM. */
    xReturn = xTaskCreate(    prvIdleTask,
                            configIDLE_TASK_NAME,
                            configMINIMAL_STACK_SIZE,
                            ( void * ) NULL,
                            portPRIVILEGE_BIT, 
                            &xIdleTaskHandle ); // Idle Task 핸들을 전역 변수에 저장 -> 나중에 참조 가능

Idle Task는 준비된 Task가 하나도 없더라도 CPU가 실행 상태에 머무르도록 하기 위해서 만들어준 Task이다. 개발자가 코드 상에서 아무런 Task를 만들지 않더라도 이 Idle Task는 커널의 시작과 함께 자동으로 생성이 되기 때문에 CPU가 뭐라도 하는 상태로 만들어둘 수 있다. 이와 관련된 내용에 대해서는 이 글을 확인해보자.

2-2. portDISABLE_INTERRUPTS();

// xPortStartScheduler()를 호출한 동안 Timer Tick 인터럽트가 발생하여 
      // 작업 수행을 방해하지 않도록 한다
        portDISABLE_INTERRUPTS();

현재 CPU에서 전역적인 인터럽트를 비활성화 한다.

이제 Idle Task도 만들고, 지금부터 스케줄러의 핵심 전역 변수들을 세팅할 건데, 그 사이에 다른 인터럽트가 먼저 들어와서 실행 흐름을 휘저어놓으면 잘못된 스케줄링의 위험이 있음. 그래서 모든 중요한 내부 상태를 다 세팅할 때까지 아무런 인터럽트도 건들지 마라! 라는 의미이다.

실제로 위 함수(매크로)를 호출했을 때 내부에서 호출하는 함수는 아래 어셈블리코드이다.

portFORCE_INLINE static void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI;

    __asm volatile
    (
        "    mov %0, %1                                                       \n"    \
        "    msr basepri, %0                                                  \n" \
        "    isb                                                              \n" \
        "    dsb                                                              \n" \
        :"=r" (ulNewBASEPRI) : "i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY ) : "memory"
    );
}

/* The highest interrupt priority that can be used by any interrupt service
routine that makes calls to interrupt safe FreeRTOS API functions.  DO NOT CALL
INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER
PRIORITY THAN THIS! (higher priorities are lower numeric values. */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configMAX_SYSCALL_INTERRUPT_PRIORITY     ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

코드를 살펴봤을 때, 실제로 모든 인터럽트들을 다 막아버리는 것은 아니고, 1~15의 우선순위 중에서 5 우선순위 이상의 인터럽트를 막는다. 중요한 우선순위의 인터럽트는 아직 허용을 하고 있는 상태.

2-3. 스케줄러 상태 전역 변수 설정

// 타이머와 스케줄러에 대해 기본값을 넣어주고
        xNextTaskUnblockTime = portMAX_DELAY;
        xSchedulerRunning = pdTRUE;
        xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;
  • xNextTaskUnblockTime : SysTick 타이머를 기반으로, Delay에 들어가있는 Task를 깨워야하는지 검사하는 시간을 적어두는 변수. 지금은 잠들어있는 Task가 하나도 없으니 최대한 늦게 확인한다. (확인하는 시간 텀을 최대한 길게 세팅)
  • xSchedulerRunning = pdTRUE; 스케줄링을 시작했다고 플래그를 설정
  • xTickCount에 첫 TICK 카운트. 이 값은 따라가보면 매크로 상수로 0이 지정되어 있다.

2-4. xPortStartScheduler() 호출

if( xPortStartScheduler() != pdFALSE )
{
    /* Should not reach here as if the scheduler is running the
    function will not return. */
}

현재 이 함수들을 실행하고 있는 vTaskStartScheduler는 아직 논리적인 레벨에 있는 함수였다. 이제 HW와 더 가까운 포팅 계층(port~)에게 스케줄러를 실행해달라고 요청한다. 조금 더 하드웨어에 가까운 계층은 그럼 어떤 설정들을 하고 스케줄러를 시작하는지에 대해 살펴보자.

3. vPortStartScheduler 함수 호출

/*
 * Setup the hardware ready for the scheduler to take control.  This generally
 * sets up a tick interrupt and sets timers for the correct tick frequency.
 */
BaseType_t xPortStartScheduler( void ) PRIVILEGED_FUNCTION;

xPortStartScheduler 함수에서 스케줄러를 시작하기 전 하드웨어를 초기화하고, 첫 번째 태스크를 시작시킨다.

이번에는xPortStartScheduler 의 역할을 간추린 코드이다.

BaseType_t xPortStartScheduler( void )
{
    /* Context Switch를 위한 PendSV와 SysTick 의 우선순위를 최하위로 설정 */
    // 이것은 FreeRTOS의 기본 설정 (문서에 나와있음)
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

    // 타이머 인터럽트 설정
    vPortSetupTimerInterrupt();

    // 크리티컬 섹션에 중첩으로 진입한 횟수를 카운트 해 안전하게 해제할 수 있도록
    // 변수값을 여기에서 0으로 초기화해준다.
    uxCriticalNesting = 0;

    // 현재 가장 우선순위가 높은 Task를 선택하여 시작
    prvPortStartFirstTask();

    // 여기까지 코드가 내려오면 안된다 **************
    vTaskSwitchContext();
    prvTaskExitError();

    /* Should not get here! */
    return 0;
}

3-1. PendSV의 우선순위를 낮추기기

여기에서 PendSV의 우선순위를 최하위로 놓는 이유는 혹시 다른 인터럽트가 발생하여 처리하는 도중에 Context Switch를 하지 않기 위해서이다.

Context Switch는 PendSV 가 수행하는 동작이기는 하지만, SysTick 에서 발생하는 Exception이 이 PendSV를 호출하는 역할을 하기 때문에 SysTick 인터럽트도 우선순위를 낮춰 발생하지 않도록 해준다.

잘 보면 SVCall의 우선순위를 낮추라고 되어있는데 코드에는 그런 코드가 없다…?
위에 가져온 문서는 ARM 쪽 문서이고 (FreeRTOS 문서 아님), 해당 문서에서는 SVCall을 분명 PendSV랑 동일한 가장 낮은 우선순위로 두라고 했는데, Kernel Start 코드에서는 SVCall의 우선순위를 설정해주는 부분이 없어서 약간 당황했다. 내가 Systick 이랑 헷갈렸나..? 싶었는데 이게 맞는 코드임.
왜냐면 FreeRTOS에서는 SVCall을 사용하지 않는다. (펌웨어 방식에서 SVCall은 첫 Task를 실행하는 과정에서 단 한 번만 호출됨) → 그래서 딱히 우선순위를 설정하지 않아도 괜찮음.

3-2 SysTick 타이머 설정

여기에서는 잠시 SysTick 타이머를 멈춰두고 값들을 넣어준다.

이때, config 문서에 설정되어있는 값과 SystemCoreClock 값에 따라서 실제 FreeRTOS의 각 Time-Slice의 길이가 정해지게 된다.

__attribute__(( weak )) void vPortSetupTimerInterrupt( void )
{
    /* Stop and clear the SysTick. */
    /* SysTick을 멈추고 초기화 */
    portNVIC_SYSTICK_CTRL_REG = 0UL;  // ( * ( ( volatile uint32_t * ) 0xe000e010 ) )
    portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;  // ( * ( ( volatile uint32_t * ) 0xe000e018 ) )

    /* Configure SysTick to interrupt at the requested rate. */
    /* SysTick 인터럽트 속도 설정값 */
     // ( * ( ( volatile uint32_t * ) 0xe000e014 )
    portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;  
    // ( * ( ( volatile uint32_t * ) 0xe000e010
    portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );  

기본적으로 168MHz를 사용한다고 했을 때, 레지스터 설정에 따라서

  • CLK Source는 168MHz를 그대로 사용한다.
  • LOAD 레지스터에는 167,999 가 담긴다.
  • 이에 따라서 SysTick은 매 1ms (0.001초) 마다 인터럽트를 발생시킨다.

3-3. prvPortStartFirstTask 함수 호출

/*
 * Start first task is a separate function so it can be tested in isolation.
 */
static void prvPortStartFirstTask( void ) __attribute__ (( naked ));

prvPortStartFirstTask() 함수에서는 가장 처음으로 실행할 Task를 찾아서 스케줄러에게 실행을 요청한다.

이제 거의 흐름을 다 따라왔다. 시스템에 대한 설정이 끝났고, 가장 첫 Task를 어떻게 찾는지에 대해서 한 번 살펴보자!

4. 첫번째 Task를 실행시키기

4-1. prvPortStartFirstTask(void)

static void prvPortStartFirstTask( void )
{
    /* Start the first task.  This also clears the bit that indicates the FPU is
    in use in case the FPU was used before the scheduler was started - which
    would otherwise result in the unnecessary leaving of space in the SVC stack
    for lazy saving of FPU registers. */
    __asm volatile(
                    " ldr r0, =0xE000ED08              \n"
                    " ldr r0, [r0]                     \n"
                    " ldr r0, [r0]                     \n"
                    " msr msp, r0                      \n" 

                    " mov r0, #0                       \n" 
                    " msr control, r0                  \n"

                    " cpsie i                          \n" /* Globally enable interrupts. */
                    " cpsie f                          \n"

                    " dsb                              \n"
                    " isb                              \n"

                    " svc 0                            \n" /* System call to start first task. */
                    " nop                              \n"
                );
}

이 함수에서는 역시나 다양한 일들을 하고있는 것처럼 보이지만 꽤나 단순하다.

  1. 메인 스택포인터 (MSP) 가 스택의 첫 주소를 가리키도록 하기
  2. FPU 관련 설정 초기화
  3. 인터럽트 활성화
  4. SVCall 호출

이번에도 결국 우리가 궁금해했던 가장 첫 번째 Task를 찾아서 실행하는 부분은 찾지못했다. 그러나 위 코드 역시 의미가 있기 때문에 잘 살펴보자.

위 코드에서 가장 먼저 MSP에 스택의 첫 주소를 넣어주는 부분이 나온다.

" ldr r0, =0xE000ED08     \n" /* Use the NVIC offset register to locate the stack. */
" ldr r0, [r0]                 \n"
" ldr r0, [r0]                 \n"
" msr msp, r0                  \n" /* Set the msp back to the start of the stack. */

여기에서 0xE000_ED08 는 Vector Table의 위치를 가리키는 값이 담겨있는 레지스터의 주소이다.

벡터테이블의 주소 값을 꺼내와 r0 레지스터에 담고, 한 번 더 메모리에 접근해 벡터테이블의 첫 번째 값을 가져온다. (주소에 그대로 접근하면 테이블의 첫 번째 값! — like 배열)

벡터 테이블의 첫 번째 값은 프로세스가 처음 시작될 때의 Stack Pointer 값이고, 이건 Startup의 Reset_Handler에서 Stack의 가장 첫 주소(Stack이 Full Descending 방식이므로 가장 큰 주소)를 넣어준다. 위 코드에서는 이 값을 가져와서 MSP에 옮겨담고 있다.

여기 코드부분을 실행해주게 되면 MSP 레지스터에 0x20030000 이라는 값이 자리잡게된다. 이 위치가 정말 Stack의 첫 시작주소가 맞냐?

우리의 Linker Discription 파일을 보면 사용하는 Stack의 크기 및 위치를 확인할 수 있는데,

/* Memories definition */
MEMORY
{
  CCMRAM    (xrw)    : ORIGIN = 0x10000000,   LENGTH = 64K
  RAM       (xrw)    : ORIGIN = 0x20000000,   LENGTH = 192K
  FLASH      (rx)    : ORIGIN = 0x8000000,    LENGTH = 2048K
}

Base 주소가 0x2000_0000 이고 길이가 192K == $3 \times 2^{16}$ == 0x3_0000 이므로, Stack의 끝 위치가 0x2003_0000 임을 파악할 수 있다.

4-2. 크리티컬 섹션 해제

여기에서 SVCall 호출을 위해서 Critical Section을 해제하고 있다.

// Change Processor State Interrupt Enable - IRQ
// Change Processor State Interrupt Enable - Fault
" cpsie i                        \n" /* Globally enable interrupts. */
" cpsie f                        \n"

이 부분은 앞서 portDISABLE_INTERRUPTS(); 함수 호출을 통해 BASEPRI 의 값을 지정하여 인터럽트의 발생할 차단해둔 (크리티컬 섹션에 진입한) 부분을 해제하는 코드이다. 이제 인터럽트의 입력 제한이 해제되어 우선순위가 낮은 인터럽트(우선순위 5 이상의 인터럽트)들의 Handler가 실행될 수 있다.

4-3. Supervisor Call 호출

그리고 이렇게 svc 0 이라는 간단한 명령어로 Supervisor Call을 때리고 있다. 여기에서 0 이라는 값은 SVCall에 대한 인자인데, 우리의 FreeRTOS에서는 SVC를 단 한 곳에서만 사용하기 때문에 인자 값이 필요없어서 지금은 무시해도 되는 값이다. 단지 Supervisor Call 을 요청한다는 것에 집중하자!

" svc 0                            \n" /* System call to start first task. */

5. Supervisor Call Handler 호출

인터럽트가 발생하면 시스템은 정해진대로 Handler를 호출한다. 다만, 우리의 시스템에서는 SVC_Handler 를 다시 작성해주면서 vPortSVCHandler 라는 이름으로 바꿔서 사용해주도록 매크로를 작성해뒀기에, vPortSVCHandler 라는 함수가 호출되게된다.

/* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS
standard names. */
#define vPortSVCHandler    SVC_Handler
#define xPortPendSVHandler PendSV_Handler

그럼 여기에서 어떤 내용들이 실행될까?

void vPortSVCHandler( void )
{
    __asm volatile (
    // 현재 TCB의 정보를 가져와서
                    "    ldr    r3, pxCurrentTCBConst2        \n" 
                    "    ldr r1, [r3]                         \n" 
                    "    ldr r0, [r1]                         \n" 
    // 레지스터 값들을 복원해오고
                    "    ldmia r0!, {r4-r11, r14}             \n" 
  // PSP (Process Stack Pointer) 값을 복원하고
                    "    msr psp, r0                          \n" 
                    "    isb                                  \n"
    // 전체 인터럽트를 허용하고
                    "    mov r0, #0                           \n"
                    "    msr    basepri, r0                   \n"
    // 나머지 레지스터 값들도 복원
    // Task 시작 (Branch -> PC를 r14 로 이동)
                    "    bx r14                               \n"
                    "                                         \n"
                    "    .align 4                             \n"
                    "pxCurrentTCBConst2: .word pxCurrentTCB   \n"
                );
}

그럼 이 vPortSVCHandler 에서는 어떤 일을 하나 살펴보자면 다음과 같다.

  1. 현재 실행할 Task의 TCB 정보 가져오기
  2. SW적으로 불러와야하는 레지스터 값 복원하기
  3. 인터럽트 허용하고
  4. 나머지 레지스터 값을 가져오면서 해당 Task를 실행하기

이전에 Task의 생성에 대해서 설명하는 글에서, Task에 대한 정보는 TCB와 Stack이 가지고 있으며, Task Create 시에 어떤 정보들이 여기에 담기는 지에 대해서 알아보았다. 이때 처음에 만들어지는 Stack에 담기는 레지스터들은 대부분 쓰레기값이고, 값 자체가 의미있지는 않았다. 이 친구들은 그냥 커널(또는 스케줄러, 또는 핸들러)에게 실행하던 내용이 있는 것처럼 보이게 하는 것이 목적이다.

어셈블리코드에서 사용하는 부분만 간단히 발췌해보면 아래와 같다.

여기에서 SW적으로 직접 복원해주어야 하는 레지스터들이 정해져있고, 이 값들을 불러오는 코드가 아래 줄들이다. 특히 유의할 점은, pxCurrentTCB의 주소를 담은 레이블인 pxCurrentTCBConst2 로부터 pxCurrentTCBTCBTopOfStack 값을 가져와서 넣어주는 방식을 사용한다는 것.

    __asm volatile (
    // 현재 TCB의 정보를 가져와서
                    "    ldr    r3, pxCurrentTCBConst2        \n"
                    "    ldr r1, [r3]                            \n" 
                    "    ldr r0, [r1]                            \n"
    // 레지스터 값들을 복원해오고
                    "    ldmia r0!, {r4-r11, r14}          \n"
  // PSP (Process Stack Pointer) 값을 복원하고
                    "    msr psp, r0                                \n"
                    ... 
                    "pxCurrentTCBConst2: .word pxCurrentTCB                \n"
                );

그리고 남은 레지스터 값들은 HW 적으로 불러와야한다. 이를 불러오기 위해서는 LR에 담겨있는 EXC_RETURN 값에 있는 명령어를 실행하겠다고 요청하면 프로세서에서 “어 이건 명령어 주소가 아니라 특수한 요청인데? → 그럼 해당 요청을 수행해줘야지” 라고 판단하고 처리해준다. EXC_RETURN에는 Task를 만들 때 0xFFFFFFFD 라는 값을 넣어뒀다. 이게 어떤 역할을 하냐?

  • Thread Mode로 전환한다. (원래는 Handler Mode에 있었음 → 스택포인터로 MSP를 사용했는데, 이제 PSP를 사용하겠다는 이야기)
  • Floating Point 를 위한 Register는 복원하지 않음. (거의 30개 가량의 SX 레지스터들은 복원하지 않겠다)

    __asm volatile (
                    ...
    // 나머지 레지스터 값들도 복원
    // Task 시작 (Branch -> PC를 r14 로 이동)
                    "    bx r14                            \n"
                    "                                        \n"
                    "    .align 4                        \n"
                    "pxCurrentTCBConst2: .word pxCurrentTCB                \n"
                );

위 명령어에서 r14 (LR 레지스터)에 EXC_RETURN 인 0xFFFFFFFD 를 넣어뒀었다. 이 값으로 Branch 하겠다 (실행하겠다)는 요청을 bx 명령어로 전달하면, 실제로 해당 명령어 주소로 이동하는 대신에 기존의 필요한 레지스터들을 HW적으로 복원하는 과정을 수행한다!

최종적으로

  • R0 에는 처음 Task를 생성할 때 넣어준 argument가 들어있고
  • SP 에는 Stack Pointer의 주소가 들어있고
  • PC 에는 실행해야하는 Task의 명령어 주소가 들어있고
  • xPSR 에는 현재 Thumb 모드로 실행한다는 정보가 들어있다.

이때 CPU는 단순히 PC에 있는 명령어를 실행하는 일만 수행하기 때문에, 다음 CLK 부터는 Task의 Code를 한 줄 한 줄 실행하게된다! 🎉🎉🎉

이 과정은 단순히 SVC에서 첫 Task를 실행하기 위한 절차일 뿐만 아니라, PendSV를 통한 Context-Switch에서도 동일하게 동작한다. (PendSV에서는 기존 Task는 Stack에 저장하는 과정이 추가로 있다! 그러니 꼭 머리에 넣어두자.

이로써, 커널을 시작시키는 과정에서 시스템의 설정값들을 모두 세팅하는 절차 + 첫 번째 Task를 실행하기 위한 모든 절차를 한 줄 한 줄 따라가봤다. 물론 중간중간에 더 디테일한 부분들이 있을 수 있겠지만, 이 정도 파악하는 정도로도 충분히 전체적인 흐름은 잘 파악할 수 있으리라 생각한다!

320x100