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
| Component | Quantity | Specification and notes |
|---|---|---|
| Development board with a classic ESP32-WROOM-32 module | 1 | Match the reference GPIO labels; check physical header positions on the actual board. |
| USB data and power cable | 1 | Use the connector fitted to your board; the pictured kit lists Micro-USB. |
| Solderless breadboard | 1 | The kit lists 830 tie points. Check whether the power rails are split. |
| 0.96-inch OLED — reference I2C SSD1306 128×64 | 1 | VCC/GND/SDA/SCL, 3.3 V supply, onboard pull-ups to 3V3. The image does not confirm the controller, interface or resolution. |
| 10 kΩ potentiometer | 1 | Two end terminals and a wiper W; identify their physical positions on the actual component. |
| Jumper wires | 8 | Use 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.
| From | To | Connection |
|---|---|---|
ESP32.3V3 | OLED1.VCC | 128×64 I2C SSD1306 module with 3.3 V supply and pull-ups. |
ESP32.GND | OLED1.GND | Common ground. |
ESP32.GPIO21 | OLED1.SDA | I2C data; identify the SDA terminal. |
ESP32.GPIO22 | OLED1.SCL | I2C clock; identify the SCL terminal. |
ESP32.3V3 | RV1.END_A | One end terminal of the 10 kΩ potentiometer. |
ESP32.GND | RV1.END_B | The other potentiometer end. |
RV1.W | ESP32.GPIO34 | Wiper to the ADC1 analogue input. |
- Disconnect USB and identify VCC, GND, SDA, and SCL on the OLED.
- Connect VCC to 3V3, GND to GND, SDA to GPIO21, and SCL to GPIO22.
- Connect the ends of the 10 kΩ potentiometer to 3V3 and GND, and its wiper to GPIO34.
- Check that the wiper is correctly identified and that the breadboard power rails are continuous where needed.
- 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 and release | Version | Dependencies |
|---|---|---|
| Adafruit SSD1306 | 2.5.17 | Adafruit GFX Library 1.12.6 |
| Adafruit GFX Library | 1.12.6 | Adafruit BusIO 1.17.4 |
| Adafruit BusIO | 1.17.4 | None |
Wire is included in the ESP32 core; do not install it separately through Library Manager.
Complete program
#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 reading | Expected display |
|---|---|
| Near minimum | Small number and short fill |
| Reading around 2048 | Approximately half the bar |
| Reading of 4095 | Maximum fill |
| Potentiometer held still | Mostly 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
| Problem | Check and correction |
|---|---|
| Scanner finds no device | Check power, ground, SDA/SCL, and module labels. |
Scanner sees 0x3D, but updates stop | Set OLED_ADDRESS to 0x3D. |
| ACK is present, but the image is blank or shifted | Verify controller, resolution, and matching driver. |
| Reading stays at one extreme | Check the wiper, GPIO34, and both potentiometer end connections. |
| Changed text never appears | Check for display.display() after drawing. |
| Compiler cannot find a header | Install 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
- Does an ACK at
0x3Cprove that the controller is an SSD1306? - Which call transfers the drawn image to the screen?
- 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.
