7 분 소요

이 글은 ‘uC/OS-III: The Real-Time Kernel For the STM32 ARM Cortex-M3, Jean J. Labrosse, Micrium, 2009’를 번역한 글입니다. 오역이 있을 수 있으며, 발견하시면 github에 issue나 댓글 남겨주시기 바랍니다.

μC/OS-III는 특정 작업을 수행하는 일련의 함수 형태로 응용 프로그램 코드에 서비스 제공. 제공하는 서비스는 task, semaphores, message queue, mutual exclusion semaphore 관리 등이 있음.

Single task application

L3-1 app.c(1st part)

//include files
#include <app_cfg.h> (1) 
#include <bsp.h> 
#include <os.h> 
//local global variables
static OS_TCB AppTaskStartTCB; (2) 
static CPU_STK AppTaskStartStk[APP_TASK_START_STK_SIZE]; (3) 
//function prototype
static void AppTaskStart (void *p_arg); (4)

//L3-1 app.c(1st part)

L3-1(1)

app_cfg.h

application을 설정하는 헤더파일이다. 예를 들어, #define을 이용하여 task 우선순위, 스택 크기 등을 설정한다.

bsp.h

Board Support Package를 위한 헤더파일 BSP_Init(), BSP_LED_On(), OS_TS_GET() 등의 정의되어있음

os.h

os.h는 다음과 같은 파일이 포함되어있다.

  • os_cfg.h
  • cpu.h
  • cpu_core.h
  • lib_def.h
  • os_type.h
  • os_cpu.h

L3-1(2)

application task를 만들려면 task control block(OS_TCB)를 각 task 별로 할당하는 것이 필요하다.

L3-1(3)

각 task는 stack이 필요하다. 스택은 CPU_STK 타입으로 선언되어야 한다. 정적 또는 동적(malloc())으로 선언될 수 있다. 스택을 (free 함수 등으로) 해제할 필요가 없는데, task는 종료되지 않을 것이고, 따라서 스택도 계속 쓰일 것이기 때문이다.

L3-1(4)

만들어질 task의 함수 프로토 타입(선언만 있고, 내부 구현은 안되어있음)이다.

거의 모든 C 프로그램은 main() 부터 시작한다(L3-2 같이)

L3-2 app.c(2nd part) - main

void main (void) 
{ 
    OS_ERR err; 
    BSP_IntDisAll(); (1) 
    OSInit(&err); (2) 
    if (err != OS_ERR_NONE) { 
        /* Something didn’t get initialized correctly ... */ 
        /* ... check os.h for the meaning of the error code, see OS_ERR_xxxx */ 
    } 
    OSTaskCreate((OS_TCB *)&AppTaskStartTCB, (3) 
                (CPU_CHAR *)App Task Start, (4) 
                (OS_TASK_PTR )AppTaskStart, (5) 
                (void *)0, (6) 
                (OS_PRIO )APP_TASK_START_PRIO, (7) 
                (CPU_STK *)&AppTaskStartStk[0], (8) 
                (CPU_STK_SIZE)APP_TASK_START_STK_SIZE / 10, (9) 
                (CPU_STK_SIZE)APP_TASK_START_STK_SIZE, (10) 
                (OS_MSG_QTY )0, 
                (OS_TICK )0, 
                (void *)0, 
                (OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), (11) 
                (OS_ERR *)&err); (12) 
    if (err != OS_ERR_NONE) { 
        /* The task didn’t get created. Lookup the value of the error code ... */ 
        /* ... in os.h for the meaning of the error */ 
    } 
    OSStart(&err); (13) 
    if (err != OS_ERR_NONE) { 
        /* Your code is NEVER supposed to come back to this point. */ 
    } 
}
//L3-2 app.c(2nd part)

L3-2(1)

main()함수가 호출하는 첫번쨰 함수는 BSP_IntDisAll()이다. 대부분의 프로세서는 인터럽트가 application code에 의해 켜지기 전까지는 꺼져있지만, 시작하는동안 모든 주변 인터럽트를 끄는 것이 안전하다.

L3-2(2)

OSInit()는 microC/OS-III를 초기화하기 위해 호출된다.
내부 변수와 자료구조를 초기화하고 2~5개 정도의 내부 task를 만든다. 최소 2개를 만드는데, 어느 task도 준비상태가 아닐때 실행되는 idle task(OS_IdleTask())와 시간을 체크하는 tick task를 만든다.

#define으로 선언된 상수에 따라 통계 task(OS_StatTask()), 타이머 task(OS_TmrTask()), 인터럽트 핸들러 큐 관리 task(OS_IntQTask())를 만든다.

대부분의 microC/OS-III의 함수는 OS_ERR 변수의 포인터를 통해서 에러코드를 전달한다. 만약 OSInit()이 에러없이 실행이 됬다면 OS_ERR 변수(여기서는 err)는 OS_ERR_NONE으로 설정된다.
만약 오류가 발생했다면 그 즉시 리턴되며 err 변수를 설정(OS_ERR_으로 시작)한다.

OSInit()은 다른 함수보다 가장 먼저 호출되야한다.

L3-2(3)

task는 OSTaskCreate()함수를 호출하여 만든다.
OSTaskCreate()는 13개의 인자(argument)가 필요하다.
첫 인자는 L3-1에서 선언된 OS_TCB의 주소(여기서는 &AppTaskStartTCB)이다.

L3-2(4)

OSTaskCreate()로 task를 만들 때 task에 이름을 붙일 수 있다. μC/OS-III는 그 task의 OS_TCP내부에 task 이름의 포인터를 저장한다. 이름 길이의 제한은 없다.

L3-2(5)

task에서 실행될 코드의 주소이다. 보통 microC/OS-III에서 실행되는 task는 다음과 같이 무한 루프로 구현된다.

void MyTask (void *p_arg)
{
    /*Do something with "p_arg"*/
    while(1){
        /* Task body */
    }
}

task는 처음 시작할 때 인자를 하나 받는다.
MyTask()는 무조건 microC/OS-III를 통해 실행되어야한다. 즉 application code가 직접 MyTask를 호출하면 안된다.

L3-2(6)

OSTaskCreate()의 4번째 인자는 실행될 task가 받을 인자이다. 즉 Mytask()의 p_arg이다.여기서는 NULL 포인터를 받는다.

L3-2(7)

다음 인자는 task의 우선순위이다. 숫자가 낮을 수록 우선순위가 높다. 1이상, OS_CFG_PRIO_MAX-2 이하로 설정할 수 있다. 0이나 OS_CFG_PRIO_MAX-1로 설정하는 것은 피해야 하는데, microC/OS-III의 우선순위이기 때문이다. OS_CFG_PRIO_MAX는 os_cfg.h에 정의된, 컴파일 타임에 설정되는 상수이다.

L3-2(8)

6번째 인자는 task에 할당된 stack의 base address이다. base address는 항상 stack에서 가장 낮은 위치에 있다.

L3-2(9)

다음 인자는 stack의 watermark 위치이다. watermark란 stack이 얼마나 채워질 수 있는지 표시하는 것이다. 예제에서는 스택의 90%가 채워젔을 때이다.

L3-2(10)

다음 인자는 task stack의 크기이다. 바이트가 아닌 CPU_STK 단위로 입력한다. 만약 1KB의 stack을 할당하고 싶으면 CPU_STK이 32 bit일때 256이다(256*4B 1KB).

L3-2(11)

OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR 를 통해, 실행시간에 스택 체크를 하고 task가 만들어질때 stack이 한번 지워진다(0으로 초기화된다).

L3-2(12)

마지막 인자는 오류 코드를 받을 변수의 포인터이다.

L3-2(13)

main()의 마지막 단계는 OSStart()이다. OSStart()를 호출하면 multitasking을 시작하며 μC/OS-III는 OSStart()전에 만들어진 task 중 가장 우선순위가 높은 것을 실행한다.

가장 높은 우선순위의 task는 항상 OS_IntQTask()이다. 만약 이 task가 os_cfg.h안에 있는 OS_CFG_ISR_POST_DEFERRED_EN 상수에 의해 활성화되어있다면 OS_IntQTask()는 초기화를 실행한 후, μC/OS-III는 다음으로 가장 우선순위가 높은 task로 switch한다.

OSStart()를 호출하기 이전에 원하는 만큼 많은 task를 만들 수 있지만 하나의 task만 만드는 것을 권장한다. 왜냐하면 하나의 application을 가지면 μC/OS-III가 CPU의 상대적인 속도를 알 수 있고 CPU 사용률을 결정할 수 있기 때문이다.

Semaphore나 message queue 같은 다른 kernel object가 필요하다면 OSStart()를 호출하기 전에 kernel object를 생성하는 것이 좋다. 그리고 위 코드에서 인터럽트는 활성화되지 않았다.

L3-3 app.c (3rd Part) - AppTaskStart

static void AppTaskStart (void *p_arg) (1)
{
    OS_ERR err;
    p_arg = p_arg;
    BSP_Init(); (2)
    CPU_Init(); (3)
    BSP_Cfg_Tick(); (4)
    BSP_LED_Off(0); (5)
    while (1) { (6)
        BSP_LED_Toggle(0); (7)
        OSTimeDlyHMSM((CPU_INT16U) 0, (8)
                        (CPU_INT16U) 0,
                        (CPU_INT16U) 0,
                        (CPU_INT32U)100,
                        (OS_OPT )OS_OPT_TIME_HMSM_STRICT,
                        (OS_ERR *)&err);
        /* Check for ‘err’ */
    }
}
//L3-3 app.c (3rd Part)

L3-3(1)

인자 p_arg는 OSTaskCreate()함수가 AppTaskStart()에 넘겨준다.

L3-3(2)

BSP_Init()은 보드의 하드웨어 초기화를 담당하는 Board Support Package(BSP)의 기능이다. (evaluation board에는 GPIO(General Purpose Input Output), 릴레이, 센서 등이 있을 수 있다.)

L3-3(3)

CPU_Init()은 μC/CPU 서비스를 초기화한다. μC/CPU가 제공하는 서비스는 interrupt latency 측정, obtain time stamp, count leading zero instruction 에뮬레이션 등을 제공한다.(count leading zero instruction: “counts the number of zero bits preceding the most significant one bit.”)

L3-3(4)

BSP_Cfg_Tick()은 μC/OS-III tick 인터럽트를 설정한다. 이를 위해 이 함수는 OS_cfg_app.h에 정의된 OSCfg_TickRate_Hz 비율로 CPU를 인터럽트하기 위해 하드웨어 타이머를 초기화해야한다(OS_CFG_TICK_RATE_HZ 참조)

L3-3(5)

BSP_LED_Off() 는 LED를 끄는 함수이다. 인자로 사용된 0은 모든 LED를 의미한다,

L3-3(6)

대부분 μC/OS-III task는 무한 루프로 설계된다.

L3-3(7)

이 BSP 함수는 특정한 LED를 끄고켜는 함수이다. 인자로 사용된 0은 모든 LED가 토글된다는 의미이다. 인자를 1로 바꾸면 1번 LED가 토글된다. 어느 LED가 1번 LED인지는 BSP 개발자에 달려있다. LED를 제어하는 함수는 BSP_LED_On(), BSP_LED_Off(), BSP_LED_Toggle()이 있다. 그리고 이식성을 높이기 위해 LED에 논리적인 값을 부여한다(1,2,3 …).

L3-3(8)

각 task는 마지막에 “이벤트를 기다리도록”하는 μC/OS-III함수를 호출해야한다. 일정 시간을 기다리게 할 수도 있고(OSTimeDly(), OSTimeDlyHMSM() 등), 다른 task나 ISR로 부터 신호나 메시지를 받을 때 까지 기다릴 수 있다. 여기서는 OSTimeDlyHMSM()을 사용하였다.

  • OSTimeDlyHMSM() : 일정 시간, 분, 초, 밀리초 동안 기다리는 함수. 여기서는 100ms를 기다린다. page 203쪽의 Time Management 파트에서 더 자세하게 다룬다.

Multiple task application with kernel objects

L3-4 app.c(1st part)

/*
***********************************************************************************************
* INCLUDE FILES
***********************************************************************************************
*/
#include <app_cfg.h>
#include <bsp.h>
#include <os.h>
/*
***********************************************************************************************
* LOCAL GLOBAL VARIABLES
***********************************************************************************************
*/
static OS_TCB AppTaskStartTCB; (1)
static OS_TCB AppTask1_TCB;
static OS_TCB AppTask2_TCB;
static OS_MUTEX AppMutex; (2)
static OS_Q AppQ; (3)
static CPU_STK AppTaskStartStk[APP_TASK_START_STK_SIZE]; (4)
static CPU_STK AppTask1_Stk[128];
static CPU_STK AppTask2_Stk[128];
/*
***********************************************************************************************
* FUNCTION PROTOTYPES
***********************************************************************************************
*/
static void AppTaskStart (void *p_arg); (5)
static void AppTask1 (void *p_arg);
static void AppTask2 (void *p_arg);

//L3-4 app.c(1st part)

L3-4(1)

OS_TCB들을 저장하는 공간을 할당한다.

L3-4(2)

mutual exclusion semaphore(a.k.a a mutex)는 여러 task가 공유자원을 쓰는 것을 막아주는 커널 객체(자료구조)이다. 공유 자원에 접근하려면 접근하기전에 mutex를 얻어야한다. 공유자원에 대한 접근이 끝나면 mutex를 방출해야한다.

L3-4(3)

message queue는 ISR 및/또는 task들이 메시지들을 다른 task로 송신하는 커널 객체이다. 송신자는 메시지를 만들어내서 message queue 로 보낸다. 이 메시지를 받고 싶어하는 task들은 message queue 상에서 기다린다. message queue에 메시지가 있으면, 수신자는 메시지를 즉시 가져온다. 만약 메시지가 message queue에 없다면, 수신 task는 message queue와 연관된 대기목록(wait list)에 배치된다.

L3-4(4)

스택이 각 task별로 할당된다.

L3-4(5)

task들의 프로토타입이 선언된다.

L3-5 app.c (2nd part) - main

아래 코드는 main()코드(C 프로그램 실행시 진입점)이다.

void main (void)
{
    OS_ERR err;

    BSP_IntDisAll();
    OSInit(&err);
    /* Check for ‘err’ */

    OSMutexCreate((OS_MUTEX *)&AppMutex, (1)
                (CPU_CHAR *)"My App. Mutex",
                (OS_ERR *)&err);
    /* Check for ‘err’ */

    OSQCreate ((OS_Q *)&AppQ, (2)
            (CPU_CHAR *)"My App Queue",
            (OS_MSG_QTY )10,
            (OS_ERR *)&err);
    /* Check for ‘err’ */

    OSTaskCreate((OS_TCB *)&AppTaskStartTCB, (3)
            (CPU_CHAR *)"App Task Start",
            (OS_TASK_PTR )AppTaskStart,
            (void *)0,
            (OS_PRIO )APP_TASK_START_PRIO,
            (CPU_STK *)&AppTaskStartStk[0],
            (CPU_STK_SIZE)APP_TASK_START_STK_SIZE / 10,
            (CPU_STK_SIZE)APP_TASK_START_STK_SIZE,
            (OS_MSG_QTY )0,
            (OS_TICK )0,
            (void *)0,
            (OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
            (OS_ERR *)&err);

    /* Check for ‘err’ */

    OSStart(&err);
    /* Check for ‘err’ */

    //app.c (2nd part)
}

L3-5(1)

OSMutexCreate() 함수로 mutex를 만든다. mutex로 사용될 OS_MUTEX 객체의 주소를 넘겨주어야한다. mutex에 ASCII 이름을 지정할 수 있다.

L3-5(2)

OS_Q 객체의 주소를 인자로 하여 OSQCreate()함수를 호출하여 message queue를 만들 수 있다.

message queue에 ASCII 이름을 지정할 수 있다.

또한 message queue가 수신할 수 있는 메시지의 수를 (0보다 크게) 지정해야한다. 만약 송신자가 소비되는 것보다 더 빨리 메시지를 보낸다면 메시지는 손실될 것이다. 이것은 message queue의 크기를 늘리거나, 수신 task의 우선순위를 크게하면 된다.

L3-5(3)

첫번째 application이 만들어진다.

L3-6 app.c(3rd part) - AppTaskStart

L3-6은 멀티태스킹을 시작했을 때 다른 task를 만드는 방법을 보여준다.

static void AppTaskStart (void *p_arg)
{
    OS_ERR err;

    p_arg = p_arg;
    BSP_Init();
    CPU_Init();
    BSP_Cfg_Tick();
    OSTaskCreate((OS_TCB *)&AppTask1_TCB, (1)
                (CPU_CHAR *)App Task 1,
                (OS_TASK_PTR )AppTask1,
                (void *)0,
                (OS_PRIO )5,
                (CPU_STK *)&AppTask1_Stk[0],
                (CPU_STK_SIZE)0,
                (CPU_STK_SIZE)128,
                (OS_MSG_QTY )0,
                (OS_TICK )0,
                (void *)0,
                (OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
                (OS_ERR *)&err);

    OSTaskCreate((OS_TCB *)&AppTask2_TCB, (2)
                (CPU_CHAR *)App Task 2,
                (OS_TASK_PTR )AppTask2,
                (void *)0,
                (OS_PRIO )6,
                (CPU_STK *)&AppTask2_Stk[0],
                (CPU_STK_SIZE)0,
                (CPU_STK_SIZE)128,
                (OS_MSG_QTY )0,
                (OS_TICK )0,
                (void *)0,
                (OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
                (OS_ERR *)&err);
    BSP_LED_Off(0);
    while (1) {
        BSP_LED_Toggle(0);
        OSTimeDlyHMSM((CPU_INT16U) 0,
                    (CPU_INT16U) 0,
                    (CPU_INT16U) 0,
                    (CPU_INT32U)100,
                    (OS_OPT )OS_OPT_TIME_HMSM_STRICT,
                    (OS_ERR *)&err);
    }
}
//app.c(3rd part)

L3-6(1)

Task #1은 OSTaskCreate()함수를 호출하여 만든다. Task #1이 우선순위가 AppTaskStart()보다 높다면 Task #1을 즉시 시작할 것이고, 우선순위가 낮다면 OSTaskCreate()로 task를 만든 후, AppTaskStart()로 돌아올 것이다.

L3-6(2)

Task #2가 만들어지고 만약 AppTaskStart()보다 우선순위가 높다면, μC/OS-III는 Task #2로 switch할 것이다.

L3-7 app.c(4th part) - AppTask1

static void AppTask1 (void *p_arg)
{
    OS_ERR err;
    CPU_TS ts;

    p_arg = p_arg;
    while (1) {
        OSTimeDly ((OS_TICK )1, (1)
                    (OS_OPT )OS_OPT_TIME_DLY,
                    (OS_ERR *)&err);
        OSQPost ((OS_Q *)&AppQ, (2)
                (void *)1;
                (OS_MSG_SIZE)sizeof(void *),
                (OS_OPT )OS_OPT_POST_FIFO,
                (OS_ERR *)&err);
        OSMutexPend((OS_MUTEX *)&AppMutex, (3)
                    (OS_TICK )0,
                    (OS_OPT )OS_OPT_PEND_BLOCKING;
                    (CPU_TS *)&ts,
                    (OS_ERR *)&err);
        /* Access shared resource */ (4)
        OSMutexPost((OS_MUTEX *)&AppMutex, (5)
                    (OS_OPT )OS_OPT_POST_NONE,
                    (OS_ERR *)&err);
    }
}
//app.c(4th part)

L3-7(1)

이 task는 시작하고 1 tick을 기다린다. 만약 μC/OS-III의 tick rate가 1000Hz라면 1밀리초를 기다린다.

L3-7(2)

그런 다음 task는 message queue AppQ를 이용하여 메시지를 다른 task로 전송한다. 이 경우, 위의 예제는 1이라는 메시지를 전달하지만, 메시지는 버퍼의 주소, 함수의 주소 등이 될 수 있다.

L3-7(3)

그리고 task는 공유 자원에 접근해야하므로 mutual exclusion semaphore(mutex)에서 대기한다. 다른 task가 mutex를 소유하고 있는 경우, mutex가 방출될때까지 영원히 기다릴 것이다. 2번째 인자인 0의 뜻이 mutex가 방출될때까지 영원히 기다린다는 뜻이다.

L3-7(4)

OSMutexPend()함수가 종료되면, mutex를 소유하고 공유자원에 접근할 수 있다. 공유자원은 변수, 배열, 자료구조, I/O 장치 등이 될 수 있다.

L3-7(5)

공유자원 사용이 끝나면, OSMutexPost()를 호출하여 mutex를 방출해야한다.

L3-8 app.c(5th part) - AppTask2

static void AppTask2 (void *p_arg)
{
    OS_ERR err;
    void *p_msg;
    OS_MSG_SIZE msg_size;
    CPU_TS ts;
    CPU_TS ts_delta;


    p_arg = p_arg;
    while (1) {
        p_msg = OSQPend((OS_Q *)&AppQ, (1)
                        (OS_MSG_SIZE *)&msg_size,
                        (OS_TICK )0,
                        (OS_OPT )OS_OPT_PEND_BLOCKING,
                        (CPU_TS *)&ts,
                        (OS_ERR *)&err);
        ts_delta = OS_TS_GET()  ts; (2)
        /* Process message received */ (3)
    }
}

L3-8(1)

Task #2는 message queue AppQ를 통해 message가 송신되기를 영원히 기다리는 것으로 시작한다. 3번쨰 인수가 영원히 기다리는 것을 의미한다. 메시지가 수신될 때 p_msg는 메시지(예를 들어, 어떤 것에 대한 포인터)를 포함할 것이다. 예제에서 AppTask2()는 항상 1을 수신할 것이다. 메시지의 의미에 대해서는 송신자 수신자와의 합의가 필요하다. 수신된 메시지의 크기는 msg_size에 저장된다. 만약 p_msg가 버퍼의 주소라면, msg_size는 버퍼의 크기가 저장된다.

또, 메시지가 수신될 때, ts에는 메시지를 보냈을 때의 timestamp가 저장된다. timestamp는 상당히 빠른 free-running 타이머로부터 읽은 값이다. timestamp는 unsigned 32bit(또는 그 이상)값이다.

L3-8(2)

메시지가 언제 보내졌는지 아는 것은, 메시지를 얻기 위해 얼마나 걸렸는지 알 수 있게 해준다. 현재 timestamp에서 ts 값을 빼서 메시지를 수신하기까지 얼마나 걸렸는지 알 수 있게 해준다. ISR이나 높은 우선순위 task 때문에 메시지를 즉시 받지 못할 수 있다.

L3-8(3)

여기에 메시지를 처리하는 코드를 추가할 수 있다.

Reference

  • uC/OS-III: The Real-Time Kernel For the STM32 ARM Cortex-M3, Jean J. Labrosse, Micrium, 2009

책 링크

댓글남기기