Flickering TFT display when drawing graphics

Hello,
I have an Arduino Uno R3 and a 1.44-inch TFT display from Adafruit, and I'm trying write a program that will display a white circle (on a black background) that will move when I move a joystick. I'm using Adafruit's GFX library and my method is to start each loop by filling the screen with black and then drawing a white circle that matches the position of the joystick:

  tft.fillScreen(0x0000);
  tft.fillCircle(64 + xCoor, 64 + yCoor, 30, 0xFFFF); // xCoor is from joystick

I understand that refilling the screen with black each cycle is what's causing it to flicker, but I don't know what other options there are for drawing a white circle that moves around with out leaving a trail of white behind it.
I have tried the "canvas" method that Adafruit's GFX library has, which allows you to draw your graphics onto a 1-bit canvas (fill screen with black, then draw white circle) that isn't on the screen, then send it to the screen:

canvas.fillScreen(0x0000);
canvas.fillCircle(64 + xCoor, 64 + yCoor, 10, 0xFFFF);
tft.drawBitmap(0, 0, canvas.getBuffer(), canvas.width(), canvas.height(), 0xFFFF, 0x0000);

The problem is that, unless I'm implementing something wrong, this method doesn't really put anything usable on my TFT. If I implement it at full canvas resolution (1-bit color) and a 30-pixel-radius circle, the display just goes blank or shows static. If I reduce the canvas resolution and decrease the circle radius to 10-pixels, then I can get the circle to show up, but it refreshes extremely slowly, only moving at about a frame and a half per second or so. This makes me wonder if my Uno is not powerful enough or does not have enough memory for this method.

Most of the advice about this I can find online is about updating text on the screen, not graphics, so I just want to ask if there are any methods that people use to draw a shape on a TFT display that can be moved around.
Thanks!

Rather thank filling the entire screen with black, why not draw a black circle in the previous position before you draw a white circle in the new position? It may be faster than re-drawing the entire screen. I don't know - you will have to try it out. If faster enough, you won't see the flicker.

How much memory would be required for such a canvas? How much memory does Uno R3 have?

Hi @it_ross!

I second @blh64 's post:

Usually to avoid/reduce flickering

  • START: Draw the circle
  • store Its coordinates and radius
  • calculate the new position
  • redraw the "old" circle using the stored data in background color
  • goto START

Good luck!
ec2021

Just an example using this possibility which I wrote for a different thread:

https://wokwi.com/projects/422068446568927233

@blh64 and @ec2021 , I submitted this question to a reddit post and someone had already suggested I try that, and I did. It doesn't fix the problem because drawing a black circle to cover the previous white circle means the the entire screen still has to be black between each white circle. The TFT can only "draw" one thing at a time, so if I draw a white circle, then a black circle over that, then another white circle in a new location, there is still a gap between the two white circles where the entire screen has to appear black, hence the flickering.

A 1-bit canvas at the same resolution as the 1.44" display, which is 128x128, would be 16384 bits. I get confused about the different types of memory and what they're used for, but the Uno R3 has 32kB of flash memory and 2kB of SRAM, as well as a 1kB EEPROM.

The solution though not really doable on a UNO unless what ever you're making is very simple, would be to draw your content to a buffer and then draw the buffer.

Similar link here

For this, it would be the SRAM that would be used.

Would you mind to post your code?

Edit: If you have a pure black background there should be no need to fill it black between the circle erasure and redrawing.

If you have other objects (e g. a cross hair) one would erase the circle, redraw the cross hair and then draw the new circle.

In any case it would help to (hopefully) understand your application if you post the code and a link to the display.

and that is 2kB which is the entire SRAM of the Uno, which means you can not do it unless you move to a board with more memory.

If you are interested to learn about the different memories of an Arduino board, feel free to read here

https://docs.arduino.cc/learn/programming/memory-guide/

For a quick reference scroll down to "Memory Types" :wink:

Just realized that you use

void fillCircle(int16_t x0, int16_t y0, int16_t r, uint16_t color);

instead of

  void drawCircle(int16_t x0, int16_t y0, int16_t r, uint16_t color);

fillCircle takes much more time than drawCircle.

Change fillCircle to drawCircle, replace fillScreen() by drawing a black circle and give it a try ...

@it_ross Here is an example using the GFXcanvas1 class. The radius of the ball is 10 pixels and is assigned to the 'size' variable. The example creates a canvas/sprite 26 x 26 pixels square to contain the ball, the ball is centered in the canvas with canvasWidth/2, canvasHeight/2 which in this case works out to be 13 x 13. The 26 x 26 canvas gives a slight black overlap to erase any white trails while still giving a reasonably small sprite. The canvas is moved around the display by modifying the x and y coordinates.

I tested this on an standard ESP32 dev module so the setup and pin numbering may differ slightly from the UNO but the ball function will still be valid.

#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7735.h> // Hardware-specific library for ST7735
#include <SPI.h>

#define canvasWidth  26
#define canvasHeight 26
#define size 10
#define xlimit 128 - canvasWidth
#define ylimit 126 - canvasHeight // reduced ylimit to give a better bounce

int xvelocity=1;
int yvelocity=1;
int x=0;
int y=0;

#define TFT_CS        5
#define TFT_RST       4 
#define TFT_DC        16

Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
GFXcanvas1 canvas(canvasWidth, canvasHeight);

void setup() {
  tft.initR(INITR_BLACKTAB);      // Init ST7735S chip, black tab
  tft.setRotation(1);
  tft.setSPISpeed(40000000);
  tft.fillScreen(0);
  bounceBall();
}

void bounceBall(){
  while(1){
  canvas.fillScreen(0);
  canvas.fillCircle(canvasWidth/2, canvasHeight/2, size, 0xff);
  tft.drawBitmap(x, y, canvas.getBuffer(),canvasWidth,canvasHeight,0xff,0);
 
  x += xvelocity;
  if (x == xlimit or x == 0){xvelocity = -xvelocity;}
  y += yvelocity;
  if (y == ylimit or y == 0){yvelocity = -yvelocity;}
  }   
}

void loop() {
  // put your main code here, to run repeatedly:

}

Hi @sumguy ,

a nice demo of a "sprite" function :wink:

It works as long as the steps are not larger than the required overlap to remove the old sprite.

It is possible to split the sprite creation and the drawing function:

void createBall(){
  canvas.fillScreen(0);
  canvas.fillCircle(canvasWidth/2, canvasHeight/2, size, 0xff);
}


void bounceBall(){
  while(1){
  tft.drawBitmap(x, y, canvas.getBuffer(),canvasWidth,canvasHeight,0xff,0);
  x += xvelocity;
  if (x == xlimit or x == 0){xvelocity = -xvelocity;}
  y += yvelocity;
  if (y == ylimit or y == 0){yvelocity = -yvelocity;}
  }   
}

Just call createBall() once before bounceBall() ...

If it's only a circle and not a filled circle that is required I would prefer the method to redraw the circle with background color to erase it ...

Thank you, I realized if velocity exceeded 2 there are problems and possibly the limit test would be better with <= as opposed to ==. Also I tried again without canvas.fillScreen(0); and it worked fine.

Perhaps you or someone else can answer a question for me while we are here. I used a 1.8" display for creating bounceBall and I had to offset the column and row using the following instruction tft.setColRowStart(2, 1); to properly center the image. This instruction is in the 'ST77xx.h' file and is set 'protected'. To use it I moved the function prototype to the section labelled 'public' , is that the correct thing to do or is there another way to access the method.

This function Just sets the internal variables so - as you know when and how to use it - it should be ok.

A change in a library that's externally maintained has the disadvantage that it will be overwritten with an update.

I saw that there is one initialization in the ST7735 lib that supports your setup

if (options == INITR_GREENTAB) {
    displayInit(Rcmd2green);
    _colstart = 2;
    _rowstart = 1;

but it might not work for you ... :wink:

1 Like

The Adafruit_GFX library draws a filled circle as a sequence of vertical lines. It is not that difficult to use their equations to calculate each line in the previous circle, find any lines in the new circle that occupy the same vertical column of the display, and only erase the pixels that fall outside the new circle. That tends to minimize the flicker except for the edges of the circle where pixels are either being erased or added.

Unfortunately the slow SPI speed of an UNO causes noticeable shearing of the image when a large circle moves rapidly.

#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
#include <Wire.h>

// For the Adafruit shield, these are the default.
#define TFT_DC 9
#define TFT_CS 10

// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);

struct _line {
  int16_t y;
  int16_t h;
};

int xPrev;
int yPrev;

void setup() {
  Serial.begin(115200);
  Serial.println(F("ILI9341 Test!"));

  tft.begin();
  tft.fillScreen(ILI9341_BLACK);

  int x = 50;
  int y = 50;
  int r = 30;
  xPrev = x;
  yPrev = y;
  int xIncrement = 1;
  int yIncrement = 1;
  while (true) {
    moveCircle(x, y, r, ILI9341_WHITE, xPrev, yPrev, r, ILI9341_BLACK);
    //tft.drawCircle(xPrev, yPrev, r, ILI9341_BLACK);
    //tft.drawCircle(x, y, r, ILI9341_WHITE);
    xPrev = x;
    yPrev = y;
    x += xIncrement;
    y += yIncrement;
    if (x >= (tft.width() - r)) {
      xIncrement = -xIncrement;
    }
    if (x <= r) {
      xIncrement = -xIncrement;
    }
    if (y >= (tft.height() - r)) {
      yIncrement = -yIncrement;
    }
    if (y <= r) {
      yIncrement = -yIncrement;
    }
  }

}


void loop(void) {
}


/**************************************************************************/
/*!
    @brief   check to determine if a vertical line segment is in the same
            vertical column as a circle
    @param    x0     Center-point x coordinate
    @param    y0     Center-point y coordinate
    @param    r      Radius of circle
    @param    xTest  x coordinate of vertical line
    @param    line   y coordinate and height of vertical line
*/
/**************************************************************************/
bool checkFillCircle(int16_t x0, int16_t y0, int16_t r,
                      int16_t xTest, _line &line) {
  bool found = false;

  //  if ((xTest < x0 - r) || (xTest > x0 + r) /*|| (line.y > (y0 + r)) || ((line.y + line.h) < (y0 - r))*/){
  //     found = false;
  //  } else {
  if (x0 == xTest) {
    line.y = y0 - r;
    line.h = 2 * r + 1;
    found = true;
  } else {
    int16_t f = 1 - r;
    int16_t ddF_x = 1;
    int16_t ddF_y = -2 * r;
    int16_t x = 0;
    int16_t y = r;
    int16_t px = x;
    int16_t py = y;

    while (!found && (x < y)) {
      if (f >= 0) {
        y--;
        ddF_y += 2;
        f += ddF_y;
      }
      x++;
      ddF_x += 2;
      f += ddF_x;
      // These checks avoid double-drawing certain lines, important
      // for the SSD1306 library which has an INVERT drawing mode.
      if (x < (y + 1)) {
        if (((x0 + x) == xTest) || ((x0 - x) == xTest)) {
          line.y = y0 - y;
          line.h = 2 * y + 1;
          found = true;
        }
      }
      if (y != py) {
        if (((x0 + py) == xTest) || ((x0 - py) == xTest)) {
          line.y = y0 - px;
          line.h = 2 * px + 1;
          found = true;
        }
        py = y;
      }
      px = x;
    }
    //}
  }
  return found;
}

/**************************************************************************/
/*!
    @brief  erase portion of line that lies outside a circle
    @param  x0       Center-point x coordinate of circle
    @param  y0       Center-point y coordinate of circle
    @param  r0       Radius of circle
    @param  xD       x coordinate of vertical line
    @param  yLine    y coordinate of vertical line
    @param  hLine    length of vertical line
    @param  bgColor  16-bit 5-6-5 Color of vertical line
*/
/**************************************************************************/
void eraseLine(int16_t x0, int16_t y0, int16_t r0,
               int16_t xD, int16_t yLine, int16_t hLine, uint16_t bgColor) {
  _line fgLine = {0, 0};
  _line bgLine = {yLine, hLine};

  if (checkFillCircle(x0, y0, r0, xD, fgLine)) {
    if (bgLine.y < fgLine.y) {
      if ((bgLine.y + bgLine.h) < fgLine.y) {
        tft.writeFastVLine(xD, bgLine.y, bgLine.h, bgColor);
      }
      else if ((bgLine.y + bgLine.h) <= (fgLine.y + fgLine.h)) {
        tft.writeFastVLine(xD, bgLine.y, fgLine.y - bgLine.y, bgColor);
      }
      else {
        tft.writeFastVLine(xD, bgLine.y, fgLine.y - bgLine.y, bgColor);
        tft.writeFastVLine(xD, fgLine.y + fgLine.h, (bgLine.y + bgLine.h) - (fgLine.y + fgLine.h), bgColor);
      }
    }
    if (bgLine.y > fgLine.y) {
      if (bgLine.y > (fgLine.y + fgLine.h)) {
        tft.writeFastVLine(xD, bgLine.y, bgLine.h, bgColor);
      } else {
        if ((bgLine.y + bgLine.h) > (fgLine.y + fgLine.h)) {
          tft.writeFastVLine(xD, fgLine.y + fgLine.h, bgLine.h - (fgLine.y + fgLine.h - bgLine.y), bgColor);
        }
      }
    }
    if (bgLine.y == fgLine.y) {
      if (bgLine.h > fgLine.h) {
        tft.writeFastVLine(xD, bgLine.y + fgLine.h , bgLine.h - fgLine.h, bgColor);
      }
    }
  }
  else {
    tft.writeFastVLine(xD, bgLine.y, bgLine.h, bgColor);
  }
}

/**************************************************************************/
/*!
    @brief  draw a filled circle after erasing the previous circle
            only the portion of the previous circle that lies outside the
            new circle will be erased
    @param  x0       Center-point x coordinate of new circle
    @param  y0       Center-point y coordinate of new circle
    @param  r0       Radius of new circle
    @param  fgColor    16-bit 5-6-5 Color of new circle
    @param  x1       Center-point x coordinate of previous circle
    @param  y1       Center-point y coordinate of previous circle
    @param  r1       Radius of previous circle
    @param  bgColor  16-bit 5-6-5 Color to overwrite previous circle
*/
/**************************************************************************/

void moveCircle(int16_t x0, int16_t y0, int16_t r0, uint16_t fgColor,
                   int16_t x1, int16_t y1, int16_t r1, uint16_t bgColor) {
  int16_t f;
  int16_t ddF_x;
  int16_t ddF_y;
  int16_t x;
  int16_t y;
  int16_t px;
  int16_t py;

  tft.startWrite();

  //erase the background circle that lies outside the foreground circle
  eraseLine(x0, y0, r0, x1, y1 - r1, 2 * r1 + 1, bgColor);

  f = 1 - r1;
  ddF_x = 1;
  ddF_y = -2 * r1;
  x = 0;
  y = r1;
  px = x;
  py = y;

  while (x < y) {
    if (f >= 0) {
      y--;
      ddF_y += 2;
      f += ddF_y;
    }
    x++;
    ddF_x += 2;
    f += ddF_x;
    // These checks avoid double-drawing certain lines, important
    // for the SSD1306 library which has an INVERT drawing mode.
    if (x < (y + 1)) {
      eraseLine(x0, y0, r0, x1 + x, y1 - y, 2 * y + 1, bgColor);
      eraseLine(x0, y0, r0, x1 - x, y1 - y, 2 * y + 1, bgColor);
    }
    if (y != py) {
      eraseLine(x0, y0, r0, x1 + py, y1 - px, 2 * px + 1, bgColor);
      eraseLine(x0, y0, r0, x1 - py, y1 - px, 2 * px + 1, bgColor);
      py = y;
    }
    px = x;
  }

  //draw the foreground circle
  tft.writeFastVLine(x0, y0 - r0, 2 * r0 + 1, fgColor);

  f = 1 - r0;
  ddF_x = 1;
  ddF_y = -2 * r0;
  x = 0;
  y = r0;
  px = x;
  py = y;

  while (x < y) {
    if (f >= 0) {
      y--;
      ddF_y += 2;
      f += ddF_y;
    }
    x++;
    ddF_x += 2;
    f += ddF_x;
    // These checks avoid double-drawing certain lines, important
    // for the SSD1306 library which has an INVERT drawing mode.
    if (x < (y + 1)) {
      tft.writeFastVLine(x0 + x, y0 - y, 2 * y + 1, fgColor);
      tft.writeFastVLine(x0 - x, y0 - y, 2 * y + 1, fgColor);
    }
    if (y != py) {
      tft.writeFastVLine(x0 + py, y0 - px, 2 * px + 1, fgColor);
      tft.writeFastVLine(x0 - py, y0 - px, 2 * px + 1, fgColor);
      py = y;
    }
    px = x;
  }

  tft.endWrite();
}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.