STM32 з нуля без HAL. Частина 21: Звідки прийшов звук
Нарешті 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 з непереконливою силою збігу, тобто топові кандидати лагів відрізнялись за силою на якихось
Поправив: зменшив час вибірки 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-семпли).
// 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 дає катастрофічно хибне число. Виправимо за модульним
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 в тіках таймера).
Але головна практична знахідка інша: точна величина ненадійна, а знак надійний. У серії тестів «ляскіт ліворуч / праворуч» знак диференціалу жодного разу не переплутався, навіть коли конкретне число стрибало досить сильно. Для задачі «куди повертати камеру Автобота» точний кут не обов’язковий — досить грубого «ліворуч чи праворуч, чи один канал взагалі не почув», і саме це система дає надійно.
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. Стандартний, ще з
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) у системі з двома приймачами (наприклад, мікрофонами чи антенами), що базується на часовій затримці між ними.
1 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарівдобрався до триангуляції, а якщо дві-три плати ©(тм)?