1. FreeRTOS 개념
RTOS는 임베디드 마이크로컨트롤러(MCU)에 올라가는 커널 운영체제의 일종이다. RTOS 개념과 왜 사용해야하는지에 대해서는 이미 정리한바 있으니 본 블로그의 포스팅을 참고바란다.
FreeRTOS 운영체제 공식사이트에서 자세한 내용을 확인할 수 있다.
주요특징으로는
- 오픈 소스이며 무료이다. (GPL)
- 각종 의료기기나 자동차 ECU 또한 FreeRTOS 커널을 통해 임베디드 S/W로 사용되어 진다.
- ARM, AVR, PIC, INTEL, ESP32 등 35개의 마이크로컨트롤러(MCU)에 이미 포팅되어 있으며 컴파일러에도 이식되어 있다.
Github에 FreeRTOS 커널과 데모코드가 공개되어 있다.
2. FreeRTOS 스케줄링 동작 예시
- TASK 1 시작
- RTOS 커널에서 TASK1을 스왑아웃(중단)
- RTOS 커널에서 TASK2를 스왑인 (실행)
- TASK2가 실행되는 동안, 특정 프로세서 주변 장치를 자신의 독점적인 엑세스를 위해 잠금
- RTOS 커널에서 TASK2를 스왑아웃(중단)
- RTOS 커널에서 TASK3를 스왑인(실행)
- TASK3이 TASK2가 잠근 주변 장치에 접근하려고 시도하지만, 잠겨 있는 것을 발견 주변 장치가 해제될 까지 대기 상태로 전환
- RTOS 커널에서 TASK2가 다시 실행되며, 주변 장치 사용을 마친 후 잠금을 해제
- TASK2가 다시 실행되며 주변 장치 사용을 마친 후 잠금해제
- TASK3가 다시 실행되며, 주변 장치에 접근할 수 있는 것을 확인하고 실행을 계속 진행, 이후 다시 스왑아웃
3. FreeRTOS TASK 기본 설계
Arduino 환경에서는 이미 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의 듀얼코어를 활용한 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() {
}
'Embedded System > 소프트웨어 (C,C++)' 카테고리의 다른 글
[C++] 클래스 상속(Inheritance) 개념 정리 (0) | 2024.10.01 |
---|---|
실시간 운영체제 (RTOS) (1) | 2024.09.04 |
[모듈 제작] 전류 CT 센서 인터페이스 모듈 - RMS 취득 펌웨어 (0) | 2024.08.21 |
[GUI & 터치스크린] LVGL 구현 관련 정리 (0) | 2024.07.31 |
[C/C++] LVGL GUI 라이브러리 (0) | 2024.07.10 |