본 문서는 ESP32S3를 사용하였습니다.

FreeRTOS (ESP-IDF) - ESP32-S3 - — ESP-IDF Programming Guide latest documentation

ESP-IDF 는 FreeRTOS 기반 ESP MCU 개발 프레임워크 입니다.

바닐라 FreeRTOS 는 싱글 코어를 기반으로 설계된 운영체제로, 듀얼코어를 사용하는 ESP를 위해 ESP-IDF 에서는 제조사에서 수정된 FreeRTOS 를 사용합니다.

태스크의 생성

Task를 생성하는 바닐라 FreeRTOS 는 다음 함수를 사용합니다.

  • xTaskCreate()
  • xTaskCreateStatic()

ESP-IDF 에서는 PinnedToCore 시리즈 함수를 사용해서 태스크를 실행할 CPU를 지정할 수 있습니다.

  • xTaskCreatePinnedToCore()
  • xTaskCreateStaticPinnedToCore()

기본적으로 xTaxkCreate...() 함수와 사용법은 같으나, 마지막에 CPU 번호를 지정해주는 매게 변수가 있습니다.

ESP32 와 ESP32S3 기준 다음 인자값을 넣어줄 수 있습니다.

  • 0 태스크를 CPU0 에 할당
  • 1 태스크를 CPU1 에 할당
  • tskNO_AFFINITY 두 CPU에서 작업을 실행할 수 있음

FreeRTOS 의 기본 태스크 생성 함수인 xTaskCreate...() 함수를 사용할 수는 있지만,
이들은 tskNO_AFFINITY 를 인자로 사용하는 PinnedToCore() 시리즈를 호출하는 함수로 수정되어 있습니다.

태스크는 기본적으로 바닐라 FreeRTOS 와 동일합니다.

태스크는 다음 특징을 가집니다.

  • 다음 상태 중 하나의 상태를 가짐
    • Running 실행중
      태스크가 실행중인 상태입니다.
    • Ready 준비됨
      실행 가능하지만, 우선 순위가 높은 태스크 혹은, 우선순위가 같지만 다른 태스크가 실행중인 상태에 있기 때문에 실행이 불가능한 상태 입니다.
    • Blocked 대기중
      지연 혹은 이벤트 대기중인 상태 입니다. 대기중에 있는 태스크는 항상 제한 시간을 가지고 있으며, 대기중인 태스크는 다른 태스크에게 순위를 양보 합니다.
    • Suspended 중지됨
      vTaskSuspend() 로 태스크를 중지하고, xTaskResume() 함수로 태스크의 중지 상태를 벗어날 수 있습니다. 중지 상태의 태스크는 스케줄링 되지 않으며, 제한시간을 명시하지 않습니다.
  • 태스크는 보통 무한루프로 구현됩니다.
  • 태스크는 절대로 리턴하지 않습니다.
    • 태스크는 return 을 호출하거나 함수의 끝에 도달하지 않도록 구성해야 합니다.
      무한 루프로 구현하거나, vTaskDelete() 함수를 호출하여 태스크의 종료를 명시해야 합니다.
  • 우선순위에 관한 내용은 스케줄링 파트에 설명하겠습니다.

xTaskCreate()

새 작업을 생성하고 실행할 준비가 된 작업 목록에 추가합니다.

xTaskCreate() 는 내부적으로 필요한 메모리를 함수 내부에서 동적으로 할당합니다.

파라미터

  • TaskFunction_t pxTaskCode

void 를 리턴하고 void* 를 매게변수로 사용하는 함수 포인터

  • const char* const pcName

태스크 이름 configMAX_TASK_NAME_LEN 매크로에 \0 룰 포함한 문자열의 최대 길이가 정의되어 있습니다. 일반적으로 FreeRTOS는 이 매게변수를 사용하지 않으며, 디버깅 목적으로 사용합니다.

  • const configSTACK_DEPTH_TYPE usStackDepth

작업 스택의 크기 입니다. 바이트 수로 정의됩니다. (워드 사이즈 X)

  • void* const pvParameters

태스크의 매게변수 포인터

  • UBaseType_ t uxPriority

작업 우선순위 입니다. MPU 시스템을 지원하는 경우, portPRIVILEGE_BIT 매크로를 OR 연산해서 시스템 모드로 설정할 수 있습니다. 숫자가 커질수록 우선순의가 높은 작업이며, 입력가능한 범위는 0~(configMAX_PRIORITIES - 1) 입니다.

  • TaskHandle_t* const pxCreatedTask

생성된 작업의 핸들러를 저장할 변수 포인터

  • return

작업이 성공적으로 생성된 경우 pdPASS 를 리턴하며, 그렇지 않은 경우 오류 코드를 리턴 합니다.

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "sdkconfig.h"

static const char *TAG = "taskTest";

// hello 를 무한이 로그하는 태스크
static void taskTest(void* args) {
    while (true) {
        ESP_LOGI(TAG, "hello");
        vTaskDelay(100);
    }
}

TaskHandle_t handle;

void app_main() {
    // 태스크 생성
    xTaskCreate(taskTest, "test", 4096, NULL, 10, &handle);
}

xTaskCreateStatic()

xTaskCreate() 와 기본적으로 동일 하지만, 태스크의 메모리를 정적으로 선언해서 전달 해야하는 태스크 생성 함수입니다.

파라미터

  • TaskFunction_t pxTaskCode
  • const char* const pcName
  • const uint32_t ulStackDepth

puxStackBuffer의 바이트 길이를 입력합니다.

  • void* const pvParameters
  • UBaseType_ t uxPriority
  • StaticTask_t* const puxStackBuffer

StackType_t 변수의 배열을 전달하면 되며, 최소 ulStackDepth 의 길이를 가져야 합니다. 이는 태스크의 스택으로 사용됩니다.

  • StackTask_t* const pxTaskBuffer

StaticTask_t 변수 포인터를 전달하면 되며, 태스크의 데이터 구조를 유지하는데 사용합니다.

  • return

태스크 핸들 혹은 에러 코드

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "sdkconfig.h"

static const char *TAG = "taskTest";

#define STACK_SIZE 200
StaticTask_t xTaskBuffer;
StackType_t xStack[STACK_SIZE];

// hello 를 무한이 로그하는 태스크
static void taskTest(void* args) {
    while (true) {
        ESP_LOGI(TAG, "hello");
        vTaskDelay(100);
    }
}

TaskHandle_t handle;

void app_main() {
    // 태스크 생성
    xTaskCreateStatic(taskTest, "name", STACK_SIZE, NULL, 2, xStack, &xTaskBuffer);
}

xTaskCreatePinnedToCore()

기본적으로 xTaskCreate() 와 동일 하지만,  태스크를 실행할 CPU를 지정하는 매게변수를 지정 해 주어야 합니다.

xTaskCreate() 함수는 내부적으로 xTasckCreatePinnedToCore() 함수가 tskNO_AFFINITY 인자로 호출 됩니다.

파라미터

  • TaskFunction_t pxTaskCode
  • const char* const pcName
  • const configSTACK_DEPTH_TYPE usStackDepth
  • void* const pvParameters
  • UBaseType_ t uxPriority
  • TaskHandle_t* const pxCreatedTask
  • const BaseType_t xCoreID

0, 1, tskNO_AFFINITY 을 입력하며, configNUM_CORES 보다 큰 값을 입력 시, 태스크 실행에 실패합니다.

  • return
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "sdkconfig.h"

static const char *TAG = "taskTest";

// hello 를 무한이 로그하는 태스크
static void taskTest(void* args) {
    while (true) {
        ESP_LOGI(TAG, "hello");
        vTaskDelay(100);
    }
}

TaskHandle_t handle;

void app_main() { 
    // 태스크 생성
    xTaskCreatePinnedToCore(taskTest, "test", 4096, NULL, 10, &handle, tskNO_AFFINITY);
    // xTaskCreate(taskTest, "test", 4096, NULL, 10, &handle); 와 동일
}

xTaskCreateStaticPinnedToCore()

xTaskCreateStatic() 과 동일하지만, 태스크를 실행할 CPU를 지정 할 수 있습니다.

파라미터

  • TaskFunction_t pxTaskCode
  • const char* const pcName
  • const uint32_t ulStackDepth
  • void* const pvParameters
  • UBaseType_ t uxPriority
  • StaticTask_t* const puxStackBuffer
  • StackTask_t* const pxTaskBuffer
  • const BaseType_t xCoreID
  • return
// ...
xTaskCreateStatic(taskTest, "name", STACK_SIZE, NULL, 2, xStack, &xTaskBuffer, 1);
// ...

태스크 삭제

바닐라 FreeRTOS 에서 태스크 삭제 함수로 vTaskDelete() 함수를 사용합니다.

vTaskDelete() 함수의 인자로, 삭제할 태스크의 핸들러 혹은 NULL 값을 지정하면 됩니다.

NULL 값을 입력하면, vTaskDelete() 함수를 호출한 태스크가 종료 됩니다. 태스크를 완료하고, 사용한 자원을 정리한 후 스스로 호출하면 됩니다.

ESP-IDF에서도 동일한 함수를 제공하지만, 이중 코어의 특성으로 인해 바닐라 FreeRTOS 와 다른 동작 차이가 존재합니다.

  • 다른 코어에 Pinned된 태스크를 삭제할 때 해당 작업의 메모리는 항상 다른 코어의 Idle 태스크에 의해 해제됩니다(FPU 레지스터를 지워야 하기 때문에).
  • 다른 코어에서 현재 실행 중인 작업을 삭제하면 다른 코어에서 양보가 트리거되고 태스크의 메모리는 Idle 태스크중 하나에 의해 해제됩니다(태스크의 코어 선호도에 따라 다름).

vTaskDelete() 함수로 태스크를 삭제할 때 다음 상황일 경우 태스크가 점유중인 메모리는 즉시 해제됩니다.

  • 태스크가 실행중이며 Pinned 된 코어에서 호출
  • 실행중인 태스크가 아니며 어떤 코어에서도 Pinned 되지 않음

다른 코어의 실행중인 태스크에 vTaskDelete() 를 호출하면 안됩니다. 해당 태스크가 무엇을 실행중인지 예측하기 어렵기 때문에, 다음과 같은 상황이 발생할 수 있습니다.

  • 뮤텍스를 보유한 작업 삭제 - 데드락 유발
  • 할당한 메모리를 해제하지 않은 상태에서 태스크 삭제 - 메모리 누수 발생

가능한 다음처럼 안전하게 태스크가 삭제되도록 설계를 하는 것이 좋습니다.

  • 점유중인 자원(메모리, 뮤텍스 등) 을 정리하고 vTaskDelete(NULL) 을 호츌하여 스스로 태스크 삭제
  • 다른 태스크가 삭제하기 전, 스스로 Suspend 상태로 전환
// 다른 태스크 삭제 예시

TaskHandle_t xHandle;
xTaskCreate( vTaskCode, "NAME", STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );
vTaskDelete( xHandle );

// 스스로의 태스크 삭제 예시
vTaskDelete( NULL );

스케줄링

바닐라 FreeRTOS는 고정 우선순위 선점형 시분할 스케줄링을 사용합니다.

우선순위

  • 태스크는 생성 시 우선순위가 지정됩니다.

우선순위는 0~(configMAX_PRIORITIES - 1)  의 값을 가지며, 숫자가 작을수록 낮은 우선순위임을 의미합니다. 우선순위의 값이 클수록 많은 RAM 자원을 소비하기 때문에, 가능한 낮은 값을 사용하도록 구성하는 것이 좋습니다.

  • 스케줄러는 우선순위가 높은 준비 상태 태스크부터 태스크를 실행합니다.
  • 테스크가 대기중이면 다른 테스크에게 순위를 양보합니다.
  • 바닐라 FreeRTOS 와 다르게 다중코어 상태의 ESP-IDF는 각 코어별로 테스크 우선순위에 따라 테스크가 배정 됩니다.

동일한 우선순위

동일한 우선순위를 가지는 테스크는 라운드 로빈 방식으로 각 테스크를 작업을 전환하며 테스크를 실행 합니다.

IDLE 태스크

스케줄러가 시작될 때 자동적으로 생성되는 우선순위가 0인 태스크 입니다.

  • 삭제된 태스크 메모리 해제
  • IDLE 후크 애플리케이션 실행

바닐라 FreeRTOS 와 다르게 ESP-IDF 는 각 코어에 IDLE 테스크가 생성 됩니다.