10.10 마이크로컨트롤러 환경의 디버깅 및 트러블슈팅

10.10 마이크로컨트롤러 환경의 디버깅 및 트러블슈팅

클라우드의 컨테이너가 죽으면 로그가 남지만, 칩셋이 죽으면 아무런 흔적도 남지 않는다.
LED 만 깜빡이다가 로봇이 장렬히 산화하는 모습을 지켜봐야 하는 눈먼 엔지니어들을 위한 최후의 구명정이다.

Zenoh-Pico 가 칩을 박살낼 때, 도대체 어디서부터 네트워크가 막혔고, 내 메모리를 언제 다 갉아먹었는지를 찾아내는 하드웨어 제어 엔지니어의 부검(Autopsy) 런북이다.

1. 시리얼 출력을 활용한 Zenoh-Pico 내부 로그 추적

“연결이 안 돼요” 에 대한 해답은 칩 안에 숨겨져 있다. Pico 는 내부적으로 무엇을 하고 있는지 계속 당신에게 소리치고 싶어 하지만, 당신이 귀를 막아뒀을 뿐이다.

1.0.1 [Runbook] JTAG/UART 로그 터널 개통 전술

1. CMake 빌드 옵션 개방
Pico 를 빌드할 때 무조건 꺼뒀던 디버그 모드를 켠다.

add_definitions(-DZ_DEBUG_LEVEL=4) # 0: None, 1: Error, 2: Warn, 3: Info, 4: Trace
## 주의: 이 옵션을 켜는 순간 펌웨어 용량이 10KB~20KB 팽창할 수 있다!

2. UART(시리얼) 출력 포팅
Pico 엔진이 printf 를 호출할 때, 이 함수가 실제로 당신이 연결한 USB 라인이나 UART 라인을 타고 나오게 끔 _write 시스템 콜을 재정의(Override)해야 한다. (GCC 기준)

#include <stdio.h>
// 외부 UART 하드웨어 핸들러 (예: STM32)
extern UART_HandleTypeDef huart1;

int _write(int file, char *ptr, int len) {
    // Pico 엔진 내부의 printf 가 결국 이 함수를 거쳐 나간다.
    HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, 10);
    return len;
}

이제 테라텀(TeraTerm) 이나 푸티(PuTTY) 로 시리얼 포트를 열어보면, Pico 가 TCP 악수(Handshake)를 할 때마다, 패킷을 조립할 때마다 화면을 미친 듯이 스크롤 하며 생존 신고를 할 것이다. 에러가 나면 정확히 “어떤 버퍼가 모자른지” 로그가 찍힌다.

2. 메모리 릭(Memory Leak) 및 스택 오버플로우(Stack Overflow) 방지

펌웨어를 올려서 3시간은 잘 돌아가는데, 다음 날 출근해 보면 로봇이 돌덩이가 되어 있다. 전형적인 메모리 파괴 증상이다.

2.0.1 [Runbook] 콜백(Callback) 스택 압살 방어 전술

1. 스택 터짐(Stack Overflow)의 원인
Pico 의 콜백 함수(cmd_callback 등)는 메인 함수의 z_session_yield 가 도는 아주 협소한 스택 공간 위에서 기생하여 돌아간다.
그런데 당신이 만약 콜백 함수 안에서 버퍼를 한 1KB 짜리 잡았다 치자.

void cmd_callback(const z_sample_t *s, void *ctx) {
    // ❌ 절대 금지: 콜백 안에서 거대한 배열 할당!
    char big_buffer[1024]; 
    sprintf(big_buffer, "..."); 
}

함수가 실행되는 순간 스택 포인터(SP)가 한도를 넘어가버려(Overflow), 다른 태스크의 메모리나 운영체제 코어를 박살 내버리고 하드 파울트(Hard Fault)를 일으킨다.
콜백 안에서는 무조건 전역 변수(Static)를 쓰거나 극도로 작은 지역 변수(Heap 도 안 됨)만 써야 한다.

2. FreeRTOS 스택 워터마크 모니터링
통신 태스크가 스택을 얼마나 파먹었는지 수시로 감시하라.

// 통신 task의 무한루프 안에서 호출
UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
// 이 값이 0 에 가까워진다면 당장 태스크 생성 시 스택 크기(Stack size)를 두 배로 늘려라!
printf("남은 스택 공간: %lu\n", uxHighWaterMark); 

3. 네트워크 연결 단절 감지 및 자동 재연결(Auto-reconnect) 루틴 구현

로봇이 터널에 들어갔다. 공유기와의 연결이 끊어졌다. Zenoh 의 세션 파이프는 깨져버렸고, 터널을 빠져나와도 영원히 통신은 복구되지 않는다. 소프트웨어 리셋이 필요하다.

3.0.1 [Runbook] 피닉스(Phoenix) 불사신 파이프라인 전술

가장 좋은 리셋은 “시스템 보드 자체를 초기화(Hard Reset)” 하는 것이지만, 모터가 도는 도중에 그럴 순 없다. Pico 세션만 닫았다가 다시 개통해야 한다.

#include <zenoh-pico.h>

extern z_session_t session;

// 타임아웃 감시자
uint32_t last_rx_time = 0;

void keepalive_callback(const z_sample_t *s, void *ctx) {
    // 서버가 나에게 주기적으로 쏴주는 Heartbeat
    last_rx_time = HAL_GetTick(); 
}

void network_monitor_loop() {
    uint32_t current = HAL_GetTick();
    
    // 5초 동안 단 하나의 데이터도 도착하지 않았다면? 망이 끊긴 것이다!
    if (current - last_rx_time > 5000) {
        printf("🚨 통신 단절 감지! Zenoh 세션을 강제 부활시킨다!\n");

        // 1. 기존 세션을 파괴한다 
        // (이 때 LwIP 소켓 자원이 반환되며 메모리 누수를 막는다)
        z_close(&session);
        
        // 시간차를 둔다 (공유기에서 소켓 할당 해제할 시간)
        vTaskDelay(pdMS_TO_TICKS(1000));
        
        // 2. 다시 환경 설정 세팅
        z_config_t config = z_config_default();
        zp_config_insert_json(&config, ZP_PEER_KEY, "\"tcp/192.168.0.10:7447\"");
        
        // 3. 재개통 (성공할 때까지 무한 재시도!)
        while (z_open(&session, &config, NULL) < 0 || !z_check(session)) {
            printf("재접속 시도 중...\n");
            vTaskDelay(pdMS_TO_TICKS(2000));
        }

        // 4. 구독(Subscriber) 라인 재구축
        // (세션이 부활했으므로 이전에 세팅했던 구독자 선언은 다 날아갔다!)
        setup_my_subscribers(); 

        // 5. 부활 완료
        last_rx_time = HAL_GetTick();
        printf("✅ 예비 파이프라인 개통 완료.\n");
    }
}

이 끊임없는 Re-declare(재선언) 아키텍처는 스마트 기기가 와이파이를 잡았다 놨다 하는 현실 환경에서 생존하기 위한 최소한의 발버둥이자 궁극의 해결책이다.

4. 패킷 스니핑(Wireshark)을 통한 프로토콜 레벨 디버깅

MCU 쪽 코드를 백날 들여다봐도 문제가 어딘지 모르겠다면, 전파 속으로 손을 집어넣어 데이터가 공중에 날아다니는 형태 자체를 눈으로 확인해야 한다.

4.0.1 [Runbook] 허공 낚시(Air-sniffing) 전술

라우터(Raspberry Pi 등)나 로컬 PC의 네트워크 인터페이스를 도청한다.

1. Wireshark 준비 및 Zenoh 플러그인 장착
Zenoh 통신망은 UDPTCP 든 7447 포트를 기점으로 작동한다. 일반 와이어샤크(Wireshark)로 보면 그냥 “데이터(Data)” 로 뭉뚱그려 보여서 읽을 수가 없지만, Zenoh 가 제공하는 전용 Wireshark Dissector(해석 플러그인)를 깔면 완전히 까발려진다.

2. 도청 필터(Filter) 설정
Wireshark 의 필터 창에 다음 커맨드를 갈겨 넣는다.

tcp.port == 7447 || udp.port == 7447

3. 트러블슈팅 부검 결과 해석

  • 현상 1: 로봇의 센서가 계속 변하는데, 대시보드는 멈춰있다?
    -> 와이어샤크를 보니 PUT 프레임은 계속 날아가고 있는데, 대상 Key 가 매핑 ID로 변환되는 과정(DECLARE) 에서 꼬여서 라우터가 못 알아먹고 버리고 있다! (ID 설정 코드 확인)

  • 현상 2: 로봇이 공유기에 연결이 안 된다?
    -> 와이어샤크를 보니, 내 로봇 IP (192.x.x.x) 에서 목적지 라우터로 Init 패킷을 쐈는데, 라우터가 불쌍한 센서에게 Ack(확인) 패킷을 쏴주지 않고 있다! 라우터의 zenohd 가 꺼져있거나, 라우터 PC 윈도우 방화벽이 7447 포트를 블로킹한 게 확실하다!

MCU 개발자는 코딩 절반, 와이어샤크 절반의 비율로 일해야만 퇴근할 수 있음을 명심하라.