Arduino library 路 Displays

Adafruit SSD1306: drive a 128x64 OLED from an Arduino

Adafruit SSD1306 is the library most tutorials use for the small 0.96 inch OLED modules: 128 by 64 (or 128 by 32) white, blue or yellow pixels on a four-pin I2C board. It keeps a copy of the whole screen in the board鈥檚 RAM, you draw into that copy with the shape and text calls it inherits from Adafruit GFX, and nothing appears on the glass until you call display(), which sends the buffer over I2C.

Built in on Mokxi: #include <Adafruit_SSD1306.h> and it builds, with nothing to install. Free, and no account needed.

What Adafruit SSD1306 does

That buffer is the thing to know before you start. A 128x64 panel needs 1 KB of RAM for it, taken from the heap when begin() runs, which is half of an Uno鈥檚 2 KB. That is why a sketch that compiles fine can still print "SSD1306 allocation failed" and stop, and why two 128x64 screens will not fit on an Uno at all.

The library talks to the module at I2C address 0x3C on almost every board you can buy (0x3D when the address jumper is moved), on the board鈥檚 SDA and SCL pins: A4 and A5 on an Uno or Nano, 20 and 21 on a Mega, GPIO 21 and 22 on a classic ESP32.

Library Manager nameAdafruit SSD1306
AuthorAdafruit
Include#include <Adafruit_SSD1306.h>
Boardsevery board Mokxi compiles for but ATtiny85
In the simulatorSimulated: it runs against the part the way it does on the bench.
Parts it drivesOLED display, 128x64, OLED display, 128x32

Install it

It builds on every board Mokxi compiles for but ATtiny85.

In Mokxi it is built in: #include <Adafruit_SSD1306.h> and the sketch compiles, with no download, no zip and no Library Manager.

On a real board, install "Adafruit SSD1306" by Adafruit from the Arduino IDE鈥檚 Library Manager (Sketch, Include Library, Manage Libraries), or add Adafruit SSD1306 to lib_deps in PlatformIO. Mokxi follows the calls of release 2.5.13, so a sketch written here builds there too.

What to know in the simulator: The SPI constructors build, and begin() says on Serial that the SPI module is not simulated: Mokxi鈥檚 SSD1306 parts are the I2C modules.

ESP32-C3 路 Arduino Uno 路 Arduino Nano 路 Arduino Mega 2560 路 Arduino Leonardo 路 SparkFun Pro Micro 路 Arduino Micro 路 ESP32-C6 路 Raspberry Pi Pico 路 Raspberry Pi Pico W 路 Seeed XIAO SAMD21 路 STM32 路 STM32 Nucleo-F411RE 路 STM32 Blue Pill 路 micro:bit V2 路 Arduino Uno R4 路 ESP32 DevKit 路 ESP32-S3-DevKitC-1 路 ESP8266 NodeMCU 路 Arduino Pro Mini 3.3 V 路 Seeed XIAO ESP32-C3 路 ESP32-C3 SuperMini 路 Seeed XIAO ESP32-C6 路 ESP32-DevKitC V4 路 LOLIN32 Lite 路 Wemos D1 R32 路 Cheap Yellow Display 路 TTGO T-Display 路 Seeed XIAO ESP32-S3 路 Arduino Nano ESP32 路 Arduino Nano RP2040 Connect 路 Seeed XIAO RP2040 路 Waveshare RP2040-Zero 路 Arduino Uno R4 WiFi

The calls you will use

In the order a sketch meets them.

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

Creates the display object: width, height, the I2C bus and the reset pin. -1 means the module has no reset pin wired, which is true of the four-pin boards.

display.begin(SSD1306_SWITCHCAPVCC, 0x3C)

Starts I2C, takes the frame buffer from the heap and sets the panel up. Returns false when there is no RAM for the buffer or nothing answers at the address, so check it.

if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
  Serial.println(F("SSD1306 allocation failed"));
  for (;;);
}

display.clearDisplay()

Clears the buffer to black. The screen itself does not change until the next display().

display.setTextSize(2)

Scales the built-in 5 by 7 font: size 1 is 6 pixels a character with spacing, so 21 characters by 8 lines on a 128x64 panel; size 2 doubles both.

display.setTextColor(SSD1306_WHITE)

Picks the color text is drawn in. SSD1306_WHITE lights pixels, SSD1306_BLACK clears them, and SSD1306_INVERSE flips them, which draws text over a filled box.

display.setCursor(0, 16)

Moves the text cursor to x, y in pixels from the top left. print() and println() then write from there.

display.setCursor(0, 16);
display.print("Temp: ");
display.print(23.5, 1);
display.println(" C");

display.drawRect(x, y, w, h, SSD1306_WHITE)

One of the Adafruit GFX shapes, with drawLine(), drawCircle(), fillRect(), fillCircle(), drawTriangle() and drawPixel(): each draws into the buffer in the color you pass.

display.drawBitmap(0, 0, logo, 128, 64, SSD1306_WHITE)

Draws a one-bit image stored as a byte array, one bit a pixel, eight pixels a byte, rows top to bottom. Keep large arrays in PROGMEM on an AVR board.

display.display()

Sends the buffer to the panel. Call it once after you have drawn a whole frame, not after every line: each call is a full I2C transfer.

Example: OLED on an Uno

On the Arduino Uno R3. It runs in the browser exactly as printed.

sketch.ino 路 Arduino Uno R3
// OLED: text and shapes on a 128x64 SSD1306, over bit-banged I2C.
//
// Wiring, module to board:
//   SCL -> A5      SDA -> A4      VCC -> 5 V      GND -> GND
//
// Those are the Uno's I2C pins, but nothing here uses the TWI peripheral: the
// Mokxi ATmega328P model does not have one (docs/uno.md section 3), so
// mokxi_i2c.h pulls the two lines down and lets them go, and the module's own
// 4k7 pull-ups bring them back up. Any two pins would do.
//
// # The 1 KB
//
// A 128x64 monochrome frame is 1024 bytes, and the panel cannot be read back,
// so a driver that wants to change one pixel has to remember the other 1023.
// That is half of this part's SRAM. It is what a pixel display costs on an
// ATmega328P, and it is why the screen furniture below is drawn once into that
// buffer and only the ball, the counter and the bar are touched again.
//
// Each display() sends only the eight-column blocks of each page that actually
// changed. A moving ball is a few dozen bytes rather than a kilobyte, which is
// the difference between twenty-five frames a second and two: this board's
// software I2C manages about 32 kbit/s, so a full 1 KB refresh would take a
// third of a second.

#include "mokxi_i2c.h"
#include "mokxi_ssd1306.h"

const uint8_t SCL_PIN = A5;
const uint8_t SDA_PIN = A4;

// 400 kHz nominal. On this board the pin work dominates and it lands near
// 100 kHz, which is still twice what the 5 us standard-mode delays give.
SoftI2c bus(SCL_PIN, SDA_PIN, 400);
Ssd1306 oled(bus, SSD1306_ADDRESS);

// The arena the ball bounces in, and the ball.
const int16_t AX = 2;
const int16_t AY = 21;
const int16_t AW = 60;
const int16_t AH = 41;
const int16_t R = 4;

int16_t bx = AX + R + 6;
int16_t by = AY + R + 4;
int16_t bdx = 2;
int16_t bdy = 1;

uint16_t ticks = 0;

const int FRAME_MS = 40;

// Everything that never moves, drawn once into the buffer.
static void furniture() {
  oled.clear();
  oled.text(3, 2, "MOKXI", 2);
  oled.text(100, 8, "UNO", 1);
  oled.hLine(0, 18, 128);
  oled.rect(AX - 1, AY - 1, AW + 2, AH + 2);

  oled.rect(68, 22, 24, 16);
  oled.circle(110, 30, 7);
  oled.rect(106, 44, 20, 16);
}

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Uno: SSD1306 over bit-banged I2C");
  bus.begin();
  if (!oled.begin()) {
    Serial.println("no OLED answered 0x3C; check SCL, SDA and power");
    return;
  }
  furniture();
  oled.display();
}

void loop() {
  oled.fillCircle(bx, by, R, false);

  bx = (int16_t)(bx + bdx);
  by = (int16_t)(by + bdy);
  if (bx - R <= AX) {
    bx = (int16_t)(AX + R);
    bdx = (int16_t)-bdx;
  } else if (bx + R >= AX + AW - 1) {
    bx = (int16_t)(AX + AW - 1 - R);
    bdx = (int16_t)-bdx;
  }
  if (by - R <= AY) {
    by = (int16_t)(AY + R);
    bdy = (int16_t)-bdy;
  } else if (by + R >= AY + AH - 1) {
    by = (int16_t)(AY + AH - 1 - R);
    bdy = (int16_t)-bdy;
  }
  oled.fillCircle(bx, by, R, true);

  // The counter and the bar move a good deal more picture than the ball does,
  // so they tick at a sixth of the frame rate and the lozenge at a third. The
  // driver only sends the eight-column blocks that changed, and a frame where
  // only the ball moved is a few dozen bytes on the bus instead of four hundred.
  ticks++;
  if (ticks % 6u == 0u) {
    oled.number(68, 44, (long)((ticks / 6u) % 1000u), 2, true, true);
    int16_t fill = (int16_t)(1 + ((ticks / 6u) % 16u));
    oled.fillRect(108, 46, 16, 12, false);
    oled.fillRect(108, 46, fill, 12, true);
  }
  if (ticks % 3u == 0u) {
    // The lozenge in the top box slides, so the shapes panel is not quite still.
    oled.fillRect(70, 24, 20, 12, false);
    oled.fillRect((int16_t)(70 + ((ticks / 3u) % 14u)), 24, 6, 12, true);
  }

  oled.display();
  delay(FRAME_MS);
}

Open it in the editor, wired and ready to run 路 OLED on an Uno: the project page

Common errors and how to fix them

SSD1306 allocation failed

begin() could not get 1 KB of RAM for the buffer. Free some: put fixed strings in F("..."), shrink big global arrays, move bitmaps to PROGMEM, or use a board with more RAM. A 128x32 panel needs only 512 bytes.

The sketch runs but the screen stays black

Check the address first: run an I2C scanner, and pass 0x3D to begin() if that is what it finds. Then check that display() is called after drawing, and that SDA and SCL go to the board鈥檚 I2C pins, not two random digital pins.

'SSD1306_WHITE' was not declared in this scope

Older sketches use WHITE and BLACK, newer ones SSD1306_WHITE and SSD1306_BLACK. The current library has both on most boards, but if a sketch copied from an old tutorial fails, swap the old names for the SSD1306_ ones.

A 1.3 inch OLED shows noise or a shifted picture

Most 1.3 inch modules use the SH1106 chip, not the SSD1306, and its memory is laid out differently. Use the Adafruit SH110X library (Adafruit_SH1106G) or U8g2 for those.

Questions and answers

Do I need to include Adafruit_GFX.h and Wire.h too?

Include all three, Wire.h, Adafruit_GFX.h and Adafruit_SSD1306.h, as the tutorials do. Adafruit SSD1306 depends on Adafruit GFX for every shape and the font, and the Library Manager installs it alongside when you ask.

Why does nothing show until I call display()?

Every drawing call changes the copy of the screen in RAM. display() is the one call that sends that copy to the panel, so you can draw a whole frame and show it at once without flicker.

Try Adafruit SSD1306 without installing anything

Open the example, change the sketch, and keep your version in a free account.