В этой статье я продолжу воплощать свое вдохновение лабораторной работой №3 уже в железе. Речь пойдет о детектировании цифры по звуку в тоновом режиме набора на Arduino с помощью алгоритма Герцеля.
Для реализации задуманного я использовал 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, и разница была бы очевидна.
Курс подходит к концу, но идей еще много.
Спасибо за внимание.
Автор: zjor