ESP32CAM with OV5640 Camera Sensor and GC9A01 Display

As requested via the contact page, this is a short write-up on how to interface the AI-Thinker ESP32CAM and 5MP OV5640 camera sensor with a GC9A01 display.

This particular sensor has the 68 degree lens and 21mm long ribbon cable.

This request also spurred the idea for the digital panning experiment, which windows a larger camera frame and uses a joystick to move the window within the camera frame.


Hardware

This setup is an identical copy to the original GC9A01 camera setup, but to assist in the wiring, the connections are as follows.

  • GC9A01 GND -> ESP32-CAM GND
  • GC9A01 VCC -> ESP32-CAM 3.3V
  • GC9A01 SCL-> ESP32-CAM GPIO 14
  • GC9A01 SDA -> ESP32-CAM GPIO 13
  • GC9A01 RES -> ESP32-CAM GPIO 15
  • GC9A01 DC -> ESP32-CAM GPIO 12
  • GC9A01 CS -> ESP32-CAM GPIO 2
  • GC9A01 BLK -> [not connected]

Software – User_Setup.h

When compiling the sketch, the User_Setup.h file in the TFT_eSPI library needs to be the following:

#define USER_SETUP_INFO "User_Setup"
#define GC9A01_DRIVER
#define TFT_SDA_READ      // This option is for ESP32 ONLY, tested with ST7789 and GC9A01 display only
#define TFT_HEIGHT 240 // GC9A01 240 x 240
#define TFT_MOSI 13 // In some display driver board, it might be written as "SDA" and so on.	
#define TFT_SCLK 14
#define TFT_CS   2  // Chip select control pin
#define TFT_DC   12  // Data Command control pin
#define TFT_RST  15  // Reset pin (could connect to Arduino RESET pin)
#define TOUCH_CS 21     // Chip select pin (T_CS) of touch screen
#define LOAD_GLCD   // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2  // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define LOAD_FONT4  // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
#define LOAD_FONT6  // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm
#define LOAD_FONT7  // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:-.
#define LOAD_FONT8  // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.
#define LOAD_GFXFF  // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts
#define SMOOTH_FONT
#define SPI_FREQUENCY  40000000
#define SPI_READ_FREQUENCY  20000000
#define SPI_TOUCH_FREQUENCY  2500000
#define USE_HSPI_PORT
#define SUPPORT_TRANSACTIONS

If you do not get a “loading” screen on your display, then it’s likely that there is an issue with the setup. Reducing the SPI_FREQUENCY to 27000000 can help, but it will negatively impact the refresh rate of your display.


Software – Main Sketch

Again, this uses the same code from the previous experiment, but for ease, it is included in full below.

Please note that this uses the dual core functionality of ESP32-S. Lesser models will likely crap out if they don’t have the second CPU core. The single core code can be found here.

#include "esp_camera.h"
#include <TFT_eSPI.h> // Hardware-specific library
#include <SPI.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

TaskHandle_t Task1;

TFT_eSPI tft = TFT_eSPI();       // Invoke custom library
TFT_eSprite spr = TFT_eSprite(&tft);

camera_config_t config;

uint16_t *scr;
long initalTime = 0;
long frameTime = 1;
volatile bool screenRefreshFlag = true;

/////////////////////////////////
void Task1code( void * pvParameters ) {
  //core0 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_RGB565;
  config.grab_mode = CAMERA_GRAB_LATEST;    //option CAMERA_GRAB_WHEN_EMPTY
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 2;                          //need more than 1 for latest grab

  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

  //core0 loop
  for (;;) {
    //take picture
    camera_fb_t  * fb = NULL;
    fb = esp_camera_fb_get();
    //transfer frame buffer data to pointer
    for (size_t i = 0; i < 57600; i++) {    //240x240px = 57600
      byte first_byte = fb->buf[i * 2];
      byte second_byte = fb->buf[i * 2 + 1];
      scr[i] = (second_byte << 8) + first_byte;
    }
    screenRefreshFlag = true;
    esp_camera_fb_return(fb);   //return the frame buffer back to the driver for reuse
  }
}

//////////////////////////////////////////////////////////
void setup() {

  tft.init();
  tft.setRotation(0);
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_BLACK, TFT_WHITE);
  scr = (uint16_t*)spr.createSprite(240, 240);
  tft.drawString("Loading...", 105, 105, 2);
  
  xTaskCreatePinnedToCore(
    Task1code,   // Task function.
    "Task1",     // name of task.
    100000,      // Stack size of task
    NULL,        // parameter of the task
    1,           // priority of the task
    &Task1,      // Task handle to keep track of created task
    0);          // pin task to core 0

  delay(1000); 
}

//////////////////////////////////
void loop() {
  //refresh display if there is a new image from the camera
  if (screenRefreshFlag) {
    initalTime = millis();
    spr.drawString(String(frameTime), 100, 220, 2); //print frame time in milliseconds
    spr.drawString("ms", 125, 220, 2);
    spr.pushSprite(0, 0);
    screenRefreshFlag = false;
    frameTime = millis() - initalTime;
  }
}

All being well, once the sketch is compiled, uploaded and the device is reset, you should get the following result:


Troubleshooting

While trying this setup I encountered two main problems.

Problem #1 – Brownout

This issue shows itself as a dim display coupled with the RX/TX light on the FTDI flashing. You can confirm that it’s a brownout by opening the serial monitor and you’ll see the below

To solve this I simply held a flying ground pin to the GND/R pin (closest to the LED flash) for about 3 seconds before releasing. I’m guessing that this gives suitable time for the capacitors to charge.

Problem #2 – Thermal Runaway

I’m unsure if this problem is common amoungst all of the OV5640 sensors, but this one got very hot, very fast. The image below shows a 60.5 degree C temperature coming from the sensor.

This will need a heatsink or other form of active cooling to prevent overheating as the datasheet notes that the stable picture temperature maxes out at 50 degrees C.