Skip to main content

Using a Joy-IT 1.3" 128x64 OLED display

What is the OLED display?

An OLED display (Organic Light-Emitting Diode display) is a screen technology where each pixel emits its own light using organic carbon-based materials. Unlike LCDs, OLEDs do not need a backlight, which allows them to produce deeper blacks, higher contrast, and thinner, more flexible screens.

If you want to use the display for animation, please reference this tutorial.

3.3V Logic
This OLED displays have a logic level of 3.3V. If you use any classic Arduino (UNO and Leonardo) with 5V logic, the display may misbehave. 3.3V Arduino includes Due, Zero, Nano 33 and MKR series. In this tutorial, we will be using Nano 33 Sense Rev2.

OLED vs LCD

OLED vs LCD Comparison
Feature OLED LCD (TFT / IPS)
Light source Self-emissive: each pixel emits its own light (organic diodes). Requires an external backlight (usually LED) that passes through liquid crystals.
Black level Perfect blacks — pixels can turn completely off. Blacks are grayish due to backlight bleed; improved in VA panels but not perfect.
Contrast ratio Extremely high (practically infinite for pixel-off blacks). Lower than OLED; good on high-quality IPS/VA but limited by backlight.
Viewing angles Very wide — colors and brightness remain consistent at off-angles. IPS: wide viewing angles; TN: narrow. Overall varies by LCD type.
Response time Very fast — excellent for motion and low input lag. Slower than OLED on average; IPS is better than TN for response consistency.
Power consumption Efficient for darker content (black areas consume no power); bright scenes use more power. Backlight is constant — power more consistent and sometimes better for bright full-screen content.
Thickness & flexibility Very thin; can be made flexible/curved (used in foldables). Thicker due to backlight and layers; rigid though some thin, curved LCDs exist.
Burn-in & image retention Risk of permanent burn-in / image retention with static content over long periods. No burn-in (generally safe for static UI elements); temporary image retention very rare.
Lifespan Organic materials can degrade (blue subpixel often ages faster) — lifespan improving with tech. Typically longer stable lifespan for backlight and LCD stack; LED backlight replacement possible.
Brightness in sunlight Can be less visible than the very brightest LCDs; high-end OLEDs mitigate this with strong peak brightness. LCDs can reach higher sustained brightness, giving better legibility in direct sunlight.
Color & HDR Vibrant colors and excellent HDR highlights due to per-pixel control; deep blacks improve perceived dynamic range. Good color (especially wide-gamut IPS); HDR performance depends on backlight local dimming (FALD/mini-LED).
Typical cost Generally more expensive (premium devices), though prices are falling. Usually more affordable across many sizes and use-cases.
Common uses Smartphones, premium TVs, smartwatches, VR headsets, foldable devices, high-end laptops. Monitors, budget-to-midrange TVs, laptops, industrial displays, large-size TVs (including mini-LED LCDs).
Best for Best black levels, contrast, thin/flexible designs, and immersive multimedia where perfect blacks matter. Best where cost, high sustained brightness, and static UI longevity are priorities (work monitors, outdoor screens).

Wiring

Wiring up the sensor is simple:

  1. VCC to 3.3V
  2. GND to GND
  3. SCL to SPI CLock Pin (Pin 13 on Nano 33 Sense)
  4. SDA to SPI MOSI Pin (Pin 11 on Nano 33 Sense)
  5. RST to Pin 4
  6. DC to Pin 5
  7. CS to Pin 6

Library

To use this code you will need the Adafruit_SH110X Library. We have a tutorial on how to install a library here.

Getting started

This code is modified from the library example code SH1106_128x64_SPi_QTPY.

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>


#define OLED_MOSI     11
#define OLED_CLK      13
#define OLED_DC       5
#define OLED_CS       6
#define OLED_RST      4

Adafruit_SH1106G display = Adafruit_SH1106G(128, 64,OLED_MOSI, OLED_CLK, OLED_DC, OLED_RST, OLED_CS);

#define NUMFLAKES 10
#define XPOS 0
#define YPOS 1
#define DELTAY 2


#define LOGO16_GLCD_HEIGHT 16
#define LOGO16_GLCD_WIDTH  16
static const unsigned char PROGMEM logo16_glcd_bmp[] =
{ B00000000, B11000000,
  B00000001, B11000000,
  B00000001, B11000000,
  B00000011, B11100000,
  B11110011, B11100000,
  B11111110, B11111000,
  B01111110, B11111111,
  B00110011, B10011111,
  B00011111, B11111100,
  B00001101, B01110000,
  B00011011, B10100000,
  B00111111, B11100000,
  B00111111, B11110000,
  B01111100, B11110000,
  B01110000, B01110000,
  B00000000, B00110000
};


void setup()   {
  Serial.begin(9600);

  //display.setContrast (0); // dim display

  // Start OLED
  display.begin(0, true); // we dont use the i2c address but we will reset!


  // Show image buffer on the display hardware.
  // Since the buffer is intialized with an Adafruit splashscreen
  // internally, this will display the splashscreen.
  display.display();
  delay(2000);

  // Clear the buffer.
  display.clearDisplay();

  // draw a single pixel
  display.drawPixel(10, 10, SH110X_WHITE);
  // Show the display buffer on the hardware.
  // NOTE: You _must_ call display after making any drawing commands
  // to make them visible on the display hardware!
  display.display();
  delay(2000);
  display.clearDisplay();

  // draw many lines
  testdrawline();
  display.display();
  delay(2000);
  display.clearDisplay();

  // draw rectangles
  testdrawrect();
  display.display();
  delay(2000);
  display.clearDisplay();

  // draw multiple rectangles
  testfillrect();
  display.display();
  delay(2000);
  display.clearDisplay();

  // draw mulitple circles
  testdrawcircle();
  display.display();
  delay(2000);
  display.clearDisplay();

  // draw a SH110X_WHITE circle, 10 pixel radius
  display.fillCircle(display.width() / 2, display.height() / 2, 10, SH110X_WHITE);
  display.display();
  delay(2000);
  display.clearDisplay();

  testdrawroundrect();
  delay(2000);
  display.clearDisplay();

  testfillroundrect();
  delay(2000);
  display.clearDisplay();

  testdrawtriangle();
  delay(2000);
  display.clearDisplay();

  testfilltriangle();
  delay(2000);
  display.clearDisplay();

  // draw the first ~12 characters in the font
  testdrawchar();
  display.display();
  delay(2000);
  display.clearDisplay();


  // text display tests
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(0, 0);
  display.println("Failure is always an option");
  display.setTextColor(SH110X_BLACK, SH110X_WHITE); // 'inverted' text
  display.println(3.141592);
  display.setTextSize(2);
  display.setTextColor(SH110X_WHITE);
  display.print("0x"); display.println(0xDEADBEEF, HEX);
  display.display();
  delay(2000);
  display.clearDisplay();

  // miniature bitmap display
  display.drawBitmap(30, 16,  logo16_glcd_bmp, 16, 16, 1);
  display.display();
  delay(1);

  // invert the display
  display.invertDisplay(true);
  delay(1000);
  display.invertDisplay(false);
  delay(1000);
  display.clearDisplay();

  // draw a bitmap icon and 'animate' movement
  testdrawbitmap(logo16_glcd_bmp, LOGO16_GLCD_HEIGHT, LOGO16_GLCD_WIDTH);
}


void loop() {

}


void testdrawbitmap(const uint8_t *bitmap, uint8_t w, uint8_t h) {
  uint8_t icons[NUMFLAKES][3];

  // initialize
  for (uint8_t f = 0; f < NUMFLAKES; f++) {
    icons[f][XPOS] = random(display.width());
    icons[f][YPOS] = 0;
    icons[f][DELTAY] = random(5) + 1;

    Serial.print("x: ");
    Serial.print(icons[f][XPOS], DEC);
    Serial.print(" y: ");
    Serial.print(icons[f][YPOS], DEC);
    Serial.print(" dy: ");
    Serial.println(icons[f][DELTAY], DEC);
  }

  while (1) {
    // draw each icon
    for (uint8_t f = 0; f < NUMFLAKES; f++) {
      display.drawBitmap(icons[f][XPOS], icons[f][YPOS], bitmap, w, h, SH110X_WHITE);
    }
    display.display();
    delay(200);

    // then erase it + move it
    for (uint8_t f = 0; f < NUMFLAKES; f++) {
      display.drawBitmap(icons[f][XPOS], icons[f][YPOS], bitmap, w, h, SH110X_BLACK);
      // move it
      icons[f][YPOS] += icons[f][DELTAY];
      // if its gone, reinit
      if (icons[f][YPOS] > display.height()) {
        icons[f][XPOS] = random(display.width());
        icons[f][YPOS] = 0;
        icons[f][DELTAY] = random(5) + 1;
      }
    }
  }
}


void testdrawchar(void) {
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(0, 0);

  for (uint8_t i = 0; i < 168; i++) {
    if (i == '\n') continue;
    display.write(i);
    if ((i > 0) && (i % 21 == 0))
      display.println();
  }
  display.display();
  delay(1);
}

void testdrawcircle(void) {
  for (int16_t i = 0; i < display.height(); i += 2) {
    display.drawCircle(display.width() / 2, display.height() / 2, i, SH110X_WHITE);
    display.display();
    delay(1);
  }
}

void testfillrect(void) {
  uint8_t color = 1;
  for (int16_t i = 0; i < display.height() / 2; i += 3) {
    // alternate colors
    display.fillRect(i, i, display.width() - i * 2, display.height() - i * 2, color % 2);
    display.display();
    delay(1);
    color++;
  }
}

void testdrawtriangle(void) {
  for (int16_t i = 0; i < min(display.width(), display.height()) / 2; i += 5) {
    display.drawTriangle(display.width() / 2, display.height() / 2 - i,
                         display.width() / 2 - i, display.height() / 2 + i,
                         display.width() / 2 + i, display.height() / 2 + i, SH110X_WHITE);
    display.display();
    delay(1);
  }
}

void testfilltriangle(void) {
  uint8_t color = SH110X_WHITE;
  for (int16_t i = min(display.width(), display.height()) / 2; i > 0; i -= 5) {
    display.fillTriangle(display.width() / 2, display.height() / 2 - i,
                         display.width() / 2 - i, display.height() / 2 + i,
                         display.width() / 2 + i, display.height() / 2 + i, SH110X_WHITE);
    if (color == SH110X_WHITE) color = SH110X_BLACK;
    else color = SH110X_WHITE;
    display.display();
    delay(1);
  }
}

void testdrawroundrect(void) {
  for (int16_t i = 0; i < display.height() / 2 - 2; i += 2) {
    display.drawRoundRect(i, i, display.width() - 2 * i, display.height() - 2 * i, display.height() / 4, SH110X_WHITE);
    display.display();
    delay(1);
  }
}

void testfillroundrect(void) {
  uint8_t color = SH110X_WHITE;
  for (int16_t i = 0; i < display.height() / 2 - 2; i += 2) {
    display.fillRoundRect(i, i, display.width() - 2 * i, display.height() - 2 * i, display.height() / 4, color);
    if (color == SH110X_WHITE) color = SH110X_BLACK;
    else color = SH110X_WHITE;
    display.display();
    delay(1);
  }
}

void testdrawrect(void) {
  for (int16_t i = 0; i < display.height() / 2; i += 2) {
    display.drawRect(i, i, display.width() - 2 * i, display.height() - 2 * i, SH110X_WHITE);
    display.display();
    delay(1);
  }
}

void testdrawline() {
  for (int16_t i = 0; i < display.width(); i += 4) {
    display.drawLine(0, 0, i, display.height() - 1, SH110X_WHITE);
    display.display();
    delay(1);
  }
  for (int16_t i = 0; i < display.height(); i += 4) {
    display.drawLine(0, 0, display.width() - 1, i, SH110X_WHITE);
    display.display();
    delay(1);
  }
  delay(250);

  display.clearDisplay();
  for (int16_t i = 0; i < display.width(); i += 4) {
    display.drawLine(0, display.height() - 1, i, 0, SH110X_WHITE);
    display.display();
    delay(1);
  }
  for (int16_t i = display.height() - 1; i >= 0; i -= 4) {
    display.drawLine(0, display.height() - 1, display.width() - 1, i, SH110X_WHITE);
    display.display();
    delay(1);
  }
  delay(250);

  display.clearDisplay();
  for (int16_t i = display.width() - 1; i >= 0; i -= 4) {
    display.drawLine(display.width() - 1, display.height() - 1, i, 0, SH110X_WHITE);
    display.display();
    delay(1);
  }
  for (int16_t i = display.height() - 1; i >= 0; i -= 4) {
    display.drawLine(display.width() - 1, display.height() - 1, 0, i, SH110X_WHITE);
    display.display();
    delay(1);
  }
  delay(250);

  display.clearDisplay();
  for (int16_t i = 0; i < display.height(); i += 4) {
    display.drawLine(display.width() - 1, 0, 0, i, SH110X_WHITE);
    display.display();
    delay(1);
  }
  for (int16_t i = 0; i < display.width(); i += 4) {
    display.drawLine(display.width() - 1, 0, i, display.height() - 1, SH110X_WHITE);
    display.display();
    delay(1);
  }
  delay(250);
}

Something fun

Nano 33 Sense Rev2 has built-in IMU sensor, temperature and humidity sensor and microphone. The code below will have a ball displayed on the OLED to follow Nano 33's motion.

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <Arduino_BMI270_BMM150.h>
#include <math.h>

// OLED SPI pins
#define OLED_MOSI     11
#define OLED_CLK      13
#define OLED_DC       5
#define OLED_CS       6
#define OLED_RST      4

Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, OLED_MOSI, OLED_CLK, OLED_DC, OLED_RST, OLED_CS);

// -------- TUNING --------
const int W = 128;
const int H = 64;
const int BALL_RADIUS = 6;

// How strongly linear accel (in g or m/s^2 units) maps to pixels/sec^2.
// Increase to make ball accelerate more for the same hand motion.
const float LIN_ACCEL_TO_PIX = 220.0f;

// Low-pass alpha used to estimate gravity from accel: gravity = LPF(accel).
// Lower alpha -> smoother/slow gravity estimate. Typical 0.01..0.05
const float GRAV_LPF_ALPHA = 0.02f;

// Ignore very small linear accelerations (g) to reduce jitter
const float LINEAR_DEADZONE = 0.02f; // ~0.02 g

// Friction & bounce
const float FRICTION = 0.98f;      // per-frame damping factor (closer to 1 = less friction)
const float BOUNCE_DAMP = 0.6f;    // velocity retained after hitting wall

// Loop timing
const unsigned long LOOP_DELAY_MS = 5;

float gravX = 0.0f, gravY = 0.0f, gravZ = 0.0f; // gravity estimate (same units as IMU accel)
float vx = 0.0f, vy = 0.0f;   // velocity in pixels/sec
float px = W/2.0f, py = H/2.0f; // position in pixels

unsigned long lastMillis = 0;

void setup() {
  Serial.begin(115200);
  while (!Serial) {}

  display.begin(0, true);
  display.clearDisplay();
  display.display();

  if (!IMU.begin()) {
    Serial.println("Failed to initialize IMU!");
    while (1);
  }

  // Let sensor settle, sample a bit for initial gravity
  delay(200);
  // Initialize gravity estimate with a few samples
  for (int i=0; i<50; ++i) {
    if (IMU.accelerationAvailable()) {
      float ax, ay, az;
      IMU.readAcceleration(ax, ay, az);
      if (i==0) { gravX = ax; gravY = ay; gravZ = az; }
      else {
        gravX = gravX + GRAV_LPF_ALPHA * (ax - gravX);
        gravY = gravY + GRAV_LPF_ALPHA * (ay - gravY);
        gravZ = gravZ + GRAV_LPF_ALPHA * (az - gravZ);
      }
    }
    delay(5);
  }

  lastMillis = millis();

  display.clearDisplay();
  display.fillCircle((int)round(px), (int)round(py), BALL_RADIUS, SH110X_WHITE);
  display.display();
}

void loop() {
  unsigned long now = millis();
  float dt = (now - lastMillis) / 1000.0f;
  if (dt <= 0.0f) dt = 0.001f;
  lastMillis = now;

  float ax = 0.0f, ay = 0.0f, az = 0.0f;
  if (IMU.accelerationAvailable()) {
    IMU.readAcceleration(ax, ay, az);
  }

  // Update gravity estimate with LPF: grav = grav + alpha*(acc - grav)
  gravX = gravX + GRAV_LPF_ALPHA * (ax - gravX);
  gravY = gravY + GRAV_LPF_ALPHA * (ay - gravY);
  gravZ = gravZ + GRAV_LPF_ALPHA * (az - gravZ);

  // Linear acceleration = measured - gravity (in sensor units)
  float linX = ax - gravX;
  float linY = ay - gravY;
  float linZ = az - gravZ; // not used here but available

  // Deadzone to reduce small noise
  if (fabs(linX) < LINEAR_DEADZONE) linX = 0;
  if (fabs(linY) < LINEAR_DEADZONE) linY = 0;

  // Map linear accel to pixels/sec^2
  // Note: choose mapping so motion of device moves the ball in the same direction.
  // You might need to invert signs depending on your mounting/orientation.
  //
  // Common mapping (OLED on top of Nano 33, X axis left-right, Y axis forward-back):
  //   linX -> px (left/right)
  //   linY -> py (up/down)
  //
  // If the ball moves opposite to the board, invert sign(s) below.
  float ax_pixels = linX * LIN_ACCEL_TO_PIX;
  float ay_pixels = linY * LIN_ACCEL_TO_PIX;

  // Integrate velocity (vx, vy) using accel (pixels/sec^2)
  vx += ax_pixels * dt;
  vy += ay_pixels * dt;

  // Apply friction (scaled by dt so behaviour is consistent with loop time)
  float frictionScaled = pow(FRICTION, dt * 60.0f);
  vx *= frictionScaled;
  vy *= frictionScaled;

  // Integrate position
  px += vx * dt;
  py += vy * dt;

  // Keep inside bounds (account for radius)
  if (px < BALL_RADIUS) {
    px = BALL_RADIUS;
    if (vx < 0) vx = -vx * BOUNCE_DAMP;
  } else if (px > W - 1 - BALL_RADIUS) {
    px = W - 1 - BALL_RADIUS;
    if (vx > 0) vx = -vx * BOUNCE_DAMP;
  }

  if (py < BALL_RADIUS) {
    py = BALL_RADIUS;
    if (vy < 0) vy = -vy * BOUNCE_DAMP;
  } else if (py > H - 1 - BALL_RADIUS) {
    py = H - 1 - BALL_RADIUS;
    if (vy > 0) vy = -vy * BOUNCE_DAMP;
  }

  // Render
  display.clearDisplay();
  // border
  display.drawRect(0, 0, W, H, SH110X_WHITE);
  // ball
  display.fillCircle((int)round(px), (int)round(py), BALL_RADIUS, SH110X_WHITE);
  // small velocity vector for visualization
  int vx_end_x = (int)round(px + vx * 0.02f);
  int vy_end_y = (int)round(py + vy * 0.02f);
  display.drawLine((int)round(px), (int)round(py), vx_end_x, vy_end_y, SH110X_WHITE);
  display.display();

  // Optional debug print periodically
  static unsigned long lastPrint = 0;
  if (now - lastPrint > 200) {
    Serial.print("ax: "); Serial.print(ax, 4);
    Serial.print(" ay: "); Serial.print(ay, 4);
    Serial.print(" linX: "); Serial.print(linX, 5);
    Serial.print(" linY: "); Serial.print(linY, 5);
    Serial.print(" px: "); Serial.print(px, 2);
    Serial.print(" py: "); Serial.print(py, 2);
    Serial.print(" vx: "); Serial.print(vx, 3);
    Serial.print(" vy: "); Serial.println(vy, 3);
    lastPrint = now;
  }

  delay(LOOP_DELAY_MS);
}