К финальному проекту у меня на столе стоял зоопарк: скетч с температурой, скетч с графиком, скетч с часами — каждый со своим setup, и каждый показывал что-то одно. Гаражная стенка требовала одного прибора: посмотреть — увидеть всё — переключиться, если интересна деталь. Прибором это стало в вечер, когда я прикрутил к экрану меню из трёх экранов и вторую кнопку: дисплей тот же, датчики те же, но появился выбор, что смотреть.
Здесь собираем дашборд целиком: три экрана с меню на кнопках D2 и D3, зоны из урока про текст, график истории, датчики на A0 и A1 — и всё это без единого delay. Нового API почти нет: новым оказывается масштаб сборки, и именно он ломает первые большие скетчи.
Архитектура: экраны, зоны, планировщик
Экран разбит по сетке 21×8 из урока про текст: строка 0 — шапка с названием экрана и его номером справа, строка 1 — разделитель, середина — данные, последняя строка — подсказка по кнопкам. Номер к правому краю прижимает правое выравнивание из того же урока: setCursor(114, 0) для однозначного номера. Подсказка в подвале — мелочь, которая избавляет от листания мануала: D2 — дальше, D3 — назад.
У скетча три бюджета времени, и все на millis по схеме из урока про кооперативный цикл: опрос кнопок — раз в 20 мс по таймеру; точка истории — раз в секунду; кадр — раз в 250 мс. Датчики читаю прямо при сборке кадра: analogRead стоит десятки микросекунд и на фоне передачи кадра не виден. Доля шины считается честно: четыре кадра в секунду по 92 мс — это 37 процентов занятой шины на 100 кГц; после Wire.setClock(400000) остаётся 9 процентов, и на той же паре A4/A5 спокойно живут часы.
График истории — двадцать одна точка, ровно ширина строки при size(1). Хранится в массиве int на 21 ячейку — 42 байта ОЗУ против 1024 байтового буфера кадра. Раз в секунду массив сдвигается влево на ячейку, новая точка встаёт в конец — график ползёт, как лента самописца. Рисуем тремя рядами из штрихов: если значение точки достигает уровня ряда, печатаем вертикальную черту, иначе точку. Три уровня грубовато для лаборатории, но для гаражной стенки — в самый раз: цифры читаются с трёх метров, тренд — с одного взгляда.
#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(128, 64, &Wire, -1); int screen = 0; // 0 сенсоры, 1 график, 2 статусint history[21];long lastFrame = 0;long lastPoint = 0;long lastPoll = 0;int prevBtn = HIGH;int prevTog = LOW; void drawHeader(String title, int num) { display.setTextSize(1); display.setCursor(0, 0); display.print(title); display.setCursor(114, 0); // номер экрана к правому краю display.print(num); display.setCursor(0, 8); display.print("--------------------");} void drawSensors() { display.setTextSize(2); display.setCursor(0, 20); display.print(analogRead(A0)); display.setTextSize(1); display.setCursor(48, 20); display.print("pot A0"); display.setTextSize(2); display.setCursor(0, 44); display.print(analogRead(A1)); display.setTextSize(1); display.setCursor(48, 44); display.print("ldr A1");} void drawChart() { for (int row = 0; row < 3; row++) { display.setTextSize(1); display.setCursor(0, 16 + row * 8); for (int i = 0; i < 21; i++) { if (history[i] >= 3 - row) { display.print("|"); } else { display.print("."); } } } display.setCursor(0, 48); display.print("uroven 0-3, tochka 1 s");} void drawStatus() { display.setTextSize(1); display.setCursor(0, 16); display.print("Uptime, sekund:"); display.setTextSize(2); display.setCursor(0, 32); display.print(millis() / 1000); display.setTextSize(1); display.setCursor(0, 56); display.print("kadr 4 raza v sekundu");} void drawScreen() { display.clearDisplay(); if (screen == 0) { drawHeader("SENSORY", 1); drawSensors(); } if (screen == 1) { drawHeader("GRAFIK", 2); drawChart(); } if (screen == 2) { drawHeader("STATUS", 3); drawStatus(); } display.display();} void setup() { Serial.begin(9600); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.clearDisplay(); display.setTextColor(SSD1306_WHITE); pinMode(2, INPUT_PULLUP); // кнопка дальше pinMode(3, INPUT_PULLUP); // тумблер назад prevBtn = digitalRead(2); // считываем покой: иначе первое чтение сойдёт за щелчок prevTog = digitalRead(3); for (int i = 0; i < 21; i++) { history[i] = 0; } drawScreen();} void loop() { // кнопки опрашиваем раз в 20 мс: дребезг успевает закончиться if (millis() - lastPoll >= 20) { lastPoll = millis(); // кнопка: шаг вперёд по фронту нажатия int b = digitalRead(2); if (prevBtn == HIGH && b == LOW) { screen = (screen + 1) % 3; drawScreen(); Serial.print("scr="); Serial.println(screen); } prevBtn = b; // тумблер: шаг назад на любой щелчок int t = digitalRead(3); if (t != prevTog) { screen = (screen + 2) % 3; drawScreen(); Serial.print("scr="); Serial.println(screen); } prevTog = t; } // точка истории: раз в секунду if (millis() - lastPoint >= 1000) { lastPoint = millis(); for (int i = 0; i < 20; i++) { history[i] = history[i + 1]; } history[20] = map(analogRead(A0), 0, 1023, 0, 3); } // кадр: раз в 250 мс if (millis() - lastFrame >= 250) { lastFrame = millis(); drawScreen(); }}Кнопки: фронты без библиотек
Кнопка на D2 стоит между пином и землёй, пин поджат к единице режимом INPUT_PULLUP: покой даёт HIGH, нажатие — LOW. Ловим фронт: запоминаем прошлое чтение и сравниваем с текущим; шаг меню происходит на переходе HIGH в LOW. При опросе каждые 20–30 мс дребезг типовой тактильной кнопки — 5–10 мс — успевает закончиться между двумя соседними опросами, и отдельный антидребезг не нужен. Условие важно: у изношенной кнопки с дребезгом под 40 мс уже понадобится фильтр или проверка по времени.
Вторая кнопка у меня — тумблер на D3: он держит состояние, поэтому щелчок ловится на любое изменение чтения, в любую сторону. Один щелчок — один шаг назад по меню. Это честнее, чем делать из тумблера кнопку: у прибора появляется тактильное различие — кнопка листает вперёд, тумблер возвращает, и путаться не в чем. Одна тонкость старта: прошлое состояние тумблера я считываю в setup, сразу после pinMode — иначе первое чтение цикла примет неподжатый пин за щелчок, и прибор проснётся уже переключённым на экран назад.
Почему перерисовка и по событию, и по таймеру: нажатие рисует новый экран мгновенно — палец не должен ждать четверти секунды; таймер на 250 мс страхует обновление значений, когда кнопок нет. Два вызова drawScreen не конфликтуют: кадр всегда собирается с чистого буфера, и модель из урока про display() гарантирует, что промежуточных состояний на стекле не бывает.
| Задача | Период | Цена |
|---|---|---|
| Опрос кнопок D2, D3 | раз в 20 мс по millis | микросекунды; шину не трогает |
| Кадр на экран | раз в 250 мс | 4 × 92 мс = 37% шины на 100 кГц; 9% на 400 кГц |
| Точка истории | раз в 1 с | сдвиг 21 ячейки массива — копейки против кадра |
| analogRead датчиков | при сборке кадра | десятки микросекунд каждый; на фоне кадра не видно |
Прибор собран из навыков всего раздела: конвейер кадра и цена display() — из урока про буфер, зоны и правое выравнивание — из текстового урока, примитивы для рамок и шторок — из урока про графику, адрес и соседи по шине — из урока про сканер. На ту же пару A4/A5 я вешаю часы DS3231 из урока про RTC — на экране статуса появляется реальное время, и прибор окончательно перестаёт быть макеткой.
Домашнее задание: добавьте четвёртый экран с порогом освещённости и зуммером на D8 — LDR ушёл за порог, зуммер пискнул, порог правится кнопками на пятом экране. Это уже не учебный проект, а охрана гаража. И последнее: сохраните скетч под коротким именем — ваши следующие приборы будут начинаться с этого файла.
- 1. Сколько стоит кадр 128×64 на шине 400 кГц?
- 2. Кнопка с INPUT_PULLUP в покое даёт на пине...
- 3. Зачем перерисовывать экран и по нажатию, и по таймеру 250 мс?
- 4. Сколько ОЗУ ест история из 21 точки типа int?
- 5. Опрос кнопок каждые 20–30 мс заменяет антидребезг, потому что...
- Три экрана, две кнопки: D2 листает вперёд по фронту HIGH→LOW, тумблер D3 возвращает на любой щелчок
- Кнопки опрашиваются раз в 20 мс по millis: дребезг 5–10 мс успевает закончиться между опросами
- Бюджеты на millis: точка истории 1 с, кадр 250 мс — ни одного delay
- График: 21 точка int — 42 байта ОЗУ, три уровня из штрихов, сдвиг влево раз в секунду
- Кадр раз в 250 мс — 37% шины на 100 кГц; Wire.setClock(400000) снижает до 9%
Частые вопросы по теме урока
- Сколько стоит кадр 128×64 на шине 400 кГц?
- 1024 байта × 9 бит / 400 000 бит/с — около 23 мс. На стандартных 100 кГц тот же кадр едет 92 мс. Около 23 мс
- Кнопка с INPUT_PULLUP в покое даёт на пине...
- Подтяжка прижимает пин к единице; нажатие замыкает пин на землю и даёт LOW. Фронт нажатия — переход HIGH в LOW. HIGH
- Зачем перерисовывать экран и по нажатию, и по таймеру 250 мс?
- Событийная отрисовка отвечает пальцу, таймерная — обновляет цифры. Кадр собирается с чистого буфера, конфликтов нет. Нажатие даёт мгновенную реакцию, таймер обновляет значения
- Сколько ОЗУ ест история из 21 точки типа int?
- int на Uno — два байта: 21 × 2 = 42 байта. Смешно против 1024 байтового буфера кадра. 42 байта
- Опрос кнопок каждые 20–30 мс заменяет антидребезг, потому что...
- У типовой тактильной кнопки дребезг 5–10 мс — между двумя опросами он заканчивается. У изношенной с 40 мс уже нужен фильтр. дребезг короче интервала и успевает закончиться