Термостат для рассады на подоконнике держал ровно двадцать пять — так писал его экран. Ртутный термометр, поставленный в ту же точку, показывал двадцать шесть и половину. Рассаде всё равно, но проект был пробой пера перед инкубатором, а там полтора градуса решают, вылупится птенец или нет. Вечер я подозревал датчик в смерти, а потом дошло: он не сломан, он смещён. Показывает на полтора градуса меньше, чем есть, — стабильно, при каждой температуре.
Смещение — не брак, а свойство. В даташите DHT11 класс точности заявлен как плюс-минус два градуса при комнатной температуре: два датчика из трёх из коробки будут врать в пределах двух градусов, и никто за это деньги не вернёт. NTC-делитель в середине диапазона держится получше, но по краям гуляет. Поэтому вопрос не в том, как избежать смещения, а в том, где хранить его поправку, чтобы она пережила выключение питания и не потребовала перепрошивки.
В этом уроке заведём в EEPROM одну float-переменную — смещение, которое прибавляется к каждому чтению; войдём в режим калибровки кнопкой, зададим поправку, выйдем — и после любого сброса питания термостат проснётся с той же поправкой. Отдельно разберём знак поправки, на котором я спотыкался дважды.
Про симулятор честно: EEPROM в нём не эмулируется — модель памяти это массив в скетче. Логика при этом та же, что на плате: magic-байт, загрузка поправки при старте, одна запись при выходе из калибровки, применение при каждом чтении. На реальной плате массив заменяется на EEPROM.get и EEPROM.put один в один.
Знак поправки: эталон минус показание
Определение одно, и его стоит записать на полях: смещение равно эталону минус показание. Датчик говорит 23.5, эталонный термометр в той же точке 25.0 — смещение плюс полтора, и его прибавляют к каждому чтению. Прибор врёт вверх, показывает 26.0 при честных 25.0 — смещение минус ноль-пять, прибавление отрицательного числа само всё исправит. Моё правило: поправку всегда прибавляют и никогда не вычитают — тогда формула в коде везде одинаковая, а знак ведёт в нужную сторону без раздумий. Тот же приём работает и вне температур: барометр BMP280, уехавший на пару гектопаскалей после сверки с метеосводкой соседнего города, лечится такой же прибавкой — смещение есть смещение.
Тупик, в который я попадал дважды: поправка применена в двух местах. Сначала я прибавил смещение в функции чтения датчика — правильно. Потом, забыв об этом, добавил ту же поправку в регулирование — оно брало уже исправленное значение и сдвигало его ещё раз. Полтора градуса превратились в три, и рассада вместо тепла получила духоту. Лечится дисциплиной: смещение живёт в одном месте, внутри функции, возвращающей температуру, и весь остальной код ест только её результат.
Одна поправка на весь диапазон — приближение, скажем честно. NTC-датчик, откалиброванный при двадцати пяти, может уехать на полградуса при минус десяти: у термистора и резисторов делителя есть температурный дрейф. Для комнатного термостата это невидимо. Для диапазона от минус сорока до плюс восьмидесяти нужна уже двухточечная калибровка — смещение плюс масштаб, две константы вместо одной, — механика хранения та же, просто ячеек четыре, а не одна.
#include <EEPROM.h> // Карта: 0 - magic, 1-4 - float-поправкаconst uint8_t ADDR_MAGIC = 0;const uint8_t ADDR_OFFSET = 1; const int PIN_BTN = 2; // вход в режим калибровкиconst int PIN_LED = 12; // горит - режим калибровкиconst int PIN_TRIM = A0; // подстроечник поправкиconst int PIN_SENSOR = A1; // датчик (NTC-делитель) float offset = 0.0; // сколько градусов прибавлять к сырым показаниямbool calMode = false;bool prevBtn = HIGH; void setup() { Serial.begin(9600); pinMode(PIN_BTN, INPUT_PULLUP); pinMode(PIN_LED, OUTPUT); EEPROM.get(ADDR_OFFSET, offset); // четыре байта float одним вызовом if (EEPROM.read(ADDR_MAGIC) != 170) { offset = 0.0; // свежая плата - поправки нет EEPROM.put(ADDR_OFFSET, offset); EEPROM.update(ADDR_MAGIC, 170); } Serial.print("start offset="); Serial.println(offset, 1);} void loop() { bool btn = digitalRead(PIN_BTN) == LOW; if (btn && !prevBtn) { // фронт нажатия calMode = !calMode; digitalWrite(PIN_LED, calMode ? HIGH : LOW); if (!calMode) { offset = map(analogRead(PIN_TRIM), 0, 1023, -125, 125) / 10.0; EEPROM.put(ADDR_OFFSET, offset); // пишем один раз при выходе Serial.print("saved offset="); Serial.println(offset, 1); } } prevBtn = btn; int rawX10 = map(analogRead(PIN_SENSOR), 0, 1023, 150, 350); float t = rawX10 / 10.0 + offset; // поправка применяется здесь и только здесь Serial.print("t="); Serial.println(t, 1); delay(300);}Три опорные точки в этом скетче. Первая: EEPROM.get читает четыре байта float одним вызовом — раскладку дробного числа по ячейкам мы разбирали в уроке про побайтовую запись, а put и get делают её за вас. Вторая: magic-байт из урока про первые настройки — чистая EEPROM читается как 255, и без проверки четыре ячейки 255 превратятся в какое-то число, которое выглядит как поправка, хотя это мусор пустой памяти. Третья: put стоит внутри выхода из режима калибровки, а не в loop — за весь сеанс в память уходит ровно одна запись, и ресурс ячейки из расчёта износа не тревожится.
При старте поправка читается до всего остального, поэтому после сброса питания — хоть через час, хоть после зимы на даче — термостат просыпается с тем же смещением. Одна запись занимает около 3,3 мс и происходит при выходе из калибровки: пользователь моргнуть не успеет. Если калибруете раз в месяц, ячейка с поправкой переживёт и плату, и вас.
Порядок калибровки на живом устройстве: сначала прогрейте сборку до рабочего режима — минут двадцать-тридцать работы, чтобы датчик и плата вышли на устоявшуюся температуру, — и только потом вводите поправку. Поправка, снятая на холодной плате сразу после включения, отличается от устоявшейся на несколько десятых: электроника рядом с датчиком его подогревает. Для гистерезиса комнатного термостата, к которому мы шли в уроке про термостат, эти десятые не страшны, для инкубатора — уже нет. И да, светодиод в скетче не украшение: он показывает, что крутилка сейчас задаёт поправку, а не служит датчиком. В симуляторе роль крутилки двойная — в обычном режиме это показания датчика, в калибровке — задание поправки; на реальной плате датчик держат на отдельном входе A1, а подстроечник остаётся на A0.
Чем калибровать и как писать бережно
Эталон — узкое место всей затеи, и здесь работает жёсткое правило: точность поправки не выше точности эталона. Лабораторный термометр даёт десятые доли, стакан с тающим льмом даёт ровный ноль силами физики, кипяток — плюс-минус градус и зависит от высоты над морем. Рабочий бытовой вариант — второй датчик, который вы сверили по первым двум: так я калибрую термостаты по DS18B20, проверенному со спиртовым термометром в стакане со льдом.
По ресурсу калибровка — идеальный клиент EEPROM: читается на старте, пишется при выходе из режима. Десятые доли градуса, кстати, можно хранить и без float — умножить на десять и держать int в двух ячейках той же арифметикой, что в уроке про счётчик включений: минус полтора превращается в минус пятнадцать. Выбор между float через put и целыми десятыми — дело вкуса и соседей по памяти: если рядом уже лежит структура настроек с float из урока про put и get, кладите float туда же, одним ящиком.
Ввод поправки без подстроечника — через Serial: вошли в режим, набрали число, подтвердили. Чтение чисел с монитора порта разобрано в уроке про parseInt, а когда команд станет больше трёх, поверх parseInt собирается текстовое меню с именами и валидацией. Кнопка в любом случае остаётся единственным входом в калибровку: исправить показания мимо режима должно быть невозможно, иначе первый же пролиставший меню человек собьёт прибор.
| Эталон | Точность в этой точке | Когда годится |
|---|---|---|
| Лабораторный или медицинский термометр | ±0,1 градуса | комнатные температуры, лучший бытовой вариант |
| Стакан с тающим льдом | ровно 0 градусов | проверка нижней точки; датчик — в пакетике |
| Кипящая вода | около 100 у моря, минус 1 на каждые 300 м высоты | верхняя точка, если датчик переносит пар |
| Второй датчик, сверенный заранее | ±0,5 градуса | серийная калибровка: оба в один объём, ждём выравнивания |
- Смещение = эталон минус показание; поправку всегда прибавляют, знак ведёт сам
- Смещение применяется в одном месте — в функции чтения датчика; второй раз прибавить его — удвоить ошибку
- Float-поправка занимает четыре ячейки: EEPROM.put и get раскладывают её сами, magic-байт отличает пустую память
- Пишите поправку один раз — при выходе из режима калибровки; одна запись — около 3,3 мс и капля ресурса
- Точность поправки не выше точности эталона: лабораторный термометр, лёд, кипяток или сверенный второй датчик
- Калибруйте на прогретой сборке; одна поправка лечит сдвиг, нелинейность по краям требует второй точки и масштаба
- 1. Датчик показывает 22.0, эталонный термометр в той же точке 23.4. Какая поправка пойдёт в EEPROM?
- 2. Где в скетче должно применяться смещение?
- 3. Сколько записей в EEPROM делает сеанс калибровки — вошёл, покрутил, вышел?
- 4. Зачем поправке нужен magic-байт рядом?
- 5. Когда одной поправки-смещения мало?
В симуляторе попробуйте полный цикл: вошли в калибровку, задали поправку, вышли — и дальше каждая пара raw и corr расходится ровно на сохранённое смещение. Массив обнулится при перезапуске скетча, зато на реальной плате после сброса питания setup прочитает сохранённое и продолжит с той же поправкой — в этом весь смысл держать смещение в EEPROM, а не в переменной.
Пять ячеек — magic и четыре байта поправки — заняли своё место честно и после сброса питания вернулись на место. Но это первый жилец. Дальше появятся пороги, режимы, счётчики и журнал, и все они захотят свои адреса в той же тысяче ячеек. Как распланировать килобайт EEPROM так, чтобы соседи не наезжали друг на друга и добавление поля не ломало старые прошивки — следующий урок.
Частые вопросы по теме урока
- Датчик показывает 22.0, эталонный термометр в той же точке 23.4. Какая поправка пойдёт в EEPROM?
- Смещение равно эталону минус показание: 23.4 − 22.0 = +1.4. Прибавляем его к сырым показаниям — и прибор показывает как эталон. Плюс 1.4 — прибавлять к каждому чтению
- Где в скетче должно применяться смещение?
- Двойное применение — классическая ловушка: полтора градуса превращаются в три. Одна функция, одно смещение, все потребители читают поправленное. В одном месте — в функции чтения датчика, весь остальной код ест её результат
- Сколько записей в EEPROM делает сеанс калибровки — вошёл, покрутил, вышел?
- Пока крутится крутилка, поправка живёт в переменной. При выходе из режима — один put, около 3,3 мс. Ресурсу ячейки такие сеансы безразличны. Одну — при выходе из режима
- Зачем поправке нужен magic-байт рядом?
- Четыре байта 255 в IEEE-754 — это какое-то число, а не ноль. Magic 170 отличает первый запуск от данных и разрешает записать дефолтную поправку. Чистая память читается как 255, и без проверки мусор пустых ячеек сойдёт за поправку
- Когда одной поправки-смещения мало?
- Смещение лечит только параллельный сдвиг. Если погрешность растёт к краям — это нелинейность, калибруйте по двум точкам: offset плюс scale. Когда врёт по-разному в середине и по краям диапазона — нужна вторая точка и масштаб