OLED Display

Anand Ramaswami

Welcome Back, Maker!

So far everything your Arduino has communicated back to you has gone through the Serial Monitor. Numbers, sensor values, motor speeds, button states — all of it appearing as text on your computer screen.

That is great for testing and debugging. But what happens when you unplug the USB cable and take your project out into the world? No laptop. No Serial Monitor. No way to see what your Arduino is thinking.

That is exactly the problem the OLED display solves.

What Is an OLED Display?

OLED stands for Organic Light Emitting Diode.

It is a small screen. The one on your NanoMake Pro is 128 pixels wide and 64 pixels tall and every single one of those pixels produces its own light. Unlike a regular LCD screen that needs a backlight behind it, each OLED pixel glows on its own. This is why OLED screens look so sharp and crisp, especially in the dark. Black pixels are truly black because they are simply off.

The display on your NanoMake Pro is already wired up and ready. You do not need to connect anything.

How Does the Arduino Talk to the Display?

The OLED display uses a communication protocol called I2C — pronounced "eye squared see."

I2C is clever because it only needs two wires to send and receive data — a clock line called SCL and a data line called SDA. Both are already connected on your NanoMake Pro. Multiple devices can share the same two wires, which is why the board also has an extra I2C port for adding more sensors later without any extra wiring complexity.

Every device on an I2C bus has its own address — like a house number on a street. Your OLED display has the address 0x3C. When the Arduino wants to send something to the display, it calls out that address and the display responds. Everything else on the bus ignores it.

You do not need to manage any of this manually. The libraries handle it for you.

The Libraries You Need

Two libraries work together to drive this display.

The first is the Adafruit SSD1306 — this is the driver library. It handles the low-level job of talking to the display hardware, sending data through I2C, and managing the pixel buffer.

The second is Adafruit GFX — this is the graphics library. It sits on top of the driver and gives you all the useful drawing commands — text, lines, rectangles, circles, and more.

How to Install Them

Open Arduino IDE. Go to Sketch → Include Library → Manage Libraries. Search for Adafruit SSD1306 and install it. When it asks if you want to install dependencies, click Install All — this automatically installs Adafruit GFX at the same time.

That is all. Both libraries are now ready to use.

Setting Up the Display in Code

Every project in this blog starts with the same setup. Here is what it looks like and why each part is there.

#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

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#include <Wire.h> loads the I2C communication library. This is what allows the Arduino to send data through the SCL and SDA lines to the display.

#include <Adafruit_GFX.h> loads the graphics library that gives you drawing and text commands.

#include <Adafruit_SSD1306.h> loads the display driver that knows how to talk to this specific OLED hardware.

#define SCREEN_WIDTH 128 and #define SCREEN_HEIGHT 64 set the dimensions of your display in pixels. These are just named constants — using them makes the code easier to read and easier to change if you ever use a different display.

#define OLED_RESET -1 tells the library that this display does not have a separate reset pin. On the NanoMake Pro the reset is handled internally, so -1 means not used.

#define SCREEN_ADDRESS 0x3C is the I2C address of your display. This is how the Arduino identifies which device on the bus it is talking to.

Adafruit_SSD1306 display(...) creates the display object. This is the name you will use for every display command from here on. Think of it as the same idea as Servo myServo — you are creating a named object that represents your physical hardware.

And inside setup() you always need these three lines:

display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);

display.clearDisplay();

display.display();

display.begin() initialises the hardware and gets the display ready. If this fails — for example if the display is not connected properly — nothing else will work.

display.clearDisplay() wipes the internal pixel buffer clean. The buffer is a section of memory that holds a copy of what should be on screen. You always draw into the buffer first, then push it to the screen.

display.display() pushes whatever is in the buffer onto the actual screen. This two-step process — draw into buffer, then push to screen — means the display updates all at once instead of flickering as each element is drawn..

 

Project 1: Hello World

Every new display starts here. This project puts two lines of text on the screen — a greeting and a small tagline. Simple, clean, and the perfect way to confirm your display is working before building anything more complex.

Code:

#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

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup() {

  display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);

  display.clearDisplay();

  display.setTextSize(2);

  display.setTextColor(SSD1306_WHITE);

  display.setCursor(10, 10);

  display.println("Hello!");

  display.setTextSize(1);

  display.setCursor(10, 40);

  display.println("Welcome, Maker!");

  display.display();

}

void loop() {

}

The Key Display Commands

Before the projects, here are the commands you will use most often.

 display.clearDisplay();

Wipe the buffer clean. Call this at the start of each new frame before drawing anything new.

 display.setTextSize(size);

Set the text size. Size 1 is small — each character is about 6x8 pixels. Size 2 doubles it. Size 3 triples it. Bigger text is easier to read from a distance but takes up more space on screen.

 display.setTextColor(SSD1306_WHITE);

Sets the text colour to white — which on this monochrome display means the pixels are ON and glowing. SSD1306_BLACK would turn pixels off, which is useful for creating inverted effects.

display.setCursor(x, y);

Moves the text cursor to a position on screen. x is the horizontal position in pixels from the left edge. y is the vertical position in pixels from the top edge. The top-left corner is (0, 0).

 display.print("text");

Prints text or numbers at the current cursor position. println() moves to the next line after printing. print() stays on the same line.

 display.display();

 Pushes everything from the buffer onto the actual screen. This is the moment the display lights up with your text. Nothing you drew before this line was visible — it was all waiting in the buffer.

Explanation:

display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS) starts the display. SSD1306_SWITCHCAPVCC tells the library to use the display's internal voltage converter to power the OLED pixels. SCREEN_ADDRESS is 0x3C — the I2C address of your display. This line must come before any drawing commands.

display.clearDisplay() clears the buffer. Even though this is the very start of the program and nothing has been drawn yet, it is good practice to always clear first.

display.setTextSize(2) sets a large text size for the main greeting. Size 2 makes each character 12x16 pixels — bold and easy to read.

display.setTextColor(SSD1306_WHITE) sets the text to white — glowing pixels. On this display there is really only one colour but you always need to set it before printing.

display.setCursor(10, 10) moves the cursor to 10 pixels from the left and 10 pixels from the top. This gives a small margin from the edge of the screen so the text does not look cramped.

display.println("Hello!") writes the greeting into the buffer at the current cursor position. println() also moves the cursor down to the next line after printing.

display.setTextSize(1) switches to the smaller text size for the tagline. This contrast between big and small text makes the display feel organised and intentional.

display.setCursor(10, 40) moves the cursor down to y position 40 for the second line. With size 2 text taking up roughly 16 pixels of height starting at y=10, position 40 gives a comfortable gap between the two lines.

display.println("Welcome, Maker!") writes the second line into the buffer.

display.display() pushes everything from the buffer onto the actual screen. This is the moment the display lights up with your text. Nothing you drew before this line was visible — it was all waiting in the buffer.

The loop() is empty because this is a static display. The text appears once in setup() and stays there. Upload the code, unplug the USB if you like, and the text is still there — powered by the board alone.

One Screen, Many Possibilities

Output:

That is what a dashboard is. Not a fixed message but a live view of your system. And since your NanoMake Pro already has sensors built in — an LDR reading light levels and a potentiometer tracking knob position — you already have live data to display.

 

Project 2: Sensor Dashboard

This project turns the OLED into a live sensor dashboard. It shows the potentiometer value and the LDR light level updating in real time on screen — no Serial Monitor needed. This is what a standalone embedded system looks like.

Code:

#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

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup() {

  display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);

  display.clearDisplay();

  display.display();

}

void loop() {

  int potValue = analogRead(A6);

  int ldrValue = analogRead(A7);

  display.clearDisplay();

  display.setTextSize(1);

  display.setTextColor(SSD1306_WHITE);

  display.setCursor(0, 0);

  display.print("POT : ");

  display.println(potValue);

  display.setCursor(0, 16);

  display.print("LDR : ");

  display.println(ldrValue);

  display.setCursor(0, 40);

  display.print("Light: ");

  if (ldrValue > 600) {

    display.println("BRIGHT");

  } else if (ldrValue > 300) {

    display.println("DIM");

  } else {

    display.println("DARK");

  }

  display.display();

  delay(200);

}

Explanation:

In setup(), display.begin() and display.clearDisplay() initialise the screen and display.display() pushes the blank buffer to screen — starting with a clean display.

Inside loop(), everything redraws every cycle. This is the pattern for any live display — clear, draw, push, repeat.

analogRead(A6) reads the potentiometer. analogRead(A7) reads the LDR. Both give values between 0 and 1023.

display.clearDisplay() wipes the buffer clean at the start of every loop. Without this each new frame would be drawn on top of the previous one and the screen would turn into a mess of overlapping numbers very quickly.

display.setCursor(0, 0) positions the cursor at the very top-left of the screen. display.print("POT : ") writes the label and display.println(potValue) writes the current potentiometer reading right next to it on the same line. println() then moves the cursor to the next line.

display.setCursor(0, 16) moves down 16 pixels for the second row — enough space below the first line of size 1 text. display.print("LDR : ") and display.println(ldrValue) display the light sensor reading the same way.

display.setCursor(0, 40) positions the cursor further down for the status label. The if-else chain reads the LDR value and prints a human-readable description — BRIGHT, DIM, or DARK — instead of just a raw number. This is what makes a dashboard useful. Raw numbers tell you the data. Labels tell you what the data means.

display.display() pushes the completed frame onto the screen. The screen updates all at once — no flicker, no partial drawing.

delay(200) gives a 200 millisecond pause between updates. Updating faster than this makes the numbers change too quickly to read. 200ms feels responsive without being frantic.

Turn the potentiometer and watch the POT number change in real time. Cover the LDR and watch the LDR value drop and the label switch to DARK. This is your NanoMake Pro running as a completely standalone device with its own display. Unplug the USB, power it from a battery, and it still works perfectly.

Output:

Displaying What a Motor Is Doing

A dashboard for sensors is one thing. But what about showing the state of something that is doing work — like a motor?

When you built the motor control projects you used the Serial Monitor to see the speed value. Now you can show that on the OLED instead. The motor speed, the raw potentiometer value, and a simple visual bar — all on one screen — so anyone can see what the system is doing without needing a laptop anywhere near it.

 

Project 3: Motor Speed Display

The potentiometer controls the motor speed and the OLED shows a live readout — the raw knob value, the converted speed, and a progress bar that fills up as the motor runs faster.

Code:


#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


Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);


void setup() {

  pinMode(5, OUTPUT);

  pinMode(6, OUTPUT);

  digitalWrite(6, LOW);

  display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);

  display.clearDisplay();

  display.display();

}


void loop() {

  int potValue = analogRead(A6);

  int speed = map(potValue, 0, 1023, 0, 255);

  int barWidth = map(speed, 0, 255, 0, 118);

  analogWrite(5, speed);

  display.clearDisplay();

  display.setTextColor(SSD1306_WHITE);

  display.setTextSize(1);

  display.setCursor(0, 0);

  display.print("Motor Speed Control");

  display.setTextSize(2);

  display.setCursor(0, 18);

  display.print("Spd: ");

  display.println(speed);

  display.setTextSize(1);

  display.setCursor(0, 45);

  display.print("Knob: ");

  display.print(potValue);

  display.drawRect(5, 54, 118, 8, SSD1306_WHITE);

  display.fillRect(5, 54, barWidth, 8, SSD1306_WHITE);

  display.display();

  delay(100);

}



Explanation:

digitalWrite(6, LOW) in setup() fixes Motor IN2 as LOW throughout the project — locking the direction as forward so only speed changes.

map(potValue, 0, 1023, 0, 255) converts the potentiometer reading into a PWM speed value for the motor. analogWrite(5, speed) sends that PWM signal to the motor driver.

map(speed, 0, 255, 0, 118) converts the speed into a bar width in pixels. The bar is 118 pixels wide at maximum so this maps 255 to 118. Why 118 and not 128? Because the bar starts at x position 5 on screen and the display is 128 pixels wide — leaving 5 pixels of margin on each side gives a width of 118.

display.setTextSize(1) with the title "Motor Speed Control" at the top gives a clear header for the screen.

display.setTextSize(2) for the speed value makes it large and easy to read at a glance — this is the most important number on the screen so it gets the most space.

display.setCursor(0, 45) positions the smaller knob value below the large speed number. Showing both the raw potentiometer reading and the converted speed side by side helps you understand how the conversion works every time you look at it.

display.drawRect(5, 54, 118, 8, SSD1306_WHITE) draws an empty rectangle at the bottom of the screen. This is the outline of the progress bar. The parameters are x position, y position, width, height, and colour.

display.fillRect(5, 54, barWidth, 8, SSD1306_WHITE) fills part of that rectangle with white pixels. The width of the fill is barWidth — which directly tracks the motor speed. At 0 speed the bar is empty. At full speed it fills the entire rectangle. At half speed it fills exactly half.

Turn the knob slowly from one end to the other. Watch the speed number climb, the motor spin faster, and the bar grow across the screen in perfect sync. Everything — the number, the motor, the bar — all responding to the same knob at the same time.

Output:

Click Here

Displays Are Everywhere

Every device with a screen is running some version of what you just built.

The small screen on a microwave shows the timer — that is a display driver receiving numbers from a microcontroller and rendering them as pixels. Same idea as Project 1.

The dashboard in a car showing speed, fuel level, and engine temperature simultaneously — that is a sensor dashboard exactly like Project 2. Multiple sensors, one display, live updates.

Once you understand how a display is driven, you can read any screen around you and roughly understand what is happening inside the device behind it.

Final Thoughts

The OLED display is the moment your NanoMake Pro stops needing a laptop.

Before this blog everything your Arduino communicated went through a USB cable to a Serial Monitor. Now it has its own screen. Its own interface. Its own way of showing you what it is doing and letting you interact with it.

You have printed text, built a live sensor dashboard, showed motor speed with a progress bar, and created a multi-screen navigable UI. Four completely different ways to use the same 128 by 64 pixel display — and each one is the foundation for something bigger.

Keep experimenting, keep building, and keep making.

Back to blog

Leave a comment

Please note, comments need to be approved before they are published.