Techniques for drawing "snow" static on TFT without tearing?

I have a 128x128 ST7735 display that I'm driving with a Seeeduino Xiao (SAMD21). I'd like to draw static fuzz, of the type found on old televisions - i.e. random pixels or squares of black and white.

I tried putting bitmaps in PROGMEM, and using the Adafruit GFX library, drawBitmap() is far too slow, and results in tearing.

drawXBitmap is faster, but still has noticeable tearing.

Drawing individual pixels in a loop also has a tearing effect.

My next steps to try are:

  • Creating smaller tiles of bitmap static fuzz, and updating random tiles on the screen
  • draw larger rectangles instead of pixels. i.e. perceived lower resolution

Does anyone have other ideas or techniques you can recommend?

Hi mst123,

I was thinking of using the random generator to generate an x-position and an y position and then applying a draw.Pixel (x,y,WHITE) or something similar, with some decoration around it to make the noise pseudorandom,

e.g., in the loop:
rand_x = random (128);
rand_y = random (128);
tft.drawPixel (rand_x,rand_y, WHITE);
delay(10); // or something depends on the speed

Of course the program must remember previous positions of the 'noise' pizels or else your screen will become completely white in seconds!
success, photoncatcher

Hey, thanks for the reply.

I didn't describe what I was looking for very well, but to be clear it's a full-screen of random black/white pixels. Like this: TV Static Noise 10 hours , HD 1080p - YouTube

You don't say which library you are using.

You could set a window and then call random() e.g. random(255)
According to the library you pushPixel(), writePixels(), pushPixels(), ..., pushImage(), ...

This means that you only call random(255) for every 8 pixels. WHITe or BLACK according to the bit value.

If you post a link to the actual display.
And quote the name of the library.

... you would get an accurate reply,

David.

Hi David,

I tried drawPixel in a rush but couldn't get it in the right syntax (yet)
regards, photoncatcher

david_prentice:
If you post a link to the actual display.
And quote the name of the library.

The specific PCB is this one: Amazon.com
However this is just a prototype so if there are meaningful differences between ST7735 boards, I'd love to hear it. I'm struggling to imagine how the answer is going to differ depending on the board, but if it does then point me to the ones that'll help (if similar cost in bulk: ~$1-4).

Similar with libraries... I'm not married to any library in particular. I've experimented with a couple, but most of the trials were with the Adafruit_GFX + Adafruit_ST7735.h
If a particular library has some tricks up its sleeve to combat the general issues with trying to write large areas of the screen at one time, I'd love to hear it.

(EDIT: And going even further, I'm open to using a board with a different TFT driver if it helps, provided similar pricing and availability and drivers for the samd21.)

Failing that, my next hope was that somebody had a clever idea to fake it. e.g. the tiling option I mentioned, though the jury is out on whether that'll look good.
Another technique I plan to try next is write a bitmap image of pre-rendered static to the screen (since it seems to be faster than doing random() + drawing a pixel 128x128 times), then just use every spare cycle to write a random pixel. I'm guessing it will look a bit too static in appearance, but I'll give it a try.

Due to the number of pixels, it would be a good idea to call an update for every two pixels. With a teensy 4 @ 600 Mhz, I have used the ST7735_t3 library.

const int BufferX = 128;
const int BufferY = 128;

int Buffer[BufferX][BufferY];

#include <ST7735_t3.h>
#define TFT_RST 8
#define TFT_DC  9
#define TFT_CS 10  //ftf
ST7735_t3 display = ST7735_t3(TFT_CS, TFT_DC, TFT_RST);

uint16_t  color;
#define BLACK           0x0000
#define WHITE           0xFFFF

void setup() {
  Serial.begin(115200);
  display.initR(INITR_18BLACKTAB_OFFSET);
  display.setRotation(1); 
  display.fillScreen(ST7735_BLACK);
  
  for ( int i = 0; i < BufferY; ++i )
  {
    for ( int j = 0; j < BufferX; ++j )
    Buffer[i][j] = random(0,2);
  }

  for ( int i = 0; i < BufferY; ++i ) 
  {
      for ( int j = 0; j < BufferX; ++j )
      {
       Serial.print (Buffer[ i ][ j ] );
       if(Buffer[ i ][ j ]==0){color = BLACK;}else{color = WHITE;}
       display.drawPixel(j, i, color);
      }
      Serial.println (" " ) ;
   }
  
}
void loop()
{

  for ( int i = 0; i < BufferY; ++i )
  {
    //for ( int j = 0; j < BufferX; ++j )
    for ( int j = 0; j < BufferX; j=j+2 )
    Buffer[i][j] = random(0,2);
  }

  for ( int i = 0; i < BufferY; ++i ) 
  {
      for ( int j = 0; j < BufferX; ++j )
      {
       //Serial.print (Buffer[ i ][ j ] );
       if(Buffer[ i ][ j ]==0){color = BLACK;}else{color = WHITE;}
       display.drawPixel(j, i, color);
      }
      //Serial.println (" " ) ;
   }
}

I have also tried a cyclical reproduction of 27 jpg images of 128x128 pixels, obtained from this page:
https://pinetools.com/en/generator-map-of-random-bits

I am using the JPGDEC library to load the images from the SDIO reader that can be installed on the teensy 4.

As soon as possible I upload a couple of videos

I tried writing it to a buffer just as you did, but the tearing is still very prevalent. Simply going through all those writePixel or drawPixel calls is what's taking up the time (as opposed to the random()'s which were pulled out of the loop).

If yours looks good in real time, then either the "_t3" library or the teensy itself is the difference. I came across the library but haven't tried it yet since I assumed it wouldn't work with a SAMD21 seeeduino xiao. But perhaps I should try.

The sparse docs (Getting Started with Seeed Studio XIAO SAMD21 - Seeed Wiki) seem to imply it has hardware SPI support, and I followed their pin recommendations. But perhaps this is another thing to dig into. I have another samd21 board (adafruit itsybitsy) with known hardware SPI support that I can try to rule out SPI issues.

Edit: by the way, if I had more memory I'd try double buffering and conditionally write the pixels only if they changed.. I suspect that'd make a fair difference. Perhaps I'll try it with a 64x64 grid of double-sized "pixels".

Yeah, double buffering and conditionally drawing makes a huge difference.

It's still too slow with 2px "pixels" (i.e. 64x64 grid), but with 4px "pixels" and a 32x32 grid it's flawless. And just to verify, I tried doing a single buffer and sure enough even at 32x32 it's tearing really bad.

So in short: double buffer a 32x32 grid and it looks perfect.

That said, I can't afford to tie up that much memory in a global buffer since this is only a tiny portion of the functionality I'm putting on the microcontroller. But it was a fun experiment.

void snowDB() {

int i,j;
for (i = 0; i < NUM_PIXELS; i++) {
for (j = 0; j < NUM_PIXELS; j++) {
this_buffer*[j] = random(0,2);*

  • }*
  • }*
  • for (i = 0; i < NUM_PIXELS; i++) {*
  • for (j = 0; j < NUM_PIXELS; j++) {*
    if (this_buffer[j] != last_buffer*[j]){
    if (this_buffer[j] == 0) {
    tft.fillRect(PIXEL_SIZEi,PIXEL_SIZEj,PIXEL_SIZE,PIXEL_SIZE,TFT_BLACK);
    _ } else {_
    tft.fillRect(PIXEL_SIZEi,PIXEL_SIZEj,PIXEL_SIZE,PIXEL_SIZE,TFT_WHITE);
    _ }
    }_

    last_buffer[j] = this_buffer[j];
    _ }
    }
    }*_

Hi mst123, David

I created a sketch for the Uno with a nameless 320x240 TFT display shield, ILI9341 controller, using the tft.writePixel (x,y,COLOR) instruction, combined with the random function.
Here is the sketch, and a picture of the effect on my TFT

regards, photoncatcher

// Uno_random noise speckles_nn.ino 
//
// microcontroller: Arduino Uno
// 2.8 inch TFT shield ILI 9341 controller - 320*240
//
// uses mcufriend_kbv library by David Prentice 
// public domain
// by Floris Wouterlood
// thanks to everybody who made this possible 

   #include <Adafruit_GFX.h>                                                   // library                             
   #include <MCUFRIEND_kbv.h>                                                  // library

   MCUFRIEND_kbv tft;
  
   #define LCD_CS A3 
   #define LCD_CD A2 
   #define LCD_WR A1 
   #define LCD_RD A0 
   #define LCD_RESET A4 

// some principal color definitions
// RGB 565 color picker at https://ee-programming-notepad.blogspot.com/2016/10/16-bit-color-generator-picker.html
   #define WHITE       0xFFFF
   #define BLACK       0x0000
   #define BLUE        0x001F
   #define RED         0xF800
   #define GREEN       0x07E0
   #define CYAN        0x07FF
   #define MAGENTA     0xF81F
   #define YELLOW      0xFFE0

  int x1, x2, x3, x4, y1, y2, y3, y4;
  

void setup() {


// TFT controller detection
   uint16_t ID;
   ID = tft.readID ();                                                         // valid for Uno shields  
   Serial.begin (9600);
   Serial.println ("starting TFT display");
   Serial.print  ("controller ID = 0x");
   Serial.println (ID,HEX); 
   
   tft.reset ();                                                               // reset TFT and begin
   tft.begin (ID);  
   tft.setRotation (2);                                                        // display in portrait
   tft.fillScreen (BLACK); 
}


void loop() {
   x1 = random (240);
   y1 = random (320);
   tft.writePixel (x1,y1, WHITE);

   x2 = random (240);
   y2 = random (320);
   tft.writePixel (x2,y2, BLACK);

   x3 = random (240);
   y3 = random (320);
   tft.writePixel (x3,y3, MAGENTA);

   x4 = random (240);
   y4 = random (320);
   tft.writePixel (x4,y4, BLACK);
}

Go go as simple as possible...

#include <ST7735_t3.h>
#define TFT_RST 8
#define TFT_DC  9
#define TFT_CS 10  //ftf
ST7735_t3 display = ST7735_t3(TFT_CS, TFT_DC, TFT_RST);

uint16_t  color;
#define BLACK           0x0000
#define WHITE           0xFFFF

void setup() {
  Serial.begin(115200);
  display.initR(INITR_18BLACKTAB_OFFSET);
  display.setRotation(1); 
  display.fillScreen(ST7735_BLACK);
}

int Px1, Px2, Py1, Py2;

void loop()
{
   Px1 = random (128);
   Py1 = random (128);
   display.drawPixel (Px1,Py1, WHITE);

   Px2 = random (128);
   Py2 = random (128);
   display.drawPixel (Px2,Py2, BLACK);
}

There are no horizontal or vertical sweeps, just that it is slow.

By reducing the area covered by the buffer, a much faster scan of the entire screen can be achieved.

uint16_t  color;
#define  BLACK          0x0000
#define WHITE           0xFFFF

const uint8_t  Xdisplay_width = 100;
const uint8_t  Ydisplay_height = 100;

void loop() {
  
  for (int y = 0; y < Ydisplay_height; y++)
  {
    for (int x = 0; x < Xdisplay_width; x++)
    {
      display.drawPixel(x, y, ((rand() & 0x01) == 0) ? WHITE : BLACK);
    }
  }
}

The encoding is simpler, but when used near the limits of the pixel surface of the screen, a sweep appears in both x and y, which seems more natural to me, similar to that seen in those old TV monitors . Up to 110x110 px I find it to be natural behavior, maybe up to 115x115 px.

I stick with the 105x105 resolution, it looks great, I would center that surface in the 128x128 px space, leaving a black or gray frame as the background

hi TFTLCDCyg,

That's a pretty simple and nice sketch. It produces the fine grain snow that invites one to immediately check the wiring.....
Actually I would like on my 320240 TFT bigger grains (22 blocks of pixels) creating the impression of random (pseudo)noise. That would imply the use of small bitmaps or some complicated stuff remembering previous positions of displayed pixels; I am not sure whether my Uno is fast enough to create a realistic effect. Any suggestion?

regards, photoncatcher.

My apologies. My reply #3 was expecting to write a fresh set of 16384 random pixels one set after another.

This is considerable effort for 2048 random() calls in every set.
And you can notice the fresh set being drawn top to bottom.

TFTLCDCyg's first sketch in #10 actually gives a better snow effect. And you don't notice tearing or regular top-bottom drawing.

If I run both algorithms on a Teensy4.0 the random() call overhead is trivial.
However a Uno struggles.

void setup(void)
{
    tft.begin(tft.readID());
    tft.setRotation(0);
    tft.fillScreen(TFT_DARKGREY);
}

void loop(void)
{
    uint16_t buf[8];
    uint8_t first = 1, mask, val;
    tft.setAddrWindow(0, 0, 127, 127);
    for (uint16_t cnt = 128 * 16; cnt; cnt--) {
        val = random(256);
        mask = 0x80;
        for (uint8_t i = 0; i < 8; i++) {
            buf[i] = (val & 0x80) ? 0xFFFF : 0x0000;
            val <<= 1;
        }
        tft.pushColors(buf, 8, first);
        first = 0;
    }
}

@photoncatcher,

Instead of drawPixel() you call fillRect() on the 240x320 screen. i.e. 2x2 pixel rectangle instead of 1x1 rectangle.
You loop in steps of 2 pixels at a time.

It will still look crap on a Uno compared to Teensy.
Why do you want snow ?

@mst123,

Running both algorithms on a 48MHz M0_Pro works somewhere in between 16MHz Uno and 600MHz Teensy4.0
i.e. random() calculation time is better than Uno. TFTLCDCyg's snow looks best but still a bit slow.

David.

david_prentice:
TFTLCDCyg's first sketch in #10 actually gives a better snow effect. And you don't notice tearing or regular top-bottom drawing.

@mst123,

Running both algorithms on a 48MHz M0_Pro works somewhere in between 16MHz Uno and 600MHz Teensy4.0
i.e. random() calculation time is better than Uno. TFTLCDCyg's snow looks best but still a bit slow.

David.

The speed definitely seems to be an issue. The first sketch on #10 is simply too static looking. i.e. not enough pixels are changing fast enough and it looks more like a static image than an animation.

The second sketch have extreme tearing for anything over 35'ish pixels for me.

I think for now I'll pull the plug on the static snow noise. I'm creating a series of animated simple "tv shows" and wanted to show this static for a moment between changing channels, but it's proving to be more trouble than it's worth. Either that or I'll see about switching to a faster processor.

Doing this with an OLED screen was trivial and worked perfectly since you can write the entire screen without flicker or tearing on even the slowest hardware (e.g. attiny85). Here's the first prototype if anyone's curious. Unfortunately this is on an audio device and those OLEDs are SO, SO, SO NOISY and I eventually threw in the towel in battling all those problems.

Thanks everyone! This was a fun set of experiments and I appreciate you giving it a try too.

One more time:

#include <ST7735_t3.h>
#define TFT_RST 8
#define TFT_DC  9
#define TFT_CS 10
ST7735_t3 display = ST7735_t3(TFT_CS, TFT_DC, TFT_RST);

#define  BLACK         0x0000
#define  WHITE         0xFFFF

#define PIXEL_SIZE    2
const uint8_t  Xdisplay_width = 128/PIXEL_SIZE;
const uint8_t  Ydisplay_height = 128/PIXEL_SIZE;

void setup() {
  Serial.begin(9600);
  display.initR(INITR_18BLACKTAB_OFFSET);
  display.setRotation(1); 
  delay(1000);
  display.fillScreen(BLACK);
}

void loop() {
  for (int y = 0; y < Ydisplay_height; y++)
  {
    for (int x = 0; x < Xdisplay_width; x++)
    {
      display.fillRect(PIXEL_SIZE*x,PIXEL_SIZE*y,PIXEL_SIZE,PIXEL_SIZE,((rand() & 0x01) == 0) ? WHITE : BLACK);
    }
  }
}

TFTLCDCyg:
One more time:

Yeah, this is an unbuffered version of #8 but it has terrible tearing on my hardware, even with 4px sized pixels. It looks perfect with double buffering though, but uses too much memory.
It looks great with 8px pixels though, so I may go with that if I don't abandon the idea altogether.

You need to think about visual effect rather than any arithmetic accuracy.

e.g. coloured patterns, logos, photos, ...
or perhaps a sequence of abstract dots.

There is no need to have perfect randomness. Hence you could reduce the time spent by random().

Yes, OLED animations look quite nice. The processing time is spent in drawing to the buffer.
Blitting the pixels from the buffer goes in one swift operation.
TFT has less shadowing than OLED but 16-bit colour takes 16x as long to write as monochrome 1-bit.
Since you can address each colour pixel individually TFT applications need to be designed differently.

David.

Unfortunately the SPI library is very basic, so you have to arm yourself with courage and try the use of two-dimensional matrices. The last sketch allows adjusting the pixel dimensions and reduces the call for color changes with this instruction for random change:

((rand () & 0x01) == 0)? WHITE: BLACK


Teensy 4@600 MHz, ST7735S, ST7735_t3 lib. 128x128 px, pitch: 2x2 px

However what is needed is more processing power, the teensy 4 can work from 150 MHz to 816 MHz without an active heatsink.

This is the result of the last sketch: dimensions of each pixel: 2x2 px, 128x128 arrangement

The cell phone camera is not synchronized to the speed with which the pixels appear on the screen, this generates a sloping cross band and an apparent increase in speed, which do not really exist in the TFT. Keep in mind that 128x128 = 16,384 pixels are drawn in each step

Hi TFTLCDCyg,

I saw your movie. Impressive noise! Just as noise would apppear on an old CRT. I expected at the end the face of Darth Vader coming through... "All resistance is futile - you will be assimilated....." (although the latter is a Star Trek quote).

regards, photoncatcher

It is possible to "overclock" the ST7735 so that faster screen updates are then possible. The data sheet gives a 16MHz maximum but they work at 27MHz reliably and often at 40MHz.

David's sketch is very efficient compared to drawing filled rectangles because setting the bounding box requires 11 bytes plus then the pixels. For small rectangles this is inefficient, for example a 2x2 rectandgle required 11 bytes address window and 8 bytes for the pixels.

This is an adaption of David's sketch which gives a small performance boost for 32 bit processors (ESP32, STM32 etc):

#include <SPI.h>
#include <TFT_eSPI.h>
TFT_eSPI tft;

void setup(void)
{
  Serial.begin(115200);
  tft.begin();
  tft.setRotation(0);
  tft.fillScreen(TFT_DARKGREY);
  tft.setAddrWindow(0, 0, 128, 128);
  tft.startWrite();
}

void loop(void)
{
  static uint32_t c = 0;
  static uint32_t dt = millis();

  uint16_t buf[32];
  uint32_t mask, val;

  for (uint16_t cnt = 128 * 4; cnt; cnt--) {
    val = random(65536) << 16 | random(65536);
    mask = 0x80000000;
    for (uint8_t i = 0; i < 32; i++) {
      buf[i] = (val & 0x80000000) ? 0xFFFF : 0x0000;
      val <<= 1;
    }
    tft.pushPixels(buf, 32);
  }

  c++;
  if (c > 99) {
    Serial.println(100.0 / ((millis() - dt) / 1000.0));
    c = 0;
    dt = millis();
  }
}

This prints the frame rate.

The ESP32 manages 96fps when every pixel of the 128x128 display is updated per frame, not bad for a $5 processor board. This is at a 26.66MHz SPI clock, so the theoretical maximum is 102fps with no looping delays. This is clearly faster than an old TV frame rate so delays could be added, other code run or a lower SPI clock rate used.