Задержка между несколькими AnalogReads

Я пытаюсь прочитать 4 аналоговых входа (скорость 1 кГц). Я изменил прескалер на 16 для своего Arduino Leonardo по этой ссылке. Затем я попытался прочитать аналоговые выводы и использовал следующий код для отображения времени и показаний:


#define FASTADC 1
// определяет для установки и очистки битов регистра
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif
double t = 0;
void setup() {
  int start;
  int i;
  #if FASTADC
  // устанавливаем предварительное масштабирование на 16
  sbi(ADCSRA, ADPS2);
  cbi(ADCSRA, ADPS1);
  cbi(ADCSRA, ADPS0);
  #endif

  Serial.begin(9600);
  Serial.print("ADCTEST: ");
  start = millis();
  for (i = 0 ; i < 1000 ; i++)
    analogRead(0);
  Serial.print(millis() - start);
  Serial.println(" msec (1000 calls)");
}

// процедура цикла выполняется снова и снова навсегда:
void loop() {
  // распечатать прочитанное значение:
  t = millis() / 1000;
  Serial.print(t);
  Serial.print('\t');
  Serial.print(analogRead(A0)); delay(1);
  Serial.print('\t');
  Serial.print(analogRead(A1)); delay(1);
  Serial.print('\t');
  Serial.print(analogRead(A2)); delay(1);
  Serial.print('\t');
  Serial.println(analogRead(A3)); delay(1);
}

Я подключил 2 В от источника постоянного тока ко всем 4 каналам, и показания были одинаковыми. Я использовал вывод последовательного монитора и вставил показания за 1 секунду в Excel, чтобы увидеть частоту. При скорости 9600 бод и отсутствии задержки между показаниями я получил 200 Гц (для 4 аналоговых входов).

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

Наконец, я буду отображать показания в Matlab, поэтому я думаю, что это повлияет на частоту дискретизации. Как я могу контролировать его, чтобы я сэмплировал с частотой 1 кГц для всех каналов?

Вот скриншот показаний 4-х каналов (вход 2В).

Подводя итог, мне нужно знать следующее:

  1. Влияние добавления задержки между каждыми двумя показаниями на точность показаний и частоту дискретизации.

  2. Как найти наилучшую частоту дискретизации для каждого входа и как ею управлять?

  3. Повлияет ли использование Matlab для построения графика данных на частоту дискретизации?

Спасибо.

РЕДАКТИРОВАТЬ: я принял ответ ниже, если у кого-то возникла такая же проблема, убедитесь, что вы изменили предварительный делитель, как я упоминал выше, для достижения наилучших результатов.

, 👍1

Обсуждение

Снимать показания с плавающего вывода бессмысленно. Вы можете получить что угодно. Пожалуйста, подключите что-нибудь к контактам, прежде чем проводить эксперимент., @Edgar Bonet


4 ответа


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

2

Сначала я думал, что основная проблема не в том, что вы слишком медленно сэмплируете, а в том, что вы слишком медленно передаете данные. Как уже сказал Эдгар, при скорости 9 600 бит/с вам нужно 22,88 мс, чтобы отправить линию, поэтому максимальная частота составляет менее 50 Гц.

Однако, как вы уже поняли, скорость, которую вы получаете, составляет 200 Гц. Это потому, что этот расчет верен в случае реального последовательного интерфейса. Arduino Leonardo, с другой стороны, эмулирует последовательный интерфейс. Но... Нет такой вещи, как скорость передачи данных. Если вы посмотрите на исходный код эмуляции CDC, вы увидит, что скорость передачи данных, которую вы передаете функции begin, игнорируется.

Итак... Почему все еще медленно? Потому что вы используете функцию delay! Вы ждете 1 мс каждый раз, когда выполняете захват, а затем вам все равно нужно выполнить всю отправку. Это содержит две встроенные ошибки: 1) использование функции delay прерывает синхронизацию — лучше использовать таймер — и 2) вы работаете слишком медленно.

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

#define INTERVAL_LENGTH_US 1000UL

unsigned long previousMicros;

void loop()
{
    unsigned long currentMicros = micros();

    if ((currentMicros - previousMicros) >= INTERVAL_LENGTH_US)
    {
        previousMicros += INTERVAL_LENGTH_US;

        int val_a0 = analogRead(A0);
        int val_a1 = analogRead(A1);
        int val_a2 = analogRead(A2);
        int val_a3 = analogRead(A3);

        Serial.print(((double)currentMicros) / 1000000UL, 1); // 1 - количество десятичных знаков для печати
        Serial.print('\t');
        Serial.print(val_a0);
        Serial.print('\t');
        Serial.print(val_a1);

        Serial.print('\t');
        Serial.print(val_a2);
        Serial.print('\t');
        Serial.println(val_a3);
    }
}

ПРИМЕЧАНИЕ. Функция millis() заменена на micros() в соответствии с комментариями Эдгара и Ника.

Измените только определение, чтобы изменить шаг расчета. Теперь он установлен на 1000 мкс (т.е. 1 мс, то есть 1 кГц), но не увеличивайте его намного выше, иначе вам придется точно проверить время выборки и скорость передачи. Переменная t больше не нужна.

Небольшое примечание: это также исправит ошибку в вашем коде, которая не позволяет программе отображать текущее время (вместо вывода 2.5 она печатает 2.0).

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

,

Обратите внимание, что millis() иногда пропускает миллисекунду, например, он переходит с 41 сразу на 43. Вы можете использовать micros() вместо millis(), чтобы избежать проблемы: это дает разрешение 4 мкс., @Edgar Bonet

@EdgarBonet это звучит для меня в новинку ... В каких случаях он пропускает (кроме случаев, когда код цикла на самом деле длиннее 1 мс)?, @frarugi87

На 16-кГц AVR счетчик миллисекунд обновляется каждые 1024 мкс: счетчик миллисекунд увеличивается на единицу, а дробный счет увеличивается на 3 125-е миллисекунды. Всякий раз, когда эта доля достигает полной миллисекунды, счетчик миллисекунд увеличивается на два вместо одного. Это происходит примерно раз в каждые 1000/24 ≈ 41,67 обновлений., @Edgar Bonet

@ frarugi87 Я попробовал ваш код, и он работает! Мне нужен был способ исправить мою частоту дискретизации, и это делает это. Как вы сказали, скорость передачи данных ни на что не влияет. Я пробовал 9600 и 250000, и оба дали мне 1 кГц. Получение показаний в Matlab вместо последовательного монитора не повлияет на частоту, верно?, @Isra

@frarugi87 Эдгар Боне прав, я рассказываю об этом [здесь](http://www.gammon.com.au/forum/?id=12127&reply=1#reply1). Как сказал Эдгар, из-за предделителя таймера «тик» миллиса на самом деле немного медленный. Код подстраивается под это каждые 41/42 обновления., @Nick Gammon

@EdgarBonet Я сделал (быстрый) поиск в Google и не смог найти такое поведение, поэтому я проверил его и ... Что ж, 42 может быть окончательным ответом на жизнь, вселенную и все такое, но не для «миллис». .. Спасибо, что указали на это: я обновил ответ кодом, который, как мне кажется, исправляет это (так что micros() / prescaler), @frarugi87

@NickGammon Я прочитал много замечательных ответов на SO и здесь от вас, но я не знал, что у вас есть форум, где вы размещаете много вещей. Спасибо за ссылку: буду читать ваш сайт, @frarugi87

@Isra, пожалуйста, обновите код на тот, что в ответе, так как он решает проблему Эдгара. Этот немного более гибкий, чем предыдущий, и обновляется точно (ну, точнее, чем миллисекунды) каждую мс. Что касается матлаба, то источник данных тот же, поэтому скорость такая же, при условии, что матлаб достаточно быстр, чтобы прочитать все данные, @frarugi87

спасибо @frarugi87 за указание на это, я исправил. Но когда я читаю последовательные данные в Matlab, это дает мне эту ошибку «Предупреждение: неудачное чтение: тайм-аут произошел до того, как был достигнут Терминатор». У вас есть идеи, почему это происходит? Я использую fscanf(s, '%d')*(5.0/1023.0)); чтобы прочитать значения. И последний момент: если аналоговые показания имеют ширину 10 бит, как они передаются? Я знаю, что ширина данных последовательной связи составляет всего 8 бит (на всякий случай я установил скорость передачи данных 250000 на обоих концах), @Isra

При замене millis() на micros() вы ввели ошибочное деление на INTERVAL_LENGTH_US. Я позволил себе отредактировать ваш код, чтобы восстановить его прежнюю логику, и это было хорошо. Обратите внимание, что тест if (((currentMicros - previousMicros) / INTERVAL_LENGTH_US) > 0) технически не является неправильным, и компилятор все равно оптимизирует его в if (currentMicros - previousMicros >= INTERVAL_LENGTH_US). Но я думаю, что понятнее без деления. @Isra: вы можете попробовать эту новую версию., @Edgar Bonet

@EdgarBonet Я уже заметил это и исправил, как вы исправили, прежде чем писать комментарии, но, возможно, я напортачил с кнопкой «Назад» в браузере, и старая версия осталась в сети. Спасибо за исправление..., @frarugi87

@EdgarBonet Большое спасибо, я попробовал, но все равно получаю ту же ошибку: «Предупреждение: Неудачное чтение: входной буфер был заполнен до того, как был достигнут терминатор». Я попытался прочитать контакты с обычной скоростью, и я был в состоянии прочитать это в матлабе. Эта ошибка появляется только тогда, когда я устанавливаю скорость на 1 кГц., @Isra

@Isra попробуйте использовать пошаговый подход: сначала откройте и прочитайте только полученные строки (с помощью fscanf(s)), затем прочитайте целочисленное значение и, наконец, преобразуйте и используйте его. Посмотрите, когда вы заблокируете, и попытайтесь найти решение (я не очень разбираюсь в интерфейсе последовательного порта Matlab, поэтому я не знаю, как его правильно отлаживать). Что касается 10 бит, вы печатаете в последовательный порт, поэтому полученное значение (например, 1001) передается в кодировке ascii «1001», поэтому четыре байта («1», что равно 49, «0», что равно 48, затем «0» и «0»). Если ошибка связана со скоростью, попробуйте снизить ее до 100 Гц (установите определение на 10000)., @frarugi87

@ frarugi87 Я сделал то, что вы предложили, оказалось, что я должен печатать показания каждое в одной строке (из Arduino), Matlab не мог видеть терминатор (конец строки). Другая проблема: при чтении времени в Matlab вы должны использовать fscanf(s,%f) вместо fscanf(s,%d), потому что это двойное число. Спасибо всем за вашу МАССИВНУЮ поддержку, и я надеюсь, что это поможет и другим!, @Isra


0

Я попробовал вашу программу на моем Arduino Uno с аналогичными результатами: она печатает около 44 строк в секунду. Затем я сделал расчет:

Каждая строка состоит из 22 символов, включая CR и LF в конце. Каждый символ передается как 10 бит (8 бит данных, один стартовый бит и один стоповый бит). При скорости 9600 бит/с каждый бит передается за 0,104 мс. передано. Все это составляет 22,88 мс на строку (22×10×0,104), или в среднем около 43,7 печатных строк в секунду.

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

,

Я был абсолютно уверен, что это правильная интерпретация, но... Что ж, Leonardo (и все платы Arduino, которые эмулируют последовательный порт) игнорируют установленную вами скорость передачи данных. Таким образом, скорость передачи не является проблемой в ЭТОМ конкретном случае (см. мой ответ), @frarugi87

Когда задействован CDC/виртуальный коммуникационный порт, установка скорости передачи выполняется в драйвере устройства CDC на стороне хоста. Это может быть Windows или Linux или .... Или это не будет рукопожатие с вашей терминальной программой хоста., @dannyf

@dannyf Я никогда не использовал леонардо или микро, поэтому я никогда не использовал эмуляцию CDC. Но... Действительно ли он установлен на хосте? Я провел быстрое исследование в Интернете и нашел [эту тему о микрочипе CDC](http://www.microchip.com/forums/m285533.aspx). Они говорят, что нет такой вещи, как скорость передачи данных, поскольку данные передаются блоками, когда есть время, поэтому не существует «скорости передачи данных»; вы можете установить любое значение, и оно будет проигнорировано как на хосте, так и на Arduino... Это неправильно?, @frarugi87

@ Эдгар Я попробовал метод frarugi87, и он работал с несколькими скоростями передачи! Я никогда не знал, что Леонардо использует виртуальную последовательную связь. Я хотел бы попробовать это на Arduino pro Micro (также Atmega32u4) и Arduino DUE (потому что у него есть 12-битный АЦП). Думаю с про микро будет тоже самое, а с ДЮЭ будет так же? К сожалению, у меня пока нет двух плат, чтобы проверить их самостоятельно., @Isra

@Isra: Pro Micro действительно должен вести себя как Леонардо. Однако у Due, похоже, есть дополнительный ATmega16U2 в качестве чипа USB-to-serial. Таким образом, я ожидаю, что он будет вести себя как Uno (т.е. скорость передачи имеет значение)., @Edgar Bonet


1

Два способа сэмплирования с заданной частотой или близкой к ней:

  1. Выберите правильный предварительный делитель тактового сигнала АЦП. Это имеет некоторые ограничения и может не дать вам точную частоту дискретизации.

  2. Используйте таймер для запуска выборки. Можно сделать и в том и в другом. Аппаратное или программное обеспечение. Здесь это самый гибкий способ, особенно в сочетании с АЦП isr.

,

0

Несколько рекомендаций для получения хорошей производительности плат Arduino.

  • Максимально увеличьте значение AnalogResolution, если значение по умолчанию еще не максимальное (как в случае с Due).
  • Если возможно, используйте простой генератор вместо источника постоянного напряжения. Если вы можете создать синусоидальную или треугольную волну с пиками в пределах диапазона входного напряжения Arduino, вы можете построить что-то более интересное, чем прямая линия.
  • Нет необходимости выполнять какие-либо AnalogReads в setup(), и вы НЕ хотите ничего делать с объектом Serial в setup(), кроме как настраивать его. (Для этого есть скрытая причина.)
  • Чтобы получить хорошие частоты дискретизации от плат Arduino, не преобразовывайте в текст до НАМНОГО позже в потоке данных. ЧТОБЫ попробовать эту ЗНАЧИТЕЛЬНУЮ оптимизацию скорости, объявите массив из 4 элементов типа unsigned int, еще один массив из 12 элементов типа unsigned char и unsigned long для хранения времени.
  • В цикле() начните с назначения micros() беззнаковому длинному элементу
  • Затем вызовите четыре AnalogReads в непрерывной последовательности, назначив их четырем элементам массива unsigned int.
  • Затем скопируйте четыре байта из unsigned long и восемь байтов из четырех unsigned int в 12 элементов массива unsigned char. Для выполнения этой части вам потребуется использовать оператор сдвига вправо C++ ">> 4" и преобразование типа "(unsigned char)".
  • Затем, все еще находясь в цикле(), вызовите Serial.write(byteArray, 12) один раз, чтобы сразу отправить время и цифровые представления (в указанном вами порядке байтов) через последовательное соединение.
  • Поместите delayMicroseconds в самый конец цикла, чтобы получить задержку с более высоким разрешением между итерациями цикла.
  • На другой стороне последовательного соединения (рабочая станция или ноутбук) используйте dd или другую низкоуровневую программу передачи (вместо эмулятора терминала), чтобы скопировать из двоичных 12-байтовых выборок векторов с последовательного устройства, которое ваша рабочая станция или ноутбук использует для доступа последовательное соединение к двоичному файлу.

Вышеприведенное устраняет несколько узких мест, типичных для программ-примеров для новичков. Остается проблема в том, что для оптимизации скорости вы никогда не преобразовывали необработанные векторы двоичных данных в текстовый файл с разделителями табуляцией, который MATLAB или GnuPlot могут легко прочитать.

Простая программа на C, Java или Python затем может считывать 12-байтовые векторы и преобразовывать группы байтов из 4, 2, 2, 2 и 2 обратно в 32-битную метку времени и четыре 16-битных беззнаковых значения канала. в петле. Порядок байтов — это то, что вы выбрали в своем скетче Arduino. Вам нужно будет использовать оператор сдвига влево и побитовые операции и и/или для восстановления метки времени и значений четырех каналов. В конце этого цикла вы можете записать эти значения в виде одной строки с разделителями табуляции, соответствующей текущему двоичному входному вектору. Переберите все векторы, чтобы получить полный файл данных с разделителями табуляцией.

Вы можете поэкспериментировать с различными значениями delayMicrosecond, чтобы получить разные частоты дискретизации, и вы можете рассчитать нашу скорость передачи данных, чтобы опережать выборку, чтобы программа теоретически могла работать бесконечно без резервного копирования последовательного буфера на стороне Arduino. Каждый объект AnalogRead и Serial Arduino имеет свою максимальную скорость, которую вы можете найти экспериментальным путем.

,

1) Вы написали: «_Вы НЕ хотите ничего делать с объектом Serial в setup(), кроме его настройки. (Для этого есть скрытая причина.)_». Не могли бы вы уточнить? 2) Вы не можете получить хороший контроль над частотой дискретизации с помощью delayMicroseconds(), потому что время цикла равно задержке плюс время, необходимое для выполнения кода. Использование micros(), как и в ответе @frarugi87, является стандартным методом, и он работает хорошо. Только аналоговое считывание, запускаемое по таймеру, даст вам лучшую точность (меньше джиттера)., @Edgar Bonet