176_아두이노메가_운영체제_FreeRTOS기초 ⏱️🎼
단일 루프의 한계를 넘어 실시간 멀티태스킹의 세계로! 아두이노 메가에 탑재하는 실시간 운영체제(RTOS)
베어메탈(Super Loop)의 delay() 병목 한계, 8KB 대용량 SRAM을 활용한 FreeRTOS 포팅 원리, 태스크(Task) 생성 및 선점형 스케줄러(Preemptive Scheduler) 실습을 진행합니다.

- 🖥️ 강의 슬라이드: slides.md
1. 아두이노에 운영체제(OS)가 왜 필요할까요? 🤔
지금까지 우리가 작성한 아두이노 코드는 모두 void setup()이 한 번 실행된 후, void loop() 함수가 무한히 뱅글뱅글 도는 단일 순차 실행 방식(Bare-metal Super Loop)이었습니다.
하지만 대형 로봇이나 복잡한 스마트팩토리 시스템을 만들다 보면 다음과 같은 치명적인 벽에 부딪힙니다:
delay()의 재앙: 모터를 부드럽게 돌리기 위해delay(500)을 넣는 순간, 그 0.5초 동안 초음파 센서도 못 읽고 화재 감지 경보기도 작동을 멈춥니다!- 복잡한
millis()상태 머신: 여러 동작을 동시에 하려고millis()타이머 변수를 10개씩 만들다 보면 코드가 걷잡을 수 없이 꼬여버립니다. - 엄격한 시간 보장 실패: 센서 데이터를 10ms마다 정확히 읽어야 하는데, 무거운 LCD 화면을 그리는 작업 때문에 타이밍을 놓치게 됩니다.
👧 도로시: “지니야! 초음파 센서로 장애물도 피해야 하고, 모터 속도도 조절해야 하고, 블루투스 명령도 받아야 하는데 코드가 서로 엉켜서 멈칫거려!”
🐱 지니: “그럴 때 바로 컴퓨터의 Windows나 스마트폰의 Android처럼 일정을 분배해 주는 실시간 운영체제(FreeRTOS)를 올리면 된단다! 각 작업을 독립된 ‘태스크’로 쪼개어 마치 여러 개의 두뇌가 동시에 일하는 것처럼 만들어주지!”
🐶 토토: “멍멍! (마치 오케스트라 지휘자처럼 순서와 박자를 맞춰준다 멍!)”
2. 왜 우노는 힘들고 메가 2560에서 RTOS가 날아다닐까? 💾
운영체제(RTOS)가 여러 작업을 동시에 번갈아가며 실행(문맥 교환, Context Switching)하려면, 각 태스크마다 CPU 레지스터와 로컬 변수를 보관할 독립된 스택(Stack) 메모리를 할당해야 합니다.
[ SRAM 메모리 크기와 RTOS 수용 능력 비교 ]
■ 아두이노 우노 (SRAM 2 KB)
┌──────────────────────────────────────┐
│ 시스템 변수 + FreeRTOS 커널 (1.2KB) │ -> 태스크 2개만 만들어도 스택 오버플로우로 다운!
└──────────────────────────────────────┘
■ 아두이노 메가 2560 (SRAM 8 KB! - 4배의 여유 공간)
┌───────────────────┬─────────┬─────────┬─────────┬─────────┬──────────────────────┐
│ 커널 기본 (1.5KB) │ 태스크1 │ 태스크2 │ 태스크3 │ 태스크4 │ 넉넉한 여유 힙 버퍼 │
└───────────────────┴─────────┴─────────┴─────────┴─────────┴──────────────────────┘
- 우노(2KB SRAM): 태스크 1개당 최소 150~200바이트의 스택이 필요한데, 2~3개만 띄워도 메모리가 고갈되어 MCU가 재부팅(Crash)됩니다.
- 메가(8KB SRAM): 넉넉한 8KB 덕분에 센서 태스크, 통신 태스크, 디스플레이 태스크, 모터 PID 태스크 등 5~8개의 태스크를 동시에 여유롭게 구동할 수 있습니다!
3. FreeRTOS 라이브러리 설치와 기본 뼈대 📦
아두이노 IDE에서 오픈소스 실시간 OS인 FreeRTOS를 설치합니다.
- IDE 메뉴:
스케치(Sketch)➡️라이브러리 포함하기➡️라이브러리 관리... - 검색창에
FreeRTOS입력 ➡️ Richard Barry의FreeRTOS(또는 Phillip Stevens의FreeRTOS) 라이브러리를 설치합니다. - 코드 최상단에
#include <Arduino_FreeRTOS.h>를 선언하면 준비 완료!
📌 FreeRTOS의 핵심 함수 2가지
xTaskCreate(): 새로운 독립 작업(태스크)을 생성하고 스케줄러에 등록합니다.xTaskCreate( 함수이름, // 실행할 태스크 함수 포인터 "태스크이름", // 디버깅용 텍스트 이름 스택크기, // 할당할 스택 바이트 수 (메가 기준 128~256 추천) 전달파라미터, // NULL 또는 전달할 데이터 주소 우선순위, // 0(가장 낮음) ~ 3(높음) &태스크핸들 // NULL 또는 태스크 제어용 핸들 );vTaskDelay(): 마이크로컨트롤러를 멍때리게 만드는delay()와 완전히 다릅니다!
자신이 지정된 시간 동안 잠자는 동안, 다른 태스크가 CPU를 쓸 수 있도록 권한을 즉시 양보(Yield)합니다.
4. 실전 실습: 3대 태스크 병렬 멀티태스킹 🚀
메가 2560에서 3개의 독립된 작업이 서로의 타이밍을 방해하지 않고 완벽하게 동시 실행되는 예제입니다:
- 태스크 1: 13번 내장 LED 점멸 (0.5초 주기)
- 태스크 2: A0 아날로그 센서 고속 수집 및 필터링 (0.1초 주기)
- 태스크 3: PC 시리얼 모니터로 종합 리포트 전송 (1초 주기)
/*
* [아두이노 메가 2560 FreeRTOS 멀티태스킹 기초]
* 3개의 태스크가 서로 방해하지 않고 독립 주기 실행!
*/
#include <Arduino_FreeRTOS.h>
// 태스크 함수 프로토타입 선언
void TaskBlink(void *pvParameters);
void TaskSensor(void *pvParameters);
void TaskReport(void *pvParameters);
// 공유 데이터 변수 (센서 최신값 저장)
volatile int globalSensorValue = 0;
void setup() {
Serial.begin(115200);
while (!Serial) { ; } // 시리얼 연결 대기
Serial.println(F("========================================"));
Serial.println(F(" 👑 아두이노 메가 2560 FreeRTOS 시작! "));
Serial.println(F("========================================"));
// 1. LED 점멸 태스크 생성 (우선순위 1, 스택 128)
xTaskCreate(TaskBlink, "BlinkLED", 128, NULL, 1, NULL);
// 2. 센서 수집 태스크 생성 (우선순위 2 - 더 높은 우선순위!, 스택 128)
xTaskCreate(TaskSensor, "ReadSensor", 128, NULL, 2, NULL);
// 3. 시리얼 리포트 태스크 생성 (우선순위 1, 스택 192)
xTaskCreate(TaskReport, "SerialReport", 192, NULL, 1, NULL);
// FreeRTOS 스케줄러가 자동으로 구동되므로 setup() 이후 loop()는 비워둡니다.
}
void loop() {
// FreeRTOS 커널이 동작 중이므로 loop()는 아무 일도 하지 않습니다.
}
// [태스크 1] 13번 LED 점멸 (Heartbeat)
void TaskBlink(void *pvParameters) {
(void) pvParameters;
pinMode(13, OUTPUT);
for (;;) { // 태스크는 무한 루프로 동작해야 합니다!
digitalWrite(13, HIGH);
vTaskDelay(500 / portTICK_PERIOD_MS); // 500ms 동안 CPU 양보!
digitalWrite(13, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
// [태스크 2] A0 아날로그 센서 실시간 수집 (100ms 주기)
void TaskSensor(void *pvParameters) {
(void) pvParameters;
for (;;) {
// 100ms마다 A0 센서 읽기
globalSensorValue = analogRead(A0);
vTaskDelay(100 / portTICK_PERIOD_MS); // 100ms 대기
}
}
// [태스크 3] 시리얼 모니터 현황 출력 (1000ms 주기)
void TaskReport(void *pvParameters) {
(void) pvParameters;
for (;;) {
Serial.print(F("[FreeRTOS 모니터] 가동시간: "));
Serial.print(millis() / 1000);
Serial.print(F("초 | A0 센서값: "));
Serial.println(globalSensorValue);
vTaskDelay(1000 / portTICK_PERIOD_MS); // 1초마다 출력
}
}
5. FreeRTOS 선점형 스케줄러(Preemptive Scheduler)의 마법 🪄
위 코드에서 TaskSensor의 우선순위는 2, 나머지 태스크는 1입니다.
이것은 무엇을 의미할까요?
- 우선순위 우선권: CPU가
TaskBlink를 실행 중이더라도, 100ms가 되어TaskSensor가 깨어나면 스케줄러는 즉시TaskBlink를 일시 정지시키고 우선순위가 더 높은TaskSensor에 CPU를 넘겨줍니다(선점, Preemption). - 센서의 정확성 보장: 이 원리 덕분에 모터가 돌든, 화면을 그리든, 중요한 센서 측정 루틴은 1마이크로초의 오차도 없이 칼같이 제시간에 실행될 수 있는 것입니다!
🌟 도로시와 지니의 핵심 요약! 🏆

- 단일 루프 탈피: FreeRTOS를 적용하면 여러 동작을 독립된 태스크로 나누어 병렬로 실행할 수 있습니다.
- 메가의 8KB SRAM 축복: 우노에서는 불가능에 가까웠던 다중 태스크 스택 할당이 메가에서는 여유롭게 동작합니다.
- vTaskDelay()의 미학: 무식하게 시간을 끄는
delay()대신, 다른 작업에 CPU를 넘겨주는 친절한 비차단 딜레이를 제공합니다. - 선점형 우선순위: 중요한 작업(센서, 비상 정지)에 높은 우선순위를 부여하여 완벽한 실시간 반응성을 확보합니다.
👧 도로시: “loop() 하나에 모든 걸 억지로 구겨 넣지 않고, 각자 맡은 일을 하는 독립된 태스크로 나누니까 코드가 너무 깔끔해!”
🐱 지니: “대형 프로젝트의 기초를 훌륭하게 다졌구나! 그런데 태스크들이 데이터를 주고받을 때 서로 부딪치면 데이터가 깨질 수 있단다. 다음 시간 ‘168_아두이노메가_운영체제_동기화와큐’에서 안전한 통신 비법을 알아보자!”