Driving TFT and OLED Displays with Arduino and ESP32: SSD1306, ST7789, ILI9341, and Choosing a Graphics Library
Almost every ESP32 or Arduino project eventually needs to show something more informative than a blinking LED, and the display market for makers has consolidated around a small handful of controller chips: the SSD1306 for small monochrome OLEDs, the ST7789 and ILI9341 for color TFTs, and a long tail of similar parts that all speak a similar language over SPI or I2C. The hardware is cheap and well documented, but the software side trips up a lot of people the first time — there are at least four popular graphics libraries, they don't all support the same displays, and picking the wrong one for your project means either fighting slow refresh rates or drowning in flash usage on a small microcontroller. This guide walks through the common display types, how to wire them, and which library actually fits your project.
The Display Landscape
Nearly all hobbyist displays fall into one of these families:
- SSD1306 / SH1106 OLED — tiny (typically 0.96″ to 1.3″) monochrome displays at 128x64 or 128x32, almost always driven over I2C, occasionally SPI. Extremely low power and high contrast since unlit pixels draw no current, but no color and limited resolution.
- ST7789 TFT — the most common small color IPS panel right now (240x240 and 240x320 are typical), SPI only, used in everything from smartwatch modules to the displays bundled with ESP32 dev boards.
- ILI9341 TFT — an older but still very common 240x320 color SPI panel, often the one bundled with resistive touch overlays and the classic 2.8″ "TFT shield" boards.
- ST7735 — a smaller, cheaper cousin of the ST7789 in similar resolutions (128x160, 128x128), common on cheap breakout boards.
- GC9A01 — the round 240x240 SPI displays used for gauge-style UIs and smartwatch faces.
The practical difference that matters for wiring: I2C displays (SSD1306, most character LCDs) need only SDA and SCL plus power, so they're trivial to add to a project that already has other I2C sensors on the bus. SPI displays (everything color) need MOSI, SCK, CS, DC (data/command), and usually a RST line, which eats more GPIO pins but refreshes dramatically faster — SPI clock speeds of 40–80MHz are common, versus I2C's practical ceiling around 400kHz–1MHz, which matters a lot once you're pushing full-color frames.
Wiring Reference
DisplayInterfaceTypical PinsNotes SSD1306 OLEDI2CVCC, GND, SDA, SCLAddress usually 0x3C or 0x3D; check with an I2C scanner sketch if unsure SSD1306 OLEDSPI variantVCC, GND, SCK, MOSI, CS, DC, RSTFaster refresh than I2C, rare on hobby boards ST7789 TFTSPIVCC, GND, SCK, MOSI, CS, DC, RST, BLK (backlight)No MISO needed unless reading back framebuffer; tie BLK to a PWM pin to dim ILI9341 TFTSPIVCC, GND, SCK, MOSI, MISO, CS, DC, RST, LEDTouch controller (XPT2046) usually shares the SPI bus with its own CS/IRQ pins GC9A01 round TFTSPISame as ST7789Software init sequence differs slightly from ST7789; use a GC9A01-specific driverOn the ESP32, you have multiple hardware SPI peripherals (HSPI/VSPI on the original ESP32, more flexible GPIO matrix routing on the S2/S3/C3), so you can put a display on its own bus separate from an SD card or other SPI peripheral if you need the throughput. On classic AVR Arduinos (Uno, Nano, Mega), SPI pins are fixed, so plan your wiring around them rather than the other way around.
Choosing a Graphics Library
This is where most confusion happens, because the four most common libraries overlap in what they support but aren't interchangeable:
- Adafruit_GFX + Adafruit_SSD1306 / Adafruit_ST7789 / Adafruit_ILI9341 — the most beginner-friendly option. One shared drawing API (Adafruit_GFX) with a thin hardware driver underneath for each controller. Well documented, huge amount of example code, but it's not optimized for speed — full-screen redraws on a 240x320 ILI9341 can visibly tear or lag if you're doing animation.
- U8g2 — the standard choice for small monochrome displays (SSD1306, SH1106, and dozens of other single-color controllers). Extremely memory efficient because it supports page-buffered rendering instead of holding a full framebuffer in RAM, which matters on an 8-bit AVR with 2KB of SRAM. Excellent font support, less commonly used for color TFTs.
- TFT_eSPI — the library most ESP32 projects with a color screen actually want. It's written specifically for ESP32/STM32 with hand-tuned SPI transfers and DMA support, and it's dramatically faster than Adafruit's drivers for the same hardware — often 5-10x on full-screen fills. The catch is configuration: instead of passing pins in your sketch, you edit a `User_Setup.h` file in the library folder to declare your display type and pin mapping, which trips up people used to constructor-argument configuration.
- LVGL — not a display driver itself but a full graphics/UI toolkit (buttons, sliders, animations, styling) that sits on top of a driver like TFT_eSPI. Worth reaching for once your project needs an actual touchscreen interface with multiple screens and widgets rather than just drawing text and shapes; overkill for a simple status display.
A reasonable rule of thumb: SSD1306/monochrome → U8g2. Color TFT with simple text/graphics → TFT_eSPI on ESP32, Adafruit_GFX stack on AVR where TFT_eSPI's ESP-focused DMA path doesn't apply. Full touchscreen UI with multiple views → LVGL on top of TFT_eSPI.
A Minimal ST7789 Example (TFT_eSPI)
After configuring User_Setup.h for your specific board and pin mapping, a basic sketch looks like this:
#include <TFT_eSPI.h> TFT_eSPI tft = TFT_eSPI(); void setup() { tft.init(); tft.setRotation(1); tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.setTextSize(2); tft.setCursor(10, 10); tft.println("Hello, ESP32!"); } void loop() {}The equivalent U8g2 sketch for an SSD1306 over I2C is similarly compact, but note U8g2's two rendering modes: full-buffer mode holds the whole frame in RAM and is simplest to work with, while page-buffer mode redraws in horizontal strips to save RAM at the cost of needing to wrap your drawing code in a `u8g2.firstPage() / nextPage()` loop. On an ESP32 with plenty of RAM, full-buffer mode is fine; on an ATmega328-based Arduino, page mode is often mandatory.
Common Problems
SymptomLikely Cause Screen stays white or shows garbage on first bootWrong init sequence for your specific panel variant — many ST7789 modules from different vendors need slightly different offsets/inversion settings Colors look swapped (blue looks red)RGB vs BGR panel order; most libraries have a color-order flag or `invertDisplay()` call to fix this Image is mirrored or rotated wrongSet the rotation constant appropriately; also check the display isn't wired backwards relative to its ribbon orientation SSD1306 not detected on I2C scanWrong address (0x3C vs 0x3D) or missing pull-up resistors on SDA/SCL if using a bare breakout without them built in TFT_eSPI compiles but nothing drawsUser_Setup.h pin definitions don't match your actual wiring — this is the single most common TFT_eSPI support request Flickering or tearing during animationUsing Adafruit_GFX direct-to-display draws instead of an off-screen sprite/buffer; switch to double-buffered drawing where the library supports itPower and Backlight Notes
Color TFTs draw considerably more current than OLEDs, especially with the backlight at full brightness — budget 80–120mA for a typical 2.4″–2.8″ panel versus 10–20mA for a small OLED showing a mostly-dark UI. If you're running from battery, wire the backlight (BLK/LED pin) to a PWM-capable GPIO instead of straight to 3.3V so you can dim it or shut it off entirely during sleep, which is often the single biggest power draw in a battery-powered display project.
Between the three-letter alphabet soup of controller chips and four competing libraries, the display side of a project can eat more debugging time than the rest of the firmware combined. Match the library to the job — U8g2 for small monochrome status screens, TFT_eSPI for anything color on an ESP32, and LVGL once you're building a real interface — and most of that friction disappears.
Related Guides
- Brushless DC Motors and ESCs for Makers: KV Rating, Sensored vs Sensorless, and Driving with Arduino/ESP32
- Drag Engraving Metal on a CNC Router: Diamond Drag Bits, Depth Control, and Speeds
- Food-Safe Laser Engraving: Cutting Boards, Coasters, and Kitchenware Materials and Safety
- Writing NDEF Records and vCard NFC Tags with the Flipper Zero: Contact Cards, WiFi Configs, and URL Tags
- Pocket-Hole and Biscuit Joinery for the Maker Shop: Kreg Jigs, Biscuit Joiners, and When to Use Each
- Belt and Disc Sander Setup and Technique for the Maker Shop: Sanding Sequences, Grits, and Shop-Made Jigs