Нарешті dual ADC синхронно читає два мікрофони, саме так закінчився минулий тиждень. Наступний крок мав би навчити плату слухати через таймер, без CPU. Але реальність виявилась цікавішою, я взагалі не дійшов до таймер-тригера continuous capture. Вся ідея крос-кореляції, на якій тримався план тижня, у мому випадку просто не працює і тре шукати геть інший шлях.
◆ Навіщо взагалі знати, звідки прийшов звук
Почав себе заспокоювати та навіщо це взагалі? Можна і так закінчити серію, адже розглянули майже усе, що на мою думку варто уваги саме на перших порах, для того щоб зрозуміти, що цікаво більше, а що менше. Лайки і перегляди чітко вказували на те, що тре зупинитись і перемкнутись на ініш серії, які чекають свого часу. І таке виправдання знайшлось, хоча б тому що це дійсно цікава тема, тому я ттрохи ще вам понабридаю із своїм STM32 з нуля без HAL.
Отже уяви, що стоїш посеред кімнати із зав’язаними очима, і хтось ляскає в долоні. Ти можеш приблизно вказати напрямок тому що твої вуха почули ляскіт не одночасно. Різниця в частки мілісекунди і мозок з неї вираховує напрямок. Це і є TDOA (Time Difference Of Arrival).
Як виявилось це абстракція, а досить поширена технологія, наприклад розумні колонки (Amazon Echo, Google Home) роблять рівно те саме двома способами одночасно: напрямок уваги (кільце світла показує, звідки говорять) і beamforming – електронне “наведення” чутливості масиву мікрофонів у бік джерела, з придушенням шуму з інших напрямків. Знайшлось навіть дослідження робота-квадрупеда, який саме через TDOA повертається обличчям до людини, що покликала його голосом це буквально те, що мені потрібно і не тільки для Автобота, у мому випадку мій власний АІ асистент за допомогою TDOA визначає напрямок -> камера повертається на голос і наприклад визначає, що це я (як я планую це робити ми розглядали в попердніх статтях) -> Катя прокидається (хто це і чому прокидається теж розглядали раніше).
Наче формула проста: θ = arcsin(c·τ/d), де c – швидкість звуку, τ – виміряна затримка, d – відстань між мікрофонами.
Формула визначає кут приходу хвилі (Direction of Arrival, DOA) у системі з двома приймачами (наприклад, мікрофонами чи антенами), що базується на часовій затримці між ними.
- θ — кут падіння хвилі (відносно нормалі до лінії, що з’єднує датчики);
- c — швидкість поширення хвилі в середовищі (наприклад, швидкість звуку в повітрі ≈ 343 м/с);
- τ — затримка часу (Time Difference of Arrival, TDOA) між сигналами на двох датчиках;
- d — відстань (база) між центрами датчиків.
Фізичний механізм
-
Хвиля досягає першого та другого датчиків із затримкою τ, оскільки проходить додатковий шлях c ⋅ τ.
-
З геометричної різниці ходу формується співвідношення для прямолінійної симетричної решітки.
-
Обернена функція арксинус дозволяє безпосередньо обчислити кут приходу сигналу. Обов’язковою умовою для обчислення є виконання обмеження |c ⋅ τ| ≤ d.
◆ Залізо: два KY-037
Обрав готові модулі KY-037 – електретний капсуль + компаратор LM393 з двома виходами: A0 (аналоговий, підсилений сигнал) і D0 (цифровий, просто “звук перевищив поріг чи ні”). Спершу планував використовувати тільки A0.
⚠ Проблема №0: живлення. Модуль номінально на 5V, а межа входу ADC STM32 це 3.3V. Перевірив мультиметром на 3.3V-живленні база сигналу в тиші виявилась підозріло низькою (~0.2-0.4V замість очікуваних “посередині діапазону”). Виявилось, це нормальна конструкція: на платі немає окремого підсилювального каскаду для A0, сигнал іде майже сирий з капсуля. Підключив напряму на 3.3V, без дільника напруги це цілком безпечно, просто малоамплітудний сигнал.
◆ Перша спроба: буфер і крос-кореляція
Записував ляскіт у циклічний буфер (2000 семплів) з за пороговим тригером, і рахував крос-кореляцію амплітуди між каналами, щоб знайти лаг, на якому збіг буде максимальний.
Перші тести дали лаг = 0 з непереконливою силою збігу, тобто топові кандидати лагів відрізнялись за силою на якихось 60-70%, замість чіткого домінуючого піка. Спробував гучніший ляскіт і амплітуда підскочила до 3863 (з 12-бітних де максимум 4095!) – клипінг, спотворення форми хвилі.
Поправив: зменшив час вибірки ADC з 239.5 такту (правильний вибір для повільних джерел типу потенціометра з Ч19) до 7.5 такту , а це роздільна здатність у 12 разів швидша, з 63мкс до 5мкс на семпл. Отримав значно багатший сигнал, довгу коливальну хвилю замість кількох точок на краю буфера.
⚠ Проблема №1: наївна кореляція не бачить фронт хвилі
Навіть на насиченому сигналі кореляція лишалась розмитою, а сусідні лаги були майже однакової сили (1.00×–1.03×, замість чіткого піка). Спробував high-pass фільтр (першу різницю), але це не допомогло.
Причина виявилась не програмна, а радше фундаментальна, звук же не імпульсний сигнал, він плавний, реверберуючий, з відлунням. Сусідні семпли природно сильно корелюють між собою через це, і крос-кореляція цілого хвоста ляскоту (майже 3мс з відлунням) дає розмитий пік замість точного моменту атаки. Професійні системи через це не покладаються на наївну кореляцію амплітуди напряму, вони використовують GCC-PHAT (Generalized Cross-Correlation Phase Transform), який “вибілює” спектр перед кореляцією. Це вже окрема, глибша тема з FFT, залишаю як цікавинку на майбутнє, я йду іншим шляхом.
◆ Інший шлях: цифровий вихід і апаратний таймер
Замість того щоб вимірювати точну форму аналогової хвилі, вирішив ловити момент, коли компаратор LM393 перемикається, це можливо на D0-вихідоді, який я спершу планував ігнорувати.
STM32 має для цього готову периферію – timer input capture. Таймер апаратно фіксує момент перепаду на вході, з роздільною здатністю самого таймера (125нс/тик на 8MHz у 40 разів точніше за наші 5мкс ADC-семпли).
|
1 2 3 |
// TIM3_CH1 <- PA6 <- D0 мікрофона 1 // TIM3_CH2 <- PA7 <- D0 мікрофона 2 // D0 інверсний (LOW при спрацюванні) -> capture по спадаючому фронту |
Перший тест дає мені хаотичні числа в діапазоні мілісекунд, фізично неможливі для мікрофонів розташованих на столі.
⚠ Дурість №2: цифрові піни не були підключені
Найпростіша причина з усіх: забув фізично підключити D0 обох модулів до PA6/PA7. Плаваючі входи ловили випадкові наведення. Підключив і числа впали до розумного порядку, але все таки виглядали дивними.
⚠ Проблема №3: переповнення 16-бітного лічильника
t1=62961, t2=870 – коли лічильник переходить через межу (65535->0) між двома захопленнями, просте віднімання (int32_t)t2 - (int32_t)t1 дає катастрофічно хибне число. Виправимо за модульним 16-бітним відніманням:
|
1 |
r.diff = (int16_t)(t2 - t1); // природно коректно обробляє wraparound |
⚠ Проблема №4: очікування другого спрацювання
Без обмеження часу очікування другий канал міг зловити зовсім не той ляскіт, що перший, а випадковий пізніший шум замість парної події. Додав стопор (перші 40000 тиків) і отримав нову, проблему де сам механізм перевірки таймауту ламався через переповнення, якщо вікно перевищує половину діапазону лічильника (32768). Скоротив до безпечних 8000 тиків (1мс, з запасом покриває будь-яку реалістичну відстань між мікрофонами) і нарешті отримав стабільні, повторювані diff.
◆ Ще одна змінна: різна чутливість мікрофонів
Навіть з виправленим кодом результати іноді не збігались зі знаком, який очікувався фізично. Причина —в тому, що кожен KY-037 має власний, незалежно виставлений підстроювальний резистор порогу. Той, що чутливіший, спрацьовує раніше навіть коли звук фізично прийшов пізніше, похибка від порогів “забиває” реальну затримку в кілька сотень наносекунд своєю власною, набагато більшою.
Спроба клацати впритул до кожного мікрофона по черзі (3-4см) дала суцільний TIMEOUT на дальньому каналі, а локалізований, тихий клац просто не долає поріг на відстані. А клацання по колу навколо пари теж жодного разу не дало парного спрацювання одночасно і це вже можливо вказує на структурну межу самого компаратора LM393, а не на пороги спрацювання.
⚠ Проблема №5: LED реагує миттєво, а цифри ні
Окремо від затримки через deadline (Проблема №4 вище) я помітив, що ляскав швидше, ніж з’являлись нові рядки в терміналі, хоча LED на платі при цьому реагував на кожен звук миттєво, без затримки. Це виключило гіпотезу про гістерезис компаратора (якби LED теж “зависав”, то це підтвердило б повільність LM393, але він же не зависав). Найімовірніше, причина та сама, що й у Проблемі №4 , тобто старе, ще не перепрошите вікно 40000 заглиблювалось у переповнення й “губило” події, поки я тестував саме цей симптом. Після переходу на 8000 затримка зникла разом з рештою хаотичних показників.
◆ Результат: надійний бінарний напрямок
Найкращі виміри (мікрофони на базі 14см, ляскіт долонями з 30-40см) дали повторювані, фізично правдоподібні diff з стабільним знаком на кожній позиції, величина в межах теоретичного максимуму для цієї бази (d/c в тіках таймера).
Але головна практична знахідка інша: точна величина ненадійна, а знак надійний. У серії тестів “ляскіт ліворуч / праворуч” знак диференціалу жодного разу не переплутався, навіть коли конкретне число стрибало досить сильно. Для задачі “куди повертати камеру Автобота” точний кут не обов’язковий – досить грубого “ліворуч чи праворуч, чи один канал взагалі не почув”, і саме це система дає надійно.
|
1 2 3 4 5 6 7 |
typedef enum { TDOA_STRONG_LEFT, // тільки mic1 спрацював - джерело далеко збоку TDOA_LEFT, TDOA_CENTER, TDOA_RIGHT, TDOA_STRONG_RIGHT } tdoa_zone_t; |
Кут через arcsin (таблиця пошуку, без float і без libm — той самий стиль, що й увесь HAL) просто експериментальний, режим напогратись, на жаль не пригодний для точного наведення.

◆ Механіка: 18 см, ширина Автобота
Порахував: більша база мікрофонів дає кращу точність за той самий абсолютний шум порогів (не гірше, як інтуїтивно здається — правило “піврізниці хвилі” тут не застосовується, ми міряємо окремий імпульс через компаратор, не фазу неперервного тону). Ширина Автобота по колесах 18см, майже ідеально збігається з практичним максимумом, який дозволяють самі модулі.
Та мене вже непокоїть те, що рука-маніпулятор і колеса даватимуть механічний шум просто на платформі, де стоятимуть мікрофони. Чи вистачить простого правила “слухай тільки коли стоїш нерухомо”, чи знадобиться робити щось складніше, з’ясується вже тільки на реальному шасі, а не в лабораторних умовах.
◆ Не ADC єдиним: альтернативні шляхи
Під Ч20 flyman у коментарях підняв три технічні напрямки, які варто записати для історії, ну і корисно розуміти весь ландшафт варіантів.
Готовий кодек G.711. Стандартний, ще з 1970-х, кодек для телефонії — стискає лінійний сигнал нелінійно (companding), 8 біт замість 16, орієнтуючись на те, як вухо сприймає гучність. Готова мікросхема бере аналоговий вхід, віддає вже стиснений цифровий потік — не треба писати власний ADC-код. Хоча думаю: G.711 не вирішив би саме мою проблему. Сompanding стискає динамічний діапазон для передачі голосу телефонією, а не розв’язує синхронний parallel sampling, який якраз і був вузьким місцем.
I2S-інтерфейс і MEMS-мікрофони. Цифрова передача звуку (той самий INMP441, який спливав ще в ідеях “смішного робота”) — синхронізований, багатоканальний потік цифрових ліній замість аналогових напруг через ADC. Позбавлений усіх проблем з dual-ADC-пакуванням, які я розгрібав, але Blue Pill тягне I2S лише через SPI2/SPI3 у спеціальному режимі, це вже окремий суттєвий шматок зайвої роботи, а не проста заміна капсуля.
AGC (автоматичне регулювання підсилення). Влучне зауваження про слабкий динамічний діапазон — KY-037 має фіксоване підсилення, без AGC взагалі. Звідси й клипінг на гучних ляскотах, який ловився. MAX9814 (той самий, що вже фігурував у моїх планах щодо Каті) має вбудований AGC — реалістичний апгрейд, якщо колись повернусь до аналогового шляху.
CPLD/FPGA. Програмована логіка — на відміну від мікроконтролера з послідовним виконанням коду, апаратно паралельні логічні схеми. Паралельний, детермінований timing без сюрпризів на кшталт “недокументований DMA-біт”. Та буду чесний це інша вагова категорія.
◆ Висновок
Точний TDOA лишається недосяжним на цьому залізі — потрібні 3+ мікрофони (прибрати front-back ambiguity) і плата з triple simultaneous ADC чи інша архітектура. Але надійного бінарнго напрямку достатньо Автоботу, щоб почати повертатись у бік голосу і це вже працює.
Наступний крок – фінал.
Формула визначає кут приходу хвилі (Direction of Arrival, DOA) у системі з двома приймачами (наприклад, мікрофонами чи антенами), що базується на часовій затримці між ними. 



для прямолінійної симетричної решітки.