TIL-FreeRTOS(02)

치삼이·2022년 3월 17일
1
post-thumbnail

1. FreeRTOS Task란?

FreeRTOS의 Task란 어느 한 시점에서 실행되고있는 함수이다. 이러한 Task는 C언어의 함수를 이용해 구현하며, return 값은 void 타입이며 파라미터로는 void * 타입이어야 한다.

void TaskFunction(void *pvParameters);

각각의 Task는 그 자체로 작은 프로그램이며, 무한 loop을 사용한다. 또한 return 을 이용해 종료되지 않으며 더 이상의 작업이 필요가 없을시에는 아래와 같이 vTaskDelete()를 이용해 명시적으로 제거를 해야한다.

void TaskFunction( void *pvParameters )
{
	int32_t lVariableExample = 0;
     /* A task will normally be implemented as an infinite loop. */
     for( ;; )
     {
     /* The code to implement the task functionality will go here. */
     }
	 vTaskDelete( NULL );
}

우리가 FreeRTOS를 사용하는 이유는 사용자의 MCU 내부의 Application에서 다양한 Task들이 병렬적으로 수행되길 원하기 때문이다. 반면 Single core내부에서는 어느 한 시점에는 하나의 Task만 수행된다. FreeRTOS는 주기적으로 Tick Interrupt를 발생해 매 Tick 마다 다음 실행시킬 Task를 선택한다. 이러한 방법으로 내부의 Task들이 동시에 실행되는 것 처럼 느낄수 있게 해준다.

이렇게 한 시점에는 하나의 Task만이 실행되며, 매 Tick 마다 Task에서 Task간에 전환이 이루어진다는 의미는 한 시점에서 한 개의 실행(Running) Task 와 한 개 이상의 실행되지않는(Not Running) Task가 존재한다는 의미이다.

Tick 마다 Running 되는 Task는 우선순위에 의해 변할수도 변하지 않을수도 있지만, Not Running ➔ Running 상태 전환을 'switched in' 혹은 'swapped in' 반대의 경우인 Running ➔ Not Running의 상태 전환을 'switched out' 혹은 'swapped out' 이라고 한다.

1.1 Create Task

BaseType_t xTaskCreate(	TaskFunction_t pvTaskCode, 
                        const char * const pcName, 
                        uint16_t usStackDepth, 
                        void *pvParameters, 
                        UBaseType_t uxPriority, 
                        TaskHandle_t *pxCreatedTask );
  • pvTaskCode

    무한루프를 수행하는 함수를 넣어준다. 위에서 언급했듯 return 타입은 void 며 파라미터로 void * 를 가지고 있는 함수여야 한다.

  • pcName

    Task의 이름이다. 실제 FreeRTOS에서는 사용하지않고 사용자의 Debugging을 위해 존재한다.

  • usStackDepth

    Task마다 할당되는 Stack의 크기를 지정한다. 바이트의 크기가 아닌 Stack 워드의 크기이다. 예를 들어 Stack의 넓이가 32-bit이고 usStackDepth에 100을 넣으면 400byte가 할당이 된다. 이런 최종적인 크기가 2^16^ - 1를 초과해서는 안된다.

  • pvParameters

    pvTaskCode에서 정의했던 파라미터이다. 마찬가지로 타입은 void * 타입으로 정의된다.

  • uxPriority

    Task의 우선순위이다. 0이 가장 낮은 우선순위이며 FreeRTOSConfig.h 내부의 configMAX_PRIORITIES – 1 값이 가장 높은 우선순위 이다.

  • pxCreatedTask

    Task를 제어할수 있는 Handle이다. 이 Handle이용해 다른 Task에서 우선순위를 조정하거나, Task를 삭제하는 등 Task를 외부에서 제어할 수 있다.

  • Returned value

    • pdPASS

      Task가 성공적으로 생성되었음을 의미한다.

    • pdFAIL

      Task 및 Task가 요청한 Stack의 사이즈가에 비해 남은 메모리리가 부족하면 실패하게된다.

1.2 Create Task Example

예제를 통해 이제 실제 FreeRTOS를 이용한 예제를 통해 Task의 실행이 어떻게 이루어지는지 확인해 보자. 개발환경 및 사용한 MCU는 다음과 같다.

  • MCU: STM32F407G-DISC1(STM32F407VG)
  • FreeRTOS Version: FreeRTOSv202122.00

예제를 실행하기에 앞서 몇 가지 설명하자면, 이번 예제들은 STM32F407G-DISC1 내부에서 Task의 실행여부 확인을 위해 USART를 이용한다. 따라서 USART 주변장치 및 인터럽트를 활성화 시켜야한다. 또한 ARM Cortex-M에는 SysTick이라는 시스템을위한 Timer가 존재하고, FreeRTOS는 이 타이머를 Tick Interrupt에 이용한다. 그러므로 자신의 System Clock의 정확한 주파수를 알아야한다. STM32F407G-DISC1 외부 크리스털 8Mhz를 클럭소스로 사용하며, 이를 체배하여 System Clock이 최대값인 168Mhz로 설정했다.

void BoardInit(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
	
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_USART2);
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_USART2);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	USART_InitStructure.USART_BaudRate = 115200;
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	USART_InitStructure.USART_Parity = USART_Parity_No;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	USART_Init(USART2, &USART_InitStructure);
	USART_Cmd(USART2, ENABLE);
	
	USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);

	NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x060;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00;
	NVIC_Init(&NVIC_InitStructure);
	
	return;
}

또 편의성을위해 void USART_Puts(const char *str)void _printf(char *format, ...) 함수를 정의해 두자.

#include <stdarg.h>
#include <string.h>
#include <stdlib.h>

static char _printf_buffer[256] = {0,};

void USART_Puts(const char *str)
{
	while(*str)
	{
		while(!(USART2->SR & USART_FLAG_TXE));
		USART_SendData(USART2, *str++);
		while(!(USART2->SR & USART_FLAG_TXE));
	}
	return;
}

#pragma __printf_args
void _printf(char *format, ...)
{
	va_list args;
	_printf_buffer[0] = 0;
	
	va_start(args, format);
	vsnprintf(_printf_buffer, 255, format, args);
	va_end(args);
	
	USART_Puts(_printf_buffer);
	
	return;
}

EXAMPLE 1에서는 Task가 어떻게 생성이 되는지만을 확인한다.

EXAMPLE 1 main.c

/* EXAMPLE 1 main.c */

#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"

void BoardInit(void);
void vTask1(void *pvParameters);
void vTask2(void *pvParameters);

int main()
{
	SystemInit();
	BoardInit();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
	
	vTaskStartScheduler();
	
	for(;;);
	
	return 0;
}

void vTask1(void *pvParameters)
{
	const char *pcTaskName = "Task 1 is running\r\n";
	volatile uint32_t ul; 
	
	for(;;)
	{
		USART_Puts(pcTaskName);
		for(ul = 0; ul < 0x004FFFFF; ul++);
	}
}

void vTask2(void *pvParameters)
{
	const char *pcTaskName = "Task 2 is running\r\n";
	volatile uint32_t ul; 
	
	for(;;)
	{
		USART_Puts(pcTaskName);
		for(ul = 0; ul < 0x004FFFFF; ul++);
	}
}

EXAMPLE 2에서는 Task에 파라미터를 넘기는 법을 확인한다.

/* EXAMPLE 2 main.c */

#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"

void BoardInit(void);
void vTaskFunction(void *pvParameters);

static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTextForTask2 = "Task 2 is running\r\n";

int main()
{
	SystemInit();
	BoardInit();
	
	xTaskCreate(vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL);
	xTaskCreate(vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL);
	
	vTaskStartScheduler();
	
	for(;;);
	
	return 0;
}

void vTaskFunction(void *pvParameters)
{
	char *pcTaskName;
	volatile uint32_t ul;
	
	pcTaskName = (char *)pvParameters;
	
	for(;;)
	{
		USART_Puts(pcTaskName);
		for(ul = 0; ul < 0x002FFFFF; ul++);
	}
}

두 예제 모두 실행하면 같은 실행결과를 얻을 수 있다.

이제 내부에서 Task들이 어떻게 Running 및 NotRunning 상태로 전환되는지 그림으로 확인해 보자.

t~1~ 일때 Task1은 Running 상태로 들어간다. 그 뒤 t~2~ 때, Tick Interrupt가 발생하면 Task1은 다시 swapped out 되 Not Running 상태로 Task2가 swapped in 되어 Running 상태로 들어간는것을 매 Tick Interrupt 마다 반복한다.

2. 다음 실행될 Task는 무엇이며, 언제 실행이 되는가?

2.1 Task Priorities

일반적인 OS처럼 FreeRTOS는 다음실행시킬 Task를 우선순위에 따라 결정하게 된다(물론 다른 OS 처럼 Task가 오랬동안 대기한다면 에이징이나 나이스 값을 통해 양보를 하진 않는다). xTaskCreate() API를 호출할 때 파라미터로 우선순위를 명시적으로 넘기게 되어 있지만, FreeROTS 스케줄러가 시작된 후에도 vTaskPrioritySet() API로 다시 설정이 가능하다.

이 우선순위라는 값은 0부터 FreeRTOSConfig.h 에 정의되어있는 configMAX_PRIORITIES - 1 의 값으로 설정이 가능하다. 이 configMAX_PRIORITIES 값은 FreeRTOS 스케줄러에 의존적이다. 즉, FreeRTOS는 두 가지 스케줄러가 있으며, 각 방법에 따라 configMAX_PRIORITIES 가 다르다.

  • 일반적 방법

    일반적 방법이란 오직 C만 사용된 방법이다. 일반적 방법에서는 configMAX_PRIORITIES 값에 제한이 있지 않다. 그러나 항상 필요 이상으로 값을 크게 사용하는것은 권장하지 않는다. FreeRTOSConfig.h 에서 configUSE_PORT_OPTIMISED_TASK_SELECTION 이 정의되지 않거나 0으로 정의 되어있으면 일반적 방법으로 스케줄러가 동작한다.

  • 아키텍쳐 최적화 방법

    아키텍쳐 최적화에서는 약간의 어셈블리어가 사용되며, 일반적 방법보다 빠르다. configMAX_PRIORITIES 최대 값은 32를 초과할 수는 없다.

2.2 Time Measurement and the Tick Interrupt

EXAMPLE 1 및 EXAMPLE 2에서 확인했듯이 Task는 번갈아 가며 실행이 된다. 이러한 시분할(time slicing) 방식을 통해 두 Task가 병렬적으로 수행하는 모습을 확인할 수 있다. 사실 Task의 병렬실행 시간을 좀더 확대해서 확인해보면 다음과 같이 짧은 시간동안 커널이 CPU를 점유해 다음 실행시킬 Task를 선택하는 것을 확인할 수 있다.

이러한 시분할의 길이는 Tick Interrupt의 발생주기와 관련이 깊다. FreeRTOSConfig.h 내부에 정의되어 있는configTICK_RATE_HZ 를 사용자가 원하는 상수값으로 설정하면 컴파일시 Tick Interrupt의 주기를 설정할 수 있다. 만약 configTICK_RATE_HZ 를 100으로 설정한다면 시분할의 길이는 10msec (1/100 sec)가 된다. 일반적으로 100을 지정해서 사용하지만 개발 애플리케이션에 따라 원하는 값으로 조정할 수 있다.

아래 예시는 configTICK_RATE_HZ 의 셋팅을 5 와 10으로한 예시이다. configTICK_RATE_HZ 상수가 클수록 Tick Interrupt가 더 빈번하게 일어나 병렬성이 두드러 지지만, 스케줄러의 CPU 점유시간이 늘어나는 단점도 있다.

Tick을 시간(millisecond)으로 변환하고 싶을때는 pdMS_TO_TICKS() 매크로를 이용할 수 있다. 이 매크로를 이용하면 사용자기 원하는 시간 길이 만큼의 Tick 수를 반환해 준다.

TickType_t xTimeInTicks = pdMS_TOTICKS(200);

매크로 내부를 확인해보면 간단한 계산수식이 들어있는것을 확인할 수 있다. 인수로 200을 넘겨주면 configTICK_RATE_HZ 상수와의 곱셈을 한뒤 1000으로 나눠준다. 만약 configTICK_RATE_HZ 100 이라면 20 이 반환이 된다. 매 10msec 마다 Tick Interrupt가 일어나고 Tick Interrupt가 20번 발생하면 200msec가 지나갔다는 뜻이다.

EXAMPLE 3 은 우선순위에 따른 스케줄러의 선택을 보여주는 예시이다.

/* EXAMPLE 3 main.c */

#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"

void BoardInit(void);
void vTaskFunction(void *pvParameters);

static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTextForTask2 = "Task 2 is running\r\n";

int main()
{
	SystemInit();
	BoardInit();
	
	xTaskCreate(vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL);
	xTaskCreate(vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL);
	
	vTaskStartScheduler();
	
	for(;;);
	
	return 0;
}

void vTaskFunction(void *pvParameters)
{
	char *pcTaskName;
	volatile uint32_t ul;
	
	pcTaskName = (char *)pvParameters;
	
	for(;;)
	{
		USART_Puts(pcTaskName);
		for(ul = 0; ul < 0x002FFFFF; ul++);
	}
}

위에 언급했듯 스케줄러는 우선순위가 가장높은 Task를 다음 Task로 선택하기 때문에 Task2만 계속 실행됨을 확인할 수 있다.

실제 MCU내부 Application을 Time 도메인으로 표현하면 아래와 같은 현상이 일어나고 있다.

2.3 Expanding the ‘Not Running’ State

지금까지의 예제를 살펴보면 우선순위가 같은 작업은 병렬 수행이 되고 높은 우선순위의 작업이 있다면, 가장 높은 우선순위의 작업만이 실행됨을 확인할 수 있었다. 지금까지 예제로 살펴본 Task는 스스로 switched out 한것이 아닌 스케줄러에 의해 switched out 된 것이다. 이처럼 계속해서 무엇인가를 실행하는 Task를 'continuous processing' 타입이라고 한다. 이러한 continuous processing 타입의 Task는 일반적으로 낮은 우선순위를 가지고 있다. 또한 예제 3에서 확인했듯 더 높은 우선순위의 continuous processing 타입의 Task가 있다면 우선순위에 밀려 전혀 실행되지 않는다.

이러한 기아현상을 방지하고 좀더 유용한 시스템을 위해서는 이벤트기반 으로 Task를 작성할 필요가 있다. 이벤트 기반이란 Task가 Running 상태로 진입하기 위해서 특정 이벤트를 기다림을 의미한다. 높은 우선순위의 Task들은 이벤트를 기다리고 이벤트가 발생전까지 Not Running 상태를 유지하고 낮은 우선순위의 Task들은 Running 상태를 유지하면 이러한 기아현상을 방지할 수 있다. FreeRTOS에서는 이러한 이벤트 방식을 위해서 Not Running 상태를 여러 하위 상태로 나누고 있다.

  • Block State

    이벤트를 기다리는 상태를 의미한다. 이러한 Block 상태는 두 가지 유형의 이벤트를 기다리기 위한 상태로써 각각의 이벤트는 다음과 같다.

    1. 시간관련 일시적 이벤트

      일시적인 딜레이를 이용하여 특정 시간을 기다리거나 혹은 절대 시각까지 기다리는 이벤트등이 있다.

    2. 동기화 이벤트

      이 이벤트는 다른 Task 혹은 인터럽트에서 발생하는 경우를 의미한다. 예를들면 Task A는 Task B의 연산을 기다리는 동안 계속 Block 상태를 유지할 수 있다.

  • Suspended State

    Block이 차단을 의미한다면 Suspended는 정지상태를 의미한다. Suspended 상태에 진입하는 유일한 방법은vTaskSuspend() API 함수를 호출하는 것이다. 또한 Suspended 상태를 탈출하는 유일한 방법은 vTaskResume() 혹은 xTaskResumeFromISR() 를 호출하는 것이다. 대부분의 Application에서는 Suspended 상태를 사용하지 않는다.

  • The Ready State

    Not Running 상태지만 Block 혹은 Suspended 상태가 아니라면 Task는 Ready 상태라고 한다. 바로 실행이 가능하지만 실행되지 않은 상태를 의미한다.

지금까지의 예제는 Running 상태와 Ready 상태간의 전환만을 나타내었다. 지금 까지 예제에서 문자열을 출력한 뒤 아무 의미가 없는 for 문으로 Delay를 구현하고 이 Delay 중에 스케줄러에 의해 상태 전환이루어 졌으며, 이때의 상태는 Ready이다.

명시적으로 Delay중 Block 상태로 진입하기 위해서는 vTaskDelay() 를 호출하면 된다. 이 API 함수의 원형은 다음과 같다.

void vTaskDelay( TickType_t xTicksToDelay );

vTaskDelay() 를 호출하면 인자로 넘어간 상수만큼 Tick Interrupt를 발생한 뒤 다시 Ready상태로 진입한다. 예를들어 TaskDelay(100) 을 호출했고, 호출 시점에 Tick Count가 1000 이면 Tick Count 1100일때 Ready 상태로 진입한다. pdMS_TO_TICKS() 매크로와 같이 이용한다면, 특정 millisecond만큼 Block 상태로 들어갈 수 있다.

또 다른 Delay API 함수로는 vTaskDelayUntil() 함수가 있다. 이 함수는 vTaskDelay()와 유사하지만 Task내부에서 작업이 얼마나 걸리든 상관없이 일정하게 Task를 switched in 시켜준다. 원형을 확인하면 다음과 같다.

void vTaskDelayUntil( TickType_t * pxPreviousWakeTime, TickType_t xTimeIncrement );

파라미터 TickType_t * pxPreviousWakeTime 은 일반적으로 현재 Tick Count를 집어넣고 TickType_t xTimeIncrement 깨어나기 위해 얼마나 Tick Interrupt를 소비하는지를 적어준다. 여기서 중요한점은 무한 루프에 들어가기전 TickType_t * pxPreviousWakeTime 한번만 초기화 해주는 것이다. 이후에는 vTaskDelayUntil() 내부에서 자동으로 갱신한다.

두 함수 모두 FreeRTOSConfig.h 에서 각각 INCLUDE_vTaskDelayINCLUDE_vTaskDelayUntil 이 1로 정의되어야 한다.

이해를 돕기위해 다음 그림을 확인해보자. Task 내부에는 랜덤으로 1 ~ 9 msec동안 Block 상태로 들어가지 않는 NonBlockingDelay 함수가 있다고 가정하면, Task의 형태는 아마 아래와 같은 형태가 될것이다.

void vTaskFunction1(void *pvParameters)
{
    const TickType_t xDelay250ms = pdMS_TO_TICKS(250);
	unsigned int random = 0;
	
	for(;;)
	{
		random = rand()%9
		NonBlockingDelay(pdMS_TO_TICKS(random));
        vTaskDelay(xDelay250ms)
	}
}

void vTaskFunction2(void *pvParameters)
{
    TickType_t xLastWakeTime;
	unsigned int random = 0;
    
    xLastWakeTime = xTaskGetTickCount();
	
	for(;;)
	{
		random = rand()%9
		NonBlockingDelay(pdMS_TO_TICKS(random));
        vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(250));
	}
}

다음과 같이 Task 내부 수행작업의 시간에 관계없이 일정한 시간 간격으로 Task를 실행할 수 있다.

아래 그림 두번째 Timeline에서 T~a~ ~ T~b~ ,T~b~ ~ T~c~ , T~c~ ~ T~d~ 는 모두 같은 시간간격을 갖는다. 당연하게 xTimeIncrement 보다 Task의 실행시간이 길거나 혹은 Task 종료 시 Tick Count가 pxPreviousWakeTime + xTimeIncrement 보다 클 수 없다.

예제를 이용해 두 함수의 사용법을 확인하면 다음과 같다.

/* EXAMPLE 4 main.c */

#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"

void BoardInit(void);
void vTaskFunction(void *pvParameters);

static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTextForTask2 = "Task 2 is running\r\n";

int main()
{
	SystemInit();
	BoardInit();
	
	xTaskCreate(vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL);
	xTaskCreate(vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL);
	
	vTaskStartScheduler();
	
	for(;;);
	
	return 0;
}

void vTaskFunction(void *pvParameters)
{
	char *pcTaskName;
	const TickType_t xDelay250ms = pdMS_TO_TICKS(250);
	pcTaskName = (char *)pvParameters;
	
	for(;;)
	{
		USART_Puts(pcTaskName);
		vTaskDelay(xDelay250ms);
	}
}
/* EXAMPLE 5 main.c */

#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"

void BoardInit(void);
void vTaskFunction(void *pvParameters);

static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTextForTask2 = "Task 2 is running\r\n";

void vTaskFunction(void *pvParameters);

int main()
{
	SystemInit();
	BoardInit();
	
	xTaskCreate(vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL);
	xTaskCreate(vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL);
	
	vTaskStartScheduler();
	
	for(;;);
	
	return 0;
}

void vTaskFunction(void *pvParameters)
{
	char *pcTaskName;
	TickType_t xLastWakeTime;
	pcTaskName = (char *)pvParameters;
	
	xLastWakeTime = xTaskGetTickCount();
	
	for(;;)
	{
		USART_Puts(pcTaskName);
		vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(250));
	}
}

마찬가지로 EXAMPLE 4 및 EXAMPLE 5의 MCU내부 Application을 Time 도메인으로 표현하면 다음과 같다.

2.4 Combining blocking and non-blocking tasks

EXAMPLE 6에서는 Block 상태로 진입하는 Task와 Block으로 진입하지 않는 Task 두 유형의 Task 동시에 실행되는 상황을 보여준다.

/* EXAMPLE 6 main.c */

#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"

void BoardInit(void);
void vContinuousProcessingTask(void *pvParameters);
void vPeriodicTask(void *pvParameters);

static const char *pcTextForTask1 = "Continuous task 1 is running\r\n";
static const char *pcTextForTask2 = "Continuous task 2 is running\r\n";

int main()
{
	SystemInit();
	BoardInit();
	
	xTaskCreate(vContinuousProcessingTask, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL);
	xTaskCreate(vContinuousProcessingTask, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL);
	xTaskCreate(vPeriodicTask, "Task 3", 1000, (void*)pcTextForTask2, 2, NULL);
	
	vTaskStartScheduler();
	
	for(;;);
	
	return 0;
}

void vContinuousProcessingTask(void *pvParameters)
{
	char *pcTaskName;
	pcTaskName = (char *)pvParameters;
		
	for(;;)
	{
		USART_Puts(pcTaskName);
	}
}

void vPeriodicTask(void *pvParameters)
{
	TickType_t xLastWakeTime;
	const TickType_t xDealy100ms = pdMS_TO_TICKS(100);
	
	xLastWakeTime = xTaskGetTickCount();
	
	for(;;)
	{
		USART_Puts("Periodic task is running\r\n");
		vTaskDelayUntil(&xLastWakeTime, xDealy100ms);
	}
}

3. 가장 낮은 우선순위의 Task: Idle Task 와 우선순위 변경, Task 삭제

3.1 Idle Task

FreeRTOS에서는 모든 한 시점에 Running 상태의 Task가 존재해야한다. 모든 태스크가 Block 혹은 Suspended 상태라면 어떤 Task가 Runnning 상태에 있을때는 Idle Task가 Running 상태로 존재하게 된다. 이 Idle Task는 vTaskStartScheduler() 를 호출하면 자동으로 생성된다.

Idle Task의 우선순위는 가능한 가장낮은 우선순위(0) 을 가지게되며, 이에따라 Idel Task로 인해 Application 내부에 다른 사용자 정의 Task가 Idle Task때문에 기아현상을 겪게해서는 않된다. 만약 Idle Task가 사용자 Task가 같은 우선순위를 가져야 하는경우 FreeRTOSConfig.h 내부에 configIDLE_SHOULD_YIELD 를 1로 설정하는 방법도 존재한다.

3.2 Idle Task Hook Function

Idle Task 역시 Task이기 때문에 Application에서 특별한 기능을 추가하는것이 가능하다. 이때 사용되는 것이 바로 Idle Task Hook Function 이다.

일반적인 Idle Task Hook Function의 기능은 다음과 같다.

  • 낮은 우선순위, continuous processing, 백그라운드 실행 기능을 수행한다.
  • 유휴 시간에 할당된 시간을 측정해 여유 처리 용량을 확인한다.
  • 저전력 진입점의 역할

Idle Task Hook Function을 사용할때는 Block 혹은 Suspended 상태에 진입해서는 안되며, vTaskDelete() API를 사용하는 Task가 있을경우, 무한루프에 빠져서는 안된다. Idle Task는 삭제된 Task를 정리를 하는데, 무한루프에 진입하게되면 삭제된 Task를 정리할 수 없다.

Idle Task Hook Function을 사용하기 위해서는 FreeRTOSConfig.h 내부에 configUSE_IDLE_HOOK 를 1로정의하고 아래 vApplicationIdleHook() 를 정의해주면 된다. vApplicationIdleHook()의 프로토 타입은 다음과 같다.

void vApplicationIdleHook( void );

EXAMPLE 7을통해 사용예시를 확인하자.

/* EXAMPLE 7 main.c */

#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"

void BoardInit(void);
void vTaskFunction(void *pvParameters);

volatile uint32_t ulIdleCycleCount = 0UL;
static const char *pcTextForTask1 = "Task 1 is running\r\n";
static const char *pcTextForTask2 = "Task 2 is running\r\n";

int main()
{
	SystemInit();
	BoardInit();
	
	xTaskCreate(vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL);
	xTaskCreate(vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL);
	
	vTaskStartScheduler();
	
	for(;;);
	
	return 0;
}

void vApplicationIdleHook(void)
{
	ulIdleCycleCount++;
}

void vTaskFunction(void *pvParameters)
{
	char *pcTaskName;
	const TickType_t xDelay250ms = pdMS_TO_TICKS(250);
	pcTaskName = (char *)pvParameters;
	
	for(;;)
	{
		_printf("%sulIdleCycleCount = %d\r\n", pcTaskName, ulIdleCycleCount);
		vTaskDelay(xDelay250ms);
	}
}

3.3 Changing the Priority of a Task

Task의 우선순위를 설정하는 방법은 Task를 생성시 명시적으로 선언하는 방법 외에, vTaskPrioritySet() API 함수를 이용하는 방법도 있다. 이 역시 다른 함수와 마찬가지로 FreeRTOSConfig.h 내부 INCLUDE_vTaskPrioritySet 가 1로 설정되어 있어야 한다. 이 함수의 원형은 다음과 같다.

void vTaskPrioritySet( TaskHandle_t pxTask, UBaseType_t uxNewPriority );

pxTask 는 우선순위를 바꿀 Task의 핸들을 의미하며, NULL을 사용할 경우 호출한 Task 자신을 의미하게 된다. uxNewPriority 는 바뀔 우선순위를 명시하게 된다.

우선순위를 Get 하는 API 함수 역시 존재한다. uxTaskPriorityGet() 는 우선순위를 얻는 함수이다. 이 함수 역시 FreeRTOSConfig.h 내부 INCLUDE_uxTaskPriorityGet 이 1로 설정되어 있어야 한다. 원형은 다음과 같다.

UBaseType_t uxTaskPriorityGet( TaskHandle_t pxTask );

pxTask는 우선순위를 얻을 Task의 핸들을 의미하며, NULL을 넣으면 호출한 Task의 우선순위를 얻을 수 있다.

EXAMPLE 8을 통해 우선순위 설정 예시를 살펴보면 다음과 같다.

/* EXAMPLE 8 main.c */

#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"

void BoardInit(void);
void vTask1(void *pvParameters);
void vTask2(void *pvParameters);

TaskHandle_t xTask2Handle = NULL;


int main()
{
	SystemInit();
	BoardInit();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 2, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle);
	
	vTaskStartScheduler();
	
	for(;;);
	
	return 0;
}

void vTask1(void *pvParameters)
{
	UBaseType_t uxPriority;
	uxPriority = uxTaskPriorityGet(NULL);
	
	for(;;)
	{
		USART_Puts("Task 1 is running\r\n");
		USART_Puts("About to raise the Task2 priority\r\n");
		vTaskPrioritySet(xTask2Handle, (uxPriority + 1));
	}
}
	
void vTask2(void *pvParameters)
{
	UBaseType_t uxPriority;
	uxPriority = uxTaskPriorityGet( NULL );
 
	for( ;; )
	{
		USART_Puts("Task 2 is running\r\n");
		USART_Puts("About to lower the Task 2 priority\r\n" );
		vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
	}
}

vTaskPrioritySet() 이용해 설정된 Task의 우선순위가 실행중인 Task보다 높으면 즉시 실행이된다.

3.4 Deleting a Task

각 Task들은 vTaskDelete() API 함수 호출을 통해 자신 혹은 다른 Task를 삭제할 수 있다. 이 함수역시 FreeRTOSConfig.h 내부에 INCLUDE_vTaskDelete 를 1로 설정해서 사용할 수 있다. 삭제된 Task는 더 이상 존재하지 않고 Running 상태로 진입할 수 없다. 위에서 언급했듯 vTaskDelete() 를 호출했으면 삭제된 Task는 Idle Task에서 정리한다. 따라서 Application이 Idle Task를 실행시 킬 수 있도록 시스템을 설계하자.

vTaskDelete() 의 원형 및 파라미터를 확인하자.

void vTaskDelete( TaskHandle_t pxTaskToDelete );

pxTask 는 삭제할 Task의 핸들을 의미하며, NULL을 사용할 경우 호출한 Task 자신을 삭제한다.

/* EXAMPLE 9 main.c */

#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"

void BoardInit(void);
void vTask1(void *pvParameters);
void vTask2(void *pvParameters);

TaskHandle_t xTask2Handle = NULL;

int main()
{
	SystemInit();
	BoardInit();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 2, NULL);
	
	vTaskStartScheduler();
	
	for(;;);
	
	return 0;
}

void vTask1(void *pvParameters)
{
	const TickType_t xDelay100ms = pdMS_TO_TICKS(100UL);
	
	for(;;)
	{
		USART_Puts("Task 1 is running\r\n");
		xTaskCreate(vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle);
		vTaskDelay(xDelay100ms);
	}
}
	
void vTask2(void *pvParameters)
{
	USART_Puts("Task 2 is running and about to delete itself\r\n");
	vTaskDelete(xTask2Handle);
}

Task 1에서 xTaskCreate() 를 실행해 Task 2를 생성하면, 즉시 실행 이 된다.

4. Scheduling Algorithms

스케줄링 알고리즘이란 Ready 상태에 있는 Task를 Running 상태로 전환하는 일련의 Software 루틴을 의미한다. 지금까지의 예시는 같은 스케줄링 알고리즘을 이용해 Task를 전환했다. 사실 FreeRTOSConfig.h 내부에 configUSE_PREEMPTIONconfigUSE_TIME_SLICING 을 조정해서 알고리즘을 변환할 수 있다.

위에서 언급한 두 상수 외에 configUSE_TICKLESS_IDLE 을 이용해 Tick Interrupt를끈 뒤 저전력으로 진입하는 방법의 스케줄링 알고리즘도 있지만 여기서는 다루지 않는다.

4.1 Prioritized Pre-emptive Scheduling with Time Slicing

Prioritized Pre-emptive Scheduling with Time Slicing 이란 소규모 ROTS Application에서 자주 사용되는 스케줄링 알고리즘이다. 이 알고리즘을 사용하기위해서는 두 매크로를 다음과 같이 사용하면 된다.

  • configUSE_PREEMPTION: 1
  • configUSE_TIME_SLICING: 1

아래 그림은 Prioritized Pre-emptive Scheduling with Time Slicing 알고리즘을 이용한 Time 도메인을 나타낸 것이다.

아래 그림역시 Prioritized Pre-emptive Scheduling with Time Slicing을 이용한 Time 도메인이지만, Task2 와 Idle Task의 우선순위가 같아서 Time slice마다 Idle Task가 실행이 되는것을 알 수 있다. Idle Task와 같은 우선순위 이면서 Idle Task를 실행하지 않게하기 위해서는 configIDLE_SHOULD_YIELD 을 1로 설정하면 된다.

  • configIDLE_SHOULD_YIELD가 0으로 설정되면 유휴 작업은 우선 순위가 더 높은 작업에 의해 선점되지 않는 한 전체 타임 슬라이스 동안 실행 상태를 유지한다.

  • configIDLE_SHOULD_YIELD가 1로 설정되면 유휴 태스크는 준비 상태에 다른 유휴 우선 순위 태스크가 있는 경우 루프의 각 반복에서 양보한다.

아래 그림은 위와 같은 Task들이 같은 우선순위를 갖더라도 Time 도메인이 다른것을 확인할 수 있다.

4.2 Prioritized Pre-emptive Scheduling (without Time Slicing)

Prioritized Preemptive Scheduling without time slicing 은 이전에 설명한 Prioritized Pre-emptive Scheduling with Time Slicing 스케줄링 알고리즘과 동일하지만 Time Slicing은 사용하지 않는다. 이 스케줄링 알고리즘을 사용하기 위해서는 각 매크로를 다음과 같이 설정한다

  • configUSE_PREEMPTION: 1
  • configUSE_TIME_SLICING: 0

아래 그림을 확인하면 Tick 인터럽트 발생이후에도 Time Slicing을 사용하지않는 시나리오를 보여준다. Task2는 Idel Task와 우선순위가 같아서 Tick Interrupt가 발생해도 점유당하지 않는다. 다만 더 높은 Task가 점유한 이후에 스케줄러에 의해 선택받을 수 있다( configIDLE_SHOULD_YIELD 은 0으로 설정).

Time Slicing을 사용하지 않으면 스케줄러의 문맥교환 오버헤드가 줄어드는 장점이 있지만, 같은 우선순위 Task도 처리시간이 달라지기에 경험이 많은 개발자에게 사용이 권장된다.

Co-operative Scheduling

Co-operative Scheduling 이란 점유를 하지 않는 스케줄링 알고리즘이다. 이 스케줄링 알고리즘을 사용하기 위해서는 매크로를 다음과 같이 설정하면 된다.

  • configUSE_PREEMPTION: 0
  • configUSE_TIME_SLICING: Any value

Idle Task가 taskYIELD()를 호출하기 전까지 CPU를 점유하고 있기때문에 우선순위가 높은 Task가 Ready 상태라도 실행될 수없다.

🔗 Cheesam31 GitHub FreeRTOS_Tutorial Repository(Task)

0개의 댓글