https://www.freertos.org/Documentation/02-Kernel/07-Books-and-manual/01-RTOS_book
FreeRTOS Documentation - FreeRTOS™
freertos.org
포스트를 시작하기 전, AWS에서 제공하는 공식 튜토리얼 가이드가 있는데, Real Time OS가 뭔지? 시분할 시스템이 뭔지? 선점/비선점 스케줄링이란? ... 궁금해하는 분들에게는 좋은 자료가 될 것 같다.
본격적으로 시작하면서, 새롭게 API를 이제 배워야하는데 정리하기 귀찮고, 이걸 보는 분들도 귀찮을 거라고 생각한다. 그래서, 좀 더 흥미를 높이기 위해 main.c 에서 freeRTOS를 도입했을 때 어떻게 함수가 진행되는가??? 를 중점으로 알아보자.
우선 FreeRTOS를 CubeMX에 넣고 프로젝트 생성하면 다음과 같은 함수들이 while loop 이전에 실행된다.
/* Create the thread(s) */
/* definition and creation of defaultTask */
osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
우선 중요한거 하나 집고 넘어가자. 함수 앞에 os가 붙은 놈들은, CMSIS에서 제공하는 놈들이다!
CubeIDE에서 F3을 누르면 함수의 definition을 따라갈 수 있다. osThreadDef를 눌러보면,
이건 osThreadDef_t 형 구조체를 초기화시켜주는 매크로이고, osThreadDef_t에서 다시 F3을 누르면,
/// Thread Definition structure contains startup information of a thread.
/// \note CAN BE CHANGED: \b os_thread_def is implementation specific in every CMSIS-RTOS.
typedef struct os_thread_def {
char *name; ///< Thread name
os_pthread pthread; ///< start address of thread function
osPriority tpriority; ///< initial thread priority
uint32_t instances; ///< maximum number of instances of that thread function
uint32_t stacksize; ///< stack size requirements in bytes; 0 is default stack size
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
uint32_t *buffer; ///< stack buffer for static allocation; NULL for dynamic allocation
osStaticThreadDef_t *controlblock; ///< control block to hold thread's data for static allocation; NULL for dynamic allocation
#endif
} osThreadDef_t;
아하, 이런 느낌으로 thread의 이름, 주소, 우선순위 등을 CMSIS 호환 format으로 "초기화" 한다는 것을 알 수 있다.
다시 main으로 돌아와서, 적어둔 CMSIS format thread definition에 대해 Create 함수를 호출함을 알 수 있다.
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
F3을 누르면, 다음과 같이 thread에 대한 id를 할당해주는 함수임을 알 수 있다.
/*********************** Thread Management *****************************/
/**
* @brief Create a thread and add it to Active Threads and set it to state READY.
* @param thread_def thread definition referenced with \ref osThread.
* @param argument pointer that is passed to the thread function as start argument.
* @retval thread ID for reference by other functions or NULL in case of error.
* @note MUST REMAIN UNCHANGED: \b osThreadCreate shall be consistent in every CMSIS-RTOS.
*/
osThreadId osThreadCreate (const osThreadDef_t *thread_def, void *argument)
{
TaskHandle_t handle;
if((thread_def->buffer != NULL) && (thread_def->controlblock != NULL)) {
handle = xTaskCreateStatic((TaskFunction_t)thread_def->pthread,(const portCHAR *)thread_def->name,
thread_def->stacksize, argument, makeFreeRtosPriority(thread_def->tpriority),
thread_def->buffer, thread_def->controlblock);
}
else {
if (xTaskCreate((TaskFunction_t)thread_def->pthread,(const portCHAR *)thread_def->name,
thread_def->stacksize, argument, makeFreeRtosPriority(thread_def->tpriority),
&handle) != pdPASS) {
return NULL;
}
}
return handle;
}
여기에서 xTaskCreate()라는 함수가 등장했다. 살펴보면,
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
{
TCB_t *pxNewTCB;
BaseType_t xReturn;
/* If the stack grows down then allocate the stack then the TCB so the stack
does not grow into the TCB. Likewise if the stack grows up then allocate
the TCB then the stack. */
#if( portSTACK_GROWTH > 0 )
{
/* Allocate space for the TCB. Where the memory comes from depends on
the implementation of the port malloc function and whether or not static
allocation is being used. */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );
if( pxNewTCB != NULL )
{
/* Allocate space for the stack used by the task being created.
The base of the stack memory stored in the TCB so the task can
be deleted later if required. */
pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
if( pxNewTCB->pxStack == NULL )
{
/* Could not allocate the stack. Delete the allocated TCB. */
vPortFree( pxNewTCB );
pxNewTCB = NULL;
}
}
}
#else /* portSTACK_GROWTH */
{
StackType_t *pxStack;
/* Allocate space for the stack used by the task being created. */
pxStack = pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e9079 All values returned by pvPortMalloc() have at least the alignment required by the MCU's stack and this allocation is the stack. */
if( pxStack != NULL )
{
/* Allocate space for the TCB. */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /*lint !e9087 !e9079 All values returned by pvPortMalloc() have at least the alignment required by the MCU's stack, and the first member of TCB_t is always a pointer to the task's stack. */
if( pxNewTCB != NULL )
{
/* Store the stack location in the TCB. */
pxNewTCB->pxStack = pxStack;
}
else
{
/* The stack cannot be used as the TCB was not created. Free
it again. */
vPortFree( pxStack );
}
}
else
{
pxNewTCB = NULL;
}
}
#endif /* portSTACK_GROWTH */
if( pxNewTCB != NULL )
{
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e9029 !e731 Macro has been consolidated for readability reasons. */
{
/* Tasks can be created statically or dynamically, so note this
task was created dynamically in case it is later deleted. */
pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
}
#endif /* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE */
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
prvAddNewTaskToReadyList( pxNewTCB );
xReturn = pdPASS;
}
else
{
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
return xReturn;
}
주목할 곳은 바로 여기!!!!
if( pxNewTCB != NULL )
{
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e9029 !e731 Macro has been consolidated for readability reasons. */
{
/* Tasks can be created statically or dynamically, so note this
task was created dynamically in case it is later deleted. */
pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
}
#endif /* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE */
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
prvAddNewTaskToReadyList( pxNewTCB );
xReturn = pdPASS;
}
마치 pxNewTCB에 공간을 확보해준 후, 성공했다면 이걸 준비 큐(Ready List) 에 넣는 다는 것을 알 수 있다!! 이 과정을 통해 FreeRTOS 스케줄러에 태스크가 "등록" 되는 것이다. 이 과정에서 태스크는 Ready State에 들어가니깐 kernel을 start한 후 우선순위에 의해 Running으로 전환되면서 "실행"이 되는거다!
이제 이 그림도 이해할 수 있다!
여기에 또 한가지를 알 수 있다.
osThreadCreate() 함수가 아니라, xTaskCreate() 함수 만으로도 FreeRTOS 커널에 태스크를 등록할 수 있다!!!
이 정보를 얻었으므로 앞으로 연습 코드를 작성할 때 xTaskCreate() 만 써서 간단하게 코딩해볼 것이다.
어쨌든 다시 osThreadCreate()로 돌아오면, TaskCreate 이후 CMSIS 포맷에 맞게 handle 을 해주고 이를 리턴한다. handle은 osThreadId 타입인데, 이는 몇 단계를 거쳐 최종적으로는 Task Control Block 구조체에 해당한다. 이 구조체 포인터를 리턴하는 것이 handle이다. (꼭 직접 F3 눌러가면서 찾아보길 바란다!!)
근데 기본 생성 main.c에서 default Task를 보면,
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for(;;)
{
osDelay(1);
}
/* USER CODE END 5 */
}
이렇게 그냥 무한 루프 함수이기에 딱히 의미없다. 만약 다음과 같이 작성해주면 500ms마다 printf() 하는 task를 수행할 것이다.
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for(;;)
{
printf("Hello RTOS world!\r\n");
osDelay(500);
}
/* USER CODE END 5 */
}
정말 마지막으로 main으로 돌아와서, osKernelStart() 하면, vTaskStartScheduler() 실행되고
/**
* @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;
}
이 함수에서는 어떤 일이 일어날까? 우선 헤더파일에 다음에 해당하는 설명이 있다.
/**
* task. h
* <pre>void vTaskStartScheduler( void );</pre>
*
* Starts the real time kernel tick processing. After calling the kernel
* has control over which tasks are executed and when.
*
* See the demo application file main.c for an example of creating
* tasks and starting the kernel.
*
/*
이 함수 실행 이후로는, real time kernel이 모든 태스크의 실행권한을 갖는다는 것이다.
그럼 이제 CMSIS를 쓰지 않고, 다음과 같이 FreeRTOS API 만 이용해 함수에서 Task 를 만들고 바로 SchedulerStart할 수도 있다.
void vAFunction( void )
{
// Create at least one task before starting the kernel.
xTaskCreate( vTaskCode, "NAME", STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL );
// Start the real time kernel with preemption.
vTaskStartScheduler ();
// Will not get here unless a task calls vTaskEndScheduler ()
}
그럼 xTaskCreate()는 중요한 함수라고 여겨지니, 자세히 분석해보자. 마찬가지로 header file에 친절한 설명이 나와 있다.
* @param pvTaskCode Pointer to the task entry function. Tasks
* must be implemented to never return (i.e. continuous loop).
*
* @param pcName A descriptive name for the task. This is mainly used to
* facilitate debugging. Max length defined by configMAX_TASK_NAME_LEN - default
* is 16.
*
* @param usStackDepth The size of the task stack specified as the number of
* variables the stack can hold - not the number of bytes. For example, if
* the stack is 16 bits wide and usStackDepth is defined as 100, 200 bytes
* will be allocated for stack storage.
*
* @param pvParameters Pointer that will be used as the parameter for the task
* being created.
*
* @param uxPriority The priority at which the task should run. Systems that
* include MPU support can optionally create tasks in a privileged (system)
* mode by setting bit portPRIVILEGE_BIT of the priority parameter. For
* example, to create a privileged task at priority 2 the uxPriority parameter
* should be set to ( 2 | portPRIVILEGE_BIT ).
*
* @param pvCreatedTask Used to pass back a handle by which the created task
* can be referenced.
*
* @return pdPASS if the task was successfully created and added to a ready
* list, otherwise an error code defined in the file projdefs.h
번역해서 정리하면,
- @param pvTaskCode
작업(task) 엔트리 함수의 포인터입니다. 작업은 절대 반환되지 않아야 하며(즉, 무한 루프 형태여야 함), 지속적으로 실행되어야 합니다. - @param pcName
작업의 설명을 위한 이름입니다. 주로 디버깅을 쉽게 하기 위해 사용됩니다.
최대 길이는 configMAX_TASK_NAME_LEN으로 정의되며, 기본값은 16자입니다. - @param usStackDepth
작업 스택의 크기를 지정하는 값으로, 바이트 수가 아닌 변수가 몇 개 들어갈 수 있는지를 나타냅니다.
예를 들어, 스택이 16비트(2바이트) 단위로 구성되고 usStackDepth 값이 100으로 지정되면,
총 100 * 2 = 200 바이트가 스택 저장을 위해 할당됩니다. - @param pvParameters
생성된 작업에서 사용할 매개변수에 대한 포인터입니다. - @param uxPriority
작업이 실행될 우선순위를 지정합니다.
MPU(Memory Protection Unit) 기능이 포함된 시스템에서는,
선택적으로 작업을 특권 모드(privileged mode) 에서 실행할 수 있습니다.
이를 위해 우선순위 값에 portPRIVILEGE_BIT 비트를 설정할 수 있습니다.
예를 들어, 우선순위 2의 특권 작업을 생성하려면,
uxPriority 값을 (2 | portPRIVILEGE_BIT) 로 설정하면 됩니다. - @param pvCreatedTask
생성된 작업을 참조할 수 있는 핸들(handle)을 반환받기 위해 사용됩니다. - @return
작업이 성공적으로 생성되어 준비 리스트(ready list)에 추가되었을 경우 pdPASS를 반환하며,
그렇지 않으면 projdefs.h 파일에 정의된 오류 코드 중 하나를 반환합니다.
이에 대한 예시는,
// Task to be created.
void vTaskCode( void * pvParameters )
{
for( ;; )
{
// Task code goes here.
}
}
// Function that creates a task.
void vOtherFunction( void )
{
static uint8_t ucParameterToPass;
TaskHandle_t xHandle = NULL;
// Create the task, storing the handle. Note that the passed parameter ucParameterToPass
// must exist for the lifetime of the task, so in this case is declared static. If it was just an
// an automatic stack variable it might no longer exist, or at least have been corrupted, by the time
// the new task attempts to access it.
xTaskCreate( vTaskCode, "NAME", STACK_SIZE, &ucParameterToPass, tskIDLE_PRIORITY, &xHandle );
configASSERT( xHandle );
// Use the handle to delete the task.
if( xHandle != NULL )
{
vTaskDelete( xHandle );
}
}
위와 같이 xTaskCode에 무한루프 함수 만들고 이름 만들고(이름은 아무렇게해도 상관없음) STACK SIZE 설정하고 parameter는 있어도 되고 없어도 되고, priority, 참조핸들러(함수 처리 과정에서 값 가져올 때 씀) 주소 매개변수로 넣어주면 된다.
이로써 가장 중요한 API인 xTaskCreate()에 대해 알아보았다. 나머지 부분은, 이 Task 를 조절하는 부분에 해당하니, 나머지도 잘 배워보도록 하자.
'Embedded SW > FreeRTOS' 카테고리의 다른 글
[FreeRTOS] 0. STM32F411RE 환경 설정 (0) | 2025.04.01 |
---|