├── README.md └── gpslogger.ino /README.md: -------------------------------------------------------------------------------- 1 | Arduino GPS Logger 2 | ================== 3 | 4 | Fetch GPS data over serial and log it to an SD card. 5 | 6 | This uses TinyGPS to decode NMEA (RMS and GGA) sentences from a [LS20031 GPS module](https://www.sparkfun.com/products/8975), and then uses the the SD library to write GPX formatted data to a [micro SD card](https://www.sparkfun.com/products/544). 7 | 8 | This is a functional logger, and a good lesson about GPS modules, NMEA, and SD/FAT from Arduino. It is probably not the most efficient GPS logger (see "improvements"). 9 | 10 | ![assembled logger](https://farm8.staticflickr.com/7580/16198442305_999b0dc2b0_z.jpg) 11 | 12 | [more photos on flickr](https://www.flickr.com/photos/markfickett/sets/72157650108735212) 13 | 14 | Setup 15 | ----- 16 | 17 | Recommended configuration for the LS20031 GPS module: 18 | 19 | // FULL COLD RESTART (clears any bad almanac data) 20 | Serial.println("$PMTK104*37"); 21 | // GGA + RMC (all that is used by TinyGPS), 1Hz 22 | Serial.println("$PMTK314,0,5,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0*28"); 23 | // Reduce serial output rate 57600 => 14400 baud, since SoftwareSerial 24 | // on an 8MHz Arduino Pro Mini can't keep up (though an Uno can). 25 | Serial.println("$PMTK251,14400*29"); 26 | 27 | Since the module sometimes loses its configuration, the Arduino startup code always sends the latter two configuration commands. 28 | 29 | Also edit SoftwareSerial.h to have a larger buffer (default is 64): 30 | 31 | #define _SS_MAX_RX_BUFF 256 32 | 33 | On MacOS, it's in Arduino.app in Contents/Resources/Java/libraries/SoftwareSerial/. 34 | 35 | Voltage Detection 36 | ----------------- 37 | 38 | Voltage and power-off detection is designed for an Arduino Pro Mini 3.3v 39 | powered from at least 3.8v (3x NiMH AAs), with a 680uF 12+v capacitor across 40 | the raw voltage supply, and a 560KOhm/120KOhm voltage divider to A3. The 41 | setup (with GPS and SD writing) draws around 42mA. 42 | 43 | Improvements 44 | ------------ 45 | 46 | * [The MTK3339 from Adafruit](http://www.adafruit.com/product/746) looks better for this application: smaller, lower power (20mA), built-in datalogging, 5v safe but accepts 3.3v power. 47 | * Power-off detection is unreliable; a shutdown switch (perhaps a capacitive touch sensor on the casing) would be safer. 48 | 49 | Resources 50 | --------- 51 | 52 | * [SparkFun GPS getting-started guide](https://www.sparkfun.com/tutorials/176) 53 | * [NMEA explanations](http://www.gpsinformation.org/dale/nmea.htm#position) 54 | * [NMEA checksum calculator](http://www.hhhh.org/wiml/proj/nmeaxor.html) 55 | * GPX tags specification on [OpenStreetMap](http://wiki.openstreetmap.org/wiki/GPX) or [Topografix](http://www.topografix.com/gpx_manual.asp) 56 | * [Similar project on Arduino forums](http://forum.arduino.cc/index.php?topic=199019.15) 57 | -------------------------------------------------------------------------------- /gpslogger.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch GPS data over serial and log it to an SD card. See README for more. 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #define SAMPLE_INTERVAL_MS 1000 10 | 11 | #define PIN_STATUS_LED 13 12 | 13 | #define PIN_RX_FROM_GPS 2 14 | #define PIN_TX_TO_GPS 4 15 | #define PIN_SD_CHIP_SELECT 10 16 | #define PIN_SPI_CHIP_SELECT_REQUIRED 10 17 | // The SD library requires, for SPI communication: 18 | // 11 MOSI (master/Arduino output, slave/SD input) 19 | // 12 MISO (master input, slave output) 20 | // 13 CLK (clock) 21 | 22 | #define DEFAULT_GPS_BAUD 57600 23 | #define GPS_BAUD 14400 24 | 25 | // Seek to fileSize + this position before writing track points. 26 | #define SEEK_TRKPT_BACKWARDS -24 27 | #define GPX_EPILOGUE "\t\n\n" 28 | #define LATLON_PREC 6 29 | 30 | #define PIN_BATTERY_DIVIDED_VOLTAGE A3 31 | #define VOLTAGE_ANALOG_CONSTANT 53.71 32 | #define CUTOFF_DV -0.1 33 | #define CUTOFF_VOLTAGE 3.4 34 | 35 | TinyGPS gps; 36 | SoftwareSerial nss(PIN_RX_FROM_GPS, PIN_TX_TO_GPS); 37 | 38 | SdFat sd; 39 | SdFile gpxFile; 40 | SdFile voltageFile; 41 | 42 | // General-purpose text buffer used in formatting. 43 | char buf[32]; 44 | 45 | struct GpsSample { 46 | float lat_deg, 47 | lon_deg; 48 | float altitude_m; 49 | 50 | int satellites; 51 | int hdop_hundredths; 52 | 53 | // How many ms, according to millis(), since the last position data was read 54 | // from the GPS. 55 | unsigned long fix_age_ms; 56 | 57 | float speed_mps; 58 | float course_deg; 59 | 60 | // How many ms, according to millis(), since the last datetime data was read 61 | // from the GPS. 62 | unsigned long datetime_fix_age_ms; 63 | 64 | int year; 65 | byte month, 66 | day, 67 | hour, 68 | minute, 69 | second, 70 | hundredths; 71 | }; 72 | // The latest sample read from the GPS. 73 | struct GpsSample sample; 74 | 75 | // The previously sampled / recorded voltages. 76 | float lastVoltage; 77 | float lastRecordedVoltage; 78 | byte lastRecordedVoltageMinute; 79 | 80 | void setup() { 81 | pinMode(PIN_STATUS_LED, OUTPUT); 82 | digitalWrite(PIN_STATUS_LED, HIGH); 83 | lastVoltage = 0; 84 | lastRecordedVoltage = 0; 85 | lastRecordedVoltageMinute = 61; 86 | Serial.begin(115200); 87 | setUpSd(); 88 | resetGpsConfig(); 89 | getFirstGpsSample(); 90 | startFilesOnSdNoSync(); 91 | digitalWrite(PIN_STATUS_LED, LOW); 92 | } 93 | 94 | void loop() { 95 | readFromGpsUntilSampleTime(); 96 | fillGpsSample(gps); 97 | if (sample.fix_age_ms <= SAMPLE_INTERVAL_MS) { 98 | // TODO: Write whenever there is new data (trust GPS is set at 1Hz). 99 | writeGpxSampleToSd(); 100 | } 101 | recordVoltage(); 102 | } 103 | 104 | void setUpSd() { 105 | if (PIN_SD_CHIP_SELECT != PIN_SPI_CHIP_SELECT_REQUIRED) { 106 | pinMode(PIN_SPI_CHIP_SELECT_REQUIRED, OUTPUT); 107 | } 108 | 109 | if (!sd.begin(PIN_SD_CHIP_SELECT, SPI_QUARTER_SPEED)) { 110 | sd.initErrorHalt(); 111 | } 112 | } 113 | 114 | /** 115 | * Redoes GPS configuration, assuming it has factory-default settings. 116 | * 117 | * Although this should only have to be done once, the GPS module sometimes 118 | * drops these customizations (possibly due to power brown-out). 119 | */ 120 | void resetGpsConfig() { 121 | digitalWrite(PIN_STATUS_LED, HIGH); 122 | nss.begin(DEFAULT_GPS_BAUD); 123 | nss.println("$PMTK314,0,5,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0*28"); 124 | nss.println("$PMTK251,14400*29"); 125 | nss.flush(); 126 | nss.end(); 127 | digitalWrite(PIN_STATUS_LED, LOW); 128 | 129 | nss.begin(GPS_BAUD); 130 | } 131 | 132 | void getFirstGpsSample() { 133 | nss.begin(GPS_BAUD); 134 | 135 | while (true) { 136 | readFromGpsUntilSampleTime(); 137 | fillGpsSample(gps); 138 | if (sample.fix_age_ms == TinyGPS::GPS_INVALID_AGE) { 139 | Serial.println(F("Waiting for location fix.")); 140 | } else if (sample.fix_age_ms == TinyGPS::GPS_INVALID_AGE) { 141 | Serial.println(F("Waiting for datetime fix.")); 142 | } else { 143 | Serial.println(F("Got GPS fix.")); 144 | break; 145 | } 146 | } 147 | } 148 | 149 | static void readFromGpsUntilSampleTime() { 150 | unsigned long start = millis(); 151 | // Process a sample from the GPS every second. 152 | while (millis() - start < SAMPLE_INTERVAL_MS) { 153 | readFromGpsSerial(); 154 | getVoltageMaybeExit(); 155 | } 156 | } 157 | 158 | static bool readFromGpsSerial() { 159 | while (nss.available()) { 160 | gps.encode(nss.read()); 161 | } 162 | } 163 | 164 | static void startFilesOnSdNoSync() { 165 | // directory 166 | sprintf( 167 | buf, 168 | "%02d%02d%02d", 169 | sample.year, 170 | sample.month, 171 | sample.day); 172 | if (!sd.exists(buf)) { 173 | if (!sd.mkdir(buf)) { 174 | sd.errorHalt("Creating log directory for today failed."); 175 | } 176 | } 177 | 178 | // SdFat will silently die if given a filename longer than "8.3" 179 | // (8 characters, a dot, and 3 file-extension characters). 180 | 181 | // GPX log 182 | openTimestampedFile(".gpx", gpxFile); 183 | gpxFile.print(F( 184 | "\n" 185 | "\n" 186 | "\t\n")); 187 | gpxFile.print(F(GPX_EPILOGUE)); 188 | 189 | // input (battery) voltage log 190 | openTimestampedFile("v.csv", voltageFile); 191 | voltageFile.print(F("datetime,voltage\n")); 192 | } 193 | 194 | static void openTimestampedFile(const char *shortSuffix, SdFile &file) { 195 | sprintf( 196 | buf, 197 | "%02d%02d%02d/%02d%02d%02d%s", 198 | sample.year, 199 | sample.month, 200 | sample.day, 201 | sample.hour, 202 | sample.minute, 203 | sample.second, 204 | shortSuffix); 205 | Serial.print(F("Starting file ")); 206 | Serial.println(buf); 207 | if (sd.exists(buf)) { 208 | Serial.println(F("warning: already exists, overwriting.")); 209 | } 210 | if (!file.open(buf, O_CREAT | O_WRITE)) { 211 | sd.errorHalt(); 212 | } 213 | } 214 | 215 | static void writeFloat(float v, SdFile &file, int precision) { 216 | obufstream ob(buf, sizeof(buf)); 217 | ob << setprecision(precision) << v; 218 | file.print(buf); 219 | } 220 | 221 | static void writeFormattedSampleDatetime(SdFile &file) { 222 | sprintf( 223 | buf, 224 | "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", 225 | sample.year, 226 | sample.month, 227 | sample.day, 228 | sample.hour, 229 | sample.minute, 230 | sample.second, 231 | sample.hundredths); 232 | file.print(buf); 233 | } 234 | 235 | static void writeGpxSampleToSd() { 236 | gpxFile.seekSet(gpxFile.fileSize() + SEEK_TRKPT_BACKWARDS); 237 | gpxFile.print(F("\t\t")); 244 | getVoltageMaybeExit(); 245 | 246 | gpxFile.print(F("")); 249 | 250 | if (sample.altitude_m != TinyGPS::GPS_INVALID_F_ALTITUDE) { 251 | gpxFile.print(F("")); // meters 252 | writeFloat(sample.altitude_m, gpxFile, 2 /* centimeter precision */); 253 | gpxFile.print(F("")); 254 | } 255 | getVoltageMaybeExit(); 256 | 257 | if (sample.speed_mps != TinyGPS::GPS_INVALID_F_SPEED) { 258 | gpxFile.print(F("")); 259 | writeFloat(sample.speed_mps, gpxFile, 1); 260 | gpxFile.print(F("")); 261 | } 262 | if (sample.course_deg != TinyGPS::GPS_INVALID_F_ANGLE) { 263 | gpxFile.print(F("")); 264 | writeFloat(sample.course_deg, gpxFile, 1); 265 | gpxFile.print(F("")); 266 | } 267 | getVoltageMaybeExit(); 268 | 269 | if (sample.satellites != TinyGPS::GPS_INVALID_SATELLITES) { 270 | gpxFile.print(F("")); 271 | gpxFile.print(sample.satellites); 272 | gpxFile.print(F("")); 273 | } 274 | if (sample.hdop_hundredths != TinyGPS::GPS_INVALID_HDOP) { 275 | gpxFile.print(F("")); 276 | writeFloat(sample.hdop_hundredths / 100.0, gpxFile, 2); 277 | gpxFile.print(F("")); 278 | } 279 | getVoltageMaybeExit(); 280 | 281 | gpxFile.print(F("\n")); 282 | 283 | gpxFile.print(F(GPX_EPILOGUE)); 284 | 285 | getVoltageMaybeExit(); 286 | digitalWrite(PIN_STATUS_LED, HIGH); 287 | if (!gpxFile.sync() || gpxFile.getWriteError()) { 288 | Serial.println(F("SD sync/write error.")); 289 | } 290 | digitalWrite(PIN_STATUS_LED, LOW); 291 | } 292 | 293 | static void fillGpsSample(TinyGPS &gps) { 294 | gps.f_get_position( 295 | &sample.lat_deg, 296 | &sample.lon_deg, 297 | &sample.fix_age_ms); 298 | sample.altitude_m = gps.f_altitude(); 299 | 300 | sample.satellites = gps.satellites(); 301 | sample.hdop_hundredths = gps.hdop(); 302 | 303 | sample.course_deg = gps.f_course(); 304 | sample.speed_mps = gps.f_speed_mps(); 305 | 306 | gps.crack_datetime( 307 | &sample.year, 308 | &sample.month, 309 | &sample.day, 310 | &sample.hour, 311 | &sample.minute, 312 | &sample.second, 313 | &sample.hundredths, 314 | &sample.datetime_fix_age_ms); 315 | } 316 | 317 | static float getVoltageMaybeExit() { 318 | // TODO: Do safety calculation in int to be faster? 319 | float voltage = 320 | analogRead(PIN_BATTERY_DIVIDED_VOLTAGE) / VOLTAGE_ANALOG_CONSTANT; 321 | float dv = voltage - lastVoltage; 322 | // Regulated (USB) supply voltage is approximately 0 on the battery vin, 323 | // otherwise abort if vin is low or falling. 324 | if (voltage > 2.0 325 | && (voltage < CUTOFF_VOLTAGE || dv < CUTOFF_DV)) { 326 | gpxFile.close(); 327 | voltageFile.print(F("x")); 328 | voltageFile.close(); 329 | digitalWrite(PIN_STATUS_LED, HIGH); 330 | Serial.print(F("Exiting, final voltage was ")); 331 | Serial.println(voltage); 332 | Serial.flush(); 333 | exit(0); 334 | // TODO: Resume in case of a false alarm. 335 | } 336 | lastVoltage = voltage; 337 | return voltage; 338 | } 339 | 340 | static bool recordVoltage() { 341 | float voltage = getVoltageMaybeExit(); 342 | if (voltage != lastRecordedVoltage 343 | || lastRecordedVoltageMinute != sample.minute) { 344 | writeFormattedSampleDatetime(voltageFile); 345 | voltageFile.print(F(",")); 346 | writeFloat(voltage, voltageFile, 2); 347 | voltageFile.print(F("\n")); 348 | lastRecordedVoltageMinute = sample.minute; 349 | lastRecordedVoltage = voltage; 350 | 351 | digitalWrite(PIN_STATUS_LED, HIGH); 352 | if (!voltageFile.sync() || voltageFile.getWriteError()) { 353 | Serial.println(F("SD sync/write error.")); 354 | } 355 | digitalWrite(PIN_STATUS_LED, LOW); 356 | } 357 | 358 | } 359 | --------------------------------------------------------------------------------