μC/OS-III ch.3 Getting Started with microC/OS-III
이 글은 ‘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
댓글남기기