Понимание того, почему следует избегать «String» и альтернативных решений

  1. Почему «строки» вредны для Arduino?
  2. Какое решение является наиболее эффективным и быстрым для чтения и хранения данных с акселерометра и GPS?

Жала — зло для Arduino


У Uno или другой платы на базе ATmega328 только 2048 байт SRAM.

SRAM состоит из трех частей:

  1. Статика
  2. Куча
  3. Стек

Куча — это место, где случайным образом размещается строковая информация C++.

Если я добавлю информацию в свою строку C++, система скопирует себя и удалит старую копию, оставив дыру в куче, которая увеличивается с циклами.

Когда я написал свой первый скетч для чтения и записи информации с MPU6050 и сохранения на SD (без подключения к GPS), я заметил, что время выборки не было постоянным.

Благодаря отзывам, полученным в этот вопрос на StackOverflow, я понял, что проблема заключалась в чрезмерном использовании строк, и теперь я понимаю почему.

Поэтому я начал копаться в этом вопросе.

Это была одна из первых версий кода:

/*************************************************** ****************************************
CSV_Logger_TinyGPSPlus.ino
Записывать данные GPS в файл CSV на карту памяти USB.
Джим Линдблом @ SparkFun Electronics
9 февраля 2016 г.
https://github.com/sparkfun/GPS_Shield

В этом примере используется SoftwareSerial для связи с модулем GPS на
контакты 8 и 9, а затем обмениваются данными через SPI, чтобы записать эти данные на карту USB.

Он использует библиотеку TinyGPS++ для анализа строк NMEA, отправляемых модулем GPS.
и печатает интересную информацию GPS (через запятую) во вновь созданный
файл на SD-карте.

Ресурсы:
Библиотека TinyGPS++ — https://github.com/mikalhart/TinyGPSPlus/releases
SD-библиотека (встроенная)
Программное обеспечениеSerial Library (встроенная)

Особенности разработки/аппаратной среды:
Arduino IDE 1.6.7
GPS Logger Shield v2.0 — убедитесь, что переключатель UART установлен в положение SW-UART.
Arduino Uno, RedBoard, Pro, Mega и т. д.
******************************************************* ****************************/

#include <SPI.h>
#include <SD.h>
#include <TinyGPS++.h>
#include<Wire.h>
#define ARDUINO_USD_CS 10 // контакт CS карты USB (контакт 10 на SparkFun GPS Logger Shield)

//////////////////////////
// Определения файла журнала //
//////////////////////////
// Имейте в виду, что библиотека SD имеет максимальную длину имени файла от 8,3 до 8 символов префикса,
// и 3-символьный суффикс.
// Наши файлы журнала называются "gpslogXX.csv", поэтому "gpslog99.csv" является нашим максимальным файлом.
#define LOG_FILE_PREFIX "gpslog" // Имя файла журнала.
#define MAX_LOG_FILES 100 // Количество файлов журнала, которые можно создать
#define LOG_FILE_SUFFIX "csv" // Суффикс файла журнала
char logFileName[13]; // Строка Char для хранения имени файла журнала
// Данные для регистрации:
#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 печатается вверху файла.

///////////////////////
// Контроль скорости логирования //
///////////////////////
#define LOG_RATE 1000 // Записывать каждую 1 секунду
unsigned long lastLog = 0; // Глобальная переменная для хранения последней регистрации

//////////////////////////
// Определения TinyGPS //
//////////////////////////
TinyGPSPlus tinyGPS; // объект tinyGPSPlus, который будет использоваться повсюду
#define GPS_BAUD 9600 // Скорость передачи модуля GPS по умолчанию

//////////////////////////////////
// Определения последовательного порта GPS //
//////////////////////////////////
// Если вы используете Arduino Uno, Mega, RedBoard или любую другую плату, использующую
// 0/1 UART для программирования/мониторинга последовательного порта, используйте SoftwareSerial:
#include <SoftwareSerial.h>
#define ARDUINO_GPS_RX 9 // GPS TX, вывод Arduino RX
#define ARDUINO_GPS_TX 8 // GPS RX, вывод Arduino TX
SoftwareSerial ssGPS(ARDUINO_GPS_TX, ARDUINO_GPS_RX); // Создаем SoftwareSerial

// Установите для gpsPort значение ssGPS при использовании SoftwareSerial или Serial1 при использовании
// Arduino с выделенным аппаратным последовательным портом
#define gpsPort ssGPS  // В качестве альтернативы используйте Serial1 на Leonardo

// Определяем последовательный порт монитора. На Uno, Mega и Leonardo это «Serial».
// на других платах это может быть 'SerialUSB'
#define SerialMonitor Serial
const int MPU=0x68;  // I2C-адрес MPU-6050
int16_t AcX,AcY,AcZ,Tmp,GyX,GyY,GyZ;



void setup()
{
   Wire.begin();
  Wire.beginTransmission(MPU);
  Wire.write(0x6B);  // регистр PWR_MGMT_1
  Wire.write(0);     // установить в ноль (пробуждает MPU-6050)
  Wire.endTransmission(true);
  SerialMonitor.begin(9600);
  gpsPort.begin(GPS_BAUD);

  SerialMonitor.println("Setting up SD card.");
  // смотрим, присутствует ли карта и может ли она быть инициализирована:
  if (!SD.begin(ARDUINO_USD_CS))
  {
    SerialMonitor.println("Error initializing SD card.");
  }
  updateFileName(); // Каждый раз, когда мы начинаем, создаем новый файл, увеличивая номер
  printHeader(); // Печатаем заголовок вверху нового файла
}

void loop()
{
 Wire.beginTransmission(MPU);
  Wire.write(0x3B);  // начиная с регистра 0x3B (ACCEL_XOUT_H)
  Wire.endTransmission(false);
  Wire.requestFrom(MPU,14,true);  // запрашиваем всего 14 регистров
  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())
  { // Если с момента последней регистрации прошло LOG_RATE миллисекунд:
    if (tinyGPS.location.isUpdated()) // Если данные GPS действительны
    {
      if (logGPSData()) // Зарегистрировать данные GPS
      {
        SerialMonitor.println("GPS logged."); // Печатаем отладочное сообщение
        lastLog = millis(); // Обновить переменную lastLog
      }
      else // Если нам не удалось зарегистрировать GPS
      { // Вывести ошибку, не обновлять lastLog
        SerialMonitor.println("Failed to log new GPS data.");
      }
    }
    else // Если данные GPS недействительны
    {
      // Печатаем отладочное сообщение. Может быть, у нас еще не хватает спутников.
      SerialMonitor.print("No GPS data. Sats: ");
      SerialMonitor.println(tinyGPS.satellites.value());
    }
  }

  // Если мы не логируем, продолжаем "кормить" объект tinyGPS:
  while (gpsPort.available())
    tinyGPS.encode(gpsPort.read());
}
}


byte logGPSData()
{
  File logFile = SD.open(logFileName, FILE_WRITE); // Открываем лог-файл

  if (logFile)
  { // Выводим долготу, широту, высоту (в футах), скорость (в милях в час), курс
    // in (градусы), дата, время и количество спутников.
    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 0; // Если нам не удалось открыть файл, возвращаем ошибку
}

// printHeader() - печатает имена наших восьми столбцов в начало файла журнала
void printHeader()
{
  File logFile = SD.open(logFileName, FILE_WRITE); // Открываем лог-файл

  if (logFile) // Если файл журнала открыт, выводим имена столбцов в файл
  {
    int i = 0;
    for (; i < LOG_COLUMN_COUNT; i++)
    {
      logFile.print(log_col_names[i]);
      if (i < LOG_COLUMN_COUNT - 1) // Если это что угодно, кроме последнего столбца
        logFile.print(','); // печатаем запятую
      else // Если это последний столбец
        logFile.println(); // печатаем новую строку
    }
    logFile.close(); // закрыть файл
  }
}

// updateFileName() - Просматривает файлы журнала, уже имеющиеся на карте,
// и создает новый файл с увеличенным файловым индексом.
void updateFileName()
{
  int i = 0;
  for (; i < MAX_LOG_FILES; i++)
  {
    memset(logFileName, 0, strlen(logFileName)); // Очистить строку logFileName
    // Установить для logFileName значение "gpslogXX.csv":
    sprintf(logFileName, "%s%d.%s", LOG_FILE_PREFIX, i, LOG_FILE_SUFFIX);
    if (!SD.exists(logFileName)) // Если файл не существует
    {
      break; // Выход из этого цикла. Мы нашли наш индекс
    }
    else // Иначе:
    {
      SerialMonitor.print(logFileName);
      SerialMonitor.println(" exists"); // Печатаем оператор отладки
    }
  }
  SerialMonitor.print("File name: ");
  SerialMonitor.println(logFileName); // Отладка напечатать имя файла
}

Затем я начал:

  1. Изучение работы памяти Arduino
  2. Решение проблемы, сосредоточившись только на том, чтобы научиться читать и записывать на SD информацию об акселерометре (устройство не подключено к ПК)

Следующий код является моей последней версией:

 void Read_Write()
 // функция, которая читает MPU и записывает данные на SD.
  {
   // Локальные переменные:
int16_t AcX,AcY,AcZ,Tmp,GyX,GyY,GyZ; // Переменные считываются из MPU6050

// Чтение данных:
Wire.beginTransmission(MPU);
Wire.write(0x3B);  // начиная с регистра 0x3B (ACCEL_XOUT_H)
Wire.endTransmission(false);
Wire.requestFrom(MPU,14,true);  // запрашиваем всего 14 регистров
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)




// Подготовка данных для сохранения файла:
String dataString = ""; // строка для сборки данных в лог:

// Добавляем метку времени:
dataString += String(Time0); dataString += ",";

// Добавляем данные MPU6050 в строку:
dataString += String(AcX); dataString += ",";
dataString += String(AcY); dataString += ",";
dataString += String(AcZ); dataString += ",";
dataString += String(GyX); dataString += ",";
dataString += String(GyY); dataString += ",";
dataString += String(GyZ);

// Открываем файл в режиме добавления:
File dataFile = SD.open("datalog.txt", FILE_WRITE);

// Если файл доступен, пишем в него:
if (dataFile) 
{
dataFile.println(dataString);
dataFile.close();
if (Serial_plus_SD)
  Serial.println(dataString);
}
// если файл не открывается, выскакивает ошибка:
else 
errorFW();

return;
}

Я получил другие предложения, такие как "создать буфер", но без технического описания и мотивации мне трудно понять, как это сделать.

(Я не хочу просто копировать и вставлять код, как я сделал изначально с первой версией)

Я должен использовать строки C вместо строк C++?

У меня есть опыт работы инженером-строителем, поэтому я впервые занимаюсь программированием.

Спасибо за терпение.

, 👍6


7 ответов


Лучший ответ:

4

Во-первых, позвольте мне указать на проблему в вашем коде, совершенно не связанную с ваш вопрос:

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

Стандарт C++ не указывает, в каком порядке будут выполняться два чтения. выполненный. Это может хорошо работать с конкретной версией определенный компилятор, который вы используете, но он может испортить вам день (или обновление до Arduino IDE) изменяет версию компилятора или компилятор флаг. Вместо этого вы должны выполнить одно чтение для каждого оператора, например:

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

Далее я хотел бы сказать, что простейший способ Arduino сделать то, что вы пытаетесь использовать ни String, ни массив символов. Вместо этого сначала откройте файл, затем отформатируйте и отправьте данные на лету. используя print() или println():

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

Таким образом, вам никогда не придется хранить всю строку в памяти. Это тогда очень удобное решение для памяти.

Теперь я понимаю, что это кажется неудобным, если вы хотите распечатать данные в как файл SD, так и последовательный порт. В таком случае можно поставить вывод кода внутри функции, которая может печатать в любой объект Print (например, файл данных и Serial), а затем вы можете вызвать функция дважды:

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

В качестве альтернативы вы можете использовать своего рода тройник для дублирования данных в оба потока. См., например, эту реализацию.

Изменить. Более элегантным способом добиться того же было бы обернуть все данные IMU в struct (или class), а затем для печати:

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

Вы можете напечатать struct так же, как и строку:

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

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

На самом деле это хороший ответ., @Patrick Trentin

@ Эдгар, почему вы используете этот Print &printer, int16_t Time0, [...] (или где я могу найти более (конкретную) информацию о теории, лежащей в основе этого)?, @Andrea Ciufo

Пожалуйста, будьте более конкретными и, возможно, задайте новый вопрос. Насколько я понимаю, вы просите руководство по «теории» языка C++, а это слишком широкий вопрос для этого сайта., @Edgar Bonet

@EdgarBonet да, это нубский вопрос, я пытался понять и изучить указатель, в этой конкретной ситуации, как работает & принтер :), @Andrea Ciufo

@uomodellamansarda: это [ссылка] (https://www.tutorialspoint.com/cplusplus/cpp_references.htm), похожая на указатель, но более безопасная., @Edgar Bonet


2

Для фиксированных строк (которые не изменяются) используйте, например, F("Text")... это поместит строку во флэш-память, а не в кучу. Обратите внимание, что Uno имеет 32 КБ флэш-памяти и 2 КБ SRAM (которая, помимо прочего, используется для кучи).

Если вам нужны строки переменного размера, но одна или несколько одновременно, создайте специальный буфер (например, 64 байта) и используйте этот буфер для всех операций со строками. Он все время будет занимать 64 байта, но не оставит пробелов в куче. Вы можете создать его как глобальную переменную (просто):

char string_buffer[64];

и использовать этот string_buffer для строковых операций.

Обратите внимание, что пробелы в куче вредны для всех устройств/операционных систем, однако, поскольку в Arduino очень мало встроенной SRAM, она быстро заполняется при использовании строк без учета управления памятью.

,

2

Система чата, которую я использовал «в прошлом», использовала фиксированный буфер строк на основе «стека».

В основном в начале программы создается один буфер char * фиксированного размера, который инициализируется значением 0.

Затем к этому буферу добавлялись строки с использованием подходящих на тот момент функций. Указатель сохранялся, указывающий на начало «свободного» пространства в буфере. Это указатель, который использовался для помещения нового текста в этот буфер.

Затем весь буфер печатался в выходной поток одним блоком, первый байт очищался, а указатель сбрасывался на начало буфера.

Это сделало очень эффективный и простой строковый буфер, который никоим образом не фрагментировал память и по-прежнему позволял использовать стандартные строковые функции C, такие как sprintf(), strcpy() и т. д.

Например:

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

3

Я бы использовал sprintf со статически выделенным буфером.

Например

заменить

// Подготовка данных для сохранения файла:
String dataString = ""; // строка для сборки данных в лог:

// Добавляем метку времени:
dataString += String(Time0); dataString += ",";

// Добавляем данные MPU6050 в строку:
dataString += String(AcX); dataString += ",";
dataString += String(AcY); dataString += ",";
dataString += String(AcZ); dataString += ",";
dataString += String(GyX); dataString += ",";
dataString += String(GyY); dataString += ",";
dataString += String(GyZ);

с

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

// Это просто в целях отладки для проверки кода
if (ret < 0) {
    // ошибка: байт не записан
} else if (ret >= PIPPO_SIZE) {
    // сбой: записано слишком много байт, я ошибся :(
} else {
    // Ok
}

Размер буфера pippo вычисляется следующим образом:

  • 1 байт на каждую запятую, у вас 6
  • 6 байтов для каждой переменной int16_t (5 байтов для цифр плюс 1 байт для знака, который, однако, может вам не понадобиться)
  • 10 байтов для Time0, который, как я предполагаю, имеет тип unsigned long, который в Arduino составляет 32 бита. .
  • 1 байт для закрытия \0

Так что должно быть 1 * 6 + 6 * 6 + 10 + 1 = 53, если только я не ошибся. Добавьте следующее в начало вашей программы:

#define PIPPO_SIZE 53

Мотивация использования статически выделенной памяти проста и уже указана в вашем вопросе: объединение объектов String приводит к повторным вызовам new и удалить с последующим выделением и освобождением динамической памяти, которая фрагментируется. Более того, подход, основанный на String, интуитивно медленнее, потому что для выполнения одной и той же простой задачи требуется гораздо больше шагов.

String неплохо сама по себе, но в вашем приложении вы хотите выжимать из производительности все до последнего фрагмента, вот и все.

,

4

Эдгар очень хорошо отвечает на ваш первый вопрос о String и печати. У Маженко также есть хорошее описание здесь ловушек.

Относительно вашего второго вопроса о GPS/эффективности/скорости:

  1. использовать NeoGPS,
  2. используйте AltSoftSerial,
  3. использовать MPU FIFO,
  4. используйте последнюю версию SdFat,
  5. остерегайтесь задержек записи SD и
  6. закрыть файл журнала в какой-то момент.


1. NeoGPS – это самый маленький, быстрый и точный из доступных парсеров GPS. Примеры программ правильно структурированы, и есть пример для ведения журнала SD.

В отличие от других библиотек, в NeoGPS есть FIFO с исправлениями. Это позволяет избежать «выборки» GPS с некоторой частотой, определяемой millis(). Кристалл Arduino не так точен, как атомные часы GPS, поэтому ваша скорость регистрации должна основываться на появлении доступных исправлений, ровно одно в секунду. Ваш набросок будет «дрейфовать» относительно обновлений GPS, периодически теряя фиксацию (в зависимости от точности кристалла).

2. Возможно, самое большое улучшение, которого вы могли бы добиться, это НЕ использовать SoftwareSerial. Это очень неэффективно, потому что отключает прерывания на длительные периоды времени. На 9600 он отключает прерывания на 1 мс, пока принимается каждый символ от GPS-устройства. Arduino мог бы выполнить ~10000 инструкций за это время, но вместо этого он вертит пальцами, ожидая исключительно поступления каждого бита байта... -_-

Вместо этого следует использовать AltSoftSerial, так как вы подключили устройство GPS к нужным контактам 8 & 9. Если бы вы использовали два других контакта, вы могли бы использовать мой NeoSWSerial. Обе эти библиотеки намного, намного эффективнее, чем SoftwareSerial.

3. Если вам нужны равномерно распределенные выборки MPU, переведите устройство в режим FIFO. Он будет записывать выбранные регистры в FIFO в выбранное время выборки. Это также дает вам некоторую гибкость при чтении образца: вам просто нужно прочитать его до того, как 1024-байтовый FIFO заполнится.

4. Вы используете старую версию библиотеки SD. Я настоятельно рекомендую вам использовать последнюю библиотеку SdFat. В нем много улучшений и исправлений ошибок.

5. Запись некоторых SD-карт иногда занимает больше времени. Время записи 47 мс не является чем-то необычным. На самом деле, 100 мс довольно распространены. Если за это время буфер MPU FIFO не переполнится, вы не потеряете образцы MPU.

Если вы обнаружите, что запись SD иногда приводит к переполнению FIFO, возможно, вам придется изменить библиотеку SDFat, чтобы она вызывала функцию yield во время ожидания завершения записи. Эта функция yield может в это время считывать данные из FIFO, сохраняя образцы в области ОЗУ Arduino.

6. Я бы порекомендовал вам закрывать файл журнала, когда происходит какое-либо событие. Может нажатие кнопки? Вы можете использовать светодиод, чтобы показать, что регистрация активна или неактивна. Вы также можете наблюдать за неактивностью в течение некоторого периода времени (нет скорости, нулевые значения MPU).


С учетом этих предложений, вот версия NeoGPS/AltSoftSerial/SdFat вашего исходного скетча (проверено GPS, непроверено MPU/SD, но компилируется):

#include <SPI.h>
#include <SdFat.h>
#include <NMEAGPS.h>
#include <Wire.h>
#define ARDUINO_USD_CS 10 // контакт CS карты USB (контакт 10 на SparkFun GPS Logger Shield)

//////////////////////////
// Определения файла журнала //
//////////////////////////
// Имейте в виду, что библиотека SD имеет максимальную длину имени файла от 8,3 до 8 символов префикса,
// и 3-символьный суффикс.
char logFileName[13] = "gpslogXX.csv";
// Наши файлы журнала называются "gpslogXX.csv", поэтому "gpslog99.csv" является нашим максимальным файлом.
#define MAX_LOG_FILES 100 // Количество файлов журнала, которые можно создать

// Данные для регистрации:
#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"
  // печатается в начале файла.

SdFat SD;
File  logFile;

//////////////////////////
// Определения NeoGPS //
//////////////////////////
NMEAGPS gps; // объект NeoGPS, который будет использоваться повсюду
gps_fix fix; // Последняя информация GPS, полученная от gpsPort
#define GPS_BAUD 9600 // Скорость передачи модуля GPS по умолчанию

//////////////////////////////////
// Определения последовательного порта GPS //
//////////////////////////////////
// Если вы используете Mega, Leo или Due, используйте Serial1 для GPS:
//#определить gpsPort Serial1

// Если вы используете Arduino Uno или другую плату ATmega328, которая использует
// 0/1 UART для программирования/мониторинга последовательного порта, используйте AltSoftSerial:
#include <AltSoftSerial.h>
AltSoftSerial gpsPort; // Всегда на контактах 8 & 9
//Если вы не можете использовать контакты 8 & 9, используйте это:
//#include <NeoSWSerial.h>
//NeoSWSerial gpsPort( 2, 3 ).

// Определяем последовательный порт монитора. На Uno, Mega и Leonardo это «Serial».
// на других платах это может быть 'SerialUSB'
#define SerialMonitor Serial

//#определить USE_MPU
const int MPU=0x68;  // I2C-адрес MPU-6050
int16_t AcX,AcY,AcZ,Tmp,GyX,GyY,GyZ;


void setup()
{
  Wire.begin();
  Wire.beginTransmission(MPU);
  Wire.write(0x6B);  // регистр PWR_MGMT_1
  Wire.write(0);     // установить в ноль (пробуждает MPU-6050)
  Wire.endTransmission(true);

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

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

  updateFileName(); // Каждый раз, когда мы начинаем, создаем новый файл, увеличивая номер
  // смотрим, присутствует ли карта и может ли она быть инициализирована:
  if (!SD.begin(ARDUINO_USD_CS))
  {
    SerialMonitor.println( F("Error initializing SD card.") );
  } else {
    logFile = SD.open( logFileName, FILE_WRITE );
    // Печатаем заголовок вверху нового файла
    logFile.println( F(LOG_COLUMN_HEADER) );
  }

}

void loop()
{
  Wire.beginTransmission(MPU);
  Wire.write(0x3B);  // начиная с регистра 0x3B (ACCEL_XOUT_H)
  Wire.endTransmission(false);
  Wire.requestFrom(MPU,14,true);  // запрашиваем всего 14 регистров
  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();  // получить всю структуру исправления, один раз в секунду

    if (logGPSData()) { // Регистрируем данные GPS
      SerialMonitor.println( F("GPS logged.") ); // Печатаем отладочное сообщение
    } else {// Если нам не удалось зарегистрировать GPS
      // Вывести ошибку, не обновлять lastLog
      SerialMonitor.println( F("Failed to log new GPS data.") );
    }
  }
}


byte logGPSData()
{
  if (logFile.isOpen())
  { // Выводим долготу, широту, высоту (в футах), скорость (в милях в час), курс
    // in (градусы), дата, время и количество спутников.

    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(); // убедитесь, что файл содержит как минимум столько

    return 1; // Возвращаем успех
  }

  return 0; // Если нам не удалось открыть файл, возвращаем ошибку
}

// updateFileName() - Просматривает файлы журнала, уже имеющиеся на карте,
// и создает новый файл с увеличенным файловым индексом.
void updateFileName()
{
  for (uint8_t i; i < MAX_LOG_FILES; i++)
  {
    // Установить для logFileName значение "gpslogXX.csv":
    logFileName[6] = (i/10) + '0';
    logFileName[7] = (i%10) + '0';

    if (!SD.exists(logFileName))
      break; // Мы нашли наш индекс

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

В исходном скетче использовалось 24 114 байт программного пространства и 1 690 байт ОЗУ. Версия NeoGPS использует 21198 байт программного пространства и 1400 байт ОЗУ, значительная экономия.

Незначительные моменты:

  • Макрос F используется для экономии оперативной памяти.
  • Он открывает файл журнала в setup и очищает его после каждого обновления GPS. Вам следует выяснить, когда следует закрыть файл журнала.
  • Он считывает образцы IMU максимально быстро, но вам следует переключиться на подход FIFO
  • Для создания имени файла журнала не используется sprintf (это экономит место для программы)
  • Заголовок столбца не обязательно должен быть массивом строк символов. Это может быть один #define. Макрос F применяется при печати.
  • Он печатает поля GPS, только если они имеют допустимое значение.
  • Комментарии в разделе порта GPS были неверными по поводу использования SoftwareSerial на Mega.
  • Очевидные комментарии удалены. else оператора if не заслуживает комментария // else. -_-
,

@ slash-dev Я пытаюсь углубиться и понять весь код, извините за мои нубские вопросы. 1) Почему в своем коде вы используете AltSoftSerial и SoftwareSerial.h ? 2) Почему вы создали два объекта "gps" и "Fix" я также прочитал документацию на github Спасибо за ваше время!, @Andrea Ciufo

@uomodellamansarda, прочитайте пункт 2 в моем ответе. SoftwareSerial очень неэффективен, поэтому вам следует использовать что-то другое. Подробнее об этом читайте здесь. Объект gps отвечает за чтение символов из gpsPort и создание структур исправления для чтения (так же, как TinyGPS++). Объект «fix» содержит все значения GPS, которые были проанализированы из «gpsPort». Вызов gps.read() заполняет объект fix (это локальная копия, которую вы можете использовать в любом месте вашего скетча). Вы можете использовать различные части этого объекта, например fix.latitude()., @slash-dev

@slash-dev моя нубская ошибка, спасибо, что поделились ссылкой! :), @Andrea Ciufo


3

Теперь для Arduino доступна библиотека SafeString через диспетчер библиотек (мой :-) )
SafeStrings ХОРОШИ для Arduino
Они всегда помещаются только в статическую или стековую память и, таким образом, полностью исключают фрагментацию кучи, создание небольших временных объектов и ненужное копирование данных, НИКОГДА не вызывают перезагрузку вашего скетча и содержат обширные сообщения об отладке и ошибках.

SafeStrings предназначены для начинающих и включают расширенную проверку ошибок и подробные сообщения об ошибках, которые помогут вам найти ошибки и исправить код. SafeStrings также исправляет ряд ошибок в коде String.

Есть подробное руководство по адресу
https://www.forward.com.au/pfod/ArduinoProgramming/SafeString/index.html
и обширные примеры включены в библиотеку, включая синтаксический анализ/токенизацию

В этом случае, поскольку SafeString реализует почти всю функциональность String, можно легко заменить EVIL Strings GOOD SafeString

    #include "SafeString.h"
    createSafeString(dataString,60); // выделить глобальную 60-байтовую строку безопасности только один раз,
// для сборки данных в лог
// Вы также можете использовать createSafeString внутри метода, если вам нужна локальная SafeString
    .. .. .. 
    void setup() {
    Serial.begin(9600);
    // ....
    SafeString::setOutput(Serial); // включить отправку сообщений об отладке/ошибке в Serial
    // Таким образом, вы получите сообщение, если в dataString закончится место (но без перезагрузки)
    // ....
    }
    
    // Подготовка данных для сохранения файла:
    
    // Добавляем метку времени:
    dataString = Time0; dataString += ","; // или же ',';
    
    // Добавляем данные MPU6050 в строку:
    // здесь не создаются новые строки!!!
    dataString += AcX; dataString += ","; 
    dataString += AcY; dataString += ","; 
    dataString += AcZ; dataString += ","; 
    dataString += GyX; dataString += ","; 
    dataString += GyY; dataString += ","; 
    dataString += GyZ;
    
    // так далее ....
    
    // SafeString можно распечатать, поэтому можно использовать println(dataString)
    // вы также можете напечатать в SafeString, чтобы вы могли использовать, например
    // dataString.print(AcX);
    // для доступа ко всем функциям форматирования, предоставляемым печатью
,

Кажется действительно Интересно! Спасибо, что поделился :), @Andrea Ciufo


1

Какое решение является наиболее эффективным и быстрым для чтения и хранения данных с акселерометра и GPS?

Вы не указываете, насколько быстро достаточно быстро, и как долго вы хотите записывать данные. В частности, для данных акселерометра.

Это предлагаемое решение будет регистрировать примерно 800 отсчетов акселерометра в секунду (7 x 16-битных целых чисел) непрерывно в течение 30 дней.

Есть две проблемы i) чтение и разбор GPS и чтение акселерометра ii) сохранение данных на SD-карте.

Чтение/анализ данных GPS требует времени. В этом руководстве Последовательный ввод-вывод Arduino для реального мира рассматривается буферизация последовательного ввода, анализ Данные GPS и буферизация последовательного (текстового/отладочного) вывода. Чтение данных акселерометра происходит намного быстрее, оставляйте их в бинарном виде.

Сохранение данных на SD-карте может быть очень медленным. Пример AnalogBinLogger Билла Греймана в более ранней версии его библиотеки SdFat Arduino использует предварительно выделенный и стертый файл SD и специальные записи блоков для минимизации задержки SD. https://github.com/greiman/Sd Fat (код для более ранней версии с AnalogBinLogger)

В этом проекте Высокочастотная и длительная регистрация данных используется модуль Uno /Mega для хранения 512-байтовых блоков данных на SD-карте менее чем за 1 мс каждый блок непрерывно в течение 30 дней. Он использует прерывание для сбора данных АЦП и два буфера по 512 байт для сбора входящих данных и записи на SD-карту.

Применяя эти два проекта к вашей ситуации, вы можете

  1. Используйте на Mega2560 для чтения/буферизации/фильтрации/анализа данных GPS и чередования их с данными акселерометра и буферизации их для двоичной передачи в второй Mega2650 через последовательный порт (115200 бод), т.е. ~11,5 Кбайт/сек. Для 7 переменных акселерометра x int16_t это будет около 800 выборок в секунду по последовательному соединению в двоичном формате. Стандартного 64-байтного буфера TX Serial для Mega2560 должно быть достаточно.

  2. Во втором Mega265 вам нужно изменить обработчик прерываний UART для одного из UART, чтобы заполнить текущий буфер SD вместо обычного приемного буфера. Когда буфер заполнен, прерывание заменяет его на второй буфер, как в проекте высокоскоростного регистратора выше.

Второй Mega также будет иметь командный и управляющий последовательный ввод для подготовки SD-карты и запуска/остановки хранилища.

Единый подход

Поскольку SD-запись блока 512 занимает < 1 мс, вы также можете объединить эти два процесса во внутреннюю память.

Чтение/буферизация/анализ заполняют блок 512. Когда он заполнен, заблокируйте его на 1 мс, пока SD записывает этот буфер. Такой подход уменьшит частоту дискретизации акселерометра. См. Простая многозадачность Arduino, чтобы узнать, как чередовать задачи чтения/анализа GPS с выборкой. акселерометр и запись на SD-карту

,