Autonomous RC Car Using NeoGPS

I've created a working code for an autonomous rc car that uses GPS as well as a compass to move to predetermined waypoints. Recently I discovered NeoGPS and it is a very powerful system. However, I cannot fully implement the code into my existing code. Mainly trying to figure out how get the code to work first without the compass being used, and only GPS.

I need a way to get the car to move by having the GPS tell the car to move forward, left, or right if it begins to veer off course in either direction.

/*
Name:    MoBot.ino
Created:  8/15/2017 10:56:15 AM
Author:   Mad Monkey Projects
*/

// ARDUINO (UNO) SETUP:
// =======================
// Ping sensor = 5V, GRND, D11 (for both trigger & echo)
// Adafruit GPS = D8 & D9 (GPS Shield, but pins used internally)
// Adafruit Magnetometer Adafruit HMC5883L = I2C :  SCL (A5) &  SDA (A4)
// SD Card (D10, D11, D12, D13)

#include <NMEAGPS.h>
#include <AltSoftSerial.h>
#include <Wire.h>
#include "MotorDriver.h"
#include "Compass.h" // written by https://github.com/helscream/HMC5883L_Header_Arduino_Auto_calibration

// requires specific pins (.g., 8&9 on an UNO):
AltSoftSerial gpsPort(8, 9); // baud rate must be 9600, 19200 or 38400;

/******************************
*  Pin definition for motors  *
******************************/
// drive motor
constexpr uint8_t ENA = 5;
constexpr uint8_t IN1 = 4;
constexpr uint8_t IN2 = 3;
// steering motor
constexpr uint8_t ENB = 6;
constexpr uint8_t IN3 = 14;
constexpr uint8_t IN4 = 15;

// motor instance 
MotorDriver Drive(ENA, IN1, IN2);
MotorDriver Turn(ENB, IN3, IN4);

/******************************
*       GPS information       *
******************************/
NMEAGPS gps;
gps_fix fix;

/******************************
   Waypoint information
******************************/
// the vehicle will stop when currentTarget is equal to number of coordinates.
constexpr uint8_t NUM_OF_COORDINATES = 4; 

// enter waypoints 
NeoGPS::Location_t TARGET_WAYPOINTS[] =
{ /*   LAT          LON    */
  { 348326564, -922737374L },
  { 348328046, -922737897L }, 
  { 348327593, -922738434L },
  { 348325934, -922735979L }
};

float CurrentLat, CurrentLong;

/* CurrentTarget keeps track of the target in the array. The heading threshold is the amount of degrees 
the vehicle can be off its desired heading before adjusting the course. Distance threshold is the amount 
of meters away from target coordinate, that when exceeded it will continue towards the next coordinate. */
uint8_t CurrentTarget = 0, HeadingThreshold = 5, DistanceThreshold = 2;

// the distance to the current target coordinate is calculated
float Distance = fix.location.DistanceKm(TARGET_WAYPOINTS[CurrentTarget]);

// calculates the desired/"optimal" heading based on the current location and target location
float TargetHeading = fix.location.BearingToDegrees(TARGET_WAYPOINTS[CurrentTarget]);


void setup() 
{
  Serial.begin(115200);
  gpsPort.begin(9600);

  // configure GPS
  gpsPort.println(F("$PMTK314,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29\r\n" // RMC only
                    "$PMTK220,100*2F\r\n" // 10 Hz update
                    "$PGCMD,33,0*6D")); // no antenna

  // used to improve accuracy of HMC5883L compass 
  // found by running the calibration functions supplied by the library
  Wire.begin();
  compass_x_offset = -199.89;
  compass_y_offset = -70.435;
  compass_z_offset = -716.97;
  compass_x_gainError = 0.97;
  compass_y_gainError = 1.01;
  compass_z_gainError = 1.00;
  
  // intial speed and direction
  Drive.setSpeed(255);
  Drive.forward();
}

void loop() 
{
  Serial.println("----------------------------------------------");
  compass_heading();
  heading += 0.017;
  GrabGPSData();
  AdjustCourse();
  Serial.print("Compass Heading: ");
  Serial.println(heading);
  Serial.println("----------------------------------------------");
}

void GrabGPSData() 
{
  gps.available(gpsPort); 
  gps_fix fix = gps.read();
  if (fix.valid.location) {
    CurrentLat = fix.latitudeL();
    CurrentLong = fix.longitudeL();

    //Serial.print("Current Location: ");
    //Serial.print(CurrentLat, 7);
    //Serial.print(", ");
    //Serial.println(CurrentLong, 7);
  }
  else {
    //Serial.println("No GPS fix!");
  }
    return;
}

//The function for adjusting the course of the vehicle
//Based on the current heading and desired heading
void AdjustCourse()
{
  // stops the vehicle when there are no coordinates left
  // if the distance threshold is exceeded, move on to the next target location in the array
  if(CurrentTarget == NUM_OF_COORDINATES){
    //Serial.println("No coordinates left in the array.");
  }
  if(Distance < DistanceThreshold){
    CurrentTarget++;
  }
  
  //Serial.print("Distance to next coordinate: ");
  //Serial.println(Distance);
  // prints the target location
  //Serial.print("Target Location(");Serial.print(CurrentTarget);Serial.print("): ");
  
  //Serial.print("Target heading: ");
  //Serial.print(TargetHeading);
  // find the difference between the current heading and the target heading
  // does some correction to find the correct error
  int16_t Error = TargetHeading - heading;
  if(Error < -180) Error += 360;
  if(Error > 180) Error -= 360;
  //Serial.print(" - Error: ");
  //Serial.print(Error);
  
  // depending on the error, the vehicle will then adjust its course
  // small delays for the left and right turns are added to ensure it doesn't oversteer.
  if(abs(Error) <= HeadingThreshold) {
    //Serial.println(" - On course");
    Drive.setSpeed(255);
    Drive.forward();
    Turn.release();
  }
  else if(Error < 0) {
    //Serial.println(" - Adjusting towards the left");
    Turn.setSpeed(255);
    Turn.left();
  }
  else if (Error > 0) {
    //Serial.println(" - Adjusting towards the right");
    Turn.setSpeed(255);
    Turn.right();
  }
  else {
    Drive.setSpeed(255);
    Drive.forward();
    Turn.release();
  }  
}

MoBot_v0.4.ino (5.25 KB)

The main problem is that you are printing too much:

void loop() 
{
  Serial.println("----------------------------------------------");
  compass_heading();
  heading += 0.017;
  GrabGPSData();
  AdjustCourse();
  Serial.print("Compass Heading: ");
  Serial.println(heading);
  Serial.println("----------------------------------------------");
}

Your main loop always prints a lot of characters. After one time through loop, the Arduino spends all of its time waiting for characters to go out. This causes it to lose the GPS characters that are coming in. This is called "input buffer overflow".

A secondary problem is that you are not testing gps.available:

void GrabGPSData() 
{
  gps.available(gpsPort); 
  gps_fix fix = gps.read();

Then you always call gps.read. What if no new fix data is available? You will get a default fix (no values), and then you use that for later calculations. This is not how the NeoGPS examples are structured. You cannot read a fix structure unless a new one is available.

In general, you should do most of the "work" right after a GPS update comes in, once per second. Calculate the distance and bearing from the new fix, then control the motors to get there:

void loop() 
{
  //compass_heading();
  GrabGPSData();
}

void GrabGPSData() 
{
  if (gps.available(gpsPort)) {
    gps_fix fix = gps.read();

    if (fix.valid.location) {
      heading += 0.017;

      CurrentLat = fix.latitudeL();
      CurrentLong = fix.longitudeL();

      Distance = fix.location.DistanceKm(TARGET_WAYPOINTS[CurrentTarget]);
      TargetHeading = fix.location.BearingToDegrees(TARGET_WAYPOINTS[CurrentTarget]);

      Serial.print("Current Location: ");
      Serial.print(CurrentLat);
      Serial.print(", ");
      Serial.println(CurrentLong);

      Serial.print( "Target bearing: " );
      Serial.print( TargetHeading );
      Serial.print( " distance:" );
      Serial.println( Distance );

      Serial.print("Compass Heading: ");
      Serial.println(heading);

      AdjustCourse();

    }
    else {
      Serial.println("GPS location not valid!");
    }
  }
  else {
    //Serial.println("GPS data not available!");
  }
}

But since a GPS device does not know which way it is facing, you also need the compass. The difference between the bearing (to the target) and the orientation (from the compass) is how much you need to adjust the steering.

You also have to coordinate these decisions with the ping feed back. When you run into an obstacle, you have to enter a "state" that avoids the obstacle. In this state, the GPS information is useless. The compass information might be good to know, so that you can tell when the vehicle has turned "enough".

Then move some distance. This could be calculated from GPS, but it's not very good at small distances.
Wheel rotations or time would probably be better. Then you can again try to seek the target by using the GPS information.

Thank you for the response and adjustments Dev. The prints are for debugging only. I comment out all of them before uploading the sketch.

Monday I will run the updated code and report back the results. It will be the first time to run the car with your code. The great thing about your code is how light weight it is. I was doing all I could before your code to cut weight and increase speed. I even used port manipulation in my motor driver class to do away with all the checksums of digitalWrite to increase speed.

As a young C++ padawan, are there any other areas of my code that could be made more efficient? I have included my entire program below. Thanks in advance.

Compass.cpp (12.9 KB)

Compass.h (754 Bytes)

MoBot_v0.4.ino (5.25 KB)

MotorDriver.cpp (1.72 KB)

MotorDriver.h (783 Bytes)

I ran the code today and tested as well. The issue that the code is having, is that there is no valid GPS data being received. I'm using the built in pin 13 led of the adafruit GPS shield, as well as a pin on 10 and 11 to test the conditions outside. I get a valid location just no valid data.

I get a valid location just no valid data.

How can you get a valid location from invalid data?

Ok.... it looks like you didn't read my lowercase letters. -_-

THIS WON'T WORK:

void GrabGPSData() 
{
  gps.available(gpsPort);    <-- not an IF statement?!?
  gps_fix fix = gps.read();  <-- reads an empty fix 9999 times out of 10000

YOU ARE NOT TESTING gps.available. Read reply #1 again.

Like PaulS, I didn't understand how you could have a valid location and no valid data. ?!!? But if you try to read a fix when no fix is available, you will get a fix that has no valid data.

This is the same thing as reading from Serial without testing Serial.available.

THIS WON'T WORK EITHER:

void loop()
{
  Serial.available();      <-- does nothing
  char c = Serial.read()   <-- reads a -1 char 9999 times out of 10000

You must read characters like this:

void loop()
{
  if (Serial.available()) {
    char c = Serial.read()   <-- reads one available character

This is the current code with your modifications from this past weekend. Besides implementing your code, I also added in two LED's to flash when either else statement was entered. When the car is outside and the GPS shows a fix, pin 13 LED on the shield continues to flash. This indicates that no valid data is being received. My impression is the same as yours. How can I have a valid location, but not valid GPS data.

MoBot_v0.4.3.ino (7.99 KB)

float Distance = fix.location.DistanceKm(TARGET_WAYPOINTS[CurrentTarget]);

// calculates the desired/"optimal" heading based on the current location and target location
float TargetHeading = fix.location.BearingToDegrees(TARGET_WAYPOINTS[CurrentTarget]);

This is executable code that belongs in a function.

      Delay_LED2;

What is this doing? Hint: Nothing.

PaulS points out the two big problems. The sketch I posted calculated the distance every time a new fix was received.

I can recommend a few other things:

  1. Turn off as many sentences from the GPS device as possible. In your sketch, you only use lat/lon. According to this table, you would only need one sentence: GGA, GLL or RMC. Send the configuration command to enable one of those sentences. Pick one of these:
    gps.send_P( &gpsPort, F("PMTK314,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") ); // GLL only
    gps.send_P( &gpsPort, F("PMTK314,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") ); // RMC only
    gps.send_P( &gpsPort, F("PMTK314,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") ); // GGA only

Then make sure NMEAGPS_cfg.h matches your choice.

To make your sketch smaller, you could comment out all options in GPSfix_cfg.h except LOCATION.

  1. Verify that you have chosen the correct LAST_SENTENCE (It should be the sentence chosen in step 1). If you don't, it is possible that you are trying to do something while the GPS is still sending data. You should do as much as possible during the GPS quiet time (AdjustCourse in your sketch).

  2. Don't use LED1. The GPS data is sent in batches, with a GPS quiet time between batches. LED1 would toggle thousands of times during the quiet time. Furthermore, each character is separated by 0.5ms (at 19200), so LED1 would toggle several thousand times (each time loop is called and no character has arrived yet). All that toggling tells you nothing about receiving a fix or whether the location is valid.

  3. Turn off LED2 when a fix is available. LED2 might be staying on even though you are getting valid locations.

Okay, made the necessary changes to the program and it is working as expected. First let me say that honestly I am glad I took the time to implement NeoGPS to this project. Using the Adafruit GPS library, the car would contently make course directions. It looked like Ray Charles driving the thing. But with NeoGPS, the car drives much more smoothly and makes changes far less often.

The issue I am having now is that I can not change the GPS baud rate above 9600. If I change it to 19200, the serial monitor shows nothing. Then I put it back to 9600 and the serial monitor displays the information.

MoBot_v0.4.3.ino (6.5 KB)

Do not use floating-point values for lat/lon. Use the integer forms, where you multiply by 10000000:

  NeoGPS::Location_t target( 348326564L, -922737374L );

A float only has about 6 or 7 digits of precision, so your target was ignoring the last 1 or 2 digits. The long integer forms have 10 significant digits. It's also the form used internally by NeoGPS, to maintain the full precision provided by the GPS device.

And you should use a NeoGPS::Location_t to hold on to a lat/lon coordinate pair, just like the target:

  NeoGPS::Location_t Current;

Then it's easy to calculate distance and bearing with that "location type":

    Current  = fix.location;
    distance = Current.DistanceKm( target );
    bearing  = Current.BearingToDegrees( target );

See also NMEAdistance.ino and Tabular.ino for examples. Location.h is the header file for that structure.

I can not change the GPS baud rate above 9600.

I can't see what you tried (hint, hint), but to change the baud rate you have to do something like this:

  gpsPort.begin( 9600 );                      // assume it's still @ 9600
  gpsPort.println( F("$PMTK,xxx,xxxx,*FF") ); // send the baudrate command
  gpsPort.flush();                            // and wait for it to be sent (1ms per character)

  delay( 200 );           // Let the GPS device start *sending* at new baudrate

  gpsPort.end();          // Throw away any received characters
  gpsPort.begin( xxxx );  // Start *receiving* at the new baudrate

If after trying this, it doesn't report any fixes, try NMEAdiagnostic.ino. It will try all the baudrates and display a few extra messages and received data. Of course, it will use the serial port specified by GPSport.h

Or try a simple echo test:

void setup()
{
  gpsPort.begin( 19200 );
  Serial.begin( 19200 );
}

void loop()
{
  if (gpsPort.available())
    Serial.write( gpsPort.read() );
}

I should have been more specific about the baud rate issue. Currently I'm using AltSoftwareSerial. I meant that I am unable to change the baud rate above 9600 in the program. If I set it to 19200 or higher in the program, then when I open the serial port, it's blank, nothing, nada. I ran the AltSoftwareSerial echo.ino and the issue was still the same. Anything above 9600 in the program, prevents the serial monitor from displaying anything.

I will however try your solutions above and report back. Thanks guys for your help. I am learning so much.

End mushiness.....

How would I go about reading the waypoints from an SD card, instead using up memory to add more waypoints?
I can read the waypoints using the SdFat library. I just cannot incorporate it into the NeoGPS::Location_t. Thanks in advance.

Mad_Monkey_Project

I am trying to start building an autonomous rc car using gps from adafruit and the tinygps library like you were initially doing. It seems like the "great circle" distance computation tinygps uses is inefficient after reading about neogps. I am using an rc car with a servo for steering. Does this add complication to the routine that determines which way to turn? Seems like you would need to tell it how much to turn in a certain direction. I guess one could tell it to turn max right or max left?

Also did you get all the issues worked out with your code and would you be willing to post or share your final version? Thanks much and hope I am doing this post correctly as it is my first time to post in these forums