GPS speedometer with analog gauge output, needle is jumpy

My question is about getting smooth output from a stepper motor through good programming.

Here's a little background:
I've built a GPS speedometer running on a MEGA 2560. It works well for the most part, I'm able to get data from the GPS module to the Arduino, and then out to an OLED display, as well as an analog gauge stepper motor. You can see it working here: Analog and digital Arduino GPS speedometer - YouTube

The video shows a little of this, but the needle can be kind of jumpy. You'll see it jump up and back down, and it does this a lot while decelerating. Obviously this is not actually what is happening. The GPS signal comes in at 5Hz, and I've plotted the output from the GPS. It looks like what I would expect.

Now if I update the motor position only when data comes in, the needle motion will be jumpy. So what I've attempted to do is let my motor position lag behind GPS updates, and interpolate between the last and current GPS values. You'll see this in the "GPS Data Fetching Loop" and "Gauge 1 Update Function".

It seems obvious to me that this interpolation strategy has not worked. What am I doing wrong? I'd appreciate your thoughts.

///////  MAIN LOOP /////////////////////////////////////////////////////////////////////////

void loop()
{

if (timer3 > millis())  timer3 = millis();
      if (millis() - timer3 > 8) {
          int alpha_a1 = 64;          // exponential moving average alpha value
          angle1_last = angle1;       // save last gauge angle  
          
          angle1 = gauge1();          // read gauge value and return angle
          angle1 = (angle1*alpha_a1 + angle1_last*(256-alpha_a1))>>8;  // calculate exponential moving average
          motor1.setPosition(angle1);     // send needle angle to motor       
          
          Serial.print(0);            // serial plotter debugging
          Serial.print(" ");          // serial plotter debugging
          Serial.print(540);          // serial plotter debugging
          Serial.print(" ");          // serial plotter debugging
          Serial.println(angle1);     // serial plotter debugging
        
          void motorUpdate();             // update motor position of all motors
      }
// DISPLAY  timer loop
  if (timer1 > millis())  timer1 = millis();
      if (millis() - timer1 > 100) {
        timer1 = millis();        // reset timer1
        
      dispUpdate();             // update OLED display
      }

    
// GPS DATA FETCHING LOOP //
    if (GPS.newNMEAreceived()) {
        if (!GPS.parse(GPS.lastNMEA()))   // this also sets the newNMEAreceived() flag to false
          return;  // we can fail to parse a sentence in which case we should just wait for another  }
      // if millis() or timer wraps around, we'll just reset it
      if (timer2 > millis())  timer2 = millis();
      if (millis() - timer2 > 200) { 
        timer2 = millis();        // reset timer2
             unsigned long alpha_0 = 128; // filter coefficeint to set speedometer response rate
          
             v_lo = v_hi;                     // save previous value of velocity    
             t_lo = t_hi;                     // save previous time value
             lag = t_hi-t_lo;                 // 
             v = GPS.speed*1.150779;          // fetch velocity from GPS object, convert to MPH             
             t_hi = micros();                 // get current time value
             v_100 = (unsigned long)v*100;               // x100 to preserve hundredth MPH accuracy         
             v_hi = (v_100*alpha_0 + v_hi*(256-alpha_0))>>8; //filtered velocity value
             
            // Serial.println(v_hi);
             
       }
    }
    
}


//  GAUGE 1 DATA UPDATE FUNCTION  //
int gauge1 () {

  v_g = map(micros()-lag, t_lo,t_hi,v_lo,v_hi);               // interpolate values between GPS data fix
  if (v_g < 150 || v_g > 20000) {                              // bring speeds below 1.5mph and above 200 mph to zero
    v_g = 0;
  }

  int angle = map( v_g, 0, 6000, 0, STEPS1);                  // calculate angle of gauge 
  return angle;                                               // return angle of motor
  
}


//  ADAFRUIT GPS INTERRUPT FUNCTION  //
//  I don't really understand this code, but it works, so don't freaking mess with it
//  Interrupt is called once a millisecond, looks for any new GPS data, and stores it
SIGNAL(TIMER0_COMPA_vect) {
  char c = GPS.read();
  // if you want to debug, this is a good time to do it!
#ifdef UDR0
  if (GPSECHO)
    if (c) UDR0 = c;  
    // writing direct to UDR0 is much much faster than Serial.print 
    // but only one character can be written at a time. 
#endif
}

void useInterrupt(boolean v) {
  if (v) {
    // Timer0 is already used for millis() - we'll just interrupt somewhere
    // in the middle and call the "Compare A" function above
    OCR0A = 0xAF;
    TIMSK0 |= _BV(OCIE0A);
    usingInterrupt = true;
  } else {
    // do not call the interrupt function COMPA anymore
    TIMSK0 &= ~_BV(OCIE0A);
    usingInterrupt = false;
  }
}


//  GAUGE POSITION UPDATE FUNCTION  //
void motorUpdate ()
{
  motor1.update();

}


// OLED DISPLAY UPDATE FUNCTION //
void dispUpdate() {
    display.clearDisplay();             //clear buffer
    display.setTextSize(2);             // text size
    display.setCursor(12,1);
    display.print("SPD:");        // GPS speed
    display.println(v);
    display.setTextSize(1);             // text size
    display.setCursor(12,17);
    //display.print("disp updates:  ");   //counter for debugging
    //display.println(testnum);
    display.print("v_hi:  ");   //counter for debugging
    display.println(v_hi);
    display.display();                  //print to display
    testnum ++; 
}

gauge_feb_4_2020_debug.ino (8.17 KB)

1 Like

Haven't looked at the code in depth, but from what you describe, you're not interpolating, you're extrapolating. A better approach would be to use a descrete-time low pass filter (LPF). You can use something like this to easily design such a filter - you just have to know what your doing when plugging in the options.

It's not super clear what is causing the jumpiness. It could be variations in the measured speed, which could be smoothed by averaging, say, 10 speed measurements together before sending a new angle to your motor. Motors can also get jumpy if you don't give them time to reach the last position you set for them. Not sure what is causing it, just two suggestions.

Power_Broker:
Haven't looked at the code in depth, but from what you describe, you're not interpolating, you're extrapolating.

You may be right that I'm extrapolating, but seeing that this is not what I intend to do, I'm trying to understand how that could happen.

Power_Broker:
A better approach would be to use a descrete-time low pass filter (LPF). You can use something like this to easily design such a filter - you just have to know what your doing when plugging in the options.

A LPF seems a bit like a bandaid in this case. The data coming in is smooth enough (I'll have to see if I can record and share some data from the GPS), so why can't I use interpolation to resample the data at a higher rate?

neongreen:
The data coming in is smooth enough

A timestamped sample log (i.e. csv) would be helpful in determining that. Also, filtering is used in a plethora of professional designs and is rarely a bandaid.

neongreen:
why can't I use interpolation to resample the data at a higher rate?

Because that "resampled" point is useless. Interpolation is not what you need, and, after looking at your code, not what you're currently doing anyway. It seems that you're actually filtering the velocity data already (which is good!). You might consider adjusting your filter to get rid of the data inconsistency (i.e. alpha).

Ok, got some data.

I feel like the unfiltered GPS data is pretty smooth.

I deleted the code that attempts to interpolate, just using an exponential moving filter. Still not happy with the needle motion. I definitely need to play with the filter, but I have my doubts that it will actually do what I want.

gauge_feb_4_2020.ino (9.31 KB)

GPS_data_2020_02_08a.txt (4.52 KB)

It may "look" smooth, but your timescale is massive. I still wouldn't be surprised if the data did correlate to the needle "jerkyness"

Power_Broker:
It may "look" smooth, but your timescale is massive. I still wouldn't be surprised if the data did correlate to the needle "jerkyness"

Which is why I attempted to interpolate in my original code. I figured relying on a filter alone would cause a lot of lag.

I'm not 100% sure the timescale is accurate, watching it come into the serial monitor it looked more like 1Hz, but the data shows 5Hz.

This is the code I used to bring the data in, within the GPS Data Fetching Loop.

             Serial.print(millis());
             Serial.print(",");
             Serial.println(v);

If I watch the OLED, the speedometer definitely updates several times per second, so I know the data is coming in faster than 1Hz. I watch the stepper motor position or listen to the stepper motor, it is updating at 1HZ at best.

Got it all sorted out. I had some code in there that was messing up my timers.

Also, my previous attempt at interpolation was not totally off base, the theory was sound, but I didn't quite execute it correctly.

All is good, the gauge needle moves smoothly, and everything behaves as expected. For now..

gauge_feb_10_2020.ino (9.06 KB)