Спор двух коллег я помню дословно. Первый: для анимации OLED нужен только SPI, I2C — для менюшек. Второй: I2C хватает всем, SPI — для тех, кто любит паять. Первый водил пальцем по экрану своей осциллограф-игрушки, второй тыкал в метеостанцию, которая и без разгона жила на батарейках годами. Оба ссылались на чужие замеры с чужих плат: у одного цифра была снята на ESP32, у второго — вообще на софтверной шине. Я устал слушать и сделал то, что должен был сделать сразу: один скетч, одна плата, три конфигурации шины — и собственные цифры за вечер.
Замер простой до неприличия: считаем кадры, отправленные display(), в течение секунды, печатаем результат в Serial и повторяем. В скетче меняю только две строки — частоту шины и конструктор дисплея: &Wire против &SPI. Всё остальное — Uno, библиотека Adafruit, полный кадр 1024 байта — остаётся общим, иначе замер сравнивал бы не шины, а скетчи.
Бумажную оценку мы уже делали в уроке про буфер и display(): 1024 байта по девять бит — 92 мс на 100 кГц и 23 мс на 400 кГц. Сейчас проверим эти числа на живом скетче и добавим третьего участника — SPI на 8 МГц из урока про SPI-подключение.
Скетч-замерщик: кадры по millis, окно в секунду
Сердце замера — два счётчика и окно. В каждом проходе loop рисуем кадр и отправляем display(), увеличивая счётчик kadr. Раз в 1000 мс по millis() записываем накопленное в fps и обнуляем окно. Никаких delay: цикл крутится сам, а темп задаёт только шина со своим временем передачи — замеряем упор именно в шину, а не в наш скедулер. Каркас тайминга тот же, что в millis вместо delay. Сам кадр для замера я беру простой — шапка и счётчик: чем проще кадр, тем ближе цифра к чистой шине; жирный интерфейс занизит результат, и это тоже честное число, если скетч не меняется между прогонами.
Два правила гигиены замера. Первое: кадр считается по display(), а не по проходам loop — без экрана loop пролетает за микросекунды, и счётчик завысит цифру в сотни раз. Второе: Serial-печать — раз в секунду, между окнами, а не из каждого кадра: строка на 9600 бод стоит около 30 мс, и печать на каждый кадр врежется в замеряемый интервал. Пары fps= и t= в выводе — привычка из Serial-отладки, чтобы числа читались в логе без сносок.
Про само окно — почему ровно секунда. Короче — и кадр, разрезанный границей, уедет в чужое окно: на I2C-100 кадр живёт девяносто миллисекунд, и окно в двести миллисекунд будет то пустым, то полным. Длиннее — и провалы усреднятся: окно в пять секунд спрячет короткое зависание от чужой передачи. Секунда — компромисс: числа целые, провал длиной в кадр ещё виден, а средняя цифра уже осмысленна.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1);// SPI-конфигурация того же замера:// Adafruit_SSD1306 display(128, 64, &SPI, 9, 8, 10); long lastOkno = 0; // граница секундного окнаlong kadr = 0; // кадры в текущем окнеlong fps = 0; // кадров за последнюю секунду void setup() { Serial.begin(9600); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // Wire.setClock(400000); // разгон I2C: кадр 92 -> 23 мс display.setTextColor(SSD1306_WHITE);} void loop() { display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 0); display.print("FPS test t="); display.println(millis() / 1000); display.display(); kadr = kadr + 1; if (millis() - lastOkno >= 1000) { lastOkno = millis(); fps = kadr; kadr = 0; Serial.print("fps="); Serial.print(fps); Serial.print(" t="); Serial.println(millis() / 1000); }}Числа моего стенда. I2C на 100 кГц: девять-десять кадров в секунду — бумажные 92 мс плюс время отрисовки в буфер, итог чуть ниже десяти. I2C на 400 кГц после Wire.setClock(400000): 23–25 кадров — оценка в 23 мс сходится, гуляние в пару кадров добавляют отрисовка и Serial. Замер я повторял с разным питанием — от USB и от лабораторного блока: цифры совпали, шине безразличен источник, пока стабилизатор платы жив. Обе цифры повторяемы: за вечер замера расхождение между прогонами не превысило единицы кадров.
SPI на 8 МГц — самое интересное: упор смещается из шины в скетч. С простым кадром из шапки и счётчика мой Uno выдавал счёт на десятки кадров в секунду и выше; с жирным интерфейсом из урока про графику цифра падала, но всё равно оставалась выше разогнанного I2C. Как отличить, куда именно упёрся ваш скетч: усложните кадр — добавьте зону. Если fps почти не дрогнул, вы упирались в шину; если упал пропорционально новой работе, узкое место — отрисовка в буфер. Сама передача при этом всегда целая: display() тащит все 1024 байта при любом интерфейсе, сэкономить можно только частотой вызовов. Формулировка «сколько именно» принципиально зависит от того, что вы рисуете: SPI снимает потолок шины, и дальше темп определяет ваш код.
Чужие замеры тем же скетчем не повторяются — и это нормально: форумные цифры про десятки fps по I2C и сотни по SPI сняты на других платах и других библиотеках, где и шина, и отрисовка быстрее. Вывод из моего замера не в абсолютных числах, а в порядке величин: десять, потом 23–25, потом десятки и выше. И в том, что скачок между первыми двумя точками стоит одну строку кода.
| Конфигурация | Кадр по бумаге | FPS на моём Uno |
|---|---|---|
| I2C 100 кГц (по умолчанию) | ~92 мс | 9–10 |
| I2C 400 кГц (Wire.setClock) | ~23 мс | 23–25 |
| SPI 8 МГц | ~1–2 мс на проводе | десятки и выше — упор в скетч, а не в шину |
Компромиссы: провода, скорость, память
Провода: четыре против семи — и это единственный ресурс, где I2C выигрывает всухую. Условие простое: на макетке с короткими перемычками разницы вы не почувствуете, а в корпусе с разъёмами каждый дополнительный провод — точка отказа и минута поиска на каждой сборке. Переносимый модуль с семью проводами соберётся с ошибкой раньше, чем с четырьмя, — и вы это почувствуете в самый неподходящий момент. В постоянных сборках я вдобавок сажаю экраны на штыревой разъём с ключом: семь проводов, собранных вслепую в темноте корпуса, перепутываются чаще, чем четыре.
Скорость: десять, 23–25, десятки и выше. Правило из урока про графику — 4–10 кадров в секунду для цифр и текста — означает, что дашборду хватает и стокового I2C. Разгон до 400 кГц бесплатен и полезен при соседях на шине: экран занимает её не девяносто процентов времени, а четверть. И держите в голове провода: разогнанная шина любит короткие линии — метровый хвост перемычек вернёт дрожь кадров даже на четырёхстах килогерцах. SPI оставьте сценариям, где темп нужен по-настоящему: анимация, развёртка, игра — и шинам, задыхающимся от соседей. И не путайте темп с плавностью: десять честных кадров с целой посылкой выглядят лучше тридцати рваных, наложенных друг на друга.
Память: буфер 1024 байта одинаков во всех трёх конфигурациях — SPI не освобождает ни байта ОЗУ, половина SRAM Uno уходит при любом интерфейсе. Мой итог с границей: экран один и данных мало — I2C на ста килогерцах и не думать; соседи на A4/A5 — сначала разгон до 400 кГц, потом, если тесно, переезд экрана на SPI; нужен темп — сразу SPI и замерщик из этого урока на стол. Про тайминги соседей по шине говорили на примере часов DS3231.
- Замер FPS: счётчик растёт при каждом display(), окно 1000 мс по millis(), печать парами fps= и t= раз в секунду
- Кадр считается по display(): проходы loop без экрана завышают цифру в сотни раз
- Мой стенд: I2C 100 кГц — 9–10 fps; 400 кГц — 23–25; SPI 8 МГц — упор в скетч, десятки кадров и выше
- Разгон до 400 кГц — одна строка Wire.setClock(400000) и запас для соседей по шине
- Буфер 1024 байта одинаков для всех шин: SPI экономит время, а не память
- Выбор с границей: дашборд — I2C; занятая шина — 400 кГц или переезд экрана на SPI; анимация — SPI и замер на стол
- 1. По какому событию увеличивать счётчик кадров в FPS-замере?
- 2. Что покажет замер, если считать кадры по проходам loop без экрана?
- 3. Сколько кадров в секунду даёт I2C 400 кГц на Uno с полным кадром?
- 4. Что меняется в ОЗУ платы при переходе с I2C на SPI?
- 5. На A4/A5 сидят RTC и датчик, экран тормозит их опрос. Первый шаг?
Три конфигурации, один скетч, три числа — спор о шинах теперь решается за вечер и по-честному. Дальше по разделу — железные сюрпризы: как отличить SH1106 от SSD1306, почему экран «тот же, но больше» молчит с рабочим скетчем и что делать со сдвигом на два пикселя.
Домашнее задание: прогоните замерщик на своей связке — запишите fps на ста килогерцах, добавьте Wire.setClock(400000) и снимите ещё раз; у кого SPI-модуль — третья точка. Если SPI-модуля под рукой нет, не страшно: первых двух точек достаточно, чтобы решить судьбу соседей по шине. Заодно сравните окна с Serial-печатью и без неё: разница покажет, сколько в ваш кадр вносит консоль. Числа запишите в комментарий к скетчу — при выборе шины для следующего экрана они пригодятся раньше, чем любой форум.
Частые вопросы по теме урока
- По какому событию увеличивать счётчик кадров в FPS-замере?
- Кадр существует только после display() — без него всё рисование живёт в буфере и шину не тратит, значит, и кадр в счёт не идёт. По вызову display()
- Что покажет замер, если считать кадры по проходам loop без экрана?
- Пустой loop пролетает за микросекунды, и счётчик меряет процессор, а не шину. Привязка к display() обязательна. Завышенную в сотни раз цифру
- Сколько кадров в секунду даёт I2C 400 кГц на Uno с полным кадром?
- Кадр в 23 мс даёт 23–25 fps с учётом отрисовки. На ста килогерцах тот же кадр стоит 92 мс — девять-десять кадров. Около 23–25
- Что меняется в ОЗУ платы при переходе с I2C на SPI?
- Буфер кадра завязан на библиотеку, а не на шину. SPI ускоряет передачу, но ОЗУ не экономит ни байта. Ничего: буфер те же 1024 байта
- На A4/A5 сидят RTC и датчик, экран тормозит их опрос. Первый шаг?
- Разгон бесплатен и сжимает занятие шины с девяноста процентов до четверти. SPI — следующий шаг, если и после разгона тесно. Разогнать шину до 400 кГц через Wire.setClock