The SSD1306 OLED display is one of the most popular output devices in the maker world, and for good reason. At under 100 rupees for a 0.96-inch module, it gives you a crisp 128x64 pixel screen that works over just two wires. Whether you are building a weather station, a handheld game, or a sensor dashboard, this display is often the perfect fit.
This tutorial takes you from zero to confident. We will wire the display, install the libraries, and work through eight progressive code examples — text, shapes, fonts, sensor readouts, menus, animations, bitmaps, and scrolling. By the end, you will have a toolkit of patterns you can drop into any project.
Why OLED Displays?
Traditional LCD screens like the 16x2 character display or the Nokia 5110 rely on a backlight to illuminate pixels from behind. OLED (Organic Light-Emitting Diode) technology is fundamentally different. Each pixel emits its own light.
This gives you several practical advantages:
- True black and infinite contrast. Pixels that are off produce zero light. Text and graphics look sharp and vivid, even in dim environments.
- No backlight needed. The display module is thinner, lighter, and simpler in construction.
- Low power consumption. A typical SSD1306 module draws around 20mA when displaying mixed content. Because only lit pixels consume power, a mostly-black screen draws very little current — ideal for battery-powered projects.
- Wide viewing angle. OLED panels are readable from nearly any angle without color shifting or dimming.
- Fast response time. Pixel switching is nearly instantaneous, making smooth animations possible.
For most hobbyist projects that need a small, readable display without the complexity of color TFTs, an SSD1306 OLED is the sweet spot between capability and simplicity.
SSD1306 Overview
The SSD1306 is a driver IC that controls a monochrome OLED panel. The most common module you will encounter is:
| Specification | Value |
|---|---|
| Resolution | 128 x 64 pixels |
| Diagonal size | 0.96 inches |
| Interface | I2C (default) or SPI |
| Operating voltage | 3.3V to 5V (onboard regulator) |
| I2C address | 0x3C (sometimes 0x3D) |
| Driver IC | SSD1306 |
| Color | Monochrome (white, blue, or yellow-blue) |
The display communicates over I2C using just two data lines (SDA and SCL), plus power and ground. That means four wires total. Some modules also expose SPI pads for faster communication, but I2C is the standard for most breakout boards.
Display Variants
You will find several variants in the market:
| Size | Driver IC | Resolution | Notes |
|---|---|---|---|
| 0.96" | SSD1306 | 128x64 | Most common, cheapest |
| 0.91" | SSD1306 | 128x32 | Shorter, half the vertical resolution |
| 1.3" | SH1106 | 128x64 | Larger but uses a different driver — requires a different library or configuration |
| 0.96" blue | SSD1306 | 128x64 | Blue-tinted pixels instead of white |
| 0.96" yellow-blue | SSD1306 | 128x64 | Top 16 rows are yellow, bottom 48 are blue — fixed, not software-controllable |
The yellow-blue variant is popular for dashboards because the yellow strip naturally highlights a title or status bar.
Important: The 1.3-inch displays typically use the SH1106 driver, not the SSD1306. The SH1106 has a 132x64 internal buffer (versus 128x64 on the SSD1306), so code written for the SSD1306 will show a 2-pixel offset on an SH1106. We will discuss the differences later.
Wiring
Arduino Uno / Nano
The Arduino Uno and Nano use dedicated I2C pins:
| OLED Pin | Arduino Pin |
|---|---|
| VCC | 5V (or 3.3V) |
| GND | GND |
| SDA | A4 |
| SCL | A5 |
These pins are fixed on the Uno and Nano. You cannot change them in software.
ESP32
The ESP32 has configurable I2C pins, but the default mapping is:
| OLED Pin | ESP32 Pin |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SDA | GPIO 21 |
| SCL | GPIO 22 |
On the ESP32, you can reassign I2C to almost any GPIO pair using Wire.begin(SDA_PIN, SCL_PIN). This is useful when GPIO 21/22 are occupied by other peripherals.
// Example: using GPIO 16 and GPIO 17 for I2C on ESP32
Wire.begin(16, 17);
Installing the Libraries
We will use two libraries from Adafruit throughout this tutorial:
- Adafruit SSD1306 — the display driver
- Adafruit GFX — the graphics library that provides drawing primitives (text, shapes, bitmaps)
In Arduino IDE:
- Go to Sketch > Include Library > Manage Libraries
- Search for
Adafruit SSD1306and click Install - When prompted to install dependencies (Adafruit GFX Library and Adafruit BusIO), click Install All
In PlatformIO, add to your platformio.ini:
lib_deps =
adafruit/Adafruit SSD1306@^2.5.7
adafruit/Adafruit GFX Library@^1.11.5
Understanding the Display Buffer
Before diving into code, you need to understand one core concept. The SSD1306 library uses a frame buffer — a region of RAM on your microcontroller that holds the entire screen contents (128 x 64 / 8 = 1024 bytes).
All drawing functions (drawPixel, print, drawRect, etc.) write to this buffer in memory. Nothing appears on the screen until you call display.display(). This two-step process prevents partial draws and flickering.
The typical drawing loop is:
display.clearDisplay(); // Clear the buffer
// ... draw stuff to the buffer ...
display.display(); // Push buffer to the screen
On an Arduino Uno, the 1KB frame buffer is a significant chunk of the 2KB SRAM. If your sketch is memory-constrained, consider the 128x32 variant (512 bytes buffer) or switch to an ESP32.
Example 1: Hello World
The simplest starting point — initialize the display and print text.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // -1 if sharing Arduino reset pin
#define SCREEN_ADDRESS 0x3C // Common address; use 0x3D if this doesn't work
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void setup() {
Serial.begin(115200);
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Halt
}
display.clearDisplay();
display.setTextSize(1); // 1x scale = 6x8 pixels per character
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("Hello, World!"));
display.println(F("Wavtron.in"));
display.display();
}
void loop() {
// Nothing here
}
Key points:
SSD1306_SWITCHCAPVCCtells the library the display generates its high voltage internally (standard for most modules).F()macro stores strings in flash memory instead of SRAM — critical on Arduino Uno.- Text size 1 gives you roughly 21 characters per line and 8 lines of text.
Example 2: Drawing Shapes
The Adafruit GFX library provides a full set of geometric primitives.
void setup() {
// ... initialization as above ...
display.clearDisplay();
// Horizontal line
display.drawLine(0, 0, 127, 0, SSD1306_WHITE);
// Rectangle (outline)
display.drawRect(10, 10, 50, 30, SSD1306_WHITE);
// Filled rectangle
display.fillRect(70, 10, 50, 30, SSD1306_WHITE);
// Circle (outline)
display.drawCircle(35, 50, 10, SSD1306_WHITE);
// Filled circle
display.fillCircle(95, 50, 10, SSD1306_WHITE);
// Rounded rectangle
display.drawRoundRect(5, 5, 118, 54, 8, SSD1306_WHITE);
// Triangle
display.drawTriangle(60, 42, 50, 58, 70, 58, SSD1306_WHITE);
display.display();
}
Available drawing functions:
| Function | Description |
|---|---|
drawPixel(x, y, color) |
Single pixel |
drawLine(x0, y0, x1, y1, color) |
Line between two points |
drawRect(x, y, w, h, color) |
Rectangle outline |
fillRect(x, y, w, h, color) |
Filled rectangle |
drawCircle(x, y, r, color) |
Circle outline |
fillCircle(x, y, r, color) |
Filled circle |
drawRoundRect(x, y, w, h, r, color) |
Rounded rectangle outline |
fillRoundRect(x, y, w, h, r, color) |
Filled rounded rectangle |
drawTriangle(x0, y0, x1, y1, x2, y2, color) |
Triangle outline |
fillTriangle(x0, y0, x1, y1, x2, y2, color) |
Filled triangle |
Coordinates start at (0, 0) in the top-left corner. X increases to the right (0-127), Y increases downward (0-63).
Example 3: Custom Fonts
The default font is functional but blocky. The Adafruit GFX library includes a set of scalable FreeFont alternatives.
#include <Fonts/FreeSerif9pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeMonoBold9pt7b.h>
void setup() {
// ... initialization ...
display.clearDisplay();
// Default font
display.setFont(); // Reset to default
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Default font");
// FreeSerif 9pt
display.setFont(&FreeSerif9pt7b);
display.setCursor(0, 28);
display.println("Serif 9pt");
// FreeSansBold 12pt
display.setFont(&FreeSansBold12pt7b);
display.setCursor(0, 55);
display.println("Bold 12");
display.display();
}
Note: When using custom fonts, setCursor positions the baseline of the text, not the top-left. This is different from the default font. You will need to adjust Y values accordingly.
Custom fonts consume flash memory. Each font typically adds 2-5KB. On an Arduino Uno with 32KB flash, be selective. On an ESP32 with 4MB flash, use as many as you like.
Example 4: Displaying Sensor Data (DHT22)
A practical example — reading temperature and humidity from a DHT22 sensor and displaying the values.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
#define DHTPIN 4
#define DHTTYPE DHT22
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
DHT dht(DHTPIN, DHTTYPE);
void setup() {
Serial.begin(115200);
dht.begin();
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 failed"));
for (;;);
}
}
void loop() {
float temp = dht.readTemperature();
float hum = dht.readHumidity();
if (isnan(temp) || isnan(hum)) {
Serial.println(F("DHT read failed"));
return;
}
display.clearDisplay();
// Title bar
display.fillRect(0, 0, 128, 16, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setTextSize(1);
display.setCursor(20, 4);
display.println(F("WEATHER STATION"));
// Temperature
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 24);
display.print(F("Temp: "));
display.setTextSize(2);
display.print(temp, 1);
display.setTextSize(1);
display.println(F(" C"));
// Humidity
display.setCursor(0, 48);
display.print(F("Humidity: "));
display.setTextSize(2);
display.print(hum, 1);
display.setTextSize(1);
display.println(F(" %"));
display.display();
delay(2000);
}
This pattern — clear, draw, display, delay — is the foundation of every real-time dashboard. Adjust the delay to control how often the screen refreshes.
Example 5: Simple Menu System with Button Navigation
Menus are essential for any project with user interaction. This example uses two buttons (UP and SELECT) to navigate a three-item menu.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
#define BTN_UP 2
#define BTN_SELECT 3
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
const char* menuItems[] = {"Temperature", "Humidity", "Settings"};
const int menuLength = 3;
int selectedItem = 0;
void setup() {
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_SELECT, INPUT_PULLUP);
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
for (;;);
}
drawMenu();
}
void loop() {
if (digitalRead(BTN_UP) == LOW) {
selectedItem = (selectedItem + 1) % menuLength;
drawMenu();
delay(200); // Simple debounce
}
if (digitalRead(BTN_SELECT) == LOW) {
executeMenuItem(selectedItem);
delay(200);
}
}
void drawMenu() {
display.clearDisplay();
// Header
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(30, 0);
display.println(F("MAIN MENU"));
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
// Menu items
for (int i = 0; i < menuLength; i++) {
int y = 16 + (i * 16);
if (i == selectedItem) {
display.fillRect(0, y, 128, 14, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
} else {
display.setTextColor(SSD1306_WHITE);
}
display.setCursor(8, y + 3);
display.println(menuItems[i]);
}
display.display();
}
void executeMenuItem(int index) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 28);
display.print(F("Selected: "));
display.println(menuItems[index]);
display.display();
delay(1500);
drawMenu();
}
The technique here is highlighting the selected item by drawing a filled white rectangle behind it and switching the text color to black. This inverted appearance is a standard pattern in monochrome UI design.
Example 6: Progress Bar and Loading Animation
void showProgressBar(int progress) {
// progress: 0 to 100
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(30, 10);
display.println(F("LOADING..."));
// Progress bar outline
int barX = 10;
int barY = 30;
int barW = 108;
int barH = 14;
display.drawRect(barX, barY, barW, barH, SSD1306_WHITE);
// Progress bar fill
int fillW = map(progress, 0, 100, 0, barW - 4);
display.fillRect(barX + 2, barY + 2, fillW, barH - 4, SSD1306_WHITE);
// Percentage text
display.setCursor(52, 52);
display.print(progress);
display.println(F("%"));
display.display();
}
void setup() {
// ... initialization ...
for (int i = 0; i <= 100; i += 2) {
showProgressBar(i);
delay(50);
}
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(25, 24);
display.println(F("READY!"));
display.display();
}
For a smoother animated loading effect, you can add a spinning indicator alongside the progress bar by drawing and rotating a small line on each frame.
Example 7: Displaying Bitmap Images
You can display custom images by converting them to byte arrays. The process is:
- Prepare a monochrome image at the correct resolution (128x64 or smaller)
- Use an online converter like image2cpp (javl.github.io/image2cpp) to generate a C byte array
- Store the array in PROGMEM and draw it with
drawBitmap
// Example: 16x16 heart icon
const unsigned char heartBitmap[] PROGMEM = {
0x00, 0x00, 0x06, 0x60, 0x0F, 0xF0, 0x1F, 0xF8,
0x3F, 0xFC, 0x7F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0x7F, 0xFE, 0x3F, 0xFC, 0x1F, 0xF8,
0x0F, 0xF0, 0x07, 0xE0, 0x03, 0xC0, 0x01, 0x80
};
void setup() {
// ... initialization ...
display.clearDisplay();
display.drawBitmap(56, 24, heartBitmap, 16, 16, SSD1306_WHITE);
display.display();
}
Tips for bitmap images:
- Always store bitmaps in
PROGMEMto save SRAM. - A full-screen 128x64 bitmap uses 1024 bytes of flash.
- Use the
image2cpptool with these settings: Canvas size matching your image, Background color black, Invert if needed, Output as Arduino code.
Example 8: Scrolling Text
For messages longer than the screen width, the SSD1306 has built-in hardware scrolling commands.
void setup() {
// ... initialization ...
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 24);
display.println(F("Hello from Wavtron!"));
display.display();
// Start scrolling left
display.startscrollright(0x00, 0x0F); // Scroll all rows
}
void loop() {
// Scrolling runs in hardware — no code needed in loop
}
Available scroll functions:
| Function | Direction |
|---|---|
startscrollright(start, stop) |
Right |
startscrollleft(start, stop) |
Left |
startscrolldiagright(start, stop) |
Diagonal right |
startscrolldiagleft(start, stop) |
Diagonal left |
stopscroll() |
Stop all scrolling |
The start and stop parameters define the page range (0x00 to 0x07 for 64-pixel height, where each page is 8 pixels tall).
For software-based scrolling with more control (variable speed, pixel-level precision):
void scrollText(const char* text, int y, int delayMs) {
int textWidth = strlen(text) * 6; // 6 pixels per char at size 1
for (int x = SCREEN_WIDTH; x > -textWidth; x--) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(x, y);
display.print(text);
display.display();
delay(delayMs);
}
}
Power Consumption and Optimization
The SSD1306 module typically draws 20mA during normal operation, but actual consumption depends on how many pixels are lit:
| Screen state | Approximate current |
|---|---|
| All pixels off (display on, buffer cleared) | ~5mA |
| 50% pixels lit | ~15mA |
| All pixels lit | ~25mA |
| Display off (sleep mode) | <10uA |
Techniques for reducing power:
- Dim the display using
display.dim(true), which reduces brightness and current draw. - Sleep mode with
display.ssd1306_command(SSD1306_DISPLAYOFF)when the display is not needed. Wake withSSD1306_DISPLAYON. - Minimize lit pixels. White text on black background uses less power than inverted (black text on white background).
- Use the 128x32 variant if you do not need the full 64-pixel height — it uses roughly half the current for a full screen.
Common Issues and Troubleshooting
Nothing Displayed
This is the most common problem, and the cause is almost always the I2C address.
Most SSD1306 modules use address 0x3C, but some use 0x3D. Run the I2C scanner sketch to find your display:
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(115200);
Serial.println(F("Scanning I2C bus..."));
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) {
Serial.print(F("Device found at 0x"));
Serial.println(addr, HEX);
}
}
}
void loop() {}
Other causes:
- Wrong SDA/SCL wiring. Double-check your connections.
- Module not receiving power. Verify 3.3V or 5V is present at VCC.
- Defective display. Try a different module if you have one.
Garbled or Shifted Output
- Wrong driver. If you are using an SH1106 (1.3-inch) display with the SSD1306 library, the output will be offset by 2 pixels. Use the
Adafruit_SH110Xlibrary instead. - Wrong screen dimensions. Make sure
SCREEN_WIDTHandSCREEN_HEIGHTmatch your actual display (128x64 vs 128x32).
Flickering
- Excessive
clearDisplay()anddisplay()calls in a tight loop. Add a delay or only redraw when data changes. - Insufficient power supply. Some breadboard setups have voltage drops. Use shorter jumper wires or power the display directly.
Text Appears Too Small or Too Large
setTextSize(1)produces 6x8 pixel characters.setTextSize(2)doubles that to 12x16. Adjust to your needs.- For finer control, use custom fonts instead of scaling the default font.
SSD1306 vs SH1106 vs ST7789: Choosing the Right Display
| Feature | SSD1306 | SH1106 | ST7789 |
|---|---|---|---|
| Type | OLED monochrome | OLED monochrome | TFT color |
| Resolution | 128x64 | 128x64 | 240x240 or 240x320 |
| Size | 0.96" | 1.3" | 1.3" or 1.54" |
| Interface | I2C or SPI | I2C or SPI | SPI only |
| Colors | 1-bit (on/off) | 1-bit (on/off) | 65K (16-bit RGB) |
| Power draw | ~20mA | ~25mA | ~40-80mA |
| Backlight | None (self-emitting) | None (self-emitting) | Yes (always on) |
| Library | Adafruit SSD1306 | Adafruit SH110X | Adafruit ST7789 |
| Price (India) | 80-150 INR | 150-250 INR | 250-500 INR |
| Best for | Small dashboards, status screens, battery projects | Larger monochrome output | Color graphics, image display, rich UI |
When to use each:
- SSD1306: Best all-rounder for small text-based displays. Lowest cost and power. Use this if you need to show numbers, text, simple icons, or basic graphs.
- SH1106: When you need a physically larger screen but still want monochrome simplicity and low power.
- ST7789: When you need color — charts, images, camera previews, or any UI that benefits from color differentiation. Expect higher power draw and SPI-only communication.
Multiple Displays on the Same I2C Bus
You can run two SSD1306 displays on one I2C bus if they have different addresses (one at 0x3C, the other at 0x3D). Some modules have a solder jumper on the back to change the address.
Adafruit_SSD1306 display1(128, 64, &Wire, -1);
Adafruit_SSD1306 display2(128, 64, &Wire, -1);
void setup() {
display1.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display2.begin(SSD1306_SWITCHCAPVCC, 0x3D);
display1.clearDisplay();
display1.setTextSize(2);
display1.setTextColor(SSD1306_WHITE);
display1.setCursor(0, 24);
display1.println(F("Screen 1"));
display1.display();
display2.clearDisplay();
display2.setTextSize(2);
display2.setTextColor(SSD1306_WHITE);
display2.setCursor(0, 24);
display2.println(F("Screen 2"));
display2.display();
}
Limitation: The SSD1306 only supports two possible I2C addresses, so two displays is the maximum on a single bus. If you need more, use an I2C multiplexer like the TCA9548A, which gives you up to eight independent I2C channels.
On the ESP32, you can also use the second I2C bus (Wire1) to add two more displays:
// ESP32: second I2C bus on GPIO 25 (SDA) and GPIO 26 (SCL)
TwoWire Wire1_custom = TwoWire(1);
Wire1_custom.begin(25, 26);
Adafruit_SSD1306 display3(128, 64, &Wire1_custom, -1);
display3.begin(SSD1306_SWITCHCAPVCC, 0x3C);
Quick Reference: Essential Functions
| Function | What it does |
|---|---|
begin(SSD1306_SWITCHCAPVCC, addr) |
Initialize display |
clearDisplay() |
Clear the frame buffer |
display() |
Push buffer to screen |
setTextSize(n) |
Set text scale (1, 2, 3...) |
setTextColor(color) |
WHITE or BLACK |
setCursor(x, y) |
Position for next text |
print() / println() |
Write text to buffer |
setFont(&font) |
Switch to custom font |
drawBitmap(x, y, bmp, w, h, color) |
Draw a bitmap image |
dim(true/false) |
Reduce brightness |
invertDisplay(true/false) |
Invert all pixels |
setRotation(0-3) |
Rotate display 0/90/180/270 degrees |
startscrollleft(start, stop) |
Hardware scroll left |
stopscroll() |
Stop hardware scroll |
Wrapping Up
The SSD1306 OLED display hits a rare balance of low cost, low power, high readability, and simple wiring. With the Adafruit libraries handling the driver-level details, you can go from a blank screen to a fully functional sensor dashboard in under an hour.
The examples in this tutorial build on each other. Start with Hello World to confirm your wiring and address are correct. Then layer on shapes, fonts, and sensor data. Once you are comfortable with the drawing primitives, menus and animations become straightforward.
For your next project, consider combining several of these patterns: a menu system that navigates between sensor readouts, each displayed with custom fonts and a progress indicator for data refresh. The 128x64 canvas is small but surprisingly capable when you design for it intentionally.
All the components used in this tutorial — SSD1306 OLED displays, ESP32 development boards, Arduino Nano, DHT22 sensors, and jumper wires — are available at wavtron.in.



