Using a SH1106 OLED Screen to Display Live Video from ESP32-CAM

Important note: this setup uses the I2C bus for communication with the display. Therefore, the performance will be limited. If you want fast video in full colour then click here for the tutorial with a GC9A01 colour TFT display.


Hardware

The photo shows the typical breadboard layout for the AI-thinker dev-board using an FTDI adapter to interface with the PC. The connections are better described here

This is the same SH1106 display written about here. However the connections this time are:

  • Display Vcc = ESP32CAM 5V
  • Display GND = ESP32CAM GND
  • Display SDA = ESP32CAM GPIO 15
  • Display SCL = ESP32CAM GPIO 13

The only positive factor of this project is that by utilising the I2C bus, there are spare GPIO pins available for other tasks.

Important note: GPIO 12 cannot be used as part of the I2C bus because the pull-up resistors bugger up the MCU’s ability to flash correctly.


Software

Important Note: there is no down-scaling used in this algorithm, so the display will show the upper-left 128×64 pixels from a 240×240 frame giving the output frame a crooked perspective with a narrow field of view. Oh, and it’s slow – taking about 640ms to complete a single frame.

#include "esp_camera.h"
#include <U8g2lib.h>
#include <Wire.h>

#define SDA_PIN 15
#define SCL_PIN 13

#define CAMERA_MODEL_AI_THINKER
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

The code starts with the normal camera declarations, along with including the necessary libraries.

camera_config_t config;

U8G2_SH1106_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, SCL_PIN, SDA_PIN);

uint8_t thresholdValue = 128; //set threshold range 0 - 255

Constructors and global variable.

void setup() {
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.frame_size = FRAMESIZE_240X240;
  config.pixel_format = PIXFORMAT_GRAYSCALE;
  config.grab_mode = CAMERA_GRAB_LATEST;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 2;

  esp_err_t err = esp_camera_init(&config);

  sensor_t * s = esp_camera_sensor_get();
  s->set_brightness(s, 0);     // -2 to 2
  s->set_contrast(s, 0);       // -2 to 2
  s->set_saturation(s, 0);     // -2 to 2
  s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect, 1 - Negative, 2 - Grayscale, 3 - Red Tint, 4 - Green Tint, 5 - Blue Tint, 6 - Sepia)
  s->set_whitebal(s, 1);       // 0 = disable , 1 = enable
  s->set_awb_gain(s, 1);       // 0 = disable , 1 = enable
  s->set_wb_mode(s, 0);        // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home)
  s->set_exposure_ctrl(s, 1);  // 0 = disable , 1 = enable
  s->set_aec2(s, 0);           // 0 = disable , 1 = enable
  s->set_ae_level(s, 0);       // -2 to 2
  s->set_aec_value(s, 300);    // 0 to 1200
  s->set_gain_ctrl(s, 1);      // 0 = disable , 1 = enable
  s->set_agc_gain(s, 0);       // 0 to 30
  s->set_gainceiling(s, (gainceiling_t)0);  // 0 to 6
  s->set_bpc(s, 0);            // 0 = disable , 1 = enable
  s->set_wpc(s, 1);            // 0 = disable , 1 = enable
  s->set_raw_gma(s, 1);        // 0 = disable , 1 = enable
  s->set_lenc(s, 1);           // 0 = disable , 1 = enable
  s->set_hmirror(s, 0);        // 0 = disable , 1 = enable
  s->set_vflip(s, 0);          // 0 = disable , 1 = enable
  s->set_dcw(s, 1);            // 0 = disable , 1 = enable
  s->set_colorbar(s, 0);       // 0 = disable , 1 = enable

  u8g2.begin();
  u8g2.setDrawColor(1);
  Serial.begin(115200);

}

Setup function. Note that the PIXFORMAT is set to GRAYSCALE to give a 0-255 value.

void loop() {
  long startTime = millis();
  u8g2.clearBuffer();
  camera_fb_t  * fb = NULL;
  fb = esp_camera_fb_get();

  for (int i = 0; i < 8192; i++) {
    int line = floor(i / 128);
    int row = i % 128;
    int framePixel = fb->buf[(line * 240) + row];
    if (framePixel > thresholdValue) {
      u8g2.drawPixel(row, line);
    }
  }

  u8g2.sendBuffer();
  esp_camera_fb_return(fb);   //return the frame buffer back to the driver for reuse
  long frameTime = millis() - startTime;
  Serial.println(frameTime);
}

The loop function is simple.

The camera takes a picture, then the value of the top left 128×64 pixels are checked against a threshold value and will draw a pixel at that point if the threshold is met.

Repeat a further 8191 more times, and then send the buffer to the display.

Simple.


Conclusion

Below is evidence that the setup does work, albeit poorly. I’m not au-fait with the u8g2 library; so there could be great optimisation available.

However, this project was driven more by curiosity, and the piss-poor refresh rate means it’s a non-starter for any future camera projects.

1-bit selfie:

The video shows the abysmal refresh rate:

If I had more time then I would like to implement some sort of dithering algorithm (just to learn about them), as well as better camera pixel sub-sampling to widen the field of view.

I did fuck about with a 1024 byte array and then converting 8 pixel chunks into a single byte. But my programming is janky and the ESP32 just shit a brick.


Page created: 05/03/2025
Last updated: 05/03/2025