В этой статье я продолжу воплощать свое вдохновение лабораторной работой №3 уже в железе. Речь пойдет о детектировании цифры по звуку в тоновом режиме набора на Arduino с помощью алгоритма Герцеля.

Для реализации задуманного я использовал Arduino UNO, электретный микрофон (adafruit) и дисплей 8х8 с драйвером MAX7219.

План действий


  • Дискретизировать достаточное количество отсчетов (с помощью программы из предыдущей статьи я убедился, что 256 достаточно).
  • Найти амплитуды АЧХ, соответствующие искомым частотам, кодирующим символы.
  • Два максимальных значения амплитуды дадут индексы строки и столбца искомого символа, так, например, выглядит цифра 3.
    image

Реализация


Перед тем как браться за реализацию, ответим на вопрос — хватит ли нам производительности Arduino UNO?

Тактовая частота: 16МГц
Один цикл дискретизации занимает 13 тактов
Значение прескейлера, обеспечивающего наибольшую точность: 128

Получается 16МГц / 13 / 128 ~ 9615Гц — искомая частота дискретизации, значит, можно работать с частотами до 4.8кГц.

Настройка АЦП


Есть несколько режимов работы АЦП, ниже приведены наиболее интересные (полный список в datasheet по ключевому слову ADCSRB)

  • single read — с помощью метода analogRead(), но это блокирующий вызов, который занимает 100µs, и используя его невозможно обеспечить постоянную частоту дискретизации
  • free-run mode — в этом режиме следующий цикл дискретизации начинается сразу после окончания предыдущего и обеспечивается максимальная частота дискретизации
  • timer overflow — дискретизация начинается по переполнению таймера, это позволяет точно настроить частоту дискретизации

Код настройки АЦП, для простоты я использовал free-run mode.

ADMUX  = 0; // Channel sel, right-adj, use AREF pin
ADCSRA = _BV(ADEN)  | // ADC enable
         _BV(ADSC)  | // ADC start
         _BV(ADATE) | // Auto trigger
         _BV(ADIE)  | // Interrupt enable
         _BV(ADPS2) | _BV(ADPS1) | _BV(ADPS0); // 128:1 / 13 = 9615 Hz
ADCSRB = 0; // Free-run mode
DIDR0  = _BV(0); // Turn off digital input for ADC pin      
TIMSK0 = 0;                // Timer0 off

Обработка сигнала


Как только наберется полный массив сэмплов, выключаем прерывание по АЦП и вычисляем амплитуды спектра с помощью алгоритма Герцеля. Не буду соперничать в описании алгоритма с этим исчерпывающим ресурсом, но приведу свою реализацию:

void goertzel(uint8_t *samples, float *spectrum) {
  float v_0, v_1, v_2;
  float re, im, amp;
    
  for (uint8_t k = 0; k < IX_LEN; k++) {
    float cos = pgm_read_float(&(cos_t[k]));
    float sin = pgm_read_float(&(sin_t[k]));
    
    float a = 2. * cos;
    v_0 = v_1 = v_2 = 0;  
    for (uint16_t i = 0; i < N; i++) {
      v_0 = v_1;
      v_1 = v_2;
      v_2 = (float)(samples[i]) + a * v_1 - v_0;
    }
    re = cos * v_2 - v_1;
    im = sin * v_2;
    amp = sqrt(re * re + im * im);
    spectrum[k] = amp;        
  } 
}

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

Выводы


Самое главное, что получилось и ресурсов Arduino UNO хватает для простой обработки звука.


Главный урок, который я извлек, что АЦП — штука чувствительная, после успешного распознавания и отправки символа на консоль, я потратил неделю на отладку, чтобы все работало и с дисплеем, потому что соединил землю микрофона и max7219 и все сэмплы сразу превращались в шум.

Можно ли было сделать еще лучше? Да, более правильно было бы подобрать частоту дискретизации и количество сэмплов так, чтобы искомые частоты совпадали с решеткой дискретизации, это бы предотвратило растекание спектра. Такие параметры уже есть f = 8кГц, N = 205, в таком случае надо запускать ADC не в режиме free-run, а timer overflow, и разница была бы очевидна.



Курс подходит к концу, но идей еще много.
Спасибо за внимание.

Комментарии (10)


  1. Taraflex
    09.12.2018 05:33

    float cos = pgm_read_float(&(cos_t[k]));
    float sin = pgm_read_float(&(sin_t[k]));
    

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


    1. dlinyj
      10.12.2018 10:37

      Это прям совсем порочная практика. Интересно, почему компилятор не ругался?


  1. aamonster
    09.12.2018 10:11
    +3

    Думать, "хватит ли производительности Arduino" (изначально известно, что есть запас раз в 5 минимум: в 90-е АОН делался на Z80, требуемая обработка пракически та же) и при этом использовать float? Немного странно.


    1. jok40
      09.12.2018 14:11

      Причём в тех АОНах даже АЦП не было. Звук пропускался через компаратор и дальше обрабатывался в однобитном виде. А в качестве умножения использовался самый обыкновенный XOR.


      1. Sdima1357
        10.12.2018 00:11

        И z80 тоже избыточен. Dtmf decoder делал на 400 килогерцовом 4-битном контроллере, в 92 году. Самой большой проблемой был не Dtmf, а обычный программный uart из-за отсутствия высокочастотного кварца. Приходилось перекалибровывать генератор по часовому кварцу перед каждым сеансом или при ошибке. Естественно прозрачно для пользователя


  1. GipsyIF
    09.12.2018 12:11
    +4

    А я то думал что автор скажет «один» — и ардуина высветит 1. А тут банальный DTMF, а вовсе не «определение цифры на слух» :(


  1. PLG
    10.12.2018 12:07
    +1

    Для ускорения от конечного sqrt можно же отказаться? И затем сравнивать с квадратом threshold.


  1. Kosmoss
    10.12.2018 12:07
    -1

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


    1. dlinyj
      10.12.2018 12:13

      У меня такое ощущение, что вы не читали статью. Или читали и не поняли совершенно


      1. Kosmoss
        11.12.2018 00:10

        Читал, может не так понял.