Ардуїно як профі. Частина 2: Смерть delay(), прискорення заліза та цифрова фільтрація

◆ Де ми зупинились

У першій частині ми розібрались, що таке ADC, порівняли 10-біт Uno з 12-біт ESP32 і навіть навчились вичавлювати з Mega точність до 1.07 мВ через внутрішній референс 1.1V. Але реальні проєкти одразу підкидають двох нових ворогів точних вимірювань: зависання коду через delay() і хаотичний цифровий шум на графіках.

Сьогодні прибираємо delay() назавжди, розганяємо внутрішнє залізо ADC через прямий запис у регістри, і пишемо перший математичний DSP-фільтр — вже на Raspberry Pi Pico.

◆ Чому delay() — ворог точних вимірів

Ефект “сліпоти” мікроконтролера

Коли ви пишете delay(20), мікроконтролер повністю зупиняє виконання програми на 20 мілісекунд. Процесор буквально засинає. Якщо саме в цей момент на аналоговому вході станеться швидкий сплеск напруги чи прийде важлива команда по Serial — плата цього просто не побачить. Для якісного оцифрування сигналу заміри мають відбуватись через строго однакові, передбачувані проміжки часу — а delay() гарантує лиш одне: що ваш код на цей час випаде з реальності.

Це, до речі, той самий ефект, який ми мимохідь зачепили в першій статті — приклад із блогу про аліасинг, де Serial.println() посеред циклу семплування зводив нанівець реальну частоту дискретизації. delay() — просто найбільш очевидний і найгрубіший варіант тієї самої хвороби.

Звідки береться цифровий шум

Підключіть довгий дріт до аналогового піна — і графік у Serial Plotter почне тремтіти, навіть якщо датчика ніхто не чіпає. Це не несправність плати. Наші будинки буквально пронизані електромережею на частоті 50 Гц, а довгий провід датчика працює як антена, яка ловить цей фон. Чим довший дріт і чим вищий вихідний опір джерела сигналу — тим сильніше наведення.

⚠ Якщо шум заважає навіть після програмної фільтрації (яку зберемо нижче) — це сигнал звернути увагу на апаратну частину: конденсатор 10 нФ між сигнальним піном і землею прямо на платі, коротший і екранований провід, чи розв’язка живлення ADC (невеликий резистор + конденсатор між AVCC і живленням) прибирають значну частку “сміття” ще до того, як сигнал долетить до коду.

Щоб очистити корисний сигнал від залишку шуму — використовують математичну фільтрацію. Саме цим і займемось у другій вправі.

◆ Вправа 2.1 — осцилограф без гальм і розгін заліза (Arduino Uno)

Тут дві незалежні речі одночасно: переводимо опитування ADC на асинхронний таймер через millis(), щоб плата ніколи не зависала, і прискорюємо сам ADC напряму через регістри.

За замовчуванням analogRead() на Uno займає близько 100-110 мкс. Причина — внутрішній тактовий генератор ADC працює на частоті процесора, поділеній на 128 (це prescaler за замовчуванням, обраний саме так, щоб втрапити в рекомендований даташитом діапазон 50-200 кГц для повної 10-бітної точності). Ми змінимо коефіцієнт ділення на 16 через регістр ADCSRA, скоротивши час одного заміру приблизно до 13-16 мкс.

Схема підключення: будь-який аналоговий датчик чи звичайний потенціометр на пін A0 Arduino Uno. Живлення датчика — 5V чи 3.3V, крайня ніжка — на GND.

Чесне застереження, якого немає в більшості туторіалів: прискорення ADC — це не безкоштовний обід. Даташит вимагає 50-200 кГц тактової частоти ADC саме для збереження повної 10-бітної точності. При prescaler=16 (тактова частота ADC ~1 МГц) ви виходите далеко за рекомендований діапазон, і незалежні тести спільноти (зокрема відомий ресурс Ніка Геммона по AVR-регістрах) показують, що ефективна точність на такій швидкості падає приблизно до 8.5 біт замість повних 10. Для швидкого прототипування чи де важлива саме швидкість реакції (наприклад, майбутній осцилограф з цієї серії) — це прийнятний компроміс. Якщо ж потрібна максимальна точність виміру — лишайте prescaler за замовчуванням чи обирайте проміжне значення (наприклад, 32).

◆ Вправа 2.2 — математичний DSP-фільтр Moving Average (Raspberry Pi Pico)

Переходимо на 32-бітну Raspberry Pi Pico — тактова частота 133 МГц робить її куди зручнішою платформою для цифрової обробки сигналів (DSP), ніж 16-мегагерцевий AVR.

Принцип Moving Average (рухоме середнє): код тримає в пам’яті кільцевий буфер на N останніх вимірювань. З приходом кожного нового значення найстаріше в буфері викидається, а по решті рахується середнє арифметичне. Ефект — гострі піки шуму “розмазуються” й зникають, а повільна, справжня зміна сигналу (наприклад, рух руки над датчиком) залишається помітною.

Схема підключення для Pi Pico:

Результат у Serial Plotter: дві лінії. Raw_Signal тремтить і скаче — це і є той самий шум. Filtered_Signal — плавна, повторює реальний рух без “бруду”. Ви щойно написали свій перший цифровий фільтр.

◆ Бонус: зворотний бік прискорення — oversampling заради додаткової точності

У вправі 2.1 ми пришвидшили ADC ціною точності. А що, якщо потрібно навпаки — вичавити з дешевого 10-бітного ADC точність вище номінальної? Тут в гру вступає протилежна техніка — oversampling з децимацією.

Ідея (детальний опис — в офіційній аплікаційній нотатці Atmel AVR121): якщо в сигналі є трохи природного шуму (буквально той самий “тремтливий” Raw_Signal, який ми щойно фільтрували), можна підсумувати кілька вимірів і зсунути результат вправо — і це математично додає біти точності. Наприклад, підсумувавши 4 виміри й зсунувши суму на 1 біт, ви отримуєте один “виграний” додатковий біт роздільної здатності.

⚠ Це працює тільки за наявності певного рівня шуму (dither) — приблизно на рівні 1 молодшого розряду. На абсолютно “чистому” сигналі без жодного шуму oversampling нічого не додасть, бо немає що усереднювати.

Тобто в одному й тому самому чипі ховаються дві протилежні стратегії: prescaler=16 — коли важливіша швидкість, oversampling — коли важливіша точність. Обирати доведеться під конкретну задачу, і саме в цьому — різниця між “просто підключити датчик” і “спроєктувати вимірювальну систему”.

◆ Типові помилки

  • Лишити delay() десь у коді “на всякий випадок” — навіть одна забута затримка ламає рівномірність дискретизації для всієї програми.
  • Занадто маленьке вікно фільтра (FILTER_WINDOW) — фільтр майже не згладжує шум.
  • Занадто велике вікно — фільтр згладжує й реальні швидкі зміни сигналу, система починає “запізнюватись”.
  • Прискорити ADC (prescaler) і одразу ж намагатись отримати від нього максимальну точність — ці дві мети суперечать одна одній, треба свідомо обирати.
  • Забути ініціалізувати масив readings[] нулями — перші N вимірів після старту будуть заниженими, поки буфер не заповниться реальними даними.

◆ Домашнє завдання

  1. Зберіть вправу 2.1, переконайтесь через micros() навколо analogRead(), що вимір справді пришвidшився.
  2. Зберіть вправу 2.2 на Pi Pico, поекспериментуйте з FILTER_WINDOW — спробуйте 3, 10, 50, порівняйте плавність і затримку реакції.
  3. Спробуйте об’єднати обидва прийоми: розігнаний ADC на Uno + Moving Average фільтр — чи компенсує фільтр втрату точності від prescaler?
  4. Якщо є під рукою — спробуйте oversampling (підсумувати 4 виміри, зсунути на 1 біт) і порівняйте роздільну здатність з фільтрованим сигналом.

◆ Запитання для самоперевірки

  1. Чому delay() шкідливий саме для точних вимірів, а не просто “неефективний”?
  2. Звідки фізично береться 50-герцовий шум на довгому аналоговому дроті?
  3. Що саме змінює запис ADCSRA = (ADCSRA & 0xF8) | 0x04;, і чому саме ці біти?
  4. Яка ціна прискорення ADC через prescaler, і коли вона виправдана?
  5. Чому Moving Average згладжує шум, але не відновлює втрачену prescaler-ом точність — це два різні механізми чи один?

◆ Далі в серії

У Модулі 3 повністю відмовляємось від зручних функцій Arduino на користь низькорівневого програмування регістрів STM32 Blue Pill на чистому C — і розберемось, як зробити безпечний дільник напруги, щоб не спалити плату.

Коли будете готові зазирнути під капот ще потужнішого заліза — рухаємось далі.

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

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