Подключил я однажды к самописцу датчик температуры в теплице: за сутки он гулял от 24,0 до 25,3 градуса. А график стоял ровный, как линия горизонта. Виноват был мой же map(v, 0, 1023, 60, 4): весь ход аналогового входа растянут на экран, и живой сигнал в два с половиной градуса сжался в два пикселя у середины. Шкала «на весь диапазон датчика» хороша, пока диапазон реально используется; как только прибор работает в узкой полосе, нужна шкала по факту окна — минимум и максимум именно тех точек, что сейчас на экране. Ремесло это называют автоскейлом, и сегодня я разбираю его целиком: расчёт границ по буферу, поле сверху и снизу, мёртвая зона против дрожи и страховка от равных границ.
Основа знакома: кольцевой буфер последних N значений и перерисовка по факту новой точки — механику кольца и оси мы собирали в уроке про самописец с буфером, а осциллографный вариант с триггером и стоп-кадром живёт в отдельном уроке. Новое здесь две вещи: рисование лентой точек вместо полилинии и шкала, которая сама подстраивается под данные.
Разберём расчёт границ, полный скетч ленты и разбор граблей: дрожащая шкала, деление на ноль и подписи, которые делят колонки с точками.
Шкала по факту окна: минимум и максимум
Автоскейл — это два числа, посчитанные перед каждым кадром: vmin и vmax по всем точкам буфера. Дальше знакомый map, только границы не константы, а эти числа с запасом: map(v, vmin - pole, vmax + pole, 60, 4). Поле — пять-десять процентов размаха, я беру pole = (vmax - vmin) / 10 + 1: без него самая высокая точка прижимается к y = 4 и визуально сливается с подписью максимума, а при новом рекорде точка вообще выскакивает за рассчитанный ряд. С полем точка-рекорд ложится у края, но не на нём, и шкала готова к следующему подъёму.
Первый подводный камень сидит в самих границах: если датчик стоит на месте, vmin и vmax равны, и map получает нулевой знаменатель. Целочисленное деление на ноль в C++ даёт непредсказуемый результат, и на AVR это оборачивается мусорной геометрией точек. Страховка на три строки: если vmax - vmin меньше восьми единиц, раздвинуть границы — vmin -= 4 и vmax += 4. Второй камень — дрожь: считать границы по каждому кадру честно, но шумный датчик заставит шкалу прыгать каждые двести миллисекунд, и график будет «дышать» даже при стабильном сигнале. Лечение — мёртвая зона, о ней ниже в рамке.
Adafruit_SSD1306 display(128, 64, &Wire, -1); const int N = 64; // tochek v lenteint buf[64];int head = 0;long lastSample = 0;long lastKadr = 0; void zapis(int v) { buf[head] = v; head = (head + 1) % N;} int chitat(int i) { // i = 0 - staraya tochka int j = head + i; if (j >= N) j -= N; return buf[j];} int yPoShkale(int v, int vmin, int vmax) { // pole 5-10 protsentov razmaha sverhu i snizu int pole = (vmax - vmin) / 10 + 1; return map(v, vmin - pole, vmax + pole, 60, 4);} void risujLentu() { int vmin = buf[0]; int vmax = buf[0]; for (int i = 1; i < N; i++) { int v = chitat(i); if (v < vmin) vmin = v; if (v > vmax) vmax = v; } if (vmax - vmin < 8) { // strah ot ravnih granits vmin -= 4; vmax += 4; } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.print(vmax); display.setCursor(0, 56); display.print(vmin); for (int i = 0; i < N; i++) { int x = i * 2; int y = yPoShkale(chitat(i), vmin, vmax); display.drawPixel(x, y, SSD1306_WHITE); } display.display();} void setup() { Serial.begin(9600); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE); for (int i = 0; i < N; i++) buf[i] = 512;} void loop() { long now = millis(); if (now - lastSample > 200) { lastSample = now; zapis(analogRead(A0)); Serial.print("v="); Serial.println(chitat(N - 1)); } if (now - lastKadr > 200) { lastKadr = now; risujLentu(); } delay(5);}Разбор по числам. Буфер 64 точки int — 128 байт ОЗУ, рядом с килобайтным буфером дисплея на Uno это чувствуется, но пережить можно; кольцевую запись и чтение от старейшей точки разбирали в уроке про массивы и журналы. Выборка и кадр идут на одном такте 200 мс: окно истории — 64 умножить на 0,2 секунды, около тринадцати секунд. Подписи vmax сверху и vmin снизу делят крайние колонки с точками ленты — цена компактности; если это мешает, сдвиньте ленту на x = 12 и потеряйте шесть точек окна.
Мёртвая зона работает так: пересчитывать vmin и vmax можно каждый кадр, но применять новый масштаб только когда точка вышла за текущие границы или размах сжался на треть от прежнего. Без неё шкала реагирует на каждый шумовой выброс, с ней — на настоящий тренд. Для прибора, который работает без присмотра, я ставлю именно второй вариант; для отладки на столе сходит и голый пересчёт — дрожь на глазах даже полезна, она показывает качество сигнала.
Точки против линии: что врёт при редких отсчётах
Лента точек рисует каждый отсчёт одним drawPixel и ничего между ними. Полилиния из drawLine, как в самописце из прошлых уроков, соединяет соседние точки отрезками — и на редких отсчётах начинает рисовать то, чего не было: между двумя замерами через секунду линия провела бесконечно много «значений», которых датчик не показывал. Для медленных процессов — температура почвы, уровень воды — это косметика, для всего, что прыгает, — враньё. Правило у меня такое: период выборки меньше времени наибыстрейшего изменения сигнала — можно линии; иначе только точки. Цена вопроса копеечная: drawPixel стоит те же единицы микросекунд, а узкое место всё равно шина — полный кадр по I2C на 100 кГц идёт около 92 мс, как в замере FPS и цены кадра.
| Стратегия шкалы | Когда годится | Что сломается |
|---|---|---|
| map(v, 0, 1023, 60, 4) — весь ход АЦП | датчик реально гуляет от нуля до максимума | узкий живой сигнал сожмётся в плоскую линию |
| Автоскейл без защиты | демо и отладка на столе | шкала дёргается от шума; при равных границах — деление на ноль |
| Автоскейл с полем и мёртвой зоной | прибор, работающий без присмотра | реакция на тренд чуть медленнее: масштаб меняется не от каждого выброса |
Что попробовать в симуляторе: выкрутите ручку в край и подержите — лента съедет, а границы lo и hi подтянутся к новому плато через пару секунд, поле по краям сохранится. Потом верните ручку резко: старые точки у краёв уйдут из окна, и шкала сожмётся следом. Это и есть автоскейл глазами: график всегда использует всю высоту, что бы ни делал датчик. На плате тот же алгоритм рисует пиксели: цикл по буферу, drawPixel на каждом шаге, один display() на кадр.
Готовый пример прибора с графиком на борту — монитор батареи в уроке про график от АКБ: там шкала фиксированная, потому что напряжение известно заранее, и это второй честный ответ на вопрос о масштабе — не всегда автоскейл нужен, но выбор обязан быть осознанным.
- 1. Что покажет шкала map(v, 0, 1023) для датчика, гуляющего между 240 и 250?
- 2. vmin и vmax оказались равны. Что сделает map без защиты?
- 3. Зачем автоскейлу мёртвая зона?
- 4. Чем лента точек честнее полилинии при редких отсчётах?
- 5. Окно истории ленты из 32 точек при выборке раз в 250 мс — это:
- Автоскейл: vmin и vmax по буферу перед каждым кадром, y = map(v, vmin - pole, vmax + pole, 60, 4)
- Поле 5-10 процентов размаха сверху и снизу — точка-рекорд не прилипает к краю кадра
- vmin равен vmax без защиты — целочисленное деление на ноль; страховка: размах меньше 8 единиц — раздвинуть границы
- Мёртвая зона против дрожи: масштаб меняется только при выходе точки за края или сжатии размаха на треть
- Лента точек честнее полилинии на редких отсчётах: линии дорисовывают переходы, которых не было
Частые вопросы по теме урока
- Что покажет шкала map(v, 0, 1023) для датчика, гуляющего между 240 и 250?
- Диапазон в десять единиц из тысячи возможных — два пикселя высоты. Спасает автоскейл: границы по факту окна. Почти плоскую линию: весь ход сжат в два пикселя
- vmin и vmax оказались равны. Что сделает map без защиты?
- Нулевой знаменатель в целочисленном делении — непредсказуемый результат. Страховка: если размах меньше восьми единиц, раздвинуть границы. Результат непредсказуем: целочисленное деление на ноль
- Зачем автоскейлу мёртвая зона?
- Пересчёт границ от каждого выброса заставляет график дышать на ровном сигнале. Масштаб меняют только при выходе за края или сжатии размаха на треть. Чтобы шкала не дёргалась от каждого шумового выброса
- Чем лента точек честнее полилинии при редких отсчётах?
- drawLine между замерами через секунду придумывает бесконечно много промежуточных значений. Точка рисует только то, что измерено. Линии дорисовывают переходы, которых датчик не показывал
- Окно истории ленты из 32 точек при выборке раз в 250 мс — это:
- Окно — число точек, умноженное на период: 32 × 0,25 с = 8 секунд. Шина влияет только на скорость перерисовки. 8 секунд