Built-in project · Arduino Mega 2560 R3

GPS logger on an Arduino Mega

A NEO-6M on the Mega’s second serial port and a microSD card: every position goes into TRACK.CSV, which you can watch grow and download. The whole build, an Arduino Mega and 2 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Advanced About 30 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $58. On Mokxi they're free.

Step by step, with the wiring and the common mistakes: Arduino SD Card Module: A GPS Data Logger

The circuit runs here once the page has loaded.

What you'll learn

  • Using a second hardware serial port
  • Parsing GPS data with a library
  • Writing a CSV file to an SD card over SPI
  • Batching writes so a slow device does not lose data
  • Telling when a GPS has lost its fix
  • Difficulty: Advanced
  • Time: About 30 minutes
  • Board: Arduino Mega 2560 R3
  • Parts: 4

How it works

The GPS module streams text sentences on the Mega's second serial port, pin 19. The TinyGPS++ library decodes them one character at a time. Each time a new position arrives, the sketch builds a line with the time, latitude, longitude and speed in km/h.

SD cards are slow, and the GPS does not wait for them. So the sketch keeps five lines in memory and writes them all to TRACK.CSV together. The Serial Monitor also shows each line as it is made.

If the position stops updating for three seconds, it prints No fix yet. once every three seconds.

Parts list

4 parts, plus the jumper wires. They retail for about $58; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
GPS module, NEO-6M pin TXArduino Mega 2560 R3 pin 19
microSD card module pin MISOArduino Mega 2560 R3 pin 50
microSD card module pin MOSIArduino Mega 2560 R3 pin 51
microSD card module pin SCKArduino Mega 2560 R3 pin 52
microSD card module pin CSArduino Mega 2560 R3 pin 53
GPS module, NEO-6M pin VCCArduino Mega 2560 R3 pin 5V
microSD card module pin VCCArduino Mega 2560 R3 pin 5V
GPS module, NEO-6M pin GNDArduino Mega 2560 R3 pin GND
microSD card module pin GNDArduino Mega 2560 R3 pin GND

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Mega 2560 R3
// GPS logger: where the module is, once a second, written to a microSD card.
//
// The NEO-6M sends NMEA sentences on Serial1 (its TX to the Mega's RX1, pin
// 19). TinyGPS++ reads them a character at a time; each time a new position
// comes in, one line "time,latitude,longitude,km/h" is added to TRACK.CSV.
//
// The module is moving: its speed and course properties walk the position
// along a heading once a second. Change them and watch the numbers change.
// Click the GPS to take its fix away: the sentences keep coming, but empty,
// the position stops updating and its age() climbs, and nothing is logged
// until the fix is back.
//
// The track grows in the list beside the card. Click TRACK.CSV to download it;
// it opens in a spreadsheet or a mapping tool.
//
// The card is slow and the GPS does not wait. While the sketch is busy writing
// a sector, the bytes arriving on Serial1 pile up in a 64-byte buffer and the
// rest of the sentence is lost. So the lines are kept in memory and written
// to the card five at a time: four seconds in five nothing is missed.

const int LINES_PER_WRITE = 5;
const int LINE_LENGTH = 48;
char pending[LINES_PER_WRITE][LINE_LENGTH];
int pendingCount = 0;

#include <SPI.h>
#include <SD.h>
#include <TinyGPS++.h>

TinyGPSPlus gps;
const int CARD_CS = 53;

bool cardReady = false;
unsigned long lastWarning = 0;
uint32_t lastStamp = 0xFFFFFFFF;

void setup()
{
    Serial.begin(9600);
    Serial1.begin(9600);
    cardReady = SD.begin(CARD_CS);
    Serial.println(cardReady ? "Card ready." : "No card: positions go to the serial monitor only.");
    if (cardReady) {
        File file = SD.open("track.csv", FILE_WRITE);
        if (file) {
            if (file.size() == 0) {
                file.println("time,lat,lng,kmh");
            }
            file.close();
        }
    }
}

// A Print that writes into one of the pending lines.
class LineBuffer : public Print {
  public:
    explicit LineBuffer(char *line) : line_(line) { line_[0] = 0; }
    size_t write(uint8_t c) override
    {
        if (length_ + 1 >= LINE_LENGTH) {
            return 0;
        }
        line_[length_++] = (char)c;
        line_[length_] = 0;
        return 1;
    }

  private:
    char *line_;
    int length_ = 0;
};

void printTwo(Print &out, int value)
{
    if (value < 10) {
        out.print('0');
    }
    out.print(value);
}

void writeLine(Print &out)
{
    printTwo(out, gps.time.hour());
    out.print(':');
    printTwo(out, gps.time.minute());
    out.print(':');
    printTwo(out, gps.time.second());
    out.print(',');
    out.print(gps.location.lat(), 6);
    out.print(',');
    out.print(gps.location.lng(), 6);
    out.print(',');
    out.println(gps.speed.kmph(), 1);
}

void writePending()
{
    File file = SD.open("track.csv", FILE_WRITE);
    if (file) {
        for (int i = 0; i < pendingCount; i++) {
            file.print(pending[i]);
        }
        file.close();
    }
    pendingCount = 0;
}

void loop()
{
    while (Serial1.available() > 0) {
        gps.encode(Serial1.read());
    }
    if (!gps.location.isUpdated()) {
        // No new position. After three seconds of that, say so, once every
        // three seconds.
        if (gps.location.age() > 3000 && millis() - lastWarning > 3000) {
            Serial.println("No fix yet.");
            lastWarning = millis();
        }
        return;
    }
    // RMC and GGA both carry the position, so it is updated twice a second;
    // one line a second is enough, so the second one is skipped by its time.
    uint32_t stamp = gps.time.value();
    if (stamp == lastStamp) {
        gps.location.lat(); // read, so isUpdated() waits for the next one
        return;
    }
    lastStamp = stamp;
    LineBuffer line(pending[pendingCount]);
    writeLine(line);
    Serial.print(pending[pendingCount]);
    pendingCount++;
    if (pendingCount == LINES_PER_WRITE) {
        if (cardReady) {
            writePending();
        }
        pendingCount = 0;
    }
}

Code walkthrough

The key functions of the sketch, and what each one does.

setup sketch.ino

Starts the serial monitor and the GPS port at 9600 baud, then tries to start the SD card on chip select pin 53. If the card works and the file is empty, it writes the header line time,lat,lng,kmh. If not, positions only go to the monitor.

void setup()
{
    Serial.begin(9600);
    Serial1.begin(9600);
    cardReady = SD.begin(CARD_CS);
    Serial.println(cardReady ? "Card ready." : "No card: positions go to the serial monitor only.");
    if (cardReady) {
        File file = SD.open("track.csv", FILE_WRITE);
        if (file) {
            if (file.size() == 0) {
                file.println("time,lat,lng,kmh");
            }
            file.close();
        }
    }
}

writePending sketch.ino

Opens the file, writes every saved line in one go, closes it and resets pendingCount. Doing this once per five lines keeps the card busy only a short time, so the serial buffer does not overflow.

void writePending()
{
    File file = SD.open("track.csv", FILE_WRITE);
    if (file) {
        for (int i = 0; i < pendingCount; i++) {
            file.print(pending[i]);
        }
        file.close();
    }
    pendingCount = 0;
}

loop sketch.ino

Feeds every waiting GPS character to gps.encode. When a new position is ready, it skips repeats within the same second, builds one line with writeLine and stores it. After LINES_PER_WRITE lines, it writes them to the card.

void loop()
{
    while (Serial1.available() > 0) {
        gps.encode(Serial1.read());
    }
    if (!gps.location.isUpdated()) {
        // No new position. After three seconds of that, say so, once every
        // three seconds.
        if (gps.location.age() > 3000 && millis() - lastWarning > 3000) {
            Serial.println("No fix yet.");
            lastWarning = millis();
        }
        return;
    }
    // RMC and GGA both carry the position, so it is updated twice a second;
    // one line a second is enough, so the second one is skipped by its time.
    uint32_t stamp = gps.time.value();
    if (stamp == lastStamp) {
        gps.location.lat(); // read, so isUpdated() waits for the next one
        return;
    }
    lastStamp = stamp;
    LineBuffer line(pending[pendingCount]);
    writeLine(line);
    Serial.print(pending[pendingCount]);
    pendingCount++;
    if (pendingCount == LINES_PER_WRITE) {
        if (cardReady) {
  // ... 5 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The monitor says No card This project
    Check the four SPI wires: MISO on pin 50, MOSI on 51, SCK on 52 and CS on 53. On the Mega these are different pins than on the Uno.
  • No lines appear This project
    The GPS TX must go to pin 19 (RX1). If it went to pin 18, the Mega would not hear it.
  • The file seems empty after a few seconds This project
    Lines reach the card in groups of five, so wait at least five seconds.
  • Garbage text or no data from the module. GPS module
    The default is 9600 baud. Connect module TX to the board RX, and RX to TX. Set the serial speed to match. Use SoftwareSerial pins on an Uno.
  • No fix is ever reported. GPS module
    A real NEO-6M needs a clear sky view and up to several minutes for a cold start. Indoors it usually never fixes. Keep it powered so it stays warm.
  • SD.begin() fails. microSD card module
    Check MOSI, MISO, SCK and CS wiring, and that CS matches the pin in your code. Use a card formatted FAT16 or FAT32. Many modules are 5 V logic with a level shifter, so check your board.
  • A file is empty or corrupted. microSD card module
    Call file.close() or file.flush() after writing, before removing power. Use short 8.3 style file names. Do not open the same file twice at once.
  • The upload fails or nothing runs. Arduino Mega 2560 R3
    Under Tools, choose Arduino Mega or Mega 2560 as the board, then pick the right port. An Uno selection uploads the wrong build.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.