TIL-FreeRTOS(01)

치삼이·2022년 2월 27일
0
post-thumbnail

FreeRTOS(01)

1. About FreeRTOS

FreeRTOS는 Richard Barry가 만든 실시간 운영체제이며 2017년 Amazon으로 인수되었다. FreeRTOS는 소형 마이크로 컨트롤러 혹은 마이크로프로세서에서 사용하기 위한 실시간 운영체제로 Soft-Realtime(연성 실시간) 혹은 Hard-Realtime(경성 실시간) 요구하는 어플리케이션에 적합한 운영체제이다.

  • Soft-Realtime

    연성 실시간 시스템은 작업실행에 있어 시간제약이 따르지만 실패해도 시스템에 큰 무리가 가지않는 시스템을 의미한다.

  • Hard-Realtime

    경성 실시간 시스템은 작업실행에 있어 시간제약이 따르며 이를 엄격하게 지켜야하는 시스템을 의미한다.

일반적인 단일코어 프로세서에서는 한 번의 단일 쓰레드만을 실행할 수 있는데 FreeRTOS에서 제공하는 실시간 커널(실시간 스케줄러)를 이용하면 우선순위에 따라 독립적으로 여러 쓰레드를 실행할 수 있다. 또한 FreeRTOS는 Opensource로 누구나 이용가능하며 상업적으로도 이용할 수 있다.

FreeRTOS 🔗 공식홈페이지에서 다운로드 받을수 있으며, 2022년 2월 22일 기준 최신 버전은 202112.00 LTS 버전은 202012.03 각각 커널 버전은 10.4.6 과 10.4.3 이다.

2. FreeRTOS Distribution

공식홈페이지의 LTS 및 Latest 버젼의 트리 구조를 비교하면 다음과 같다. LTS에서는 Demo를 따로 관리하지는 않지만 Latest 에서는 Demo 및 FreeROTS+(FreeRTOS TCP/IP Stack) 에서 AWS 및 기타 TCP/IP 관련 프로토콜을 함께 넣어 놓은 점이다.

  • LTS version Tree

  • Latest version Tree

FreeRTOS의 코어인 📁 FreeRTOS-Kernel(latest ver: 📁 FreeRTOS\Source)의 내부에는 📁 portable📁 icnlude 두 디렉토리 및 핵심 source 파일이 있다. FreeRTOS를 기반으로 한 프로젝트를 진행하려면 공통으로 필요한 소스파일과 컴파일러 및 기종 또는 메모리관리에 맞는 소스파일이 다르다.

2.1 FreeRTOS Source Files Common to All Ports

📁 FreeRTOS-Kernel 디렉토리 내부에는 총 7개 source file이 존재한다. 모든 source file이 반드시 사용되는것은 아니지만, task.c, list.c 두 파일은 반드시 필요하다. 그 외 queue.c 는 거의 항상 쓰이며 나머지 파일은 옵션으로 필요하면 사용하는 소스파일이다.

또 각 프로젝트마다 FreeRTOSConfig.h 파일을 통해 FreeRTOS의 옵션을 선택할 수 있다. 예를들면 configUSE_PREEMPTION 상수를 이용해 스케줄링시 선점여부를 설정할 수 있다.

2.2 FreeRTOS Source Files Specific to a Port

FreeRTOS는 여러 GCC, IAR, Tasking 등 여러 컴파일러 및 다양한 프로세서 아키텍쳐를 지원한다. 📁 portable 디렉토리 내부에는 compiler/architecture 형식으로 디렉토리 구조가 짜여져 있으며 FreeRTOS 및 특정 컴파일러와 마이크로프로세서를 이용한 프로젝트를 진행할 떄 추가해 주어야 한다.

version 9.0.0 이상부터는 FreeRTOS에서 Heap 메모리를 관리기능이 필수사항이아닌 옵션으로 바뀌었다. 9.0.0 이전에서는 반드시 Heap 메모리를 관리해주는 소스코드를 포함하게 되어있으나, 이제 FreeRTOSConfig.h 내부에 configSUPPORT_DYNAMIC_ALLOCATION 매크로가 1이거나 정의되있지 않을때만 Heap관리 소스코드를 필요로한다. Heap은 총 5개의 다른형태로 관리될 수 있다.

3. Data Types and Coding Style Guide

3.1 Data Type

📁 portable 에는 각각의 컴파일러 및 아키텍쳐마다 고유한 portmarcor.h가 있으며, TickType_tBaseType_t 두 타입에 대한 정의가 있다.

  • TickType_t

    FreeRTOS는 주기적으로 Tick Interrupt라는 Timer Interrupt를 발생한다. 이 때 발생하는 인터럽트로 tick count 라는 수를 주기적으로 증가하는데 이 tick count 는 시간을 측정할 때 이용이 된다(매 Tick Interrupt 사이의 일정한 간격을 tick period 라고한다). 이러한 tick count 를 저장하기위한 데이터 타입이 TickType_t 이다.

  • BaseType_t

    BaseType은 보통 리턴타입에 쓰이는 타입이다. 범위가 한정되 있는 값 혹은 Ture/Fasle 같은 불리언 타입에 쓰인다. BaseType은 아키텍쳐마다 데이터 크기가 다르다. 만약 32bit 아키텍쳐라면 32bit 크기를 16bit 아키텍쳐라면 16bit 사이즈를 갖는다.

3.2 Variable Names

변수의 이름에 prefix를 붙이며 각각의 규칙은 다음과 같다.

  • c : char
  • s: int16_t(short)
  • l: int32_t(long)
  • x: BaseType 혹은 구조체 task handles, queue handle 같은 비표준 타입
  • u: unsigned
  • p: pointer

예를들어 uint8_t 타입을 갖는 변수 앞에는 uc 라는 prefix가 붙게된다. 또한 uint32_t * 타입은 pl이 붙게 된다.

3.3 Function Names

함수 리턴타입 및 정의된 파일에 맞춰 prefix를 지정한다. 예시를 들면 다음과 같다.

  • vTaskPrioritySet(): return Type이 void 이며 task.c 에서 정의되어 있다.
  • xQueueRecieve(): return Type이 BaseType_t 이며 queue.c 에 정의되어 있다.
  • pvTimerGetTimerID() return Type이 void pointer 이며, timer.c에 정의되어 있다.

3.4 Macro Names

매크로는 정의되어있는 헤더파일에 따라 정해진다. 아래 테이블을 참조해 보자.

PreifxLocation of macro definition
port (for example, portMAX_DELAY)portable.h or portmacro.h
task (for example, taskENTER_CRITICAL())task.h
pd (for example, pdTRUE)projdefs.h
config (for example, configUSE_PREEMPTION)FreeRTOSConfig.h
err (for example, errQUEUE_FULL)projdefs.h

이밖에 pdTRUE/pdFALSE, pdPASS/pdFAIL 은 각각 1/0 을 나타내며, Tab은 space 4개로 취급한다.

3. Heap Memory Management

Heap 관리기법에서는 운영체제에서 배웠던 이야기들이 나온다. 최적 적합, 최초 적합, 단편화 등...

3.1 Heap 1

Heap 1은 간단하게 설명하면, 커널 객체(Task, Queue, Semaphore...)를 만든 뒤, 삭제를 하지 않는 방법이다. Heap 1을 이용하면 pvPortMalloc() 을 이용해 메모리를 할당받아 커널 객체를 만들 수 있지만, vPortFree()를 이용해 메모리를 해제하여 객체를 제거할 수는 없다.

pvPortMalloc()를 호출하면 Heap을 작은 메모리 부분으로 분리하고 할당을 실시한다. Heap의 총 크기는 FreeRTOSConfig.hconfigTOTAL_HEAP_SIZE 상수를 이용해 설정할 수 있다. Heap크기를 미리 지정하기 때문에 메모리사용량이 많아 보일 수 있다.

아래 그림은 FreeRTOS에서 Task 객체를 Heap에 할당하는 과정을 보여준다. Task는 메모리를 할당받을 때 TCB(Task Control Block)와 Stack을 같이 할당받는다.

3.2 Heap 2

Heap 2는 구형 FreeRTOS를 위해 유지되는 메모리 관리 기법이다. 현재는 Heap 4를 이용해 메모리를 관리하는 것을 추천하고 있다.

Heap 2 는 Heap 1과 다르게 메모리의 할당 및 해제가 가능하다. Heap 2 역시 configTOTAL_HEAP_SIZE로 지정된 만큼의 Heap을 가지고 시작하며 메모리 할당을 받기위해 최적 적합(best fit) 할당 기법을 이용하여 메모리를 할당 받는다. 아래 예시를 살펴보자.

  1. 우선 Task가 3개가 존재해 Heap에 TCB 와 각 TCB와 연관된 Stack이 3개가 존재한다. 이를 살펴보면 Heap에는 A 처럼 할당이 되어있다.
  2. 이때 중간의 Task가 종료되어 메모리에서 해제되었다면 B와 같은 상황이 벌어지게 된다.
  3. 다시 Task가 생성되면, TCB 와 Stack은 최적 적합에 의해 자신의 크기와 가장 잘 맞는 메모리를 할당받게 된다.

Heap 2는 같은 객체의 반복된 할당과 삭제 시 적절한 메모리할당 기법이다. 모든 TCB는 사이즈가 같으므로 Stack의 크기가 동일하다면, 항상 같은 위치에 할당되는 것을 보장한다.

Heap 2의 단편화에 대해 생각해보자 최적알고리즘을 아래 그림 B 까지는 위 의 시나리오와 같다. 하지만 새로운 Task가 생성된것이 아닌 Queue가 생성되고 이 Queue는 TCB보다 크고 Stack의 사이즈보다 작다고 가정하면 최적 할당에 의해 직전 까지 Stack에 할당된 공간에 Queue가 할당될 것이다.

이렇게 객체의 크기가 다르면 단편화가 생길 수 있다.

3.3 Heap 3

Heap 3은 표준 pvPortMalloc()vPortFree() 대신 표준 라이브러리 malloc()free()를 이용하는 방법이다. 이 때 힙의 크기는 링커에 의해 정해지며 configTOTAL_HEAP_SIZE상수는 영향을 미치지 않는다.

Heap 3는 FreeRTOS 스케줄러를 일시적으로 일시 중단하여 malloc()free()를 스레드 세이프하게 만든다.

3.4 Heap 4

Heap 1, Heap 2와 마찬가지로 Heap 4 역시 커널에서 사용하는 configTOTAL_HEAP_SIZE 크기(byte)의 Heap을 미리 지정한 뒤, 이 Heap을 쪼개서 사용한다.

Heap 4는 최초 적합(first fit) 할당 기법을 이용한다. 또한 해지된 서브 메모리이 서로 인접할 경우 하나로 합쳐져 단편화에 대한 위협을 최소화 한다.

  1. 지난 시나리오와 마찬가지로 3개의 Task를 생성해 Heap에 각각 3개의 TCB 와 Stack이 존재한다고 가정하자.(A)
  2. 1개의 Task(가운데)가 작업을 끝내고 메모리에서 해제되면 Heap 2와 달리 1개의 큰 메모리로 합쳐지게된다.(B)
  3. 이때 새로운 커널 객체인 Queue가 할당되면 최초 적합에 의해 방금 해지되었던 메모리에 할당이 된다. (C)
  4. 다시 사용자가 동적할당 요청을 통해 Heap에 요청을하고(D) Queue가 해지된다.(E)
  5. 마지막으로 사용자가 요청했던 메모리가 해지되면 다시 세 공간이 하나로 합쳐지게된다.(F)

3.5 Heap 5

Heap 5의 메모리 할당 및 해제는 기존 Heap 4와 같은 알고리즘을 이용한다. 다만 지금 까지 Heap 관리는 정적으로 선언된 단일 배열이였다면, Heap 5는 서로 분리된 다수의 메모리 공간에서 메모리를 할당할 수 있다.

Heap 5는 다른 Heap 관리 체계와는 다르게 pvPortMalloc() 호출을 하기전에 vPortDefineHeapRegions()를 호출해서 명시적인 초기화를 해줘야한다. vPortDefineHeapRegions API를 살펴보자.

  • vPortDefineHeapRegions() API

    vPortDefineHeapRegions() API는 각각 독립된 메모리 영역의 시작 주소와 크기를 지정할 때 사용된다. 이러한 메모리 영역이 모두 heap_5에서 사용하는 총 메모리가 된다.

    /* The vPortDefineHeapRegions() API function prototype */
    void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );

    분리된 메모리는 HeapRegion_t 을 통해 나타낸다.

    typedef struct HeapRegion
    {
     /* The start address of a block of memory that will be part of the heap.*/
     uint8_t *pucStartAddress;
     /* The size of the block of memory in bytes. */
     size_t xSizeInBytes;
    } HeapRegion_t;

    vPortDefineHeapRegions() 의 파라미터인 pxHeapRegions 에 대해 알아보자. 이 구조체 포인터는 HeapRegion_t 배열을 의미한다. 이 배열에는 규칙이 있는데 각각의 변수들 즉 pxHeapRegions[0] ~ pxHeapRegions[n] 들은 각 pucStartAddress을 기준으로맞추어 정렬이 되어있어야 하며 마지막에는 NULL이 들어간다.

그림을 보며 예시를 확인해보자. 예시에서는 RAM1, RAM2, RAM3 각 세개의 독립된 메모리 맵이 있다. 이때 Heap 5를 사용하기 위해 vPortDefineHeapRegions() 를 호출하려면 다음과 같이 호출할 수 있다.

/* Define the start address and size of the three RAM regions. */
#define RAM1_START_ADDRESS ( ( uint8_t * ) 0x00010000 )
#define RAM1_SIZE ( 65 * 1024 )
#define RAM2_START_ADDRESS ( ( uint8_t * ) 0x00020000 )
#define RAM2_SIZE ( 32 * 1024 )
#define RAM3_START_ADDRESS ( ( uint8_t * ) 0x00030000 )
#define RAM3_SIZE ( 32 * 1024 )
/* Create an array of HeapRegion_t definitions, with an index for each of the three 
RAM regions, and terminating the array with a NULL address. The HeapRegion_t 
structures must appear in start address order, with the structure that contains the 
lowest start address appearing first. */
const HeapRegion_t xHeapRegions[] =
{
 { RAM1_START_ADDRESS, RAM1_SIZE },
 { RAM2_START_ADDRESS, RAM2_SIZE },
 { RAM3_START_ADDRESS, RAM3_SIZE },
 { NULL, 0 } /* Marks the end of the array. */
};
int main( void )
{
 /* Initialize heap_5. */
 vPortDefineHeapRegions( xHeapRegions );
 /* Add application code here. */
}

위 그림의 가정은 RAM1의 정보만 링커스크립트에 추가되었다고 가정한다. 이렇게 되면 사용자 변수 및 자료들이 RAM1에 적재되며, B에 보이는 것처럼 0x01nnnn 번째부터 Heap 으로 사용하게 된다. 이렇게 되면 다음과 같은 문제가 발생하게 된다.

  • 시작주소를 결정하기 힘들다.
  • HeapRegion_t 구조에 사용되는 시작 주소로 업데이트해야 할 경우 링커에 사용되는 RAM의 크기가 향후 빌드에서 바뀔 수 있다.
  • Heap 5에서 사용되는 RAM이 중복되더라도 빌드 도구가 이를 인식하지 못한다.

아래 코드는 위와 같은 상황을 미연에 방지해주는 솔루션이다. ucHeap 배열은 변수이므로 RAM1에 할당이 되며, xHeapRegions 배열에서 첫 번째 HeapRegion_t 구조는 ucHeap의 시작 주소와 크기를 나타내기 때문에 Heap 5에서 관리하는 메모리에 포함이 된다. ucHeap의 크기는 위 그림의 C의 상황과 같이 RAM1을 소진할 때 까지 할당받을 수 있다.

/* Define the start address and size of the two RAM regions not used by the 
linker. */
#define RAM2_START_ADDRESS ( ( uint8_t * ) 0x00020000 )
#define RAM2_SIZE ( 32 * 1024 )
#define RAM3_START_ADDRESS ( ( uint8_t * ) 0x00030000 )
#define RAM3_SIZE ( 32 * 1024 )
/* Declare an array that will be part of the heap used by heap_5. The array will be 
placed in RAM1 by the linker. */
#define RAM1_HEAP_SIZE ( 30 * 1024 )
static uint8_t ucHeap[ RAM1_HEAP_SIZE ];
/* Create an array of HeapRegion_t definitions. Whereas in Listing 6 the first entry 
described all of RAM1, so heap_5 will have used all of RAM1, this time the first 
entry only describes the ucHeap array, so heap_5 will only use the part of RAM1 that 
contains the ucHeap array. The HeapRegion_t structures must still appear in start 
address order, with the structure that contains the lowest start address appearing 
first. */
const HeapRegion_t xHeapRegions[] =
{
 { ucHeap, RAM1_HEAP_SIZE },
 { RAM2_START_ADDRESS, RAM2_SIZE },
 { RAM3_START_ADDRESS, RAM3_SIZE },
 { NULL, 0 } /* Marks the end of the array. */
};

이런 기법은 다음과 같은 이점을 갖게 된다.

  • 하드 코딩된 시작 주소를 사용할 필요 없습니다.
  • HeapRegion_t 구조에서 사용되는 주소가 링커에서 자동으로 설정되기 때문에 링커에서 사용되는 RAM의 크기가 이후 빌드에서 바뀌더라도 항상 올바른 주소를 사용할 있다.
  • Heap 5에 할당되는 RAM이 링커에서 RAM1에 배치되는 데이터와 중복되지 않는다.
  • ucHeap 이 너무 크면 애플리케이션이 링크되지 않는다.
  • xPortGetFreeHeapSize()

    xPortGetFreeHeapSize() API 함수는 호출 시 힙에 남아있는 자유공간의 수를 반환한다.

  • xPortGetMinimumEverFreeHeapSize()

    xPortGetMinimumEverFreeHeapSize() API 함수는 FreeRTOS 애플리케이션이 실행된 이후 지금까지 할당되지 않고 힙에 존재하는 최소 바이트 수를 반환한다.

  • Malloc Failed Hook Functions

    malloc() 을 이용할때와 마찬가지로 pvPortMalloc() 역시 메모리할당 요청에 실패하면 NULL을 반환하게 된다. 이때 FreeRTOSConfig.hconfigUSE_MALLOC_FAILED_HOOK 이 활성화 되어 있다면(1로 설정되어 있다면), 아래 와 같은 이름을 갖는 프로토타입을 개발자가 직접 만들어 주어야 한다.

    void vApplicationMallocFailedHook( void );

0개의 댓글