◆ Де ми зупинились
У перших двох частинах ми вичавили з плат Arduino максимум: розібрались із розрядністю ADC, прибрали delay(), розігнали ADC через prescaler і навчились фільтрувати шум програмно. Але якщо потрібно оцифровувати справді швидкі сигнали — звук з мікрофона, радіоімпульси для майбутнього осцилографа — 8-бітні плати й стандартні функції Arduino стають головним гальмом.
Сьогодні переходимо на 32-бітний ARM Cortex-M3 — STM32 Blue Pill. Навчимось збирати безпечний захисний дільник напруги і повністю відмовимось від analogRead() на користь прямого керування регістрами процесора чистим C.
◆ Чому analogRead() повільний на STM32
Коли викликаєте analogRead(PA0) через Arduino-обгортку (STM32duino) — під капотом щоразу запускається важкий цикл: ADC вмикається заново, налаштовується пін, обирається канал, запускається вимір — і, найголовніше, процесор блокується, чекаючи завершення конверсії. Це та сама проблема, з якою ми боролись в Uno в другій частині, тільки тепер ціна вища — STM32 фізично здатен на значно більше, а обгортка цей потенціал ховає.
В архітектурі ARM уся периферія керується через регістри — комірки пам’яті, фізично з’єднані з логікою чипа. Замість того, щоб просити абстрактну функцію виконати замір, можна напряму перемикати потрібні біти.
Ключова ідея цієї статті — режим безперервного перетворення (Continuous Conversion Mode):
- Вмикаємо ADC один раз при старті.
- Кажемо: “вимірюй напругу на PA0 без зупину”.
- ADC циклічно робить заміри сам, у фоні. Процесору більше не треба чекати — у будь-яку мить можна просто зазирнути в регістр даних
ADC1->DRі забрати найсвіжіше значення.
◆ Залізна безпека — як не спалити STM32
Перш ніж підключати хоч один дріт до Blue Pill, запам’ятайте головне правило: аналогові піни STM32 працюють тільки в діапазоні 0-3.3V, і вони не толерантні до 5V. Подача напруги від Arduino (5V) чи будь-якого 5-вольтового джерела миттєво пошкодить пін чи весь мікроконтролер.
Щоб безпечно виміряти сигнал з амплітудою вище 3.3V, потрібен резистивний дільник напруги — два резистори:
|
1 2 3 4 5 |
Вхідний сигнал (до 5В) ───[ R1: 1.8 кОм ]───┬───► Пін PA0 │ [ R2: 3.3 кОм ] │ GND |
Формула:
|
1 |
Vout = Vin × R2 / (R1 + R2) |
При R1=1.8 кОм, R2=3.3 кОм і Vin=5V (максимум з Arduino):
|
1 |
5V × 3300 / (1800 + 3300) = 5V × 0.647 = 3.23V |
⚠ Формально це вкладається в безпечний діапазон STM32, але запас всього ~70 мВ — надто тонкий для комфорту, особливо якщо джерело сигналу нестабільне чи Vin випадково трохи перевищить 5V (наприклад, USB-живлення “гуляє” в межах 4.75-5.25V за специфікацією). На практиці варто цілити не в “формально влазить під 3.3V”, а в 3.0V з запасом — саме тому в домашньому завданні нижче для автомобільного акумулятора (12V) прямо стоїть підказка “не більше 3.0V на виході”, а не 3.3V впритул.
Важливо: якщо сигнал береться напряму з потенціометра, живленого від 3.3V-піна самої Blue Pill — дільник взагалі не потрібен, напруга вже безпечна за визначенням.
◆ Вправа 3.1 — прямий доступ до заліза ADC
Схема підключення:
- Зберіть дільник напруги за схемою вище, якщо подаєте сигнал до 5V. Якщо живите потенціометр від 3.3V самої Blue Pill — дільник не потрібен.
- Вихід сигналу (0-3.3V) — на пін PA0.
- Об’єднайте всі GND.
Код (переконайтесь, що в Arduino IDE обрано плату STM32F1xx / STM32duino):
|
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
void setup() { Serial.begin(115200); // 1. Вмикаємо тактування модуля ADC1 і порту GPIOA. // Без цього кроку блоки заліза фізично знеструмлені для економії енергії. // Увага: правильні імена бітів — ADC1EN і IOPAEN (без кінцевої "R", // бо сам регістр вже називається ENR — Enable Register). RCC->APB2ENR |= RCC_APB2ENR_ADC1EN | RCC_APB2ENR_IOPAEN; // 2. Налаштовуємо швидкість шини ADC (ADCPRE). // Частота APB2 = 72 МГц. Максимум для ADC за даташитом RM0008 — 14 МГц. // Ділимо 72 / 6 = 12 МГц — безпечно і максимально швидко. RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_ADCPRE) | RCC_CFGR_ADCPRE_DIV6; // 3. Конфігуруємо PA0 як аналоговий вхід. // Очищення всіх 4 бітів конфігурації пінa 0 дає MODE=00, CNF=00 — // це і є "Analog mode" за таблицею з RM0008. GPIOA->CRL &= ~(0xF << (0 * 4)); // 4. Час вибірки (Sample Time) для каналу 0 — мінімальний, 1.5 циклу, // заради максимальної швидкості. ADC1->SMPR2 &= ~ADC_SMPR2_SMP0; // 5. Базова конфігурація режиму роботи. ADC1->CR1 = 0; // незалежний режим, без додаткових опцій ADC1->CR2 |= ADC_CR2_CONT; // безперервний режим ADC1->CR2 |= ADC_CR2_ADON; // перше "натискання" ADON — подаємо живлення на ADC delayMicroseconds(10); // час на стабілізацію заліза після подачі живлення // 6. Апаратне калібрування — вбудована фіча STM32F1, якої немає в AVR. ADC1->CR2 |= ADC_CR2_CAL; while (ADC1->CR2 & ADC_CR2_CAL); // чекаємо, поки прапорець калібрування згасне сам // 7. Друге "натискання" ADON фактично запускає безперервний цикл вимірювань. ADC1->CR2 |= ADC_CR2_ADON; ADC1->CR2 |= ADC_CR2_SWSTART; } void loop() { // Процесор взагалі не бере участі в оцифруванні — // ADC працює у фоні сам по собі. int rawValue = ADC1->DR; // миттєве зчитування регістра даних, блокування = 0 float voltage = (rawValue * 3.3) / 4095.0; Serial.print("STM32_Register_ADC:"); Serial.print(rawValue); Serial.print(",Voltage:"); Serial.println(voltage); delay(5); // штучна пауза лише щоб графік у Serial Plotter не літав як скажений } |
⚠ Дві речі варто перевірити особисто на своєму залізі, перш ніж показувати комусь цей код як остаточний:
По-перше, крок 7 покладається на класичний трюк “подвійного ADON” — коли
EXTTRIG/EXTSELлишаються не налаштованими (за замовчуванням 0), другий запис у бітADONсам по собі запускає перетворення, а рядокSWSTARTв такому разі просто ігнорується апаратно (нешкідливо, але й без ефекту). Це поширений і робочий патерн у прикладах для STM32F103, але поведінка периферії — тонка річ; перш ніж показувати цей код студентам як канонічний, варто підтвердити на реальній платі, щоADC1->DRдійсно оновлюється безперервно, а не застряг на одному значенні.По-друге,
SMP0виставлений на мінімальний час вибірки (1.5 циклу) заради максимальної швидкості — але, як і з prescaler на Uno в другій статті, швидкість тут не безкоштовна. Коротший час вибірки означає, що внутрішній конденсатор sample-and-hold менше встигає зарядитись від джерела сигналу — і чим вищий вихідний опір джерела (а дільник напруги з резисторами кОм-діапазону саме таким і є), тим сильніша похибка. Для демо на потенціометрі це непомітно; для точних вимірів через дільник — варто спробувати збільшити час вибірки (наприклад,SMP0на кілька тактів більше) і порівняти стабільність показань.
◆ Бонус: апаратне калібрування — те, чого немає в AVR
Зверни увагу на крок 6 — ADC_CR2_CAL. Це не просто “гарний тон”, а вбудована апаратна функція STM32F1: чип сам вимірює й компенсує внутрішні похибки перетворювача при кожному калібруванні. Це принципово інша філософія за той “секретний вольтметр” з першої статті серії, де ми калібрували AVR вручну, мультиметром, підбираючи константу під конкретний екземпляр чипа. STM32 частину цієї роботи бере на себе сам, апаратно, за 10 мікросекунд — ще один наочний приклад того, наскільки далі просунулась периферія в 32-бітних ARM-чипах порівняно з 8-бітним AVR.
◆ Типові помилки
- Підключити 5V сигнал напряму до PA0 без дільника — миттєве пошкодження піна чи всього чипа.
- Розрахувати дільник “впритул” до 3.3V без запасу — на межі допуску, а не з комфортним відступом.
- Забути об’єднати GND між джерелом сигналу і Blue Pill — без спільної землі дільник напруги працює непередбачувано.
- Скопіювати назви регістрів з чужого прикладу не перевіривши — як побачили вище, навіть згенерований код може містити помилки в іменах CMSIS-констант, які просто не скомпілюються.
- Плутати “резистор обмеження струму” (один резистор, захищає від струму, але не знижує напругу) з “дільником напруги” (два резистори, справді знижує напругу) — це різні речі з різним призначенням.
◆ Домашнє завдання
- Скопіюйте код у свою Arduino IDE, перевірте компіляцію і роботу на реальній платі.
- Змініть
RCC_CFGR_ADCPRE_DIV6наDIV8чиDIV4, порахуйте нову тактову частоту ADC і звірте з максимумом 14 МГц із даташита RM0008. - Розрахуйте резистори дільника для виміру автомобільного акумулятора на 12V (підказка: на виході має бути не більше 3.0V — навмисно з запасом, а не впритул до 3.3V).
- Якщо є осцилограф чи логічний аналізатор — перевірте, чи справді
ADC1->DRоновлюється безперервно в режимі, описаному вище.
◆ Запитання для самоперевірки
- Чим “режим безперервного перетворення” концептуально відрізняється від звичайного
analogRead()? - Чому резистор обмеження струму і дільник напруги — не одне й те саме, і чим небезпечна ця плутанина?
- Навіщо STM32 потребує апаратного калібрування ADC, а AVR у попередніх статтях калібрували вручну?
- Що станеться, якщо виставити занадто короткий час вибірки (
SMP0) при високому вихідному опорі джерела сигналу? - Чому запас “3.23V формально під 3.3V” вважається тонким, і яке значення варто цілити натомість?
◆ Далі в серії
У фінальній частині розбираємо найпотужнішу інженерну технологію цього курсу — DMA (Direct Memory Access), яка дозволить ADC записувати тисячі точок сигналу в пам’ять взагалі без участі процесора. І зберемо все разом — бездротовий Wi-Fi осцилограф на базі ESP32, з яким ця серія і починалась.
Коли залізо стабільно показує числа на екрані — рухаємось до фіналу.