GPS and OLED latency

Hello - this is my first post here.

I have been working on a GPS speedometer based on instructions and code in a YouTube video. So far it's working well, thanks to the helpful video and a few additional searches on this forum!

My GPS is a Neo 6M, my OLED is a DEVMO 12864 0.96 Inch I2c, and I am using an ELEGOO Nano board. I believe the GPS is 1 hertz, so I get 1 speed reading per second.

I would like my OLED to "show" each mile-per-hour as it goes from one number to another. For example, if a reading is 30 mph, and then the next reading is 35 mph, I would like the display to show 31, 32, 33, and 34, before showing the 35 mph.

I have produced some code which tries to calculate the difference in mph from two readings, then divide 1 second into that many intervals, and then update the mph by +1 for each of those intervals. In the example above, it would (I think) do 35 - 30 = 5, so it would divide 1 second into 5 parts of 200ms each, and it would show 31, 32, 33, 34, 35 with a 200 ms delay between each.

The challenge is that there appears to be some latency in the updates on the OLED, sometimes. For example, if it's going from 40 to 50, it might do: 40-41-42-43-44-45-46--------47-48-49-50. I don't know how to tell if this latency is coming from the Nano processing, or if it's coming from the OLED display. (If the latter, I guess this latency would not be there if I removed the display and was just passing the mph number to another device).

My code is below, but just figured I'd ask in case there is an obvious cause of the random latency that experienced coders know of. Thank you!

#include <SoftwareSerial.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <TinyGPS++.h>


#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

//On an arduino UNO: A4(SDA), A5(SCL)
#define OLED_RESET -1 //Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C //See datasheet for Address
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define rxPin 2
#define txPin 3
SoftwareSerial neogps(rxPin, txPin);

TinyGPSPlus gps;

int displaySpeed = 0;
int currentSpeed = 0;
int diff = 0;
int msdelay = 250;

void setup() {
  Serial.begin(115200);
  //Begin serial communication Arduino IDE (Serial Monitor)

  //Begin serial communication Neo6mGPS
  neogps.begin(9600);

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;); // Don't proceed, loop forever
  }

  display.clearDisplay();
  display.display();
  delay(2000);

}

void loop() {

  boolean newData = false;
  for (unsigned long start = millis(); millis() - start < 1000;)
  {
    while (neogps.available())
    {
      if (gps.encode(neogps.read()))
      {
        newData = true;
      }
    }
  }

  //If newData is true
  if (newData == true)
  {
    newData = false;
    print_speed();
  }
  else
  {
    display.clearDisplay();
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0, 0);
    display.setTextSize(3);
    display.print(F("No Data"));
    display.display();
  }

}

void print_speed()
{
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);

  if (gps.location.isValid() == 1)
  {


    currentSpeed = currentSpeed + random(0, 40) - 10 ; // set it back to this for actual car testing:   currentSpeed = int(gps.speed.mph()) ;
    diff = currentSpeed - displaySpeed;
    msdelay = abs((1000 / abs(diff)) - 50);

    for (int i = 1; i < abs(diff) + 1; i++) {
      displaySpeed = (displaySpeed + (diff / abs(diff))) ;

      Serial.print("displaySpeed = "); Serial.println(displaySpeed);

      display.clearDisplay();
      //String gps_speed = String(gps.speed.mph());
      display.setCursor(0, 0);
      display.setTextSize(3);
      display.print(currentSpeed);    //display.print(gps.speed.mph());

      display.setCursor(80, 13);
      display.setTextSize(1);
      display.print(F("mph"));

      //String gps_speed = String(gps.speed.mph());
      display.setCursor(0, 35);
      display.setTextSize(3);
      display.print(displaySpeed);

      display.setCursor(80, 48);
      display.setTextSize(1);
      display.print(F("avg mph"));

      display.display();

      delay(msdelay);
    }

  }

}

Do you need to set the "mph" and "avg mph" text each time inside the loop? Will it not stay from the previous time it was set? That might cut down some of the processing time.

If that doesn't help, I would try removing all the GPS code completely and simulating it using fixed numbers then you'd be able to figure out which part causes the delay.

Also, it looks like it would be easy to create a divide by zero error here if the speed is exactly the same as it was the previous second.

The delay might be caused by SoftwareSerial when it is receiving data from the GPS. Although 9600 baud is slow and should not cause much delay in regular code, the update process for the display is also fairly slow, especially since you are clearing the display each time.

Are you getting GPS data reliably? I would think there is a risk of overrunning the receive buffer since you are spending so much time in the display routine.

Hi.
Are you interested in trying out a 4-digit 7-segment LED display? They update faster than an OLED display.

Will you really jump 5mph in one second?

This is important. It's tricky to understand exactly how any display or Serial.print statements impact the receival of data from the GPS into the soft serial buffer. I don't know if anyone has fully sussed out this issue.

I suspect that this unusual construct, and the display code that follows the for loop, may cause the MCU to miss every other GPS sentence.

Take a look at the simple examples that come with TinyGPS to see how to feed the GPS sentence parser properly.

Fortunately the NMEA sentences from the GPS have a checksum at the end, and the TinyGPS library verifies this, so it can reject anything that was not received correctly.

1 Like

Yep, and that leads directly to the OP's problem, when the code skips reading parts of sentences.

At this point, I think we need a clarification.

When the display pauses, is it a pause within the 1-second period that the display is given to update from one speed to another, or is the first part of the speed change done over 1 second, then there is a second or two delay before the display continues to the final speed?

Thank you all for this great help, and so quickly!

[touch1337] I tried moving the text outside of the loop but I think I need the display.clearDisplay(); piece to occur every loop, otherwise the pixels for the numbers get lit on top of each other, if that makes sense. If there was a way to just clear some pixels (where the numbers are), that would definitely work though. On the GPS piece, I am right now just simulating data, so I can test at my laptop, and there is still the latency every 5 to 8 loops. As for the divide-by-zero issue, thank you for that catch! My phony data should come up with this issue about once every 40 seconds but I haven't seen any issues yet so I'm not sure if I have been lucky or if something is correcting it for me. I've added the second and third lines here though:

  diff = currentSpeed - displaySpeed;
    if (diff == 0) {
      diff = 1;
    }

[david_2018] I am not super familiar with baud rate and what to do about that. But I did have the code using gps data in an earlier version, and used double numbers instead of integers (as sitting on my desk, the GPS was giving 0.00 to 0.50 mph numbers) and it seemed to work similarly to my integer-based version I've been tinkering with since then. I am actually getting GPS data reliably, so far, with the unit about 8 feet from my south-facing window. I am absolutely astounded at how well it's working!

[HillmanImp] Yes, in fact I do plan to transition to 7-segment LED display. That's a whole new code to learn, I suspect, so I wanted to just proof-of-concept with this version first. As for my acceleration... I believe a Chevy Bolt cites 0 to 60 mph in a bit over 6 seconds, so it's averaging +10mph per second and I am sure it may hit +15mph for a couple of those seconds. That said... I don't drive like that and you're right that +5mph per second is actually probably sufficient for "real world" conditions with my driving style. If the display jumps a couple mph when it's accelerating from stop, aesthetically I think that's fine as you're right that it likely wouldn't move much more than +5mph per second at most speeds and I think the latency at that point is pretty minimal.

[jremington] Regarding your question about when the latency is, I cannot tell. I tried to print to serial so I could debug but I am pretty new to coding and I cannot tell from there. Here is a gif of what I see; you'll notice it increments very quickly for 5 to 10 mph changes, then slight pause, then increments quickly again. My next step might be to take this in the car and see if this is "good enough" for real world conditions, but I am definitely enjoying debugging and learning as I try to make it perfect first!

ezgif.com-gif-maker

This is a mistake.

Fix it by studying the TinyGPS example code and following that outline.

Save the previous value that was written to the display.
When you need to update the display do the following:
Set the text color to match the background color (SSD1306_BLACK).
Print the old value to the display. This effectively erases the old value.
Set the text color back to normal (SSD1306_WHITE).
Print the new value to the display.

Avoid any use of delay, or at least use the somewhat misnamed smartDelay() used in some of the TInyGPS examples. That uses millis() for the delay, so that the serial port can be read and fed to TinyGPS during the delay period. Overall it would be much better to use millis instead of delay, that way you can add additional features to the code without markedly affecting the GPS or display.

If I read you guys right, it sounds like that millis piece of code right now is causing it to only look for new speed data every 1 second or so. (And if things get misaligned, it might miss a new data point by a millisecond or two and cause me to need to skip another 1 second or so to get the next data point. That makes sense to me.

That being said, I don't understand how to implement the smartDelay into my code. Here is what I have now, and I see an error message "a function-definition is not allowed here before '{' token".

void loop() {

  static void smartDelay(unsigned long ms)
  {
    unsigned long start = millis();
    do
    {
      while (neogps.available())
        gps.encode(neogps.read());
      print_speed();
    } while (millis() - start < ms);
  }



}

void print_speed()

...even though the Full Example does appear to look like that.

Give this a try:

#include <SoftwareSerial.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <TinyGPS++.h>


#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

//On an arduino UNO: A4(SDA), A5(SCL)
#define OLED_RESET -1 //Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C //See datasheet for Address
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define rxPin 2
#define txPin 3
SoftwareSerial neogps(rxPin, txPin);

TinyGPSPlus gps;

int displaySpeed = 0;
int currentSpeed = 0;
int diff = 0;
int msdelay = 250;

void setup()
{
  Serial.begin(115200);
  // Begin serial communication Arduino IDE (Serial Monitor)

  // Begin serial communication Neo6mGPS
  neogps.begin(9600);

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS))
  {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;); // Don't proceed, loop forever
  }

  display.clearDisplay();
  display.display();
}

void loop()
{
  // Handle GPS messages
  while (neogps.available())
  {
    if (gps.encode(neogps.read()))
    {
      if (gps.location.isValid())
      {
        currentSpeed = gps.speed.mph();

        // This generates a random speed change for testing.
        currentSpeed = currentSpeed + random(0, 40) - 10;
      }
      else
      {
        display.clearDisplay();
        display.setTextColor(SSD1306_WHITE);
        display.setCursor(0, 0);
        display.setTextSize(3);
        display.print(F("No Data"));
        display.display();
      }
    }
  }

  static unsigned long lastDisplayTime = 0;
  if (millis() - lastDisplayTime >= 200) // Five changes per second
  {
    if (currentSpeed > displaySpeed)
    {
      displaySpeed++;
      print_speed();
    }
    else if (currentSpeed < displaySpeed)
    {
      displaySpeed--;
      print_speed();
    }
  }
}

void print_speed()
{
  Serial.print("displaySpeed = ");
  Serial.println(displaySpeed);

  display.clearDisplay();
  
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(3);

  display.setCursor(0, 0);
  display.print(currentSpeed);

  display.setCursor(0, 35);
  display.print(displaySpeed);

  display.setTextSize(1);

  display.setCursor(80, 13);
  display.print(F("mph"));

  display.setCursor(80, 48);
  display.print(F("avg mph"));

  display.display();
}

This spends an enormous amount of time clearing and writing "No Data" to the display, as fast as the display can handle it. Seems counterproductive.

As fast as valid messages arrive from the GPS without a valid fix. At that point, I don't think the sketch has anything better to do.

[david_2018] I saw your note earlier on the black pixel piece and forgot to comment; that is clever!

[johnwasser] I tried your code, thank you! The good news is it's lightning fast at incrementing the displayed speed.

However it does two weird things: one is that it sometimes shows a new GPS reading very quickly after a previous one. (in the GIF, watch when the top number goes from 25 to 3).

The other is that it sometimes gets hung up as the bottom "display" speed is incrementing towards the GPS reading. (Kind of like my code does). The top GPS speed waits for the bottom to match it in these cases, then I get a new reading on the top. (example in the GIF, when the top number is 19).

To the point of [jremington]... indeed this really only ever needs to show the speed, and if it's not getting data, I'd shut it down after a minute or two.

GIF below:

ezgif.com-gif-maker (1)

You are having a problem running low on ram - the random dots in the lower corner of the display indicate that something in the code is overwriting the display buffer.

Good eye!

A clever sleuth would deduce from those bit patterns the binary values of the variables overwriting the display buffer.

No, not at all. I use TM1637 modules which have an on board processor that directly controls the LED segments. All your code has to do is tell it what number to display. Couldn't be simpler.

I've found that many ideas I thought would be useful in a GPS speedometer turned out to be just fanciful and not needed. Simplest is best. We have to remember that driving a vehicle is a serious business and nothing must be allowed to distract the driver.

The videos and tutorials can be quite helpful & instructive, but can also be unrealistic. There's a big gulf between theory and practice, between a workbench contraption and a safe device installed in a vehicle on the road.

As others have said, displays and prints are slow to execute and can lead to buffer overflow and lost bytes from the GPS output. You are trying to do many display ops in between receiving successive sentences. Do you know how much time there is between the GPS outputting one sentence and the next?

With well populated sentences, i.e. when it has a fix, the GPS outputs all the sentences (up to 9 of them) in about 500ms. Then there's 500ms idle time. If you do all your calculations and display work in that idle time, you still can get buffer overflow and lost bytes.