Зимой я утеплял гаражный щиток пенопластом и хотел ответа на простой вопрос: когда именно внутри теплее всего — от солнца на воротах днём или от работающего зарядника ночью. Текущее значение из встроенного термометра DS3231 на это не отвечало: чтобы увидеть суточный рельеф, надо было просидеть у монитора весь день. Ответом стал суточный архив — двадцать четыре ячейки, по снимку в час, и печать столбиками по кнопке: одним взглядом видно и пик, и провал.
В этом уроке собирается такой архив: слот выбирает сам час из rtc.now(), снимок делает getTemperature(), минимум и максимум трекаются на лету, кнопка на D2 печатает ASCII-график в Serial, светодиод на D12 мигает на каждом снимке. В конце — куда архив растёт: перенос массива в EEPROM парами байт, как в кольцевом журнале, и место в итоговом проекте теплицы, где суточная карточка встала бы рядом с расписанием полива.
Слот = hour(): массив, который сам знает своё расписание
Индекс слота не надо выдумывать — его отдаёт само время: now.hour() возвращает 0..23, и это готовый номер ячейки массива int t10[24]. Никакого поиска и сдвига: пришёл четырнадцатый час — пишем в четырнадцатую ячейку. Храню в десятых долях градуса: int v = rtc.getTemperature() * 10 превращает 23,5 в 235. Выигрыш двойной: массив занимает 48 байт вместо 96 у float, а сравнения рекордов идут в целых числах, без наледи равенств с плавающей точкой. Шаг датчика — четверть градуса, так что десятые выражают его без потерь.
Момент снимка привязан к смене часа: int h = now.hour(); if (h != prevHour) — снимаем. Это надёжнее delay(3600000): задержка блокирует loop на целый час и плывёт с каждой перезагрузкой, а сравнение часов спрашивает у RTC, который переживает перезагрузки батарейкой. Цена подхода — честные пропуски: питание мигнуло на два часа, два слота остались пустыми. В пустых слотах живёт -1 и печатается прочерк — сразу видно, что показания нет, а не ноль, который в минусовую ночь выглядел бы подозрительно правдоподобным.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc;const int BTN = 2;const int LED = 12; int t10[24]; // десятые доли градуса, -1 = слот пустint prevHour = -1;int prevBtn = HIGH;int tMin = 32767;int tMax = -32768;int hMin = -1;int hMax = -1;int zapolneno = 0;unsigned long tLed = 0;int ledMs = 0; void grafik() { Serial.println("--- Sutochnyi arhiv, C ---"); for (int i = 0; i < 24; i++) { if (i < 10) { Serial.print("0"); } Serial.print(i); Serial.print(" "); if (t10[i] < 0) { Serial.println("pustoi slot"); } else { int n = (t10[i] - 180) / 5; // ноль шкалы +18.0, клетка = 0.5 if (n < 0) { n = 0; } if (n > 20) { n = 20; } for (int k = 0; k < n; k++) { Serial.print("#"); } Serial.print(" "); Serial.println(t10[i] / 10.0, 1); } } Serial.print("MIN "); Serial.print(tMin / 10.0, 1); Serial.print(" v "); Serial.print(hMin); Serial.print("h | MAX "); Serial.print(tMax / 10.0, 1); Serial.print(" v "); Serial.print(hMax); Serial.println("h");} void setup() { Serial.begin(9600); pinMode(BTN, INPUT_PULLUP); pinMode(LED, OUTPUT); rtc.begin(); for (int i = 0; i < 24; i++) { t10[i] = -1; } Serial.println("Arhiv gotov: 24 slota, knopka D2 = grafik");} void loop() { DateTime now = rtc.now(); int h = now.hour(); if (h != prevHour) { // смена часа: снимок в свой слот prevHour = h; int v = rtc.getTemperature() * 10; // 23.5 -> 235 t10[h] = v; if (zapolneno < 24) { zapolneno++; } if (v < tMin) { tMin = v; hMin = h; } if (v > tMax) { tMax = v; hMax = h; } digitalWrite(LED, HIGH); tLed = millis(); ledMs = 100; // вспышка «снимок сделан» if (v == tMin || v == tMax) { ledMs = 400; // рекорд держит вспышку дольше } Serial.print("snap "); Serial.print(h); Serial.print(" -> "); Serial.println(v / 10.0, 1); } int btn = digitalRead(BTN); if (btn == LOW && prevBtn == HIGH) { // фронт кнопки if (zapolneno == 0) { Serial.println("Arhiv pust"); } else { grafik(); } } prevBtn = btn; if (ledMs > 0 && millis() - tLed > ledMs) { digitalWrite(LED, LOW); ledMs = 0; } delay(100);}Печать графика — та часть, ради которой всё собрано. Столбик строится решётками: ноль шкалы прибит к плюс восемнадцати, каждая клетка — полградуса, потолок двадцать клеток. Формула (t10[i] - 180) / 5 при 23,5 даёт одиннадцать клеток, при 18,0 — ноль, а зимние плюс пять обрезаются в прочерк: n уходит в минус и гасится порогом. Такой график читается прямо в Serial-мониторе, хоть с телефона через первое знакомство с портом — без ноутбука и таблиц. Мелочь про печать десятых: Serial.println(v / 10.0, 1) выводит знак и дробную часть правильно, а целочисленная склейка v / 10 с v % 10 зимой врёт — для минус двадцати пяти напечатает −2.−5.
Куда архив растёт. Первый шаг — EEPROM: 48 байт массива ложатся в тысячу байт Uno с запасом, и каждая ячейка получает ровно одну запись в сутки при ресурсе в сто тысяч циклов — износ уезжает за горизонт в две сотни лет; писать лучше парами байт через EEPROM.update, не перезаписывая одинаковое, — приём и кольцевую адресацию разбирает урок про кольцевой журнал. Второй шаг — встраивание: в итоговой теплице суточная карточка встаёт рядом с расписанием, и сразу видно, что полуденный прогрев совпадает с окном проветривания. Третий — смена масштаба: 96 слотов по пятнадцать минут — механика та же, меняется только индекс.
| Хранилище | Объём и запись | Что даёт и чем платит |
|---|---|---|
| int[24] в ОЗУ | 48 байт, запись в своё место по часу | мгновенно и без износа; всё стирается перезагрузкой |
| EEPROM парами байт | 48 байт из 1024, update раз в час | переживает питание; ячейка — одна запись в сутки, износ за горизонтом |
| SD-журнал со штампами | строка на замер, рост без предела | полная история, но карточку суток ещё надо собрать из файла |
Симулятор: сутки за минуту
Время в симуляторе сжато: снимок делается каждые две секунды по millis-расписанию, слоты заполняются по кругу 0..23 — сутки проживаются за минуту. Вторая честная условность: движок отвечает getTemperature() одним и тем же числом 23,5, поэтому в сим-скетче снимок берёт random(180, 260) — демо-разброс, чтобы рельеф и рекорды были видны; на плате строку с random меняют на строку с getTemperature из код-блока выше. Кнопка на D2 печатает график и рекорды, светодиод на D12 мигает коротко на каждом снимке и держит вспышку дольше, если установлен новый минимум или максимум.
- Слот выбирает сам час: now.hour() — готовый индекс 0..23, снимок по смене часа, а не по delay
- Температура живёт в десятых: int вместо float — 48 байт вместо 96 и честные целочисленные сравнения
- Рекорды стартуют с недостижимых значений: tMin = 32767, tMax = -32768 — иначе зимний минимум вечно ноль
- Шкала столбиков: ноль на +18,0, клетка — полградуса; Serial.print(v / 10.0, 1) печатает десятые правильно
- Рост: EEPROM.update парами байт — 48 байт из 1024, ячейка пишется раз в сутки, износ за горизонтом
- 1. Сколько байт ОЗУ занимает архив int t10[24]?
- 2. Почему снимок по смене часа, а не delay(3600000)?
- 3. Зачем температуру кладут в десятые — int v = t * 10?
- 4. tMin инициализировали нулём, а в гараже ночью минус пятнадцать. Что покажет кнопка?
- 5. Каждая ячейка EEPROM пишется раз в сутки, ресурс ячейки — сто тысяч циклов. Сколько лет до износа?
Частые вопросы по теме урока
- Сколько байт ОЗУ занимает архив int t10[24]?
- int на Uno — два байта, массив из 24 слотов — 48. Тот же массив во float потянул бы 96, а в EEPROM хранится всё те же 48 байт парами. 48 байт: 24 слота по два байта int
- Почему снимок по смене часа, а не delay(3600000)?
- Заблокированный loop не видит кнопку и датчики, а после каждой перезагрузки отсчёт задержки начинается заново. Сравнение now.hour() с прошлым часом привязано к батареечному времени. delay блокирует loop на час целиком и сбивается перезагрузками, а сравнение часов спрашивает у RTC, который их переживает
- Зачем температуру кладут в десятые — int v = t * 10?
- 48 байт вместо 96 и целочисленные сравнения без сюрпризов с плавающей точкой. Шаг датчика — четверть градуса, десятые доли выражают его без потерь. массив вдвое компактнее, а сравнения рекордов идут в целых числах
- tMin инициализировали нулём, а в гараже ночью минус пятнадцать. Что покажет кнопка?
- Ни один отрицательный замер не бывает меньше нуля, поэтому «минимум» навсегда останется стартовым значением. Стартовые рекорды должны быть недостижимыми: 32767 и -32768. ноль: ни один замер не пробьёт стартовый порог
- Каждая ячейка EEPROM пишется раз в сутки, ресурс ячейки — сто тысяч циклов. Сколько лет до износа?
- Одна запись в сутки при ста тысячах циклов — 100000 / 365, примерно 274 года. Раз в час пишется лишь одна из 24 ячеек, износ размазывается по всему массиву. порядка двухсот семидесяти лет