Understanding why should avoid “String” and alternative solutions

First, let me point out a problem in your code, completely unrelated to your question:

AcX = Wire.read()<<8|Wire.read();

The C++ standard does not specify in which order the two reads will be performed. This may work well with the particular version of the particular compiler you are using, but it can break the day you (or an update to the Arduino IDE) changes the compiler version or a compiler flag. You should instead do a single read per statement, e.g.:

AcX  = Wire.read() << 8;
AcX |= Wire.read();

Next, I would like to say that the simplest Arduino way of doing what you are trying to do is to use neither a String nor a character array. Instead, open the file first, then format and send the data on the fly using print() or println():

dataFile.print(Time0); dataFile.print(",");
dataFile.print(AcX);   dataFile.print(",");
[...]

This way you never need to store the whole string in memory. It is then a very memory-friendly solution.

Now, I realize this seems inconvenient if you want to print the data to both the SD file and the serial port. In such a case you could put the printing code inside a function that can print to any Print object (for instance the data file and Serial), and you can then call the function twice:

static void printImuData(Print &printer, int16_t Time0,
        int16_t AcX, int16_t AcY, int16_t AcZ,
        int16_t GyX, int16_t GyY, int16_t GyZ)
{
    printer.print(Time0); printer.print(",");
    printer.print(AcX);   printer.print(",");
    [...]
}

[...]

printImuData(dataFile, Time0, AcX, AcY, AcZ, GyX, GyY, GyZ);
if (Serial_plus_SD)
    printImuData(Serial, Time0, AcX, AcY, AcZ, GyX, GyY, GyZ);

Alternatively, you could use a kind of tee to duplicate the data to both streams. See for example this implementation.

Edit: A more elegant way to achieve the same would be to wrap all the IMU data into a struct (or a class), and then make the whole thing printable:

struct ImuData : public Printable
{
    uint16_t Time0, AcX, AcY, AcZ, GyX, GyY, GyZ;
    size_t printTo(Print& p) const;
};

size_t ImuData::printTo(Print& p) const
{
    size_t bytes = 0;
    bytes += p.print(Time0); bytes += p.write(',');
    [...]
    bytes += p.print(GyZ);
    return bytes;
}

The you can print the struct like you would print the string:

ImuData imu;
imu.Time0 = ...;
imu.AcX   = Wire.read() << 8;
imu.AcX  |= Wire.read();
[...]

dataFile.println(imu);
if (Serial_plus_SD)
    Serial.println(imu);

Edgar answers your first question about String and printing very nicely. Majenko also has a nice description here of the pitfalls.

Regarding your second question about GPS/efficiency/speed:

  1. use NeoGPS,
  2. use AltSoftSerial,
  3. use the MPU FIFO,
  4. use the up-to-date SdFat,
  5. watch out for SD write delays, and
  6. close the log file at some point.


1. NeoGPS is the smallest, fastest and most accurate GPS parser available. The example programs are structured properly, and there is an example for SD logging.

NeoGPS has a FIFO of fixes, unlike other libraries. This avoids "sampling" the GPS at some rate defined by millis(). The Arduino crystal is not as accurate as the atomic GPS clock, so your logging rate should be based off a fix becoming available, at exactly one per second. Your sketch will "drift" against the GPS updates, losing a fix periodically (depending on the accuracty of the crystal).

2. Perhaps the single greatest improvement you could achieve is NOT to use SoftwareSerial. It is very inefficient, because it disables interrupts for long periods of time. At 9600, it disables interrupts for 1ms while each character from the GPS device is received. The Arduino could have executed ~10000 instructions during that time, but instead it twiddles its thumbs while waiting exclusively for each bit of a byte to arrive... -_-

Instead, you should use AltSoftSerial, as you have connected the GPS device to the required pins, 8 & 9. If you were using two other pins, you could have used my NeoSWSerial. Both of these libraries are much, much more efficient than SoftwareSerial.

3. If you want evenly-spaced MPU samples, put the device in the FIFO mode. It will write the selected registers into a FIFO at a selected sampling time. This also gives you a little flexibility in reading the sample: you just have to read it before the 1024-byte FIFO fills up.

4. You are using an old version of the SD library. I strongly suggest that you use the latest SdFat library. It has many improvements and bug fixes.

5. Some SD cards will occasionally take a longer time to write. The 47ms write time is not unusual. In fact, 100ms is fairly common. As long as the MPU FIFO does not overflow during this time, you will not lose any MPU samples.

If you find that an SD write occasionally causes the FIFO to overflow, you may have to modify the SDFat library to call a yield function while it's waiting for the write to complete. This yield function can read from the FIFO during that time, saving the samples into an Arduino RAM area.

6. I would recommend that you close the logFile when some event occurs. Perhaps a button press? You can use the LED to indicate that logging is active or inactive. You could also watch for inactivity for some period of time (no speed, MPU zero values).


With those suggestions, here is a NeoGPS/AltSoftSerial/SdFat version of your original sketch (GPS tested, MPU/SD untested but compiles):

#include <SPI.h>
#include <SdFat.h>
#include <NMEAGPS.h>
#include <Wire.h>
#define ARDUINO_USD_CS 10 // uSD card CS pin (pin 10 on SparkFun GPS Logger Shield)

/////////////////////////
// Log File Defintions //
/////////////////////////
// Keep in mind, the SD library has max file name lengths of 8.3 - 8 char prefix,
// and a 3 char suffix.
char logFileName[13] = "gpslogXX.csv";
// Our log files are called "gpslogXX.csv, so "gpslog99.csv" is our max file.
#define MAX_LOG_FILES 100 // Number of log files that can be made

// Data to be logged:
#define LOG_COLUMN_HEADER \
  "longitude," "latitude," "altitude," "speed," "course," "date," "time," \
  "satellites," "Acc.X," "Acc.Y," "Acc.Z," "Gy.X," "Gy.Y," "Gy.Z," "Temp"
  // printed at the top of the file.

SdFat SD;
File  logFile;

/////////////////////////
// NeoGPS Definitions //
/////////////////////////
NMEAGPS gps; // NeoGPS object to be used throughout
gps_fix fix; // The latest GPS information received from the gpsPort
#define GPS_BAUD 9600 // GPS module's default baud rate

/////////////////////////////////
// GPS Serial Port Definitions //
/////////////////////////////////
// If you're using a Mega, Leo or Due, use Serial1 for the GPS:
//#define gpsPort Serial1

// If you're using an Arduino Uno or other ATmega328 board that uses the
// 0/1 UART for programming/Serial monitor-ing, use AltSoftSerial:
#include <AltSoftSerial.h>
AltSoftSerial gpsPort; // Always on pins 8 & 9
//If you can't use pins 8 & 9, use this:
//#include <NeoSWSerial.h>
//NeoSWSerial gpsPort( 2, 3 ).

// Define the serial monitor port. On the Uno, Mega, and Leonardo this is 'Serial'
//  on other boards this may be 'SerialUSB'
#define SerialMonitor Serial

//#define USE_MPU
const int MPU=0x68;  // I2C address of the MPU-6050
int16_t AcX,AcY,AcZ,Tmp,GyX,GyY,GyZ;


void setup()
{
  Wire.begin();
  Wire.beginTransmission(MPU);
  Wire.write(0x6B);  // PWR_MGMT_1 register
  Wire.write(0);     // set to zero (wakes up the MPU-6050)
  Wire.endTransmission(true);

  SerialMonitor.begin(9600);
  gpsPort.begin(GPS_BAUD);

  SerialMonitor.println( F("Setting up SD card.") );

  updateFileName(); // Each time we start, create a new file, increment the number
  // see if the card is present and can be initialized:
  if (!SD.begin(ARDUINO_USD_CS))
  {
    SerialMonitor.println( F("Error initializing SD card.") );
  } else {
    logFile = SD.open( logFileName, FILE_WRITE );
    // Print a header at the top of the new file
    logFile.println( F(LOG_COLUMN_HEADER) );
  }

}

void loop()
{
  Wire.beginTransmission(MPU);
  Wire.write(0x3B);  // starting with register 0x3B (ACCEL_XOUT_H)
  Wire.endTransmission(false);
  Wire.requestFrom(MPU,14,true);  // request a total of 14 registers
  AcX=Wire.read()<<8|Wire.read();  // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)     
  AcY=Wire.read()<<8|Wire.read();  // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
  AcZ=Wire.read()<<8|Wire.read();  // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
  Tmp=Wire.read()<<8|Wire.read();  // 0x41 (TEMP_OUT_H) & 0x42 (TEMP_OUT_L)
  GyX=Wire.read()<<8|Wire.read();  // 0x43 (GYRO_XOUT_H) & 0x44 (GYRO_XOUT_L)
  GyY=Wire.read()<<8|Wire.read();  // 0x45 (GYRO_YOUT_H) & 0x46 (GYRO_YOUT_L)
  GyZ=Wire.read()<<8|Wire.read();  // 0x47 (GYRO_ZOUT_H) & 0x48 (GYRO_ZOUT_L)

  while (gps.available( gpsPort )) {
    fix = gps.read();  // get the entire fix structure, once per second

    if (logGPSData()) { // Log the GPS data
      SerialMonitor.println( F("GPS logged.") ); // Print a debug message
    } else {// If we failed to log GPS
      // Print an error, don't update lastLog
      SerialMonitor.println( F("Failed to log new GPS data.") );
    }
  }
}


byte logGPSData()
{
  if (logFile.isOpen())
  { // Print longitude, latitude, altitude (in feet), speed (in mph), course
    // in (degrees), date, time, and number of satellites.

    if (fix.valid.location)
      logFile.print(fix.longitude(), 6);
    logFile.print(',');
    if (fix.valid.location)
      logFile.print(fix.latitude(), 6);
    logFile.print(',');
    if (fix.valid.altitude)
      logFile.print(fix.altitude() * 3.2808, 1);
    logFile.print(',');
    if (fix.valid.speed)
      logFile.print(fix.speed_mph(), 1);
    logFile.print(',');
    if (fix.valid.heading)
      logFile.print(fix.heading(), 1);
    logFile.print(',');

    if (fix.valid.date) {
      logFile.print( fix.dateTime.full_year() );
      if (fix.dateTime.month < 10)
        logFile.print( '0' );
      logFile.print( fix.dateTime.month );
      if (fix.dateTime.date < 10)
        logFile.print( '0' );
      logFile.print( fix.dateTime.date );
    }
    logFile.print(',');

    if (fix.valid.time) {
      if (fix.dateTime.hours < 10)
        logFile.print( '0' );
      logFile.print( fix.dateTime.hours );
      if (fix.dateTime.minutes < 10)
        logFile.print( '0' );
      logFile.print( fix.dateTime.minutes );
      if (fix.dateTime.seconds < 10)
        logFile.print( '0' );
      logFile.print( fix.dateTime.seconds );
    }
    logFile.print(',');

    if (fix.valid.satellites)
      logFile.print(fix.satellites);
    logFile.print(',');
    logFile.print(AcX);
    logFile.print(',');
    logFile.print(AcY);
    logFile.print(',');
    logFile.print(AcZ);
    logFile.print(',');
    logFile.print(GyX);
    logFile.print(',');
    logFile.print(GyY);
    logFile.print(',');
    logFile.print(GyZ);
    logFile.print(',');
    logFile.print(Tmp);
    logFile.println();
    logFile.flush(); // make sure the file contains at least this much

    return 1; // Return success
  }

  return 0; // If we failed to open the file, return fail
}

// updateFileName() - Looks through the log files already present on a card,
// and creates a new file with an incremented file index.
void updateFileName()
{
  for (uint8_t i; i < MAX_LOG_FILES; i++)
  {
    // Set logFileName to "gpslogXX.csv":
    logFileName[6] = (i/10) + '0';
    logFileName[7] = (i%10) + '0';

    if (!SD.exists(logFileName))
      break; // We found our index

    SerialMonitor.print(logFileName);
    SerialMonitor.println( F(" exists") );
  }
  SerialMonitor.print( F("File name: ") );
  SerialMonitor.println(logFileName);
}

Your original sketch used 24114 bytes of program space and 1690 bytes of RAM. The NeoGPS version uses 21198 bytes of program space and 1400 bytes of RAM, a significant savings.

Minor points:

  • It uses the F macro to save RAM
  • It opens the log file in setup and flushes after every GPS update. You should investigate when to close the log file.
  • It reads IMU samples as fast as possible, but you should switch this to the FIFO approach
  • It doesn't use sprintf to compose the log filename (this saves program space)
  • The column header does not need to be an array of character strings. It can be one #define. The F macro is applied when it is printed.
  • It only prints GPS fields if they have a valid value.
  • Comments in the GPS port section were wrong about using SoftwareSerial on a Mega.
  • Obvious comments deleted. The else of an if statement does not deserve an // otherwise comment. -_-

A chat system I used to use "back in the day" used a fixed "stack" based string buffer.

Basically a single char * buffer of a fixed size was created at the beginning of the program and initialised to 0.

Then strings were appended to that buffer using whatever functions were appropriate at that time. A pointer was kept pointing to the start of the "free" space in the buffer. This is the pointer which was used for placing new text into that buffer.

The entire buffer was then printed to the output stream in one block, the first byte cleared, and the pointer reset to the start of the buffer.

This made a very efficient and simple string buffer which didn't fragment memory in any way and still allowed the use of standard C string functions such as sprintf(), strcpy(), etc.

For example:

char strbuf[128] = { 0 };
char *strptr = strbuf;

strcpy_P(strptr, (PGM_P)F("Temp = "));
strptr += strlen(strptr);
itoa(temperature, strptr, 10);
strptr += strlen(strptr);

Serial.println(strbuf);
strbuf[0] = 0;
strptr = strbuf;