Displaying ESP32-CAM image to ST7920 12864 LCD Screen

This page will show you how to show a real time image from the ESP32-CAM to the 128×64 pixel LCD with the ST7920 driver.

The big problem with this is converting an 8-bit value (256 greyscale) to a 1-bit value (monochrome). The code below does this by using a simple mid-point threshold, so if a pixel is above the mid-value of 127, the display draws a pixel on the screen.

While the display is 128×64 pixels, the output is actually 120×64 pixels due to pixel binning on a 240pixel camera frame giving a max width of 120 pixels.


Hardware

This requires three main components: the ESP32-CAM, ST7920 display, and an FTDI adapter for programming and serial communications with the computer. There are a couple of passive components, too: a 220ohm resistor for the backlight, and a 330uF capacitor on the 5V power rail.

The circuit connections between the display and the ESP32-CAM are as per the table below:

Pin numberPin nameConnection
1GroundCommon ground
2VCCWill depend on display type.
The above uses 5V.
3V0Not connected
4RSESP32-CAM GPIO 15
(This can be changed in software)
5R/WESP32-CAM GPIO 13
(This can be changed in software)
6EESP32-CAM GPIO 14
(This can be changed in software)
7DB0Not connected
8DB1Not connected
9DB2Not connected
10DB3Not connected
11DB4Not connected
12DB5Not connected
13DB6Not connected
14DB7Not connected
15PSBCommon Ground
16(NC)Not connected
17RSTNot connected
18VOUTNot connected
19BLA5V via a 220ohm resistor.
OR, directly to a 3.3V feed
20BLKCommon Ground

This write-up makes the assumption that you know how to programme the ESP32-CAM already.


Software

The below sketch currently achieves a frame rate of 8.3 FPS. Interesting, the time seems to alternate from 106ms per frame to 134ms per frame, so really the frame rate is between 9.4 – 7.5 FPS. There will likely be inefficiencies.

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

#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

const int clockPin = 14;
const int dataPin = 13;
const int csPin = 15;
uint8_t thresholdValue = 128; //set threshold range 0 - 255

U8G2_ST7920_128X64_F_SW_SPI u8g2(U8G2_R2, clockPin, dataPin, csPin, U8X8_PIN_NONE );

camera_config_t config;

Libraries, Pin Definitions, Constructors and Global Variables.

This uses the U8g2 library for the display, and the standard “esp_camera.h” library.

The pin definitions are based on the AI-Thinker camera board with the OV2640 camera module.

The display uses software SPI on the SD card pins, therefore, you cannot use the SD card at the same time.

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

The important configuration settings to check on this is the config.frame_size and config.pixel_format.

This setup uses the onboard PSRAM, therefore GPIO 16 is off bounds.

void loop() {

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

  for (int y = 0; y < 64; y++) {
    for (int x = 0; x < 120; x++) {
      int framePixel = fb->buf[(((2 * y) + 56) * 240) + (2 * x)];
      if (framePixel > thresholdValue) {
        u8g2.drawPixel(x + 2, y);
      }
    }
  }
  u8g2.sendBuffer();
  esp_camera_fb_return(fb);   //return frame buffer   
  long frameTime = millis() - startTime;
  Serial.println(frameTime);
}

The loop ( )

The loop grabs a camera frame, then runs through 64 rows of 120 pixels, checking if the value is greater than 127. If it does then a pixel at that place is drawn.

Simple.

Simply strap GPIO 0, upload the code, remove the strap and restart. Et voila!


Page created: 21/06/2025
Page last updated: 21/06/2025