Определение цифры на слух: реализация на Arduino

В этой статье я продолжу воплощать свое вдохновение лабораторной работой №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, и разница была бы очевидна.
_k5orqgso4b-s8ee1ibvfdyms3s.png

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

© Habrahabr.ru