Go Down

Topic: 1.44" ST7735 TFT - visible flickering when refreshing, what can be done? (Read 213 times) previous topic - next topic

VioletGiraffe

I've got a chinese 1.44" TFT - very similar to AdaFruit's. In fact, I think it's the same and the Adafruit ST7735 library works great with it right away.

But I have a problem: the screen is too slow to update and flickers too much. Even if I only update it once per second the flickering is still very noticeable and prevents me from achieving visual smoothness, which I will need for my project.

Upon some googling I came across the PDQ GFX library, it claims to be a drop-in replacement for Adafruit's but much better optimized. So I use that in my sketch now. I thing it's a bit better with this library, but not much.

Here's my sketch code for the moment.

Code: [Select]
#define TFT_RST 0  // you can also connect this to the Arduino reset, in which case, set this #define pin to 0!

#include "PDQ_ST7735_config.h"
#include <PDQ_FastPin.h>
#include <PDQ_ST7735.h>

#include <gfxfont.h>
#include <PDQ_GFX.h>

#include <TimerOne.h>

#include <StandardCplusplus.h>
#include <SPI.h>

#include <algorithm>
#include <math.h>

#define analogInPin A0

uint16_t sample = 0, maxSampleValue = 0;

#ifndef _PDQ_ST7735H_
Adafruit_ST7735 tft = Adafruit_ST7735(ST7735_CS_PIN,  ST7735_DC_PIN, TFT_RST);
#else
PDQ_ST7735 tft;
#endif

void setup() {
  // initialize serial communications at 9600 bps:
  Serial.begin(9600);
  while (!Serial);

  // Use this initializer if you're using a 1.8" TFT
  //tft.initR(INITR_BLACKTAB);   // initialize a ST7735S chip, black tab

  // Use this initializer (uncomment) if you're using a 1.44" TFT
  tft.initR(ST7735_INITR_144GREENTAB);   // initialize a ST7735S chip, black tab

  tft.setTextSize(3);
  tft.fillScreen(ST7735_BLACK);

  Timer1.initialize(100L * 1000L); // initialize timer1, 1/30th sec period
  Timer1.attachInterrupt(updateScreen);

  Serial.println("Initialized");
}

template <typename T> void printNumber(T number, uint16_t color, bool newLineAfter = false)
{
  tft.setTextColor(color);
  if (newLineAfter)
    tft.println(number);
  else
    tft.print(number); 
}

inline uint16_t RGB888_to_565(uint8_t R, uint8_t G, uint8_t B)
{
  return
    (((R >> 3) & 0x1f) << 11) |
    (((G >> 2) & 0x3f) <<  6) |
    (((B >> 3) & 0x1f)      );
}

void loop() {
  // read the analog in value:
  sample = analogRead(analogInPin);
  maxSampleValue = std::max(maxSampleValue, sample);

  // print the results to the serial monitor:
  Serial.print("sensor = ");
  Serial.println(maxSampleValue);

  delay(1);
}

void updateScreen()
{
  tft.fillRect(0, 0, 100, 40, ST7735_BLACK);

  static const auto textColor1 = RGB888_to_565(255, 235, 0);
  static const auto textColor2 = RGB888_to_565(255, 0, 200);
 
  tft.setTextSize(3);
  tft.setCursor(0, 0);
  printNumber(sample, textColor1);

  tft.setTextSize(2);
  tft.setCursor(0, 25);
  tft.print("Max: ");
  printNumber(maxSampleValue, textColor2);
}


Note that I have set up a timer using the TimerOne library, it's currently set for 100 ms = 10 FPS. But as I said, the code speed is not the problem, I can get higher FPS. It's flickering that's the problem.

I have then tried to bring down the amount of pixels re-painted in updateScreen() even further. I did two things differently here: moved the static "Max:" label out of the update routine, and instead of clearing the previous contents with fillRect() I just draw the same number in black. That did not bring any improvement to speak of.

You can view the updated code in this gist or in the following code section:

Code: [Select]
#define TFT_RST 0  // you can also connect this to the Arduino reset, in which case, set this #define pin to 0!

#include "PDQ_ST7735_config.h"
#include <PDQ_FastPin.h>
#include <PDQ_ST7735.h>

#include <gfxfont.h>
#include <PDQ_GFX.h>

#include <TimerOne.h>

#include <StandardCplusplus.h>
#include <SPI.h>

#include <algorithm>
#include <math.h>

#define analogInPin A0

uint16_t sample = 0, maxSampleValue = 0;

#ifndef _PDQ_ST7735H_
Adafruit_ST7735 tft = Adafruit_ST7735(ST7735_CS_PIN,  ST7735_DC_PIN, TFT_RST);
#else
PDQ_ST7735 tft;
#endif

void setup() {
  // initialize serial communications at 9600 bps:
  Serial.begin(9600);
  while (!Serial);

  // Use this initializer if you're using a 1.8" TFT
  //tft.initR(INITR_BLACKTAB);   // initialize a ST7735S chip, black tab

  // Use this initializer (uncomment) if you're using a 1.44" TFT
  tft.initR(ST7735_INITR_144GREENTAB);   // initialize a ST7735S chip, black tab

  tft.setTextSize(3);
  tft.fillScreen(ST7735_BLACK);
  tft.setCursor(0, 25);
  tft.setTextSize(2);
  tft.print("Max: ");

  Timer1.initialize(100L * 1000L); // initialize timer1, 1/30th sec period
  Timer1.attachInterrupt(updateScreen);

  Serial.println("Initialized");
}

template <typename T> void printNumber(T number, uint16_t color, bool newLineAfter = false)
{
  tft.setTextColor(color);
  if (newLineAfter)
    tft.println(number);
  else
    tft.print(number); 
}

inline uint16_t RGB888_to_565(uint8_t R, uint8_t G, uint8_t B)
{
  return
    (((R >> 3) & 0x1f) << 11) |
    (((G >> 2) & 0x3f) <<  6) |
    (((B >> 3) & 0x1f)      );
}

void loop() {
  // read the analog in value:
  sample = analogRead(analogInPin);
  maxSampleValue = std::max(maxSampleValue, sample);

  // print the results to the serial monitor:
  Serial.print("sensor = ");
  Serial.println(maxSampleValue);

  delay(1);
}

const auto textColor1 = RGB888_to_565(255, 235, 0);
const auto textColor2 = RGB888_to_565(255, 0, 200);

void updateScreen()

  tft.setTextSize(3);
  tft.setCursor(0, 0);
  printNumber(10, 0);
  tft.setCursor(0, 0);
  printNumber(10, textColor1);

  tft.setTextSize(2);
  tft.setCursor(70, 30);
  printNumber(20, 0);
  tft.setCursor(70, 30);
  printNumber(20, textColor2);
}


What can I do to improve the perceived smoothness of this screen's re-draw? Guess I'll have to shoot 60 FPS video of the screen to see what exactly it's doing, but it looks like it goes to black for significant amount of time before drawing the new contents. It's as if fillRect() is carried out extra fast, and then it takes a long while for the numbers to show up.

Note that my project requires drawing complex geometry across the whole screen, and optimizing its re-draw (e. g. only updating the pixels that actually changed) will not be feasible because Arduino Uno lacks the RAM required for that (calculating diff between two frames, even in 1-bit color).
What I'm aiming to achieve in the end is this (note how smooth it is): https://youtu.be/EnvhEgjrHsw

Granted, that's an OLED display and a different controller. But is it really not doable with a TFT?

The sketch and all the required libraries are on Github in case you care to fiddle with it: https://github.com/VioletGiraffe/ArduinoAudioSpectrumAnalyzer

david_prentice

I am not too sure what a const auto means.

You would normally use tft.setColor(foreground, background) if you want to redraw the background.
And think carefully about minimising code in the update_screen()

If your library or Font only does transparent printing,  you would fill the background first (i.e. to rub out the previous text)

Yes,  you could just redraw the text in the background colour.   However it is normally quicker to fill a rectangle than to render text transparently.   (which requires addressing and drawing individual pixels)

Your OLED video should be quite possible to do with a TFT.   Just remember that the TFT is colour.  The OLED is monochrome.    It takes at least 16 bytes to draw 8 pixels in a filled rectangle.   The OLED needs 1 byte to draw 8 pixels.
Avoid drawPixel().  If you can use a filled rectangle it is far more efficient.
Likewise,  if you can blit a buffer full of pixels with pushColors()

Also remember that TFTs are bigger.    This needs more data traffic.

David.

VioletGiraffe

Thanks, David. I'll experiment some more, especially with pushColors() which is something I have not tried yet.
I'll be happy enough if it works within a 128x64 region - half my screen - which is the resolution of the OLED from the video. It's also a dual-color OLED, which means 2 bits per pixel (still much less than a color TFT, of course - 8 times less).

I have a feeling that raw data rate is not the issue here. It's either a bug of the TFT controller (maybe it can't physically update the screen fast enough, or this process is not implemented optimally), or a bug of the library (then we can solve it by implementing the crucial stuff manually - not that I'm fond of this notion).

I am not too sure what a const auto means.
Good point. I meant "auto" as a C++11 keyword - it's a type specifier, not a storage class specifier. But if the compiler is not C++11-compliant then it would interpret this incorrectly (and I'd probably end up with default-int instead of the desired type).

david_prentice

Regarding your update speed.

128x128 pixels @ 2 SPI per pixel take 32.8ms with SCK=8Mhz.
60 fps means 16.7ms per frame.

So you need to minimise the screen writes.   Only updating the area(s) that change.

You have to strike a balance between intelligence and SPI traffic.
The AVR USART_MSPI can manage 8MHz.   The regular SPI has gaps.   Library methods might not be optimal.   In fact they might be appalling.

I would concentrate on the bar display rather than text.   The human eye sees the moving graphic display.   You are never going to read text at 60fps.   Just update text at 5fps.

David.

VioletGiraffe

Agreed, text doesn't need to update often. And even the bar graphics should look smooth enough at 20 or even 15 FPS (while, according to your calculations, 30 FPS - 33 ms - is theoretically possible).

As I pointed out, it's not just the refresh speed that's the problem, it's flickering. It's very noticeable even at 1 FPS. I'll record the flickering and post a video in a couple hours.

david_prentice

The flickering is due to your rubout and redraw method.

OTOH,  you might have chosen an update that is close to the Frame Rate of the display.

David.

VioletGiraffe

You are probably right - after I display the black rectangle (background) there's a visibly long delay before the new text appears, and it looks like flickering (it flickers to black and then back on). But how else can I "rub" the invalid pixels out when I don't have the RAM to compare two frames and generate diff?

david_prentice

All Adafruit_GFX style libraries have a setColor(foreground, background) method.

The regular setColor(foreground) does not draw the background.  i.e. transparent mode.

Just read the Adafruit docs.

David.

bodmer

As David say's draw the characters with background.  Also a small gain can be obtained by pre-calculating or defining the colours outside of the functions.

Try these alternative functions:

Code: [Select]

template <typename T> void printNumber(T number, uint16_t color, bool newLineAfter = false)
{
  tft.setTextColor(color, TFT_BLACK);
  if (newLineAfter)
    tft.println(number);
  else
    tft.print(number);
}


void updateScreen()
{
  tft.setTextSize(3);
  tft.setCursor(0, 0);
  printNumber(sample, TFT_YELLOW);

  tft.setTextSize(2);
  tft.setCursor(0, 25);
  tft.print("Max: ");
  printNumber(maxSampleValue, TFT_MAGENTA);
}


You will have to pre-calculate the colours. Flicker should then be almost negligible.

You will also need to add spaces at the beginning of numbers to pad them and erase old numbers, this also has the advantage of neatly right justifying the numbers so the units digit does not jump position.

You should also declare variables that are used in interrupt routines as volatile, search the web to find out why. When updating the variables outside of the interrupt loop you should switch off interrupts as operations on variables of more than 8 bits are not "atomic" on 8 bit micro-controllers. Again search the web to find out what this means.
Formerly Rowboteer (now a broken user profile!)

VioletGiraffe

Thanks for the tips! I have spent most of the weekend and yesterday evening trying to make the screen update in a way that doesn't look utterly terrible. With some non-versatile tricks (it'll go to waste if I want to display some other shapes) I've managed to draw a set of vertical bars of varying length, and I cut down the updateDisplay() routine duration from 50+ ms to 12-15 ms. I was stuck with the text which flickers no matter what I do, but it seems that your solution will help! Going it to test it first thing when I get home.

P. S. The e-mail notification option got turned off for some reason, that's why I didn't see your replies and ended up stumbling in the dark for the better part of the last two days :/

VioletGiraffe

On a side note, I still find the reaction of the display to various commands odd. For example, how come

Code: [Select]

for (int i = 0; i < 128; ++i)
  tft.drawFastVLine(i, tft.height() - 80, 80, RGB_to_565(255, 255, 200));


is very fast (like 5 ms or even less), and

Code: [Select]

for (int i = 0; i < 128; ++i)
{
  tft.drawFastVLine(i, 0, 127, RGB_to_565(0, 0, 0));
  tft.drawFastVLine(i, tft.height() - 80, 80, RGB_to_565(255, 255, 200));
}


is 50+ ms and flickering is back again?

Go Up
 


Please enter a valid email to subscribe

Confirm your email address

We need to confirm your email address.
To complete the subscription, please click the link in the email we just sent you.

Thank you for subscribing!

Arduino
via Egeo 16
Torino, 10131
Italy