Community project

ESP32 Display Design Lab

ESP32
Photo of ESP32 Display Design Lab
Generated with AI

Muhammad Shoaib Ali

Published November 1, 2026

The ESP32 Display Design Lab teaches how to build an interactive display system using an ESP32 microcontroller paired with a 128x64 SSD1306 OLED screen and two push buttons. The project demonstrates responsive button handling with debouncing, tap versus hold detection, and dynamic screen updates—practical skills for any embedded display application.

This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions from board placement through final power-on inspection. The included firmware handles button events with configurable timing thresholds and manages display state transitions, giving makers a solid foundation for building their own interactive ESP32 projects.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

SSD1306 OLED

0.96-inch 128 x 64 white monochrome OLED display module driven by an SSD1306 controller over I2C (address 0x3C). It runs from 3 V to 5 V and connects through a 4-pin 2.54 mm header (GND, VCC, SCL, SDA); the self-emitting pixels need no backlight.

1

Push Button

Next expression button

Momentary push button switch

1

Push Button

Sleep/wake button

Momentary push button switch

Assemble it in 5 steps

1. Place the board and screen

Put the ESP32 DevKit V1 and the 0.96-inch OLED on the breadboard with their metal pins in separate rows. Keep the OLED label facing you so you can read the names printed beside its four pins.

  • Do not assume the order of the OLED pins: read the tiny labels on your own module before connecting wires.
  • Do not connect the OLED power wires until you have identified its VCC and GND labels; swapped power can damage the screen.

2. Wire the OLED screen

Use four jumper wires: OLED VCC → ESP32 3V3 (power), OLED GND → ESP32 GND (ground), OLED SDA → GPIO21 (data), and OLED SCL → GPIO22 (clock).

  • Use red for VCC, black for GND, and two other colors for SDA and SCL so a loose wire is easy to find later.
  • Use the 3V3 pin, not the 5V/VIN pin, for this screen in this project.

3. Add the next-expression button

Place the first push button so its two connected legs are on opposite sides of the breadboard's center gap. Connect one side to GPIO25 (signal) and the opposite side to GND (ground). This button changes the face on the screen.

  • A small four-leg tactile button has two legs already connected on each side; placing it across the center gap prevents the button from being shorted all the time.

4. Add the sleep button

Place the second push button across the breadboard center gap. Connect one side to GPIO26 (signal) and the opposite side to GND (ground). This button turns the face into a sleeping screen and wakes it again.

  • The buttons do not need separate resistors because the firmware turns on the ESP32's internal pull-up resistors.

5. Inspect before power

With the USB cable unplugged, compare every wire to the list: VCC → 3V3 (power), all GND wires → GND (ground), SDA → GPIO21 (data), SCL → GPIO22 (clock), first button → GPIO25 (signal), and second button → GPIO26 (signal). Then plug the ESP32 into USB.

  • If the screen stays blank later, first check whether your OLED's printed pin order differs from the common GND, VCC, SCL, SDA order.
  • Never move jumper wires while the board is powered; an accidental short can damage the board or the OLED.

Review all connections

1. Connections between "oled_1" and "ESP32"

Functionoled_1ESP32
powerVCC → OLED power3V3
groundGND → OLED groundGND
i2cSDA → OLED dataGPIO 21
i2cSCL → OLED clockGPIO 22

2. Connections between "button_next" and "ESP32"

Functionbutton_nextESP32
groundGND → Button groundGND
digitalSIGNAL → Select next expressionGPIO 25

3. Connections between "button_sleep" and "ESP32"

Functionbutton_sleepESP32
groundGND → Button groundGND
digitalSIGNAL → Toggle sleep/wakeGPIO 26

Deploy the firmware

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

constexpr int OLED_SDA_PIN = 21;
constexpr int OLED_SCL_PIN = 22;
constexpr int NEXT_BUTTON_PIN = 25;
constexpr int SLEEP_BUTTON_PIN = 26;
constexpr uint8_t SCREEN_WIDTH = 128;
constexpr uint8_t SCREEN_HEIGHT = 64;
constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr unsigned long DEBOUNCE_MS = 35;
constexpr unsigned long HOLD_MS = 700;
constexpr unsigned long AUTO_FACE_MS = 2500;

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

enum ButtonEvent { NO_EVENT, TAP_EVENT, HOLD_EVENT };

struct Button {
  uint8_t pin;
  bool rawState = HIGH;
  bool stableState = HIGH;
  bool holdReported = false;
  unsigned long changedAt = 0;
  unsigned long pressedAt = 0;
};

Button nextButton{NEXT_BUTTON_PIN};
Button sleepButton{SLEEP_BUTTON_PIN};
uint8_t faceIndex = 0;
bool isAwake = true;
bool autoFaces = false;
unsigned long lastAutoFaceAt = 0;

ButtonEvent readButton(Button &button) {
  const unsigned long now = millis();
  const bool reading = digitalRead(button.pin);
  if (reading != button.rawState) {
    button.rawState = reading;
    button.changedAt = now;
  }
  if ((now - button.changedAt) >= DEBOUNCE_MS && button.stableState != button.rawState) {
    button.stableState = button.rawState;
    if (button.stableState == LOW) {
      button.pressedAt = now;
      button.holdReported = false;
    } else if (!button.holdReported) {
      return TAP_EVENT;
    }
  }
  if (button.stableState == LOW && !button.holdReported && now - button.pressedAt >= HOLD_MS) {
    button.holdReported = true;
    return HOLD_EVENT;
  }
  return NO_EVENT;
}

void drawFooter() {
  display.setTextSize(1);
  display.setCursor(1, 55);
  if (autoFaces) {
    display.print("AUTO  HOLD SLEEP");
  } else {
    display.print("NEXT  HOLD=BACK");
  }
}

void drawFace() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  if (faceIndex == 0) {
    display.setTextSize(1);
    display.setCursor(39, 2);
    display.print("HAPPY");
    display.fillCircle(42, 26, 7, SSD1306_WHITE);
    display.fillCircle(86, 26, 7, SSD1306_WHITE);
    display.drawCircle(42, 26, 12, SSD1306_WHITE);
    display.drawCircle(86, 26, 12, SSD1306_WHITE);
    display.drawCircle(64, 38, 20, SSD1306_WHITE);
    display.fillRect(40, 28, 48, 20, SSD1306_BLACK);
    display.drawLine(48, 46, 56, 51, SSD1306_WHITE);
    display.drawLine(56, 51, 72, 51, SSD1306_WHITE);
    display.drawLine(72, 51, 80, 46, SSD1306_WHITE);
  } else if (faceIndex == 1) {
    display.setTextSize(1);
    display.setCursor(31, 2);
    display.print("SURPRISED");
    display.drawCircle(42, 27, 12, SSD1306_WHITE);
    display.drawCircle(86, 27, 12, SSD1306_WHITE);
    display.fillCircle(42, 27, 5, SSD1306_WHITE);
    display.fillCircle(86, 27, 5, SSD1306_WHITE);
    display.drawCircle(64, 48, 10, SSD1306_WHITE);
  } else if (faceIndex == 2) {
    display.setTextSize(1);
    display.setCursor(43, 2);
    display.print("SLEEPY");
    display.drawLine(29, 28, 53, 28, SSD1306_WHITE);
    display.drawLine(75, 28, 99, 28, SSD1306_WHITE);
    display.drawLine(46, 48, 82, 48, SSD1306_WHITE);
    display.setCursor(98, 38);
    display.print("z");
    display.setCursor(107, 30);
    display.print("z");
  } else {
    display.setTextSize(1);
    display.setCursor(43, 2);
    display.print("CURIOUS");
    display.fillCircle(42, 27, 6, SSD1306_WHITE);
    display.fillCircle(86, 27, 9, SSD1306_WHITE);
    display.drawCircle(42, 27, 11, SSD1306_WHITE);
    display.drawCircle(86, 27, 14, SSD1306_WHITE);
    display.drawLine(48, 48, 80, 48, SSD1306_WHITE);
  }
  drawFooter();
  display.display();
}

void drawSleepingScreen() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(2);
  display.setCursor(39, 20);
  display.print("Zz");
  display.setTextSize(1);
  display.setCursor(12, 50);
  display.print("Tap SLEEP to wake");
  display.display();
}

void setup() {
  pinMode(NEXT_BUTTON_PIN, INPUT_PULLUP);
  pinMode(SLEEP_BUTTON_PIN, INPUT_PULLUP);
  Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN);
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
    while (true) delay(1000);
  }
  drawFace();
}

void loop() {
  const ButtonEvent sleepEvent = readButton(sleepButton);
  const ButtonEvent nextEvent = readButton(nextButton);

  if (sleepEvent == TAP_EVENT) {
    isAwake = !isAwake;
    if (isAwake) drawFace();
    else drawSleepingScreen();
  }

  if (sleepEvent == HOLD_EVENT && isAwake) {
    autoFaces = !autoFaces;
    lastAutoFaceAt = millis();
    drawFace();
  }

  if (isAwake && nextEvent == TAP_EVENT) {
    autoFaces = false;
    faceIndex = (faceIndex + 1) % 4;
    drawFace();
  }

  if (isAwake && nextEvent == HOLD_EVENT) {
    autoFaces = false;
    faceIndex = (faceIndex + 3) % 4;
    drawFace();
  }

  if (isAwake && autoFaces && millis() - lastAutoFaceAt >= AUTO_FACE_MS) {
    faceIndex = (faceIndex + 1) % 4;
    lastAutoFaceAt = millis();
    drawFace();
  }
  delay(5);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik