A little FFT demo for 480x320 TFT (HX8357D)

Status
Not open for further replies.

MarkT

Well-known member
teensy_fft_display.jpg

Just a little hacked together example of AudioAnalyzeFFT1024, using the Adafruit HX8357D 480x320 display.
the grid is 23 x 16 pixels, and as the FFT bins are spaced approx 43Hz apart, each vertical div is 1kHz, and the
vertical scale is set to 5dB/div.

Source is an electret mic soldered to Audio shield rev D, hiding underneath by stacking header breakout under
the Teensy 4.0 (@ 150MHz)

The function at the end, display_fft(), does the work of pulling the FFT values and plotting them.

Code:
#include <Audio.h>
#include <SPI.h>
#include "Adafruit_GFX.h"
#include "Adafruit_HX8357.h"



// Audio setup using audio shield ver D (which uses SPI pins 10-13 and also 6-8, 15, 18-21, 23

AudioAnalyzeFFT1024  fft ;
AudioInputI2S        i2s1 ;
AudioOutputI2S       i2s2 ;
AudioControlSGTL5000 sgtl5000 ;

AudioConnection patchcord1 (i2s1, 0, fft, 0) ;


// Display pin assignment:

// The audio shield uses pin 10 as SPI CS for uSD, so we use CS = 14 for display SPI
// 16 for display reset

#define TFT_DC  9
#define TFT_CS  14
#define TFT_RST 16

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


void setup() 
{
  tft.begin () ;
  tft.setRotation(3);  // orient as landscape, 1 or 3 depending on which way up
  draw_grid (true) ;

  start_audio () ;
}

void loop(void) 
{ 
  if (fft.available())
    display_fft() ;
}

void start_audio ()
{
  AudioMemory (12) ;
  fft.windowFunction (AudioWindowHanning1024) ;
  
  sgtl5000.enable ();
  sgtl5000.inputSelect (AUDIO_INPUT_MIC);  // microphone needed on Audio shield
  sgtl5000.micGain (55); // microphone gain 0 .. 63
  sgtl5000.volume (0.5); // for headphone socket
}


// drawing graph

#define PIXELS_PER_KHZ 23  // approximately,  1000 / (44100 / 1024)
#define PIXELS_PER_DIV 16  // pixels per vertical division

#define gw (20 * PIXELS_PER_KHZ)  // graph width
#define gh 19 * PIXELS_PER_DIV    // graph height

#define DB_PER_DIV 5
  
uint16_t text_colour = tft.color565 (128, 255, 128) ;
uint16_t grid_colour = tft.color565 (128, 48, 0) ;
uint16_t grid2_colour = tft.color565 (192, 100, 0) ;

// draw graticule for display of FFT
void draw_grid (bool clear)
{
  int w = tft.width();
  int h = tft.height();
  Serial.print (w) ; Serial.print (", ") ; Serial.println (h) ;

  if (clear)
    tft.fillScreen(HX8357_BLACK);

  tft.setTextColor (text_colour);  
  tft.setTextSize (1);
  int kHz = 0 ;
  for (int i = 0 ; i <= gw ; i += PIXELS_PER_KHZ)
  {
    if ((i/PIXELS_PER_KHZ % 5) == 0)
    {
      tft.drawLine (i, 0, i, gh+7, grid2_colour) ;
      tft.setCursor (i-2, gh+7) ;
      tft.printf ("%i", kHz) ;
    }
    else
      tft.drawLine (i, 0, i, gh, grid_colour) ;
    kHz += 1 ;
  }

  int dB = 0 ;
  for (int i = 0 ; i <= gh ; i += PIXELS_PER_DIV)
  {
    tft.drawLine (0, i, gw, i, grid_colour) ;
    tft.setCursor(gw-14, i-2);
    tft.printf("%4i", dB);
    dB -= DB_PER_DIV ;
  }
}

// redraw a 16 bit wide column of the graticule
void draw_part_grid (int x)
{
  int h = tft.height();
  for (int i = x ; i < x+16 ; i ++)
    if ((i % PIXELS_PER_KHZ) == 0)
      tft.drawLine (i, 0, i, gh, (i/PIXELS_PER_KHZ % 5) == 0 ? grid2_colour : grid_colour) ;

  for (int i = 0 ; i <= gh ; i += PIXELS_PER_DIV)
    tft.drawLine (x, i, x+15, i, grid_colour) ;
}


#define MIN_DB (-85)   // clip dB values to this for 0 samples.

// convert FFT amplitude values to dB scale, 0dB = full scale
int dB_mag (float ampl)
{
  ampl *= 1.41421 ;  // convert to rms.
  int res = ampl == 0.0 ?
    round (MIN_DB / DB_PER_DIV * 16) : 
    round (16 * log10 (ampl) * 20 / DB_PER_DIV) ;  // 16 pixels / div vertical scale
  return constrain (res, -gh, 0) ;
}


void display_fft()
{
  static int prev_trace[gw+1] ;  // record previous trace to erase it next time round
  
  int i = 1 ;
  tft.drawLine (i-1, prev_trace[i-1], i, prev_trace[i], HX8357_BLACK) ;
  int pp = 0 - dB_mag (fft.read (0)) ;
  prev_trace[0] = pp ;
  while (i <= gw-16)
  {
    // redraw in columns, 16 pixels wide, for flicker-free refresh
    // erase last plot
    for (int j = 1 ; j < 17 ; j ++)
      tft.drawLine (i+j-1, prev_trace[i+j-1], i+j, prev_trace[i+j], HX8357_BLACK) ;

    // redraw grid for this column
    draw_part_grid (i-1);

    // plot new fft data
    for (int j = 0 ; j < 16 ; j ++)
    {
      int p = 0 - dB_mag (fft.read (i+j)) ;
      prev_trace[i+j] = p ;
      tft.drawLine (i+j-1, pp, i+j, p, HX8357_GREEN) ;
      pp = p ;  
    }
    i += 16 ;
  }
}
 
Thanks for posting this.

It's a nice little demo and just what I needed to show someone. Porting to my LCD was trivial and saved me heaps of time. :)
 
I've enhanced the example to include a higher resolution FFT (uses 32 bit fixed point internally), included as .cpp and .h files (including
a generic window function implementation). (attached as a zip)

The example now toggles between Audio library FFT1024 (green) and higher res FFT (32 bit, 4096 bins) in yellow, every 4 seconds,
to show the difference in noise floor you get.

P1030268.JPG

And details of the difference (the little peak is a spur the audio shield rev D seems to generate for the microphone input at about 17.5kHz)
P1030265.JPGP1030257.JPG


I should clarify the issue with the existing FFT is that is uses internal 16 bit fixed point to work with a 16 bit input
signal - but rounding errors accumulate across the passes of the FFT, lowering the bit resolution significantly below
16 bits, and introducing the blocky artifacts visible in the green trace. Using 32 bit internal values prevents this
lose of accuracy and allows full details of the signal to be revealed.
 

Attachments

  • fft_example.zip
    9.9 KB · Views: 118
Mark, nice work, thanks
I added to your code a little and messaged to the 320 by 240 display Paul sells.
I'm trying to understand why the gfft's/second seem to top out at 150MHz cpu while the fft/second keep going higher but still not close to linear.

fft speed.pnggfft4096.png

Any thoughts?
 
The gfft are your AudioAnalyzeFFT objects with the size appended.
Added globals and a few additions to the main loop:


Code:
int fft_count;
bool is_gfft=true;

void loop(void) { 
  if (millis() & 0x1000){  // switch between the two FFTs every 4 seconds.
    if(is_gfft==true){
      Serial.print("gfft's = ");Serial.println(fft_count);
      fft_count=0;
      is_gfft=false;
    }
    if (fft.available()){
      display_fft(false) ;
      fft_count++;
    }
  }else{
    if(is_gfft==false){
      Serial.print("fft's = ");Serial.println(fft_count);
      fft_count=0;
      is_gfft=true;
    }
    if (gfft.available()){
      display_fft (true) ;
      fft_count++;
    }
  }
}
 
Just ran again with the display_FFT(xxx) lines commented out and got the same number 45 for the gfft(4096) and a even higher 353 for the audio library fft.

Just realized I posted the wrong graph. The graph shows the results per 4 seconds not 1 second in all cases.
 
Just had a thought about this - I think I use new/delete for the variable sized FFT to get its data vectors, so
its not using tightly coupled memory. I suspect its limited by memory latency/bandwidth?
 
Status
Not open for further replies.
Back
Top