코드 구조 분석을 진행하기 앞서, FreeRTOS에는 중요한 코딩룰을 먼저 알아보자.
FreeRTOS에서는 변수명 결정시, 타입에 따라 아래와 같은 접두어를 사용한다
c
: char
Types
: int16_t
Type (short
)i
: int32_t
Type (int
)x
: BaseType_t
Type, 주로 구조체나 인스턴스 핸들 등 일반적인 타입을 제외하면 대부분 x
u
: unsigned
p
: pointer
v
: void
, 반환값이 없는 함수를 의미x
: 변수명의 접두어 x
와 같은 의미, BaseType_t
를 반환하는 함수pv
: void*
타입을 반환하는 함수prv
: private
함수, 대표적으로 아무런 task도 실행되지 않을 때 실행되는 idle task가 호출하는 callback 함수인 hook가 이 접두어를 사용하고 있음내부적으로 생성된 객체를 관리하고 식별하기 위한 포인터 타입, 보통 Task, Queue, Semaphore, Timer 등과 같은 RTOS 객체를 가리키는 참조(포인터)
형태의 자료 구조.
TaskHandle_t
: Task를 식별하는 데 사용되는 핸들QueueHandle_t
: Queue를 식별하는 데 사용되는 핸들SemaphoreHandle_t
: Semaphore(Mutex 등)를 식별하는 데 사용되는 핸들TimerHandle_t
: 소프트웨어 타이머를 식별하는 데 사용되는 핸들FreeRTOS에서 제공해주는 Task 관련 API가 있다. 대표적으로 몇가지 알아보자면,
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
unsigned short usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask)
pvTaskCode
: task의 기능이 선언된 함수의 함수 포인터pcName
: 디버깅 용도로 사용하는 문자열이며, task의 이름을 말함usStackDepth
: task마다 할당되는 stack 메모리를 말하며, 단위는 WORD
. 일반적으로 사용하는 ARM Cortex-M 보드에서는 1WORD == 4Byte
pvParameters
: task 함수로 전달할 매개변수, 없으면 NULL
사용. 전달할 매개변수를 (void*)
타입으로 캐스팅한 뒤 여기다가 넣어주면 task 함수에서 사용할 수 있음uxPriority
: *pxCreatedTask
: task를 제어하기 위한 TaskHandle_t
타입 핸들, Task의 우선순위를 바꾸거나, task를 멈추거나 등 task에 대한 설정은 모두 이 핸들을 통해 이루어짐.(예시)
xTaskCreate( prvQueueReceiveTask, // Task 함수, 실제 실행될 로직
"RX", // 디버깅 시 확인할 수 있는 이름, 커널 내부에서 직접 쓰이지는 않음
configMINIMAL_STACK_SIZE, // 스택 크기
NULL, // 입력 파라미터, Task가 필요로 한다면 다른 파라미터를 넣어 사용할 수 있음
mainQUEUE_RECEIVE_TASK_PROIRITY, // 우선순위, 어느 Task를 더 자주/빨리 실행할지 결정, 우선순위가 높을수록 CPU 점유 기회가 많아짐
NULL ); // Null을 넘기면 Task 핸들을 별도로 받지 않음, Task 핸들이 필요하면 변수로 받아서 vTaskDelete()등에 활용 가능
void vTaskDelay( TickType_t xTicksToDelay );
Running
에서 Blocked(waiting)
으로 변경하는 함수xTicksToDelay
동안 Task는 blocked
Task가 되며, 다음 우선순위를 가진 task가 실행됨tick
단위 시간을 사용하는 것 보다 pdMS_TO_TICKS()
매크로를 사용해서 ms 단위를 tick
으로 변환해서 사용하는 것이 좋음vTaskDelay()
와 똑같은 기능을 수행하지만, vTaskDelay()
는 호출 시점부터 지정된 시간만큼 blocked 되는 반면,vTaskDelayUntile()
은 호출 시점과 관계없이 목표 절대 시간 주기에 맞춰 blocked 됨드디어 main_blinky()를 분석해보자.
main_blinky()는 Posix_gcc Demo의 main() 함수에 정의된 mainSELECTTED_APPLICATION = 0
일 때, 호출되는 함수이다.
main 함수가 호출됨가 동시에, 먼저 시그널 처리 함수를 등록한다.
signal( SIGINT, handle_sight);
signal 함수는 시그널 처리 함수를 등록하는 함수로, SIGINT
는 일반적으로 프로그램 실행 중, 키보드로부터 인터럽트가 들어왔을 때 발생하는 시그널
프로그램 실행 중, 사용자가 Ctrl+C를 눌러 종료 신호를 보냈을 때 바로 종료되는 대신 handlo_sight
함수에서 필요한 동작(자원 정리 및 로그 출력 등)을 수행하도록 설정해주는 코드
그 다음으로는 콘솔(또는 표준 입출력) 사용을 위한 뮤텍스(Mutex)를 생성하는 함수 console_init()
을 호출한다.
console_init();
해당 함수에서는 FreeRTOS에서 제공하는 xSemaphtoreCreateMutex()
또는 xSemaphtoreCreateMutexStatic()
함수를 사용해, 멀티태스킹 환경에서 여러 Task가 동시에 콘솔 입출력을 수행할 때 충돌이 일어나지 않도록 보호한다.
void console_init(void)
{
#if (configSUPPORT_STATIC_ALLOCATION == 1)
{
xStdioMutex = xSemaphoreCreateMutexStatic( &xStdioMutexBuffer );
}
#else
{
xStdioMutex = xSemaphoreCreateMutex();
}
#endif
}
mainSELECTED_APPLICATION = 0
이라는 가정하에, 이제 main_blinky()
함수로 진입한다. 해당 함수 안에는 Timer 및 Task, Scheduler 등이 선언되어, main 함수에서 실질적인 동작을 수행한다.
먼저, 변수 타이머 동작 주기 변수 xTimerPeriod
는 다음과 같이 선언된다.
const TickType_t xTimerPeriod = mainTIMER_SEND_FREQUENCY_MS;
이때, mainTIMER_SEND_FREQUENCY_MS
는
#define mainTIMER_SEND_FREQUENCY_MS pdMS_TO_TICKS(2000UL)
로 정의된다.
즉, 매크로 pdMS_TO_TICKS
를 통해 1 Tick을 2000ms으로 정의하고 (pdMS_TO_TICKS(2000UL)
),
타이머 동작 주기를 1 Tick, 즉 2000ms로 설정해주는 과정이다.
xQueue = xQueueCreate( mainQUEUE_LENGTH, sizeof( uint32_t ));
xQueueCreate
함수로, Queue를 생성해주는 과정이다.
생성된 Queue인 xQueue
는 길이가 mainQUEUE_LENGTH
이고, 각 element의 크기는 32bit 정수 크기인 형태로 생성된다(sizeof( uint32_t))
)
해당 데모에서는 2개의 Task를 생성한다. Task를 생성하기 위해선, xTaskCreate
API를 사용한다.
하나는 Queue로부터 데이터를 받아 메시지를 생성해주며,
xTaskCreate( prvQueueReceiveTask, // Task 함수, 실제 실행될 로직
"RX", // 디버깅 시 확인할 수 있는 이름, 커널 내부에서 직접 쓰이지는 않음
configMINIMAL_STACK_SIZE, // 스택 크기
NULL, // 입력 파라미터, Task가 필요로 한다면 다른 파라미터를 넣어 사용할 수 있음
mainQUEUE_RECEIVE_TASK_PROIRITY, // 우선순위, 어느 Task를 더 자주/빨리 실행할지 결정, 우선순위가 높을수록 CPU 점유 기회가 많아짐
NULL ); // Null을 넘기면 Task 핸들을 별도로 받지 않음, Task 핸들이 필요하면 변수로 받아서 vTaskDelete()등에 활용 가능
그리고, 다른 하나는 Tick을 Count한 값을 Queue에 값을 전송하는 Task이다.
xTaskCreate( prvQueueSendTask,
"TX",
configMINIMAL_STACK_SIZE,
NULL,
mainQUEUE_SEND_TASK_PRIORITY,
NULL );
그리고 그 다음으로 소프트웨어 타이머를 생성해준다.
xTimer = xTimerCreate( "Timer", //타이머 이름, 디버깅용
xTimerPeriod, //타이머 주기
pdTRUE, //pdTRUE면, 타이머가 만료될 떄마다 자동으로 다시 시작, pdFALSE면 한번 만료된 후 자동으로 멈춤
NULL, //타이머 ID, NULL이므로 별도로 ID를 저장하진 않음. 여러 타이머를 관리할 때 식별이 필요하다면 다른 값을 넣을 수 있음
prevQueueSendTimerCallback ); //콜백 함수 : prvQueueSendTimerCallback이 타이머가 만료될 때마다 실행하는 함수를 가리킴
타이머가 정상적으로 생성된 경우, xTimerStart()
함수를 통해 타이머 시작
if( xTimer!=NULL )
{
xTimerStart( xTimer, 0 );
}
vTaskStartScheduler();
모든 Task와 타이머가 생성된 뒤, RTOS 스케줄러를 시작하면, 등록된 Task들이 우선순위와 준비 상태(Ready State)에 따라 CPU를 할당받아 실행된다.
등록된 소프트웨어 타이머도 FreeRTOS 타이머 서비스(Task)에 주기적으로 콜백이 호출되고, 스케줄러가 시작된 후에는 main()
함수로는 다시 돌아오지 않는 것이 일반적이다.
*참고 : RTOS 스케줄러가 시작되면, 내부적으로 무한 반복 구조로 동작한다.
따라서, 일반적인 C프로그램처럼 While(1) 루프를 직접쓰지 않아도, RTOS 스케줄러가 각 Task를 무기한으로 스케줄링하고 실행해준다.
가장 기초적인 예제를 통해, FreeRTOS에서 Task를 생성하는 방법, API를 사용하는 방법 등을 알아봤다. 다음 글에는 Posix_gcc에서 두번째 실행 함수 main_full()
함수를 분석하겠다.