Home IoT Hub Series · 21

[확장편] 컬러 TFT로 두뇌 디스플레이 업그레이드 - 2.4인치 ILI9341

[확장편] 컬러 TFT로 두뇌 디스플레이 업그레이드 - 2.4인치 ILI9341 대표 이미지

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원

핀 연결

[확장편] 컬러 TFT로 두뇌 디스플레이 업그레이드 - 2.4인치 ILI9341 배선도

OLED를 떼어내고 TFT를 연결한다. I2C(4핀) 대신 SPI(7핀)다.

센서/모듈보드 연결
VCC3.3V
GNDGND
CSD8 (GPIO15)
RESETRST
DCD3 (GPIO0)
SDI (MOSI)D7 (GPIO13)
SCKD5 (GPIO14)
LED3.3V (또는 D4로 밝기 제어)
NodeMCU의 RST 핀은 보드 리셋과 공유되지만 TFT RESET으로 써도 무방하다. LED는 백라이트. 3.3V에 직결하면 항상 최대 밝기.

선행 작업

Arduino IDE 툴 → 라이브러리 관리에서 TFT_eSPI를 설치합니다. 설치 후 User_Setup.h 파일을 수정해야 합니다. Arduino 라이브러리 폴더에서 TFT_eSPI/User_Setup.h를 열고 기본 설정 대신 아래처럼 변경합니다.

[확장편] 컬러 TFT로 두뇌 디스플레이 업그레이드 - 2.4인치 ILI9341 - 전체 코드
// 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_SSIDYOUR_PASSWORD를 사용 중인 공유기 정보로 변경합니다. 안 바꾸면 Wi-Fi에 연결되지 않는다.

전체 코드

아래는 생략 없이 바로 업로드해 볼 수 있는 전체 코드입니다. 2편에서 달라진 부분은 display.xxxtft.xxx로 바꾼 것과 색상·좌표를 추가한 부분입니다. 나머지 구성도 함께 포함했습니다.

[확장편] 컬러 TFT로 두뇌 디스플레이 업그레이드 - 2.4인치 ILI9341 - 전체 코드
#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×64240×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원짜리 업그레이드로 두뇌가 진짜 "대시보드"다워졌다.