ST7789 draw BMP files faster


My hardware:
1.3" ST7789 color lcd with SPI, 240 x 240px

My Code:

 *  Basic example to show how to read a BMP image from SPIFFS
 *  and display using Adafruit GFX
 *  Tested with esp32 devboard and 160x128 ST7735 display

// Required libraries
#include <SPIFFS.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <SPIFFS_ImageReader.h>

// Display interface configuration
#define TFT_CS_PIN    15
#define TFT_DC_PIN    16
#define TFT_RST_PIN   17

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN);

// Display backlight enable pin

// Image reader
SPIFFS_ImageReader reader;

void setup() {
  tft.init(240, 240, SPI_MODE2);           // Init ST7789 240x240

  // initialize SPIFFS
  if(!SPIFFS.begin(false)) {
    Serial.println("SPIFFS initialisation failed!");
    while (1);

void loop() {

  uint32_t time = millis();
  reader.drawBMP("/DZM.bmp", tft, 0, 0);
  Serial.println(millis() - time);

I tried the example code of the SPIFFS_Imagereader lib for drawing a BMP image which is stored on SPIFFS.
It works, but it’s also slow as hell… It needs about 1140ms to draw this file! I think, I know why, it seems the SPIFFS is a lot slower than I expected.
So, this is not suitable for my application. What I want to do is, loading a background image, draw some informations on it and then refresh again. So basically, I need to refresh as fast as possible or at least with a frequency of 60Hz.
I need to store the image in RAM, which could be done, because the ESP32 has 520kB of it and my picture only needs anout 173kB. but how can this achieved? Is it already possible to store files in RAM and pass them to the Imagereader, using a library? Or do I need to make a library myself to do this?

Go on. You have an ESP32. Why don't you try TFT_eSPI library ?

Please post a link to the actual display that you bought.
I get fed up with repeating the same request.


Go on. You have an ESP32. Why don't you try TFT_eSPI library ?

Please post a link to the actual display that you bought.
I get fed up with repeating the same request.


Because I didn't know that library before?
Why does the exact display matter in this case? It's a software question.
However, I will give TFT_eSPI a try.

I found a solution, the SPIFFS_ImageReader lib contains a method called loadBMP() which is intended for storing images in RAM. It's a lot faster, but 29ms are not fast enough. But I'm getting closer.

Why does the exact display matter in this case? It's a software question.

The hardware makes an enormous difference. e.g. 3.3V logic, power supply, SPI interface in several incarnations, 8080-8, 8080-16, 8080-18, ..., level shifters, ...

It takes you 30 seconds to paste a link to your purchase e.g. Ebay Sale Page.
Or with a bit more effort, you link to the "identical item" e.g. from another Sale page.

I can recognise most things from pcb, photos, text, ...
even though Ebay sellers are notorious for lying through their teeth (or just ignorance)

I suggest that you run all of Bodmer's examples first.

Then offer proper information. e.g. size of images, number of colours, ..., format.

You can decode JPEGs with one ESP32 core and display with the second core.
You can blit RAW data from SPIFFS or SD card.


Until now, the hardware didn’t matter.
I got it working using the TFT_SPIFFS_BMP example, with some minor changes.
Now the hardware seems to limit, but I guess this can be solved by some optimizations.

By the way, it’s this LCD. But I don’t want to use it for my project, it was just for testing purposes. SPI speed seems to be limited @40MHz, but maybe shorter connections would solve this. Refresh needs 25ms.

// This sketch draws BMP images pulled from SPIFFS onto the TFT. It is an
// an example from this library:

// Images in SPIFFS must be put in the root folder (top level) to be found
// Use the SPIFFS library example to verify SPIFFS works!

// The example image used to test this sketch can be found in the sketch
// Data folder, press Ctrl+K to see this folder. Use the IDE "Tools" menu
// option to upload the sketches data folder to the SPIFFS

// This sketch has been tested on the ESP32 and ESP8266


//                                  Libraries
// Call up the SPIFFS FLASH filing system this is part of the ESP Core
#include <FS.h>
#define ESP32
#ifdef ESP32
  #include "SPIFFS.h"  // For ESP32 only

// Call up the TFT library
#include <TFT_eSPI.h> // Hardware-specific library for ESP8266

// Invoke TFT library
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite spr = TFT_eSprite(&tft);  // Declare Sprite object "spr" with pointer to "tft" object

//                                    Setup
void setup()

  if (!SPIFFS.begin()) {
    Serial.println("SPIFFS initialisation failed!");
    while (1) yield(); // Stay here twiddling thumbs waiting
  Serial.println("\r\nSPIFFS initialised.");

  // Now initialise the TFT
  tft.setRotation(0);  // 0 & 2 Portrait. 1 & 3 landscape
  tft.fillScreen(TFT_BLACK);  // Create a sprite of defined size
  spr.createSprite(240, 240);
  drawBmp("/DZM.bmp", 0, 0, spr);

//                                    Loop
void loop()
  uint32_t time = millis();
  Serial.println(String(millis() - time) + "ms");

// Bodmers BMP image rendering function

void drawBmp(const char *filename, int16_t x, int16_t y, TFT_eSprite spr) {

  if ((x >= spr.width()) || (y >= spr.height())) return;

  fs::File bmpFS;

  // Open requested file on SD card
  bmpFS =, "r");

  if (!bmpFS)
    Serial.print("File not found");

  uint32_t seekOffset;
  uint16_t w, h, row, col;
  uint8_t  r, g, b;

  uint32_t startTime = millis();

  if (read16(bmpFS) == 0x4D42)
    seekOffset = read32(bmpFS);
    w = read32(bmpFS);
    h = read32(bmpFS);

    if ((read16(bmpFS) == 1) && (read16(bmpFS) == 24) && (read32(bmpFS) == 0))
      y += h - 1;

      bool oldSwapBytes = spr.getSwapBytes();

      uint16_t padding = (4 - ((w * 3) & 3)) & 3;
      uint8_t lineBuffer[w * 3 + padding];

      for (row = 0; row < h; row++) {
       , sizeof(lineBuffer));
        uint8_t*  bptr = lineBuffer;
        uint16_t* tptr = (uint16_t*)lineBuffer;
        // Convert 24 to 16 bit colours
        for (uint16_t col = 0; col < w; col++)
          b = *bptr++;
          g = *bptr++;
          r = *bptr++;
          *tptr++ = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);

        // Push the pixel row to screen, pushImage will crop the line if needed
        // y is decremented as the BMP image is drawn bottom up
        spr.pushImage(x, y--, w, 1, (uint16_t*)lineBuffer);
      Serial.print("Loaded in "); Serial.print(millis() - startTime);
      Serial.println(" ms");
    else Serial.println("BMP format not recognized.");

// These read 16- and 32-bit types from the SD card file.
// BMP data is stored little-endian, Arduino is little-endian too.
// May need to reverse subscript order if porting elsewhere.

uint16_t read16(fs::File &f) {
  uint16_t result;
  ((uint8_t *)&result)[0] =; // LSB
  ((uint8_t *)&result)[1] =; // MSB
  return result;

uint32_t read32(fs::File &f) {
  uint32_t result;
  ((uint8_t *)&result)[0] =; // LSB
  ((uint8_t *)&result)[1] =;
  ((uint8_t *)&result)[2] =;
  ((uint8_t *)&result)[3] =; // MSB
  return result;

You won't get any TFT controller running SPI faster than 40MHz. Ok, the ILI9341 might.

240x240 = 57600 pixels = 115200 SPI bytes @ 40MHz = 23.04ms

Actually, there is a few bytes of housekeeping. So it will be about 23.05ms theoretical "best".

You should be able to blit this with DMA and still decode even with a single core. And run the rest of your application. ESP8266 and ESP32 handle SPI by DMA natively.

Animations seldom change every pixel. Some intelligence can minimise the pixel traffic.
Obviously random photos don't "morph".


240x240 = 57600 pixels = 115200 SPI bytes @ 40MHz = 23.04ms

Shit, you're right :smiley:
I miscalculated.
However, maybe this LCD controller can get it:
Timing Diagram on page 189 seems to be promising.


SCL tscycw Serial Clock Cycle (Write) 10 - ns

SCL tscycr Serial Clock Cycle (Read) 150 - ns

i.e. 100MHz SPI. which is 80ns for 8-bits.

The 8080 Parallel timings are similar to almost all modern controllers.

WRX twc Write Cycle 66 - ns

RDX trc Read Cycle (ID) 160 - ns

Note that SPI can use DMA whereas Parallel needs direct writes.

All the same, I would look carefully at your data.

If you want sensible answers, you should provide adequate information. e.g. two typical images (any PC format is fine)
And a genuine Frame Rate, electronics etc.

I think that I have a GC9101 and a GC9102 which are 132x162 controllers. They do not perform as well as Sitronix (from memory)

I fully understand that some projects are commercial or terrorist related.
Don’t post incriminating photos.
But if you are asking genuine questions you can still provide information.


Indeed, this project is intended to be commercial. I never tried to be cryptic or something like that.

Do you have any advices for pcb design when working at such frequencies? I never designed pcbs working at higher frequencies than 10MHz.
I think, the two most important aspects are keeping the capacitance and the impedance as low as possible i.e. keeping the traces as short(and slim) as possible. Something else to be aware of? Maybe a minimum substrate thickness?
I am going to use a WROOM-32 Module one the pcb.

I don't think you will have layout problems with SPI. I would worry more about Parallel.

You still have not answered.
Are they photos that you want to display ?
Are they all 240x240 with 65536-colors.
How fast? e.g. Frame Rate.
How many?

Any redundancy?
SD card, Flash or SPIFFS ?


Sorry about that.
Well, there is one background Image, stored on the SPIFFS and an animation(Sprite), I think I am going to use an Image too, which is moving on the foreground.
I has been thinking about it. If I would only update the part of the LCD which shows the animation (before und updated) I could save at least half of the data size to be transmitted.
The LCD (only one) I am going to use will have a resolution of 240x240, 16Bit colors, so it's the same as now. But it's round. Maybe I could also spare some time don't sending the edges.
What I would like to achieve is a framerate of 60FPS, completely tearing free.

What do you mean by redundancy? If you mean storage, there won't be any redundancy. It's SPIFFS only.

Well, there is one background Image, stored on the SPIFFS and an animation(Sprite), I think I am going to use an Image too, which is moving on the foreground.

You don't mention the size of the sprite. Let me guess 32x32.

I don't understand the moving image. Do you mean 240x240? Or a smaller image? Do you want it transparent, shadow or destructive?

A circular display makes no difference. The controller works in rectangular "windows".

You have enough Flash to store the images in PROGMEM or in SPIFFS.
That is very different to requiring 10000 Frames at 60FPS

Seriously. There is nothing incriminating. You can even invent a parallel universe to keep secrecy.
e.g. a typical cartoon animation with "similar" features
The important details are number of images with their dimensions and Frame Rate.


Well, I want to make a fully electronic rev counter for old motor cycles which simulate the look of the original ones. The idea is simple, the shown dial-plate has to look like the original one, maybe without copyright protected signs like logos, but I want to add some features like enabling a clock, which I personally miss very much when driving by bike, or a thermometer.
So basically, I have the dial-face as a background, the needle as sprite and, if enabled, some additional indicators as sprites, too. In order to give users the ability to change or add different dial-face designs it would be the best to use the SPIFFS. Storing in program memory would be fast and would also save RAM, but I think this is no problem, since I have plenty available.

A circular display makes no difference. The controller works in rectangular "windows".

Hey, you wanted every information :smiley:
As I understood those displays, the visible resolution of those circular ones is smaller compared to rectangular lcds. So it should be possible to save some time, don't transferring the corners.
The framerate has to be high enough in order to avoid any stuttering. Every movement on the screen must be smooth.
There is an example attached to the TFT_eSpi library which shows such an approach, but they draw the needle using the drawing methods of the library.
I would like to use an image of a genuine like looking needle and, if the CPU can do this, maybe some simple anti-aliasing to smooth the edges of the needle a little bit.

Rotating a thin needle pointer involves small numbers of pixels e.g. restoring the background for the old pointer and drawing a fresh pointer.

Computationally it is complex. Triangles are difficult. But painting the actual pixels is not going to take too long.

The original question about blitting a full 240x240 picture in 23ms may be theoretical. Any complex computation might exceed the time that DMA sends the SPI bytes.

Actually, I was playing with an animation on an 8-bit XMEGA. DMA can paint the screen while the next data is prepared. The 8-bit MCU took much longer to compute than an ARM (or your ESP32)

So it is important to consider the compute power.

I suggest that you study Bodmer's examples. Your dashboard will make some graphical compromises. Realism is not always the most effective.
Do you really stare hard at the handlebars while riding your motorcycle?


Without some basic example we are hitting blindly.
Do you have an example that we can see on the web, that is similar to what you want to achieve with your hardware?

Regular, inexpensive drivers will not be able to help you without applying additional programming effort. What you need is a real graphics processing chip like the FT81X or BT81X, they work in AVR, ARM and STM32, with some limitations in ESP32, but that can be solved using images in the form of cell arrays; It could also be feasible the series of industrial displays that have appeared lately with drivers for Arduino, or perhaps some chips from 4D systems. This particular type of display requires an extra investment, which in the end is worth considering.

Riverdi FT813 5" + teensy 3.6

FT843 (4D systems FT801 4.3") + Arduino Due

So it is important to consider the compute power.

I suggest that you study Bodmer's examples. Your dashboard will make some graphical compromises. Realism is not always the most effective.
Do you really stare hard at the handlebars while riding your motorcycle?


Indeed, from the beginning of this project I was aware of the need for some computation power.
Maybe the Dual Core features of the ESP32 can help about that.
I found this Demo 25: How to configure ESP32 Dual core - Multicore in Arduino ESP32 - IoT Sharing
One core can transfer the data to the LCD while the other core is busy computing stuff.
What do you mean by "staring"? Do you never look at the watch or speedometer of your vehicle's dashboard while driving? It's not like this would take ages :smiley:
I'm pretty sure the more dangerous people are the ones, which stare all the time at their cell phones while driving.
This whole project is just something I try to do by myself, in general to learn something new and maybe with the outcome of something which might be marketable. My fundings are very limited, which applies to most students I guess, so the hardware is basically fixed.
I am going to work at this, and if I have further questions, I will ask.
Thanks for your help David and TFTLCDCyg :slight_smile:

As a fat person on a bicycle I spend most of my time puffing and panting.

I glance at my speed, average, ... I don't take my eyes off the road for a long time.
I don't study my neighbour's crops. Just get an overall impression from my peripheral vision.
If I was on a motorbike I would end up in the hedge.

Seriously, design your project how you want it to look. Achieve the exact image you want on the display. (pick up some tips from Bodmer's examples)

Write down some numbers for the geometry, frame rate etc. We might be able to help.
Some things are physically impossible but we don't know until we have numbers.