Crazy fast TFT action with Adafruit GFX and ESP32

I have made a few projects using small and inexpensive TFT displays. Usually I will write some code in Arduino and use the Adafruit GFX library in conjunction with it’s associated hardware specific driver to draw text and graphics on the display. For most applications it works fine but it is super slow and here is why:

the Adafruit GFX library is a unified way to add text and graphics capabilities to any display. The library itself does not deal with the actual display that will eventually show the graphics. Instead it uses a single method: drawPixel(x,y,color) to draw all graphics as individual pixels. Whoever writes a hardware driver to a specific display only need (simply put) to write this method to set a dot on the display in question. This trades speed for flexibility for a number of reasons. First the Arduino does not need to have enough memory to hold all the pixels on the display. Even a moderate display of e.g. 128*160 pixels requires 40960 bytes to hold a whole color image. On the flip side a lot of calls to the drawPixel method is required. When writing dynamic graphics like updating numbers or graphs even more calls to drawPixel is required to selectively delete old graphics resulting in slow and blinky updates. We are however entering an age where even cheap microcontrollers have quite a bit of RAM and if you have enough RAM to hold a whole image a much faster method becomes possible: By first letting Adafruit GFX library write to memory and then, when you are good and ready, using a super fast and optimized method transfer this image to the display hardware.

You control where the library writes it’s pixels by extending the Adafruit_GFX class. This means that you basically makes a new version of the Adafruit_GFX class wherein you add your own writePixel method that writes pixels to memory:

class myNewClass : public Adafruit_GFX {
public:
 ..Allocate some memory for pixels..
 void drawPixel( int16_t x, int16_t y, uint16_t color)
 {
  ..put color in memory at x,y..
 }
};

myNewClass now has all the usual graphics methods but puts it in the memory of your choice.

The next thing needed is a method to transfer this memory to the physical display via. SPI. In most hardware drivers for Adafrut GFX the methods setASddressWindow and SPI_WRITE16 are used to transfer each pixel to the display.:

startWrite();
setAddrWindow(x, y, 1, 1);
SPI_WRITE16(color);
endWrite();

Each pixel require 4 function calls and completely restarts the SPI interface and all of this takes time.

The setASddressWindow(x,y,h,w) function tells the display where the next pixel(s) will go in the displays internal memory and thus on the screen.  This function takes the resolution and orientation of the display into consideration and sets up the displays hardware such that consecutive data written will end up in a square on the screen starting at x,y with a width and height of w,h.

If you already have all the pixels to be written (in the microcontroller memory)  the write process can be rearranged so all pixels are written at once  in one big chunk:

tft.setAddrWindow(0, 0, 160, 128);
digitalWrite(TFT_DC, HIGH);
digitalWrite(TFT_CS, LOW);
SPI.beginTransaction(SPISettings(80000000, MSBFIRST, SPI_MODE0));
   for (uint16_t i = 0; i < 160 * 128; i++)
   {
      SPI.write16(buffer[i]);
   }
SPI.endTransaction();
digitalWrite(TFT_CS, HIGH);

In the above example the object tft is a Adafruit_ST7735 class implementing the hardware stuff for a ST7735 based display:

Adafruit_ST7735 tft = Adafruit_ST7735(&SPI, TFT_CS, TFT_DC, TFT_RST); //-Just used for setup

Normally you would use tft object to do all the display communication like:

tft.print("Hello slow");

Which would use the slow one pixel at a time communication. If we do our own fast update from memory we only need the display initialization and tft.setAddrWindow(0, 0, dispWidth, dispHeight) method from the tft object.

To still have all the good graphics methods from Adafruit GFX we use our own “myNewClass” described above to write graphics to memory:

Adafruit_ST7735 tft = Adafruit_ST7735(&SPI, TFT_CS, TFT_DC, TFT_RST); //-Just used for setup
myNewClass fast = myNewClass(128,160); //-Write to RAM

...

tft.init .. Set up hardware stuff

...

fast.drawLine(20,30,100,130,65535); //-Write a line in RAM


//This send all to display:
tft.setAddrWindow(0, 0, 160, 128);
digitalWrite(TFT_DC, HIGH);
digitalWrite(TFT_CS, LOW);
SPI.beginTransaction(SPISettings(80000000, MSBFIRST, SPI_MODE0));
for (uint16_t i = 0; i < 160 * 128; i++)
{
   SPI.write16(buffer[i]);
} 
SPI.endTransaction();
digitalWrite(TFT_CS, HIGH);

So what kind of speed are we talking about?! I have been using the ESP32 and a 1.8′ ST7735 based  128×160 pixel display for these tests but  I am sure that other micro controllers (with enough RAM) and similar displays works the same way.

This video show the same graphics being run using the standard and updated methods. Numbers are frame counter / frame rate:

The schematic used in above test:

Code for demos.

I have yet to try this with other displays but I’m pretty sure this can be a general method. If the microcontroller used has enough memory to use this method it opens up the possibility to do frame by frame based graphics like you do in e.g. Processing or OpenFrameworks.

UPDATE: It works with at least one other display 🙂 :

Above video translates and draws 1000 circles. Number in the corner is the frame rate.

With ESP32 is is also possible to completely offload the display communication using DMA so all available processor time can be dedicated to generate graphics and do other things.

I will be exploring this in a future post.

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *