Introduction: Fun With OLED Display and Arduino

About: I'm a software developer interested on robotics and electronics project as a hobby. I really enjoy sharing what I've learned.

I’m pretty sure you’ve definitely heard about OLED display technology. It’s relatively new and offers a better quality than old LCD technology. In this tutorial we want to review the steps required to display data on one of the most common single color OLED display modules available on market. I'll try to explain functionalities provided by corresponding Adafruit library to display data on this module.

Step 1: What OLED Modules Are We Going to Use?

OLED modules are available in wide variety of sizes and features. The one we’re going to use in this tutorial is a mono color 128x64 OLED module. This type of module is available in following sizes (In order that you see on the pictures):

  • 128x64
  • 128x32
  • 96x16
  • 64x48
  • 64x32

As all these modules support I2C protocol as a mean for communication, the code and wiring of all of them is exact same. The only difference is that you have to consider the size of the display on your code so that the contents that you’re going to display, fit properly on it.

Step 2: I2C in a Nutshell

The inter-integrated circuit (IIC) which is normally called I2C (I squared C) developed by Philips on 80s as a data exchange bus used to transfer data between the central processing unit (CPU) or microcontroller unit (MCU) of a device and peripheral chips. It was basically targeted for TV application. Due to its simplicity, it became so popular that after awhile it became one of the primary mechanisms of data transfer for CPUs and MCUs and peripheral devices that are not necessary part of the same PCB board and are connected to it via wire (e.g. sensors, display modules, etc.).

I2C consists of a communication bus made of two wire that supports bidirectional data transfer between a master and several slave devices. Typically the master node is in charge of controlling the bus – which is actually done by generating a synchronization signal on the serial clock line (SCL) . It’s a signal that would be sent continuously by master during the transfer and all other nodes connected to the bus will use it to sync their communication and detect the speed of the bus. Data is transferred between the master and slave through a serial data (SDA) line. The transmission speed can be up to 3.4 Mbps. All devices that want to transfer data via I2C should have a unique address and can operate as either transmitter or receiver depending on the function of the device. For example an OLED display module is a receiver which accepts some data and displays them, while a temperature sensor is a transceiver that sends captured temperature via I2C bus. Normally a master device is the device that initiates a data transfer on the bus and generates the clock signals to permit the transfer. During that transfer, any device addressed by this master is considered a slave and reads that data.

When a node wants to send some data, the very first byte of the data should be the address of the receiver and then actual data comes afterwards. This means that in order to send a data to an output device using I2C (e.g. I2C OLED display module) we should first find its I2C address and this is what we’ll do first on next steps.

If you are interested to know more about the details and theories about I2C bus, you can use following references:

http://www.i2c-bus.org

https://learn.sparkfun.com/tutorials/i2c

Step 3: Required Modules and Components

Here you can find the list of components that you would need to complete this tutorial:

eBay links:

Amazon.com links:

Step 4: Wiring OLED Display Module to Arduino

An important note about I2C enabled devices is that the way you should connect them to Arduino are all the same. This is because Arduino runs its I2C communication only on specific pins. In this tutorial I’m using Arduino Uno. The Arduino Uno uses pin A5 as SCK and A4 as SDA. So we can connect the OLED display module to Arduino Uno as shown in the schematic view. As you may notice in the picture I’ve taken from my OLED display module, the connector for VCC and GND are different than the schematic view. Remember to check the labels of the pins on your modules to make sure you’re connecting it in a right way.

We need only 4 pins that should be connected as below:

Arduino VCC -> OLED Module VCC

Arduino GND -> OLED Module GND

Arduino 4 -> OLED Module SDA

Arduino 5 -> OLED Module SCK

Step 5: Finding the Address of the Display Module

As a first step on connecting to an I2C enabled device, you need to have the address of the module. In order to do so, after wiring up the module to your Arduino, you should just upload the code attached, onto your Arduino. This code incorporates the Wire library which is a library included with Arduino IDE that handles I2C communication. It tries to scan connected I2C devices and sends their address via serial port to your computer. So you can access its output via Serial Monitor tool in Arduino IDE. The original version is available at Arduino Playground). Also you can view it in a more readable way in my online Arduino Editor. Don't expect anything to be displayed on the screen while this code is running.

As you can see on the picture, my module is binded to address 0x3C. Normally all the devices in a specific product line (for example all 128x64 OLED modules)have same address.

The addresse of I2C devices are limited from 1 to 126. This code simply tries to connect to each device in order (without transmitting any data) and then check if there was any error reported by the underlying library on connecting to the provided address. If there is no error, then prints the address as an available module to connect. Also it should be noted that the first 15 addresses are reserved, so it jumps over them and just prints those above this range. Remember that the address of these I2C modules are hard-coded on the device and it can not be changed. So it would be a good idea to write it down somewhere or put a label on the module when you’re going to put it back on your lab shelf so that the next time, running the scanner code would not be necessary. However it’s not a complicated procedure ;)

Step 6: Installing Libraries Required to Display Data on OLED Module

The Wire library can handle low level communication with I2C devices. When you want to connect to a specific device in order to read/write data from/to it, normally you would use a library provided by the company who has originally built that module. This library handles all I2C communication details with given module and let us concentrate more on our business which in this case is displaying the data in the way we want.

Adafruit, the company who manufactures the original version of such display modules, provides a libraries called Adafruit SSD1306 to display data on these monochrome displays. So before start coding, we’ve to install this library via Library Manager (accessible via Sketch > Include Library > Manage Libraries... menu) in Arduino IDE. There is also another library called Adafruit GFX Library which handles more low level graphical stuff and is used internally by Adafruit SSD1306. You need to have both of them installed on your Arduino IDE as you can see on the pictures.

Step 7: Initializing the Display Module

Drawing on display module is wrapped in a class named Adafruit_SSD1306. The definition of this class is on Adafruit library, so we need to first include that library. Then we have to instantiate an instance of this class first. The constructor of this class takes the port number at which the display could be reset which is pin 4 (connected to SCK). This part of the code should be located at the beginning of the file (out of setup() and loop() functions).

#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 display(4);

Now inside the setup() function we should call the begin function of the display object by passing our I2C address as below (the SSD1306_SWITCHCAPVCC is a constant value specifying the type of power source to the library):

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

void loop() {} // loop can be empty for now

Now the display object is ready and we can call its functions (e.g. display.write(), display.drawLine, etc.). The important note is that whenever we draw something on by calling on our display object, we need to call the display.display() function to make the actual drawing happen on the hardware level. This is mainly due to the fact that the drawing functions that we call, just update the "in memory" representation of the display for performance reasons. It actually caches the changes in memory. So we should always remember to call the display() function when we finished drawing something on the screen.

display.write(...);    // keeps updating in memory
display.drawLine(...); // keeps updating in memory
.
display.display();     // flushes all changes to the display hardware

If you try to upload your code in this step, you'll notice that the Adafruit Industries logo would be displayed. You may wonder who has asked it to draw that! Actually this is what the Adafruit library does. It initializes the memory of the module (the in memory representation of the display hardware) with the logo of this company. If you don't want to see that during the initialization, you can try to call display.clearDisplay() function right before calling display.display() in your setup function. This function, as its name suggests, clears the display completely.

#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 display(4);

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

void loop() {
}

Based on documentation of Adafruit_SSD1306 library, you can use different functions provided by this class to draw on display or directly manipulate the pixels on it. In next sections we'll try to present an example for each one of them so that you can have an idea about the way it works. Most of these examples will display just a simple static contents, so we can just put them inside our setup() function (after the initialization code). By doing so it would be run only once and remains there.

Step 8: Display a Simple Text

To display a text, we can use the simple display.println() function of the library. It accepts the text as an string and tries to display it. It's important to know that we have to tell the library where on the display we're going to present the text. Every pixel on the display has a coordinate that is specified with a X and Y. The X increases from left to right and Y increases from top to the bottom. The upper left corner of the screen is (X=0, Y=0) and the lower right corner is (X=127, Y=63). I noted the coordinates of the corners on the first picture. We can use the display.setCursor() function to specify where on the display we're going to display the text.

Another property of the text is its color. We can specify the color using display.setTextColor() as displayed on following sample.

display.clearDisplay();

display.setTextColor(WHITE);
display.setCursor(35,30);
display.println("Hello World!");
 
display.display();

We can also use the display.write() function to display a single character. It accepts a character code as an uint8_t type and displays the character corresponding to that code on the string. As an example, if we want to display the same string using this function, we can use the following snippet:

display.clearDisplay();
display.setTextColor(WHITE);
display.setCursor(35,30);

display.write(72);
display.write(101);
display.write(108);
display.write(108);
display.write(111);
display.write(32);
display.write(87);
display.write(111);
display.write(114);
display.write(108);
display.write(100);
display.write(33);

display.display();

It's also possible to draw texts in black color with a white background. In order to do so, you have to call the display.setTextColor function as below:

display.clearDisplay();

// Sets the color to black with a white background
display.setTextColor(BLACK, WHITE);
display.setCursor(25,30);
display.println("Inverted text!");
 
display.display();

You also have the option of setting the size of the text using display.setTextSize() function. It accepts an integer number as a size. The greater the number, the bigger the text would be. Smallest size is 1 which is the default size of texts. Following code tries to write the letter "A" in 6 different sizes:

display.clearDisplay();


display.setTextColor(WHITE);
display.setCursor(0,0);
display.setTextSize(1);
display.print("A");
display.setTextSize(2);
display.print("A");
display.setTextSize(3);
display.print("A");
display.setTextSize(4);
display.print("A");
display.setTextSize(5);
display.print("A");
display.setTextSize(6);
display.print("A");
 
display.display();

Step 9: Drawing Basic Shapes

Drawing basic shapes like rectangle, circle, triangle, line or point are very easy and there is a dedicated function for each one.


Drawing line

To draw a line you can call display.drawLine(startX, startY, endX, endY, color). For example following code draws a diagonal lines in the screen so that they shape a big X:

display.clearDisplay();

display.drawLine(0,0,display.width() - 1, display.height() - 1, WHITE);
display.drawLine(display.width() - 1,0,0, display.height() - 1, WHITE);

display.display();

You can access the width and height of the display using display.width() and display.height() functions. By doing so your code would be independent from the screen size.


Drawing rectangle

The function to draw a rectangle is display.drawRect(upperLeftX, upperLeftY, width, height, color). Here is the code that draws three rectangle on some random places:

display.clearDisplay();

display.drawRect(100, 10, 20, 20, WHITE);
display.fillRect(10, 10, 45, 15, WHITE);
display.drawRoundRect(60, 20, 35, 35, 8, WHITE);

display.display();

By calling display.fillRect(upperLeftX, upperLeftY, width, height, WHITE) you can draw a rectangle filled by specified color. Also the third function in this example is display.drawRoundRect(upperLeftX, upperLeftY, width, height, cornerRadius, color) that as you can see in the picture is used to draw a rectangle with round corners. It accepts an extra parameter before color that is an integer number indicating the corner radius. The bigger the value the rounder the corner. It also has a corresponding fill function named display.drawFillRoundRect that I think you can guess what it does.


Drawing circle

The function is display.drawCircle(centerX, centerY, radius, color). Here is an example which draws a smiley-like shape:

display.drawCircle(60, 30, 30, WHITE);
display.fillCircle(50, 20, 5, WHITE);
display.fillCircle(70, 20, 5, WHITE);

Like rectangles, you can use the display.fillCircle function to draw a circle filled with the given color.


Drawing triangle

Ahh, again a function called display.drawTriangle(poin1X, point1Y, point2X, point2Y, point3X, point3Y, color) and corresponding display.fillTriangle that draw a filled triangle.

display.drawTriangle(24, 1, 3, 55, 45, 55, WHITE);
display.fillTriangle(104, 62, 125, 9, 83, 9, WHITE);


Draw a point

You can also color a specific point (which is called pixel) on the screen via display.drawPixel(pixelX, pixelY, color) function.

display.drawPixel(20, 35, WHITE);
display.drawPixel(45, 12, WHITE);
display.drawPixel(120, 59, WHITE);
display.drawPixel(97, 20, WHITE);
display.drawPixel(35, 36, WHITE);
display.drawPixel(72, 19, WHITE);
display.drawPixel(90, 7, WHITE);
display.drawPixel(11, 29, WHITE);
display.drawPixel(57, 42, WHITE);
display.drawPixel(69, 34,  WHITE);
display.drawPixel(108, 12, WHITE);

Step 10: Drawing Image

Drawing an image is different and a bit complicated. As the display module is monocolour, we need to first convert our image to a format called mono color bitmap (also called black and white). In such a format, each pixel of the image is presented with either 0 or 1. The 1s represents the existence of the color and 0s means an empty space. You can see an example of the Arduino logo in this format on top of this section. The function to draw a bitmap image is display.drawBitmap(topLeftX, topLeftY, imageData, width, height, color). The imageData parameter is an array of numbers in bytes. Each byte has 8 bits, so each byte contains the data of 8 pixels of the image. By specifying the width and height of the image, the drawBitmap function will know from which bit the next row of pixels begins.

The solution that I chose to convert my image to this format was to first use one of online "image to ASCII converters" (e.g. http://my.asciiart.club) to convert my picture to a set of ASCII characters and then replace the characters used for empty space by 0 and other ones by 1. That's what you see below. You can think of each 0 and 1 as a pixel on the display. So the size of the picture should not exceed our display size which is 128x64.

Note: Using this ASCII technique is not a recommended approach because due to the aspect ratio of characters your image will be deformed (characters are not a square). I tried this technique just because it make it easier to convert the image to required format. Otherwise it would be possible to achieve the best result via some programming or using some utility applications which are completely out of the scope of this text.

0000000000000000000001111111111111111111111000000000000000000000
0000000000000000011111111111111111111111111111100000000000000000 0000000000000111111111111111111111111111111111111110000000000000 0000000000011111111111111111111111111111111111111111100000000000 0000000001111111111111111111111111111111111111111111111000000000 0000000111111111111111111111111111111111111111111111111110000000 0000011111111111111111111111111111111111111111111111111111100000 0000111111111111111111111111111111111111111111111111111111110000 0001111111111111111111111111111111111111111111111111111111111000 0011111111111111111111111111111111111111111111111111111111111100 0111111111111111000000011111111111111111100000001111111111111110 0111111111110000000000000001111111111000000000000000111111111110 1111111111000000001111000000001111000000001111000000001111111111 1111111110000011111111111100000110000011111111111100000111111111 1111111100000111111111111111000000001111111001111110000011111111 1111111100001111100000011111100000011111100000011111000011111111 1111111100001111100000011111100000011111100000011111000011111111 1111111100000111111111111111000000001111111001111110000011111111 1111111110000011111111111100000110000011111111111100000111111111 1111111111000000001111000000001111000000001111100000001111111111 0111111111110000000000000000111111110000000000000000111111111110 0111111111111111000000001111111111111111000000001111111111111110 0011111111111111111111111111111111111111111111111111111111111100 0001111111111111111111111111111111111111111111111111111111111000 0000111111111111111111111111111111111111111111111111111111110000 0000011111111111111111111111111111111111111111111111111111100000 0000000111111111111111111111111111111111111111111111111110000000 0000000011111111111111111111111111111111111111111111111100000000 0000000000011111111111111111111111111111111111111111100000000000 0000000000000111111111111111111111111111111111111110000000000000 0000000000000000111111111111111111111111111111110000000000000000 0000000000000000000001111111111111111111111000000000000000000000

Now we should divide each line by 8, representing a byte and store them in an array as below:

static const unsigned char PROGMEM arduino_logo[] ={
    B00000000, B00000000, B00000111, B11111111,
    B11111111, B11100000, B00000000, B00000000,
    B00000000, B00000000, B01111111, B11111111, 
    B11111111, B11111110, B00000000, B00000000,
    ... // continue up to the end of the picture
};

Then we can draw it on display by calling the drawBitmap function.

display.drawBitmap(32,16,arduino_logo, 64, 32, WHITE);

Step 11: Troubleshooting

This was a long tutorial and so it's highly probable that something goes wrong. Here is the list of some common errors that you may encounter while setting up OLED display module for your project (some of them happened for me while preparing this tutorial).

Nothing is being displayed at all

This could happen for many reasons so I suggest to check following list which is in order that may occur in your project:

  • I2C address is probably wrong

Make sure that you've set the address you got in i2c-scanner code in display.begin() function when setting up your display object.

  • SCL and SDA are connected in a wrong way

This actually happened for me. If you're using Arduino Uno then you have to check your connections again to make sure they're connected same as mine. If you're using another Arduino edition (e.g. Mega, Leonardo, etc.), you have to know that they may have their I2C set to other pins. You can check it at documentation of Wire library.

  • You’re drawing something out of the visible area

This is a software issue. It's very common when using a drawing functions to miscalculate some coordinates and so your drawing would be deformed or in worst scenario it may be completely out of the scene. Review your calculations and try to do a step by step drawing to see what's going on.


Text are not being displayed at all

  • You forgot to set the color of text or you've set it to a wrong value

You need to call setTextColor before drawing texts. Otherwise you got no errors, but you'll see nothing on display. Also you may have set the text color same as the background color.

  • You’re using a very large font

If you set the text size to a very large value, it could be possible that characters get completely out of the visible area.


There is a compile error about display size

This also happened for me and I think it would happen for most of you. It's because of the display size constant values that are defined inside the header file Adafruit_SSD1306.h that we include on top of our script. This file is located at {your-project-folder}\libraries\Adafruit_SSD1306\Adafruit_SSD1306.h. If you open this file you would notice that there is a comment section as below in which it describes that you need to uncomment only the constant that represents your OLED display module size. For a 128x64 display modules, the line #define SSD1306_128_64 should be uncommented.

/*=====================================================================

    SSD1306 Displays
 ----------------------------------------------------------------------
    The driver is used in multiple displays (128x64, 128x32, etc.).
    Select the appropriate display below to create an appropriately
    sized framebuffer, etc.

    SSD1306_128_64  128x64 pixel display
    SSD1306_128_32  128x32 pixel display
    SSD1306_96_16

 -----------------------------------------------------------------------*/
   #define SSD1306_128_64
//   #define SSD1306_128_32
//   #define SSD1306_96_16
/*======================================================================*/

Step 12: What to Do Next?

The OLED display as an output module can give you a great opportunity to provide a professional looking interface to your hobby projects. You can try following ideas as an starting point to display a meaningful data on it or help user to know what's going on or if he/she needed to do something. It would be much clearer for a user to read a message on a display than to interpret the state of a project/device via some LEDs.

What you can do as a starting point could be:

  • Read a temperature sensor value and display it on OLED module. You can add pressure or humidity sensor to it and create a fully functional weather station project.
  • Try to draw something on the display module using a joystick module as an input device.
  • Try to draw an animation on the display by a sequence of drawing/delaying function calls or Arduino interrupt
  • Display your custom logo on your system startup (instead of Adafruit Logo)

Don't forget to tell me on comments, what would you do (or you've already done) using OLED display module.