OLED display: show data without a computer

Connect an I2C OLED, check its address, and display a potentiometer reading as a number and a graphical bar.

Sensors and measurement displaysBeginner90 min

What you will learn

  • Check that the OLED module matches the reference SSD1306 profile and 3.3 V supply.
  • Connect SDA and SCL and interpret an I2C address scan.
  • Distinguish a device acknowledgement from controller identification.
  • Display an ADC reading as text and a bar using a graphics buffer.
  • Change the refresh schedule and add a relative percentage.

Before you start

Courses 01 and 05: Arduino setup, Serial Monitor, analog readings, and potentiometer wiring.

Reference setup

Board in Arduino IDE
ESP32 Dev Module
Arduino-ESP32
3.3.12
Serial Monitor
115200 baud

This is a reference profile, not an identification of the pictured board. Adapt the GPIO mapping and verify the circuit before using ESP32-C3/S2/S3 or a differently labelled board.

What we will build

We will build a small display that shows a potentiometer reading as a number and a horizontal bar. The value will remain visible without an open Serial Monitor. This prepares the display for the next course, where we add air temperature and humidity.

What you will learn

  • Connect a suitable OLED through I2C.
  • Find a device address and distinguish it from its controller type.
  • Draw text and a bar with a graphics library.
  • Convert an analog reading into the length of a graphic.
  • Schedule display refreshes separately from serial reports.

Prerequisites

Complete courses 01 and 05: installing the ESP32 package, using Serial Monitor, and connecting a potentiometer. We use the classic ESP32/ESP32-WROOM-32. GPIO21, GPIO22, and GPIO34 identify electrical functions, not numbered physical positions. C3, S2, and S3 boards require an adapted board profile.

Equipment and display identification

ComponentQuantitySpecification and notes
Development board with a classic ESP32-WROOM-32 module1Match the reference GPIO labels; check physical header positions on the actual board.
USB data and power cable1Use the connector fitted to your board; the pictured kit lists Micro-USB.
Solderless breadboard1The kit lists 830 tie points. Check whether the power rails are split.
0.96-inch OLED — reference I2C SSD1306 128×641VCC/GND/SDA/SCL, 3.3 V supply, onboard pull-ups to 3V3. The image does not confirm the controller, interface or resolution.
10 kΩ potentiometer1Two end terminals and a wiper W; identify their physical positions on the actual component.
Jumper wires8Use male-to-male or female-to-male leads to suit the board headers.

Approximate quantity, depending on layout and connector types.

The reference is a four-pin I2C OLED with an SSD1306 controller, 128 × 64 pixels, rated for 3.3 V. A “0.96-inch” label does not establish its controller, resolution, interface, or pin order. Check the actual module documentation. An SH1106 needs a suitable different driver; changing an I2C address does not turn it into an SSD1306. Adafruit: SSD1306, SH110x driver

How I2C and drawing work

I2C uses SDA for data and SCL for the clock, with a shared ground. A device responds at its address. A scanner tests addresses and reports ACK, an acknowledgement. This confirms a response, but does not identify the controller.

SDA and SCL need pull-up resistors. The reference OLED module includes them to its 3.3 V supply; verify this in its documentation. If absent, complete the bus according to the module specification. Do not use a module that pulls these lines to 5 V. Espressif: I2C

The SSD1306 library holds an image in a memory buffer. Text and rectangle commands change this buffer; display.display() sends the prepared image to the screen. Drawing alone does not update the visible image.

Wiring and assembly

Wiring diagram

The reference is a four-pin 128×64 I2C SSD1306 with pull-ups to 3.3 V. The photo does not confirm the controller or terminal order. SH1106 needs another driver. OLED_ADDRESS is 0x3C; use 0x3D only when the scan and documentation support it.

Wiring diagram — electrical connections listed in the table below
Wiring diagramEnlargeSVGPNG
Connections · Wiring diagram
FromToConnection
ESP32.3V3OLED1.VCC128×64 I2C SSD1306 module with 3.3 V supply and pull-ups.
ESP32.GNDOLED1.GNDCommon ground.
ESP32.GPIO21OLED1.SDAI2C data; identify the SDA terminal.
ESP32.GPIO22OLED1.SCLI2C clock; identify the SCL terminal.
ESP32.3V3RV1.END_AOne end terminal of the 10 kΩ potentiometer.
ESP32.GNDRV1.END_BThe other potentiometer end.
RV1.WESP32.GPIO34Wiper to the ADC1 analogue input.
  1. Disconnect USB and identify VCC, GND, SDA, and SCL on the OLED.
  2. Connect VCC to 3V3, GND to GND, SDA to GPIO21, and SCL to GPIO22.
  3. Connect the ends of the 10 kΩ potentiometer to 3V3 and GND, and its wiper to GPIO34.
  4. Check that the wiper is correctly identified and that the breadboard power rails are continuous where needed.
  5. Check the shared ground, separate rows, and 3.3 V supply, then connect USB.

If turning the potentiometer increases the reading in the unwanted direction, swap its two end connections with power disconnected. The wiper remains connected to GPIO34.

Arduino setup and libraries

Use Arduino IDE 2.x, esp32 by Espressif Systems 3.3.12, and ESP32 Dev Module. Install Adafruit SSD1306 2.5.17, Adafruit GFX Library 1.12.6, and Adafruit BusIO 1.17.4 through Library Manager. Accept the required dependencies and check their versions. Wire is included in the ESP32 package.

Extract the Arduino ZIP, open oled_display.ino in its matching folder, select the port, and run Verify, then Upload. Set Serial Monitor to 115200 baud. ESP32 installation

Library Manager — documented versions and dependencies
Library and releaseVersionDependencies
Adafruit SSD13062.5.17Adafruit GFX Library 1.12.6
Adafruit GFX Library1.12.6Adafruit BusIO 1.17.4
Adafruit BusIO1.17.4None

Wire is included in the ESP32 core; do not install it separately through Library Manager.

Complete program

oled_display.ino Arduino / C++
Download .ino

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

constexpr uint8_t SDA_PIN = 21;
constexpr uint8_t SCL_PIN = 22;
constexpr uint8_t POT_PIN = 34;
constexpr uint8_t OLED_ADDRESS = 0x3C;  // Use 0x3D only if the scan confirms it.
constexpr uint16_t OLED_WIDTH = 128;
constexpr uint16_t OLED_HEIGHT = 64;
constexpr uint32_t UPDATE_MS = 100;
constexpr uint32_t REPORT_MS = 500;
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, -1, 100000UL, 100000UL);
bool ready = false;
uint32_t lastUpdateAt = 0;
uint32_t lastReportAt = 0;
uint16_t rawAdc = 0;
uint16_t barPixels = 0;

void scanI2CBus() {
  Serial.println("I2C scan (an ACK alone does not identify the display controller):");
  uint8_t found = 0;
  for (uint8_t address = 1; address < 127; ++address) {
    Wire.beginTransmission(address);
    if (Wire.endTransmission() == 0) {
      Serial.print("ACK at 0x"); Serial.println(address, HEX);
      ++found;
    }
  }
  if (found == 0) Serial.println("No I2C device acknowledged.");
}

bool initializeDisplay() {
  if (!Wire.begin(SDA_PIN, SCL_PIN)) {
    Serial.println("OLED ERROR: I2C bus initialization failed.");
    return false;
  }
  Wire.setClock(100000);
  Wire.setTimeOut(50);
  scanI2CBus();
  Wire.beginTransmission(OLED_ADDRESS);
  if (Wire.endTransmission() != 0) {
    Serial.println("OLED ERROR: configured address did not ACK. Check wiring/address; reset to retry.");
    return false;
  }
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS, false, false)) {
    Serial.println("OLED ERROR: display buffer initialization failed; reset to retry.");
    return false;
  }
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setTextWrap(false);
  return true;
}

void drawReading() {
  display.clearDisplay();
  display.setCursor(0, 0); display.print("WB Maker Bridge");
  display.setCursor(0, 16); display.print("ADC: "); display.print(rawAdc);
  display.setCursor(0, 28); display.print("Turn potentiometer");
  display.drawRect(3, 42, 122, 14, SSD1306_WHITE);
  display.fillRect(4, 43, barPixels, 12, SSD1306_WHITE);
  display.display();
}

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);
  analogSetPinAttenuation(POT_PIN, ADC_11db);
  ready = initializeDisplay();
  if (!ready) return;
  lastUpdateAt = millis();
  lastReportAt = lastUpdateAt;
  drawReading();
  Serial.println("OLED ready for this SSD1306 profile. Potentiometer is not a calibrated voltmeter.");
}

void loop() {
  if (!ready) return;
  const uint32_t now = millis();
  if (static_cast<uint32_t>(now - lastUpdateAt) >= UPDATE_MS) {
    lastUpdateAt = now;
    rawAdc = analogRead(POT_PIN);
    barPixels = static_cast<uint32_t>(rawAdc) * 120U / 4095U;
    drawReading();
  }
  if (static_cast<uint32_t>(now - lastReportAt) >= REPORT_MS) {
    lastReportAt = now;
    Serial.print("ADC="); Serial.print(rawAdc);
    Serial.print(" bar_pixels="); Serial.println(barPixels);
  }
}

The program uses shared ASCII display labels. Course text is translated, while both language versions use the same program.

Address selection and startup

scanI2CBus() probes addresses 1 through 126. OLED_ADDRESS defaults to 0x3C. If the scanner reports the OLED at 0x3D, change the setting and upload again. Do not guess addresses or interpret a response as proof of an SSD1306 controller.

initializeDisplay() configures SDA/SCL, a 100 kHz clock, and a finite 50 ms timeout, then checks the selected address. Only then does it initialize the display library. The final false argument in display.begin(...) means Wire has already been started. If the probe or initialization fails, ready remains false and normal updates do not start. Read the reason in Serial Monitor. Library source

From potentiometer to bar

The ADC uses 12-bit resolution and ADC_11db. Its reading ranges from 0 to 4095; this is not automatically a voltage or an accurate position measurement. Saturation is possible near the ends of travel. Espressif: ADC

The program stores the reading in rawAdc and scales it into barPixels, a filled length of up to 120 pixels. UPDATE_MS is 100 ms, while REPORT_MS is 500 ms. Smooth updates therefore do not require equally frequent serial reports. Integer scaling discards fractional pixels.

Running and recording observations

First watch the scanner output. Then turn the potentiometer slowly and compare the number, bar, and serial report. The title reads WB Maker Bridge. The initial zero is a placeholder until the first reading after 100 ms.

Position or specified readingExpected display
Near minimumSmall number and short fill
Reading around 2048Approximately half the bar
Reading of 4095Maximum fill
Potentiometer held stillMostly steady number, with possible small ADC fluctuations

These are expectations to check, not previously measured results for your module. Record the actual address and at least three readings.

Troubleshooting

ProblemCheck and correction
Scanner finds no deviceCheck power, ground, SDA/SCL, and module labels.
Scanner sees 0x3D, but updates stopSet OLED_ADDRESS to 0x3D.
ACK is present, but the image is blank or shiftedVerify controller, resolution, and matching driver.
Reading stays at one extremeCheck the wiper, GPIO34, and both potentiometer end connections.
Changed text never appearsCheck for display.display() after drawing.
Compiler cannot find a headerInstall the listed libraries and check the selected board.

Independent challenge

Add a percentage above the bar. Derive it from the same ADC reading, without reading the potentiometer again. Then change UPDATE_MS to 500 ms and describe the response.

Knowledge check

  1. Does an ACK at 0x3C prove that the controller is an SSD1306?
  2. Which call transfers the drawn image to the screen?
  3. Does 50% mean a reliably measured voltage of 1.65 V?

Worked solution and explanations

Calculate the percentage with rawAdc * 100UL / 4095, using the existing reading. In drawReading(), replace the Turn potentiometer line with the percentage and % before sending the buffer. Readings of 0, 2048, and 4095 produce 0%, 50%, and 100%. These are relative values without voltage calibration.

An ACK only confirms a device response. display.display() transfers the image. A 500 ms interval gives approximately two refreshes per second and visibly slower updates than a 100 ms interval.

Downloads

Keep the diagram, program, and recorded address for course 12. Record compilation and a physical board trial as separate checks.

Primary sources