Для реализации задуманного я использовал Arduino UNO, электретный микрофон (adafruit) и дисплей 8х8 с драйвером MAX7219.
План действий
- Дискретизировать достаточное количество отсчетов (с помощью программы из предыдущей статьи я убедился, что 256 достаточно).
- Найти амплитуды АЧХ, соответствующие искомым частотам, кодирующим символы.
- Два максимальных значения амплитуды дадут индексы строки и столбца искомого символа, так, например, выглядит цифра 3.
Реализация
Перед тем как браться за реализацию, ответим на вопрос — хватит ли нам производительности 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)
aamonster
09.12.2018 10:11+3Думать, "хватит ли производительности Arduino" (изначально известно, что есть запас раз в 5 минимум: в 90-е АОН делался на Z80, требуемая обработка пракически та же) и при этом использовать float? Немного странно.
jok40
09.12.2018 14:11Причём в тех АОНах даже АЦП не было. Звук пропускался через компаратор и дальше обрабатывался в однобитном виде. А в качестве умножения использовался самый обыкновенный XOR.
Sdima1357
10.12.2018 00:11И z80 тоже избыточен. Dtmf decoder делал на 400 килогерцовом 4-битном контроллере, в 92 году. Самой большой проблемой был не Dtmf, а обычный программный uart из-за отсутствия высокочастотного кварца. Приходилось перекалибровывать генератор по часовому кварцу перед каждым сеансом или при ошибке. Естественно прозрачно для пользователя
GipsyIF
09.12.2018 12:11+4А я то думал что автор скажет «один» — и ардуина высветит 1. А тут банальный DTMF, а вовсе не «определение цифры на слух» :(
PLG
10.12.2018 12:07+1Для ускорения от конечного sqrt можно же отказаться? И затем сравнивать с квадратом threshold.
Kosmoss
10.12.2018 12:07-1Все это замечательно, но как на счет оцифровки мыслей? Там вроде уже есть какие-то продвижения, а если бы были мыслезаписи, как бы это могло изменить наш мир.
Taraflex
Пожалуй не очень хорошая идея называть переменные именами функций из стандартной сишной библиотеки.
dlinyj
Это прям совсем порочная практика. Интересно, почему компилятор не ругался?