10.4 실시간 운영체제(RTOS) 통합 및 활용

10.4 실시간 운영체제(RTOS) 통합 및 활용

공장이나 드론에 “Bare-metal(깡통 C)” 만을 쓰기엔 프로젝트가 너무 거대해졌다.
그래서 현시대 임베디드 엔지니어들은 작은 칩 안에 멀티 태스킹(Multi-tasking)을 흉내 내는 소형 운영체제, 즉 실시간 운영체제(RTOS)를 욱여넣기 시작했다.

이 환경은 양날의 검이다. 소켓(Socket) 통신이 가능해져 Zenoh 를 붙이기는 쉬워졌지만, 자칫 잘못 태스크(Task) 스케줄링을 꼬이게 하면 무선 통신을 하느라 로봇 팔의 제어 주기가 1밀리초 밀리며 시스템이 박살 난다.
이 장에서는 가장 유명한 현역 RTOS(FreeRTOS, Zephyr, Mbed) 생태계에 어떻게 Zenoh-Pico를 가장 우아하고 결정론적(Deterministic)으로 이식할 수 있는지에 대한 하드웨어 마스터들의 런북 전술을 공개한다.

1. FreeRTOS 환경에서의 Task 구성 및 Zenoh-Pico 연동

FreeRTOS는 전 세계 MCU의 사실상 표준 운영체제다.
문제는 Zenoh-Pico 가 z_session_yield() 함수를 주기적으로 호출받아야 작동하는데, 이것을 메인 태스크(Main Task)에 박을 것인가, 아니면 전담 통신 태스크(Comm Task)를 따로 팔 것인가의 아키텍처 싸움이다.

1.0.1 [Runbook] 백그라운드 폴링 전담 태스크(Task) 전술

로봇 제어 태스크(우선순위 High)와 섞지 마라. 통신은 절대적으로 낮은 우선순위의 백그라운드 태스크(Low)로 격리시킨다.

#include "FreeRTOS.h"
#include "task.h"
#include <zenoh-pico.h>

z_session_t session;

// 태스크 1: 오직 Zenoh 네트워크만 책임지는 톱니바퀴
void zenoh_worker_task(void *pvParameters) {
    // 1. 초기화 (이 때 FreeRTOS LwIP 소켓이 할당됨)
    z_config_t config = z_config_default();
    z_open(&session, &config, NULL);

    while(1) {
        // 2. 엔진 호흡 (최대 10밀리초 대기)
        // 만약 네트워크에서 데이터가 오면, 이 함수 안에서 내부적으로
        // 당신이 설정해둔 Subscriber Callback 함수들이 연쇄적으로터 터질 것이다.
        z_session_yield(&session, 10);
        
        // 3. FreeRTOS 의 다른 태스크들에게 CPU 를 양보(Yield) 
        // 10ms 쉬어준다. 너무 자주 돌면 칩이 뜨거워진다!
        vTaskDelay(pdMS_TO_TICKS(10)); 
    }
}

// 메인 부팅 함수
int main(void) {
    // ... [하드웨어 브링업] ...

    // 통신 태스크 생성 (메모리: 4096 워드 / 우선순위: 1(낮음))
    xTaskCreate(
        zenoh_worker_task, 
        "ZenohTask", 
        4096, 
        NULL, 
        1, 
        NULL
    );

    // 태스크 스케줄러 점화 (이후로 코드는 돌아오지 않음)
    vTaskStartScheduler();
}

이 패턴이 가장 강력한 “비동기 분리(Asynchronous Isolation) 기법” 이다. 통신 딜레이(Timeouts)나 패킷 드랍이 아무리 심하게 터져도, 우선순위가 높은 모터 제어 태스크는 이 네트워크 지옥을 전혀 눈치채지 못한 채 100% 자신의 성능을 발휘한다.

2. Zephyr RTOS 네트워크 스택 브릿징

Zephyr 는 리눅스 재단이 주도하는 차세대 임베디드 운영체제다.
FreeRTOS 와 달리, Zephyr 는 매우 방대하고 강력한 자체 네트워크 스택(Network Stack)을 보유하고 있다. 따라서 Zenoh-Pico를 Zephyr 에 심을 때는 포팅 레이어를 깎아낼 필요 없이 BSD 호환 소켓(BSD Sockets)을 통해 투명하게 얹을 수 있다.

2.0.1 [Runbook] Zephyr POSIX 호환 소켓(Socket) 모드 전술

Zephyr 의 prj.conf 설정 파일(Kconfig) 단에서 POSIX 소켓과 무선망을 통째로 활성화해버리면, Zenoh-Pico 는 자신이 거대한 리눅스 서버에 올라간 줄 알고 착각하게 된다.

1. prj.conf 커널 폭격 세팅

## Zephyr 네트워킹 심장 활성화
CONFIG_NETWORKING=y
CONFIG_NET_IPV4=y
CONFIG_NET_UDP=y
CONFIG_NET_TCP=y

## [핵심 매직] Zenoh-pico 가 리눅스인 것처럼 속는 BSD Sockets API 활성화
CONFIG_NET_SOCKETS=y
CONFIG_NET_SOCKETS_POSIX_NAMES=y

## 힙 메모리 넉넉하게 주기 (Zephyr는 동적할당을 자체 관리함)
CONFIG_HEAP_MEM_POOL_SIZE=8192

2. CMake 타겟팅 변경
Pico 를 빌드할 때, SYSTEM_FREERTOS 가 아닌 리눅스와 똑같은 SYSTEM_POSIX 를 옵션으로 준다!

add_definitions(-DSYSTEM_POSIX)

매우 기이하게 들리겠지만 Zephyr RTOS가 리눅스의 네트워킹(POSIX)을 완벽히 흉내 내주기 때문에, 코드를 한 줄도 수정하지 않고 일반적인 데스크톱 TCP/IP 통신용으로 만들어진 Zenoh-Pico 의 system/posix 포팅 껍데기가 칩(Zephyr) 안에서 완벽하게 들어맞게 돌아가게 된다. 이것이 Zephyr 의 위력이다.

3. ESP-IDF (ESP32) 환경 구축 및 Wi-Fi 통신 구현

단돈 3천 원에 WiFi와 듀얼코어(Dual-core)가 박혀있는 미친 가성비의 제왕, ESP32 다.
IoT 메이커들은 이 칩에 환장한다. Espressif 가 제공하는 공식 개발 프레임워크인 ESP-IDF (이 기반이 FreeRTOS 다) 안에 Zenoh-Pico 를 쑤셔 박아 진정한 포터블 브로드캐스터(Portable Broadcaster)를 만들어보자.

3.0.1 [Runbook] ESP32 Wi-Fi 대기 및 커넥션 스톨(Stall) 방어 기법

WiFi 가 연결되지도 않았는데 Zenoh 부터 켜면 소켓이 터진다. 하드웨어 WiFi 접속과 소프트웨어 Zenoh 개화를 순차적으로 격발(Chained-Trigger)해야 한다.

#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include <zenoh-pico.h>

void start_zenoh_task() {
    // Pico 는 내부적으로 lwIP(경량 TCP/IP) 를 물고 통신함
    z_config_t config = z_config_default();
    
    // (공유기에 연결된 PC의 Zenoh 라우터 IP를 수동 지정)
    zp_config_insert_json(&config, ZP_PEER_KEY, "\"tcp/192.168.0.10:7447\"");
    
    z_session_t session;
    printf(">> WiFi 연결 확인됨. Zenoh 엔진 점화!\n");
    z_open(&session, &config, NULL);

    while(1) {
        z_session_yield(&session, 100);
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

// ESP-IDF 전용 이벤트 핸들러. 
// "IP 주소를 받아왔다(GOT_IP)" 이벤트가 터질 때만 Zenoh 를 깨운다.
static void wifi_event_handler(void* arg, esp_event_base_t event_base, 
                               int32_t event_id, void* event_data) {
    if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        // [격발] 이제 인터넷이 뚫렸으니 네트워크 스레드를 가동하라!
        xTaskCreate(start_zenoh_task, "zenoh", 4096, NULL, 5, NULL);
    }
}

void app_main() {
    // 1. 플래시 메모리 초기화
    nvs_flash_init(); 
    
    // 2. 이벤트 루프 및 WiFi 무전 대기 개시
    esp_event_loop_create_default();
    esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL);
    
    // 3. 내 WiFi 접속 설정 가동 (esp_wifi_start)
    // ...
}

이 패턴이 ESP32 개발의 핵심이다. 스마트폰의 핫스팟(Hotspot)이 도중에 꺼져서 WiFi가 끊기면 어떻게 될까? Zenoh 스레드는 대기(Stall) 상태로 빠지거나 패킷 에러를 뱉는다. 이 때 WIFI_EVENT_STA_DISCONNECTED 이벤트를 낚아채서(Catch) 아예 켜져있던 Zenoh 스레드(Task)를 사살(Kill)하고 처음부터 다시 부팅하는 강제 리셋 아키텍처가 “절대 죽지 않는 드론” 의 비밀이다.

4. mbed OS 및 Mbed TLS를 활용한 통신

영국 ARM 사가 주도하는 mbed OS 는 가장 C++ 스러운 객체지향 임베디드 플랫폼이다.
주목할 점은, 마이크로컨트롤러의 미세한 통신일지라도 “암호화(TLS/DTLS)” 가 필요한 국방/보안 장비에는 Mbed TLS 가 필수적으로 붙어야 한다는 사실이다. 센서 데이터가 해킹당해 원격으로 공장 문을 열어버리는 사태를 막아야 한다.

4.0.1 [Runbook] mbed OS 브릿지 및 메모리 파상풍 경고

Pico 와 mbed 조합 시 가장 끔찍한 병목은 ‘메모리’ 다. 일반 TCP 통신은 Pico 단독으로 2KB 의 램으로 구동 가능하지만, TLS 암호화 모듈을 켜는 순간 인증서 해석(Handshake)을 위한 거대한 비대칭 키 연산(RSA) 때문에 mbed tls 가 일시적으로 20~30KB의 힙을 터뜨려버린다.

1. Mbed OS에서 Pico 빌드 타겟 선언

add_definitions(-DSYSTEM_MBED)

2. TLS 엔진을 위한 RAM 저수지(Heap) 해방 (Mbed 설정파일: mbed_app.json)

{
    "target_overrides": {
        "*": {
            "mbed-tls.enable": true,
            "target.network-default-interface-type": "ETHERNET",
            "rtos.main-thread-stack-size": 8192
        }
    }
}

3. 암호 연산을 감당할 하드웨어 스펙 지정 (Cortex-M4/M3 아키텍처)
단순한 16MHz 짜리 Cortex-M0 칩에서 TLS 환경의 Zenoh 세션을 z_open 하면 부팅(Handshake)에만 15초가 걸린다.
보안 통신이 필요하다면 반드시 하드웨어 가속 암호 모듈(Hardware Cryptographic Accelerator)이 칩 안에 내장된 MCU를 쓰거나, 아니면 과감히 TLS를 끄고 사설망(VPN 망 내) 내부에서만 평문 통신을 하는 것이 임베디드 아키텍처의 바이블이다.

5. RTOS 환경에서의 스레드 안전성(Thread Safety) 관리

여러분이 아무리 FreeRTOS 위에 우아하게 아키텍처를 세웠다 한들, “여러 태스크(Sensor Task, Display Task)가 동시에 하나의 session 변수에 접근해서 z_publisher_put을 갈기면 어떻게 될까?”

단언컨대, 장비는 3초 안에 하드 폴트(Hard Fault)를 맞고 재부팅(Watchdog Reset)된다.
Zenoh-Pico는 초경량 단일 스레드(Single-Thread) 기반 유한 상태 머신(FSM)이므로, 내부 엔진에 락(Lock)이나 뮤텍스(Mutex) 같은 더러운 방어막을 쳐두지 않았다.

[신성한 영역을 지키는 뮤텍스 보디가드 (Mutex Protection)]

멀티태스킹 환경이라면, Zenoh API (특히 put 이나 yield)를 호출할 때 반드시 RTOS 차원에서의 뮤텍스로 겹치는 호출(Concurrent Call)을 칼같이 베어내야 한다!

#include "FreeRTOS.h"
#include "semphr.h"

// [글로벌 자물쇠 생성]
SemaphoreHandle_t zenoh_mutex;

void system_init() {
    zenoh_mutex = xSemaphoreCreateMutex();
}

// [안전지대(Safe Zone) 퍼블리셔 함수 래퍼]
void safe_zenoh_put(z_owned_publisher_t *pub, uint8_t *data, size_t len) {
    // 1. 다른 태스크가 Zenoh를 쓰고 있다면 대기!
    if (xSemaphoreTake(zenoh_mutex, portMAX_DELAY) == pdTRUE) {
        
        // 2. 오직 나 혼자만 진입! (Critical Section 발동)
        z_publisher_put(z_loan(*pub), data, len, NULL);
        
        // 3. 자물쇠 풀기!
        xSemaphoreGive(zenoh_mutex);
    }
}

이 규칙은 끔찍하게 단호하다!
어떤 인터럽트 레벨(ISR)에서도 절대 z_ 로 시작하는 함수를 직접 호출하지 마라 (인터럽트는 뮤텍스를 뚫고 들어오기 때문).
오로지 RTOS의 정규 스레드(Task) 안에서, 뮤텍스 자물쇠를 거머쥔 최후의 1인만이 Zenoh-Pico 엔진의 레지스터를 허락받는다. 이 단 하나의 봉인 원칙만 지킨다면, 여러분의 MCU는 서버급의 동시성(Concurrency) 트래픽을 처리하면서도 영원히 패닉스위치에 불이 들어오지 않을 것이다!