6
  1. Why “Strings” are bad for Arduino?
  2. Which is the most efficient and fastest solution to read and store the data from Accelerometer and GPS?

Stings are evil for Arduino


An Uno or other ATmega328-based board only has 2048 bytes SRAM.

The SRAM is composed by three parts:

  1. The Static
  2. The Heap
  3. The Stack

The Heap is where, randomly, a C++ string information is allocated.

If I add an information to my C++ String the system copies itself and deletes the old copy leaving a hole in the heap, that grows up with the cycles.

When I wrote my first sketch for reading and writing the information from an MPU6050 and stored on a SD (whithout connecting to a GPS) I noticed that the sampling time was not constant.

enter image description here

Thanks to the feedback obtained in this question on StackOverflow, I understood that the problem was my excessive use of strings, and now i understand why.

So I started to dig into this issue.

This was one of the first version of the code:

/******************************************************************************
  CSV_Logger_TinyGPSPlus.ino
  Log GPS data to a CSV file on a uSD card
  By Jim Lindblom @ SparkFun Electronics
  February 9, 2016
  https://github.com/sparkfun/GPS_Shield

  This example uses SoftwareSerial to communicate with the GPS module on
  pins 8 and 9, then communicates over SPI to log that data to a uSD card.

  It uses the TinyGPS++ library to parse the NMEA strings sent by the GPS module,
  and prints interesting GPS information - comma separated - to a newly created
  file on the SD card.

  Resources:
  TinyGPS++ Library  - https://github.com/mikalhart/TinyGPSPlus/releases
  SD Library (Built-in)
  SoftwareSerial Library (Built-in)

  Development/hardware environment specifics:
  Arduino IDE 1.6.7
  GPS Logger Shield v2.0 - Make sure the UART switch is set to SW-UART
  Arduino Uno, RedBoard, Pro, Mega, etc.
******************************************************************************/

#include <SPI.h>
#include <SD.h>
#include <TinyGPS++.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.
// Our log files are called "gpslogXX.csv, so "gpslog99.csv" is our max file.
#define LOG_FILE_PREFIX "gpslog" // Name of the log file.
#define MAX_LOG_FILES 100 // Number of log files that can be made
#define LOG_FILE_SUFFIX "csv" // Suffix of the log file
char logFileName[13]; // Char string to store the log file name
// Data to be logged:
#define LOG_COLUMN_COUNT 15
char * log_col_names[LOG_COLUMN_COUNT] = {
  "longitude", "latitude", "altitude", "speed", "course", "date", "time", "satellites","Acc.X","Acc.Y","Acc.Z","Gy.X","Gy.Y","Gy.Z","Temp"
}; // log_col_names is printed at the top of the file.

//////////////////////
// Log Rate Control //
//////////////////////
#define LOG_RATE 1000 // Log every 1 seconds
unsigned long lastLog = 0; // Global var to keep of last time we logged

/////////////////////////
// TinyGPS Definitions //
/////////////////////////
TinyGPSPlus tinyGPS; // tinyGPSPlus object to be used throughout
#define GPS_BAUD 9600 // GPS module's default baud rate

/////////////////////////////////
// GPS Serial Port Definitions //
/////////////////////////////////
// If you're using an Arduino Uno, Mega, RedBoard, or any board that uses the
// 0/1 UART for programming/Serial monitor-ing, use SoftwareSerial:
#include <SoftwareSerial.h>
#define ARDUINO_GPS_RX 9 // GPS TX, Arduino RX pin
#define ARDUINO_GPS_TX 8 // GPS RX, Arduino TX pin
SoftwareSerial ssGPS(ARDUINO_GPS_TX, ARDUINO_GPS_RX); // Create a SoftwareSerial

// Set gpsPort to either ssGPS if using SoftwareSerial or Serial1 if using an
// Arduino with a dedicated hardware serial port
#define gpsPort ssGPS  // Alternatively, use Serial1 on the Leonardo

// Define the serial monitor port. On the Uno, Mega, and Leonardo this is 'Serial'
//  on other boards this may be 'SerialUSB'
#define SerialMonitor Serial
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("Setting up SD card.");
  // see if the card is present and can be initialized:
  if (!SD.begin(ARDUINO_USD_CS))
  {
    SerialMonitor.println("Error initializing SD card.");
  }
  updateFileName(); // Each time we start, create a new file, increment the number
  printHeader(); // Print a header at the top of the new file
}

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)
{
  if ((lastLog + LOG_RATE) <= millis())
  { // If it's been LOG_RATE milliseconds since the last log:
    if (tinyGPS.location.isUpdated()) // If the GPS data is vaild
    {
      if (logGPSData()) // Log the GPS data
      {
        SerialMonitor.println("GPS logged."); // Print a debug message
        lastLog = millis(); // Update the lastLog variable
      }
      else // If we failed to log GPS
      { // Print an error, don't update lastLog
        SerialMonitor.println("Failed to log new GPS data.");
      }
    }
    else // If GPS data isn't valid
    {
      // Print a debug message. Maybe we don't have enough satellites yet.
      SerialMonitor.print("No GPS data. Sats: ");
      SerialMonitor.println(tinyGPS.satellites.value());
    }
  }

  // If we're not logging, continue to "feed" the tinyGPS object:
  while (gpsPort.available())
    tinyGPS.encode(gpsPort.read());
}
}


byte logGPSData()
{
  File logFile = SD.open(logFileName, FILE_WRITE); // Open the log file

  if (logFile)
  { // Print longitude, latitude, altitude (in feet), speed (in mph), course
    // in (degrees), date, time, and number of satellites.
    logFile.print(tinyGPS.location.lng(), 6);
    logFile.print(',');
    logFile.print(tinyGPS.location.lat(), 6);
    logFile.print(',');
    logFile.print(tinyGPS.altitude.feet(), 1);
    logFile.print(',');
    logFile.print(tinyGPS.speed.mph(), 1);
    logFile.print(',');
    logFile.print(tinyGPS.course.deg(), 1);
    logFile.print(',');
    logFile.print(tinyGPS.date.value());
    logFile.print(',');
    logFile.print(tinyGPS.time.value());
    logFile.print(',');
    logFile.print(tinyGPS.satellites.value());
    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.close();

    return 1; // Return success
  }

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

// printHeader() - prints our eight column names to the top of our log file
void printHeader()
{
  File logFile = SD.open(logFileName, FILE_WRITE); // Open the log file

  if (logFile) // If the log file opened, print our column names to the file
  {
    int i = 0;
    for (; i < LOG_COLUMN_COUNT; i++)
    {
      logFile.print(log_col_names[i]);
      if (i < LOG_COLUMN_COUNT - 1) // If it's anything but the last column
        logFile.print(','); // print a comma
      else // If it's the last column
        logFile.println(); // print a new line
    }
    logFile.close(); // close the file
  }
}

// updateFileName() - Looks through the log files already present on a card,
// and creates a new file with an incremented file index.
void updateFileName()
{
  int i = 0;
  for (; i < MAX_LOG_FILES; i++)
  {
    memset(logFileName, 0, strlen(logFileName)); // Clear logFileName string
    // Set logFileName to "gpslogXX.csv":
    sprintf(logFileName, "%s%d.%s", LOG_FILE_PREFIX, i, LOG_FILE_SUFFIX);
    if (!SD.exists(logFileName)) // If a file doesn't exist
    {
      break; // Break out of this loop. We found our index
    }
    else // Otherwise:
    {
      SerialMonitor.print(logFileName);
      SerialMonitor.println(" exists"); // Print a debug statement
    }
  }
  SerialMonitor.print("File name: ");
  SerialMonitor.println(logFileName); // Debug print the file name
}

Then I started:

  1. Studying how Arduino Memory works
  2. Breaking the problem, focusing only to learn how to read and write on a SD the accelerometer information (the device is not connected to the PC)

The following code is my latest version:

 void Read_Write()
 // function that reads the MPU and writes the data to the SD.
  {
   // Local variables:
int16_t AcX,AcY,AcZ,Tmp,GyX,GyY,GyZ; // Variables read from MPU6050

// Read data:
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)

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)




// Data preparation for file saving:
String dataString = ""; // string for assembling the data to log:

// Add time tag:
dataString += String(Time0); dataString += ",";

// Append the MPU6050 data to the string:
dataString += String(AcX); dataString += ",";
dataString += String(AcY); dataString += ",";
dataString += String(AcZ); dataString += ",";
dataString += String(GyX); dataString += ",";
dataString += String(GyY); dataString += ",";
dataString += String(GyZ);

// Open the file in append mode:
File dataFile = SD.open("datalog.txt", FILE_WRITE);

// If the file is available, write to it:
if (dataFile) 
{
dataFile.println(dataString);
dataFile.close();
if (Serial_plus_SD)
  Serial.println(dataString);
}
// if the file does not open, pop up an error:
else 
errorFW();

return;
}

I recived other suggestions such as "create a Buffer", but without a technical description and motivation I am having difficulties understand how to do it.

(I don’t want to simply copy&paste the code as i have done intitially with the first version)

I have to use C Strings instead of C++ Strings?

I have a civil engineer background, so is my first time with coding.

Thank you for your patience.

7 Answers 7

4

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);
5
  • This is actually a good answer. Mar 20, 2017 at 13:44
  • @Edgar why you use this Print &printer, int16_t Time0, [...] (or where i can find more (specific) information about the theory behind this) ? Mar 23, 2017 at 17:07
  • Please be more specific, and maybe ask a new question. As I understand it, you are asking for a tutorial on the “theory” behind the C++ language, which is a question far too broad for this site. Mar 23, 2017 at 18:29
  • @EdgarBonet yes, it is a noob question, i was trying to understand and studying the pointer, in this specific situation how &printer works :) Mar 24, 2017 at 17:08
  • @uomodellamansarda: It's a reference, similar to a pointer but safer. Mar 24, 2017 at 17:59
4

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. -_-
3
  • @slash-dev i am tryng to go deep, and understanding the entire code, sorry for my noob questions. 1) Why in your code you use AltSoftSerial and SoftwareSerial.h ? 2) Why did you create two objects "gps" and "Fix" i also read the documentation on github Thanks for your time! May 8, 2017 at 15:23
  • @uomodellamansarda, read point 2 in my answer. SoftwareSerial is very inefficient, so you should use something else. Read more about it here. The gps object is responsible for reading chars from gpsPort and creating fix structures for you to read (just like TinyGPS++). The fix object holds on to all the GPS values that were parsed from the gpsPort. Calling gps.read() fills out the fix object (it's a local copy for you to use anywhere in your sketch). You can use various pieces of that object, like fix.latitude().
    – slash-dev
    May 8, 2017 at 16:03
  • @slash-dev my noob mistake, thanks for sharing the link! :) May 9, 2017 at 7:53
3

I would use sprintf, with a statically allocated buffer.

E.g.

substitute

// Data preparation for file saving:
String dataString = ""; // string for assembling the data to log:

// Add time tag:
dataString += String(Time0); dataString += ",";

// Append the MPU6050 data to the string:
dataString += String(AcX); dataString += ",";
dataString += String(AcY); dataString += ",";
dataString += String(AcZ); dataString += ",";
dataString += String(GyX); dataString += ",";
dataString += String(GyY); dataString += ",";
dataString += String(GyZ);

with

static char pippo[PIPPO_SIZE];
int ret = sprintf(pippo, "%lu,%d,%d,%d,%d,%d,%d",
                         (uint32_t) Time0, AcX, AcY, AcZ, GyX, GyY, GyZ);

// This is just for debugging purposes to test the code
if (ret < 0) {
    // failure: no byte written
} else if (ret >= PIPPO_SIZE) {
    // failure: too many bytes written, I did some mistake :(
} else {
    // ok
}

The size of the buffer pippo is computed as follows:

  • 1 byte for each comma, you have 6
  • 6 bytes for each int16_t variable (5 bytes for the digits plus 1 byte for the sign, which however you might not need)
  • 10 bytes for Time0, which I assume is of type unsigned long which in Arduino is 32 bits.
  • 1 byte for closing \0

So it should be 1 * 6 + 6 * 6 + 10 + 1 = 53 unless I did some mistake. Add the following at the top of your program:

#define PIPPO_SIZE 53

The motivation for using statically allocated memory is simple, and is already stated in your question: the concatenation of String objects results in repeated calls to new and delete with subsequent allocation and de-allocation of dynamic memory, which gets fragmented. More over, the String-based approach is intuitively slower because it requires many more steps to accomplish basically the same simple task.

String aren't bad per se, but in your application you want to squeeze every last fragment of performance that you can, so there's that.

3

There is now as SafeString library available for Arduino, via the Library Manager (mine :-) )
SafeStrings are GOOD for Arduino
They are only ever placed in Static or Stack memory and so completely avoid heap fragementation and small transient object construction and unnecessary data coping, NEVER causes your sketch to reboot and has extensive debugging and error messages.

SafeStrings are designed to be uses by Beginners and includes extensive error checking and detailed error messages to help you find the errors and correct your code. SafeStrings also corrects a number of errors that are in the String code.

There is a detailed tutorial at
https://www.forward.com.au/pfod/ArduinoProgramming/SafeString/index.html
and extensive examples include in the library, including parsing/tokenizing

In this case because SafeString implement almost all of String functionallity it is easy to replace EVIL Strings with GOOD SafeStrings

    #include "SafeString.h"
    createSafeString(dataString,60); // allocate global 60 byte safestring, just once,
//  for assembling the data to log 
// You could also use createSafeString within the method if you want a local SafeString
    .. .. .. 
    void setup() {
    Serial.begin(9600);
    // ....
    SafeString::setOutput(Serial); // enable sending debug/error msgs to Serial
    // So you will get a message if dataString runs out of space (but no reboots)
    // ....
    }
    
    // Data preparation for file saving:
    
    // Add time tag:
    dataString = Time0; dataString += ","; // or ',';
    
    // Append the MPU6050 data to the string:
    // no new strings created here!!!
    dataString += AcX; dataString += ","; 
    dataString += AcY; dataString += ","; 
    dataString += AcZ; dataString += ","; 
    dataString += GyX; dataString += ","; 
    dataString += GyY; dataString += ","; 
    dataString += GyZ;
    
    // etc ....
    
    // SafeStrings are printable so can use println(dataString)
    // you can also print TO a SafeString so you could use, for example
    // dataString.print(AcX); 
    // to access all the formatting print provides
1
  • Seems really Interesting! Thanks for sharing :) Jun 23, 2020 at 10:43
2

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;
2

For fixed strings (which do not change), use e.g. F("Text") ... this will place the string in flash instead of heap space. Note that the Uno has 32 KB of flash and 2 KB of SRAM (which is used for heap space among others).

If you need variable sized strings but one or few at a time, make a dedicated buffer (of e.g. 64 bytes) and use that buffer for all string operations. It will take up 64 bytes all time, but will leave no heap gaps. You can create it as a global variable (simple):

char string_buffer[64];

and use that string_buffer for string operations.

Note that heap gaps are bad for all devices/operating systems, however since the Arduino has very little builtin SRAM, it is filledup quickly when using strings without thinking about memory management.

1

Which is the most efficient and fastest solution to read and store the data from Accelerometer and GPS?

You don't state how fast is fast enough nore how long you want to record the data for. Particularly for the Accelerometer data.

This suggested solution will log ~800 Accelerometer samples/sec (7 x 16bit ints) continually for 30 days.

There are two problems i) reading and parsing the GPS and reading the Accelerometer ii) storing the data on the SD card.

Reading/parsing the GPS data takes time. This tutorial, Arduino Serial I/O for the Real World covers buffering serial input, parsing GPS data and buffering serial (text/debug) output. Reading the accelerometer data is much faster, leave it in binary form.

Storing data on the SD card can be very slow. Bill Greiman's AnalogBinLogger example in an earlier version of his SdFat Arduino library, uses a pre-allocated and erased SD file and special block writes to minimize the SD latency. https://github.com/greiman/SdFat (code for the earlier version with AnalogBinLogger)

This project, High Frequency, Long Duration Datalogging, uses an Uno/Mega to store 512byte blocks of data to an SD card in less then 1mS each block, continually for 30days. It uses an interrupt to collect the ADC data and two 512byte buffers it for collecting the incoming data and writing to the SD card.

Applying these two projects to your situation you could

  1. use on Mega2560 to read/buffer/filter/parse the GPS data and interleave it with the Accelerometer data and buffer it for binary transfer to the second Mega2650 via Serial (115200baud) i.e. ~11.5Kbytes/sec. For 7 x int16_t accelerometer variables that would be about 800 samples/sec over the serial connection in binary format. The standard Mega2560 64byte TX Serial buffer should be sufficient.

  2. In the second Mega265 you need to modify the UART interrupt handler for one of the UARTs to fill the current SD buffer instead of the normal receive buffer. When the buffer is full the interrupt swaps it for a second buffer as in the high speed logger project above.

The second Mega would also have a command and control Serial input to prepare the SD card and start/stop the storage.

A single board apporach

Since the SD write of the 512 block takes <1mS you could also look a combining these two processes into on board.

Read/buffer/parse fill the 512 block. When it is full, block for 1ms while the SD writes that buffer. This approach will reduce accelerometer sampling frequency. See Simple Multitasking Arduino for how to interleave the tasks for reading/parsing the GPS with sampling the accelerometer and writing the SD card

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.