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;
}