10.5 베어메탈(Bare-metal) 환경에서의 Zenoh-Pico

10.5 베어메탈(Bare-metal) 환경에서의 Zenoh-Pico

전통적인 산업 현장의 제어기들은 OS조차 호사격으로 여긴다. 진정한 하드코어 엔지니어들은 RTOS의 컨텍스트 스위칭(Context Switching) 오버헤드와 10KB 남짓한 스케줄러 용량조차 낭비라고 생각하여 칩을 Bare-metal 로 굴린다.

오직 C 언어 컴파일러와 레지스터, 그리고 무한히 도는 메인 루프 하나 뿐인 원시 지구(Bare-metal)에 어떻게 멀티캐스트 통신망을 이식할 것인가?
이 장은 아무런 그물망도 쳐져 있지 않은 절벽 위에서 타이머 콜백과 외부 인터럽트 하나만 믿고 Zenoh 엔진을 굴려야 하는 진정한 야생의 런북이다.

1. 운영체제 없는 환경의 제약사항 분석

RTOS 기반에서는 yield 함수 안에서 10ms 쉬어(vTaskDelay) 라고 명령하면 OS가 알아서 다른 통신 안 하는 잡일 태스크들을 돌려줬지만, 이젠 아니다.

1.0.1 [인스펙션] 타임 슬라이스(Time-slice)의 파괴

만약 당신이 Bare-metal 루프에서 10ms 동안 코드를 멍 때리게(Blocking) 만들면, 그 10ms 동안 칩은 말 그대로 정지한다. 모터의 엔코더 값을 놓치거나, 떨어지는 드론의 자이로 센서를 읽지 못해 추락하게 된다.

  1. 소켓 통신의 비동기화 강제
    모든 네트워크 읽기(read)는 무조건 Non-blocking이어야 한다. 값이 없으면 곧바로 코드를 뱉고 지나가야지 무한정 while(수신 대기) 를 타는 순간 그 로봇은 시체나 다름없다.
  2. 동적 메모리 할당 금지
    malloc 을 구현해 주는 OS 커널 따위는 없다. 사실상 heap 이라는 개념 자체가 칩 링킹 스크립트(Linker Script)에서 빠져있는 경우가 허다하다. 10.2 장의 정적 커스텀 메모리 사용 기술을 극도로 쥐어짜야만 한다.

2. 메인 루프(Main Loop) 내에서의 폴링(Polling) 구동 방식

단일 루프의 예술은 모든 함수가 “빛의 속도“로 빠져나오게(Return) 만들어, 하나의 구슬이 거대한 사이클을 1초에 만 번씩 돌게 하는 데 있다.

2.0.1 [Runbook] 무(無)지연 폴링 아키텍처

로봇의 눈과 다리, 그리고 Zenoh 통신 기능이 하나의 루프 안에서 조화롭게 굴러가는 마스터피스다.

#include <zenoh-pico.h>

void main() {
    // 하드웨어 설정 (Clock, GPIO, UART 등)
    hardware_init();
    
    // Zenoh 초기화 (이젠 네트워크 장치 연결도 블로킹되면 안 됨!)
    z_session_t session;
    init_zenoh_non_blocking(&session); 

    uint32_t last_sensor_time = 0;

    // "Super Loop" 진입
    while (1) {
        
        // 1. 하드웨어의 생존(제어) 로직 (가장 빠름)
        read_gyroscope_and_balance_drone(); 

        // 2. 10밀리초(100Hz) 마다 한 번씩 데이터 퍼블리시
        uint32_t current_time = get_hardware_tick(); 
        if (current_time - last_sensor_time >= 10) {
            uint8_t data[] = { ... };
            z_publisher_put(&pub, data, sizeof(data), NULL);
            last_sensor_time = current_time;
        }

        // 3. 네트워킹 심폐소생: 
        // 여기서 핵심은 타임아웃을 "0" 으로 주는 것이다!
        // 데이터가 없으면 빛의 속도로 이 함수를 빠져나와 다시 드론 밸런스를 잡으러 가야 한다.
        z_session_yield(&session, 0); 
    }
}

이 패턴이 무서운 이유는, Zenoh 가 패킷을 해독하고 콜백 함수를 터트리는 연산 비용(수천 번의 CPU 사이클)이 메인 루프 안에 그대로 얹혀진다는 것이다. 만약 당신이 Subscriber 콜백 함수 내부에서 지저분한 이중 for 문 루프를 돌린다면 루프가 찢어져(Jitter) 당신의 무선자동차(RC카)가 벽에 처박힐 것이다.

3. 타이머 및 인터럽트 서비스 루틴(ISR)과의 상호작용

로보틱스의 심장은 ISR(Interrupt Service Routine) 즉 인퍼럽트다.
센서에 물체가 감지되자마자 하드웨어 칩 코어에서 즉각 번개가 쳐지며 메인 코드를 강제 중단시키고 튀어나오는 긴급 대응 훈련이다.

여기서 아마추어들은 아주 멍청한 짓을 저지른다. 즉, “인터럽트가 터졌으니 당장 클라우드에 위험(Danger) 패킷을 날리자!” 라며 ISR 안에서 z_publisher_put 을 때려버리는 것이다.
그러는 순간 메인 루프가 반으로 갈라지면서 칩 레지스터가 꼬여 Hard Fault 무한 리부팅에 걸린다.

3.0.1 [Runbook] 인터럽트 오염 방어 전술 (Deferred Publishing)

인터럽트 함수 안에서는 “무조건 짧게 치고 빠져야” 한다. 통신같이 무겁게 칩을 괴롭히는 일은 깃발(Flag)만 올려놓고 메인 루프 수문장에게 짬처리를 한다.

#include <stdatomic.h>
#include <zenoh-pico.h>

// 스레드도 없는 환경이지만 인터럽트를 대비하기 위해 volatile 선언
volatile uint8_t flag_danger_detected = 0; 
volatile uint32_t sensor_critical_value = 0;

// [하드웨어 인터럽트 함수 구역]
// 이곳의 코드는 실행 속도가 0.0001초 미만이어야만 한다.
void EXTI15_10_IRQHandler(void) {
    if (HW_SENSOR_PIN_IS_HIGH()) {
        struct SensorData my_val = read_fast();
        
        // 1. 값만 복사해둔다
        sensor_critical_value = my_val.danger_level;
        // 2. 깃발만 들어올린다! (여기서 절대 z_put 을 부르지 마라)
        flag_danger_detected = 1; 

        // 3. HW 인터럽트 클리어 후 빛의 속도로 퇴출
        HW_CLEAR_INT_FLAG(); 
    }
}

// [메인 루프 구역]
void main() {
    // ... zenoh 초기화 ...
    
    while(1) {
        // [핵심] 여기서 깃발을 감시하다가, 올라가 있으면 그제서야 안전하게 쏜다.
        if (flag_danger_detected) {
            uint8_t payload[4];
            memcpy(payload, (void*)&sensor_critical_value, 4);
            
            z_publisher_put(&pub, payload, 4, NULL);
            
            flag_danger_detected = 0; // 깃발 해제
        }
        
        // 평소엔 얌전히 폴링 시전
        z_session_yield(&session, 0); 
    }
}

Bare-metal 의 신조는 명확하다. ISR 은 통지자(Notifier)이고, 묵직한 프로토콜 계산(Zenoh Pico 패킹) 은 메인 루프 워커(Worker)의 몫이다.

4. 베어메탈용 커스텀 메모리 할당자 구현

Pico 는 100% 정적 버퍼망으로 돌릴 수도 있지만 아주 복잡한 기능(동적 토픽 관리 등)을 위해선 약간의 malloc 이 필요할 때도 있다.
OS가 없다면, 당신이 직접 램의 작은 파티션 하나를 malloc 인 것처럼 속여 Pico 에게 갖다 바쳐야 한다.

4.0.1 [Runbook] 슬랩 할당자(Slab Allocator) 위장 전술

기본 malloc/free 는 메모리의 중앙 아무 데나 구멍을 꿇고 할당하지만, 메모리 풀(Pool) 패턴을 적용하면 안전한 자물쇠 버퍼를 얻을 수 있다.

#include <stddef.h>
#include <stdint.h>
#include <string.h>

#define MAX_CHUNKS 10
#define CHUNK_SIZE 128

// [메모리 창고 격리 선언]
uint8_t memory_pool[MAX_CHUNKS][CHUNK_SIZE];
uint8_t chunk_used[MAX_CHUNKS] = {0,};

// C 언어 전역 malloc 강제 탈취 방어 로직
void* custom_malloc(size_t size) {
    if (size > CHUNK_SIZE) return NULL; // 거부!
    for (int i=0; i<MAX_CHUNKS; i++) {
        if (!chunk_used[i]) {
            chunk_used[i] = 1; // 사용중 마킹 점유
            return &memory_pool[i][0];
        }
    }
    return NULL; // 메모리 아웃
}

void custom_free(void* ptr) {
    // 주소값 범위를 역추적하여 chunk index 를 반납시킨다
    for (int i=0; i<MAX_CHUNKS; i++) {
        if (ptr == &memory_pool[i][0]) {
            chunk_used[i] = 0; // 마킹 해제
            return;
        }
    }
}

이제 Pico 의 Config 초기화 부근에서 이렇게 이식 선언만 해주면,

zp_config_insert_json(&config, ZP_MALLOC_KEY, (void*)custom_malloc);
zp_config_insert_json(&config, ZP_FREE_KEY, (void*)custom_free);

이로서 Zenoh-Pico 는 OS 커널인 척하는 당신의 조악한 배열 시스템 안에서, 전혀 파편화를 염려하지 않고 평화롭게 메모리를 생성하고 닫으며 야생(Bare-Metal)의 세계 속을 항해하게 된다.