STM32 з нуля без HAL. Частина 20: Два мікрофони, один регістр
Щодня бачу рекламу embedded-курсів, де обіцяють за декілька місяців зробити з тебе спеца й ще й допомогти знайти роботу. Фронтендери й бекендери масово перекваліфіковуються в embedded — бо ж це deep tech чи military tech, а значить і «пісеньки», і бронь.
Хочу тебе розчарувати. Курси з тебе спеца не зроблять. Люди вчаться цьому роками, а профі стають одиниці — і не тому, що ти тупий чи не піддаєшся навчанню. Просто тому, що це треба любити. Тому що це важко. І тому що від тебе будуть вимагати не тільки код, але й проектування і дизайн плат, тобто усе і це треба буде на завтра, а інформації мало, і, на відміну від фронтенду чи бекенду, тут справді треба знати точні науки. Фізика — царина embedded. Якщо мрієш з цим працювати — почни з підручника елементарної фізики. Якщо це тебе не зламає — берись за STM32.
Цей тиждень — гарна ілюстрація чому. Фраза в даташиті, регістр, дні детективної роботи, де фізика (реальна напруга на справжньому піні) є важливішою за будь-яку теорію.
Зараз читаю «Ім’я рози» Умберто Еко. Сказали якщо здолаю перші сто сторінок, де ченці сперечаються про теологію та єресі, — далі піде як по маслу, справжній детектив у стилі Конан Дойля, тільки в монастирі XIV століття. Цей тиждень був рівно таким самим. Перші кроки — карти регістрів, офсети, теорія dual-режиму — це «теологічна» частина, нудна, але без неї нікуди. А потім, коли перший тест видав дивний результат, це перетворилось на детектив: підозрювані, хибні сліди, свідок, який сам себе спростував, і фінальне зізнання винного, яке виявилось геть не тим, кого підозрювали спочатку.
«Гнався за DMA. А насправді проблема була в одному простому вимикачі, який просто забув/не знав увімкнути. Звісно то не стосується тебе, це чисто моя особливість»
◆ Навіщо взагалі два мікрофони одночасно
Уяви, що ти стоїш посеред кімнати із зав’язаними очима, і хтось ляскає в долоні. Ти можеш приблизно вказати напрямок — звідки прийшов звук — тому що твої вуха почули ляскіт не одночасно. Різниця в частки мілісекунди — і мозок з неї вираховує напрямок.
Це і є ідея TDOA (Time Difference Of Arrival — різниця часу прибуття), і саме заради неї весь цей тиждень пішов на боротьбу з одним регістром. Але щоб виміряти цю крихітну різницю в часі, треба спершу навчитись чути обома «вухами» одночасно — точно в один і той самий момент. Якщо мікроконтролер спочатку послухає перший мікрофон, а через мить другий (замість одночасно) — це те саме, що вимірювати різницю в прибутті звуку, попередньо самому додавши власну затримку. Вимір стане безглуздим.
STM32F103 вміє слухати два входи в один момент — апаратно, без заморочок у коді. Називається це dual simultaneous ADC mode. У теорії — кілька рядків коду. На практиці трохи довше і складніше.
◆ Як це працює — простими словами
Уяви два фотоапарати, які мають клацнути затвор в один момент, не по черзі. У STM32 є два «фотоапарати» (їх називають ADC1 і ADC2) — кожен слухає свій пін (PA0 і PA1). Є спеціальний перемикач у налаштуваннях (DUALMOD), який каже: «коли я скажу СТАРТ одному — обидва спрацюють миттю». Результат — одне
От тільки в мене друга половина завжди була 0.
◆ Перша спроба — виглядає правильно, але працює наполовину
adc_dual_t adc_dual_read(void)
{
adc_dual_t result;
ADC1_CR2 |= ADC_CR2_SWSTART; // "СТАРТ!" для обох одразу
while (!(ADC1_SR & ADC_SR_EOC)); // чекаємо, поки готово
uint32_t packed = ADC1_DR; // читаємо ОБИДВІ відповіді за раз
result.mic1 = (uint16_t)(packed & 0xFFFF); // молодша половина
result.mic2 = (uint16_t)(packed >> 16); // старша половина
return result;
}
Підключив сонячну панель до PA0 — світиш ліхтариком, число реагує. mic2 — завжди нуль, навіть шумів немає.
◆ Крок 1: спершу перевір, чи взагалі щось зламане
Найважливіше правило будь-якого пошуку несправності — не гадати одразу, а звузити коло підозрюваних. Спочатку перевірив: а чи взагалі PA1 фізично живий, поза «спареним» режимом? Викликав старий, простий одноканальний режим саме на цьому піні:
adc1_init(1); // канал 1 = PA1, звичайний одиночний ADC
while (1) {
uart_printf("pa1=%d\r\n", adc1_read());
delay_ms(200);
}
Працює бездоганно. Значить, пін живий, проводулька підключена правильно. Проблема саме в «спареному» режимі.
◆ Крок 2: перевір, чи це взагалі не підробка
Пошук видає, що Buе Pill (плата, на якій усе це запускається) продається мільйонами копій з Китаю — і частина з них не справжні мікросхеми ST, а дешеві клони, які іноді «економлять» на рідковживаних фічах. Перш ніж підозрювати свій код, варто виключити просту версію: «може, це просто не той чіп».
Одна команда:
st-info --probe chipid: 0x0410
0x0410 — це офіційний, справжній номер мікросхеми ST. Значить копати треба в коді та в документації.
◆ Крок 3: подивись, що насправді відбувається всередині
Так, прошу плату розповісти прямо, що коїться всередині:
uart_printf("cr1=%x adc1_sr=%x adc2_sr=%x adc2_dr_raw=%x\r\n",
ADC1_CR1, ADC1_SR, ADC2_SR, ADC2_DR);
cr1=0x00060000 adc1_sr=0x00000010 adc2_sr=0x00000012 adc2_dr_raw=0x000005c7
Бачу три речі одразу:
- перемикач «спарений режим» (
cr1) увімкнений правильно - другий «фотоапарат» (ADC2) сам каже «я готовий» (
adc2_sr) - і в нього є правильна відповідь у власному регістрі (
adc2_dr_raw=0x5c7)
Тобто другий «фотоапарат» клацнув і має фото. Просто це фото не потрапляє в спільний альбом (ADC1_DR).
⚠ Пастка № 1: невірна адреса
Йдемо рядок за рядком і ось баг.
#define ADC1_SMPR2 (*(volatile uint32_t *)(ADC1_BASE + 0x14)) // мало бути +0x10
Мда, це складніше і нудніше ніж я собі то уявляв.
Що ж відбулось: ADC весь цей час слухав сигнал занадто швидко — 1.5 такту замість повних 239.5. Уяви, що фотоапарат мав час на витримку 1/2000 секунди замість нормальної 1/60 — на темному фоні знімок вийде майже чорний, «розмитий шумом». Це, до речі, пояснює дивні стрибки чисел ще з минулого тижня коли я налаштовував на сонячній панелі і тестив в темряві.
Виправив адресу — але mic2 усе одно нуль.
◆ Обхідний шлях
Якщо другий «фотоапарат» сам по собі справний — найпростіше рішення: не чекати, поки він покладе фото у спільний альбом, а піти й забрати фото:
result.mic1 = (uint16_t)(ADC1_DR & 0xFFFF); result.mic2 = (uint16_t)(ADC2_DR & 0xFFFF); // йдемо й забираємо напряму
Це запрацювало. mic2 ожив. Обидва «фотоапарати» клацають одночасно, просто результат забирається різними шляхами. Думаю для TDOA це вже цілком робочий варіант, адже синхронність не постраждала.
Але залишалось незрозуміло — чому спільний альбом (ADC1_DR) не отримує друге фото?
◆ Що таке DMA — і чому про нього взагалі варто думати
Перш ніж підозрювати DMA у пакуванні даних, варто зрозуміти, що це таке.
Уяви, що процесор — це ти особисто, і тобі треба перенести 1000 коробок з одного складу в інший. Без DMA це виглядає так: береш коробку, несеш, кладеш, повертаєшся, береш наступну — і так 1000 разів. Весь цей час ти зайнятий винятково перенесенням, і жодну іншу роботу зробити не можеш.
Це немов найняти окремого вантажника, який сам возить коробки зі складу А на склад Б, поки ти займаєшся чимось іншим. Ти лише один раз кажеш йому: «ось звідки брати, ось куди класти, ось скільки коробок» — і йдеш робити свою справу. Вантажник (DMA-контролер) працює паралельно, незалежно від того, чим зайнятий ти (CPU).
У нашому конкретному випадку «коробки» — це результати ADC-конверсій, «склад А» — регістр ADC1_DR, «склад Б» — масив у SRAM. Без DMA CPU мусив би сам, кожного разу, зупинятись і забирати черговий результат з ADC вручну (те, що ми й робили увесь цей час через adc_dual_read()). З DMA — контролер сам переносить кожен новий результат у пам’ять, а CPU в цей час може займатись іншими задачами, наприклад одночасно обробляти вже накопичені дані попереднього виміру.
Чому це критично для реального TDOA, а не тільки для нашого поточного тесту. Зараз ми читаємо ADC вручну, у циклі while(1), з паузами — CPU весь час «стоїть і чекає». Для справжнього безперервного захвату звуку на стабільній частоті (наприклад, для GCC-PHAT чи навіть для нашого простішого рішення з input capture) потрібно, щоб семпли надходили рівномірно й безперервно, без пропусків через те, що CPU в якийсь момент зайнятий чимось іншим. Апаратний таймер запускає конверсію на точному інтервалі, а DMA сама складає результати в буфер — CPU тут взагалі не потрібен у гарячому шляху. Саме цю архітектуру ми плануємо на наступний крок (TIM3 TRGO + DMA continuous capture).
Регістри, з якими ми працювали: DMA1_CCR (конфігурація каналу — напрямок, розмір даних, чи інкрементувати адресу пам’яті), DMA1_CNDTR (лічильник, скільки трансферів лишилось, рахує вниз до нуля), DMA1_CPAR/DMA1_CMAR (звідки й куди возити коробки — адреса периферії й адреса пам’яті відповідно). STM32F103 має фіксовану матрицю: ADC1 апаратно прив’язаний саме до DMA1 Channel1 — вибрати інший канал технічно неможливо, це не програмований мультиплексор.
DMA (Direct Memory Access, прямий доступ до пам’яті) — це апаратний помічник у мікроконтролері, який переносить дані між пам’яттю та пристроями без допомоги головного процесора.
Де це корисно:
- Передача великих масивів: Наприклад, виведення картинки на екран або читання даних з карти пам’яті.
- Швидкий UART / SPI / I2C: Прийом великих пакетів тексту без ризику втратити байти.
- АЦП (опитування датчиків): Автоматичне збереження вимірів у буфер пам’яті без участі ядра
Тепер, розуміючи, що таке DMA, можна повернутись до самої детективної історії — чи справді він причетний до зникнення другого мікрофона в спареному регістрі.
◆ Хибний слід: підозра на DMA
У документації є фраза про те, що «спарений
Побудував повноцінний DMA-конвеєр ( hal_dma.c) і зробив порівняльний тест — три способи забрати дані одночасно:
A dma_buf[0]=0x03760745 (через конвеєр DMA) A dma_buf[1]=0x036c05d5 B cpu_read dmabit=1 -> 0x03660902 (процесор сам читає, вимикач "DMA" увімкнений) C cpu_read dmabit=0 -> 0x03660888 (процесор сам читає, вимикач "DMA" вимкнений) VERDICT: packing gated by ADC1_CR2.DMA bit
На перший погляд — підтвердження: у варіантах A і B старша половина ненульова, у C — теж ненульова, вердикт «гейтиться вимикачем DMA». Здавалось, питання закрите.
⚠ Пастка № 2: людина вірить лише в те, в що хоче вірити
Дивлюсь уважно на C: 0x0888. Там є число відмінне від нуля. Але чи воно динамічне, тобто воно дійсно оновлюється щоразу? Чи воно просто забуте з минулого разу, коли вимикач DMA був увімкнений?
Це як подивитись на годинник один раз і побачити стрілку на цифрі 5. Це доказ, що годинник іде? Ні — треба подивитись двічі, з паузою, і побачити, чи стрілка зрушила з місця.
Зробив саме так — два заміри підряд, замість одного:
C 0x066e04e9 C2 0x066e04eb ^^^^ ← старша половина ОДНАКОВА в обох замірах ^^^ ← молодша половина ЗМІНИЛАСЬ
Молодша половина (перший «фотоапарат») щоразу оновлюється, як і має бути. Старша половина (другий «фотоапарат») — застигла на місці. Отже це старий знімок, який завис у альбомі й нікуди не рухається, поки не увімкнеш DMA-вимикач знову.
Правильний, тепер уже дійсно перевірений висновок: сам вимикач DMA в налаштуваннях (ADC_CR2_DMA) — ось що відкриває спільний альбом для запису другого фото. Сам автоматичний конвеєр (DMA-контролер) при цьому не потрібен взагалі — досить просто клацнути цей один вимикач.
⚠ Пастка № 3: перевір навіть те, у чому вже впевнений
Залишалось одне сумнівне питання: може, насправді це виправлена адреса (Пастка № 1) сама все полагодила, а вимикач DMA тут ні до чого — просто збіглось у часі?
Єдиний спосіб дізнатись напевно — прибрати вимикач DMA, залишивши виправлену адресу скриньки і прогнати знову:
// ADC1_CR2 |= ADC_CR2_DMA; // тимчасово вимкнено для перевірки Test: SMPR2 fix WITHOUT DMA bit mic1=1202 mic2=0 mic1=1231 mic2=0 mic1=1269 mic2=0
Миттєво й однозначно: mic2 знову нуль. Виправлена адреса сама по собі нічого не вирішувала. Єдина причина, чому все запрацювало це вимикач DMA.
Це найважливіший урок усього тижня: навіть коли здається, що вже все зрозуміло і причина знайдена — варто перевірити ще раз, окремо, саме той фактор, у якому найбільше впевнений.
◆ Фінальне рішення
void adc_dual_init(void)
{
RCC_APB2ENR |= RCC_ADC1EN | RCC_ADC2EN | (1 << 2);
GPIO_CRL(GPIOA_BASE) &= ~((0xF << 0) | (0xF << 4)); // PA0, PA1 -> analog
ADC1_CR1 |= ADC_CR1_DUALMOD_SIMULT; // "слухай обидва одночасно"
// ... калібрування, час вибірки (тепер справді SMPR2, +0x10) ...
ADC2_CR2 |= ADC_CR2_EXTTRIG | ADC_CR2_EXTSEL_SWSTART;
ADC1_CR2 |= ADC_CR2_EXTTRIG | ADC_CR2_EXTSEL_SWSTART;
// ОБОВ'ЯЗКОВИЙ вимикач — без нього другий мікрофон завжди мовчить,
// навіть якщо все інше налаштовано ідеально
ADC1_CR2 |= ADC_CR2_DMA;
}
adc_dual_t adc_dual_read(void)
{
adc_dual_t result;
ADC1_CR2 |= ADC_CR2_SWSTART;
while (!(ADC1_SR & ADC_SR_EOC));
uint32_t packed = ADC1_DR; // одне читання — обидва мікрофони разом
result.mic1 = (uint16_t)(packed & 0x0FFF);
result.mic2 = (uint16_t)((packed >> 16) & 0x0FFF);
return result;
}

Живий тест на панелі:
mic1=2448 mic2=881 mic1=2557 mic2=890 mic1=2545 mic2=907
Обидва канали, одним рухом, синхронно.
◆ Підсумок тижня
| Що знайдено | Що це насправді означало |
|---|---|
Адреса SMPR2 вела не туди (Пастка № 1) | Реальний баг |
Плата оригінальна (chipid=0x0410) | Спростовано підозру |
| «DMA гейтить пакування» (перший тест) | Виглядало підтвердженим, але тест сам був недостатньо строгий |
| Застигле число замість живого (Пастка № 2) | Урок: перевіряй в динаміці, а не «є число чи нема» |
| SMPR2 не мав жодного стосунку (Пастка № 3) | Урок: перевір навіть те, у чому найбільше впевнений |
ADC1_CR2.DMA-вимикач | Справжня, єдина й підтверджена причина |
💡 На замітку ардуїнщикам. Усе, що описано вище, неможливо повторити на класичному AVR (Uno, Nano, Mega) так само просто. У ATmega328p/2560 фізично один-єдиний ADC — всі «аналогові піни» A0-A5 (чи A0-A15 на Mega) це не окремі перетворювачі, а один спільний ADC за мультиплексором, який перемикається між ними по черзі. Апаратної команди «слухай два входи в один момент» там просто не існує в кристалі, скільки регістри не крути.
Якщо колись знадобиться те саме на AVR то можан спробувати такі варіанти, але це не точно: прийняти фіксовану затримку між читаннями (~13 мкс на 16МГц) і відняти її математично; поставити зовнішні sample-and-hold мікросхеми (LF398), які «заморожують» напругу синхронно, а AVR вже спокійно оцифровує обидва значення по черзі; або взяти окрему мікросхему з вбудованим simultaneous ADC (AD7899-2, LTC1867) по SPI. Усі три — це апаратний обхідний шлях там, де STM32 доступно одним регістром.
Наступний крок — навчити плату слухати мікрофони не по команді від процесора, а самостійно, на фіксованій частоті, через апаратний таймер. Скоро дізнаємось чи вийшло це в мене чи ні, чи ця стаття була останньою...
7 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівМожна було би взяти готову ІМС кодека G.711 для телефонії, або щось потужніше на IIS інтерфейс але прості мікроконтролери його не мають (але це не перешкода для справжніх ембедедистів).
Ще би АРП бо на АЦП динамічний діапазон буде дуже слабенький
P.S.
А CPLD/FPGA коли будуть?
CPLD/FPGA інша вагова категорія. Щодо G.711/I2S ціль серії зрозуміти, що взагалі відбувається на рівні регістрів, коли намагаєшся зробити щось, що виходить за межі твоїх можливостей
CPLD якраз для того щоб зробити міст напр. до того ж IIS до мікроконтролера, якщо трохи є розуміння мови Pascal то на VHDL легко (кому як)
ще не читав, але одобряю!
я так зрозумів, після попередньої теми АІ згенеровану схему підключення електректного майка слабо!
навчать С/С++, розводити плати, паяти, та кодити під ОСРЧ
Точно, повний провал і демотивація. Шютка. Усе за планом, йдемо за картою і задумкою
Дякую за можливість дізнаватись нову цікаву інформацію українською
Дякую. Вчусь по вашим статтям, пробую повторити але інакше. Хал взяла собі роблю власний проект, думаю ви не проти