Home IoT Hub Series · 22
[22편] ESP32 업그레이드 - 터치 TFT + 블루투스로 두뇌 파워업
![[22편] ESP32 업그레이드 - 터치 TFT + 블루투스로 두뇌 파워업 대표 이미지](/iot-lab/assets/esp32-touch-tft-upgrade-hero.png)
21편에서 TFT로 디스플레이를 키웠다. 그런데 GPIO가 모자랐다. 터치를 붙이려니 핀이 없고, 백라이트 PWM도 포기해야 하고, 센서 노드를 두뇌에 직결하는 것도 꿈도 못 꾼다.
이유는 간단하다. ESP8266으로는 한계가 왔다. 이제 두뇌를 ESP32로 올릴 때다.
ESP8266 vs ESP32
| 항목 | ESP8266 (NodeMCU) | ESP32 (DevKit v1) |
|---|---|---|
| CPU | 80MHz 단일 코어 | 240MHz 듀얼 코어 |
| RAM | 50KB | 520KB |
| Flash | 4MB | 4MB |
| GPIO | 9개 | 24개 |
| SPI | 1개 (소프트웨어) | 2개 (하드웨어) |
| I2C | 1개 | 2개 |
| ADC | 1채널 10비트 | 18채널 12비트 |
| 터치 핀 | 없음 | 10개 (정전식) |
| Wi-Fi | 2.4GHz b/g/n | 2.4GHz b/g/n |
| 블루투스 | 없음 | BLE 4.2 + Classic |
| 가격 | 2,500원 | 4,000원 |
IoT_Hub_Net에 접속하면 됩니다.준비물과 가격
| 부품 | 수량 | 가격 | 비고 |
|---|---|---|---|
| ESP32 DevKit v1 | 1개 | 4,000원 | 30핀 버전 |
| ILI9341 TFT (XPT2046 터치 포함) | 1개 | 7,000원 | 21편에서 터치 없는 걸 샀다면 통째로 교체 |
| 피에조 부저 | — | 기존 재사용 | |
| 점퍼 와이어 (암-암) | 12개 | — | |
| 합계 | 11,000원 |
핀 연결
ESP32는 핀이 넉넉하다. TFT(SPI) + 터치(SPI 별도) + 부저 다 연결하고도 10개 이상 남는다.
| 센서/모듈 | 보드 연결 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| CS | GPIO15 |
| RESET | EN (리셋 공유) |
| DC | GPIO2 |
| MOSI | GPIO23 (VSPI MOSI) |
| SCK | GPIO18 (VSPI SCK) |
| LED | GPIO4 (PWM 밝기 제어) |
| T_CS | GPIO5 |
| T_IRQ | GPIO21 |
| T_DO | GPIO19 |
| T_DIN | GPIO22 |
| T_CLK | GPIO17 |
| + | GPIO13 |
| - | GND |
선행 작업
1. ESP32 보드 매니저 설치: Arduino IDE Preferences → Additional Boards Manager URLs에 추가 후 보드 매니저에서 ESP32 설치.
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
2. 라이브러리: TFT_eSPI, XPT2046_Touchscreen
3. TFT_eSPI User_Setup.h: 라이브러리 폴더에서 아래처럼 수정합니다.
#define ILI9341_DRIVER #define TFT_CS 15 #define TFT_DC 2 #define TFT_RST -1 #define TFT_MOSI 23 #define TFT_SCLK 18 #define TFT_BL 4 #define SPI_FREQUENCY 40000000
🚨 코드 넣기 전에 반드시!!
Wi-Fi SSID/비밀번호를 사용 중인 공유기 정보로 변경합니다. YOUR_SSID, YOUR_PASSWORD 부분이다. 안 바꾸면 Wi-Fi에 연결되지 않는다.
const char* ssid = "YOUR_SSID"; const char* password = "YOUR_PASSWORD";
전체 코드
아래는 생략 없는 전체 코드입니다. 이 파일 하나를 Arduino IDE에 붙여넣고, SSID와 비밀번호만 자신의 환경에 맞게 바꾼 뒤 ESP32에 업로드하면 됩니다.
#include <WiFi.h> #include <WebServer.h> #include <TFT_eSPI.h> #include <XPT2046_Touchscreen.h> // ============================================= // 1. Wi-Fi 설정 (직접 입력!) // ============================================= const char* ssid = "YOUR_SSID"; const char* password = "YOUR_PASSWORD"; const char* ap_ssid = "IoT_Hub_Net"; const char* ap_password = "iothub1234"; // ============================================= // 2. 하드웨어 핀 // ============================================= #define BUZZER_PIN 13 #define TFT_BL 4 TFT_eSPI tft = TFT_eSPI(); WebServer server(80); #define TS_CS 5 #define TS_IRQ 21 XPT2046_Touchscreen ts(TS_CS, TS_IRQ); // ============================================= // 3. 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" }; // ============================================= // 4. 색상 // ============================================= #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 #define C_LINE TFT_WHITE // ============================================= // 5. 타이머 // ============================================= 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; // ============================================= // 6. 터치 카드 영역 저장용 // ============================================= struct CardZone { int x, y, w, h, slotIdx; }; CardZone cards[4]; // ============================================= // setup // ============================================= void setup() { Serial.begin(115200); pinMode(BUZZER_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW); // TFT tft.init(); tft.setRotation(1); tft.fillScreen(C_BG); tft.setTextWrap(false); // 백라이트 PWM (ESP32 전용) ledcSetup(0, 5000, 8); ledcAttachPin(TFT_BL, 0); ledcWrite(0, 200); // 터치 ts.begin(); ts.setRotation(1); // 부팅 로고 tft.setTextColor(TFT_WHITE, C_BG); tft.drawString("ESP32 IoT Hub", 40, 140, 4); tft.drawString("iothub.co.kr", 60, 180, 2); delay(1000); // Wi-Fi AP+STA 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(); // IP 표시 tft.fillScreen(C_BG); tft.drawString("ESP32 IoT Hub Ready", 20, 30, 4); tft.drawString("LAN: " + WiFi.localIP().toString(), 10, 80, 2); tft.drawString("AP : " + WiFi.softAPIP().toString(), 10, 110, 2); delay(3000); } // ============================================= // loop // ============================================= void loop() { server.handleClient(); handleTouch(); 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 ESP32</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\">ESP32 + Touch TFT</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\">ESP32 IoT Hub | 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, 320, 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; } // 카드 영역 저장 (터치용) cards[i] = {4, y, 312, 48, idx}; // 카드 배경 tft.fillRoundRect(4, y, 312, 48, 6, cardBg); // 라벨 + 단위 tft.setTextColor(C_LABEL, cardBg); tft.drawString(label[idx], 14, y + 4, 2); // 구분선 tft.drawLine(14, y + 24, 306, y + 24, C_LINE); // 값 (크게) tft.setTextColor(isWarn ? TFT_WHITE : C_VALUE, cardBg); tft.drawString(val, 18, y + 28, 4); // 단위 표시 tft.setTextColor(C_LABEL, cardBg); tft.drawString(getUnit(idx), 250, y + 28, 2); // 터치 가능 표시 (제어 가능한 슬롯만) if (idx == 3 || idx == 6 || idx == 7 || idx == 8 || idx == 10 || idx == 19) { tft.drawString(":", 290, y + 4, 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 ""; } } // ============================================= // 터치 처리 // ============================================= void handleTouch() { if (!ts.tirqTouched() && !ts.touched()) return; TS_Point p = ts.getPoint(); int tx = map(p.x, 200, 3800, 0, 320); int ty = map(p.y, 200, 3800, 0, 240); for (int i = 0; i < 4; i++) { CardZone cz = cards[i]; if (tx >= cz.x && tx <= cz.x + cz.w && ty >= cz.y && ty <= cz.y + cz.h) { int idx = cz.slotIdx; if (idx < 0 || idx >= SLOT_COUNT) break; // AC(4번): ON/OFF 토글 if (idx == 3) { String cur = slot[idx]; slot[idx] = (cur == "ON") ? "OFF" : "ON"; Serial.println("Touch: AC -> " + slot[idx]); } // 조명(11번): ON/OFF 토글 else if (idx == 10) { String cur = slot[idx]; slot[idx] = (cur == "ON") ? "OFF" : "ON"; Serial.println("Touch: Light -> " + slot[idx]); } // 경고 슬롯: 리셋 else if (idx == 6 || idx == 7 || idx == 8 || idx == 19) { slot[idx] = "0"; Serial.println("Touch: Warn reset slot " + String(idx + 1)); } lastScroll = 0; // 즉시 화면 갱신 break; } } }
터치 UX 설계
카드 4개 각각이 터치 버튼 역할을 한다. 카드 오른쪽 위에 : 표시가 있으면 터치 가능한 슬롯이다.
| 터치한 카드 | 동작 |
|---|---|
| AC (4번) | ON↔OFF 토글 |
| Living Light (11번) | ON↔OFF 토글 |
| Gas Leak / Fire / Front Door (7·8·9번) | 경고 확인 → 0으로 리셋, 부저 정지 |
| Pet Bowl (20번) | 경고 확인 → 0으로 리셋, 부저 정지 |
| 그 외 슬롯 | 읽기 전용 (반응 없음) |
업로드 및 확인
1. 툴 → 보드 → ESP32 Dev Module, Flash Size 4MB. 2. USB 케이블로 ESP32 연결 후 업로드. ESP32는 업로드 속도 921600bps 가능. 3. TFT에 ESP32 IoT Hub 부팅 로고 → 4개 카드 그리드. 4. AC 카드 터치 → 시리얼 모니터 Touch: AC -> ON. 5. /set?no7=1로 가스 누출 켜고 7번 카드 터치 → 경고 리셋 확인.
안 될 때 체크포인트
| 증상 | 원인 | 해결 |
|---|---|---|
| 터치 전혀 안 됨 | SPI 배선 오류 | T_CS→GPIO5, T_CLK→GPIO17, T_DIN→GPIO22, T_DO→GPIO19 순서 재확인 |
| 터치 좌표가 엉뚱함 | map() 범위 불일치 | Serial.println(p.x)로 실제 터치 값 확인 후 map 범위 조정 |
| ESP32 업로드 실패 | 부트로더 진입 실패 | 업로드 직전 BOOT 버튼을 누르고 있다가 Connecting...이 뜨면 뗀다 |
| TFT 깜빡임 | 전원 부족 | ESP32 USB 포트에 5V 2A 이상 충전기 사용. PC USB 포트 전류 부족 가능 |
| Wi-Fi AP 안 뜸 | 모드 설정 순서 오류 | WiFi.mode(WIFI_AP_STA)가 WiFi.begin()보다 먼저인지 확인 |
지금까지 구성한 흐름
- ESP8266 두뇌를 ESP32로 교체. GPIO 24개, 듀얼코어, 블루투스 탑재.
- 2.4인치 TFT + XPT2046 터치로 화면을 손가락으로 제어할 수 있는 컨트롤 패널로 진화.
- 센서 노드 19대는 그대로 ESP8266. 두뇌만 바꿨을 뿐
IoT_Hub_Net통신은 완벽 호환. - 11,000원으로 집 상태판이 터치스크린 대시보드가 됐다.