Interface any Generic OLED Screen on Arduino

Charlee Li
8 min readJun 23, 2022

--

An OLED screen is a popular solution to add some visual output to your DIY projects. Adafruit provides a line of screen products. They also provide Arduino library for their screens, so basically you don’t have any problem if you purchase their screens.

However, if you ever tried to use “generic” ST7753-based displays from Amazon or eBay with Arduino you may have encountered some weird issues. For example, I tried to draw a red horizontal line on blue background with my generic 0.96in OLED screen, and I got the following issues:

  • Colors are incorrect (blue → cyan, red → yellow)
  • Wrong orientation (horizontal → vertical)
  • Garbage pixels on the bottom edges

In this article I will demonstrate how to solve these issues.

Setup

I used am ESP8266 NodeMCU as the microcontroller. You can use any Arduino-compatible microcontrollers.

The 0.96in OLED screen is connected to the NodeMCU as follows:

  • GND → G (Ground pin)
  • VCC → 3V (3.3V pin)
  • SCL → D5 (Hardware SPI SCL pin)
  • SDA → D7 (Hardware SPI MOSI pin)
  • RES → D2 (GPIO4)
  • DC → D4 (GPIO2)
  • CS → D3 (GPIO0)
  • BLK → 3V through a 820Ω resistor (the value of the resistor does not matter)

If you are confused with the strange pinout of the NodeMCU, refer to the following diagram.

And the test code is simple:

#include <Adafruit_ST7735.h>#define CS 0    // D3
#define DC 2 // D4
#define RES 4 // D2
Adafruit_ST7735 tft = Adafruit_ST7735(CS, DC, RES);void setup() {
tft.initR(INITR_MINI160x80);
tft.fillScreen(ST7735_BLUE);
tft.drawFastHLine(0, 40, tft.width(), ST7735_RED);
}
void loop() {
// put your main code here, to run repeatedly:
}

Note that this code requries the “Adafruit ST7735 and ST7789 Library” so you may need to install it from ToolsLibrary Manager.

Now run this code. You will see the werid display result mentioned above.

Background Knowledge

First, let’s see how the screen is initialized.

In our code, we call tft.initR(INITR_MINI160x80) to initialize the screen. Here the constant INITR_MINI160x80 is the screen type. Take a look at the definition of the Adafruit_ST7735::initR(uint8_options) function (defined in Adafruit_ST7735.cpp, source). Here I omitted the code for other screeen types:

void Adafruit_ST7735::initR(uint8_t options) {
commonInit(Rcmd1);
...
_height = ST7735_TFTWIDTH_80;
_width = ST7735_TFTHEIGHT_160;
displayInit(Rcmd2green160x80);
_colstart = 24;
_rowstart = 0;
...
displayInit(Rcmd3);
...
uint8_t data = 0xC0;
sendCommand(ST77XX_MADCTL, &data, 1);
...
tabcolor = options;
setRotation(0);
}

We can see that the initR function consists of a series of commands for initializing the screen. Here “commands” means the byte sequence for controlling the screen, defined in the datasheet of ST7735 IC.

  • comminInit(Rcmd1): Send some common commands for all types of screens.
  • displayInit(Rcmd2green160x80): Send commands specifically for this 160x80 screen
  • displayInit(Rcmd3) : Send the rest of the common commands.
  • sendCommand(ST77XX_MADCTL, &data, 1); : Send a special command

The function comminInit(src), displayInit (src) accepts a list of commands and send them to ST7735 through SPI protocol. We won’t explain all the commands here. If you are interested, please take a look at the definition of the varaibles Rcmd1, Rcmd2green160x80, and Rcmd3. They are defined here. Also, use the command list in the Section 10 (page 77) of the datasheet for reference. Here is a part of the ST7735 commands.

Now let’s take a look some important commands used in the initR function.

  • The first two commands of Rcmd1ST77XX_SWRESET and ST77XX_SLPOUT reset the screen and bring it out of sleep mode so that the screen can work properly.
  • ST7735_INVCTR 0x07 (src) sets the screen to “No inversion”.
  • Then the two commands in Rcmd2green160x80ST77XX_CASET 0x0000004F and ST77XX_RASET 0x0000009F set the column address and row address.
  • An ad-hoc command in the initR() function — sendCommand(ST77XX_MADCTL, &data, 1) sends ST77XX_MADCTL 0xC0 to the screen, setting the row address order, column address order, whether to switch row/column, and the color mode (RGB or BGR).

Besides there are a couple of internal variables set in the initR() function:

  • _height and _width are the dimension of the screen;
  • _colstart and _rowstart are the offset of the data;
  • and setRotation() invoation sets the screen rotation.

How to Fix the Screen Display

With these knowledge we can try to fix the screen display.

Let’s take a look the issues I encountered:

  • Colors are incorrect (blue → cyan, red → yellow)
  • Wrong orientation (horizontal → vertical)
  • Garbage pixels on the bottom edges

Incorrect colors are possibly due to the wrong inversion setting ( ST77XX_INVCTR ) or wrong color order (RGB vs. BGR, ST77XX_MADCTL ) and can be fixed by sending the correct command again. Wrong orientation can be fixed by calling the setRotation() function with the correct direction. Last, the garbage pixels issue is likely due to wrong offsets, and can be possibly fixed with correct _colstart and _rowstart.

Note that although setRotation() , displayInit(), and sendCommand() functions are public and can be called like tft.setRotation(3), the variables _colstart and _rowstart are protected . So the only way of defining our own offsets is to create a subclass of Adafruit_ST7735.

Let’s start by creating a subclass:

class TFT096: public Adafruit_ST7735 {
public:
TFT096(int8_t cs, int8_t dc, int8_t rst):
Adafruit_ST7736(cs, dc, rst) {}
void initR() {
Adafruit_ST7735::initR(INITR_MINI160x80);
}
};

To use this class, replace the definition for the tft variable:

TFT096 tft = TFT096(CS, DC, RES);void setup() {
// put your setup code here, to run once:
tft.initR();

tft.fillScreen(ST7735_BLUE);
tft.drawFastHLine(0, 40, tft.width(), ST7735_RED);
}

Now we can solve this problem by adding customizing code into the TFT096::initR() method.

First of all, let’s fix the orientation issue. The super class Adafruit_ST7735 has a function setRotation(uint8_t m) which can be used to set the orientation. I tried different m values and found out that 3 is good for my screen. Now the class looks like this:

class TFT096: public Adafruit_ST7735 {
public:
TFT096(int8_t cs, int8_t dc, int8_t rst):
Adafruit_ST7736(cs, dc, rst) {}
void initR() {
Adafruit_ST7735::initR(INITR_MINI160x80);

setRotation(3);
}
};

This line will rotate the screen to the correct direction:

Next, we need to fix the color. According to the datasheet, there are two commands related to the color:

  • INVOFF / INVON, which inverts the whole screen;
  • MADCTL, one bit of which controls the color mode being RGB or BGR.

In our example code, we set the background as ST77XX_BLUE. The color is defined like this (src):

// Some ready-made 16-bit ('565') color settings:
#define ST77XX_BLUE 0x001F

Adafruit’s library initialize the screen in RGB color mode, that makes sense: 0x001F means R=00000, G=000000, B=11111. So, what color mode should my screen be, so that 0x001F means cyan? We can see that, the screen must be inverted and using BGR mode, in order to make 0x001F to be cyan:

0x1F = 00000 000000 11111
invert 11111 111111 00000
BGR BBBBB GGGGGG RRRRR
============================
B = 11111, G = 111111, R = 00000 => CYAN

So we need to set INVON and set color mode to BGR with MADCTL.

Setting INVON is as simple as sending the command with sendCommand function:

sendCommand(ST77XX_INVON, (const uint8_t*)NULL, 0);

How about setting BGR color mode? The MADCTL parameter has multiple options based on the datasheet:

We want to set the RGB bit (D3) to 1 while keeping other bits unchanged. We noticed that the setRotation(3) we called before actually sends a MADCTL command (src):

if ((tabcolor == INITR_BLACKTAB) || (tabcolor == INITR_MINI160x80)) {
madctl = ST77XX_MADCTL_MX | ST77XX_MADCTL_MV | ST77XX_MADCTL_RGB;
} else {
madctl = ST77XX_MADCTL_MX | ST77XX_MADCTL_MV | ST7735_MADCTL_BGR;
}
...
sendCommand(ST77XX_MADCTL, &madctl, 1);

So we can copy this code and change the argument:

uint8_t madctl = 
ST77XX_MADCTL_MX | ST77XX_MADCTL_MV | ST7735_MADCTL_BGR;
sendCommand(ST77XX_MADCTL, &madctl, 1);

Now our initR() function looks like this:

void initR() {
Adafruit_ST7735::initR(INITR_MINI160x80);
setRotation(3);
sendCommand(ST77XX_INVON, (const uint8_t*)NULL, 0);
uint8_t madctl =
ST77XX_MADCTL_MX | ST77XX_MADCTL_MV | ST7735_MADCTL_BGR;
sendCommand(ST77XX_MADCTL, &madctl, 1);
}

The color is correct now:

At last, we need to fix the garbage pixels at the bottom and right edges. These garbage pixels are due to wrong memory addresses. If you take a look at the source of the Adafruit_ST7735::initR() function, you can find code snippts like this:

_colstart = 2;
_rowstart = 1;

These two variables are used to compute the memory address of a given pixel. In other words, these are “offsets” of the pixels. The actual offsets depend on the device vendors, so probably the offsets of our screen do not match what were in this library. So we need to setup correct offsets. After some try and errors, I found the correct offsets for my screen:

_colstart = 26;
_rowstart = 1;

Now the initR() function looks like this:

void initR() {
Adafruit_ST7735::initR(INITR_MINI160x80);
_colstart = 26;
_rowstart = 1;
setRotation(3);
sendCommand(ST77XX_INVON, (const uint8_t*)NULL, 0);
uint8_t madctl =
ST77XX_MADCTL_MX | ST77XX_MADCTL_MV | ST7735_MADCTL_BGR;
sendCommand(ST77XX_MADCTL, &madctl, 1);
}

Now the garbage pixels are removed:

Final code

The complete code for initializing my screen is:

#include <Adafruit_ST7735.h>#define CS 0    // D3
#define DC 2 // D4
#define RES 4 // D2
//#define SDA 13 // D7
//#define SCL 14 // D5
class TFT096: public Adafruit_ST7735 {public:
TFT096(int8_t cs, int8_t dc, int8_t rst): Adafruit_ST7735(cs, dc, rst) {}
void initR() {
Adafruit_ST7735::initR(INITR_MINI160x80);
_colstart = 26;
_rowstart = 1;
setRotation(3);
sendCommand(ST77XX_INVON, (const uint8_t*)NULL, 0);
uint8_t madctl =
ST77XX_MADCTL_MX | ST77XX_MADCTL_MV | ST7735_MADCTL_BGR;
sendCommand(ST77XX_MADCTL, &madctl, 1);
}
};
TFT096 tft = TFT096(CS, DC, RES);void setup() {
// put your setup code here, to run once:
tft.initR();
tft.fillScreen(ST7735_BLUE);
tft.drawFastHLine(0, 40, tft.width(), ST7735_RED);
}
void loop() {
// put your main code here, to run repeatedly:
}

Your screen may not have the same characteristics so this code might not work for you out of the box, but hopefully the approach I used here could show you how to solve similar problems.

--

--

Charlee Li
Charlee Li

Written by Charlee Li

Full stack engineer & Tech writer @ Toronto.

No responses yet