Arduino: 128×64 OLED display (SSD1306)

The Serial Monitor is great while the Arduino is connected to a computer. But a thermometer, counter or game should work without a computer – that is what a small OLED display is for. It has 128 × 64 dots (pixels), is easy to read even in the dark, and connects to the Arduino with only four wires over I2C.

What an OLED display is

  • The most common one is a 0.96 inch display with the SSD1306 chip. It is 128 dots wide and 64 dots high.
  • Each dot gives its own light (like a tiny LED), so the display has no backlight and uses little current.
  • There are single-color (white, blue) and two-color displays (top 16 rows yellow, the rest blue) – they are programmed the same way.
  • It communicates over I2C: two wires (SDA for data and SCL for the clock), and each device has its own address. This display's address is usually 0x3C.

Wiring

Display pinArduino UnoESP32
VCC5V (or 3.3V)3V3
GNDGNDGND
SDAA4GPIO21
SCLA5GPIO22

On some modules the pin order is GND, VCC, SCL, SDA – always read the labels on the board before connecting.

In the Arduino IDE install the Adafruit SSD1306 and Adafruit GFX Library libraries (Sketch → Include Library → Manage Libraries). When the IDE asks to install the required libraries too, choose "Install all".

Check: I2C scanner

Before the first program, check whether the Arduino "sees" the display. This short program prints the addresses of all devices on the I2C bus:

// I2C scanner – prints the addresses of all I2C devices (works on Arduino and ESP32)
#include <Wire.h>

void setup() {
  Serial.begin(9600);
  Wire.begin();                 // on the ESP32 you can write Wire.begin(21, 22);
  Serial.println("Looking for I2C devices...");
  int found = 0;
  for (int address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    if (Wire.endTransmission() == 0) {      // 0 = a device answered
      Serial.print("Device at address 0x");
      Serial.println(address, HEX);
      found++;
    }
  }
  if (found == 0) {
    Serial.println("No devices - check the SDA, SCL, VCC and GND wires.");
  }
}

void loop() {
}

If 0x3C (or 0x3D) is printed, everything is connected correctly. If there are no devices, check the SDA and SCL wires – they are most often swapped.

Program 1: first text

// Arduino – 128x64 OLED display (SSD1306, I2C): first text
// Libraries: "Adafruit SSD1306" and "Adafruit GFX Library" (Library Manager)
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const int WIDTH = 128;
const int HEIGHT = 64;
// -1: the display has no separate RESET pin
Adafruit_SSD1306 display(WIDTH, HEIGHT, &Wire, -1);

void setup() {
  Serial.begin(9600);
  // 0x3C is the most common I2C address of these displays (sometimes 0x3D)
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("Display not found!");
    while (true) {
    }                           // stop the program
  }
  display.clearDisplay();         // clear the image in memory
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);         // small text: 21 characters per line
  display.setCursor(0, 0);        // top left corner
  display.println("Hello!");
  display.setTextSize(2);         // double-size text
  display.setCursor(0, 20);
  display.println("Arduino");
  display.display();              // only now is the image sent to the display
}

void loop() {
}

How drawing on the display works

  • Point (0, 0) is in the top left corner. x grows to the right (up to 127) and y downwards (up to 63).
  • All commands draw into the Arduino's memory. The image is sent to the display only with display() – if you forget it, the screen stays empty.
  • clearDisplay() clears the image in memory; we call it before drawing new content.
setTextSizeLetter heightCharacters per lineLines on screen
18 dots218
216 dots104
324 dots72

Program 2: counter and shapes

// Arduino – OLED: time since power-on and a filling bar
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 display(128, 64, &Wire, -1);

void setup() {
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setTextColor(SSD1306_WHITE);
}

void loop() {
  unsigned long seconds = millis() / 1000;

  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("Time since power-on:");
  display.setTextSize(3);
  display.setCursor(0, 16);
  display.print(seconds);
  display.print(" s");

  int length = map(seconds % 10, 0, 9, 0, 124);   // the bar fills every 10 s
  display.drawRect(0, 52, 128, 12, SSD1306_WHITE);  // bar outline
  display.fillRect(2, 54, length, 8, SSD1306_WHITE);
  display.display();
  delay(200);
}
CommandDraws
drawLine(x1, y1, x2, y2, color)a line
drawRect(x, y, width, height, color)a rectangle outline
fillRect(x, y, width, height, color)a filled rectangle
drawCircle(x, y, r, color)a circle
fillCircle(x, y, r, color)a filled circle
drawPixel(x, y, color)one dot

The color is SSD1306_WHITE (the dot is lit) or SSD1306_BLACK (the dot is dark).

Program 3: thermometer with a DHT11

We combine the display with the sensor from the lesson Temperature sensor: the DHT11 DATA pin is on pin 3, and the display is still on A4 and A5.

// Arduino – OLED thermometer: temperature and humidity from a DHT11 (DATA on pin 3)
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>

#define DHTPIN 3
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
Adafruit_SSD1306 display(128, 64, &Wire, -1);

void setup() {
  dht.begin();
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setTextColor(SSD1306_WHITE);
}

void loop() {
  float t = dht.readTemperature();
  float h = dht.readHumidity();

  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("Room thermometer");
  display.drawLine(0, 10, 127, 10, SSD1306_WHITE);   // line under the title

  if (isnan(t) || isnan(h)) {
    display.setCursor(0, 28);
    display.print("Sensor error!");
  } else {
    display.setTextSize(2);
    display.setCursor(0, 18);
    display.print(t, 1);
    display.print(" C");
    display.setCursor(0, 42);
    display.print(h, 0);
    display.print(" %");
  }
  display.display();
  delay(2000);                  // read the DHT11 every 2 seconds
}

This is the first real device that works without a computer: power the Arduino from a phone charger or a 9 V battery and put it anywhere in the room.

The OLED display on the ESP32

The same display and the same libraries also work on the ESP32. There are only two differences: the display is powered from 3V3, and the I2C connection is started with Wire.begin(21, 22).

// ESP32 – the same OLED display; only the I2C start changes
// (SDA = GPIO21, SCL = GPIO22)
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 display(128, 64, &Wire, -1);

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);           // SDA, SCL – default pins on the ESP32
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("Display not found!");
    while (true) {
      delay(100);
    }
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(2);
  display.setCursor(0, 0);
  display.println("ESP32");
  display.setTextSize(1);
  display.println("OLED runs on 3.3 V");
  display.display();
}

void loop() {
}

We will also use the display in the final project smart classroom.

Common problems

  • The screen is completely black: check the address with the scanner (0x3C or 0x3D) and whether you called display().
  • The program stops at "Display not found": SDA and SCL are swapped or the display has no power.
  • The Arduino Uno behaves strangely when you add more code: the screen image takes 1024 bytes, and the Uno has only 2048 bytes of RAM. Use shorter texts and fewer variables (or an ESP32, which has much more memory).
  • Text overlaps: clearDisplay() is missing before drawing again.

Practice tasks

Try to solve each task yourself first, then open the solution.

  1. Show on the display how many times the button on pin 2 has been pressed, in large digits (text size 4).
  2. Draw a light graph: LDR on A0, a new point every 100 ms, while the old part of the graph scrolls to the left.
  3. Make an animation: a ball with a radius of 4 dots moves around the screen and bounces off the edges.
Solution 1: press counter on the display
// Task – button press counter (pin 2) on the OLED display, in large digits
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 display(128, 64, &Wire, -1);
const int BUTTON = 2;
int count = 0;
int previous = HIGH;

void show() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("Presses:");
  display.setTextSize(4);
  display.setCursor(0, 20);
  display.print(count);
  display.display();
}

void setup() {
  pinMode(BUTTON, INPUT_PULLUP);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setTextColor(SSD1306_WHITE);
  show();
}

void loop() {
  int now = digitalRead(BUTTON);
  if (previous == HIGH && now == LOW) {
    count++;
    show();
    delay(30);
  }
  previous = now;
}
Solution 2: light graph
// Task – light graph: LDR on A0, a new point every 100 ms,
// and the graph scrolls to the left
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 display(128, 64, &Wire, -1);
byte heights[128];               // keep the last 128 readings (one per column)

void setup() {
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  for (int x = 0; x < 128; x++) {
    heights[x] = 63;
  }
}

void loop() {
  for (int x = 0; x < 127; x++) {          // shift all points to the left
    heights[x] = heights[x + 1];
  }
  heights[127] = map(analogRead(A0), 0, 1023, 63, 0);   // bigger value = higher point
  display.clearDisplay();
  for (int x = 1; x < 128; x++) {
    display.drawLine(x - 1, heights[x - 1], x, heights[x], SSD1306_WHITE);
  }
  display.display();
  delay(100);
}
Solution 3: bouncing ball
// Task – a ball bouncing off the edges of the screen
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 display(128, 64, &Wire, -1);
int x = 10, y = 10;             // ball position
int dx = 2, dy = 1;             // movement in each step
const int R = 4;                // radius

void setup() {
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
}

void loop() {
  x += dx;
  y += dy;
  if (x - R <= 0 || x + R >= 127) dx = -dx;   // hit the left or right edge
  if (y - R <= 0 || y + R >= 63) dy = -dy;    // hit the top or bottom edge
  display.clearDisplay();
  display.drawRect(0, 0, 128, 64, SSD1306_WHITE);
  display.fillCircle(x, y, R, SSD1306_WHITE);
  display.display();
  delay(20);
}

Check your knowledge

Open the quiz

Quiz: OLED display

1. Which Arduino Uno pins do SDA and SCL connect to?




2. Where is point (0, 0) on the display?




3. What happens if you forget display()?




4. What is the most common I2C address of an SSD1306 display?