Самописец из урока про график датчика берёт пробу раз в секунду и честно рассказывает про ночную температуру, но всё, что быстрее пары десятков миллисекунд, он пропускает без следа: выборка о событии просто не узнаёт. А смотреть хочется именно быстрое — фронт ручки потенциометра, пульсации питания, дребезг кнопки, который фильтр прячет, а я хочу видеть своими глазами. Компьютерный вариант — Serial Plotter — не живёт без провода, и переносить его к макетке каждый раз надоедает.
Здесь собираю мини-осциллограф на том же OLED: буфер на 64 отсчёта, развёртка от потенциометра, упрощённый триггер по порогу. Это не прибор для ремонта материнских плат — потолок у АЦП Uno порядка десяти тысяч отсчётов в секунду, и о мегагерцах речи нет. Но для сигналов в диапазоне от единиц миллисекунд до секунд — а это весь человеческий масштаб кнопок и ручек — окно в 64 точки показывает ровно то, что нужно. Дискретизации и здесь хватает: ручка потенциометра не меняется быстрее пальца, а кнопка не дребезжит быстрее контакта.
Два честных отличия от самописца заранее. Первое: здесь выборка идёт быстрее кадра, поэтому буфер пишется независимо от экрана. Второе: без триггера картинка плывёт — каждый кадр начинается с произвольной фазы сигнала; лечится это взводом по фронту через порог, и упрощённую версию триггера мы поставим в трёх состояниях. Компоненты урока ручка-в-ручку повторяют задачу: сигнал и развёртка на двух потенциометрах, кнопка для взвода — на плате к ним добавится только сам дисплей на I2C.
Выборка и развёртка: что успевает Uno
Один analogRead на Uno занимает порядка ста микросекунд: десять бит, тринадцать тактов АЦП плюс накладные расходы на вызов. Потолок — около 9-10 тысяч отсчётов в секунду, и это потолок не кода, а железа. Откуда берутся 0-1023 и почему вход нельзя оставлять висеть в воздухе — база из урока про analogRead; здесь та же функция, просто нас интересует не величина, а её скорость.
Геометрия экрана знакома по самописцу: 64 точки с шагом 2 пикселя закрывают всю ширину, высота — map(v, 0, 1023, 62, 2), линии между соседями — drawLine. Новое — темп: буфер 64 int занимает 128 байт, и именно шаг между выборками, развёртка, решает, что попадёт в окно. Окно — это 64, умноженные на шаг: при миллисекунде на точку — 64 мс, при тридцати — почти две секунды. Развёртку я вешаю на вторую ручку: map(analogRead(A1), 0, 1023, 1, 30) — сигнал идёт с A0, темп съёмки крутится рукой.
Цена кадра никуда не делась: 1024 байта — около 92 мс по I2C на 100 кГц, и обновление экрана раз в 150-200 мс занимает больше половины шины — замер кадра и границы темпа я разбирал отдельно. Поэтому сборка кадра и выборка независимы: буфер тикает по своим миллисекундам, экран — по своим, и рисование никогда не задерживает съёмку.
Память на Uno напоминает о себе сразу: 128 байт под буфер против 1024 байт, которые уже съел буфер дисплея, — из 2 КБ ОЗУ свободным остаётся меньше половины ещё до первого Serial.println. Буфер из int я оставил для универсальности: если сигнал гарантированно в 0-255, переход на byte высвобождает 64 байта — маловато для честного осциллографа, но достаточно для второго канала, если он понадобится.
| Развёртка | Окно 64 точек | Что разглядеть |
|---|---|---|
| 1 мс/точка | 64 мс | дребезг кнопки, фронт ручки, спайк питания |
| 5 мс/точка | 320 мс | поворот потенциометра рукой, кнопка целиком |
| 30 мс/точка | около 2 с | включение нагрузки, пульсации, ход ручки |
| 100 мс и реже | 6 с и больше | уже самописец из урока про график датчика |
У любого окна есть слепая зона, и о ней предупреждаю заранее: событие короче двух шагов развёртки прибор не покажет вовсе — точка либо попадает на него, либо нет. Это упрощённый Найквист: чтобы различить фронт, нужно минимум две точки на него. Развёртка 1 мс на точку различает события в единицы миллисекунд; писк на 20 кГц с периодом 50 мкс — уже нет, и никакой код это не починит, потому что каждый отсчёт сам стоит сто микросекунд. Потолок прибора держите в голове до того, как пойдёте искать на нём тональный сигнал телефона.
Второй честный нюанс — шум младшего разряда. АЦП плавает на одну-две единицы даже с заземлённым входом, и на следе из 64 точек это заметное дрожание линии. Усреднение лечит дрожь, но убивает фронты: бегущее среднее по четырём точкам смазывает дребезг, ради которого всё затевалось. Мой компромисс — рисовать сырое, а усреднять только цифру в шапке, если она вообще нужна: смотреть глазами и фильтровать головой — разные задачи, и прибор должен уметь обе.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1); const int N = 64; // 64 точки по 2 пикселя — вся ширинаint buf[N];int head = 0;int idx = 0; // сколько точек набрано после триггераint sost = 0; // 0 живой, 1 взведён, 2 набор, 3 стоп-кадрint pred = 0; // прошлый отсчёт для фронт-детектаint shag = 5; // мс на точку — развёрткаunsigned long lastSmp = 0;unsigned long lastKadr = 0;bool bPrev = false; void setup() { display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE); pinMode(2, INPUT_PULLUP); // кнопка: взвод триггера и выход из стоп-кадра Serial.begin(9600);} void ekran() { display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 0); if (sost == 0) display.print("RUN "); else if (sost == 1) display.print("ARM "); else if (sost == 2) display.print("SET "); else display.print("STOP"); display.print(" "); display.print(shag); display.println(" ms/pt"); display.drawLine(0, 63, 127, 63, SSD1306_WHITE); // база display.drawLine(0, 32, 127, 32, SSD1306_WHITE); // уровень порога 512 int xPrev = 0; int yPrev = map(buf[head], 0, 1023, 62, 2); for (int i = 1; i < N; i++) { int j = (head + i) % N; int x = i * 2; int y = map(buf[j], 0, 1023, 62, 2); display.drawLine(xPrev, yPrev, x, y, SSD1306_WHITE); xPrev = x; yPrev = y; } display.display();} void loop() { shag = map(analogRead(A1), 0, 1023, 1, 30); // ручка развёртки bool b = (digitalRead(2) == LOW); if (b && !bPrev) { if (sost == 3) { // из стоп-кадра — снова живой режим sost = 0; } else if (sost == 0) { // взводим триггер sost = 1; pred = analogRead(A0); } } bPrev = b; if (sost == 0 && millis() - lastSmp >= shag) { lastSmp = millis(); buf[head] = analogRead(A0); head = (head + 1) % N; } if (sost == 1) { // ждём восходящий фронт через середину int v = analogRead(A0); if (v > 512 && pred <= 512) { sost = 2; head = 0; idx = 1; buf[0] = v; lastSmp = millis(); Serial.println("TRIG"); } pred = v; } if (sost == 2 && millis() - lastSmp >= shag) { // набор кадра lastSmp = millis(); buf[idx] = analogRead(A0); idx = idx + 1; if (idx >= N) { sost = 3; // кадр готов — стоп Serial.println("STOP-KADR"); } } if (millis() - lastKadr >= 150) { lastKadr = millis(); ekran(); }}Зачем это без осциллографа: дребезг глазами
Самое наглядное применение — увидеть то, что фильтры прячут. Подключите кнопку между A0 и GND, включите pinMode(A0, INPUT_PULLUP): свободный вход читается как 1023, нажатие валит его к нулю. На развёртке в 1 мс на точку фронт нажатия разложится на две-десять точек болтанки — это и есть дребезг, из-за которого один тап считается как три. Механику и лечение я разбирал в уроке про дребезг; здесь вы впервые увидите врага вживую, и после этого конденсатор 100 нФ или программная задержка перестанут быть магией. Тот же фокус покажет и реле: контакт реле с искрением болтает вход так, что ваша подтяжка выглядит спасением.
В симуляторе та же логика живёт в текстовой сетке: след из звёздочек по восьми строкам, прочерки — уровень порога, точки — база. Кнопка D2 взводит триггер, ручка A0 — сигнал, ручка A1 — темп съёмки; после фронта через середину движок набирает 64 точки и замирает в стоп-кадре. Фильтровать, усреднять и строить красоту поверх этих чисел вы уже умеете — двойная буферизация кадра тут не меняется ни на байт.
Одна оговорка про честность симулятора: контакт он не изображает, поэтому дребезга в нём не будет — ручка крутится плавно и врёт только со скоростью вашей руки. Зато буфер, развёртка, триггер и стоп-кадр в симе и на плате — один и тот же код без правок: машина состояний переносится как есть, меняется только способ рисования.
- analogRead на Uno — порядка 100 мкс: потолок 9-10 тысяч отсчётов в секунду; развёртка 1-30 мс на точку выбирает окно
- Буфер 64 int — 128 байт; окно равно 64 × шаг: при 1 мс — 64 мс, там виден дребезг кнопки
- Сигнал — A0, развёртка — A1, кнопка D2: взвод триггера и выход из стоп-кадра
- Упрощённый триггер: ждём восходящий фронт через 512, набираем 64 точки, замираем — картинка перестаёт плыть
- Выборка и экран независимы: буфер тикает по своим миллисекундам, кадр обновляется раз в 150-200 мс
- 1. Окно осциллографа при развёртке 1 мс на точку и буфере 64 отсчёта?
- 2. Сколько занимает один analogRead на Uno?
- 3. Что делает упрощённый триггер?
- 4. Как подключить кнопку, чтобы разглядеть её дребезг на осциллографе?
- 5. Шина 100 кГц, кадр 92 мс, обновление экрана раз в 100 мс. Что происходит?
Домашнее задание: на плате посадите кнопку на A0 с INPUT_PULLUP, выкрутите развёртку в 1 мс и снимите её фронт в стоп-кадре; посчитайте точки болтанки — это и есть ваш личный дрейф дребезга. Второй опыт — медленно поверните ручку сигнала и посмотрите, как след из 64 точек заполняется слева направо после триггера.
Если захочется второго канала, буфер делится пополам — 32 точки на сигнал с шагом 4 пикселя, как прикидывали в уроке про самописец; развёртка при этом общая, а кнопка станет выбором канала. И финальное наблюдение из практики: после недели с этим прибором я стал писать кнопочные фильтры спокойнее — когда болтанку контакта видел глазами, пороги гистерезиса подбираются с первого раза, а не с пятого.
Границу прибора держите в голове: мегагерцы и отрицательные напряжения — это настоящий осциллограф за другие деньги. А наш формат — глаз для человеческого масштаба. Дальше вернёмся к тому, что экран делает красивым: анимации загрузки, спиннер и пасхалки для тех случаев, когда прибор должен не только считать, но и подмигивать.
Частые вопросы по теме урока
- Окно осциллографа при развёртке 1 мс на точку и буфере 64 отсчёта?
- Окно — 64 × шаг: 64 точки по миллисекунде — 64 мс. Именно поэтому дребезг кнопки на 2-10 мс виден, а на самописце с секундной выборкой его нет. 64 мс
- Сколько занимает один analogRead на Uno?
- Десять бит, тринадцать тактов АЦП плюс накладные расходы дают порядка ста микросекунд на отсчёт — потолок железа, а не кода. Около 100 мкс — потолок порядка 9-10 тысяч отсчётов в секунду
- Что делает упрощённый триггер?
- Прошлый отсчёт ниже середины, текущий выше — старт набора. Без привязки к фронту каждый кадр начинался бы с произвольной фазы и плыл. Ждёт восходящий фронт через порог и только тогда набирает кадр
- Как подключить кнопку, чтобы разглядеть её дребезг на осциллографе?
- С внутренней подтяжкой вход выдаёт 1023 в покое и падает к нулю при нажатии. analogRead честно видит эту границу со всей болтанкой фронта. Между A0 и GND с INPUT_PULLUP: свободный вход 1023, нажатие валит в ноль
- Шина 100 кГц, кадр 92 мс, обновление экрана раз в 100 мс. Что происходит?
- 92 мс из каждых 100 — около 92 процентов занятой шины. Часы и датчики на тех же проводах начинают отставать; рабочий темп — 150-500 мс. Шина занята практически целиком — соседи захлёбываются