Home IoT Hub Series · 21
[확장편] 컬러 TFT로 두뇌 디스플레이 업그레이드 - 2.4인치 ILI9341
![[확장편] 컬러 TFT로 두뇌 디스플레이 업그레이드 - 2.4인치 ILI9341 대표 이미지](/iot-lab/assets/color-tft-display-upgrade-ili9341-hero.png)
0.96인치 OLED는 작다. 4줄씩 스크롤해야 하고, 흰색 단색이라 경고가 반전될 때만 눈에 띈다. "이왕이면 컬러로, 더 크게" 하고 싶은 건 당연한 욕심이다.
이 확장편에서는 기존 OLED를 2.4인치 ILI9341 TFT LCD(240×320, 262K 컬러)로 교체한다. 아두이노 라이브러리 하나 바꾸고, 핀 몇 개 다시 꽂으면 끝. OLED보다 백배 보기 좋아진다.
준비물과 가격
| 부품 | 수량 | 가격 | 비고 |
|---|---|---|---|
| ILI9341 2.4인치 TFT LCD (SPI) | 1개 | 5,000원 | 240×320, 터치 없는 버전이면 충분 |
| 점퍼 와이어 (암-암) | 7개 | — | 기존 OLED 배선 해체 후 재사용 |
| 합계 | 5,000원 |
핀 연결
OLED를 떼어내고 TFT를 연결한다. I2C(4핀) 대신 SPI(7핀)다.
| 센서/모듈 | 보드 연결 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| CS | D8 (GPIO15) |
| RESET | RST |
| DC | D3 (GPIO0) |
| SDI (MOSI) | D7 (GPIO13) |
| SCK | D5 (GPIO14) |
| LED | 3.3V (또는 D4로 밝기 제어) |
RST 핀은 보드 리셋과 공유되지만 TFT RESET으로 써도 무방하다. LED는 백라이트. 3.3V에 직결하면 항상 최대 밝기.선행 작업
Arduino IDE 툴 → 라이브러리 관리에서 TFT_eSPI를 설치합니다. 설치 후 User_Setup.h 파일을 수정해야 합니다. Arduino 라이브러리 폴더에서 TFT_eSPI/User_Setup.h를 열고 기본 설정 대신 아래처럼 변경합니다.
// User_Setup.h 중 필요한 부분만 #define ILI9341_DRIVER #define TFT_CS 15 // D8 #define TFT_DC 0 // D3 #define TFT_RST -1 // NodeMCU RST 핀 사용 #define SPI_FREQUENCY 40000000 #define SPI_READ_FREQUENCY 20000000 #define SPI_TOUCH_FREQUENCY 2500000
🚨 코드 넣기 전에 반드시!!
YOUR_SSID와 YOUR_PASSWORD를 사용 중인 공유기 정보로 변경합니다. 안 바꾸면 Wi-Fi에 연결되지 않는다.
전체 코드
아래는 생략 없이 바로 업로드해 볼 수 있는 전체 코드입니다. 2편에서 달라진 부분은 display.xxx를 tft.xxx로 바꾼 것과 색상·좌표를 추가한 부분입니다. 나머지 구성도 함께 포함했습니다.
#include <ESP8266WiFi.h> #include <ESP8266WebServer.h> #include <TFT_eSPI.h> // ============================================= // Wi-Fi 설정 (직접 입력!) // ============================================= const char* ssid = "YOUR_SSID"; const char* password = "YOUR_PASSWORD"; const char* ap_ssid = "IoT_Hub_Net"; const char* ap_password = "iothub1234"; // ============================================= // 하드웨어 핀 // ============================================= #define BUZZER_PIN 13 // D7 (GPIO13) TFT_eSPI tft = TFT_eSPI(); ESP8266WebServer server(80); // ============================================= // 20개 메모리 슬롯 // ============================================= #define SLOT_COUNT 20 String slot[SLOT_COUNT] = { "--", "--", "--", "--", "--", "--", "--", "--", "--", "--", "--", "--", "--", "--", "--", "--", "--", "--", "--", "--" }; const char* label[SLOT_COUNT] = { "Living Temp","Living Hum","Outside Temp","AC", "Boiler","Stove","Gas Leak","Fire", "Front Door","Door Lock","Living Light","Fridge Temp", "CO2","Dust PM2.5","Power","Water", "Hot Water","Rain","Bath Hum","Pet Bowl" }; // ============================================= // 색상 (RGB565) // ============================================= #define C_BG TFT_BLACK #define C_TITLE_BG TFT_WHITE #define C_CARD TFT_DARKGREEN #define C_CARD_WARN TFT_RED #define C_LABEL TFT_SILVER #define C_VALUE TFT_YELLOW // ============================================= // 타이머 // ============================================= unsigned long lastScroll = 0; unsigned long lastBuzzer = 0; int scrollOffset = 0; const int ITEMS_PER_PAGE = 4; const int SCROLL_INTERVAL = 3000; bool warningActive = false; // ============================================= // setup // ============================================= void setup() { Serial.begin(115200); pinMode(BUZZER_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW); // TFT 초기화 tft.init(); tft.setRotation(1); // 가로 모드 (240×320) tft.fillScreen(C_BG); tft.setTextWrap(false); // 부팅 로고 tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.drawString("IoT Hub v3.0", 70, 140, 4); tft.drawString("iothub.co.kr", 80, 180, 2); delay(1000); // Wi-Fi 연결 WiFi.mode(WIFI_AP_STA); WiFi.begin(ssid, password); tft.fillScreen(C_BG); tft.drawString("WiFi connecting...", 30, 100, 2); Serial.print("Connecting to WiFi"); int dots = 0; while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); tft.drawString(".", 180 + dots * 8, 100, 2); dots++; } Serial.println(); Serial.print("STA IP: "); Serial.println(WiFi.localIP()); WiFi.softAP(ap_ssid, ap_password); Serial.print("AP IP: "); Serial.println(WiFi.softAPIP()); // 웹 서버 server.on("/set", handleSet); server.on("/", handleRoot); server.on("/status", handleStatus); server.onNotFound(handleNotFound); server.begin(); Serial.println("HTTP server started"); // IP 표시 tft.fillScreen(C_BG); tft.drawString("IoT Hub Ready", 20, 30, 4); tft.drawString("LAN: " + WiFi.localIP().toString(), 10, 90, 2); tft.drawString("AP : " + WiFi.softAPIP().toString(), 10, 120, 2); delay(3000); } // ============================================= // loop // ============================================= void loop() { server.handleClient(); updateTFT(); updateBuzzer(); } // ============================================= // 경고 상태 확인 // ============================================= bool checkWarning() { for (int i = 0; i < SLOT_COUNT; i++) { if ((i == 6 || i == 7 || i == 8) && slot[i] != "0" && slot[i] != "--") return true; if (i == 19 && slot[i] != "--" && slot[i].toInt() < 50) return true; } return false; } // ============================================= // 부저 제어 // ============================================= void updateBuzzer() { warningActive = checkWarning(); if (warningActive) { unsigned long now = millis(); if (now - lastBuzzer < 500) return; lastBuzzer = now; digitalWrite(BUZZER_PIN, !digitalRead(BUZZER_PIN)); } else { digitalWrite(BUZZER_PIN, LOW); } } // ============================================= // GET /set?no1=값&no2=값 ... // ============================================= void handleSet() { int updated = 0; for (int i = 1; i <= SLOT_COUNT; i++) { String paramName = "no" + String(i); if (server.hasArg(paramName)) { slot[i - 1] = server.arg(paramName); updated++; } } server.send(200, "application/json", "{\"updated\":" + String(updated) + "}"); } // ============================================= // GET / → HTML 대시보드 // ============================================= void handleRoot() { String html = "<!DOCTYPE html><html lang=\"ko\"><head>" "<meta charset=\"UTF-8\">" "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">" "<title>IoT Hub TFT</title>" "<style>" "*{margin:0;padding:0;box-sizing:border-box;}" "body{font-family:system-ui,sans-serif;background:#1a1a2e;color:#eee;padding:16px;}" "h1{text-align:center;font-size:1.3rem;margin-bottom:4px;color:#e94560;}" ".sub{text-align:center;font-size:0.7rem;color:#555;margin-bottom:16px;}" ".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(185px,1fr));gap:10px;}" ".card{background:#16213e;border-radius:8px;padding:12px;border-left:4px solid #0f3460;}" ".card.warn{border-left-color:#e94560;background:#1c0e1a;}" ".card .no{font-size:0.6rem;color:#555;margin-bottom:2px;}" ".card .label{font-size:0.8rem;color:#a0a0b0;margin-bottom:4px;}" ".card .value{font-size:1.2rem;font-weight:700;}" ".footer{text-align:center;margin-top:16px;font-size:0.7rem;color:#444;}" "</style></head><body>" "<h1>IoT Hub</h1><div class=\"sub\">TFT Edition</div>" "<div class=\"grid\">"; for (int i = 0; i < SLOT_COUNT; i++) { bool isWarn = false; String val = slot[i]; if ((i == 6 || i == 7 || i == 8) && val != "0" && val != "--") isWarn = true; if (i == 19 && val != "--" && val.toInt() < 50) isWarn = true; html += "<div class=\"card" + String(isWarn ? " warn" : "") + "\">"; html += "<div class=\"no\">#" + String(i + 1) + "</div>"; html += "<div class=\"label\">" + String(label[i]) + "</div>"; html += "<div class=\"value\">" + val + "</div></div>\n"; } html += "</div><div class=\"footer\">IoT Hub TFT | iothub.co.kr</div></body></html>"; server.send(200, "text/html; charset=utf-8", html); } // ============================================= // GET /status → JSON // ============================================= void handleStatus() { String json = "{"; for (int i = 0; i < SLOT_COUNT; i++) { if (i > 0) json += ","; json += "\"no" + String(i + 1) + "\":\"" + slot[i] + "\""; } json += ",\"warning\":" + String(warningActive ? "true" : "false") + "}"; server.send(200, "application/json", json); } // ============================================= // 404 // ============================================= void handleNotFound() { server.send(404, "text/plain", "404 - Use /set?no1=value or / or /status"); } // ============================================= // TFT 렌더링 (컬러 카드 4개) // ============================================= void updateTFT() { unsigned long now = millis(); if (now - lastScroll < SCROLL_INTERVAL) return; lastScroll = now; tft.fillScreen(C_BG); // 상단 타이틀 바 tft.fillRect(0, 0, 240, 28, TFT_WHITE); tft.setTextColor(TFT_BLACK, TFT_WHITE); char buf[40]; sprintf(buf, "IoT Hub [%d/%d]", scrollOffset / ITEMS_PER_PAGE + 1, (SLOT_COUNT + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE); tft.drawString(buf, 4, 6, 2); for (int i = 0; i < ITEMS_PER_PAGE; i++) { int idx = scrollOffset + i; if (idx >= SLOT_COUNT) break; int y = 36 + i * 52; String val = slot[idx]; bool isWarn = false; uint16_t cardBg = C_CARD; if ((idx == 6 || idx == 7 || idx == 8) && val != "0" && val != "--") { isWarn = true; cardBg = C_CARD_WARN; } if (idx == 19 && val != "--" && val.toInt() < 50) { isWarn = true; cardBg = C_CARD_WARN; } // 카드 배경 tft.fillRoundRect(4, y, 232, 48, 6, cardBg); // 라벨 tft.setTextColor(C_LABEL, cardBg); tft.drawString(label[idx], 12, y + 4, 2); // 구분선 tft.drawLine(12, y + 24, 228, y + 24, TFT_WHITE); // 값 (크게) tft.setTextColor(isWarn ? TFT_WHITE : C_VALUE, cardBg); tft.drawString(val, 16, y + 28, 4); // 단위 tft.setTextColor(C_LABEL, cardBg); tft.drawString(getUnit(idx), 180, y + 28, 2); } scrollOffset += ITEMS_PER_PAGE; if (scrollOffset >= SLOT_COUNT) scrollOffset = 0; } // 단위 반환 String getUnit(int idx) { switch (idx) { case 0: case 2: case 11: case 16: return "C"; case 1: case 18: return "%"; case 12: return "ppm"; case 13: return "ug"; case 14: return "W"; case 15: return "L"; case 19: return "g"; default: return ""; } }
OLED 대비 TFT의 장점
| 항목 | OLED (0.96") | TFT (2.4") |
|---|---|---|
| 크기 | 128×64 | 240×320 |
| 색상 | 흰색 단색 | 262K 컬러 |
| 가독성 | 1m 이상 어려움 | 2~3m 거리에서 OK |
| 정보량 | 4줄 스크롤 | 4개 카드 넉넉하게 |
| 경고 | 반전(흰↔검) | 빨간 카드 + 노란 글자 |
| 소비전력 | ~0.1W | ~0.5W |
| 가격 | 1,800원 | 5,000원 |
업로드 및 확인
1. User_Setup.h 수정 후 NodeMCU에 업로드합니다. 2. TFT에 부팅 로고 IoT Hub v3.0이 컬러로 표시되면 정상입니다. 3. /set?no7=1로 경고를 테스트하면 카드가 빨간색으로 바뀌고 값이 노란색으로 표시됩니다. 4. 화면이 커졌기 때문에 2~3m 떨어진 거리에서도 슬롯 내용을 읽을 수 있습니다.
안 될 때 체크포인트
| 증상 | 원인 | 해결 |
|---|---|---|
| 화면이 하얗게만 나옴 | SPI 배선 오류 | MOSI-D7, SCK-D5, CS-D8, DC-D3 순서 재확인 |
| 색이 이상함 | RGB/BGR 순서 반전 | User_Setup.h에서 #define TFT_RGB_ORDER TFT_BGR 주석 해제 |
| 화면 깜빡임 | 전원 부족 | TFT 백라이트 LED를 D4로 옮겨서 PWM 제어 |
TFT_eSPI 컴파일 에러 | 라이브러리 충돌 | 다른 TFT 라이브러리(Adafruit_ILI9341 등) 전부 제거 |
지금까지 구성한 흐름
- OLED를 2.4인치 컬러 TFT로 교체했다.
- 4줄짜리 흑백 화면이 컬러 카드 4개로 진화. 가독성, 시인성, 존재감 전부 업그레이드.
- 코드는 생략 없이 포함되어 있으며, SSID와 비밀번호만 자신의 환경에 맞게 변경하면 됩니다.
- 5,000원짜리 업그레이드로 두뇌가 진짜 "대시보드"다워졌다.