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 — відстань (база) між центрами датчиків.

Фізичний механізм

  1. Хвиля досягає першого та другого датчиків із затримкою τ, оскільки проходить додатковий шлях c ⋅ τ.

  2. З геометричної різниці ходу формується співвідношення для прямолінійної симетричної решітки.

  3. Обернена функція арксинус дозволяє безпосередньо обчислити кут приходу сигналу. Обов’язковою умовою для обчислення є виконання обмеження |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-семпли).

Перший тест дає мені хаотичні числа в діапазоні мілісекунд, фізично неможливі для мікрофонів розташованих на столі.

⚠ Дурість №2: цифрові піни не були підключені

Найпростіша причина з усіх: забув фізично підключити D0 обох модулів до PA6/PA7. Плаваючі входи ловили випадкові наведення. Підключив і числа впали до розумного порядку, але все таки виглядали дивними.

⚠ Проблема №3: переповнення 16-бітного лічильника

t1=62961, t2=870 – коли лічильник переходить через межу (65535->0) між двома захопленнями, просте віднімання (int32_t)t2 - (int32_t)t1 дає катастрофічно хибне число. Виправимо за модульним 16-бітним відніманням:

⚠ Проблема №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 в тіках таймера).

Але головна практична знахідка інша: точна величина ненадійна, а знак надійний. У серії тестів “ляскіт ліворуч / праворуч” знак диференціалу жодного разу не переплутався, навіть коли конкретне число стрибало досить сильно. Для задачі “куди повертати камеру Автобота” точний кут не обов’язковий – досить грубого “ліворуч чи праворуч, чи один канал взагалі не почув”, і саме це система дає надійно.

Кут через 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 чи інша архітектура. Але надійного бінарнго напрямку достатньо Автоботу, щоб почати повертатись у бік голосу і це вже працює.

Наступний крок – фінал.

Опубліковано в STM32

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *