Arduino: считывание частоты входного сигнала с аудиоразъема

Я хочу запустить arduino от звука из аудиоразъема мобильного телефона. Звук — это звук частотой 1 кГц, который будет воспроизводиться мобильным телефоном. Я хочу избежать случайного запуска звука, проверив именно эту частоту.

Я нашел несколько руководств по аналоговому считыванию, но выходные данные не в герцах.

Как получить выходной сигнал в Герцах?

int incomingAudio;

void setup(){
    Serial.begin(9600);  
}

void loop(){
  incomingAudio = analogRead(A0);//считывание напряжения на А0
  incomingAudio = (incomingAudio+1)/4 - 1;//масштабирование от 10 бит (0-1023) до 8 бит (0-255)
  if (incomingAudio<0){//обрабатывать отрицательные числа
    incomingAudio = 0;
  }
  PORTD = incomingAudio;
  Serial.println(PORTD);

}

, 👍2


2 ответа


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

6

Вы можете измерить одну "чистую" частоту, подсчитав количество раз, когда штифт меняется, используя встроенные таймеры. Вот эскиз с моей страницы о таймерах, который выполняет это:

// Пример таймера и счетчика
// Автор: Ник Гэммон
// Дата: 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);
  }   // конец цикла

Подавая синусоиду частотой 1 кГц (от 0 В до 5 В), я получаю следующий результат:

Frequency Counter
Frequency: 1000 Hz.
Frequency: 1002 Hz.
Frequency: 1000 Hz.
Frequency: 1000 Hz.
Frequency: 1000 Hz.
Frequency: 1000 Hz.
Frequency: 1002 Hz.
Frequency: 1000 Hz.
Frequency: 1000 Hz.
Frequency: 1000 Hz.

Иногда бывают неточности, но выглядит достаточно точно. Вы можете сделать выборку несколько раз, и если вы получите что-то между 990 и 1010 пять раз, вы можете считать это совпадением.

Вам нужно убедиться, что входной сигнал находится в диапазоне для срабатывания входной схемы Arduino. Он должен быть около 4–5 В и не должен превышать 5 В (для высокого уровня). Он также не должен быть отрицательным.

Что-то вроде этого защитит входной контакт:

Защита ввода

Предупреждение - звук из аудиоразъема будет переменным и станет отрицательным. Убедитесь, что вы используете подходящую защиту входа.


Усиление

Посмотрев Википедия - Уровень линии, можно предположить, что выходной сигнал вашего телефона будет недостаточно высоким (вероятно, около 700 мВ). Поэтому, вероятно, потребуется простой транзисторный усилитель, чтобы поднять его до уровня, который можно будет обнаружить.

Вдохновленный этим постом на Electronics Stack Exchange, я сконструировал простой усилитель:

Схема усилителя звука

Тестирование с пиковым входным сигналом около 500 мВ показало, что он успешно усилил его до 0–5 В постоянного тока:

Трассировка осциллограммы усилителя звука


Альтернативный метод с аналоговым компаратором

Основываясь на предложениях Криса Стрэттона, есть другой способ сделать это.

Вы можете использовать аналоговый компаратор для обнаружения аналогового сигнала, проходящего пороговое значение, например:

Схема аналогового компаратора

Я настроил делитель напряжения на срабатывание при 0,265 В, так что сигнал 500 мВ должен обеспечить приемлемый диапазон срабатывания.

Что касается эскиза, то на этих частотах мы можем сделать что-то проще, чем то, что я выкладывал ранее:

volatile bool counting;
volatile unsigned long count;

ISR (ANALOG_COMP_vect)
  {
  if (counting)
    count++;
  }

void setup ()
  {
  Serial.begin (115200);
  Serial.println ("Started.");
  ADCSRB = 0;           // (Отключить) ACME: Включение аналогового компаратора-мультиплексора
  ACSR =  bit (ACI)     // (Очистить) Флаг прерывания аналогового компаратора
        | bit (ACIE)    // Разрешение прерывания аналогового компаратора
        | bit (ACIS1);  // ACIS1, ACIS0: Выбор режима прерывания аналогового компаратора (запуск по заднему фронту)
   }  // конец настройки

unsigned long startTime;

void loop ()
  {

  if (!counting)
    {
    startTime = micros ();
    count = 0;
    counting = true;
    }
  else
    {
    // прошла ли секунда? (1000000 микросекунд)
    if (micros () - startTime >= 1000000)
      {
      counting = false;
      Serial.print (count);
      Serial.println (F(" Hz."));
      }  // конец секунды вверх

    }  // конец if

  }  // конец цикла
,

5

Если ваш сигнал — это чистый тон 1 кГц, то вам следует использовать Nick Метод подсчета Гаммона, так как это наиболее прямолинейное решение. Однако если вы принимаете сигнал через микрофон, скорее всего, вы также услышите некоторый фоновый шум, и это может сделать метод подсчета ненадежен.

В таком случае вам придется использовать какой-то цифровой фильтр, чтобы извлечь нужный сигнал из шума. Я бы рекомендовал использовать обнаружение гомодина, который является одновременно простым и очень устойчивым к шуму широкого спектра: пусть f = 1 кГц — частота, которую вы хотите обнаружить, а t быть текущим временем, тогда

  • вычислить cos(2 π f t ) и sin(2 π f t ): эти два периодических сигнала, в квадратуре друг к другу, называются «локальным генератором»
  • умножьте свой сигнал независимо на указанные выше cos() и sin()
  • фильтр нижних частот двух продуктов, что дает вам I и Q демодулированные сигналы
  • вычислите мгновенную мощность = I2 + Q2.

Вы знаете, что телефон звонит, когда эта мощность превышает некоторое значение экспериментально определенный порог.

Я знаю, что все это звучит как сложная математика, но это не обязательно так:

  • Все вычисления поддаются эффективному использованию фиксированной точки. реализации, и вы можете выполнять вычисления параллельно с аналого-цифровое преобразование.
  • Схема работает даже с грубыми приближениями sin() и cos() функции. Даже квадратные волны (sin() и cos() всегда +1 или −1) работают вполне прилично.
  • Фильтр нижних частот может быть реализован как экспоненциальное перемещение средний, что представляет собой всего лишь сдвиг бита, сложение и вычитание.

Я могу попробовать написать пример кода, но мне придется повозиться только если есть интерес. Если ваш сигнал настолько чист, что Ник Гаммон метод работает надежно, смысла, наверное, нет.


РЕДАКТИРОВАНИЕ: Я написал программу, демонстрирующую этот подход, просто для весело. Доступно как GitHub gist: homodyne.ino. Я не копирую сюда всю программу, а просто добавляю некоторые пояснения:

Первое, что нужно сделать, это позаботиться о частоте дискретизации. Всякий раз, когда один хочет сделать обработку сигнала, очень желательно производить выборку на фиксированной частоте и известная частота. Этого почти невозможно достичь путем зацикливания по сравнению с analogRead(), поскольку процессорное время, необходимое для прохождения цикла трудно предсказать, так как на него могут влиять прерывания и условные переходы. analogRead() также крайне неэффективен, так как он блокирует ЦП в цикле занятости, пока он ожидает аналого-цифрового преобразования преобразователь (АЦП) для выполнения своей работы.

Решение состоит в том, чтобы перевести АЦП в так называемый «режим свободного хода». этот режим, он начинает преобразование, как только предыдущее завершено завершено, что дает очень стабильную частоту дискретизации 9,615 кГц (один выборка каждые 104 мкс). Выборки обрабатываются прерыванием служебная процедура, называемая ISR(ADC_vect), которая запускается каждый раз, когда Образец готов. Здесь же происходит вся обработка сигнала.

Ниже приведено подробное объяснение каждой строки кода в ISR:

int8_t sample = ADCH - 128;

Чтение результата АЦП — это первое, что нужно сделать в ISR. Поскольку мы делаем не требуется полное 10-битное разрешение, АЦП был настроен на скорректируем его результат влево. Затем мы получаем 8-битный результат, просто считывая старший байт регистра данных АЦП. Мы вычитаем 128, чтобы удалить DC смещение и получение знакового числа в диапазоне [−128 .. +127].

Далее мы обновляем фазу нашего локального осциллятора:

static uint16_t phase;
phase += PHASE_INC;

Нам не нужно беспокоиться о возможных переполнениях: поскольку фаза сохраняется единицы по 1/216 циклов, приращение работает автоматически по модулю один цикл. Теоретически фазовый прирост должен быть 6815,744, что округляется до 6816. Это делает локальный осциллятор быстрым на 0,0376 Гц, что незначительно, учитывая точность Arduino Часы. Фаза может храниться в 8-битной переменной, но округление Ошибка тогда ускорит осциллятор на 14 Гц, что может быть граничит с неприемлемым.

Далее нам нужно построить две прямоугольные волны в квадратуре, которые будут использоваться как грубые приближения sin() и cos(). Но поскольку большинство значительная часть фазы включена в течение половины цикла, это может быть используется как одна из наших квадратных волн. Другая волна получается в аналогичном кстати, после добавления 1/4 цикла к фазе. Вот как мы умножаем наши выборка по квадратным волнам:

int8_t x = sample;
if (((phase>>8) + 0x00) & 0x80) x = -1 - x;
int8_t y = sample;
if (((phase>>8) + 0x40) & 0x80) y = -1 - y;

Выражение phase>>8 предназначено только для того, чтобы намекнуть компилятору, что оно нужно только рассмотреть самый значимый байт фазы. Это Обычно это не требуется, но я заметил, что без этой подсказки gcc генерирует неоптимальный код. -1 в строках выше предназначены для избегая возможного переполнения.

Далее идет фильтр нижних частот:

signal_I += x - (signal_I >> LOG_TAU);
signal_Q += y - (signal_Q >> LOG_TAU);

Где signal_I и signal_Q являются volatile int16_t глобальными переменными, и LOG_TAU равен 6. Это фильтрация с постоянной времени

τ = 64 × 104 мкс = 6,656 мс

что соответствует полосе пропускания −3 дБ

Δf = 1/(2πτ) = 23,9 Гц.

что довольно узко для сигнала 1 кГц. Значимость τ что, как только начинается тон, фильтру требуется около 2,3 τ ≈ 15 мс для достижения 90% от его конечного выхода. Оба τ и Δf можно изменить, просто изменив LOG_TAU.

Эта процедура ISR занимает около 176 циклов ЦП (11 мкс, включая 4 цикла, необходимые для перевода ЦП в режим прерывания) каждый 1664 цикла (104 мкс). Это составляет около 10,6% от доступная мощность ЦП. И поскольку все делается внутри ISR, программа может свободно делать все, что необходимо, пока данные Получение данных и цифровая фильтрация происходят в фоновом режиме.

Я протестировал программу, отправив ей синусоиду напряжением 4,36 В. Амплитуда от пика до пика. Сообщаемая мощность колебалась довольно сильно, но вот приблизительные средние показания в зависимости от входного сигнала частота:

frequency  |  power
-----------+-------
   929 Hz  |   480
   976 Hz  |  2400
  1005 Hz  |  4600
  1028 Hz  |  2700
  1079 Hz  |   480
,