← All projects
Displays Intermediate

Weather Dashboard on a Colour TFT: ILI9341, TFT_eSPI and DHT22

A 2.8" 320×240 colour SPI display turns the ESP32 into a good-looking panel. We configure TFT_eSPI through User_Setup.h, draw frames and bars, align text with datums, and update the numbers flicker-free with padding.

Difficulty
Intermediate
Time
1 h
Category
Displays
Updated

About the project

A monochrome OLED is nice, but a 2.8″ ILI9341 colour TFT (320×240, 65 thousand colours) is another league: instrument panels, graphs, small user interfaces. On the ESP32 the best library for it is Bodmer's TFT_eSPI, with hardware SPI and DMA. We build a weather dashboard: temperature and humidity from a DHT22, with coloured digits and bars.

What you need

  • ESP32 DevKit
  • 2.8″ ILI9341 SPI TFT module (the red board; the touch version works too)
  • DHT22 sensor
  • The TFT_eSPI and DHT sensor library libraries

Wiring

TFT ESP32
VCC 3V3
GND GND
CS GPIO15
RESET GPIO4
DC (D/C, RS) GPIO2
MOSI (SDI) GPIO23
SCK GPIO18
LED (backlight) 3V3 (or a PWM GPIO for brightness)
MISO (SDO) optional

DHT22: VCC → 3V3, GND → GND, DATA → GPIO27.

MOSI 23 and SCK 18 are the hardware VSPI bus. They give 40 MHz; other pins fall back to software SPI, several times slower.

Setting up TFT_eSPI

TFT_eSPI takes its pins and driver not from the sketch but from User_Setup.h in the library folder (or from User_Setup_Select.h). For this wiring, these lines must be uncommented:

#define ILI9341_DRIVER
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS   15
#define TFT_DC    2
#define TFT_RST   4
#define LOAD_GLCD
#define LOAD_FONT2
#define LOAD_FONT4
#define SPI_FREQUENCY  40000000

A library update overwrites the file, so keep your own copy.

Techniques worth knowing

  • Datum sets the text anchor point: MC_DATUM is the centre, TC_DATUM the middle of the top edge, ML_DATUM the middle of the left edge. Centring a title in a frame takes one line.
  • Padding. The background colour in setTextColor(colour, background) overwrites old text, but a shorter number ("9.5" after "10.5") leaves a "tail". setTextPadding(width) fills the whole given width with the background.
  • Don't clear the whole screen every time: fillScreen() on 320×240 takes ~30–40 ms and flickers. Draw the static frame once in setup(), and in loop() redraw only the parts that change.
  • Colours are 16-bit RGB565: use the ready-made TFT_RED, TFT_SKYBLUE and so on, or tft.color565(r, g, b).

Code

// Weather dashboard: 2.8" ILI9341 with TFT_eSPI + DHT22
#include <TFT_eSPI.h>
#include <DHT.h>

TFT_eSPI tft = TFT_eSPI();   // pins come from User_Setup.h: MOSI 23, SCLK 18, CS 15, DC 2, RST 4
DHT dht(27, DHT22);

// the static part of the screen, drawn once
void drawFrame() {
  tft.fillScreen(TFT_BLACK);
  tft.fillRect(0, 0, 320, 34, TFT_NAVY);
  tft.setTextColor(TFT_WHITE, TFT_NAVY);
  tft.setTextDatum(ML_DATUM);
  tft.drawString("ESP32 weather", 10, 17, 2);
  tft.drawRoundRect(8, 44, 148, 150, 10, TFT_DARKGREY);
  tft.drawRoundRect(164, 44, 148, 150, 10, TFT_DARKGREY);
  tft.setTextDatum(TC_DATUM);
  tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
  tft.drawString("Temperature", 82, 54, 2);
  tft.drawString("Humidity", 238, 54, 2);
}

// digit colour by temperature
uint16_t tempColor(float t) {
  if (t < 10) return TFT_CYAN;
  if (t < 25) return TFT_GREEN;
  if (t < 30) return TFT_ORANGE;
  return TFT_RED;
}

void drawBar(int x, int y, int w, int h, float frac, uint16_t color) {
  frac = constrain(frac, 0.0, 1.0);
  int filled = (int)(w * frac);
  tft.fillRoundRect(x, y, w, h, 4, TFT_DARKGREY);
  if (filled > 8) tft.fillRoundRect(x, y, filled, h, 4, color);
}

void setup() {
  Serial.begin(115200);
  dht.begin();
  tft.init();
  tft.setRotation(3);   // landscape, 320×240, pins on the left
  drawFrame();
}

void loop() {
  float t = dht.readTemperature();
  float h = dht.readHumidity();
  if (isnan(t) || isnan(h)) {
    tft.setTextColor(TFT_RED, TFT_BLACK);
    tft.setTextDatum(MC_DATUM);
    tft.drawString("Sensor error", 160, 215, 2);
    delay(2000);
    return;
  }
  tft.setTextDatum(MC_DATUM);
  tft.setTextPadding(tft.textWidth("-88.8", 4));   // the padding erases the previous value
  tft.setTextColor(tempColor(t), TFT_BLACK);
  tft.drawFloat(t, 1, 82, 108, 4);
  tft.setTextColor(TFT_SKYBLUE, TFT_BLACK);
  tft.drawFloat(h, 0, 238, 108, 4);
  tft.setTextPadding(0);
  tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
  tft.drawString("C", 82, 138, 2);
  tft.drawString("%", 238, 138, 2);
  drawBar(20, 165, 124, 14, (t + 10) / 50.0, tempColor(t));   // −10…+40 °C scale
  drawBar(176, 165, 124, 14, h / 100.0, TFT_SKYBLUE);
  Serial.printf("%.1f C  %.0f %%\n", t, h);
  delay(2000);
}

Common problems

  • A white screen. The pins in User_Setup.h don't match the wiring, or the wrong driver is selected (ST7789 instead of ILI9341).
  • A mirrored or shifted picture. Try another setRotation(). For "mirrored" clones, User_Setup has TFT_INVERSION_ON/OFF and TFT_RGB_ORDER.
  • Nothing visible although the code runs. The LED (backlight) pin is not connected.
  • Cyrillic. The built-in fonts are Latin only. Ukrainian needs generated smooth fonts (loadFont()) containing the right characters.