Почему частота обновления постоянно уменьшается при входе на SD-карту?

Текущий код используется для сбора состояния 12 инфракрасных лучей и записи их на SD-карту с помощью часов реального времени для отметки времени. Код использует переключатель для включения и выключения устройства и светодиод для индикации того, записываются ли данные на SD-карту. Когда позже мы просмотрели данные на SD-карте, частота обновления медленно снижалась, и через несколько часов у нас было всего 10 журналов в секунду. Почему это происходит и как это исправить?

#include <SD.h>   //For talking to SD Card
#include <Wire.h>  //For RTC
#include "RTClib.h" //For RTC

//Определение pins
//SD-карта находится на стандартных выводах SPI
//RTC находится на стандартных выводах I2C
const int CS_PIN   = 53;

//Скорость по умолчанию 25 мс
int refresh_rate = 25;

//Определить объект RTC
RTC_DS1307 RTC;

//Инициализация переменных
String year, month, day, hour, minute, second, time, date;

#define SENSOR1 22
#define SENSOR2 23
#define SENSOR3 24
#define SENSOR4 25
#define SENSOR5 26
#define SENSOR6 27
#define SENSOR7 32
#define SENSOR8 33
#define SENSOR9 34
#define SENSOR10 35
#define SENSOR11 36
#define SENSOR12 37
#define LEDPIN 6
#define SWITCH 10 

// переменные изменятся:
int sensorState1 = 0, lastState1 = 0;
int sensorState2 = 0, lastState2 = 0;       
int sensorState3 = 0, lastState3 = 0;
int sensorState4 = 0, lastState4 = 0;
int sensorState5 = 0, lastState5 = 0;
int sensorState6 = 0, lastState6 = 0;  
int sensorState7 = 0, lastState7 = 0;
int sensorState8 = 0, lastState8 = 0; 
int sensorState9 = 0, lastState9 = 0;
int sensorState10 = 0, lastState10 = 0;   
int sensorState11 = 0, lastState11 = 0;
int sensorState12 = 0, lastState12 = 0;  

//Переменная для чтения состояния коммутатора
int SWITCHSTATE ;

//определение целых чисел перемен датчика
int SensorChange1 = 0;
int SensorChange2 = 0;
int SensorChange3 = 0;
int SensorChange4 = 0;
int SensorChange5 = 0;
int SensorChange6 = 0;
int SensorChange7 = 0;
int SensorChange8 = 0;
int SensorChange9 = 0;
int SensorChange10 = 0;
int SensorChange11 = 0;
int SensorChange12 = 0;


void setup() 
{
  // инициализировать вывод датчика 24 в качестве входного сигнала:
  pinMode(SENSOR1, INPUT);
digitalWrite(SENSOR1, HIGH); // включить подтягивание // инициализировать вывод датчика 25 в качестве входного сигнала:
  pinMode(SENSOR2, INPUT);
digitalWrite(SENSOR2, HIGH); // инициализировать вывод датчика 26 в качестве входного сигнала:
  pinMode(SENSOR3, INPUT);
digitalWrite(SENSOR3, HIGH); // инициализировать вывод датчика 27 в качестве входного сигнала:
  pinMode(SENSOR4, INPUT);
digitalWrite(SENSOR4, HIGH); // инициализировать вывод датчика 28 в качестве входного сигнала:
  pinMode(SENSOR5, INPUT);
digitalWrite(SENSOR5, HIGH); // инициализировать вывод датчика 29 в качестве входного сигнала:
  pinMode(SENSOR6, INPUT);
digitalWrite(SENSOR6, HIGH); // инициализировать вывод датчика 30 в качестве входного сигнала:
  pinMode(SENSOR7, INPUT);
digitalWrite(SENSOR7, HIGH); // инициализировать вывод датчика 31 в качестве входного сигнала:
  pinMode(SENSOR8, INPUT);
digitalWrite(SENSOR8, HIGH); // инициализировать вывод датчика 32 в качестве входного сигнала:
  pinMode(SENSOR9, INPUT);
digitalWrite(SENSOR9, HIGH); // инициализировать вывод 33 датчика в качестве входного сигнала:
  pinMode(SENSOR10, INPUT);
digitalWrite(SENSOR10, HIGH); // инициализировать вывод 34 датчика в качестве входного сигнала:
  pinMode(SENSOR11, INPUT);
digitalWrite(SENSOR11, HIGH); // инициализировать вывод 35 датчика в качестве входного сигнала:
  pinMode(SENSOR12, INPUT);
digitalWrite(SENSOR12, HIGH); // инициализировать вывод светодиода в качестве выходного сигнала:  


// инициализировать вывод светодиода в качестве выходного сигнала:
  pinMode (LEDPIN, OUTPUT);

// инициализировать вывод ПЕРЕКЛЮЧАТЕЛЯ в качестве выхода:
  pinMode (SWITCH, INPUT);

// setpin 19 as high (5V) and 18 as low 
  pinMode(19, OUTPUT);     
  pinMode(18, OUTPUT);
  digitalWrite(19, HIGH);
  digitalWrite(18, LOW);
  
  Serial.begin(9600);
  Serial.println(F("Initializing Card"));
 
 //CS pin, and pwr/gnd pins are outputs
  pinMode(CS_PIN, OUTPUT);
 
  //Инициировать шину I2C и библиотеку RTC
  Wire.begin();
  RTC.begin();
 
  //Если RTC не работает, установите для него время компиляции компьютера
  if (! RTC.isrunning())
  {
    Serial.println(F("RTC is NOT running!"));
    RTC.adjust(DateTime(__DATE__, __TIME__));
  }
 
  //Инициализация SD-карты
  if (!SD.begin(CS_PIN))
  {
    Serial.println(F("Card Failure"));
    return;
  }
  Serial.println(F("Card Ready"));

  //Прочитать информацию о конфигурации (speed.txt)
  File commandFile = SD.open("speed.txt");
  if (commandFile)
  {
    Serial.println(F("Reading Command File"));
  
    while(commandFile.available())
    {
      refresh_rate = commandFile.parseInt();
    }
    Serial.print(F("Refresh Rate = "));
    Serial.print(refresh_rate);
    Serial.println(F("ms"));
    commandFile.close();
  }
  else
  {
    Serial.println(F("Could not read command file."));
    return;
  }

}

void loop()
{
  SWITCHSTATE = digitalRead(SWITCH); //read input value
  if (SWITCHSTATE == LOW)
 {
   digitalWrite(LEDPIN, LOW);
 } 
  else 
 {
   digitalWrite(LEDPIN, HIGH);
  
// считывание состояния значения датчика:
  sensorState1 = digitalRead(SENSOR1);
  sensorState2 = digitalRead(SENSOR2);
  sensorState3 = digitalRead(SENSOR3);
  sensorState4 = digitalRead(SENSOR4);
  sensorState5 = digitalRead(SENSOR5);
  sensorState6 = digitalRead(SENSOR6);
  sensorState7 = digitalRead(SENSOR7);
  sensorState8 = digitalRead(SENSOR8);
  sensorState9 = digitalRead(SENSOR9);
  sensorState10 = digitalRead(SENSOR10);
  sensorState11 = digitalRead(SENSOR11);
  sensorState12 = digitalRead(SENSOR12);


  //Получить текущую информацию о дате и времени и сохранить ее в строках
  DateTime datetime = RTC.now();
  year  = String(datetime.year(),  DEC);
  month = String(datetime.month(), DEC);
  day  = String(datetime.day(), DEC);
  hour  = String(datetime.hour(), DEC);
  minute = String(datetime.minute(), DEC);
  second = String(datetime.second(), DEC);

  //Объединить строки в дату и время
  date = year + "/" + month + "/" + day;
  time = hour + ":" + minute + ":" + second;

  if (sensorState1 == HIGH)
  {
   SensorChange1 = 1;
  }
  else 
  {
   SensorChange1 = 0;
  }
  if (sensorState2 == HIGH)
  {
   SensorChange2 = 1;
  }
  else 
  {
   SensorChange2 = 0;
  }
  if (sensorState3 == HIGH)
  {
   SensorChange3 = 1;
  }
  else 
  {
   SensorChange3 = 0;
  }
  if (sensorState4 == HIGH)
  {
   SensorChange4 = 1;
  }
  else 
  {
   SensorChange4 = 0;
  }
  if (sensorState5 == HIGH)
  {
   SensorChange5 = 1;
  }
  else 
  {
   SensorChange5 = 0;
  }
  if (sensorState6 == HIGH)
  {
   SensorChange6 = 1;
  }
  else 
  {
   SensorChange6 = 0;
  }
  if (sensorState7 == HIGH)
  {
   SensorChange7 = 1;
  }
  else 
  {
   SensorChange7 = 0;
  }
  if (sensorState8 == HIGH)
  {
   SensorChange8 = 1;
  }
  else 
  {
   SensorChange8 = 0;
  }
  if (sensorState9 == HIGH)
  {
   SensorChange9 = 1;
  }
  else 
  {
   SensorChange9 = 0;
  }
  if (sensorState10 == HIGH)
  {
   SensorChange10 = 1;
  }
  else 
  {
   SensorChange10 = 0;
  }
  if (sensorState11 == HIGH)
  {
   SensorChange11 = 1;
  }
  else 
  {
   SensorChange11 = 0;
  }
  if (sensorState12 == HIGH)
  {
   SensorChange12 = 1;
  }
  else 
  {
   SensorChange12 = 0;
  }


  File dataFile ;
  dataFile = SD.open("log1.csv", FILE_WRITE);
  if (dataFile)
    {
      dataFile.print(date);
      dataFile.print(F(","));
      dataFile.print(time);
      dataFile.print(F(","));
      dataFile.print(SensorChange1);
      dataFile.print(F(","));
      dataFile.print(SensorChange2);
      dataFile.print(F(","));
      dataFile.print(SensorChange3);
      dataFile.print(F(","));
      dataFile.print(SensorChange4);
      dataFile.print(F(","));
      dataFile.print(SensorChange5);
      dataFile.print(F(","));
      dataFile.print(SensorChange6);
      dataFile.print(F(","));
      dataFile.print(SensorChange7);
      dataFile.print(F(","));
      dataFile.print(SensorChange8);
      dataFile.print(F(","));
      dataFile.print(SensorChange9);
      dataFile.print(F(","));
      dataFile.print(SensorChange10);
      dataFile.print(F(","));
      dataFile.print(SensorChange11);
      dataFile.print(F(","));
      dataFile.print(SensorChange12);
      dataFile.println( );
      dataFile.close(); //Данные фактически не записываются, пока мы не закроем соединение!

      //Выведите то же самое на экран для отладки
      Serial.println();
      Serial.print(date);
      Serial.print(F(","));
      Serial.print(time);
      Serial.print(F(","));
      Serial.print(SensorChange1);
      Serial.print(F(","));
      Serial.print(SensorChange2);
      Serial.print(F(","));
      Serial.print(SensorChange3);
      Serial.print(F(","));
      Serial.print(SensorChange4);
      Serial.print(F(","));
      Serial.print(SensorChange5);
      Serial.print(F(","));
      Serial.print(SensorChange6);
      Serial.print(F(","));
      Serial.print(SensorChange7);
      Serial.print(F(","));
      Serial.print(SensorChange8);
      Serial.print(F(","));
      Serial.print(SensorChange9);
      Serial.print(F(","));
      Serial.print(SensorChange10);
      Serial.print(F(","));
      Serial.print(SensorChange11);
      Serial.print(F(","));
      Serial.print(SensorChange12);

    }
    else
    { 
     Serial.println(F("Couldn't open log file"));
    }
 
  lastState1 = sensorState1;
  lastState2 = sensorState2;
  lastState3 = sensorState3;
  lastState4 = sensorState4;
  lastState5 = sensorState5;
  lastState6 = sensorState6;
  lastState7 = sensorState7;
  lastState8 = sensorState8;
  lastState9 = sensorState9;
  lastState10 = sensorState10;
  lastState11 = sensorState11;
  lastState12 = sensorState12;


  delay(refresh_rate);
  }

  }

Frequency over time as graphed in R

, 👍10

Обсуждение

вы можете оптимизировать свой код, используя массивы .... sensorState[1] , SensorChange[1] и т. Д. ... затем используйте цикл " для " для перебора датчиков и обновления значений, @jsotola

Это не имеет никакого отношения к вашему вопросу, но эта длинная строка повторяющихся вызовов .print()/.println() может быть перемещена в отдельную функцию, например "пустая запись печати(Print &p) {" , которая может быть вызвана как для "Файла данных", так и для "Последовательного". printRecord будет содержать последовательность (или, как сказал джсотола, массив) " p.print(...`., @timemage

Я не мог смотреть на не-петли, поэтому я переписал это в качестве примера, чтобы вы посмотрели [здесь](https://paste.debian.net/1194584/). Я обнаружил ошибку, когда при этом вы обновляете переменные "изменить", не глядя на предыдущее значение. Я не компилировал код, но он все равно может сработать!, @pipe

Почему все эти переменные "lastState". Ты, кажется, ничего с ними не делаешь., @PimV


1 ответ


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

25

У меня нет для вас определенного ответа, но есть глубокое подозрение. Если мне станет скучно, я это подтвержду.

В каждом проходе функции loop ()у вас есть:

  dataFile = SD.open("log1.csv", FILE_WRITE);

Короче говоря, поиск конца файла - это линейная операция в файловой системе FAT. Он должен следовать цепочке распределения, чтобы найти (или создать) последний кластер файла. Цепочка распределения похожа на связанный список, причем сами ссылки являются единственной вещью в структуре связанного списка; их номера сопоставляются с номерами кластеров. Таким образом, вы просматриваете связанный список, за исключением того, что вы можете делать это через несколько секторов SD-карты, что сравнительно дорого.

Таким образом , повторно открывая его в loop (), вы заставляете его выполнять этот процесс удлинения каждый раз, по-видимому, из-за:

dataFile.close(); //Данные фактически не записываются до тех пор, пока мы не закроем соединение!

Часть .close(); которая делает это , становится доступной вам как .flush ();, без необходимости закрывать файл и забывать, где находится текущий кластер.

Другими словами, вы должны иметь возможность открыть() файл один раз в setup() и заменить свой .close() в loop() на .flush().

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

Подтверждающие Результаты Испытаний

Короче говоря, да, существует измеримая, связанная с размером файла стоимость открытия файла для записи и поиска до конца. И это просто факт о файловой системе FAT и заметен потому, что нецелесообразно загружать всю таблицу распределения в память на маленьком Arduino. Обычно он (например, на рабочем столе) загружается в память, и вы едва ли заметите разницу в открытии файлов разного размера.

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

Этот тест включал:

  • Arduino UNO.
  • 2 ГБ отформатированной карты FAT16.
  • Arduino AVR core версии 1.8.3
  • Библиотека SD - карт версии 1.2.4.

Ваши результаты, вероятно, будут сильно варьироваться в зависимости от типа MCU и размера блока файловой системы, но до тех пор, пока вы используете Arduino с SD-библиотекой, которая минимизирует объем памяти, вы будете иметь заметное увеличение времени открытия с большими размерами файлов.

Ниже будет немного занудно, но поскольку люди, похоже, заинтересованы в этом, я приведу некоторые детали. Код Arduino следует внизу для того, что было использовано для сбора данных, которые произвели график.

Я опустошил 2 Гб SD-карты. Это включало удаление "Информации о системном томе", которую Windows (или, возможно, производитель карт) поместил туда, и подтвердило, что не было выделенных кластеров, не было цепочек файлов. При этих условиях файл должен просто расти в последовательных секторах на карте; по крайней мере, как представлено картой, как в: если мы игнорируем что-либо, что карта может делать для выравнивания износа. Шаблон, используемый в программе, позволяет довольно легко просто посмотреть на необработанное изображение карты и увидеть, что именно это и произошло.

В любом случае, с моей установкой это некоторые результаты:

Graph of real timings.

Подсчет сектора 98304 справа относится к файлу примерно 50 Мегабайт.

Эта SD - карта отформатирована в формате FAT16, что-то, что я не заметил, пока не запустил цифры на этой схеме лестницы и не обнаружил, что они бросают вызов ожиданиям в 2 раза.

Будучи FAT16, каждый сектор таблицы FAT имеет 512/2 = 256 записей в нем. Каждый номер записи соответствует одному кластеру (блоку), который для файловой системы моей SD-карты составляет 32 Кбайт, или 64 числа 512-байтовых секторов. Таким образом, каждый сектор таблицы FAT (в моем случае) отвечает за размещение 64 * 256 = 16 384 файловых секторов. Другими словами, шаблон лестницы, по-видимому, отражает тот факт, что следование за цепочкой распределения для нашего последовательного файла означает чтение еще одного сектора таблицы FAT для каждых 16 384 секторов файла, следовательно, скачки на этих интервалах.

  • Если вы используете фрагментированный файл, это нужно делать чаще, и производительность будет выглядеть хуже. Я проверил SD-карту, чтобы убедиться, что она была написана последовательно.

  • Если вы используете FAT32, вы можете ожидать, что это займет больше времени, потому что каждый сектор FAT будет хранить вдвое меньше ссылок на кластеры.

  • Если вы используете меньший размер кластера (блока), то это займет больше времени, потому что каждый ввод в сектор FAT охватывает меньше содержимого файла.

Тестовый код

//
// Протестировано на UNO с Arduino Core версии 1.8.3
// 2-Гигабайтная SD-карта
//

#include <SD.h> // Версия 1.2.4 на момент тестирования.
#include <SPI.h> 


static constexpr int PIN_SDCARD_SHIELD_CHIP_SELECT = 10;
static constexpr int PIN_HALT_TEST                 =  2;

static const char test_filename[] = "testfile.txt";


struct test_state : Printable {
  uint32_t      sector_counter = -1;
  unsigned long open_duration  =  0;

  size_t printTo(Print& p) const override {
    auto r = p.write('[');
    r     += p.print(sector_counter);
    r     += p.write(':');
    r     += p.print(open_duration);
    r     += p.write(']');
    return r;
  }


  void update(unsigned long new_open_duration) {
    open_duration = new_open_duration;
    ++sector_counter;
  }
};


//
//
//
void write_numbered_sector(
  Print &p,
  const test_state &state
) {
  static constexpr size_t SECTOR_SIZE            = 512;
  static constexpr size_t CR_LF_SIZE             =   2;

  const auto printed_test_state_len = p.print(state);

  const auto xs_to_write = 
      SECTOR_SIZE
    - printed_test_state_len
    - CR_LF_SIZE;

  for (size_t i = xs_to_write; i != 0; --i) {
    p.write('X');
  }

  p.write('\r');
  p.write('\n');
}


//
//
//
void setup() {
  pinMode(PIN_HALT_TEST, INPUT_PULLUP);

  Serial.begin(9600);

  if (!SD.begin(PIN_SDCARD_SHIELD_CHIP_SELECT)) {
    Serial.println(F("Card initialization failed."));
    halt_test();
  }

  SD.remove(test_filename);
}


//
//
//
void blink_forever() {
  pinMode(LED_BUILTIN, OUTPUT);
  while (true) {
    static bool led_state = false;

    led_state = !led_state;
    digitalWrite(LED_BUILTIN, led_state);
    delay(100);
  }
}


//
//
//
void halt_test() {
  SD.end();
  SPI.end();
  Serial.end();

  blink_forever();
}


//
//
//
void loop() {
  static test_state state;


  Serial.flush(); 
  const auto started_opening = micros();
  File testfile = SD.open(test_filename, FILE_WRITE);
  const auto done_opening = micros();

  const auto open_duration = done_opening - started_opening;

  state.update(open_duration);

  write_numbered_sector(
    testfile,
    state
  );
  testfile.close();
  Serial.print(micros());
  Serial.print(' ');
  Serial.println(state);






  const auto user_requested_halt = (digitalRead(PIN_HALT_TEST) == LOW);
  if (user_requested_halt) {
    Serial.println(F("Test halt requested."));
    halt_test();
  }
}

//  vim:sw=2:ts=2:et:nowrap:ft=cpp:
,

Если вы беспокоитесь о потере питания без присмотра, вы можете периодически промывать, например, каждые 400 записей с номинальным интервалом в 10 секунд - номинальным, потому что при задержке в 25 мс ваш цикл всегда будет длиться дольше 25 мс, @Chris H

Ах да, старая проблема "выполнение ресурсоемкой операции, которую следует выполнять только один раз, но делать это каждую итерацию":), @mishan

Также возможно, что закрытие вместо сброса приведет к увеличению записи на SD - карту - в частности, в сектор каталога, содержащий запись файла, - что приведет к дополнительному износу (и дополнительному времени для контроллера карты для балансировки нагрузки секторов (если только flush _also_ не касается записи файла, если контроллер SD-карты балансирует нагрузку секторов). Кто-нибудь знает?, @davidbak

Если позже я снова покопаюсь в коде, я проверю. Моя внутренняя реакция заключается в том, что и то, и другое окажет одинаковый эффект на сектора при вызове. Но я этого *не знаю*., @timemage

@mishan это называется [алгоритм художника Шлемиэля](https://www.joelonsoftware.com/2001/12/11/back-to-basics/), @user253751

Некоторые другие полезные советы также можно найти в этом ответе. В частности, использование нескольких файлов может дать некоторые преимущества. Если у меня будет реальная причина отредактировать это снова, я, вероятно, включу ссылку., @timemage