◆ Фінальний модуль
У третій частині ми навчились керувати ADC на STM32 Blue Pill напряму через регістри — і отримали колосальну швидкість. Але є межа: якщо хочемо читати сигнал на максимальній швидкості (до 1 000 000 вимірів/сек) і складати їх у масив, процесору все одно доведеться щоразу відволікатись — забрати число з ADC1->DR, покласти в пам’ять, повернутись до роботи. На частоті 1 МГц це мільйон переривань за секунду. Жоден процесор довго так не протягне.
Сьогодні розбираємо головну магію архітектури ARM — DMA, і об’єднуємо швидкість STM32 з бездротовими можливостями ESP32, щоб зібрати справжній Wi-Fi осцилограф, графік якого видно на екрані телефона.
◆ Що таке DMA і чому це магія
DMA (Direct Memory Access) — окремий апаратний “копроцесор” всередині чипа, єдина задача якого — переносити байти з одного місця в інше, без участі головного процесора.
Без DMA, навіть з регістрами: ADC виміряв напругу → підняв прапорець → процесор кинув поточні справи → скопіював число в масив → повернувся до роботи. На частоті 1 МГц процесор просто захлинеться в цих мікрозадачах.
З DMA: процесор каже “ось тобі масив на 1024 комірки, я пішов малювати графіку. Коли ADC закінчить вимір — сам перенеси дані туди. Заповниш увесь масив — розбуди мене”. Поки ADC і DMA працюють у парі, залізо оцифровує сигнал з нульовим навантаженням на центральний процесор.
◆ Вправа 4.1 — випускний проєкт “Бездротовий Wi-Fi осцилограф”
Об’єднуємо дві плати:
- STM32 Blue Pill — “супершвидке око”. Через тандем ADC+DMA збирає 1024 точки сигналу і “вистрілює” бінарним пакетом через апаратний UART в ESP32.
- ESP32 — “комунікатор”. Приймає пакет, піднімає Wi-Fi точку доступу, малює графік на веб-сторінці.
Схема з’єднання плат:
|
1 2 3 4 |
[ STM32 Blue Pill ] [ ESP32 ] GND ─────────────────────────────────────── GND (обов'язково спільна!) PA9 (TX) ───────────────────────────────── RX2 (GPIO 16) PA0 (вхід сигналу 0-3.3V) |
Потенціометр чи датчик живимо 3.3V від будь-якої з плат.
◆ Код для STM32 Blue Pill — ADC + DMA + UART
|
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 |
#define BUFFER_SIZE 1024 uint16_t adc_buffer[BUFFER_SIZE]; void setup() { Serial1.begin(500000); // швидкий апаратний UART1, пін PA9 // 1. Тактування GPIOA, ADC1 і DMA1. // Правильні імена бітів — без кінцевої "R" (той самий момент, // що й у статті 3: сам регістр вже називається ENR, біт — просто EN). RCC->APB2ENR |= RCC_APB2ENR_ADC1EN | RCC_APB2ENR_IOPAEN; RCC->AHBENR |= RCC_AHBENR_DMA1EN; // 2. PA0 як аналоговий вхід GPIOA->CRL &= ~(0xF << (0 * 4)); // 3. Тактова частота ADC — те, чого бракувало в чернетці цього модуля! // За замовчуванням ADCPRE = APB2/2 = 36 МГц при 72 МГц шини — // вдвічі більше за максимум 14 МГц з RM0008. Обов'язково ділимо явно, // так само, як робили в третій статті серії. RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_ADCPRE) | RCC_CFGR_ADCPRE_DIV6; // 4. DMA1, канал 1 (фізично прив'язаний до ADC1) DMA1_Channel1->CPAR = (uint32_t)&(ADC1->DR); // звідки брати дані DMA1_Channel1->CMAR = (uint32_t)adc_buffer; // куди класти DMA1_Channel1->CNDTR = BUFFER_SIZE; // скільки точок за раз // Читання з периферії, інкремент пам'яті, 16-біт дані, циклічний буфер DMA1_Channel1->CCR = DMA_CCR_MINC | DMA_CCR_MSIZE_0 | DMA_CCR_PSIZE_0 | DMA_CCR_CIRC | DMA_CCR_EN; // 5. Налаштування ADC ADC1->CR2 |= ADC_CR2_CONT; // безперервний режим ADC1->CR2 |= ADC_CR2_DMA; // дозволити ADC "штовхати" дані в DMA ADC1->CR2 |= ADC_CR2_ADON; // перше "натискання" — живлення delayMicroseconds(10); ADC1->CR2 |= ADC_CR2_CAL; while (ADC1->CR2 & ADC_CR2_CAL); ADC1->CR2 |= ADC_CR2_ADON; ADC1->CR2 |= ADC_CR2_SWSTART; } void loop() { // Прапорець DMA: чи заповнився весь буфер if (DMA1->ISR & DMA_ISR_TCIF1) { DMA1->IFCR |= DMA_IFCR_CTCIF1; // скидаємо прапорець Serial1.write((uint8_t*)adc_buffer, BUFFER_SIZE * 2); // 1024 точки, бінарно delay(50); // пауза між кадрами } } |
⚠ Реальна інженерна проблема, яку варто знати перш ніж називати цей код production-ready. DMA працює в циклічному режимі (
DMA_CCR_CIRC) — тобто, щойно буфер заповнився, DMA одразу починає писати в нього знову, з початку, не чекаючи нікого. При 500000 baud передача 2048 байт (BUFFER_SIZE * 2) черезSerial1.write()займає близько 41 мс. А ADC на швидкості під 1 МГц встигає заповнити ті самі 1024 комірки буфера заново десятки разів за той самий час. Це означає: покиSerial1.write()повільно вичитує масив байт за байтом, DMA паралельно й непомітно переписує ці самі байти новими даними — і на приймальному боці (ESP32) можна отримати “розірваний” кадр, зшитий з шматків різних моментів часу.Це не гіпотетична дрібниця, а класична проблема паралельного доступу до пам’яті, яку в промислових системах вирішують подвійною буферизацією (double buffering / ping-pong buffer): поки один буфer передається по UART, DMA пише в другий, і вони міняються ролями по черзі. STM32 має для цього спеціальний режим переривань “half-transfer” — можна почати передавати першу половину буфера, поки DMA дописує другу. Реалізація такого double-buffering — гарне домашнє завдання підвищеної складності нижче, а не те, що варто показувати як “готове рішення” без застереження.
◆ Код для ESP32 — Wi-Fi веб-сервер і графік
ESP32 створює власну Wi-Fi точку доступу ESP32-Oscilloscope. Підключаєтесь до неї з телефона, відкриваєте в браузері 192.168.4.1 і бачите графік.
|
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 |
#include <WiFi.h> #include <WebServer.h> const char* ssid = "ESP32-Oscilloscope"; WebServer server(80); #define BUFFER_SIZE 1024 uint16_t local_buffer[BUFFER_SIZE]; String html_page = "<html><head><meta http-equiv='refresh' content='0.1'></head><body>" "<h1>IoT Wi-Fi Oscilloscope</h1>" "<canvas id='osc' width='800' height='400' style='border:1px solid #000'></canvas>" "<script>" "var canvas = document.getElementById('osc'); var ctx = canvas.getContext('2d');" "ctx.beginPath(); ctx.moveTo(0,200);" "[%POINTS%]" "ctx.stroke();" "</script></body></html>"; void handleRoot() { String pointsStr = ""; for (int i = 0; i < BUFFER_SIZE; i += 4) { // крок 4 — для швидшого рендеру int y = map(local_buffer[i], 0, 4095, 400, 0); // інверсія для Canvas pointsStr += "ctx.lineTo(" + String(i) + "," + String(y) + ");"; } String currentHtml = html_page; currentHtml.replace("[%POINTS%]", pointsStr); server.send(200, "text/html", currentHtml); } void setup() { Serial.begin(115200); Serial2.begin(500000, SERIAL_8N1, 16, 17); // RX2 на GPIO16 WiFi.softAP(ssid); Serial.println("Wi-Fi запущено. IP адреса: 192.168.4.1"); server.on("/", handleRoot); server.begin(); } void loop() { server.handleClient(); if (Serial2.available() >= BUFFER_SIZE * 2) { Serial2.readBytes((uint8_t*)local_buffer, BUFFER_SIZE * 2); } } |
⚠ Дві свідомі спрощення, про які варто знати, а не видавати за “правильний” фінальний підхід:
По-перше,
WiFi.softAP(ssid)без пароля створює повністю відкриту мережу — прийнятно для 30-хвилинного демо на закритому занятті, категорично неприйнятно для будь-чого поза навчальною лабораторією. Другий аргументWiFi.softAP(ssid, password)вирішує це в одну зміну коду.По-друге, підхід “перезавантажувати всю сторінку кожні 100мс через
meta http-equiv='refresh'” і будувати рядок з тисячі конкатенаційStringпри кожному запиті — робочий, простий для розуміння, але грубий спосіб. Професійний варіант — WebSockets: одне постійне з’єднання, сервер штовхає тільки нові дані (не всю HTML-сторінку заново), браузер малює на Canvas без перезавантаження. Для навчальної демонстрації принципу поточний варіант цілком виправданий — він показує суть (STM32 вимірює, ESP32 показує) без додаткової складності протоколу. Перехід на WebSockets — природний наступний крок, коли захочете зробити з цього щось більше за демо.
◆ Що ми фактично отримали
- Сигнал вимірюється на швидкості ARM-архітектури через тандем ADC+DMA — без втрати жодного такту процесора на копіювання даних вручну.
- Дані передаються не повільним текстом (
Serial.println()), а компактним бінарним потоком — вдвічі байт на семпл замість символьного представлення числа. - ESP32 бере на себе всю мережеву роботу (Wi-Fi стек, HTTP-сервер), лишаючи STM32 повністю зосередженим на швидкому вимірюванні.
Підключіть телефон до мережі ESP32-Oscilloscope, відкрийте 192.168.4.1 в браузері — перед вами власний бездротовий осцилограф.
◆ Типові помилки
- Забути налаштувати
RCC_CFGR_ADCPRE— ADC працюватиме поза специфікацією даташита, результати можуть бути недостовірними навіть якщо код компілюється й “начебто працює”. - Скопіювати назви CMSIS-констант з чужого прикладу не перевіривши — як бачили в статтях 3 і 4, навіть згенерований код регулярно містить друкарські помилки в іменах регістрів.
- Читати циклічний DMA-буфер напряму під час повільної передачі, вважаючи дані “замороженими” — насправді DMA продовжує писати паралельно.
- Лишити відкриту Wi-Fi мережу поза межами навчального стенду.
- Заплутатись, який UART на якій платі —
Serial1на STM32 (апаратний, пін PA9) іSerial2на ESP32 (програмно призначені піни 16/17) — це різні об’єкти на різних платах, легко переплутати в перших спробах.
◆ Домашнє завдання
- Зберіть обидві плати, перевірте передачу даних через UART між ними осцилографом чи логічним аналізатором, якщо є.
- Додайте пароль до Wi-Fi точки доступу ESP32.
- Підвищена складність: реалізуйте double-buffering на STM32 — два масиви замість одного, перемикання між ними по перериванню half-transfer/transfer-complete, щоб гарантовано передавати тільки “застиглі”, не перезаписувані дані.
- Якщо відчуваєте впевненість — спробуйте замінити
meta refresh+String-конкатенацію на WebSocket-з’єднання для плавнішого й ефективнішого оновлення графіка.
◆ Запитання для самоперевірки
- Чим DMA принципово відрізняється від просто “швидкого” зчитування регістра в циклі?
- Чому передача бінарних даних (
Serial1.writeзuint8_t*) ефективніша заSerial.println()з числами? - У чому конкретно полягає race condition між циклічним DMA-буфером і повільною UART-передачею, і як double-buffering її вирішує?
- Чому в цьому коді критично важливо явно налаштувати
RCC_CFGR_ADCPRE, а не покладатись на значення за замовчуванням? - Які конкретні компроміси зроблено заради простоти в ESP32-коді (мережа, спосіб оновлення сторінки), і як би ви їх усунули для production-версії?
◆ Підсумок серії — від новачка до профі
За чотири частини ви пройшли шлях від “чому не Blink” до власноруч зібраного бездротового осцилографа:
- Базову теорію аналогового світу, квантування й опорної напруги.
- Різницю розрядностей 10 біт (Uno) проти 12 біт (ESP32/STM32) — і чому “більше біт” не означає “точніше”.
- Асинхронне програмування через
millis()і апаратне прискорення через prescaler-регістри. - Математичну цифрову фільтрацію (Moving Average) для очищення сигналу від шуму.
- Низькорівневе програмування регістрової периферії ARM Cortex-M3 — без жодної Arduino-обгортки.
- Технологію прямого доступу до пам’яті (DMA) і базову архітектуру розподіленої IoT-системи.
Курс по ADC на цьому завершено. Куди рухатись далі — ШІМ і керування моторами, протоколи I2C/SPI/UART для розмови між пристроями, чи переривання й таймери для реакції на події за наносекунди — тема для наступної серії.