What can you fit on 128x64 pixels ?
Quite a lot.
I have been experimenting with electronics lately. I recently came across ESP32 and its ecosystem. ESP32 is an affordable microcontroller with WiFi and Bluetooth built-in with a very rich variety of modules that you can connect directly using its GPIO pins.
I also came across this 128x64 OLED Display Module that you can connect to ESP32. The small size alone made me wonder what I could possibly do with it? After a bit of research, I came across Adafruit GFX Library for this display. It has all these primitives that you can use to draw shapes and interact with pixels. I had a Canvas on which I could draw visuals with code now!
I decided to make an Interactive Display in which you can switch between multiple visuals using a button press!
In this post, we’ll look at how to connect ESP32 with the display module and how I programmed the Interactive display. Here’s what the final result looks like -
Hello World! - from ESP32
Before jumping into the actual code, let’s look at the ESP32 and how it interfaces with the world. There are many different models of ESP32 based on the form factor as well as the vendors that create them, knowing the model is crucial for developing anything for these microcontrollers.
I am using an ESP32 DevKit V1. It has Bluetooth, WiFi, around 30 GPIO Pins and can be powered with the built-in Micro USB port. It also has indicator LEDs and two buttons, BOOT and RESET.
Now to deploy code on these microcontrollers we have to first compile and build it with the compatible framework and flash or upload the build onto them. After this whenever the microcontroller is powered it runs the code we uploaded.
I am using PlatformIO for setting up the development environment. Check out this guide - it explains about ESP32 as well as how to set up the development environment using PlatformIO.
I have set up my project targeting Espressif ESP32 Dev Module and Arduino Framework using PlatformIO.
Let’s take a look at main.cpp that was generated. The setup() and loop() functions are at the heart of each ESP32 program.
The setup() function runs once when the ESP32 powers on. We can setup initial configs, connect to Internet, perform checks on connected modules etc. in this function.
The loop() function runs infinitely till the ESP32 is powered off. The code written inside this function literally loops.
#include <Arduino.h>
// put function declarations here:
int myFunction(int, int);
void setup() {
// put your setup code here, to run once:
int result = myFunction(2, 3);
}
void loop() {
// put your main code here, to run repeatedly:
}
// put function definitions here:
int myFunction(int x, int y) {
return x + y;
}Let’s update main.cpp to make one of the built-in ESP32’s LED blink. We also set up Serial Connection between ESP32 and use PlatformIO’s Serial monitor to read the data sent back to us from the running code on ESP32 -
#include <Arduino.h>
#define LED_BUILTIN 2
void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(115200);
Serial.println("Hello from ESP32 Setup");
}
void loop()
{
delay(1000);
digitalWrite(LED_BUILTIN, HIGH);
Serial.println("Hello from ESP32 Loop");
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
}Here’s what’s happening in the above code -
We are using ESP32’s built-in LED and configuring its mode as OUTPUT in setup() using pinMode - this will allow us to turn it ON and OFF using digitalWrite in loop()
We are setting up Serial Connection between the connected ESP32 with the USB cable and the PlatormIO Serial Monitor by calling Serial.begin(115200) - the value we pass in is called Monitor Speed or Baud Rate. This value is also set in platformio.ini file which tells the Serial Monitor the rate at which it needs to read data from the connected ESP32. This is often useful for debugging
In the loop() function we use delay to pause execution for 1 second before turning the LED ON or OFF. We also print - Hello from ESP32 Loop - to the Serial Monitor using Serial.println
Now we use PlatformIO’s build and upload the above code to ESP32 -
Once the code is uploaded to the ESP32 we will see a built-in Blinking Blue LED. If we connect the PlarformIO Serial Monitor while the ESP32 is connected with the USB Cable we will see - Hello from ESP32 Setup - once and then - Hello from ESP32 Loop - every two seconds. We can see this in the following demo -
Hello World! - from OLED
The 128x64 OLED Display Module has four Connection Pins - GND and VCC are for powering the OLED and SCL and SDA are for data. The OLED communicates using I2C with ESP32.
The pins are connected in the following order to the ESP32 -
OLED GND is connected to the ESP32 GND Pin
OLED VCC is connected to the ESP32 3.3V Pin
OLED SCL is connected to ESP32 22 Pin
OLED SDA is connected to ESP32 21 Pin
Let’s update main.cpp to display “Hello World” text on the OLED Screen -
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
void setup()
{
Serial.begin(115200);
Wire.begin(21, 22);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C))
{
Serial.println("SSD1306 init failed");
while (true)
;
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Hello World!");
display.display();
}
void loop() { }Here’s what’s happening in the above code -
We import the necessary headers for an I2C connection as well as creating a Display Object
We create a display object using Adafruit_SSD1306 class and pass in the dimensions of the OLED Screen and the Wire Object pointer.
In the setup() function we call Wire.begin(21, 22) to establish an I2C connection. Wire.begin(SDA, SCL) takes the ESP32 Pins that are connected to SCL and SDA Pins of ESP32.
After establishing an I2C connection we call display.begin(VCC value, OLED address) to initialize the display object, so that we can manipulate it using the class functions. We now use display object functions to show - “Hello World!” - text on the OLED.
After uploading the above code onto the ESP32 we can see that “Hello World!” appears on the OLED -
Faces
Now we are going to switch the visuals on the OLED using a button press. To achieve this we are going to do two things - connect a button to ESP32 and introduce an abstract class called Face which will be used to manage different visuals.
We will connect the button to ESP32 18 Pin. We connect one end to this pin and the other to the ESP32 GND Pin.
Let’s update main.cpp to demonstrate the button press -
...
#define SET_BUTTON_PIN 18
...
void setup()
{
...
Serial.begin(115200);
pinMode(SET_BUTTON_PIN, INPUT_PULLUP);
...
}
void loop()
{
int state = digitalRead(SET_BUTTON_PIN);
if (state == LOW)
{
Serial.println("BUTTON PRESSED");
}
else
{
Serial.println("BUTTON NOT PRESSED");
}
}In the above code we are using pinMode() to set up ESP32 18 Pin as an internal pull-up resistor in setup(), we then use digitalRead() in loop() to read ESP32 18 Pin and print on Serial Monitor based on its state. When the state is LOW the button is pressed, by default the state is always HIGH. We can see this happening in the demo below -
Now that we have the ability to detect a button press we will utilize it to switch between different visuals. We are going to add a class called Face. This class will contain only two functions - show() and reset().
Both these functions are going to be pure virtual functions - this will ensure that all derived faces classes that we will create from this class will always implement these. This abstraction lets us create any number of Faces. Each derived face will contain a self contained visual that we can display on the OLED by calling it’s show() function.
Let’s add Face class to main.cpp. We also create a FaceOne derived class which will implement show() and reset(), create a FACE_ONE object using this class and call FACE_ONE→show() in loop() -
...
class Face {
public:
virtual void show() = 0;
virtual void reset() = 0;
virtual ~Face() {}
};
class FaceOne : public Face {
public:
void show() {
display.clearDisplay();
display.setTextSize(3);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("1");
display.display();
}
void reset() {}
};
FaceOne* FACE_ONE = new FaceOne();
void setup()
{
...
}
void loop()
{
FACE_ONE->show();
}Let’s update main.cpp to switch visuals on button press now -
...
#include <map>
class FaceOne : public Face { ... };
class FaceTwo : public Face { ... };
class FaceThree : public Face { ... };
enum FACE_TYPE
{
FACE_ONE,
FACE_TWO,
FACE_THREE
};
FACE_TYPE CURRENT_FACE = FACE_ONE;
std::map<FACE_TYPE, Face *> FACES;
void setup()
{
...
FACES[FACE_TYPE::FACE_ONE] = new FaceOne();
FACES[FACE_TYPE::FACE_TWO] = new FaceTwo();
FACES[FACE_TYPE::FACE_THREE] = new FaceThree();
}
void loop()
{
int state = digitalRead(SET_BUTTON_PIN);
if (state == LOW)
{
CURRENT_FACE = static_cast<FACE_TYPE>(
(static_cast<int>(CURRENT_FACE) + 1) % FACES.size());
if (FACES[CURRENT_FACE])
{
FACES.at(CURRENT_FACE)->reset();
}
delay(300);
}
FACES[CURRENT_FACE]->show();
}Here’s what’s happening in the above code -
We define various Face classes - all derived from Face base class.
We define FACE_TYPE enum to maintain a list of faces, CURRENT_FACE to store the currently selected face and FACES map to store Face object instances keyed by their FACE_TYPE
In the setup() function we initialize the FACES map with all our various faces
In the loop() function we show the currently selected face as well as switch the current face on button press. We also reset the current face during the face switch.
After uploading these changes to ESP32 we can see faces switching whenever we click the button in the demo below -
Binary Clock Face
We will now implement the Binary Clock Face. A Binary Clock displays time in binary format.
Here’s how we can display it on the OLED -
We have 3 rows each representing hour, minute and seconds.
Each row contains 6 squares that can either be filled or outlined - filled representing 1 and outlined representing 0. Each square represent a binary digit in the 6 digit binary number.
Let’s add the BinaryClock face in main.cpp -
...
#include <WiFi.h>
#include <time.h>
#include "secrets.h"
...
const char *ntpServer = "time.google.com";
const long gmtOffset_sec = GMT_OFFSET_SEC;
const int daylightOffset_sec = DAYLGHT_OFFSET_SEC;
struct tm timeInfo;
const char *ssid = WIFI_SSID;
const char *password = WIFI_PASSWORD;
void initWiFi()
{
WiFi.mode(WIFI_MODE_STA);
WiFi.onEvent(
[](WiFiEvent_t event)
{
switch (event)
{
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
Serial.println("WiFi Connected!");
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
Serial.println(WiFi.localIP());
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.println("Time synced!");
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
Serial.println("WiFi Disconnected");
break;
}
});
WiFi.begin(ssid, password);
}
...
FACE_TYPE CURRENT_FACE = FACE_BINARY_CLOCK;
class BinaryClock : public Face
{
public:
void show()
{
if (getLocalTime(&timeInfo, 0))
{
display.clearDisplay();
display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, SSD1306_WHITE);
int hour =
timeInfo.tm_hour;
for (int i = 5; i >= 0; i--)
{
if ((hour >> i) & 1)
{
display.fillRect((5 - i) * 21 + 2, 2, 19, 19, SSD1306_WHITE);
}
else
{
display.drawRect((5 - i) * 21 + 2, 2, 19, 19, SSD1306_WHITE);
}
}
int minute =
timeInfo.tm_min;
for (int i = 5; i >= 0; i--)
{
if ((minute >> i) & 1)
{
display.fillRect((5 - i) * 21 + 2, 22, 19, 19, SSD1306_WHITE);
}
else
{
display.drawRect((5 - i) * 21 + 2, 22, 19, 19, SSD1306_WHITE);
}
}
int second =
timeInfo.tm_sec;
for (int i = 5; i >= 0; i--)
{
if ((second >> i) & 1)
{
display.fillRect((5 - i) * 21 + 2, 42, 19, 19, SSD1306_WHITE);
}
else
{
display.drawRect((5 - i) * 21 + 2, 42, 19, 19, SSD1306_WHITE);
}
}
}
else
{
const char *message = "...";
display.clearDisplay();
display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, SSD1306_WHITE);
display.setTextSize(1);
display.setCursor((SCREEN_WIDTH - strlen(message) * 6) / 2, (SCREEN_HEIGHT - 8) / 2);
display.printf("%s\n", message);
}
display.display();
}
void reset() {}
};
void setup()
{
...
initWiFi();
...
FACES[FACE_TYPE::FACE_BINARY_CLOCK] = new BinaryClock();
}
void loop()
{
...
FACES[CURRENT_FACE]->show();
}Let’s break down what’s happening in the above code
We define initWiFi() - this function connects to the WiFi and configures the time on the ESP32. We use WiFi.onEvent() callback handler to perform actions for different Wifi Status events. Once we are connected to the Internet, we call the configTime() function with appropriate offset and a NTP server. This synchronizes ESP32 clock with the correct time. The Wifi credentials and offset constants are stored in a secrets.h header. We call this function in setup(). You can learn more about how to synchronize time from the following article.
We define BinaryClock class - in the show() function of this class we call getLocalTime() to get time information populated into the timeInfo struct.
We then use timeInfo to get current hour, minute and second values, convert them to binary and display them in the grid using rectangles. We use right-shift bitwise operator(>>) and bitwise AND (&) to get individual bit value of the decimal representation from left to right in the for loop and use it to decide whether to fill the rectangle or not.
After uploading the above code to the ESP32 we will see a binary clock on the OLED -
Wrapping Up
In the end, I moved everything to a perfboard and added some faces (Digital Clock, Lissajous curve, DVD Logo etc) -
I hope you enjoyed reading through this post!
References









