Щодня бачу рекламу 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), який каже: “коли я скажу СТАРТ одному — обидва спрацюють миттю”. Результат — одне 32-бітне число, де в молодшій половині лежить відповідь від першого “фотоапарата”, а в старшій половині — від другого.
От тільки в мене друга половина завжди була 0.
◆ Перша спроба — виглядає правильно, але працює наполовину
|
1 2 3 4 5 6 7 8 9 10 11 |
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 фізично живий, поза “спареним” режимом? Викликав старий, простий одноканальний режим саме на цьому піні:
|
1 2 3 4 5 |
adc1_init(1); // канал 1 = PA1, звичайний одиночний ADC while (1) { uart_printf("pa1=%d\r\n", adc1_read()); delay_ms(200); } |
Працює бездоганно. Значить, пін живий, проводулька підключена правильно. Проблема саме в “спареному” режимі.
◆ Крок 2: перевір, чи це взагалі не підробка
Пошук видає, що Buе Pill (плата, на якій усе це запускається) продається мільйонами копій з Китаю — і частина з них не справжні мікросхеми ST, а дешеві клони, які іноді “економлять” на рідковживаних фічах. Перш ніж підозрювати свій код, варто виключити просту версію: “може, це просто не той чіп”.
Одна команда:
|
1 2 3 |
st-info --probe chipid: 0x0410 |
0x0410 — це офіційний, справжній номер мікросхеми ST. Значить копати треба в коді та в документації.
◆ Крок 3: подивись, що насправді відбувається всередині
Так, прошу плату розповісти прямо, що коїться всередині:
|
1 2 3 4 |
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: невірна адреса
Йдемо рядок за рядком і ось баг.
|
1 |
#define ADC1_SMPR2 (*(volatile uint32_t *)(ADC1_BASE + 0x14)) // мало бути +0x10 |
Мда, це складніше і нудніше ніж я собі то уявляв.
Що ж відбулось: ADC весь цей час слухав сигнал занадто швидко — 1.5 такту замість повних 239.5. Уяви, що фотоапарат мав час на витримку 1/2000 секунди замість нормальної 1/60 — на темному фоні знімок вийде майже чорний, “розмитий шумом”. Це, до речі, пояснює дивні стрибки чисел ще з минулого тижня коли я налаштовував на сонячній панелі і тестив в темряві.
Виправив адресу — але mic2 усе одно нуль.
◆ Обхідний шлях
Раз другий “фотоапарат” сам по собі справний — найпростіше рішення: не чекати, поки він покладе фото у спільний альбом, а піти й забрати фото:
|
1 2 |
result.mic1 = (uint16_t)(ADC1_DR & 0xFFFF); result.mic2 = (uint16_t)(ADC2_DR & 0xFFFF); // йдемо й забираємо напряму |
Це запрацювало. mic2 ожив. Обидва “фотоапарати” клацають одночасно, просто результат забирається різними шляхами. Думаю для TDOA це вже цілком робочий варіант, адже синхронність не постраждала.
Але залишалось незрозуміло — чому спільний альбом (ADC1_DR) не отримує друге фото?
◆ Хибний слід: підозра на DMA
У документації є фраза про те, що “спарений 32-бітний результат” згадується саме в контексті DMA — спеціального механізму, який може сам, без участі процесора, переносити дані з одного місця в інше, ніби автоматичний конвеєр. Виникла ідея: може, спільний альбом заповнюється лише тоді, коли забирає саме цей автоматичний конвеєр, а не коли процесор сам туди заглядає?
Побудував повноцінний DMA-конвеєр ( hal_dma.c) і зробив порівняльний тест — три способи забрати дані одночасно:
|
1 2 3 4 5 |
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. Це доказ, що годинник іде? Ні — треба подивитись двічі, з паузою, і побачити, чи стрілка зрушила з місця.
Зробив саме так — дві заміри підряд, замість одного:
|
1 2 3 4 |
C 0x066e04e9 C2 0x066e04eb ^^^^ ← старша половина ОДНАКОВА в обох замірах ^^^ ← молодша половина ЗМІНИЛАСЬ |
Молодша половина (перший “фотоапарат”) щоразу оновлюється, як і має бути. Старша половина (другий “фотоапарат”) — застигла на місці. Отже це старий знімок, який завис у альбомі й нікуди не рухається, поки не увімкнеш DMA-вимикач знову.
Правильний, тепер уже дійсно перевірений висновок: сам вимикач DMA в налаштуваннях (ADC_CR2_DMA) — ось що відкриває спільний альбом для запису другого фото. Сам автоматичний конвеєр (DMA-контролер) при цьому не потрібен взагалі — досить просто клацнути цей один вимикач.
⚠ Пастка №3: перевір навіть те, у чому вже впевнений
Залишалось одне сумнівне питання: може, насправді це виправлена адреса (Пастка №1) сама все полагодила, а вимикач DMA тут ні до чого — просто збіглось у часі?
Єдиний спосіб дізнатись напевно — прибрати вимикач DMA, залишивши виправлену адресу скриньки і прогнати знову:
|
1 2 3 4 5 6 |
// 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.
Це найважливіший урок усього тижня: навіть коли здається, що вже все зрозуміло і причина знайдена — варто перевірити ще раз, окремо, саме той фактор, у якому найбільше впевнений. Легко повірити у власний висновок, він же до того ж і логічний, а по факту перевірив не те що треба.
◆ Фінальний, протестований код
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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; } |
Живий тест на панелі:
|
1 2 3 |
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 доступно одним регістром.
Наступний крок — навчити плату слухати мікрофони не по команді від процесора, а самостійно, на фіксованій частоті, через апаратний таймер. Скоро дізнаємось чи вийшло це в мене чи ні.