💥 첫 번째 위기 — 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 검증을 진행 중입니다. 프로토타입이 완성되면 실차 테스트 영상과 함께 돌아오겠습니다.
'개인 개발 > 자동차 속도 기반 IoT 장식 만들기' 카테고리의 다른 글
| 자동차 속도 기반 IoT 장식 만들기 06 - 실차 테스트 결과와 방향 전환, OBD2에서 GPS+가속도 센서로 (0) | 2026.05.01 |
|---|---|
| 자동차 속도 기반 IoT 장식 만들기 05 - ESP32 블루투스 OBD2 연동 구현 (0) | 2026.05.01 |
| 자동차 속도 기반 IoT 장식 만들기 04 - 실제 프로토타입 만들기(납땜과 3d printer) (0) | 2026.05.01 |
| 자동차 속도 기반 IoT 장식 만들기 02 - 하드웨어 스펙 설계 (0) | 2026.04.25 |
| 자동차 속도 기반 IoT 장식 만들기 01 - 프로젝트 소개 및 비전 (0) | 2026.04.25 |