ESP32 fast drawing image on GC901A

Hi!
Is there any way to drawing images faster on ESP32?
I'm using Bodmer's TFT_eSPI and JpegDecoder lib. I have 17 pieces of 144x144 pixels jpeg files (cca. 140 kB) on SPIFFS which I want to display. Using a 3-phase encoder to select images, and it needs to be constantly poll to doesn't miss a step. The image drawing from SPIFFS takes a relatively long time, so I want to speed it up a bit. I'm tried to set SPI frequency to 40 and 80 MHz. Sometimes it works, but not very stable.
Could the jpeg image be decoded to RAM during setup and sent to the display somehow, maybe with DMA?
Any advice would be appreciated.

Here is my full code, it's not yet final:

// smartOven for esp32 with gc9a01 display
#include <TFT_eSPI.h>           // Hardware-specific library
#include <FS.h>
#include <SPI.h>
#include <WiFi.h>
#include "time.h"
#include "SPIFFS.h"
#include <JPEGDecoder.h>        // JPEG decoder library
#include <Free_Fonts.h>
#include <freeserifbold_48pt.h>     // only numerics and : 

#define GFXFF 1
#define FSB48 &freeserifbold_48pt

// Port definitions 
//#define TFT_CS 22               // Display port setting in TFT_eSPI User_settings.h
                                // Encoder
#define ENCODER_A 17
#define ENCODER_B 16
#define ENCODER_C 4
#define ENCODER_SW 0
                                // Relays
#define RLY_UPPER 13              // Upper heater
#define RLY_LOWER 12              // Lower heater
#define RLY_GRILL 14              // Grill heater
#define RLY_FAN 27                // Fan
#define RLY_LIGHT 26              // Light

#define MAX6675_SCK 32
#define MAX6675_CS 33
#define MAX6675_SO 35

TFT_eSPI tft = TFT_eSPI();

// Global variables
char currentTime[6] = "18:56";
char previousTime[6] = "";

const int menuCount = 18;
const int lineLen = 30;

char captions[menuCount][10];     // Captions for menu items (max 9chr long captions)
int shelves[menuCount];            // You can specify on which shelf the food should be placed
int times_[menuCount];            // How many minutes to cook
int temps[menuCount];             // Target temperature
byte funcs[menuCount];            // Which relays are activated during operation (it's a hexa code)

int currentMenu = 0;
int previousMenu = 0;
int level = 0;                    //0-main menu, 1-set time, 2-set temp, 3-operate

int Timer = -1;
int Temperature = -1;
unsigned long timeToShowTimer = 0;
unsigned long timeToShowTic = 0;
unsigned long timeToUpdateClock = 0;
unsigned long timeToShowTemp = 20000;
int lastMinute = 60;
bool tic;

int lastState = HIGH;             // the previous state from the switch pin
int currentState;                 // the current reading from the switch pin
unsigned long pressedTime  = 0;
unsigned long releasedTime = 0;
const int LONG_PRESS_TIME  = 500; // 500 milliseconds

unsigned int colorBackground = TFT_BLACK;
unsigned int colorText = TFT_WHITE;

int encoder = 0;
int encoderMax = 0;
int lastValue = 0;
int lastFont = 0;

int currentTemp = 0;
// #########################################################################
//  setup
// #########################################################################
void setup(void) {

  // setup ports
  pinMode(ENCODER_A, INPUT_PULLUP);
  pinMode(ENCODER_B, INPUT_PULLUP);
  pinMode(ENCODER_C, INPUT_PULLUP);
//  attachInterrupt(digitalPinToInterrupt(ENCODER_A), getEncoder, FALLING);
//  attachInterrupt(digitalPinToInterrupt(ENCODER_B), getEncoder, FALLING);
//  attachInterrupt(digitalPinToInterrupt(ENCODER_C), getEncoder, FALLING);

  pinMode(2,OUTPUT);
  digitalWrite(2,LOW);                //temporary ground for switch
  pinMode(ENCODER_SW, INPUT_PULLUP);
  pinMode(RLY_UPPER, OUTPUT);
  pinMode(RLY_LOWER, OUTPUT);
  pinMode(RLY_GRILL, OUTPUT);
  pinMode(RLY_FAN, OUTPUT);
  pinMode(RLY_LIGHT, OUTPUT);

  Serial.begin(9600);                       // serial for debugging
  WiFi.begin("fym", "WeiszPatrik21", 6);         // WiFi settings
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
  }
  configTime(3600, 3600, "pool.ntp.org");   // NTP for clock
  getTime();

  if (!SPIFFS.begin()) {
    Serial.println("SPIFFS initialisation failed!");
    while (1);                              // SPIFFS initialisation failed so wait here
  }

  tft.begin();                              // Display setup
  tft.setRotation(0);
  tft.fillScreen(colorBackground);
  tft.setTextColor(colorText,colorBackground);  
//  tft.setFreeFont(FSB12);

  csv2array("/data.csv");                   // convert data to arrays
  delay(500);
}

#define needShowCaption(item) ((funcs[item] & 0b10000000) > 0)
#define needSetTime(item) ((funcs[item] & 0b01000000) > 0)
#define needSetTemp(item) ((funcs[item] & 0b00100000) > 0)
// #########################################################################
// loop
// #########################################################################
void loop() {
  int idx = currentMenu - 1;
  buttonState();
  switch( level) {
    case 0:                               //Menu select
      previousMenu = currentMenu;
      currentMenu = encoder;
      encoderMax = menuCount - 1;
      showMenu();
      break;
    case 1:                               //Set timer
      encoderMax = -60;                      //max 5 hours
      if (shelves[idx] > 0) {
        drawShelf(shelves[idx] - 1);
      }
      if (Timer == -1) {
        Timer = times_[idx];
        encoder = Timer / 5;
      }
      if (needSetTime(idx)) {
        Timer = encoder * 5;
        timeRing(Timer);
      } else {
        level++;
      }
      break;
    case 2:                               //Set temperature
      encoderMax = -25;
      if (Temperature == -1) {
        Temperature = temps[idx];
        encoder = Temperature / 10 ;
      }
      if (needSetTime(idx)) {
        Temperature = encoder * 10;
        tempRing(Temperature);
      } else {
        level++;
      }
      break;
    case 3:                               //operate
      if (timeToShowTimer == 0) {
        TurnOn(funcs[idx]);
      }
      ShowTimer();
      CheckTemperature(Temperature);
      break;
  }
  getEncoder();
//  CheckTemperature(Temperature);

}
// ------------------------------- button ---------------------------------
void buttonState() {
  currentState = digitalRead(ENCODER_SW);
  if(lastState == HIGH && currentState == LOW) {      // button is pressed
    pressedTime = millis();
  } else { 
    if(lastState == LOW && currentState == HIGH) {    // button is released
      releasedTime = millis();
      long pressDuration = releasedTime - pressedTime;
      if ( pressDuration > 20 ) {
        switch(level) {
          case 0:
            tft.fillScreen(colorBackground);
            drawCenter(captions[currentMenu - 1], 120, 40, 1);
            break;
          case 1:
            Timer = encoder * 5;
            break;
          case 2:
            Temperature = encoder * 10;
            break;
        }
        if( pressDuration < LONG_PRESS_TIME ) {
          level++;
        } else {
          level--;
        }
        encoder = 0;
        clearRing();
      }
    }
  }
  lastState = currentState;
}

// ------------------------------- menu -----------------------------------
void showMenu() {

  if (currentMenu == 0) {
    if (previousMenu != 0) {
      strcpy(previousTime,"");                // We want to show clock
      tft.fillScreen(colorBackground);
    }
    showClock();
  } else {
    if (currentMenu != previousMenu) {
//      tft.fillRect(48,40,144,8,colorBackground);     // Clear previous caption
//      tft.fillScreen(colorBackground);
      char file[8];
      sprintf(file,"/%d.jpg",currentMenu);
      drawJpeg(file,48,48);
      if (needShowCaption(currentMenu - 1)) {   // show caption
        drawCenter(captions[currentMenu - 1], 120, 40, 1);
      }
      menuRing(currentMenu);
    }
  }
}
// ----------------------------- drawCenter ----------------------------------
void drawCenter(const char *buf, int x, int y, int font)
{
  if ( lastFont != font ) {
    lastFont = font;
    switch( font ) {
      case 1:
        tft.setFreeFont(FSB12);
        break;
      case 2:
        tft.setFreeFont(FSB24);
        break;
      case 3:
        tft.setFreeFont(FSB48);
        break;
    }
  }
  tft.setTextDatum(TC_DATUM);
  int width = tft.textWidth(buf);
  int height = tft.fontHeight();
  tft.fillRect(x + width / 2, y, 2, height / 2 + 6, colorBackground); //clear the dust
  tft.drawString(buf, x, y,GFXFF);
}

// ------------------------------- clock -----------------------------------
void showClock() {
  if ( millis() > timeToUpdateClock ) {
    getTime();
    timeToUpdateClock = millis() + 60000;             //one minute
  }
  if (strcmp(currentTime, previousTime) != 0 ) {
      strcpy(previousTime, currentTime);
      if (currentMenu != previousMenu) {
        tft.fillScreen(colorBackground);
      }
      //tft.drawFastHLine(0,120,240,TFT_RED);   //center lines helps for alignment
      //tft.drawFastVLine(120,0,240,TFT_RED);
      //tft.drawCircle(120,120,120,TFT_RED);
      //tft.drawRect(0,80,240,72,TFT_RED);
      drawCenter(currentTime, 120, 84, 3);
  }
}
// ------------------------------ getTime ----------------------------------
void getTime()
{
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo)){
    Serial.println("Failed to obtain time");
    return;
  }
  strftime(currentTime, 6, "%H:%M", &timeinfo);
  return;
}
// ------------------------------ encoder ----------------------------------
void getEncoder() {
  int8_t lookup[3][8] = {
    {  0,  0,  0,  1,  0, -1,  0,  0},
    {  0,  0,  0,  0,  0,  1, -1,  0},
    {  0,  0,  0, -1,  0,  0,  1,  0}
  };

  uint8_t pins = digitalRead(ENCODER_A) | digitalRead(ENCODER_B)  << 1 | digitalRead(ENCODER_C)  << 2;
  encoder += lookup[encoder % 3][pins];
  encoder = encoder % 120;
  if ( encoderMax > 0) {
    if ( encoder < 0 ) {
      encoder = encoderMax;
    }
    if ( encoder > encoderMax ) {
      encoder = 0;
    }
  } else {                          //negative encoderMax means it's not circular
    encoderMax *= -1;
    if ( encoder < 0 ) {
      encoder = 0;
    }
    if ( encoder > encoderMax ) {
      encoder = encoderMax;
    }
  }
}

// ------------------------------ ringMeter ----------------------------------
void menuRing(int value) {
  ringMeter(value,0,18,0);
}
void timeRing(int value) {
  ringMeter(value,0,60,1);
}
void tempRing(int value) {
  ringMeter(value,50,250,2);
}
void clearRing() {
  tft.drawArc(120,120,122,108,0,360,colorBackground,colorBackground,false);
}
void ringMeter(int value, int vmin, int vmax, byte type)
{
  int x = 0;          // X coordinate
  int y = 0;          // Y coordinate
  int r = 120;        // radius
  int w = 10;         // width;
  int scheme;         // color
  byte seg = 5;       // Segments are 5 degrees wide = 60 segments for 300 degrees
  byte inc = 5;       // Draw segments every 5 degrees, increase to 10 for segmented ring
  int angle = 180;    // full circle
  bool top = true;    // start from top
  bool slice = false; // only slice has been drawed
  int fontSize = 2;   // size of font
  int v;
  int initFor;
  int endFor;
  char buffer[5] = "    ";
  int textX = 120;
  int textY = 0;
  int h = int(value / 60);
  int m = value - h * 60;
  //int over = 0;
  int blank;

  x += r; y += r;   // Calculate coords of centre of ring

  switch (type) {
      case 0:                 //menuRing
        scheme = 1;
        slice = true;
        lastValue = value;    //Don't want to show value
        break;
      case 1:                 //timeRing
        scheme = 2;
        seg = 5;
        inc = 6;
        sprintf(buffer, "%d:%02d", h, m);
        textY = 84;
        fontSize = 3;
        if (value < 0) value = 0;
        value -= vmax * h;
        break;
      case 2:                 //tempRing
        angle = 150;
        scheme = 4; 
        top = false;
//        dtostrf(value, 3, 0, buffer);
        sprintf(buffer, "%03d", value);
        textY = 180;
        if (value > vmax) value = vmax;
        if (value < vmin) value = vmin;
        break;
  }

  if (value != lastValue) {
    lastValue = value;
    drawCenter(buffer, textX, textY, fontSize);
  }

  if (top) {
    v = map(value, vmin, vmax, 0, angle*2);
    initFor = 0;
    endFor = angle * 2;
  } else {
    v = map(value, vmin, vmax, -angle, angle); // Map the value to an angle v
    initFor = -angle;
    endFor = angle;
  }

  
  for (int i = initFor; i < endFor; i += inc) {   // Draw colour blocks every inc degrees
    int colour = 0;
    switch (scheme) {
      case 0: colour = TFT_RED; break; // Fixed colour
      case 1: colour = TFT_GREEN; break; // Fixed colour
      case 2: colour = TFT_BLUE; break; // Fixed colour
      case 3: colour = rainbow(map(i, -angle, angle, 0, 127)); break; // Full spectrum blue to red
      case 4: colour = rainbow(map(i, -angle, angle, 85, 127)); break; // Green to red (high temperature etc)
      case 5: colour = rainbow(map(i, -angle, angle, 127, 63)); break; // Red to green (low battery etc)
      default: colour = colorBackground; break; // This will clear the display
    }

    int shadesOfBlue[] = {0x0006, 0x001a, 0x421a, 0x841a, 0xc61f, 0xffff};
    if (type == 1) {                    // in time settings we have different
                                        // colours for segments depending hours
      blank = shadesOfBlue[h];
      colour = shadesOfBlue[h+1];
    } else {
      blank = colour & 0b0001100001100011;
    }
    // Calculate pair of coordinates for segment start
    float sx = cos((i - 90) * 0.0174532925);
    float sy = sin((i - 90) * 0.0174532925);
    uint16_t x0 = sx * (r - w) + x;
    uint16_t y0 = sy * (r - w) + y;
    uint16_t x1 = sx * r + x;
    uint16_t y1 = sy * r + y;

    // Calculate pair of coordinates for segment end
    float sx2 = cos((i + seg - 90) * 0.0174532925);
    float sy2 = sin((i + seg - 90) * 0.0174532925);
    int x2 = sx2 * (r - w) + x;
    int y2 = sy2 * (r - w) + y;
    int x3 = sx2 * r + x;
    int y3 = sy2 * r + y;

    if ( slice && i == v || !slice && i < v ) { // Fill in coloured segments with 2 triangles
      tft.fillTriangle(x0, y0, x1, y1, x2, y2, colour);
      tft.fillTriangle(x1, y1, x2, y2, x3, y3, colour);
    } else  {   // Fill in blank segments
      tft.fillTriangle(x0, y0, x1, y1, x2, y2, blank);
      tft.fillTriangle(x1, y1, x2, y2, x3, y3, blank);
    }
  }
}

// ------------------------------ rainbow ----------------------------------
unsigned int rainbow(byte value) {

  byte red = 0; // Red is the top 5 bits of a 16 bit colour value
  byte green = 0;// Green is the middle 6 bits
  byte blue = 0; // Blue is the bottom 5 bits
  byte quadrant = value / 32;

  if (quadrant == 0) {
    blue = 31;
    green = 2 * (value % 32);
    red = 0;
  }
  if (quadrant == 1) {
    blue = 31 - (value % 32);
    green = 63;
    red = 0;
  }
  if (quadrant == 2) {
    blue = 0;
    green = 63;
    red = value % 32;
  }
  if (quadrant == 3) {
    blue = 0;
    green = 63 - 2 * (value % 32);
    red = 31;
  }
  return (red << 11) + (green << 5) + blue;
}
//------------------------------ TurnOn ------------------------------------
void TurnOn( int func ) {
  digitalWrite(RLY_UPPER, ( func & 0b00010000 ) > 0 );
  digitalWrite(RLY_LOWER, ( func & 0b00001000 ) > 0 );
  digitalWrite(RLY_GRILL, ( func & 0b00000100 ) > 0 );
  digitalWrite(RLY_FAN,   ( func & 0b00000010 ) > 0 );
  digitalWrite(RLY_LIGHT, ( func & 0b00000001 ) > 0 );
}
//----------------------------- ShowTimer ----------------------------------
void ShowTimer() {
  char buffer[5] = "";
  int m = 0;                                    //minutes
  int s = 0;                                    //seconds

  if ( Timer > 1) {                             //above 1 minutes, only minutes will be displayed
    if ( millis() > timeToShowTimer ) {         //1 minute passed
      timeToShowTimer = millis() + 60000;
      timeRing( Timer );
      Timer--;
      if ( Timer == 1) {
        timeRing( Timer );                     //last 1 minute shows immediately
        timeToShowTimer = millis() + 1000;
      }
    }
    if ( millis() > timeToShowTic ) {           //0.5 second passed
      timeToShowTic = millis() + 500;
      if ( tic ) {
        tft.fillRect(90,105,20,50,TFT_BLACK);
      } else {
        tft.setFreeFont(FSB48);
        tft.drawString(":", 95, 84,GFXFF);
      }
      tic = !tic;
    }
  } else {                                      //under 1 minute, seconds will be displayed
    if ( lastMinute > 0 ) {
      if ( millis() > timeToShowTimer) {         //1 second passed
        timeToShowTimer = millis() + 1000;
        m = int(lastMinute / 60);
        s = lastMinute - m * 60;
        sprintf(buffer, "%d:%02d", m, s);
        drawCenter(buffer, 120, 84, 3);
        lastMinute--;
      }
    } else {
      TurnOn(0);                               //Turn OFF all relays
    }
  }
}
//--------------------------- CheckTemperature --------------------------------
void CheckTemperature( int targetTemp ) {

  if ( millis() > timeToShowTemp ) {
    timeToShowTemp = millis() + 10000;              //every 10 seconds
    currentTemp = readThermocouple();
    currentTemp = int(currentTemp/5) * 5;           //round to 5
    char buffer[4];
    sprintf(buffer, "%03d", currentTemp);    
    tft.setFreeFont(FSB24);
    drawCenter(buffer, 120, 180, 2);
  }

  if (currentTemp > targetTemp) {
    TurnOn( funcs[currentMenu - 1] & 0b11100011 );   //Turn off heaters
  } else {
    TurnOn( funcs[currentMenu - 1] );                //Turn on heaters
  }

}
//----------------------------- drawShelf ----------------------------------
void drawShelf(int shelf) {
  /*
  const int colour = 0x2104;  //DARKGREY
  for( int i = 0; i < 4; i++ ) {
    tft.drawFastHLine(40,60+i*40,3,colour);
    tft.drawFastHLine(40,63+i*40,3,colour);
    tft.drawFastHLine(198,60+i*40,3,colour);
    tft.drawFastHLine(198,63+i*40,3,colour);
  }
  tft.fillTriangle(44,61+shelf*40,48,58+shelf*40,48,64+shelf*40,colour);
  tft.fillTriangle(196,61+shelf*40,192,58+shelf*40,192,64+shelf*40,colour);
  */
}
//----------------------------- csv2array ----------------------------------
void csv2array( char * csv ) {
  int lineIndex = 0;            
  int fieldIndex = 0;           // 0-captions, 1-selves, 2-times, 3-temps, 4-functions
  char field[10];               //longest word in csv is 9 chars
  byte rb;                      //one byte of file
  int i = 0;                    //chr index

  fs::File csvFile = SPIFFS.open( csv, FILE_READ);  // or, file handle reference for SD library

  if(!csvFile){
      Serial.println("Failed to open file for reading");
      return;
  }
  while (csvFile.available()) {
    rb = csvFile.read();
    if (rb == ',' || rb == '\n') {    //end of field, so we save data to array
      field[i] = '\0';
      switch (fieldIndex) {
        case 0:
          strcpy(captions[lineIndex], field);
          break;
        case 1:
          shelves[lineIndex] = atoi(field);
          break;
        case 2:
          times_[lineIndex] = atoi(field);
          break;
        case 3:
          temps[lineIndex] = atoi(field);
          break;
        case 4:
          funcs[lineIndex] = (byte) strtol(field, NULL, 16);
          break;
      }
      fieldIndex++;
      if (rb == '\n') {              // end of line
        fieldIndex = 0;              
        lineIndex++;
      }
      i = 0;
    } else {                          // adds up data byte by byte
      field[i] = rb;                  // rb is the readedbyte from file
      i++;                            // increase byte index
    }
  }
  csvFile.close();                    // close the file
}

#define minimum(a,b)     (((a) < (b)) ? (a) : (b))
//-------------- Draw a JPEG on the TFT pulled from SPIFFS ---------------
void drawJpeg(const char *filename, int xpos, int ypos) {

  fs::File jpegFile = SPIFFS.open( filename, FILE_READ);
  if ( !jpegFile ) {
    return;
  }

  bool decoded = JpegDec.decodeFsFile(jpegFile);  // Pass the SD file handle to the decoder,
  if (decoded) {
    jpegRender(xpos, ypos);
  }
}

// Draw a JPEG on the TFT, images will be cropped on the right/bottom sides if they do not fit
void jpegRender(int xpos, int ypos) {

  uint16_t *pImg;
  uint16_t mcu_w = JpegDec.MCUWidth;
  uint16_t mcu_h = JpegDec.MCUHeight;
  uint32_t max_x = JpegDec.width;
  uint32_t max_y = JpegDec.height;

  bool swapBytes = tft.getSwapBytes();
  tft.setSwapBytes(true);
  
  uint32_t min_w = jpg_min(mcu_w, max_x % mcu_w);
  uint32_t min_h = jpg_min(mcu_h, max_y % mcu_h);

  uint32_t win_w = mcu_w;            // save the current image block size
  uint32_t win_h = mcu_h;

  max_x += xpos;
  max_y += ypos;

  while (JpegDec.read()) {          // While there is more data in the file
    pImg = JpegDec.pImage ;         // Decode a MCU (Minimum Coding Unit, typically a 8x8 or 16x16 pixel block)

    int mcu_x = JpegDec.MCUx * mcu_w + xpos;
    int mcu_y = JpegDec.MCUy * mcu_h + ypos;

    if (mcu_x + mcu_w <= max_x) win_w = mcu_w;
    else win_w = min_w;

    if (mcu_y + mcu_h <= max_y) win_h = mcu_h;
    else win_h = min_h;

    getEncoder();                 //it helps to work encoder flowlessly

    if (win_w != mcu_w) {
      uint16_t *cImg;
      int p = 0;
      cImg = pImg + win_w;
      for (int h = 1; h < win_h; h++) {
        p += mcu_w;
        for (int w = 0; w < win_w; w++) {
          *cImg = *(pImg + w + p);
          cImg++;
        }
      }
    }
    uint32_t mcu_pixels = win_w * win_h;

    if (( mcu_x + win_w ) <= tft.width() && ( mcu_y + win_h ) <= tft.height())
      tft.pushImage(mcu_x, mcu_y, win_w, win_h, pImg);
    else if ( (mcu_y + win_h) >= tft.height())
      JpegDec.abort(); // Image has run off bottom of screen so abort decoding
  }
  tft.setSwapBytes(swapBytes);
}
double readThermocouple() {

  uint16_t v;
  pinMode(MAX6675_CS, OUTPUT);
  pinMode(MAX6675_SO, INPUT);
  pinMode(MAX6675_SCK, OUTPUT);
  
  digitalWrite(MAX6675_CS, LOW);
  delay(1);

  // Read in 16 bits,
  //  15    = 0 always
  //  14..2 = 0.25 degree counts MSB First
  //  2     = 1 if thermocouple is open circuit  
  //  1..0  = uninteresting status
  
  v = shiftIn(MAX6675_SO, MAX6675_SCK, MSBFIRST);
  v <<= 8;
  v |= shiftIn(MAX6675_SO, MAX6675_SCK, MSBFIRST);
  
  digitalWrite(MAX6675_CS, HIGH);
  if (v & 0x4) 
  {    
    // Bit 2 indicates if the thermocouple is disconnected
    return NAN;     
  }

  // The lower three bits (0,1,2) are discarded status bits
  v >>= 3;

  // The remaining bits are the number of 0.25 degree (C) counts
  return v*0.25;
}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.