10.3 빌드 시스템 및 포팅 계층 (Porting Layer)

10.3 빌드 시스템 및 포팅 계층 (Porting Layer)

마이크로컨트롤러(MCU) 세계에는 npm install 이나 cargo build 같이 친절한 패키지 매니저가 없다.
그저 제조사마다 다른 컴파일러 툴체인(Toolchain)과, 물리적으로 핀(Pin) 번호가 전부 다르게 꽂혀 있는 보드들의 파편화가 존재할 뿐이다.

Zenoh-Pico 의 개발진은 이런 아비규환의 하드웨어 파편화 속에서 살아남기 위해 비즈니스 통신 로직(Core)과 하드웨어 제어 로직(Porting)을 완벽히 분리시키는 컴파일 아키텍처를 창조했다.
이 챕터에서는 CMake 빌드 시스템을 장악하고, 내가 만든 기괴한 커스텀 보드(Custom Board)마저도 Zenoh 의 품 안으로 포팅(Porting)해 내는 OS 용접 런북을 전개한다.

1. CMake 기반 독립형(Standalone) 빌드 환경 설정

보통의 임베디드 펌웨어 프로젝트는 Arduino IDE 같이 버튼 하나로 굽는 환경이거나, Eclipse 기반의 무거운 툴(STM32CubeIDE 등)을 쓴다.
하지만 진정한 시스템 아키텍트라면 이 모든 툴을 무시하고 순수 CMake 하나로 Zenoh-Pico 를 펌웨어에 이식시켜야만 나중에 CI/CD 파이프라인(Jenkins, Github Actions)에 로봇 코드를 태울 수 있다.

1.0.1 [Runbook] CMake 서브모듈(Sub-module) 빌드 주입 전술

1. 폴더 구조 설계

my_sensor_firmware/
├── src/
│   └── main.c          (내 로봇 제어 코드)
├── lib/
│   └── zenoh-pico/     (git submodule 로 땡겨온 Pico 소스코드 전체!)
└── CMakeLists.txt

2. CMakeLists.txt 파괴적 설정
Zenoh-Pico 는 미리 빌드된 .a 덩어리(Static Library)를 받아오는 것이 아니다. 내 펌웨어와 “같이 소스코드 레벨에서 통째로” 빌드되어야 한다.

cmake_minimum_required(VERSION 3.14)
project(SensorFirmware C)

## 컴파일 타겟 보드의 아키텍처나 옵션을 지정한다. 
## (여기선 데스크탑 POSIX 라고 임시 가정)
add_definitions(-DSYSTEM_POSIX) 

## [핵심] lib 폴더에 짱박아둔 zenoh-pico 의 CMake 를 여기서 호출해버린다!
## 이러면 cmake 가 알아서 pico 소스코드를 검색해서 빌드 타겟에 올려준다.
add_subdirectory(lib/zenoh-pico)

## 내 펌웨어 바디
add_executable(sensor_node src/main.c)

## 내 펌웨어에 빌드된 zenoh-pico 타겟을 묶어(Link) 버린다.
target_link_libraries(sensor_node zenoh-pico)

[주의 사항]
마이크로컨트롤러의 메모리가 극도로 모자라다면 add_definitions(-DZP_MAX_ENDPOINTS=1) 처럼 CMake 단에서 C 매크로를 때려버려라.
Pico 엔진 내부의 불필요한 배열 크기(다중 포트 지원 등)가 빌드 타임에 극적으로 줄어들어 1~2KB 의 RAM 을 구원할 수 있다.

2. 크로스 컴파일 툴체인(Cross-Compilation Toolchain) 구성

앞선 CMake 를 통해 내 코드와 Pico 가 묶였다 하더라도, 이건 Intel(x86) 칩을 위한 빌드일 뿐이다.
이 덩어리를 ARM Cortex-M 이나 Xtensa(ESP32) 칩 언어로 기계어 번역(Cross-compile)을 시켜야 한다.

2.0.1 [Runbook] 툴체인 강제 납치(Hijacking) 전술

CMake 에게 내 컴퓨터의 기본 gcc 를 압수당하고, MCU 전용 컴파일러를 쓰라고 십자창(Toolchain 파일)을 꽂아야 한다. arm-none-eabi-gcc 가 깔려있다는 전개다.

1. 툴체인 파일 깎기 (arm-toolchain.cmake)

set(CMAKE_SYSTEM_NAME Generic) # OS가 없는 Bare-metal 상태임을 선언!
set(CMAKE_SYSTEM_PROCESSOR arm)

## 무기 교체
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)

## Cortex-M4 보드(예: STM32F4)를 위한 극악의 최적화 컴파일 플래그
set(CMAKE_C_FLAGS "-mcpu=cortex-m4 -mthumb -Os -ffunction-sections -fdata-sections" CACHE INTERNAL "C Flags")

## 링커 옵션: 안 쓰는 C 함수는 롬(ROM)에서 다 날려버려라! (--gc-sections)
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--gc-sections" CACHE INTERNAL "Linker Flags")

2. 빌드 격발

mkdir build && cd build
## cmake를 돌릴 때 저 툴체인 파일을 목에 들이대야 한다.
cmake -DCMAKE_TOOLCHAIN_FILE=../arm-toolchain.cmake ..
make

이 과정이 성공하면 sensor_node.elf 혹은 sensor_node.bin 이라는, 내 컴퓨터에서는 두 번 다시 실행할 수 없는 완전한 쇳덩어리(펌웨어)가 튀어나오게 된다. 이걸 JTAG 이나 ST-Link 를 통해 칩에 주입(Flashing)하는 순간, 그 쇳조각은 스스로 Zenoh 라우터를 찾기 시작할 것이다.

3. 하드웨어 추상화 계층(HAL)의 구조

“Zenoh-Pico 는 어떻게 운영체제(OS) 없이 TCP 든 블루투스든 다 쓴다는 거지?”
비결은 그들이 통신 기능을 완전히 비워놓은 텅 빈 인터페이스(HAL, Hardware Abstraction Layer) 껍데기만 가지고 장사를 하기 때문이다.

3.0.1 [인스펙션] Pico 의 뇌 없는 더미(Dummy) 아키텍처

Pico 의 코어 로직은 데이터를 쏠 때 z_publisher_put() 안에서 이렇게 멍청하게 외친다.
“어이! 하부 통신망! 나는 네가 TCP 인지 시리얼 호스인지 몰라! 아무튼 이 바이트 배열을 파이프로 보내(Write) 줘!”

이 외침에 대답하기 위해, Pico 저장소 안에는 운영체제별로 이 껍데기를 채워 넣은 시스템 포트(System Ports) 폴더가 제공된다.

  1. system/posix: 리눅스, 맥, 윈도우(WSL) 용. 내부적으로 표준 sys/socket.h 구조가 들어있다.
  2. system/freertos: ESP32 계열 용도. LwIP 기반의 소켓 통신이 구현되어 있다.
  3. system/mbed: ARM Mbed OS 용.
  4. system/arduino: 아두이노 생태계(Ethernet Shield 등) 용도.

빌드(CMake) 시 당신이 add_definitions(-DSYSTEM_FREERTOS) 를 때리면, Pico 는 나머지 폴더는 장렬히 다 무시하고 오직 system/freertos 안의 코드만 자신의 텅 빈 인터페이스에 결합시켜 작동한다.
이것이 진정한 “의존성 주입(Dependency Injection) 기반의 크로스 플랫폼” 이다.

4. 새로운 플랫폼을 위한 커스텀 포팅 가이드

“나는 인터넷이 안 되는 칩이다. RS-485(시리얼 유선) 통신만 겨우 되는 자체 제작 센서 펌웨어다.”
여기에서는 Pico 가 지원하는 공식 포팅(posix, freertos 등)도 전혀 먹히지 않는다.

최후의 수단. 당신 스스로가 Pico 가 요구하는 텅 빈 함수 껍데기들을 깎아서(Implement) Pico 한테 갖다 바쳐야(Inject) 한다.

4.0.1 [Runbook] 백지수표 포팅(Zero-to-Hero Porting) 전술

이 세계에 진입하려면 Pico 가 요구하는 System 계층과 Network 계층의 5가지 핵심 함수만 구현하면 된다.

1. 시간의 지배 (Timer Porting)
Pico 는 1초가 뭔지, 1밀리초가 뭔지 모른다. 칩의 클럭(Clock)을 가져오는 함수를 당신이 주입해야 한다.

// [내가 구현해야 할 함수]
uint64_t z_clock_now(void) {
    // 예: STM32 의 HAL 라이브러리 (밀리초 반환)
    return HAL_GetTick(); 
}

2. 통신망 입출력 통제 (Network Porting)
이것이 RS-485 건, 블루투스 시리얼이건 아래 두 함수 구조만 맞추면 Pico 는 완벽히 속아 넘어간다.

// [내가 구현해야 할 함수 1: TX (송신)]
int z_system_write(int fd, const uint8_t *buf, size_t count) {
    // UART(시리얼) 로 바이트 덩어리를 욱여넣는다!
    HAL_UART_Transmit(&huart1, (uint8_t*)buf, count, 100);
    return count; // 성공한 바이트 수 반환
}

// [내가 구현해야 할 함수 2: RX (수신)]
int z_system_read(int fd, uint8_t *buf, size_t count) {
    // 파이프에 데이터가 있는지 확인하고, 있으면 buf 에 복사해준다.
    if (UART_Has_Data()) {
        HAL_UART_Receive(&huart1, buf, count, 10);
        return count; // 읽은 바이트 수 반환
    }
    return 0; // 아직 읽을게 없소이다. (가장 중요함! Pico를 블로킹시키면 안된다)
}

이렇게 세 개의 껍데기만 내 메인 펌웨어 C 파일에 만들어두고 컴파일을 돌리면, Pico 는 내가 만든 저 UART 함수들을 “마치 자기가 만든 최신형 TCP 소켓 펌웨어라도 되는 양” 신나게 호출하며 전 세계로 PUT/SUB 바이너리 패킷을 뿜어대기 시작할 것이다.

이것이 C 언어 하드웨어 추상화(HAL)의 파괴력이며, 세상 어떤 야생의 칩도 한 시간 안에 Zenoh 네트워크로 끌어들일 수 있는 신의 기술이다.