본문 바로가기
Embedded System/소프트웨어 (C,C++)

FreeRTOS 사용하기

by MachineJW 2024. 10. 16.

1. FreeRTOS 개념

RTOS는 임베디드 마이크로컨트롤러(MCU)에 올라가는 커널 운영체제의 일종이다. RTOS 개념과 왜 사용해야하는지에 대해서는 이미 정리한바 있으니 본 블로그의 포스팅을 참고바란다.

https://www.freertos.org/

 

FreeRTOS™ - FreeRTOS™

 

freertos.org

RTOS에서는 TASK를 효율적으로 관리하여 멀티태스킹 동시성을 보장한다.

FreeRTOS 운영체제 공식사이트에서 자세한 내용을 확인할 수 있다.

주요특징으로는

  • 오픈 소스이며 무료이다. (GPL)
  • 각종 의료기기나 자동차 ECU 또한 FreeRTOS 커널을 통해 임베디드 S/W로 사용되어 진다.
  • ARM, AVR, PIC, INTEL, ESP32 등 35개의 마이크로컨트롤러(MCU)에 이미 포팅되어 있으며 컴파일러에도 이식되어 있다.

https://github.com/FreeRTOS

 

FreeRTOS

FreeRTOS(TM) is a market leading RTOS from Amazon Web Services - FreeRTOS

github.com

Github에 FreeRTOS 커널과 데모코드가 공개되어 있다.

2. FreeRTOS 스케줄링 동작 예시

RTOS에서는 작업 상태 및 전환을 관리하고 스케줄링 알고리즘으로 동시구현되게 보장한다.

  1. TASK  1 시작
  2. RTOS 커널에서 TASK1을 스왑아웃(중단)
  3. RTOS 커널에서 TASK2를 스왑인 (실행)
  4. TASK2가 실행되는 동안, 특정 프로세서 주변 장치를 자신의 독점적인 엑세스를 위해 잠금
  5. RTOS 커널에서 TASK2를 스왑아웃(중단)
  6. RTOS 커널에서 TASK3를 스왑인(실행)
  7. TASK3이 TASK2가 잠근 주변 장치에 접근하려고 시도하지만, 잠겨 있는 것을 발견 주변 장치가 해제될 까지 대기 상태로 전환
  8. RTOS 커널에서 TASK2가 다시 실행되며, 주변 장치 사용을 마친 후 잠금을 해제
  9. TASK2가 다시 실행되며 주변 장치 사용을 마친 후 잠금해제
  10. TASK3가 다시 실행되며, 주변 장치에 접근할 수 있는 것을 확인하고 실행을 계속 진행, 이후 다시 스왑아웃

3. FreeRTOS TASK 기본 설계

Arduino 환경에서는 이미 FreeRTOS가 포함되어 있으므로 별도의 라이브러리가 필요없다.

ESP32의 경우 듀얼코어 이므로 FreeRTOS 커널을 사용하여 엄청난 기능을 활용할수 있다.

TaskHandle_t Task1; // FreeRTOS TASK 객체를 생성한다. 

// TASK 에서 실행될 함수를 자유롭게 작성한다.
void task1(void *pvParameters) {
    while (true) {
        Serial.println("Task 1 is running");
        vTaskDelay(pdMS_TO_TICKS(1000)); // 1초 대기
    }
}

// MCU 실행 시에 처음으로 실행되는 setup 함수에 FreeRTOS TASK를 생성한다.
void setup() {
    Serial.begin(115200);
    // FreeRTOS 태스크 생성
    xTaskCreate(task1, "Task1", 2048, NULL, 1, NULL);
}

void loop() {
    // loop()는 비워두거나 다른 작업에 사용
}

 

  • setup 함수의 TASK 생성부분이 가장 중요하다. TASK를 생성하는 함수를 살펴보면 다음과 같다.

[ TASK 생성방법 ]

xTaskCreatePinnedToCore(
      Task1, /* 생성할 태스크 함수 */
      "Task1", /* 태스크 이름 : 디버깅이나 시스템 모니터링시 태스크를 식별하는데 사용 */
      10000,  /* 스택 사이즈 : word 단위로 지정한다. 1 word = 4byte */
      NULL,  /* TASK에 전달할 매개변수 */
      0,  /* 태스크의 우선순위 : 우선순위가 높을수록 더 자주실행 */
      &Task1,  /* 생성된 태스크의 핸들러 */
      0); /* 태스크를 실행할 코어의 번호, ESP32는 듀얼코어 이므로 0은 첫번째 코어, 1은 두번째 코어를 의미한다. */
  • 자주 신경써줘야 하는 부분은 스택사이즈와 우선순위, 그리고 실행할 코어의 번호 정도이다.

헷갈리기도 하지만, 스택 사이즈라 해서 메모리 스택에 할당되는 것이 아니라 힙 영역에 할당된다.

  • 특히, 스택 사이즈를 신경써줘야하는데 스택은 태스크가 실행되는 동안 필요한 메모리 양을 충분히 할당하면서도 낭비를 최소한다. FreeRTOS에서 태스크의 스택은 지역 변수, 함수 호출 스택, 인터럽트 처리 등을 포함한 메모리를 저장하는 데 사용된다.
  • 이를 고려하지 않는다면 스택 오버플로우가 발생한다. (FreeRTOS는 TASK에 메모리를 얼마나 잘 할당해주느냐의 싸움인 것 같다.)

[ 스택 사이즈를 고려하는 가이드 ]

 

  • 지역 변수와 배열: 태스크가 사용하는 모든 지역 변수의 크기를 합산. 예를 들어, int형 변수 10개는 40바이트(4바이트 × 10) 이므로 10 워드가 된다.
  • 지역에서 선언된 배열 크기: 큰 배열이나 구조체를 지역 변수로 선언하면 많은 메모리를 소비한다.
  • 함수 호출 깊이: 재귀 함수나 깊은 함수 호출 체인은 추가적인 스택 메모리를 필요로 한다. 각 함수 호출 시, 함수의 매개변수와 지역 변수가 스택에 쌓인다.
  • 태스크가 실행하는 작업의 복잡도: 복잡한 계산이나 데이터 처리, 특히 큰 버퍼나 데이터 구조를 다루는 경우, 더 많은 스택 메모리가 필요하다.
  • 인터럽트 처리 루틴에서의 스택 사용: 인터럽트 서비스 루틴(ISR)이 태스크 스택을 사용할 수 있으므로 ISR의 스택 요구 사항도 고려해야 한다.

[ 기본적인 스택 크기 가이드  ]

 

  • 단순한 작업: 512 ~ 1024 word (2048 ~ 4096바이트)
  • 복잡한 작업 또는 큰 배열 사용: 2048 ~ 4096 word (8192 ~ 16384바이트)

 

[ FreeRTOS 스택 점검 하는 방법 ]

UBaseType_t remainingStack = uxTaskGetStackHighWaterMark(NULL); // 현재 태스크의 남은 스택
Serial.printf("Remaining stack: %d words\n", remainingStack);

 

  • FreeRTOS는 uxTaskGetStackHighWaterMark 함수를 제공하여 남아 있는 스택의 최소값(하이워터 마크)을 확인할 수 있다. 이 값을 통해 스택이 충분한지 모니터링할 수 있다.
  • 하이워터 마크가 너무 낮으면 스택 오버플로우 가능성이 있으므로 스택 크기를 늘려야 한다.

[ TASK 우선순위를 설정하는 가이드 ]

FreeRTOS에서 우선순위를 결정하는 것은 시스템의 성능과 응답을 최적화하기 위한 중요한 요소이다.

FreeRTOS에서는 태스크의 우선순위는 높은 숫자일 수록 높은 우선순위를 의미하며, 우선순위가 높은 태스크가 더 자주 실행된다.

  • 실시간 요구사항이 있는 태스크: 예를 들어, 센서 데이터 수집, 모터 제어, 통신 등 실시간성이 중요한 태스크는 높은 우선순위를 부여한다.
  • 배경 작업: 데이터 로깅, UI 업데이트, 유지보수와 같은 덜 긴급한 작업은 낮은 우선순위를 설정할 수 있다.
  • 짧은 주기의 태스크: 1ms 주기로 실행해야 하는 태스크는 더 높은 우선순위를 가져야 한다.
  • 긴 주기의 태스크: 1초 주기로 실행되는 태스크는 낮은 우선순위로 설정해도 충분히 수행될 수 있다.
  • 데이터 생산-소비 패턴: 데이터 생성자(프로듀서)와 소비자(컨슈머)가 있다면, 데이터 생성자가 데이터를 제공하기 전에 소비자가 기다리지 않도록 생성자의 우선순위를 높게 설정한다.
  • 락이나 자원 공유: 자원을 공유하는 태스크가 자주 대기 상태에 들어가지 않도록 자원에 접근하는 태스크의 우선순위를 조정한다.
  • CPU 사용률이 높은 태스크: 우선순위가 너무 높은 태스크가 CPU 시간을 독점하지 않도록 주의해야 한다. CPU 부하를 분산하기 위해 상대적으로 낮은 우선순위를 부여하는 것이 좋다.
  • Idle 태스크를 고려한 우선순위 설정: 우선순위가 가장 낮은 태스크(0)는 일반적으로 시스템 Idle 태스크로 사용된다. 이 외의 모든 태스크는 Idle 태스크보다 높은 우선순위를 가져야 한다.

4. FreeRTOS 예제

https://randomnerdtutorials.com/esp32-dual-core-arduino-ide/

 

ESP32 Dual Core with Arduino IDE | Random Nerd Tutorials

The ESP32 is dual core: it comes with 2 microprocessors. In this article we’ll show you how to use both ESP32 cores using Arduino IDE by creating tasks.

randomnerdtutorials.com

해당 예제는 ESP32의 듀얼코어를 활용한 FreeRTOS 사용 예제이다.

/*********
  Rui Santos
  Complete project details at http://randomnerdtutorials.com  
*********/

TaskHandle_t Task1;
TaskHandle_t Task2;

// LED pins
const int led1 = 2;
const int led2 = 4;

void setup() {
  Serial.begin(115200); 
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);

  //create a task that will be executed in the Task1code() function, with priority 1 and executed on core 0
  xTaskCreatePinnedToCore(
                    Task1code,   /* Task function. */
                    "Task1",     /* name of task. */
                    10000,       /* Stack size of task */
                    NULL,        /* parameter of the task */
                    1,           /* priority of the task */
                    &Task1,      /* Task handle to keep track of created task */
                    0);          /* pin task to core 0 */                  
  delay(500); 

  //create a task that will be executed in the Task2code() function, with priority 1 and executed on core 1
  xTaskCreatePinnedToCore(
                    Task2code,   /* Task function. */
                    "Task2",     /* name of task. */
                    10000,       /* Stack size of task */
                    NULL,        /* parameter of the task */
                    1,           /* priority of the task */
                    &Task2,      /* Task handle to keep track of created task */
                    1);          /* pin task to core 1 */
    delay(500); 
}

//Task1code: blinks an LED every 1000 ms
void Task1code( void * pvParameters ){
  Serial.print("Task1 running on core ");
  Serial.println(xPortGetCoreID());

  for(;;){
    digitalWrite(led1, HIGH);
    delay(1000);
    digitalWrite(led1, LOW);
    delay(1000);
  } 
}

//Task2code: blinks an LED every 700 ms
void Task2code( void * pvParameters ){
  Serial.print("Task2 running on core ");
  Serial.println(xPortGetCoreID());

  for(;;){
    digitalWrite(led2, HIGH);
    delay(700);
    digitalWrite(led2, LOW);
    delay(700);
  }
}

void loop() {
  
}