본문 바로가기

FreeRTOS/FreeRTOS 기본 학습

FreeRTOS - 힙 메모리 관리

글의 맨 밑에 출처로 이동하시면 Amazon FreeRTOS 사이트에서 더 깔끔하게 보실 수 있습니다.

힙 메모리 관리

V9.0.0부터는 힙 메모리를 FreeRTOS 애플리케이션에 완전히 정적으로 할당할 수 있습니다. 이 말은 힙 메모리 관리자를 추가할 필요가 없다는 것을 의미합니다.

이번 단원에서 다루는 내용은 다음과 같습니다.

FreeRTOS의 RAM 할당 시점

FreeRTOS와 함께 제공되는 5가지 메모리 할당 체계 예제

각 메모리 할당 체계의 사용 사례

 

전제 조건

FreeRTOS를 사용하려면 C 프로그래밍에 대해 잘 알고 있어야 합니다. 특히 다음 사항에 정통해야 합니다.

 

컴파일 및 링크 단계를 포함한 C 프로젝트의 빌드 방식

스택 및 힙에 대한 개념

표준 C 라이브러리 malloc() 및 free() 함수

동적 메모리 할당과 FreeRTOS에 대한 적합성

이번 안내서에서는 작업, 대기열, 세마포어, 이벤트 그룹 등 커널 객체에 대해 살펴보겠습니다. 이러한 커널 객체들은 FreeRTOS를 최대한 쉽게 사용할 수 있도록 컴파일 단계에서는 정적으로 할당되지 않고 런타임에서 동적으로 할당됩니다. FreeRTOS는 커널 객체가 생성될 때마다 RAM을 할당한 후 커널 객체가 삭제되면 RAM을 해제하여 여유 메모리를 확보합니다. 이러한 정책은 설계 및 계획 부담을 줄일 뿐만 아니라 API를 간소화하고 RAM이 차지하는 공간을 최소화합니다.

동적 메모리 할당은 C 프로그래밍 개념이지만 FreeRTOS 또는 멀티태스킹에만 해당되는 개념은 아닙니다. 커널 객체에 동적으로 할당된다는 점에서 FreeRTOS에 적합하지만 범용 컴파일러에서 제공되는 동적 메모리 할당 체계가 항상 실시간 애플리케이션에 적합한 것은 아닙니다.

표준 C 라이브러리 malloc() 및 free() 함수를 사용하면 메모리를 할당할 수 있지만 다음과 같은 여러 가지 이유로 적합하지 않은 경우도 있습니다.

 

소형 임베디드 시스템에서는 사용하지 못할 수도 있습니다.

동적 메모리 할당을 구현하면 비교적 용량이 커져 소중한 코드 공간까지 차지할 수 있습니다.

스레드 세이프가 거의 없습니다.

결정적이지 않습니다. 함수를 실행하는 데 걸리는 시간이 호출에 따라 다릅니다.

단편화로 인해 어려울 수 있습니다. 힙의 여유 메모리(RAM)가 작은 크기의 블록으로 서로 분리되는 경우 힙이 단편화되는 것으로 알려져 있습니다. 힙이 단편화되면 분리된 여유 힙 블록의 전체 크기가 할당할 수 없는 블록의 크기보다 몇 배 크더라도 여유 힙 블록 하나의 크기가 블록을 할당할 정도로 충분히 크지 않으면 블록을 할당하려는 시도가 실패하게 됩니다.

링커 구성이 복잡해질 수 있습니다.

힙 공간이 다른 변수에서 사용하는 메모리까지 차지할 정도로 커지면 디버그 오류의 원인이 될 수 있습니다.

동적 메모리 할당 옵션

초기 버전의 FreeRTOS는 메모리 풀 할당 체계를 사용했습니다. 이 체계에서는 다른 크기의 메모리 블록으로 구성된 풀을 컴파일 과정에서 사전 할당한 후 메모리 할당 함수를 통해 반환합니다. 이러한 체계가 실시간 시스템에서 흔히 사용되기는 하지만 수많은 지원 요청이 발생하였습니다. 또한 실제로 소형 임베디드 시스템에서 실행할 수 있을 만큼 RAM을 효율적으로 사용하지 못해 결국 중단되었습니다.

현재 FreeRTOS는 메모리 할당을 이식 계층에 포함시켜 처리합니다(주요 코드 베이스에 포함되지 않음). 이는 임베디드 시스템의 다양한 동적 메모리 할당 및 타이밍 요건에 대해서 잘 알고 있기 때문입니다. 애플리케이션 하위 집합인 경우에 한해 동적 메모리 할당 알고리즘 하나로 적당합니다. 또한 동적 메모리 할당을 주요 코드 베이스에서 삭제하면 애플리케이션 개발자가 가능한 경우 자신만의 특정 구현체를 입력할 수도 있습니다.

FreeRTOS에서는 RAM이 필요하면 malloc()이 아닌 pvPortMalloc()을 호출합니다. 이후 RAM을 해제할 때는 커널이 free()가 아닌 vPortFree()를 호출합니다. pvPortMalloc()은 표준 C 라이브러리 malloc() 함수와 프로토타입이 동일합니다. vPortFree() 역시 표준 C 라이브러리 free() 함수와 프로토타입이 동일합니다.

pvPortMalloc()과 vPortFree()는 퍼블릭 함수이기 때문에 애플리케이션 코드에서도 호출할 수 있습니다.

FreeRTOS는 pvPortMalloc()과 vPortFree()를 구현하는 5가지 예제를 함께 제공하며, 5가지 모두 여기에 기록되어 있습니다. FreeRTOS 애플리케이션은 이러한 구현체 예제 중 1개를 사용하거나 애플리케이션 고유의 구현체를 입력할 수도 있습니다.

5가지 예제는 FreeRTOS/Source/portable/MemMang 디렉터리에 위치한 heap_1.c, heap_2.c, heap_3.c, heap_4.c 및 heap_5.c 소스 파일에 정의되어 있습니다.

메모리 할당 체계 예제

FreeRTOS 애플리케이션에는 완전히 정적으로 할당할 수 있기 때문에 힙 메모리 관리자가 따로 필요하지 않습니다.

heap_1

소형 임베디드 시스템은 일반적으로 스케줄러 시작 이전에만 작업을 비롯한 기타 커널 객체를 생성할 수 있습니다. 메모리는 애플리케이션이 실시간 기능을 실행하기 전에 커널에서 동적으로 할당되고, 이후 애플리케이션 수명이 끝날 때까지 할당된 상태를 유지합니다. 이 말은 할당 체계를 선택한 후로는 결정론이나 단편화 같이 복잡한 메모리 할당 문제를 고려할 필요가 없다는 것을 의미합니다. 대신에 코드 크기나 간편성 같은 속성을 고려할 수 있습니다.

heap_1.c는 매우 기본적인 버전인 pvPortMalloc()을 구현하고, vPortFree()는 구현하지 않습니다. 작업 또는 기타 커널 객체를 삭제하지 않는 애플리케이션은 heap_1을 사용할 수 있습니다.

그 밖에 상업적으로 중요하거나, 안전이 필수여서 동적 메모리 할당을 제한하는 시스템 역시 heap_1을 사용하는 것이 가능합니다. 이러한 시스템들은 비결정론과 메모리 단편화, 그리고 잘못된 할당과 관련된 불확실성으로 인해 동적 메모리 할당을 제한하는 경우가 많지만 heap_1은 항상 결정적일 뿐만 아니라 메모리를 단편화할 수 없기 때문입니다.

pvPortMalloc()이 호출되면 heap_1 할당 체계가 단순 배열을 더 작은 블록으로 세분화합니다. 이러한 배열을 FreeRTOS 힙이라고 부릅니다.

배열의 총 크기(바이트)는 FreeRTOSConfig.h에서 configTOTAL_HEAP_SIZE 정의를 통해 설정됩니다. 이러한 방법으로 커다란 크기의 배열을 정의하면 배열에서 메모리가 할당되기 전에도 애플리케이션이 많은 용량의 RAM을 사용하는 것처럼 보일 수 있습니다.

생성되는 작업마다 작업 제어 블록(TCB)과 스택을 힙에서 할당해야 합니다.

다음 그림은 작업이 생성될 때 heap_1이 단순 배열을 어떻게 세분화하는지 나타낸 것입니다. 작업이 생성될 때마다 RAM이 heap_1 배열에서 할당됩니다.

A는 작업 생성 이전의 배열을 나타냅니다. 전체 배열이 여유 RAM입니다.

B는 작업이 1개 생성된 이후의 배열을 나타냅니다.

C는 작업이 3개 생성된 이후의 배열을 나타냅니다.

heap_2

heap_2는 역호환을 지원하기 위해 FreeRTOS 배포판에 포함되어 있습니다. 하지만 새로운 설계에 사용하는 것은 바람직하지 않습니다. 대신에 더욱 많은 기능을 제공하는 heap_4를 사용하는 것이 좋습니다.

heap_2.c 역시 configTOTAL_HEAP_SIZE에서 배열 크기를 정의한 후 이를 세분화합니다. 그런 다음 최적합 알고리즘에 따라 메모리를 할당합니다. 여기에서는 heap_1과 달리 메모리를 해제할 수 있습니다. 다시 말하지만 배열이 정적으로 선언되기 때문에 배열에서 메모리가 할당되기 전에도 애플리케이션이 많은 용량의 RAM을 사용하는 것처럼 보일 수 있습니다.

최적합 알고리즘을 따르기 때문에 pvPortMalloc()이 크기에서 요청 바이트 수와 가장 가까운 여유 메모리 블록을 사용합니다. 예를 들어 다음과 같은 시나리오를 가정하겠습니다.

힙에 각각 5바이트, 25바이트 및 100바이트인 여유 메모리 블록이 3개 있습니다.

pvPortMalloc()을 호출하여 RAM 20바이트를 요청합니다.

요청한 바이트 수에 적합하면서 가장 작은 여유 RAM 블록은 25바이트 블록입니다. 따라서 pvPortMalloc()이 20바이트 블록을 가리키는 포인터를 반환하기 전에 먼저 25바이트 블록을 20바이트 블록 1개와 5바이트 블록 1개로 분할합니다. (heap_2는 블록 크기에 대한 정보를 힙 영역에 저장하여 실제로 분할된 블록 2개의 합이 25보다 작기 때문에 이 예는 지나치게 단순화된 것입니다) 새로운 5바이트 블록은 향후 pvPortMalloc() 호출 시 사용할 수 있습니다.

heap_2는 heap_4와 달리 인접한 여유 블록을 더욱 큰 블록으로 결합하지 않습니다. 그렇기 때문에 단편화에 더 민감합니다. 하지만 할당되는 블록과 이후 해제되는 블록이 항상 동일한 크기라면 단편화는 문제가 되지 않습니다. heap_2는 작업을 반복해서 생성하고 삭제하는 애플리케이션에게 적합합니다. 단, 생성된 작업에 할당되는 스택의 크기가 바뀌지 않는 경우에 한합니다.

다음 그림은 작업 생성 및 삭제 시 heap_2 배열에서 할당되었다가 해제되는 RAM을 나타낸 것입니다.

위 그림은 작업의 생성, 삭제 및 생성이 반복될 때 최적합 알고리즘이 어떻게 이루어지는지 나타낸 것입니다.

A는 작업이 3개 생성된 이후의 배열을 나타냅니다. 용량이 큰 여유블록이 배열 상단을 차지합니다.

B는 작업 중 1개가 삭제된 이후의 배열을 나타냅니다. 이용량이 큰 여유 블록은 배열 상단에 그대로 남습니다. 또한 삭제된 작업의 TCB와 스택에 할당되었던, 작은 용량의 여유 블록 2개가 있습니다.

C는 다른 작업이 생성된 이후의 배열을 나타냅니다. 태스크를 생성하면서

한 번은 새로운 TCB를 할당할 목적으로, 또 한 번은 작업 스택을 할당할 목적으로 pvPortMalloc()을 두 번 호출했습니다. 작업은 xTaskCreate() API 함수를 사용해 생성되며, 자세한 내용은 작업 생성 단원에 나와있습니다. pvPortMalloc() 호출은 xTaskCreate() 내부에서 이루어집니다.

TCB는 모두 정확하게 동일한 크기이기 때문에 최적합 알고리즘에 따라 삭제된 작업의 TCB에 할당되었던 RAM 블록을 재사용해 새로운 작업의 TCB에 할당할 수 있습니다.

새롭게 생성된 작업에 할당되는 스택의 크기도 앞에서 삭제된 작업에 할당된 크기와 동일하기 때문에 최적합 알고리즘에 따라 삭제된 작업의 스택에 할당되었던 RAM 블록을 재사용해 새로운 작업의 스택에 할당할 수 있습니다.

용량이 큰 미할당 블록은 배열 상단에 그대로 남습니다.

heap_2는 결정적이지 않지만 malloc(), free() 같은 대부분 표준 라이브러리 구현체보다 빠릅니다.

heap_3

heap_3.c는 표준 라이브러리 함수인 malloc()과 free()를 사용하기 때문에 링커 구성에서 힙의 크기를 정의합니다. 따라서 configTOTAL_HEAP_SIZE 설정은 아무런 효과가 없습니다.

heap_3은 FreeRTOS 스케줄러를 일시적으로 중지하여 malloc()과 free()를 스레드 세이프로 만듭니다. 스레드 세이프와 스케줄러 일시 중지에 대한 자세한 내용은 리소스 관리 단원을 참조하십시오.

heap_4

heap_1 및 heap_2와 달리 heap_4는 배열을 더욱 작은 블록으로 세분화합니다. 배열이 정적으로 선언되어 configTOTAL_HEAP_SIZE에서 크기를 정의하기 때문에 배열에서 메모리가 할당되기 전에도 애플리케이션이 많은 용량의 RAM을 사용하는 것처럼 보일 수 있습니다.

heap_4는 최초 적합 알고리즘에 따라 메모리를 할당합니다. 또한 heap_2와 달리 인접한 여유 메모리 블록을 더욱 큰 블록으로 결합(병합)합니다. 이를 통해 메모리 단편화의 위험을 최소화합니다.

최초 적합 알고리즘을 따르기 때문에 pvPortMalloc()이 요청 바이트 수를 저장할 만큼 큰 용량의 첫 번째 여유 메모리 블록을 사용합니다. 예를 들어 다음과 같은 시나리오를 가정하겠습니다.

힙에 여유 메모리 블록이 3개 있습니다. 배열 순서는 5바이트, 200바이트, 100바이트입니다.

pvPortMalloc()을 호출하여 RAM 20바이트를 요청합니다.

요청한 바이트 수에 적합한 첫 번째 여유 RAM 블록은 200바이트 블록입니다. 따라서 pvPortMalloc()이 20바이트 블록을 가리키는 포인터를 반환하기 전에 먼저 200바이트 블록을 20바이트 블록 1개와 180바이트 블록 1개로 분할합니다. (heap_4는 블록 크기에 대한 정보를 힙 영역에 저장하여 분할된 블록 2개의 합이 200바이트보다 작기 때문에 이 예는 지나치게 단순화된 것입니다) 새로운 180바이트 블록은 향후 pvPortMalloc() 호출 시 사용할 수 있습니다.

heap_4는 인접한 여유 메모리 블록을 더욱 큰 블록으로 결합(병합)하여 단편화의 위험을 최소화합니다. 따라서 다른 크기의 RAM 블록을 반복해서 할당 및 해제하는 애플리케이션에 적합합니다.

다음 그림은 heap_4 배열에서 할당되었다가 해제되는 RAM을 나타낸 것입니다. 메모리 할당 및 해제 시 heap_4 최초 적합 알고리즘이 메모리 병합과 함께 어떻게 이루어지는지 살펴볼 수 있습니다.

A는 작업이 3개 생성된 이후의 배열을 나타냅니다. 용량이 큰 여유 블록은 배열 상단을 차지합니다.

B는 작업 중 1개가 삭제된 이후의 배열을 나타냅니다. 용량이 큰 여유 블록은 배열 상단에 그대로 남습니다. 또한 삭제된 작업의 TCB 및 스택에 할당되었던 여유 블록도 1개 있습니다. TCB 삭제와 함께 해제된 메모리와 스택 삭제와 함께 해제된 메모리가 각각 별도의 여유 블록 2개로 남지 않습니다. 오히려 더욱 큰 여유 블록 1개로 결합됩니다.

C는 FreeRTOS 대기열이 생성된 이후의 배열을 나타냅니다. 대기열은 xQueueCreate() API 함수를 사용해 생성되며, 자세한 내용은 대기열 사용 단원에 자세하게 나와있습니다. xQueueCreate()가 pvPortMalloc()을 호출하여 대기열에서 사용할 RAM도 할당합니다. heap_4는 최초 적합 알고리즘을 따르기 때문에 pvPortMalloc()이 대기열을 저장할 만큼 큰 용량의 첫 번째 여유 RAM 블록에서 RAM을 할당합니다. 그림에서 할당되는 RAM은 작업 삭제 시 해제된 RAM입니다. 대기열이 여유 블록의 RAM을 모두 사용하지 않기 때문에 블록이 둘로 분할되어 있습니다. 미사용 RAM은 향후 pvPortMalloc() 호출 시 사용할 수 있습니다.

D는 pvPortMalloc()이 FreeRTOS API 함수를 호출하여 간접적으로 호출되지 않고 애플리케이션 코드에서 직접 호출된 이후의 배열을 나타냅니다. 사용자가 할당한 블록의 크기가 첫 번째 여유 블록, 즉 대기열에 할당된 메모리와 다음 TCB에 할당된 메모리 사이의 블록에 적합할 정도로 작습니다. 작업 삭제와 함께 해제된 메모리가 이제 별도의 블록 3개로 분할되었습니다. 첫 번째 블록에는 대기열이 저장됩니다. 두 번째 블록에서 사용자 할당 메모리가 저장됩니다. 세 번째는 여유 메모리입니다.

E는 대기열이 삭제된 이후의 배열을 나타냅니다. 이때 삭제된 대기열에 할당되었던 메모리도 자동으로 해제됩니다. 이제는 사용자 할당 블록의 양쪽에 여유 메모리가 있습니다.

F는 사용자 할당 메모리까지 해제된 이후의 배열을 나타냅니다. 사용자 할당 블록에서 사용했던 메모리가 양쪽 여유 메모리와 결합되면서 더욱 큰 용량의 여유 블록이 생성되었습니다.

heap_4는 결정적이지 않지만 malloc(), free() 같은 대부분 표준 라이브러리 구현체보다 빠릅니다.

heap_4에서 사용하는 배열의 시작 주소 설정

참고: 이번 단원에는 고급 정보가 포함되어 있습니다. heap_4를 사용하면서 이번 단원을 반드시 읽어야 할 필요는 없습니다.

애플리케이션 개발자들은 heap_4에서 사용할 배열을 특정 메모리 주소에 배치해야 하는 경우가 간혹 있습니다. 예를 들어 FreeRTOS 작업에서 사용되는 스택은 힙에서 할당되기 때문에 힙이 느린 외부 메모리보다는 빠른 내부 메모리에 있어야 할 때가 그렇습니다.

기본적으로 heap_4에서 사용하는 배열은 heap_4.c 소스 파일에서 선언됩니다. 배열의 시작 주소는 링커에서 자동으로 설정됩니다. 하지만 configAPPLICATION_ALLOCATED_HEAP 컴파일 시간 구성 상수가 FreeRTOSConfig.h에서 1로 설정되어 있으면 배열이 애플리케이션에서 FreeRTOS를 사용해 선언되어야 합니다. 배열이 애플리케이션에서 선언되면 애플리케이션 개발자가 시작 주소를 설정할 수 있습니다.

configAPPLICATION_ALLOCATED_HEAP이 FreeRTOSConfig.h에서 1로 설정되어 있으면 애플리케이션의 소스 파일 중 1개에서 이름이 ucHeap이고, configTOTAL_HEAP_SIZE 설정에서 크기가 정의되는 uint8_t 배열을 선언해야 합니다.

특정 메모리 주소에 변수를 배치하는 데 필요한 구문은 컴파일러에 따라 다릅니다. 자세한 내용은 사용 중인 컴파일러 설명서를 참조하십시오.

여기에서는 두 가지 컴파일러를 예로 듭니다.

다음은 GCC 컴파일러에서 배열을 선언한 후 .my_heap이라고 하는 메모리 할당 주소에 배치하는 데 필요한 구문입니다.

 

uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] __attribute__ ( (section( ".my_heap" ) ) );

 

다음은 IAR 컴파일러에서 배열을 선언한 후 절대 메모리 주소인 0x20000000에 배치하는 데 필요한 구문입니다.

 

uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] @ 0x20000000;

 

heap_5

heap_5에서 메모리를 할당하거나 해제할 때 사용하는 알고리즘은 heap_4에서 사용하는 알고리즘과 동일합니다. 단, heap_5는 heap_4와 달리 정적으로 선언된 단일 배열에서 메모리를 할당해야 하는 것은 아닙니다. heap_5는 서로 분리된 다수의 메모리 공간에서 메모리를 할당할 수 있습니다. heap_5는 FreeRTOS가 실행되는 시스템의 RAM이 시스템의 메모리 맵에서 연속된, 즉 공간이 없는 단일 블록으로 표시되지 않을 때 유용합니다.

heap_5는 pvPortMalloc() 호출을 위해 유일하게 명시적으로 초기화해야 하는 메모리 할당 체계입니다. 초기화는 vPortDefineHeapRegions() API 함수를 통해 이루어집니다. heap_5를 사용할 때 커널 객체(작업, 대기열, 세마포어 등)를 생성하려면 먼저 vPortDefineHeapRegions()를 호출해야 합니다.

vPortDefineHeapRegions() API 함수

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

void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );

독립된 메모리 영역은 각각 HeapRegion_t 형식의 구조로 설명됩니다. 사용할 수 있는 모든 메모리 영역에 대한 설명이 HeapRegion_t 구조 배열로 vPortDefineHeapRegions()에게 전달됩니다.

 

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 구조 배열의 시작을 가리키는 포인터입니다. 배열 속 각 구조는 heap_5 사용 시 힙에 포함되는 메모리 영역의 시작 주소와 길이를 나타냅니다. 배열 속 HeapRegion_t 구조의 순서는 시작 주소를 기준으로 정렬되어야 합니다. 시작 주소가 가장 낮은 메모리 영역을 나타내는 HeapRegion_t 구조가 배열에서 첫 번째 구조가 되어야 하고, 시작 주소가 가장 높은 메모리 영역을 나타내는 HeapRegion_t 구조는 배열에서 마지막 구조가 되어야 합니다. HeapRegion_t 구조에서 pucStartAddress 멤버가 NULL로 설정되어 있으면 배열의 끝을 의미합니다.

예를 들어 다음 그림과 같이 RAM1, RAM2, RAM3 등 독립된 RAM 블록이 3개 포함된 메모리 맵을 가정하겠습니다. 실행 가능한 코드는 읽기 전용 메모리에 배치되기 때문에 그림에는 없습니다.

다음 코드는 HeapRegion_t 구조의 배열을 나타낸 것입니다. 코드가 모두 RAM 블록 3개를 설명하고 있습니다.

 

/* 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. */ 
  
  }

코드가 RAM을 정확하게 설명하고 있지만 모든 RAM을 힙에 할당하여 다른 변수에 사용할 여유 RAM이 없기 때문에 유용한 예제는 아닙니다.

프로젝트가 빌드되면 빌드 프로세스의 링크 단계에서 RAM 주소를 각 변수에 할당합니다. 링커에 사용할 수 있는 RAM은 일반적으로 링커 스크립트 같은 링커 구성 파일에서 설명됩니다. 위 그림에서 B는 RAM2 또는 RAM3이 아닌 RAM1에 대한 정보가 링커 스크립트에 추가되었다고 가정한 것입니다. 따라서 링커가 변수를 RAM1에 배치하면서 RAM1 상단 주소인 0x0001nnnn만 heap_5에서 사용할 수 있게 됩니다. 0x0001nnnn의 실제 값은 링크 대상인 애플리케이션에 포함된 변수의 전체 크기에 따라 달라집니다. RAM2와 RAM3는 링커가 사용하지 않기 때문에 heap_5에서 사용할 수 있습니다.

위의 코드를 사용한다면 heap_5 하단 주소인 0x0001nnnn에 할당되는 RAM이 변수 저장에 사용되는 RAM과 중복됩니다. 이를 피하기 위해 xHeapRegions[] 배열에서 첫 번째 HeapRegion_t 구조가 시작 주소로 0x00010000이 아닌 0x0001nnnn을 사용할 수 있습니다.

하지만 이러한 방법은 다음과 같은 이유로 권장할 만한 해결책이 아닙니다.

시작 주소를 결정하기 쉽지 않습니다.

HeapRegion_t 구조에 사용되는 시작 주소로 업데이트해야 할 경우 링커에 사용되는 RAM의 크기가 향후 빌드에서 바뀔 수 있습니다.

heap_5에서 사용되는 RAM이 중복되더라도 빌드 도구가 이를 인식하지 못하기 때문에 애플리케이션 개발자에게 경고하지 못합니다.

다음 코드는 더욱 편리하고 쉽게 관리할 수 있는 예제를 설명한 것입니다. 이 코드는 ucHeap이라고 하는 배열을 선언하고 있습니다. ucHeap은 정규 변수이기 때문에 링커에서 RAM1에 할당하는 데이터에 포함됩니다. xHeapRegions 배열에서 첫 번째 HeapRegion_t 구조는 ucHeap의 시작 주소와 크기를 나타내기 때문에 ucHeap이 heap_5에서 관리하는 메모리에 포함됩니다. ucHeap의 크기는 위 그림의 C와 같이 링커에 사용되는 RAM이 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 previous code listing,
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 구조의 배열은 RAM2와 RAM3을 모두 나타내고 있지만 RAM1은 일부만 나타내고 있습니다.

여기에서 설명하는 기법의 이점은 다음과 같습니다.

하드 코딩된 시작 주소를 사용할 필요 없습니다.

HeapRegion_t 구조에서 사용되는 주소가 링커에서 자동으로 설정되기 때문에 링커에서 사용되는 RAM의 크기가 이후 빌드에서 바뀌더라도 항상 올바른 주소를 사용할 수 있습니다.

heap_5에 할당되는 RAM이 링커에서 RAM1에 배치되는 데이터와 중복되지 않습니다.

ucHeap이 너무 크면 애플리케이션이 링크되지 않습니다.

xPortGetFreeHeapSize() API 함수

xPortGetFreeHeapSize() API 함수는 호출 시 힙의 여유 바이트 수를 반환합니다. 이렇게 반환되는 값은 힙 크기를 최적화하는 데 사용됩니다. 예를 들어 커널 객체가 모두 생성한 후 xPortGetFreeHeapSize() 함수가 2000을 반환하면 configTOTAL_HEAP_SIZE 값이 2000까지 줄어들 수 있습니다.

heap_3을 사용할 때는 xPortGetFreeHeapSize() 함수가 제공되지 않습니다.

xPortGetFreeHeapSize() API 함수 프로토타입

 

size_t xPortGetFreeHeapSize( void );

 

다음 표는 xPortGetFreeHeapSize() 반환 값을 나열한 것입니다.

 

파라미터 이름/반환 값

설명

반환 값

xPortGetFreeHeapSize() 호출 시 할당되지 않고 힙에 남아있는 바이트 수를 반환합니다.

xPortGetMinimumEverFreeHeapSize() API 함수

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

xPortGetMinimumEverFreeHeapSize()에서 반환되는 값은 애플리케이션의 힙 공간이 얼마나 줄어들었는지 나타내는 지표입니다. 예를 들어 xPortGetMinimumEverFreeHeapSize() 함수가 200을 반환하면 애플리케이션이 실행된 이후 임의 시점에 힙 공간이 거의 200바이트까지 줄어들었다는 것을 의미합니다.

xPortGetMinimumEverFreeHeapSize() 함수는 heap_4 또는 heap_5를 사용할 때만 제공됩니다.

다음 표는 xPortGetMinimumEverFreeHeapSize() 반환 값을 나열한 것입니다.

 

파라미터 이름/반환 값

설명

반환 값

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

Malloc 실패 후크 함수

pvPortMalloc()은 애플리케이션 코드에서 직접 호출할 수 있습니다. 그 밖에 커널 객체를 생성할 때마다 FreeRTOS 소스 파일에서도 호출 가능합니다. 작업, 대기열, 세마포어 및 이벤트 그룹이 커널 객체에 포함되며, 모두 나중에 자세하게 설명하겠습니다.

표준 라이브러리 malloc() 함수와 마찬가지로 요청 크기의 블록이 존재하지 않아서 pvPortMalloc() 함수가 RAM 블록을 반환하지 못할 경우에는 NULL을 반환합니다. 애플리케이션 개발자가 커널 객체를 생성하면서 pvPortMalloc() 함수를 실행했는데 pvPortMalloc() 호출에서 NULL이 반환되면 커널 객체도 생성되지 않습니다.

pvPortMalloc() 호출 시 NULL이 반환되면 경우 후크(또는 콜백) 함수가 호출되도록 모든 힙 할당 체계 예제를 구성할 수 있습니다.

configUSE_MALLOC_FAILED_HOOK가 FreeRTOSConfig.h에서 1로 설정되면 애플리케이션이 아래와 같은 이름과 프로토타입을 갖는 malloc 실패 후크 함수를 제공해야 합니다. 함수는 애플리케이션에 적합하다면 어떤 방식으로든 구현할 수 있습니다.

 

void vApplicationMallocFailedHook( void );

 

 

 

출처 : https://docs.aws.amazon.com/ko_kr/freertos-kernel/latest/dg/heap-management.html