Регистрация частоты с помощью фотодиода

Я только что купил следующий фотодиод (здесь) и пытаюсь создать простую схему/код Arduino, чтобы фотодиод мог регистрировать мигание светодиода с произвольной частотой обновления.

У меня есть Arduino Uno, управляющий датчиком, и Raspberry Pi, управляющий светодиодом на заданной частоте.

Мне нужна помощь, так как мои познания в обработке сигналов минимальны, и я буду признателен за любые отзывы или советы о том, правильно ли я делаю или нет.

Вот код Arduino:

КОД ОБНОВЛЕН!!

//#include <TimerOne.h>
#include <Arduino.h>

const unsigned long ONESEC = 1000000; // микросекунды
const int size = 600;
int sensorValue[size];
int dly = 1000000 / size; // задержка, чтобы заполнить N выборок в течение 1 полной секунды (1 000 000 микросекунд)



int maxValue = 0;
int minValue = 1000000;
double Alpha = 0.1; // [0.0 - 1.0] весовой параметр
double filteredValue = 0.0;
double prevValue = 0.0;
double avgsum = 0.0;

int total = 0;
int avgtotal = 0;
const int s = 10;
int rounds = size / s;
int tmparr[s];

const int btnPin = 7;
const int sensorPin = A0;  // вывод датчика


void setup() { 
  Serial.begin(57600);
  
  // инициализация tmparr
  for(int i=0;i<s;i++){
    tmparr[i] = 0;
  }
}


void waitForButton(int btnPin){
  Serial.println("Waiting...");
  while(digitalRead(btnPin) != 1){
    continue;
    }
  Serial.println("Button has been pressed!");
}

void loop() {  // код, который выполняет бесконечный цикл

  int i = 0;
  unsigned long curTime = micros();
  unsigned long prevTime, startTime;
  prevTime = curTime;
  startTime = curTime;
  while(curTime - startTime <= ONESEC){ // если время между текущим и начальным временем меньше 1 секунды, продолжайте записывать данные
    if(curTime - prevTime < dly){ // если текущая и предыдущая временная метка имеют разницу менее N секунд, ничего не записываем
      curTime = micros();
      continue;
    }
    sensorValue[i++] = analogRead(sensorPin);
    
    if(i > size){
      break;
    }
    prevTime = curTime;
    curTime = micros();
  }
  
  ///////////////////////
  ///////////////////////
  // Обработка данных ////
  ///////////////////////
  ///////////////////////

  // фильтруем сигнал и находим максимум/минимум и среднее значение
  
  for(int i=0;i<size;i++){
    int v = sensorValue[i];
    filteredValue = (v * Alpha) + (prevValue * (1-Alpha)); // экспоненциальная фильтрация
    sensorValue[i] = filteredValue; // перезапись данных датчика !! НЕОБРАБОТАННЫЕ ДАННЫЕ ПЕРЕЗАПИСЫВАЮТСЯ !!
    avgsum = avgsum + filteredValue;
    prevValue = filteredValue;

    if(maxValue < filteredValue){
      maxValue = filteredValue;
    }
    if(minValue > filteredValue){
      minValue = filteredValue;
    }
  }
  avgsum = avgsum / size;

  // построение графика и выделение сигнала
  int peaks = 0;
  int prevState = -1;

  for(int i=0;i<rounds;i++){
    for(int j=0;j<s;j++){
      int v = sensorValue[(i*s)+j];
      total = total - tmparr[j];
      total = total + v;
      avgtotal = total / s;
      tmparr[j] = v;
      Serial.print(avgtotal); // новое отфильтрованное значение
      Serial.print(",");
      Serial.print(avgsum);
      Serial.print(",");
      Serial.println(v); // отфильтрованное значение

      if(avgtotal > avgsum + (avgsum*0.01)){
        // Serial.println(1);
        if(prevState != 1){
          peaks++;
          prevState = 1;
        }
      }
      else{
        // Serial.println(0);
        prevState = 0;
      }
    }
  }
  Serial.print("peaks:");
  Serial.println(peaks);

  

}

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

Есть ли у вас какие-либо советы о том, что мне следует делать и как мне следует действовать, чтобы сделать данные более надежными и точными?


Обновление 1:

Вот код Пи:

import RPi.GPIO as GPIO # Import Raspberry Pi GPIO library
from time import sleep # Import the sleep function from the time module

GPIO.setwarnings(False) # Ignore warning for now
GPIO.setmode(GPIO.BOARD) # Use physical pin numbering
GPIO.setup(8, GPIO.OUT, initial=GPIO.LOW) # Set pin 8 to be an output pin and set initial value to low (off)

freq = 5.0 # Hz
cycle = 1.0/freq
hcycle = cycle / 2.0

while True: # Run forever
 GPIO.output(8, GPIO.HIGH) # Turn on
 sleep(hcycle) # Sleep for N second
 GPIO.output(8, GPIO.LOW) # Turn off
 sleep(hcycle) 

Обновление 2:

После обновления кода, для очень низких частот я получаю довольно хорошо сформированный сигнал

пример сигнала 5 Гц

Все становится сложнее с высокой частотой (не такой уж высокой) на 30 Гц, например, когда попытка разделить сигнал на основе общей средней линии не дает информативной ценности для того, сколько пиков и впадин есть, поскольку несколько пиков могут быть под линиями, а несколько впадин могут быть над линией. Здесь вывод показывает 25 пиков.

поэтому я начинаю думать, что техника фильтрации, которую я использую, возможно, не самая лучшая. Есть предложения?

, 👍0


2 ответа


2

Рассмотрите этот цикл:

for(int i=0; i< size;i++){
  // Считать значение датчика
  sensorValue[i] = analogRead(sensorPin);
  // ожидание между считываниями N микросекунд
  delayMicroseconds(dly);
}

Время, затраченное на каждую итерацию, представляет собой сумму времени, затраченного на каждую итерацию. отдельная операция (сравнение i с size, чтение АЦП, задержка, увеличить i и перейти к началу цикла). За исключением задержки, самая длинная операция здесь - analogRead(), так как она блокируется в занятом цикл, пока АЦП выполняет свое преобразование. Это может занять любое время между 104 мкс и 112 мкс. Даже пренебрегая всем, кроме analogRead() и delayMicroseconds(), вы ожидаете, что ваш цикл будет около 6–7% слишком медленно.

Вы должны получить лучшие тайминги, если используете micros() вместо delayMicroseconds(). Проверьте учебник Arduino «Мигание без задержки» чтобы увидеть, как вы можете управлять своим временем без задержки. Вы все равно не будете однако получите синхронизацию метрологического класса, поскольку micros() хорош лишь настолько, насколько хорош Керамический резонатор тактирует ваш Uno, который должен быть примерно 0,1% точный по своей частоте. Если вам нужно что-то лучшее, вам придется найти лучший стандарт частоты.


Изменить: Отвечая на обновленный вопрос.

Есть несколько вопросов, на которые я хотел бы обратить внимание:

Во-первых, способ, которым обновленный код обрабатывает время получения, немного запутанно. Более того, обновление времени с помощью prevTime = curTime; склонен к дрейфу времени: нельзя рассчитывать на то, что curTime будет точно время, на которое было запланировано считывание АЦП, может быть несколько микросекунд позже. На самом деле, curTime всегда будет позже, так как Период сбора данных (1666 мкс) не кратен micros() разрешение (4 мкс). Этот дрейф времени можно избежать, сделав prevTime представляет время, когда было сделано предыдущее приобретение запланировано (не когда было выполнено):

// Сбор данных.
unsigned long prevTime = micros();
for (int i = 0; i < size; i++) {
    while (micros() - prevTime < dly) continue;  // ждем, пока не придет время
    prevTime += dly;                         // обновить запланированное время
    sensorValue[i] = analogRead(sensorPin);  // получить данные
}

Ограниченное разрешение micros() все равно будет вызывать некоторое дрожание, но обновление с помощью prevTime += dly; гарантирует отсутствие систематического дрейфа.

Во-вторых, я бы не стал выполнять какую-либо фильтрацию на самом Arduino. Если данные в любом случае предполагается перенести на ПК, просто перенесите необработанные данные. Это дает вам возможность обрабатывать их на вашем ПК, интерактивно экспериментируя с различными типами фильтров, с удобство вашего любимого языка программирования или обработки данных пакет. Если весь проект требует, чтобы фильтрация была сделана на Arduino, сделайте это позже, когда найдете наиболее подходящий фильтр на ПК.

В-третьих, при фильтрации у вас, похоже, много шума 50 Гц здесь. Может быть, шум уже присутствует в световом потоке, а может быть он вводится где-то между фотодиодом и Arduino. Я проверил бы кабели, чтобы увидеть, есть ли способ уменьшить индуктивный датчик шума. Вы используете длинные кабели между фотодиодом и Arduino? Если да, то это витая пара? Все правильно заземлен? Есть ли контуры заземления?

Если вы не можете избавиться от шума сети в источнике, это будет очень трудно идентифицировать сигналы, которые близки к 50 Гц, так как Ваш эксперимент на 30 Гц показывает. Вы можете попробовать режекторный фильтр специально настроен на подавление любой составляющей частотой 50 Гц.

В-четвертых, вы можете попробовать хотя бы один раз приобрести более быстрыми темпами: скажем, с периодом 200 мкс. Если это приобретение раскрывает много высокочастотного шума, то, возможно, стоит использовать некоторые децимирующие фильтр. Например, вы можете хранить в каждой позиции массива сумму восемь образцов, взятых с интервалом 200 мкс. Это должно дать меньше зашумленные данные, с периодом 1,6 мс на ячейку массива (круглое значение близко к текущему значению 1,666 мс). Обратите внимание, что этот метод будет Хотя он абсолютно бесполезен против шума частотой 50 Гц.

В-пятых, можно запускать АЦП от таймера, что дает очень точный и последовательный тайминг. Однако это требует низкого уровня программирование и копание в ATmega datasheet. Мне было бы все равно об идеальных сроках сейчас, по крайней мере, пока не будут решены вышеуказанные проблемы фиксированный. Однако, это возможность, которую вы можете иметь в виду в на случай, если в будущем вы захотите получить наилучшие возможные тайминги.

,

2

Подсчет в таком цикле хорош, если у вас очень низкочастотный сигнал, и вы осваиваете, как работают циклы и считывание с входных контактов. Но будет много накладных расходов. Как указывает Эдгар Бонет в своем ответе, накладные расходы могут составлять от 104 мкс до 112 мкс для выполнения вызова analogRead. Плюс накладные расходы цикла. И задержка не будет точной до микросекунды. Все это суммируется.

Есть два способа узнать частоту:

  1. Считайте «тики» за известный интервал. Например, подсчет 50 тиков за секунду даст вам 50 Гц.

  2. Измерьте период. Другими словами, узнайте, сколько времени проходит между нарастающим и нисходящим фронтами (и удвойте это время). Например, для 50 Гц период будет 20 мс (1 / 50).

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

Считать импульсы

// Пример таймера и счетчика
// Автор: Ник Гэммон
// Дата: 17 января 2012 г.

// Вход: контакт D5

// они проверяются в основной программе
volatile unsigned long timerCounts;
volatile boolean counterReady;

// внутренний для подсчета процедуры
unsigned long overflowCount;
unsigned int timerTicks;
unsigned int timerPeriod;

void startCounting (unsigned int ms) 
  {
  counterReady = false;         // время еще не вышло
  timerPeriod = ms;             // сколько отсчетов по 1 мс нужно сделать
  timerTicks = 0;               // сбросить счетчик прерываний
  overflowCount = 0;            // переполнения пока нет

  // сбросить Таймер 1 и Таймер 2
  TCCR1A = 0;             
  TCCR1B = 0;              
  TCCR2A = 0;
  TCCR2B = 0;

  // Таймер 1 - считает события на выводе D5
  TIMSK1 = bit (TOIE1);   // прерывание по переполнению таймера 1

  // Таймер 2 - дает нам интервал подсчета в 1 мс
  // Тактовая частота 16 МГц (62,5 нс на тик) - предварительно масштабировано на 128
  // счетчик увеличивается каждые 8 мкс.
  // Итак, мы насчитали 125 из них, что дает ровно 1000 мкс (1 мс)
  TCCR2A = bit (WGM21) ;   // Режим CTC
  OCR2A  = 124;            // считаем до 125 (относительно нуля!!!!)

  // Таймер 2 - прерывание при совпадении (т.е. каждую 1 мс)
  TIMSK2 = bit (OCIE2A);   // включить прерывание Таймера 2

  TCNT1 = 0;      // Оба счетчика равны нулю
  TCNT2 = 0;     

  // Сброс предделителей
  GTCCR = bit (PSRASY);        // сбросить предделитель сейчас
  // запустить Таймер 2
  TCCR2B =  bit (CS20) | bit (CS22) ;  // предделитель 128
  // запустить Таймер 1
  // Внешний источник синхронизации на выводе T1 (D5). Синхронизация по нарастающему фронту.
  TCCR1B =  bit (CS10) | bit (CS11) | bit (CS12);
  }  // конец начала подсчета

ISR (TIMER1_OVF_vect)
  {
  ++overflowCount;               // подсчитываем количество переполнений Counter1
  }  // конец TIMER1_OVF_vect


//******************************************************************
// Служба прерывания Таймера2 вызывается аппаратным Таймером 2 каждую 1 мс = 1000 Гц
// 16МГц / 128 / 125 = 1000 Гц

ISR (TIMER2_COMPA_vect) 
  {
  // захватываем значение счетчика до того, как оно изменится
  unsigned int timer1CounterValue;
  timer1CounterValue = TCNT1;  // см. техническое описание, стр. 117 (доступ к 16-битным регистрам)
  unsigned long overflowCopy = overflowCount;

  // смотрим, достигли ли мы временного периода
  if (++timerTicks < timerPeriod) 
    return;  // пока нет

  // если просто пропустил переполнение
  if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 256)
    overflowCopy++;

  // окончание времени ворот, измерение готово

  TCCR1A = 0;    // остановить таймер 1
  TCCR1B = 0;    

  TCCR2A = 0;    // остановить таймер 2
  TCCR2B = 0;    

  TIMSK1 = 0;    // отключить прерывание Таймера 1
  TIMSK2 = 0;    // отключить прерывание Timer2
    
  // подсчитать общее количество
  timerCounts = (overflowCopy << 16) + timer1CounterValue;  // каждое переполнение на 65536 больше
  counterReady = true;              // установить глобальный флаг для окончания периода подсчета
  }  // конец TIMER2_COMPA_vect

void setup () 
  {
  Serial.begin(115200);       
  Serial.println("Frequency Counter");
  } // конец настройки

void loop () 
  {
  // остановить прерывания таймера 0 от сброса отсчета
  byte oldTCCR0A = TCCR0A;
  byte oldTCCR0B = TCCR0B;
  TCCR0A = 0;    // остановить таймер 0
  TCCR0B = 0;    
  
  startCounting (500);  // сколько мс нужно посчитать

  while (!counterReady) 
     { }  // цикл, пока не закончится

  // отрегулируйте счетчики, установив интервал подсчета, чтобы получить частоту в Гц
  float frq = (timerCounts *  1000.0) / timerPeriod;

  Serial.print ("Frequency: ");
  Serial.print ((unsigned long) frq);
  Serial.println (" Hz.");
  
  // перезапустить таймер 0
  TCCR0A = oldTCCR0A;
  TCCR0B = oldTCCR0B;
  
  // пусть серийные вещи закончатся
  delay(200);
  }   // конец цикла

На самом деле это не очень хорошо для низких частот, потому что если счет отличается хотя бы на один тик, ваша частота будет неправильной на 2%. Однако он может считать точно (более или менее) до 5 кГц.

Измерьте период

Другой метод — измерить период, вот так:

// Частотный таймер
// Автор: Ник Гэммон
// Дата: 10 февраля 2012 г.

// Вход: контакт D2

volatile boolean first;
volatile boolean triggered;
volatile unsigned long overflowCount;
volatile unsigned long startTime;
volatile unsigned long finishTime;

// здесь на переднем крае
void isr () 
{
  unsigned int counter = TCNT1;  // быстро сохраним его
  
  // подождем, пока мы не заметили последний
  if (triggered)
    return;

  if (first)
    {
    startTime = (overflowCount << 16) + counter;
    first = false;
    return;  
    }
    
  finishTime = (overflowCount << 16) + counter;
  triggered = true;
  detachInterrupt(0);   
}  // конец isr

// переполнение таймера (каждые 65536 отсчетов)
ISR (TIMER1_OVF_vect) 
{
  overflowCount++;
}  // конец TIMER1_OVF_vect


void prepareForInterrupts ()
  {
  // будьте готовы к следующему разу
  EIFR = bit (INTF0);  // очистить флаг для прерывания 0
  first = true;
  triggered = false;  // переподготовка для следующего раза
  attachInterrupt(0, isr, RISING);     
  }  // конец prepareForInterrupts
  

void setup () 
  {
  Serial.begin(115200);       
  Serial.println("Frequency Counter");
  
  // сбросить Таймер 1
  TCCR1A = 0;
  TCCR1B = 0;
  // Таймер 1 - прерывание при переполнении
  TIMSK1 = bit (TOIE1);   // включить прерывание Таймера 1
  // обнулить его
  TCNT1 = 0;  
  overflowCount = 0;  
  // запустить Таймер 1
  TCCR1B =  bit (CS10);  // без предварительного масштабирования

  // настроено для прерываний
  prepareForInterrupts ();   
  
  } // конец настройки

void loop () 
  {

  if (!triggered)
    return;
 
  unsigned long elapsedTime = finishTime - startTime;
  float freq = F_CPU / float (elapsedTime);  // каждый тик составляет 62,5 нс при 16 МГц
  
  Serial.print ("Took: ");
  Serial.print (elapsedTime);
  Serial.print (" counts. ");

  Serial.print ("Frequency: ");
  Serial.print (freq);
  Serial.println (" Hz. ");

  // чтобы мы могли это прочитать
  delay (500);

  prepareForInterrupts ();   
}   // конец цикла

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

,