Регистрация частоты с помощью фотодиода
Я только что купил следующий фотодиод (здесь) и пытаюсь создать простую схему/код 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 пиков.
поэтому я начинаю думать, что техника фильтрации, которую я использую, возможно, не самая лучшая. Есть предложения?
@wisdom, 👍0
Обсуждение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. Мне было бы все равно об идеальных сроках сейчас, по крайней мере, пока не будут решены вышеуказанные проблемы фиксированный. Однако, это возможность, которую вы можете иметь в виду в на случай, если в будущем вы захотите получить наилучшие возможные тайминги.
спасибо за совет. Я только что обновил код, используя предложенный метод для захвата данных в течение 1 секунды. Но мой главный вопрос о том, как обрабатывать записанный сигнал, чтобы сделать из него информативное чтение, все еще сбивает меня с толку, @wisdom
@wisdom Это не совсем форум, и внесение существенных правок в ваш вопрос в ответ на ответы — это попытка превратить его в таковой. Я думаю, что на ваш изначальный вопрос был дан ответ, и дальнейшие вопросы о таких вещах, как обработка сигнала, действительно должны быть преобразованы в новые вопросы сами по себе. В противном случае эта конкретная ветка станет довольно запутанной для чтения. Исправленные вопросы / исправленные ответы не способствуют хорошему потоку чтения., @Nick Gammon
Подсчет в таком цикле хорош, если у вас очень низкочастотный сигнал, и вы осваиваете, как работают циклы и считывание с входных контактов. Но будет много накладных расходов. Как указывает Эдгар Бонет в своем ответе, накладные расходы могут составлять от 104 мкс до 112 мкс для выполнения вызова analogRead. Плюс накладные расходы цикла. И задержка не будет точной до микросекунды. Все это суммируется.
Есть два способа узнать частоту:
Считайте «тики» за известный интервал. Например, подсчет 50 тиков за секунду даст вам 50 Гц.
Измерьте период. Другими словами, узнайте, сколько времени проходит между нарастающим и нисходящим фронтами (и удвойте это время). Например, для 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 ();
} // конец цикла
Это лучше для низких частот. Поскольку здесь используются аппаратные таймеры, вам нужно подключить вход к определенному контакту, как указано в коде. Не нужно выполнять аналоговые считывания, время, необходимое для этого, сведет на нет любую точность, которую вы могли бы получить.
В вашей функции isr()
есть гонка: если таймер переполнится прямо перед тем, как вы прочитаете TCNT1
, TIMER1_OVF_vect
не будет вызван сразу (потому что мы уже находимся в контексте прерывания), и вы пропустите переполнение., @Edgar Bonet
оба кода прекрасно работают для светодиодов (хотя, как я заметил, погрешность составляет до 6%).... но, возможно, я не совсем ясно выразил свои намерения, ведь я хочу использовать фотодиодный датчик для считывания частот с дисплеев в конце, а светодиод я использую пока только как средство тестирования и экспериментов по разработке кода., @wisdom
также мне интересна форма волны, как сигнал растет и падает независимо от источника света (например, монитор ПК, светодиод, проектор и т. д.). Поэтому мне нужно использовать фотодиод и для этого, @wisdom
@EdgarBonet Ты имеешь в виду во втором фрагменте опубликованного кода? Похоже, ты прав. У меня был тест в TIMER2_COMPA_vect
в первом фрагменте кода. Ты можешь добавить этот тест, где написано if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 256) ...
во второй., @Nick Gammon
@wisdom Измерения должны быть точнее 6%, возможно, тестовая частота, которую вы отправляете, не так уж и точна. Вы снова используете цикл , а не частоту, сгенерированную аппаратно, так что накладные расходы цикла, скорее всего, повлияют на общий результат. Вам действительно нужен правильный сигнал, сгенерированный аппаратно, для тестирования, а не код, работающий на процессоре с использованием цикла while
., @Nick Gammon
хорошо, понятно... последний вопрос: могу ли я каким-либо образом запустить аппаратное считывание сигнала для фотодиодного датчика? Если да, то не могли бы вы указать мне направление?, @wisdom
Эм, кроме кода, который я разместил выше? Мои тесты этого (первого) дали ошибку 0,02%. Разве этого недостаточно?, @Nick Gammon
Я понял, что ваш код должен использоваться вместо фотодиода для оценки частоты входящего сигнала (т. е. мигания светодиода), и когда я подключил другой конец светодиода к упомянутому PIN в вашем коде, ваш код дает мне точное показание частоты мигания светодиода. Но я не был уверен, как использовать сигнал фотодиода вместо входа вместо светодиода напрямую, чтобы последовательность была (LED >> Photodiode >> Arduino Pin), поскольку мне нужно использовать фотодиод, в этом и заключается вся цель вопроса., @wisdom
Немного оборудования, которое может превратить фотодиод в сигнал включения/выключения?, @Nick Gammon
- Максимальная частота цифрового сигнала в Arduino Uno?
- Генерировать 1,7 МГц с PWM в Uno?
- Можно ли сгенерировать точный тактовый импульс 15 кГц с помощью ардуино?
- Изменение частоты вывода ШИМ на Arduino Uno
- Как увеличить громкость динамика с помощью библиотеки Talkie в Arduino Uno...?
- Проблема по осуществлению ультразвукой схемы
- создание анализатора гармоник мощности, который будет измерять амплитуды основной и кратных ей частот (например, 50 Гц, 100 Гц, 150 Гц, 200 Гц,...)
- Выход частоты FG
Если вы видите 1 дополнительный пик на 15 Гц, а затем 3 дополнительных на 50 Гц (3x15 ~ 50), то, по крайней мере, это согласованно. Вы не опубликовали свой код Pi, который может иметь или не иметь проблему. Вы уверены, что Pi мигает с частотой 15 Гц, а не 16 Гц? Может быть, попробуйте 5 Гц, чтобы увидеть, будет ли это точнее, а затем медленно увеличивайте частоту. Вы пробовали мигать светодиодом с помощью Arduino (возможно, с помощью прерываний), чтобы увидеть, изменится ли количество пиков (или станет ли оно более точным)?, @Greenonline
Я только что обновил вопрос, добавив некоторую информацию, проверьте, пожалуйста. Я новичок во всех этих схемах и не был уверен, как использовать светодиод и датчик одновременно для измерения мигания светодиода, поскольку оба они будут последовательны в одном цикле (), поэтому я решил использовать другую схему для управления светодиодом., @wisdom
Не могли бы вы обновить единицы измерения на рисунках? Вы уверены, что делаете измерения каждую секунду? Или какая у вас шкала времени?, @Nick S.
Зачем вы делаете аналоговое считывание на том, что по сути является цифровым входом? Светодиод горит или нет, не так ли?, @Nick Gammon
@NickGammon Меня не интересует светодиод как таковой, он здесь только как средство проверки того, как правильно считывать показания с помощью фотодиодного датчика с аналоговым входом., @wisdom
@NickS. Я только что обновил код, используя другую стратегию для сбора данных ТОЧНО за одну секунду. Пожалуйста, посмотрите, @wisdom
Что мы здесь видим? Что это за красная линия?, @Nick Gammon
Легенда указана на рисунке выше в соответствующем цвете., @wisdom