Запись на SD-карту с частотой дискретизации 50 мс или меньше? У меня есть скетч, который записывает в sd каждую 1 секунду, но застревает на более высоких частотах дискретизации. Пожалуйста помоги?

Мой код ниже считывает данные с АЦП и 3 цифровых контактов через Mega и печатает на SD Shield (Deek Robot). Я хочу иметь возможность читать данные каждые 50 мс (в идеале 20 мс). Я не могу получить меньше 250 мс.

Я прочитал бесчисленное количество сообщений на форуме по одной и той же проблеме и понял, что мне нужно создать 2 буфера по 512 байт, хранить данные в одном, пока другой записывается на SD-карту. В сообщении на форуме упоминается использование библиотеки GPS Adafruit в качестве примера двойной буферизации, но я не могу ее найти. Другой предлагает изменить внутренний буфер с 64 байт на 256 байт в hardwareserial.cpp, но я даже не могу перейти к нему на своем ноутбуке w***ows.

Я запустил dataString.Length, и максимальная длина равна 38 для 8 столбцов данных, плюс длинный (для записи мс) еще один байт для "\t" и 3 байта запаса = 46 байтов всего, округление до 50 байт/сэмпл при частоте дискретизации 50 мс составляет 1000 байт/с. Черт возьми, я забыл добавить разделительные запятые между столбцами = плюс еще 7 байтов; назовем это 55 всего = 1100 байт/сек. Я знаю, что это можно сделать, но я не могу этого сделать; не на моем нынешнем уровне знаний/навыков.

Заранее благодарим вас за помощь.

//включаем соответствующие библиотеки
#include <EnableInterrupt.h>
#include <SPI.h>
#include <SD.h>

// глобально определяем пины
#define BUSY 3    //фиолетовый*
#define RD 4      //желтый* RD+CS связаны вместе
#define RESET 5   //серый*
#define CONVST 6  //зеленый* CONVSTA+CONVSTB спаяны вместе на плате
#define RANGE 7   //синий* *не постоянный - перепроверьте!!!*

#define DB0 22
#define DB1 23
#define DB2 24
#define DB3 25
#define DB4 26
#define DB5 27
#define DB6 28
#define DB7 29
#define DB8 30
#define DB9 31
#define DB10 32
#define DB11 33
#define DB12 34
#define DB13 35
#define DB14 36
#define DB15 37

//байт statusLed = 13;
byte sensorPin1       = 38;
byte sensorPin2       = 40;
byte sensorPin3       = 42;

int sensorValue[8];
int rawData[16];
// изменяем размер этих массивов, чтобы он соответствовал количеству каналов, считываемых АЦП
int adcChannel[5];
int adcData[5];
int channelCount=5;

volatile byte pulseCount1;
volatile byte pulseCount2;
volatile byte pulseCount3;

int pulses[3];

// cs pin для sd-shield *NB 53 для Mega
const int chipSelect = 53;
// ССК 52
// МИСО 50
// МОСИ 11

unsigned long oldTime;

void setup() {             // устанавливаем оборудование

  Serial.begin(9600);     
  //SD Card.....
  while (!Serial) {
    ; // ждем подключения последовательного порта. Требуется только для родного порта USB
  }

  Serial.print("Initializing SD card...");

  // смотрим, присутствует ли карта и может ли она быть инициализирована:
  if (!SD.begin(chipSelect)) {
    Serial.println("Card failed, or not present");
    // больше ничего не делаем:
    while (1);
  }
  Serial.println("card initialized.");

  enableInterrupt(BUSY, bitBang, FALLING);

  oldTime            = 0;

  pinMode(DB0, INPUT_PULLUP);
  pinMode(DB1, INPUT_PULLUP);
  pinMode(DB2, INPUT_PULLUP);
  pinMode(DB3, INPUT_PULLUP);
  pinMode(DB4, INPUT_PULLUP);
  pinMode(DB5, INPUT_PULLUP);
  pinMode(DB6, INPUT_PULLUP);
  pinMode(DB7, INPUT_PULLUP);
  pinMode(DB8, INPUT_PULLUP);
  pinMode(DB9, INPUT_PULLUP);
  pinMode(DB10, INPUT_PULLUP);
  pinMode(DB11, INPUT_PULLUP);
  pinMode(DB12, INPUT_PULLUP);
  pinMode(DB13, INPUT_PULLUP);
  pinMode(DB14, INPUT_PULLUP);
  pinMode(DB15, INPUT_PULLUP);

  pinMode(RESET, OUTPUT);
  pinMode(CONVST, OUTPUT);
  pinMode(RD, OUTPUT);
  pinMode(RANGE, OUTPUT);
  pinMode(BUSY, INPUT);

  //сбросьте АЦП, чтобы начать преобразование
  digitalWrite(RESET, HIGH);
  delayMicroseconds(10);
  digitalWrite(RESET, LOW);

  digitalWrite(CONVST, LOW);
  digitalWrite(RD, HIGH);
  digitalWrite(RANGE, LOW);
  digitalWrite(BUSY, LOW);

  delayMicroseconds(100);

  // Настраиваем линию светодиодов состояния как выход
// pinMode(statusLed, OUTPUT);
// digitalWrite(statusLed, HIGH); // У нас подключен светодиод с активным низким уровнем

  pinMode(sensorPin1, INPUT);
  digitalWrite(sensorPin1, HIGH);

  pinMode(sensorPin2, INPUT_PULLUP);
  digitalWrite(sensorPin2, HIGH);

  pinMode(sensorPin3, INPUT_PULLUP);
  digitalWrite(sensorPin3, HIGH);

  pulseCount1        = 0;
  pulseCount2        = 0;
  pulseCount3        = 0;

  oldTime            = 0;

  // датчики Холла настроены на срабатывание при изменении состояния ПАДЕНИЕ
  // (переход из ВЫСОКОГО состояния в НИЗКОЕ состояние)
  enableInterrupt(sensorPin1, pulseCounter1, FALLING);
  enableInterrupt(sensorPin2, pulseCounter2, FALLING);
  enableInterrupt(sensorPin3, pulseCounter3, FALLING);

}

void loop() {

  // изменяем это значение на предпочтительную частоту дискретизации
  if ((millis() - oldTime) == 50) {

    oldTime = millis();

    //Serial.print(millis());
    //Серийный.print("\t");

    // сообщаем АЦП начать чтение (преобразование аналогового ввода в цифровой вывод)
    delayMicroseconds(10);
    digitalWrite(CONVST, LOW);
    delayMicroseconds(10);
    digitalWrite(CONVST, HIGH);
    // когда чтение-преобразование завершено, АЦП посылает на вывод BUSY низкий уровень, запуская BitBang ISR

    //выводим данные АЦП из массива внутри ISR bitBang в строку
    String adcString = "";
    for(int thisChannel=0; thisChannel<channelCount; thisChannel++){
      adcString += String(adcData[thisChannel]);
      adcString += ",";
    }

    //отключаем прерывание для доступа к текущему счетчику импульсов
    disableInterrupt(sensorPin1);
    disableInterrupt(sensorPin2);
    disableInterrupt(sensorPin3);
    // получаем текущее количество импульсов и сохраняем в переменной
    pulses[0] = pulseCount1;
    pulses[1] = pulseCount2;
    pulses[2] = pulseCount3;
    //сбросить счетчик импульсов
    pulseCount1 = 0;
    pulseCount2 = 0;
    pulseCount3 = 0;
    //включаем прерывание и снова начинаем увеличивать счетчик импульсов
    enableInterrupt(sensorPin1, pulseCounter1, FALLING);
    enableInterrupt(sensorPin2, pulseCounter2, FALLING);
    enableInterrupt(sensorPin3, pulseCounter3, FALLING);

    String pulseString = "";
    for(int i=0; i<3; i++){
      pulseString += String(pulses[i]);
      if (i<2) {
        pulseString += ",";
      }
    }

   String dataString = String(adcString + pulseString);
  // открываем файл. обратите внимание, что одновременно может быть открыт только один файл,
  // так что вы должны закрыть это, прежде чем открывать другое.
  File dataFile = SD.open("dataLog.txt", FILE_WRITE);

  // если файл доступен, пишем в него:
  if (dataFile) {
    dataFile.print(millis());
    dataFile.print(",");
    dataFile.println(dataString);
    dataFile.close();
    // также печатать в последовательный порт:
    //Serial.println(dataString);
  }  
  // если файл не открыт, выскакивает ошибка:
  else {
    Serial.println("error opening dataLog.txt");
  } 

 //Serial.println(dataString);

  }  
//начнем снова
}

// захват данных АЦП ISR (для n канала)
void bitBang ()  {
  //цикл for для битбанга значений аналоговых (АЦП) каналов (максимум 8) с последовательным сохранением их в массиве переменных (равном каналам АЦП)
  for(int thisChannel=0; thisChannel<channelCount; thisChannel++){
  // отправляем низкий уровень вывода считывания АЦП для битового удара по первому каналу
  digitalWrite(RD, LOW);
  //читаем состояние 16 контактов и сохраняем в переменной
  rawData[0] = digitalRead(DB15);
  rawData[1] = digitalRead(DB14);
  rawData[2] = digitalRead(DB13);
  rawData[3] = digitalRead(DB12);
  rawData[4] = digitalRead(DB11);
  rawData[5] = digitalRead(DB10);
  rawData[6] = digitalRead(DB9);
  rawData[7] = digitalRead(DB8);
  rawData[8] = digitalRead(DB7);
  rawData[9] = digitalRead(DB6);
  rawData[10] = digitalRead(DB5);
  rawData[11] = digitalRead(DB4);
  rawData[12] = digitalRead(DB3);
  rawData[13] = digitalRead(DB2);
  rawData[14] = digitalRead(DB1);
  rawData[15] = digitalRead(DB0);
  // преобразовать в 16-битный 2-секундный комплимент и сохранить в массиве переменных
  adcData[thisChannel] = rawData[0] | (rawData[1] << 1) | (rawData[2] << 2) | (rawData[3] << 3) | (rawData[4] << 4) | (rawData[5] << 5) | (rawData[6] << 6) | (rawData[7] << 7) |  (rawData[8] << 8) | (rawData[9] << 9) | (rawData[10] << 10) | (rawData[11] << 11) | (rawData[12] << 12) | (rawData[13] << 13) | (rawData[14] << 14) | (rawData[15] << 15);
  // отправляем высокий уровень на выводе АЦП, чтобы сказать, что мы прочитали первый канал
  digitalWrite(RD, HIGH);
  //повторить для n каналов
  }
}

// подсчет импульсов ISR
void pulseCounter1(){
  // Увеличиваем счетчик импульсов
  pulseCount1++;
}
void pulseCounter2(){
  // Увеличиваем счетчик импульсов
  pulseCount2++;
}
void pulseCounter3(){
  // Увеличиваем счетчик импульсов
  pulseCount3++;
}

, 👍4

Обсуждение

if ((millis() - oldTime) == 50) { .... что произойдет, если программа ненадолго зависнет и пропустит разницу в 50 мс?, @jsotola

Вы думали о комментарии jsotola? Если нет, то я предлагаю вам подумать об этом очень и очень тщательно. Совет в ответе ниже - хороший совет, но это может быть просто комментарий, который спасает вас - и обнажает новую проблему, когда вы понимаете ответ...., @GMc

Сейчас провожу тесты. Знаешь что? Всего 10 дней назад эта часть кода говорила if ((millis() - oldTime >= 50) {, и это говорилось месяцами. Я не знаю, когда и как появилось двойное равенство. Я' Скоро сообщу о результате. Спасибо., @Microk

Да, кажется, это решило проблему. Абсолютно пинаю себя за то, что каким-то образом добавил ошибку, не осознавая этого, да ладно, humanum est errare, как гласит старая поговорка. Спасибо всем за вашу помощь, особенно человеку, который проголосовал за мой вопрос, тем самым наградив меня достаточной репутацией, чтобы комментировать везде - очень особенный день для меня. При чем тут этикет? Я не могу принять комментарий в качестве ответа, поэтому должен ли я принять комментарий @Michel Keijzer? еще раз спасибо, @Microk


2 ответа


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

2

Получается, что на самом деле проблема заключалась в этой строке кода. if ((millis() - oldTime) == 50) { его нужно было изменить на if ((millis() - oldTime >= 50) {

Я очень ценю всю помощь, которую все оказали мне, чтобы разобраться в этом, и особенно благодарен @Michel за то, что он дал ценный совет тому, кто не в курсе. Надеюсь, однажды я смогу отплатить тем же другим учеником. :D

,

4

Несколько советов:

  • Не открывайте и не закрывайте файл в каждой циклической последовательности (я думаю, вы можете использовать команду flush для сохранения/обновления файла.
  • Не сохранять строки, а сохранять необработанные данные и строку импульсов. Это займет 16 * 2 (необработанные данные + 3 байта для импульсных данных = 35 байтов на 16 отсчетов, что означает 35 байт/отсчетов * 20 отсчетов/с = 700 байт (я думаю, что ваш расчет 1100 байтов неверен).
  • Старайтесь избегать строк и тем более конкатенации строк. Я не знаю, будет ли это узким местом, но в любом случае операции со строками дороги, а для конкатенации есть вероятность (или всегда так), что память нужно выделять динамически. Вместо этого используйте строковый буфер.
,

Спасибо за ваши предложения. Я читал о проблемах со строками, и вместо этого следует использовать класс строк C. Я реализую первые 2 пункта сегодня, но мне нужно будет провести небольшое исследование, прежде чем взяться за третий. я кип., @Microk

Операции файлового ввода-вывода обычно довольно дороги, поэтому лучше свести их к минимуму. С третьим пунктом может быть немного больше работы, но, за исключением очень простых эскизов, попробуйте использовать строку C (вместо этого char*). Совет: для тестирования со строками C не используйте Arduino, а делайте это на ПК, это намного быстрее, чем, если код работает или вы знаете, как работают строки C, перенесите его в Arduino IDE., @Michel Keijzers

Итак, я использую командную строку на своем ноутбуке?, @Microk

Нет, я предлагаю вам установить либо Visual Studio 2019, либо Eclipse C(++)... оба бесплатны. Вы создаете свой код, не относящийся к Arduino, на ПК с помощью любой из этих двух IDE, и когда он работает, вы копируете его в IDE Arduino. Преимущество в том, что вы можете использовать отладку и вам не нужно каждый раз загружать программу/скетч., @Michel Keijzers

Это отличный совет, я обязательно им воспользуюсь :) Еще раз спасибо., @Microk

@Microk Это особенно полезно, если вы делаете большие программы., @Michel Keijzers

ОБНОВЛЕНИЕ: я полностью избавился от всех строк. Кажется, я не могу запустить сброс без сбоя SD-карты через короткое время, 0-2 минуты. Но у меня есть стабильный код, который делает именно то, что я хотел. Так что еще раз спасибо за помощь, @Microk

Не за что... и если программа работает стабильно, не меняйте ее (вы всегда можете добавить примечание/задачу для сброса на случай, если в следующий раз у вас возникнут проблемы)., @Michel Keijzers