10.9 시스템 튜닝 및 최적화 전략
지금까지의 챕터로 당신의 센서는 겨우 숨을 쉬며 클라우드와 연결되었다.
하지만 상용 제품화 단계에서 고객은 두 가지를 요구한다. “센서를 달고 배터리가 1년은 가야 한다.” 와 “메모리 부족으로 인한 재부팅이 1년에 1번도 없어야 한다.”
Pico 의 코어는 충분히 가볍지만, 컴파일러 플래그와 당신이 지정한 하드웨어의 미세 세팅 하나하나가 합쳐져 칩의 소비전력과 플래시 롬(ROM) 점유율을 결정짓는다.
이 장은 임베디드 시니어 아키텍트만이 다룰 수 있는, Zenoh-Pico의 한계치를 끝까지 쥐어짜내는 마이크로 옵티마이징(Micro-optimizing) 런북이다.
1. QoS(Quality of Service) 설정 및 신뢰성 제어
Zenoh 는 QoS 를 지원한다. ‘메시지를 반드시 받게 할 것인가(Reliable)’, 아니면 ‘빠르게 던지고 잊을 것인가(Best-effort)’.
메모리 제약이 극심한 Pico 환경에서는 이 선택이 곧 메모리 용량의 차이로 직결된다.
1.0.1 [Runbook] 칩 수명형 QoS 컨트롤 전술
Pico 의 z_publisher_put() 을 호출할 때, QoS 모드에 따라 칩의 동작이 완전히 뒤바뀐다.
1. Reliable 모드 (절대적 신뢰성)
재전송(Retransmission) 스펙 때문에 칩은 자신이 이전에 쐈던 데이터를 라우터가 “잘 받았어(Ack)” 라고 대답해 줄 때까지, 내부 메모리의 어느 어두운 곳에 원본을 보관(Cache) 하고 있어야 한다.
이 경우, 아무리 작은 데이터라도 당신이 지정해둔 tx_buffer 램 공간이 순식간에 꽉 차버린다. TCP 기반의 안정적인 공유기(WiFi) 밑에서 중요한 제어 명령을 다룰 때만 허용된다.
z_publisher_options_t opts = z_publisher_options_default();
opts.reliability = Z_RELIABILITY_RELIABLE;
z_publisher_put(&pub, data, len, &opts);
2. Best-Effort 모드 (발사 후 망각)
데이터를 쏘고 나면 칩은 미련 없이 버퍼를 지워버리고(100% 비워둠) 다음 턴을 뛴다. 배터리로 작동하는 드론이나, 1초에 1,000번씩 온도계를 읽어내는 기계에서 Reliable 은 미친 짓이다. 당신의 센서가 초당 수십 개의 데이터를 쏜다면, 중간에 1~2개 쯤 날아가도 시스템은 안 죽는다. 무조건 이 모드를 쓴다.
opts.reliability = Z_RELIABILITY_BEST_EFFORT;
QoS 의 본질은 “네트워크 대역폭” 이 아니라, MCU 의 한정된 “RAM 보존 시간” 을 누가 잡느냐의 싸움이다.
2. 플래시 메모리(ROM) 및 RAM 사용량 최소화를 위한 컴파일 옵션
Pico 코드를 그대로 가져다 빌드(make) 하면, 플래시(ROM) 용량이 50KB ~ 100KB 가량 증식한다.
당신의 싸구려 칩이 가진 용량이 딱 64KB 라면 어떻게 해야 할까? 코드를 도려내서 뼈만 남기는 빌드 시스템 해킹이 필요하다.
2.0.1 [Runbook] 다이어트 매크로 컴파일(Compile-time Stripping) 전술
Pico의 CMakeLists.txt 에 무자비한 매크로(Macro) 정의를 주입하여, 안 쓰는 통신 스택을 소스 레벨부터 증발시켜버린다.
1. 기능 삭제 C 매크로 (add_definitions)
add_definitions(-DZP_MAX_ENDPOINTS=1): 원래 Pico는 라우터 여러 대를 물고 스위칭하는 배열이 잡혀있다. 이걸 1개로 줄여 RAM 사용량을 500바이트 날려버린다.add_definitions(-DZP_MAX_PUBLISHERS=2): 퍼블리셔(Topic)를 2개만 쓰겠다고 미리 선언한다.add_definitions(-DZP_MAX_SUBSCRIBERS=1): 마찬가리자.add_definitions(-DZP_DEBUG=0):printf와 파일 경로 로깅 함수들. 의외로 이 문자열들이 ROM 공간을 10KB 넘게 잡아먹는다. 무조건 끈다.
2. GCC 컴파일러 최적화 플래그의 끝판왕
## 실행 속도보다 오로지 '코드 용량 크기 줄이기' 에 몰빵한 -Os
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os")
## C 함수를 개별 블록으로 잘라내서 링커가 죽은 코드(Dead code) 를
## 날려버리기 쉽도록 도마에 올려놓는다.
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffunction-sections -fdata-sections")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections")
## LTO (Link Time Optimization) - 함수 인라인화로 크기를 더 압축!
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -flto")
이 옵션들을 세팅하고 빌드 인스펙터(Size Tool) 를 돌려보면, 80KB 짜리 펌웨어가 단 18KB 짜리 기적의 통신 괴물로 압축되어 나오는 것을 보게 될 것이다.
3. 네트워크 트래픽 오버헤드 감축 방안
데이터보다 TCP 의 3-way Handshake 와 짤짤이 보내는 헤더(Header)가 더 무거울 때가 많다. 특히 모바일 종량제 유심(LTE Cat.M1)을 달고 있는 기기라면 오버헤드는 그대로 요금 폭탄이 된다.
3.0.1 [Runbook] 프로토콜 침묵(Silence) 튜닝 전술
1. KeepAlive(Heartbeat) 늘리기
라우터에게 “나 아직 안 죽었다“고 증명하는 핑(Ping) 주기가 짧으면 트래픽이 샌다.
z_config_t config = z_config_default();
// 기본 수 초인 Lease 주기를 600초(10분)로 극단적으로 늘린다.
// (10분 동안 통신이 없어도 라우터가 봐준다)
zp_config_insert_json(&config, ZP_ROUTER_LEASE_KEY, "600000");
2. 센서 데이터 취합(Batching / Packing)
온도(4byte) 1개를 보낼 때마다 z_put 을 때리면 IP 헤더 20바이트 + Zenoh 헤더 5바이트가 배보다 배꼽이 큰 통신을 만든다.
struct SensorBatch {
int temp[10]; // 10개를 모을때까지 기다린다
};
// 100밀리초 주기라면 1초에 딱 1번만 쏜다. 평균 트래픽이 90% 이상 절감된다.
3. Key Expression ID 매핑 체질화
10.7.3 장에서 배운 ID 매핑은 선택이 아니라 의무다. 문자열(“machine/sensor/1234”)은 두 번 다시 네트워크 전파를 타서는 안 된다. 오직 숫자 ID 로만 대화하라.
4. 슬립 모드(Sleep Mode) 및 네트워크 웨이크업을 고려한 전력 관리
동전 건전지(CR2032) 하나로 1년을 버티는 도어록 센서.
이런 프로젝트에서 z_session_yield 를 계속 돌린다는 건 이틀 안에 건전지를 태워버리겠다는 선언이다. 칩을 기절(Deep Sleep)시켜야 한다.
4.0.1 [Runbook] 라우터 포기(Tear-down) 및 소생 전술
대부분의 칩들은 기절했다 깨어나면(Deep Sleep Wake up) RAM 내용이 다 날아가 초기화된다. 어설프게 스레드를 멈췄다 켜려 하지 마라. 통신을 아예 끊었다가 완전히 처음부터 재부팅시키는 무식한 방법만이 배터리를 구원한다.
#include <zenoh-pico.h>
void wake_and_shoot_sensor(int value) {
// 1. 하드웨어가 깨어난 직후 처음부터 세션 개통 (약 0.1~0.5초 소요)
z_config_t config = configure_minimum_pico();
z_session_t session;
if (z_open(&session, &config, NULL) >= 0) {
if (z_check(session)) {
// 2. 단 한발의 총알(데이터)만 발사!
z_owned_keyexpr_t key = z_keyexpr_new("door/status");
z_publisher_t pub = z_declare_publisher(z_loan(session), z_loan(key), NULL);
// ... (데이터 PUT)...
z_drop(z_move(key));
}
// 3. 발사 직후 미련 없이 세션 폐기! (라우터와 연결 즉각 단절)
z_close(z_move(session));
}
// 4. 다시 영면에 든다 (Deep Sleep 명령)
// 이 상태에서 칩은 수 마이크로 암페어(uA) 만을 소모한다.
HW_ENTER_DEEP_SLEEP();
}
이 “게릴라 히트 앤 런(Hit and Run)” 아키텍처에서 주의할 점은, 칩이 깨어나 공유기에 IP를 받아오고 라우터와 세션(Handshake)을 맺는 시간(보통 1초~3초 내외) 동안에 소모되는 전력을 반드시 측정해야 한다는 것이다. 만약 이 부팅 소요 시간이 길다면, 차라리 깊은 수면(Deep Sleep) 대신 얕은 수면(Light Sleep: 메모리와 WiFi 연결만 살려둠)을 쓰면서 주기적으로 yield 를 돌려주는 것이 배터리적으로 더 유리할 수도 있다. (전력 프로파일링 필수 적용).