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.
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.
/*
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.6The same dashboard in MicroPython on boochow's ST7735.py. The number is drawn into a small framebuf with its own background and sent as one rectangle; the bar gets only its new strip; ticks_us times each update.
"""
1.8-inch TFT Display - redraw only what changed, MicroPython
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. The backlight is off until BL is HIGH.
RST -> GPIO 42 on an ESP32-S3, GPIO 4 on an ESP32, GP3 on a Pico
DC -> GPIO 2 on an ESP32-S3, GPIO 2 on an ESP32, GP4 on a Pico
SCL -> GPIO 12 on an ESP32-S3, GPIO 18 on an ESP32, GP18 on a
Pico (the SPI clock)
MO -> GPIO 11 on an ESP32-S3, GPIO 23 on an ESP32, GP19 on a
Pico (MOSI, SPI data out)
CS -> GPIO 10 on an ESP32-S3, GPIO 15 on an ESP32, GP5 on a Pico
3V3 -> 3V3 (never 5V: the display is a 3.3 V part)
GND -> GND
Thonny
Run > Configure interpreter MicroPython (ESP32) or
MicroPython (Raspberry Pi Pico)
Needs ST7735.py (boochow's MicroPython-ST7735) saved on the
board, as in first light. Stop it with Ctrl-C.
"""
import os
import sys
import time
import framebuf
from machine import SPI, Pin
from ST7735 import TFT
if sys.platform == "rp2": # Raspberry Pi Pico
SCL, MOSI, CS, DC, RST, BL = 18, 19, 5, 4, 3, 2
SPI_ID = 0
elif "ESP32S3" in os.uname().machine: # ESP32-S3
SCL, MOSI, CS, DC, RST, BL = 12, 11, 10, 2, 42, 41
SPI_ID = 1
else: # ESP32
SCL, MOSI, CS, DC, RST, BL = 18, 23, 15, 2, 4, 32
SPI_ID = 2
# True: clear the whole screen and draw everything, every frame.
# Try it once, watch the flicker, and read the time it prints.
CLEAR_EVERY_FRAME = False
BG, INK, BAR = TFT.BLACK, TFT.WHITE, TFT.GREEN
BAR_X, BAR_Y, BAR_W, BAR_H = 10, 90, 140, 14
backlight = Pin(BL, Pin.OUT, value=1) # off until BL is HIGH
spi = SPI(SPI_ID, baudrate=10_000_000, polarity=0, phase=0,
sck=Pin(SCL), mosi=Pin(MOSI))
tft = TFT(spi, DC, RST, CS)
tft.initr() # see first light if the colours are off
tft.rotation(1) # landscape, 160 x 128
# tft.invertcolor(True) # if first light needed it
def swap(colour):
return ((colour & 0xFF) << 8) | (colour >> 8)
def label(x, y, text, ink, paper):
# Text into a small buffer with its own background, then send
# only that rectangle. The old text is painted over, not
# cleared first, so nothing is blank for a moment.
w, h = 8 * len(text), 8
buf = bytearray(w * h * 2)
fb = framebuf.FrameBuffer(buf, w, h, framebuf.RGB565)
fb.fill(swap(paper))
fb.text(text, 0, 0, swap(ink))
tft.image(x, y, x + w - 1, y + h - 1, buf)
def draw_frame():
# The parts that never change: drawn once.
tft.fill(BG)
label(10, 10, "TK89 uptime", INK, BG)
tft.rect((BAR_X - 1, BAR_Y - 1), (BAR_W + 2, BAR_H + 2), INK)
bar_was = 0
def draw_value(tenths):
global bar_was
label(10, 44, "%6d.%d s" % (tenths // 10, tenths % 10), INK, BG)
# The bar: paint only the strip between the old and new ends.
bar = (tenths % 100) * BAR_W // 100
if bar > bar_was:
tft.fillrect((BAR_X + bar_was, BAR_Y), (bar - bar_was, BAR_H),
BAR)
elif bar < bar_was:
tft.fillrect((BAR_X + bar, BAR_Y), (bar_was - bar, BAR_H), BG)
bar_was = bar
draw_frame()
start = time.ticks_ms()
last_print = start
while True:
tenths = time.ticks_diff(time.ticks_ms(), start) // 100
t0 = time.ticks_us()
if CLEAR_EVERY_FRAME:
draw_frame()
bar_was = 0
draw_value(tenths)
took = time.ticks_diff(time.ticks_us(), t0)
if time.ticks_diff(time.ticks_ms(), last_print) >= 1000:
last_print = time.ticks_ms()
print("everything:" if CLEAR_EVERY_FRAME else "changes:",
took, "us")
time.sleep_ms(100)MicroPython's framebuf font is 8 × 8 pixels, smaller than the Arduino sketch's text. CLEAR_EVERY_FRAME does the same job. Stop it with Ctrl-C.
View on GitHub · blocks/tk89-tft-1-8-inch/micropython/tft_dashboard.py @ v1.6When it does not work
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.
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 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 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.
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
Questions about this product
See what other owners have asked, and read their solutions.
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.