본문 바로가기

자동차 속도 기반 IoT 장식 만들기 03 - 펌웨어 개발 및 메모리 최적화

@밀양박씨!2026. 4. 25. 01:57

💥 첫 번째 위기 — WiFi와 동시에 터지는 크래시

하드웨어를 결정하고 본격적으로 펌웨어를 짜기 시작했습니다. 기존 obdper-smartphone 프로젝트를 포팅하는 방향으로 시작했는데, 바로 벽에 부딪혔습니다.

WiFi 스택을 올리는 순간 OOM(Out of Memory) 크래시가 발생했습니다.

원인을 추적하자 범인이 나왔습니다 — TFT_eSPI의 스프라이트 더블버퍼였습니다.

// 240x320 해상도, 16-bit 색상
// 이것 하나가 153,600 bytes = 153KB를 잡아먹는다
TFT_eSprite sprite(&tft);
sprite.createSprite(240, 320); // 여기서 153KB 할당

ESP32-WROOM DRAM 320KB에서 WiFi 스택이 약 40KB, WebServer + DNS가 약 10KB를 차지합니다. 153KB짜리 버퍼가 올라갈 공간이 없었습니다.

목표를 세웠습니다: "같은 기능, 151KB 적은 RAM".

 

 

 

✂️ 최적화 1 — 스프라이트 버퍼 완전 제거

더블버퍼를 제거하고 부분 갱신(Partial Update) 방식으로 교체했습니다.

// 기존: 전체 화면 버퍼 후 flush — 153KB 필요
sprite.fillSprite(BG_COLOR);
sprite.drawXxx(...);
sprite.pushSprite(0, 0);

// 개선: 캐릭터 영역만 직접 갱신 — 버퍼 0KB
tft.fillRect(g_x, g_y, PET_W, PET_H, BG_COLOR);
tft.fillCircle(cx, cy, r, color);

화면이 약간 깜빡이지 않을까 우려했지만, 갱신 영역을 캐릭터 크기로 제한하니 속도 차이가 거의 없었습니다. 배경과 UI 요소는 상태가 바뀔 때만 다시 그립니다.

 

 

✂️ 최적화 2 — HTML을 PROGMEM에, RAM 점유 0

웹 리모컨 HTML은 원본 기준 약 8KB였습니다. 이걸 RAM에 올리면 고스란히 8KB가 사라집니다. 두 가지 작업을 했습니다.

첫째, PROGMEM (Flash) 에 저장:

// PROGMEM 키워드로 Flash에 저장 — RAM 점유 0
static const char REMOTE_HTML[] PROGMEM = R"HTML(
  <!DOCTYPE html>...
)HTML";

// 전송 시 Flash에서 직접 읽어서 스트리밍
server.send_P(200, "text/html", REMOTE_HTML);

둘째, String 객체 전면 금지:
Arduino의 String 객체는 힙 단편화를 일으킵니다. HTTP 핸들러에서 String을 쓰던 코드를 모두 문자열 리터럴과 고정 버퍼로 교체했습니다.

 

 

 

✂️ 최적화 3 — 캐릭터를 도형으로 그린다

원본 프로젝트는 캐릭터를 RGB565 비트맵으로 표현했습니다. 5종 캐릭터 × 3 상태 × 4 프레임이면 최소 수백 KB ~ 수 MB Flash가 필요합니다.

대신 Adafruit GFX의 기본 도형 API(fillCircle, fillRect, fillTriangle 등)만으로 캐릭터를 전부 그렸습니다. 5종 캐릭터 전체 코드가 5KB 이하입니다.

// 카피바라 — 비트맵 0KB, 코드로 표현
void drawCarpybara(int cx, int cy, PetState state) {
  // 몸통
  tft.fillRoundRect(cx-28, cy-18, 56, 36, 12, C_BROWN);
  // 눈
  tft.fillCircle(cx-8, cy-4, 4, C_DARK);
  tft.fillCircle(cx+8, cy-4, 4, C_DARK);
  // 코
  tft.fillRoundRect(cx-6, cy+4, 12, 6, 3, C_NOSE);
  if (state == RUN)   drawRunLegs(cx, cy);
  if (state == SLEEP) drawZzz(cx, cy);
}

 

 

 

📊 최적화 결과

항목 원본 Carpybara 절감
Sprite 더블버퍼 153 KB 0 KB −153 KB
HTML (RAM) ~8 KB 0 KB (PROGMEM) −8 KB
캐릭터 데이터 (Flash) 수백 KB~수 MB <5 KB 대폭 절약
전역 RAM 합계 ~165 KB ~14 KB −151 KB
부팅 후 자유 힙 ~50 KB ~200 KB 이상 +150 KB

 

 

🐾 캐릭터 상태 머신

속도 슬라이더 값 하나가 자동으로 캐릭터의 상태를 결정합니다.

PetState speedToState(int spd) {
  if (spd == 0)       return SLEEP;  // ZZZ 표시, 400ms 지연
  else if (spd < 20)  return WALK;
  else if (spd < 60)  return RUN;
  else                return TURBO;  // 볼살 날림
}

각 상태는 프레임 딜레이가 다르고, SLEEP 상태에서는 의도적으로 400ms 지연을 줘서 "늘어진 느낌"을 표현합니다.

 

 

📡 OBD2 데이터 처리 — 노이즈를 잡아라

OBD2 센서 원시값은 노이즈가 많습니다. 그대로 쓰면 캐릭터 애니메이션이 심하게 떨립니다. 지수 가중 이동 평균(EMA)으로 부드럽게 만들었습니다:

// alpha가 클수록 최신 데이터에 빠르게 반응
smoothed = alpha * raw_value + (1 - alpha) * previous_smoothed;
모드 alpha 이유
속도 0.3 자연스러운 가속/감속 표현
냉각수 온도 0.1 온도 변화는 느리므로 강한 평활화
RPM 0.4 엔진 반응성 유지
스로틀 0.7 운전자 입력에 즉각 반응

MAF, Engine Load처럼 차량 기종마다 출력 범위가 크게 다른 센서는 주행 중 자동 캘리브레이션을 적용합니다. 처음에는 기본 범위로 동작하다가 샘플이 쌓이면 실제 관찰 최솟값/최댓값으로 정규화합니다. 캘리브레이션 데이터는 Flash(NVS)에 저장해 재시동 시 이어받습니다.

 

 

📱 WiFi Captive Portal — OS 3종을 한번에 잡는 법

Captive Portal 구현에서 가장 까다로운 부분은 OS별로 probe 엔드포인트가 다르다는 것입니다.

OS probe 엔드포인트
Android /generate_204
iOS / macOS /hotspot-detect.html
Windows /ncsi.txt, /connecttest.txt
// DNS: 모든 도메인 → 192.168.4.1
dns.start(53, "*", WiFi.softAPIP());

// Captive Portal 유도 엔드포인트
server.on("/generate_204",        handleRoot);  // Android
server.on("/hotspot-detect.html", handleRoot);  // iOS
server.on("/ncsi.txt",            handleRoot);  // Windows
server.on("/connecttest.txt",     handleRoot);  // Windows 10+
server.onNotFound(handleRoot);                  // 나머지 전부

 

 

📁 소프트웨어 구조


Carpybara_Test/
├── Carpybara_Test.ino   메인 스케치 (setup / loop)
├── pet_config.h          핀, 색상, 속도, 상태 enum
├── pet_qr.h              부팅 QR 비트맵 (PROGMEM)
├── pet_anim.h            5종 캐릭터 드로잉 + 상태 머신
├── pet_obd.h             6 OBD 모드 HUD + 반응형 오버레이
└── pet_web.h             WiFi AP + Captive Portal + 웹 리모컨

 

 

💭 배운 것들

  • TFT 스프라이트는 편하지만 153KB는 너무 비싸다 — Adafruit GFX 부분 갱신으로 충분히 대체 가능합니다
  • PROGMEM은 선택이 아니라 필수다 — 문자열 상수, HTML, 비트맵은 모두 Flash에 두어야 합니다
  • Arduino String 객체는 임베디드에서 위험하다 — 힙 단편화가 며칠 연속 동작 후 메모리 부족을 일으킬 수 있습니다
  • Captive Portal은 OS별로 다르다 — iOS, Android, Windows를 각각 테스트해야 합니다
  • EMA alpha는 모드마다 달라야 한다 — 스로틀은 즉각 반응(0.7), 냉각수는 천천히(0.1), 같은 값으로 통일하면 어딘가 어색해집니다

 

 

🔗 마치며

3편에 걸쳐 Carpybara 프로젝트의 탄생 계기부터 하드웨어 결정, 소프트웨어 개발까지 다뤘습니다. 현재는 캐리어보드 PCB 설계와 디스플레이 EVT 검증을 진행 중입니다. 프로토타입이 완성되면 실차 테스트 영상과 함께 돌아오겠습니다.

밀양박씨!
@밀양박씨! :: 박씨의 개발블로그

lovebotw049 님의 블로그 입니다.

목차