RTOS #1

홍태준·2026년 1월 9일

RTOS

목록 보기
1/20
post-thumbnail

본 포스팅은 취준 목적으로 RTOS를 공부하는 과정에서 작성된 내용입니다. 부족한 내용에 대해선 clauude 및 구글링, 서적 레퍼런싱을 통해 추후 업데이트 할 예정입니다

Week 1 Day 1: RTOS 개요

학습 목표

  • Bare-metal과 RTOS의 차이점 이해
  • RTOS가 필요한 이유 파악
  • 실시간 시스템의 특징 학습
  • FreeRTOS 선택 이유 이해

1. Bare-metal vs RTOS 비교

Bare-metal 프로그래밍이란?

Bare-metal은 운영체제 없이 마이크로컨트롤러에서 직접 코드를 실행하는 방식입니다.

간단한 Bare-metal 예제:

C 버전:

#include "stm32f4xx.h"

void delay(volatile uint32_t count) {
    while(count--);
}

int main(void) {
    // LED 초기화
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
    GPIOD->MODER |= GPIO_MODER_MODER12_0;
    
    while(1) {
        GPIOD->ODR ^= GPIO_ODR_OD12;  // LED 토글
        delay(1000000);
        
        // 버튼을 읽어야 한다면?
        // UART 데이터를 처리해야 한다면?
        // 센서 값을 읽어야 한다면?
        // 모두 여기서 순차적으로 처리해야 함!
    }
}

C++ 버전:

#include "stm32f4xx.h"

class LED {
private:
    volatile uint32_t* odr_reg;
    uint32_t pin_mask;
    
public:
    LED(volatile uint32_t* odr, uint32_t pin) : odr_reg(odr), pin_mask(1 << pin) {
        // LED 초기화
        RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
        GPIOD->MODER |= (1 << (pin * 2));
    }
    
    void toggle() {
        *odr_reg ^= pin_mask;
    }
    
    void delay(volatile uint32_t count) {
        while(count--);
    }
};

int main(void) {
    LED led(&GPIOD->ODR, 12);
    
    while(1) {
        led.toggle();
        led.delay(1000000);
        
        // 버튼을 읽어야 한다면?
        // UART 데이터를 처리해야 한다면?
        // 센서 값을 읽어야 한다면?
        // 모두 여기서 순차적으로 처리해야 함!
    }
}

Bare-metal의 특징:

장점:

  • 코드가 간단하고 직관적
  • 메모리 사용량이 적음
  • 예측 가능한 실행 흐름
  • 학습하기 쉬움

단점:

  • 여러 작업을 동시에 처리하기 어려움
  • 타이밍이 복잡한 작업은 구현이 까다로움
  • 코드가 복잡해지면 유지보수 어려움
  • 우선순위 관리가 불가능

Bare-metal에서 여러 작업 처리하기

C 버전:

int main(void) {
    init_led();
    init_button();
    init_uart();
    init_sensor();
    
    while(1) {
        // 모든 작업을 순차적으로 처리
        if(button_pressed()) {
            handle_button();
        }
        
        if(uart_data_available()) {
            handle_uart();  // 이 함수가 오래 걸리면?
        }
        
        if(sensor_ready()) {
            read_sensor();  // 센서 읽기가 지연되면?
        }
        
        update_led();  // LED 업데이트가 늦어질 수 있음!
    }
}

C++ 버전:

class System {
private:
    LED led;
    Button button;
    UART uart;
    Sensor sensor;
    
public:
    System() {
        led.init();
        button.init();
        uart.init();
        sensor.init();
    }
    
    void run() {
        while(1) {
            // 모든 작업을 순차적으로 처리
            if(button.isPressed()) {
                button.handle();
            }
            
            if(uart.dataAvailable()) {
                uart.handle();  // 이 함수가 오래 걸리면?
            }
            
            if(sensor.isReady()) {
                sensor.read();  // 센서 읽기가 지연되면?
            }
            
            led.update();  // LED 업데이트가 늦어질 수 있음!
        }
    }
};

int main(void) {
    System system;
    system.run();
}

문제점:
1. 한 작업이 오래 걸리면 다른 작업이 지연됨
2. 긴급한 작업을 우선 처리할 수 없음
3. 정확한 타이밍 제어가 어려움


2. RTOS가 필요한 이유

RTOS (Real-Time Operating System)란?

RTOS는 실시간으로 여러 작업을 관리하고 우선순위에 따라 실행하는 운영체제입니다.

RTOS를 사용한 같은 예제:

C 버전:

#include "FreeRTOS.h"
#include "task.h"

// LED 제어 Task
void vLEDTask(void *pvParameters) {
    while(1) {
        toggle_led();
        vTaskDelay(pdMS_TO_TICKS(500));  // 500ms 대기
    }
}

// 버튼 처리 Task
void vButtonTask(void *pvParameters) {
    while(1) {
        if(button_pressed()) {
            handle_button();
        }
        vTaskDelay(pdMS_TO_TICKS(10));  // 10ms 대기
    }
}

// UART 처리 Task (높은 우선순위)
void vUARTTask(void *pvParameters) {
    while(1) {
        if(uart_data_available()) {
            handle_uart();  // 긴급 처리
        }
        vTaskDelay(pdMS_TO_TICKS(1));
    }
}

// 센서 읽기 Task
void vSensorTask(void *pvParameters) {
    while(1) {
        read_sensor();
        process_sensor_data();
        vTaskDelay(pdMS_TO_TICKS(100));  // 100ms마다
    }
}

int main(void) {
    init_hardware();
    
    // Task 생성 (우선순위 설정 가능)
    xTaskCreate(vUARTTask, "UART", 128, NULL, 3, NULL);    // 높은 우선순위
    xTaskCreate(vSensorTask, "Sensor", 128, NULL, 2, NULL);
    xTaskCreate(vButtonTask, "Button", 128, NULL, 2, NULL);
    xTaskCreate(vLEDTask, "LED", 128, NULL, 1, NULL);      // 낮은 우선순위
    
    // 스케줄러 시작 - 여기서부터 RTOS가 Task들을 관리
    vTaskStartScheduler();
    
    while(1);  // 여기는 실행되지 않음
}

C++ 버전:

#include "FreeRTOS.h"
#include "task.h"

// Task를 클래스로 추상화
class Task {
protected:
    TaskHandle_t taskHandle;
    const char* taskName;
    uint16_t stackSize;
    UBaseType_t priority;
    
    // 순수 가상 함수 - 각 Task가 구현해야 함
    virtual void run() = 0;
    
    // Task 진입점 (static)
    static void taskEntry(void* pvParameters) {
        Task* task = static_cast<Task*>(pvParameters);
        task->run();
    }
    
public:
    Task(const char* name, uint16_t stack, UBaseType_t prio) 
        : taskName(name), stackSize(stack), priority(prio), taskHandle(nullptr) {}
    
    void create() {
        xTaskCreate(taskEntry, taskName, stackSize, this, priority, &taskHandle);
    }
    
    virtual ~Task() {}
};

// LED Task 클래스
class LEDTask : public Task {
private:
    LED& led;
    
public:
    LEDTask(LED& ledRef) : Task("LED", 128, 1), led(ledRef) {}
    
    void run() override {
        while(1) {
            led.toggle();
            vTaskDelay(pdMS_TO_TICKS(500));
        }
    }
};

// Button Task 클래스
class ButtonTask : public Task {
private:
    Button& button;
    
public:
    ButtonTask(Button& btnRef) : Task("Button", 128, 2), button(btnRef) {}
    
    void run() override {
        while(1) {
            if(button.isPressed()) {
                button.handle();
            }
            vTaskDelay(pdMS_TO_TICKS(10));
        }
    }
};

// UART Task 클래스
class UARTTask : public Task {
private:
    UART& uart;
    
public:
    UARTTask(UART& uartRef) : Task("UART", 128, 3), uart(uartRef) {}
    
    void run() override {
        while(1) {
            if(uart.dataAvailable()) {
                uart.handle();
            }
            vTaskDelay(pdMS_TO_TICKS(1));
        }
    }
};

// Sensor Task 클래스
class SensorTask : public Task {
private:
    Sensor& sensor;
    
public:
    SensorTask(Sensor& sensorRef) : Task("Sensor", 128, 2), sensor(sensorRef) {}
    
    void run() override {
        while(1) {
            sensor.read();
            sensor.process();
            vTaskDelay(pdMS_TO_TICKS(100));
        }
    }
};

int main(void) {
    // 하드웨어 객체 생성
    LED led;
    Button button;
    UART uart;
    Sensor sensor;
    
    // Task 객체 생성
    LEDTask ledTask(led);
    ButtonTask buttonTask(button);
    UARTTask uartTask(uart);
    SensorTask sensorTask(sensor);
    
    // Task 생성
    ledTask.create();
    buttonTask.create();
    uartTask.create();
    sensorTask.create();
    
    // 스케줄러 시작
    vTaskStartScheduler();
    
    while(1);
}

RTOS의 핵심 개념

1. Task (태스크)

  • 독립적으로 실행되는 작은 프로그램
  • 각자의 스택과 우선순위를 가짐
  • 마치 여러 개의 main() 함수가 동시에 실행되는 것처럼 동작

2. Scheduler (스케줄러)

  • 어떤 Task를 언제 실행할지 결정
  • 우선순위에 따라 Task를 전환
  • 사용자가 신경 쓸 필요 없이 자동으로 관리

3. Context Switching (문맥 전환)

  • 한 Task에서 다른 Task로 전환하는 과정
  • CPU 레지스터 상태를 저장하고 복원
  • 하드웨어의 도움을 받아 빠르게 실행

3. 실시간 시스템의 특징

Real-Time(실시간)이란?

"빠르다"가 아니라 "정해진 시간 안에 반드시 완료"를 의미합니다.

예시:

  • 평균 1ms에 처리하지만 가끔 100ms 걸림 → 실시간 아님
  • 항상 10ms 이내에 처리 완료 → 실시간 시스템

Hard Real-Time vs Soft Real-Time

Hard Real-Time (경성 실시간)

데드라인을 한 번이라도 못 지키면 치명적인 결과 발생

예시:

  • 에어백 시스템: 충돌 감지 후 10ms 이내 전개
  • ABS 브레이크: 바퀴 잠김 감지 후 즉시 제어
  • 심장박동기: 정확한 주기로 전기 신호 전달

Soft Real-Time (연성 실시간)

데드라인을 가끔 못 지켜도 시스템이 정상 동작

예시:

  • 온도 모니터링: 100ms마다 읽어야 하지만 110ms에 읽어도 괜찮음
  • LED 깜빡임: 정확히 1초가 아니어도 괜찮음
  • 비디오 스트리밍: 가끔 프레임이 드롭되어도 됨

임베디드 시스템에서 RTOS가 필요한 경우

RTOS 사용을 권장:

  • 3개 이상의 독립적인 작업이 있을 때
  • 작업마다 다른 주기나 우선순위가 필요할 때
  • 정확한 타이밍이 중요할 때
  • 통신 프로토콜을 구현할 때
  • 센서 데이터를 지속적으로 처리할 때

Bare-metal로 충분:

  • 단순한 LED 제어
  • 하나의 센서만 읽는 경우
  • 아주 메모리가 제한적인 시스템 (8KB 미만)
  • 배터리 수명이 최우선인 경우

4. RTOS의 주요 구성요소

간단한 구조도

┌─────────────────────────────────────┐
│         사용자 Application        │
│  (Task1, Task2, Task3, ...)      │
├─────────────────────────────────────┤
│         RTOS Kernel              │
│  - Scheduler                     │
│  - Task Management               │
│  - Synchronization (Mutex, Queue)│
│  - Memory Management             │
├─────────────────────────────────────┤
│      Hardware Abstraction        │
│(Timer, Interrupt, Context Switch)│
├─────────────────────────────────────┤
│          STM32 Hardware          │
└─────────────────────────────────────┘

핵심 기능

1. Task Management (작업 관리)

  • Task 생성, 삭제, 우선순위 변경
  • Task 상태 관리 (실행, 대기, 준비)

2. Scheduler (스케줄러)

  • 가장 높은 우선순위의 Task를 선택해서 실행
  • 주기적으로 Task를 전환 (Tick)

3. Synchronization (동기화)

  • Mutex: 공유 자원 보호
  • Semaphore: 이벤트 신호
  • Queue: Task 간 데이터 전달

4. Timing (타이밍)

  • 정확한 지연 (vTaskDelay)
  • 주기적 실행 (vTaskDelayUntil)
  • Software Timer

5. FreeRTOS 선택 이유

시장에는 여러 RTOS가 있습니다:

  • FreeRTOS
  • ThreadX (Azure RTOS)
  • Zephyr
  • RT-Thread
  • RIOT OS

FreeRTOS를 선택한 이유

무료 오픈소스 (MIT 라이선스)
가장 널리 사용됨 (산업 표준)
풍부한 문서와 예제
STM32 공식 지원 (STM32CubeMX 통합)
작은 메모리 풋프린트 (4-10KB)
활발한 커뮤니티


6. 실제 적용 사례

사례 1: 센서 모니터링 시스템

요구사항:

  • 온도 센서: 100ms마다 읽기
  • UART: 데이터 수신 시 즉시 처리
  • LED: 1초마다 상태 표시
  • 버튼: 누르면 즉시 반응

Bare-metal의 문제:

C 버전:

#include "stm32f4xx_hal.h"

// 하드웨어 제어 함수들
void read_temperature(void) {
    HAL_Delay(20); // 20ms 소요 (CPU가 여기서 멈춤)
}

void check_uart(void) {
    // 데이터가 있다면 처리하는데 50ms 소요
    if (UART_Data_Ready()) {
        HAL_Delay(50); 
    }
}

void check_button(void) {
    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) {
        // 버튼 처리 (1ms)
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();

    while (1) {
        read_temperature();  // 1번: 20ms 대기
        check_uart();        // 2번: 데이터 있으면 50ms 대기
        check_button();      // 3번: 여기서 버튼을 눌러도 앞선 70ms 동안은 무시됨
        
        // 총 지연 시간: 약 71~72ms
    }
}

C++ 버전:

class Sensor {
public:
    void read() { HAL_Delay(20); }
};

class UartManager {
public:
    void check() {
        if (hasData()) { HAL_Delay(50); }
    }
private:
    bool hasData() { /* 체크 로직 */ return true; }
};

class Button {
public:
    void check() {
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) {
            // 처리
        }
    }
};

// 객체 생성
Sensor tempSensor;
UartManager uart;
Button userButton;

int main(void) {
    HAL_Init();
    
    while (1) {
        tempSensor.read();  // 객체 메서드 호출 (20ms)
        uart.check();       // 객체 메서드 호출 (50ms)
        userButton.check(); // 객체 메서드 호출
        
        // C 버전과 마찬가지로 순차 실행의 한계(지연)가 동일하게 발생
    }
}

RTOS 해결:

C 버전:

/* 버튼 태스크: 높은 우선순위 */
void vButtonTask(void *pvParameters) {
    for(;;) {
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) {
            // 버튼 눌림 처리 (즉각 반응)
        }
        // 10ms마다 체크, 그동안은 다른 태스크에 CPU 양보
        vTaskDelay(pdMS_TO_TICKS(10)); 
    }
}

/* UART 통신 태스크: 중간 우선순위 */
void vUartTask(void *pvParameters) {
    for(;;) {
        check_uart(); // 데이터 처리 (최대 50ms 소요)
        // 처리가 끝나면 다른 작업이 실행될 수 있도록 휴식
        vTaskDelay(pdMS_TO_TICKS(5));
    }
}

/* 온도 센서 태스크: 낮은 우선순위 */
void vTempTask(void *pvParameters) {
    for(;;) {
        read_temperature(); // 20ms 소요
        vTaskDelay(pdMS_TO_TICKS(100)); // 100ms마다 측정
    }
}

C++ 버전:

class TaskBase {
public:
    virtual void run() = 0;
    void start(const char* name, uint16_t stackDepth, UBaseType_t priority) {
        xTaskCreate(taskWrapper, name, stackDepth, this, priority, NULL);
    }
private:
    static void taskWrapper(void* param) {
        static_cast<TaskBase*>(param)->run();
    }
};

// 버튼 제어 클래스
class ButtonTask : public TaskBase {
public:
    void run() override {
        for(;;) {
            if (button.isPressed()) { /* 처리 */ }
            vTaskDelay(pdMS_TO_TICKS(10)); // 10ms 대기
        }
    }
};

// UART 제어 클래스
class UartTask : public TaskBase {
public:
    void run() override {
        for(;;) {
            uart.process(); // 오래 걸리는 작업
            vTaskDelay(pdMS_TO_TICKS(5));
        }
    }
};

// 메인 함수에서 객체 생성 및 시작
ButtonTask btnTask;
UartTask   uTask;

btnTask.start("Btn", 128, 3); // 우선순위 높음
uTask.start("Uart", 256, 1);   // 우선순위 낮음

사례 2: 통신 프로토콜 처리

요구사항:

  • 수신: 인터럽트로 데이터 받기
  • 파싱: 프레임 검증 및 분석
  • 처리: 명령 실행
  • 응답: 결과 전송

Bare-metal의 문제:

C 버전

volatile bool data_received = false;
uint8_t rx_buffer[64];

// UART 수신 인터럽트 콜백
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    data_received = true; // 플래그 설정
}

void main() {
    while(1) {
        if(data_received) {
            if(parse_frame(rx_buffer)) {  // 1. 파싱
                process_command();        // 2. 처리
                send_response();          // 3. 응답
            }
            data_received = false;
        }
        // 이 아래에 다른 작업(센서 읽기 등)이 있다면 
        // 통신 처리가 끝날 때까지 모두 대기해야 함
    }
}

C++ 버전(싱글톤/클래스 중심):

class ProtocolManager {
public:
    void poll() {
        if(m_isNewData) {
            handle();
            m_isNewData = false;
        }
    }
    void setDataReady() { m_isNewData = true; }
private:
    bool m_isNewData = false;
    void handle() { /* 파싱 -> 처리 -> 응답 */ }
};

ProtocolManager protocol;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    protocol.setDataReady();
}

void main() {
    while(1) {
        protocol.poll();
        otherTask.run(); // 여전히 순차 실행의 한계 존재
    }
}

RTOS방식(Queue/Semaphore 사용):

C 버전:

QueueHandle_t xProtocolQueue;

// 인터럽트: 데이터만 큐에 넣고 즉시 종료
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(xProtocolQueue, &rx_data, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 통신 전용 태스크: 데이터가 올 때까지 'Block' 상태로 대기 (CPU 점유 0%)
void vProtocolTask(void *pvParameters) {
    uint8_t data;
    for(;;) {
        if(xQueueReceive(xProtocolQueue, &data, portMAX_DELAY)) {
            parse();    // 파싱 중에도 우선순위 높은 다른 태스크는 실행 가능
            process();
            send();
        }
    }
}

C++ 버전(Active Object 패턴):

class CommunicationTask : public TaskBase {
public:
    CommunicationTask() { m_queue = xQueueCreate(10, sizeof(Packet)); }
    
    void pushPacket(Packet p) { xQueueSend(m_queue, &p, 0); }

    void run() override {
        Packet p;
        for(;;) {
            if(xQueueReceive(m_queue, &p, portMAX_DELAY)) {
                // 파싱, 처리, 응답 로직을 객체 지향적으로 수행
                Processor.execute(Parser.parse(p));
            }
        }
    }
private:
    QueueHandle_t m_queue;
};

RTOS 장점:

  • 수신 Task: ISR에서 Queue로 데이터 전달
  • 파싱 Task: Queue에서 데이터 꺼내서 분석
  • 처리 Task: 명령 실행 (시간이 오래 걸려도 수신은 계속됨)
  • 각 단계가 독립적으로 동작

요약

오늘 배운 핵심 내용

  1. Bare-metal: 단순하지만 여러 작업 처리가 어려움
  2. RTOS: 여러 Task를 우선순위에 따라 자동 관리
  3. 실시간: 빠른 것이 아니라 정해진 시간 안에 완료하는 것
  4. FreeRTOS: 가장 널리 사용되는 무료 RTOS

핵심 용어 정리

용어설명
Bare-metalOS 없이 직접 하드웨어를 제어하는 프로그래밍 방식
RTOS실시간 운영체제, 여러 작업을 우선순위에 따라 관리
Task독립적으로 실행되는 작은 프로그램 단위
SchedulerTask 실행 순서를 결정하는 RTOS의 핵심 구성요소
Context Switch한 Task에서 다른 Task로 전환하는 과정
Hard Real-Time데드라인을 반드시 지켜야 하는 시스템
Soft Real-Time데드라인을 가끔 못 지켜도 괜찮은 시스템

복습 문제

  1. Bare-metal 프로그래밍의 가장 큰 단점은 무엇인가요?
  2. RTOS에서 "실시간"의 의미는 무엇인가요?
  3. Hard Real-Time과 Soft Real-Time의 차이를 설명하세요.
  4. Task와 일반 함수의 차이점은 무엇인가요?
  5. RTOS를 사용하면 좋은 경우를 3가지 이상 나열하세요.
  6. C와 C++로 RTOS Task를 구현할 때의 차이점은 무엇인가요?
  7. C++ 클래스 기반 Task 구현의 장점은 무엇인가요?

profile
당신의 코딩 메이트

0개의 댓글