Saturday, May 21, 2016

ADC IIR LPF SPI TFT LCD GUI - Part 1

I have a project in which I need to read a signal, do some processing and display the results using a bar graph. After some fiddling with an Arduino UNO and researching the AVR microcontrollers they use, I settled on a two-MCU architecture. I use an Arduino Nano to acquire and process the data and an Arduino Mega to display the results and interface with the user. How many acronyms can be squeeze into a title?

ADC - analog-to-digital converter
IIR - infinite impulse response
LPF - low pass filter
SPI - serial peripheral interface
TFT - thin film transistor
LCD - liquid crystal display
GUI - graphical user interface

My first problem: how to get two Arduino’s to chat with each other. My work is based upon (copied from) a thorough and accurate forum post. The motivation to use SPI came from wanting to learn more about it. In researching the TVout library, I found someone generated an NTSC signal with the hardware based SPI master signal.  Given my current fascination with resurrecting my Atari days, I had an idea of trying to recreate a simplified ANTIC-like system on an Arduino in black and white. SPI seems the way to go there. But, back to my project.



The system here uses a Nano to read an analog voltage and apply a low pass filter. The Mega polls the Nano for the LPF output over SPI and displays the value on a bar graph. I offloaded the ADC work to the Nano thinking I would eventually use an interrupt-driven ADC to get a constant sampling rate. Because the TFT polls various analog channels to read the touchscreen, it seemed best to just off-load the other ADC work to a different microcontroller. The TFT fits on the Mega and leaves the header with the SPI interface unobstructed.

The SPI connections on the Nano are located on the ICSP header or pins D11-D13 and Slave Select is D10. The connections on the Mega are on the bottom header pins D50-D53. Connections are one-to-one. That is MISO:MISO, MOSI:MOSI, SCK:SCK, and SS:SS. The Master-In-Slave-Out MISO signal transfers data from the Nano to the Mega because the Nano is set up as SPI Slave and the Mega as Master. Vice-versa for MOSI. Serial Clock (SCK) sends a 2 MHz clock from the Mega to the Nano. And Slave Select (SS) signals the Nano to listen on SCK & MOSI for data and to send data on MISO synchronized to SCK. I use 2 Mbps because single-ended communications over long wires can’t go all that fast.

To low-pass filter the data, I use an exponential moving average filter implemented as an infinite impulse response (IIR) digital filter. This is very efficient because it requires only a weighted average of the current sample with the previous output. For a C++ implementation, I followed the integer implementation here. I had to add some additional 64-bit integer type casting to the coefficients in the filter equation to get it to operate correctly.. I also changed how the filter coefficient is implemented as a 16-bit unsigned integer (0-65535). The original author designed it to represent floats ranging from 1/65535 to 1 and I changed it to the range 0 to 65535/65536. Just personal preference. You can see the effect of the filter in the video - I adjust the potentiometer abruptly and it takes some time for the bargraph to totally respond.

Master Code

// Jeff Piepmeier - May 2016
//
// Main program adapted from SPI demo
// at http://www.gammon.com.au/spi 
// by Nick Gammon April 2011

//set up TFT display
#include <SPFD5408_Adafruit_GFX.h>
#include <SPFD5408_Adafruit_TFTLCD.h>
#include <SPI.h>

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

Adafruit_TFTLCD tft(LCD_CS, LCD_CD, LCD_WR, LCD_RD, LCD_RESET);

// variables for bar graph
int newHeight;
int oldHeight = 0;
int heightDiff;

void setup (void)
{
  // Serial.begin (115200);
  // Serial.println ("SPI demo");
  
  // SS = slave select, built in AVR output pin reference 
  digitalWrite(SS, HIGH);  // ensure SS stays high for now

  tft.reset();
  tft.begin(0x9341);
  tft.fillScreen(0x0000); // make the screen black
    
  // Put SCK, MOSI, SS pins into output mode
  // also put SCK, MOSI into LOW state, and SS into HIGH state.
  // Then put SPI hardware into Master mode and turn SPI on
  SPI.begin ();

  // Slow down the master a bit
  //SPI.setClockDivider(SPI_CLOCK_DIV4);
  // use 2 Mbps decided by testing. 4 Mbps has too many bit errors over 
  // jumper wires. Single-ended signals are not well suited for high-speed over wires
  SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0));

}  // end of setup

void loop (void)
{
  byte a=0; // variable to store SPI input data from Nano
  
  // get a value from the SPI - we're the master so have to ask for it from slave
  // enable Slave Select
  digitalWrite(SS, LOW);    
  a = SPI.transfer ('a');
  // disable Slave Select
  digitalWrite(SS, HIGH);

  newHeight=(int)(((long)a*319)/256); // hardcode display height of 320 lines
  heightDiff = oldHeight-newHeight; // only draw new part of bar graph for faster display
  if (heightDiff>0) { tft.fillRect(80, newHeight+1, 80, heightDiff+1, 0x0000); }
  else if (heightDiff<0) { tft.fillRect(80, oldHeight-1, 80, -heightDiff+1, 0xFFFF); }
  oldHeight=newHeight; // remember how high bar is
  
  // Serial.println (a, DEC);
}  // end of loop


Slave Code

// Main program is adapted from SPI demo at
// http://www.gammon.com.au/spi
// Written by Nick Gammon
// April 2011
//
// IIR Exponential Moving Average (EMA) Low Pass Filter (LPF)
// adapted from C++ code at
// http://stratifylabs.co/embedded%20design%20tips/2013/10/04/Tips-An-Easy-to-Use-Digital-Filter/

// filter coefficient float-to-uint16 conversion - min 0, max x=1 means 65535/65536=.9999847
#define DSP_EMA_I32_ALPHA(x) ( (uint16_t)(x * 65535) )

volatile byte command = 0;
volatile byte out1 = 0;

void setup (void)
{

  // have to send on master in, *slave out*
  pinMode(MISO, OUTPUT);
  digitalWrite(MISO, LOW); //ensure is low to start
  
  // turn on SPI in slave mode
  SPCR |= _BV(SPE);

  // turn on interrupts
  SPCR |= _BV(SPIE);

}  // end of setup

//http://stratifylabs.co/embedded%20design%20tips/2013/10/04/Tips-An-Easy-to-Use-Digital-Filter/
int32_t dsp_ema_i32(int32_t in, int32_t average, uint16_t alpha){
  int64_t tmp0;
  tmp0 = (int64_t)in * (int64_t)(alpha) + (int64_t)average * (int64_t)(65536 - alpha);
  return (int32_t)((tmp0 + 32768) / 65536);
}

// SPI interrupt routine
ISR (SPI_STC_vect)
{
  command = SPDR; // not yet used here
  SPDR = out1; 
}  // end of interrupt service routine (ISR) SPI_STC_vect

void loop (void)
{
  uint16_t adcReading = 0;
  static int32_t avg1 = 0;
  adcReading=analogRead(0); //10 bit unsigned, shift up to 31 bits for signed long int
  avg1=dsp_ema_i32( (int32_t)adcReading << 21, avg1, DSP_EMA_I32_ALPHA(0.0005));
  out1=byte(avg1 >> 23 ); // shift down to single byte
}  // end of loop