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.
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_DATUMis the centre,TC_DATUMthe middle of the top edge,ML_DATUMthe 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 insetup(), and inloop()redraw only the parts that change. - Colours are 16-bit RGB565: use the ready-made
TFT_RED,TFT_SKYBLUEand so on, ortft.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.hdon'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 hasTFT_INVERSION_ON/OFFandTFT_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.