1.8-inch TFT/Drawing fast/08. Redraw only what changed
Drawing fast · 08 of 10

Redraw only what changed

A dashboard that draws its title, labels and frame once, then every tenth of a second paints the new number over the old one and fills only the strip of the bar that changed. Nothing is cleared, so nothing is ever blank. The sketch prints how long each update took, and one line switches it to clearing the whole screen so you can see and time the difference.

Draw once, then paint over

A dashboard has two kinds of pixel: the ones that never change (a title, a label, a frame) and the ones that do (a number, a bar). The sketch draws the first kind once, in drawFrame(), and never touches them again.

The second kind is drawn so that the new picture covers the old one. setTextColor(INK, BG) with two colours makes every character cell paint its own background, so a 7 drawn where a 6 was replaces it completely. The number is padded to a fixed width, so a shorter number still covers a longer one. The bar is filled only between where it ended and where it ends now.

Clear and draw, or paint over
Each update
Per update
26.7 ms
Blank frames
0
Clock
15 MHz
fillScreen then draw: every update the screen goes black for at least 21.8 ms while 40,960 bytes of black go down the wire, then the number comes back. The eye catches that as flicker, and the loop is slow.

Nothing is ever cleared, so there is never a moment when the screen shows nothing. That, not speed alone, is what makes it steady.

Time it yourself

Once a second the serial monitor prints how long the last update took, changes: ... us. Set CLEAR_EVERY_FRAME to true at the top and upload again: the screen flickers, and the line reads everything: ... us, many times longer. The numbers are your board's, with its clock and its library, not the floor the last chapter worked out, so they are the ones to trust.

When the whole screen does change

Some pictures change everywhere: a graph that scrolls, a camera image. Then the answer is not to clear first but to draw the new frame straight over the old one, whole, in one window. That still costs 40,960 bytes a frame, so a few tens of frames a second at best, fewer once the drawing is counted. An ESP32 or a Pico has the memory to build the frame first and send it in one go; an Uno does not. For a dashboard, painting over the parts that change is enough.

The code

drawFrame() draws everything that never changes, once. drawValue() prints the uptime in large text with setTextColor(ink, background), so each character cell paints over the last, and fills only the strip between the bar's old and new ends. loop() times the update with micros() and prints it once a second.

tft_dashboard.ino
/*
  1.8-inch TFT Display - redraw only what changed       TK89 / /p/tk89

  Wiring. The eight pins top to bottom, screen facing you, header on
  the left (the front prints BL RST DC SCL MO CS 3V3 GND):

    BL   -> GPIO 41 on an ESP32-S3, GPIO 32 on an ESP32, GP2 on a
            Pico, D7 on an Uno. The backlight is off until BL is
            HIGH.
    RST  -> GPIO 42 on an ESP32-S3, GPIO 4 on an ESP32, GP3 on a
            Pico, D8 on an Uno
    DC   -> GPIO 2 on an ESP32-S3, GPIO 2 on an ESP32, GP4 on a
            Pico, D9 on an Uno
    SCL  -> the SPI clock: GPIO 12 on an ESP32-S3, GPIO 18 on an
            ESP32, GP18 on a Pico, D13 on an Uno
    MO   -> MOSI, SPI data out: GPIO 11 on an ESP32-S3, GPIO 23 on
            an ESP32, GP19 on a Pico, D11 on an Uno
    CS   -> GPIO 10 on an ESP32-S3, GPIO 15 on an ESP32, GP5 on a
            Pico, D10 on an Uno
    3V3  -> 3V3. Never 5V: the display is a 3.3 V part.
    GND  -> GND

  An Uno is a 5 V board: every line goes through a TK97 logic
  level converter, never straight to these pins.

  Arduino IDE
    Tools > Board                 your board, e.g. ESP32S3 Dev Module
    Tools > Port                  the one that appears when you plug in
    Tools > USB CDC On Boot       Enabled   (ESP32-S3 only)
    Tools > Manage Libraries      Adafruit ST7735 and ST7789 Library,
                                  and install its dependencies
                                  (Adafruit GFX, Adafruit BusIO)
*/

#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>

#if defined(ARDUINO_ARCH_AVR)              // Uno, through a TK97
const int TFT_CS = 10, TFT_DC = 9, TFT_RST = 8, TFT_BL = 7;
#elif defined(CONFIG_IDF_TARGET_ESP32S3)   // ESP32-S3
const int TFT_CS = 10, TFT_DC = 2, TFT_RST = 42, TFT_BL = 41;
#elif defined(ARDUINO_ARCH_ESP32)          // ESP32
const int TFT_CS = 15, TFT_DC = 2, TFT_RST = 4, TFT_BL = 32;
#else                                      // Raspberry Pi Pico
const int TFT_CS = 5, TFT_DC = 4, TFT_RST = 3, TFT_BL = 2;
#endif

// true: clear the whole screen and draw everything, every frame.
// Try it once, watch the flicker, and read the time it prints.
const bool CLEAR_EVERY_FRAME = false;

const uint16_t BG = ST77XX_BLACK;
const uint16_t INK = ST77XX_WHITE;
const uint16_t BAR = ST77XX_GREEN;
const int BAR_X = 10, BAR_Y = 90, BAR_W = 140, BAR_H = 14;

Adafruit_ST7735 tft(TFT_CS, TFT_DC, TFT_RST);
int barWas = 0;   // the bar's width on the screen now

// The parts that never change: drawn once.
void drawFrame() {
  tft.fillScreen(BG);
  tft.setTextColor(INK);
  tft.setTextSize(1);
  tft.setCursor(10, 10);
  tft.print("TK89  uptime");
  tft.setCursor(124, 57);
  tft.print("s");
  tft.drawRect(BAR_X - 1, BAR_Y - 1, BAR_W + 2, BAR_H + 2, INK);
  barWas = 0;
}

// The parts that change. The text is drawn with a background
// colour, so each character cell paints over the old one: no
// clearing, nothing blank for a moment, nothing to flicker.
void drawValue(unsigned long tenths) {
  char text[12];
  snprintf(text, sizeof text, "%4lu.%lu", tenths / 10, tenths % 10);
  tft.setTextColor(INK, BG);
  tft.setTextSize(3);
  tft.setCursor(10, 40);
  tft.print(text);

  // The bar: paint only the strip between the old and new ends.
  int bar = (int)((tenths % 100) * BAR_W / 100);
  if (bar > barWas) {
    tft.fillRect(BAR_X + barWas, BAR_Y, bar - barWas, BAR_H, BAR);
  } else if (bar < barWas) {
    tft.fillRect(BAR_X + bar, BAR_Y, barWas - bar, BAR_H, BG);
  }
  barWas = bar;
}

void setup() {
  Serial.begin(115200);
  delay(500);
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);   // the backlight is off until then

  tft.initR(INITR_BLACKTAB);    // see first light if colours are off
  // tft.invertDisplay(true);   // if first light needed it
  tft.setRotation(1);           // landscape, 160 x 128
  drawFrame();
}

void loop() {
  static unsigned long lastPrint = 0;
  unsigned long tenths = millis() / 100;

  unsigned long t0 = micros();
  if (CLEAR_EVERY_FRAME) drawFrame();
  drawValue(tenths);
  unsigned long took = micros() - t0;

  if (millis() - lastPrint >= 1000) {
    lastPrint = millis();
    Serial.print(CLEAR_EVERY_FRAME ? "everything: " : "changes: ");
    Serial.print(took);
    Serial.println(" us");
  }
  delay(100);
}

Set CLEAR_EVERY_FRAME to true to clear and redraw everything every update instead: watch it flicker, and compare the time it prints. Same pins as first light. The sketch compiles for an Uno, an ESP32 and an ESP32-S3.

View on GitHub · blocks/tk89-tft-1-8-inch/arduino/tft_dashboard/tft_dashboard.ino @ v1.6

When it does not work

Old digits are left behind when the number gets shorter.

The text is drawn over the old text, so the new string has to cover the old one. The sketch pads the number to a fixed width with spaces; a space drawn with a background colour erases what was under it. Keep that padding if you change the format.

The number flickers even though I do not call fillScreen.

Check setTextColor has two colours, the ink and the background. With only the ink, text is drawn over whatever is there: the old digits show through, and clearing the area first to hide them brings the flicker back.

The time it prints on my Uno is much longer than the book's figure.

The book's figure is the bytes on the wire alone, a floor. An Uno's SPI runs at up to 8 MHz, the library does work between transfers, and a TK97 may need a slower clock. The sketch's own number is the real one for your board.

The bar leaves a sliver behind when it shrinks.

The sketch fills from the new end to the old end in the background colour. To move or resize the bar, change BAR_X, BAR_Y, BAR_W and BAR_H at the top, not numbers inside the functions, so the frame and the fill stay lined up.

Alongside this page
Where this goes next

Two pairs of pads on the back that each save a wire, and what each costs.

The two jumpers →

Edit this page — content/books/tft-1-8-inch/redraw-only-what-changed.mdx

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

1.8-inch TFT Display

Loading discussions…

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse Modules and blocks on the forum →