Уличный термостат после offset-калибровки вёл себя безупречно ровно до первого холодного сезона. Регулировали по стакану с водой при двадцати — показания встали идеально; в минус десять датчик врал уже на три градуса. Поправка, снятая в одной точке, выпрямляет показания там, где снята, и молча уезжает везде дальше: прибор показывал не температуру, а температуру плюс константу, хотя датчик врал не константой, а пропорционально диапазону.
Одно смещение мы уже хранили в уроке про offset — там float-поправка прибавлялась к каждому чтению и переживала питание. Здесь ступень выше: две опорные точки дают прямую y = k·x + b с масштабом k и сдвигом b, и обе константы уезжают в EEPROM. Та самая «двухточечная калибровка — смещение плюс масштаб», которую offset-урок обещал на будущее, — просто ячеек вдвое больше, а механика та же.
Разберём формулы, снимем две опоры — лёд и кипяток как учебный пример с известными оговорками — упакуем k и b в память и прикрутим пересчёт так, чтобы он жил в одном месте скетча. В симуляторе роль датчика играет ползунок: точка фиксируется кнопкой, и через два нажатия у прибора появляется своя личная прямая.
Две опоры дают прямую
Математика — школьная прямая через две точки. k = (y2 − y1)/(x2 − x1): на сколько реальных единиц меняется выход на один код АЦП. b = y1 − k·x1: где прямая пересекает ноль сырому коду. В примере с термометром y — градусы: у тающего льда честные 0,0 — фаза не пускает температуру вверх, пока лёд не растаял, — у кипятка около 100 с минусом градус на каждые 300 метров высоты, та же оговорка, что в уроке про offset. Снимаем сырой код датчика в обеих точках — и прямая готова.
Числа покажут разницу с одной поправкой. Пусть датчик врёт на 5 градусов в нуле и на 12 в сотне: offset честно исправит ноль и оставит минус семь в сотне — одна поправка, а верить прибору начинает хуже. Двухточечный расчёт по этим же точкам даёт k ≈ 1,07 и b ≈ −5,4: масштаб растягивает показания, сдвиг опускает ноль, и оба конца встают по эталону. Цена — вторая опорная точка и восемь ячеек памяти вместо четырёх.
Честная граница: две точки выпрямляют только линейную часть вранья. NTC-термистор по природе экспоненционален — прямая, снятая на 0 и 100, на середине диапазона уедет; для широких разбросов берут больше точек и интерполяцию по таблице. Комнатный и уличный диапазоны на двух точках живут спокойно: кривизна там меньше собственной точности датчика, и точнее прибор не станет — прямая исправляет то, что есть, а не добавляет разрядов.
#include <EEPROM.h> const int PIN_POT = A0; // сырой сигнал датчика: 0-1023const int PIN_BTN = 2; // захват опорной точкиconst int PIN_LED = 12; // горит — ждём вторую точку const byte ADDR_MAGIC = 20; // 202 — калибровка уже записанаconst byte ADDR_K = 21; // float k: байты 21-24const byte ADDR_B = 25; // float b: байты 25-28 float k = 100.0 / 1023.0; // заводской дефолт: весь диапазон на весь АЦПfloat b = 0.0;int points = 0;long x1 = 0;bool prevBtn = HIGH; void setup() { Serial.begin(9600); pinMode(PIN_LED, OUTPUT); pinMode(PIN_BTN, INPUT_PULLUP); if (EEPROM.read(ADDR_MAGIC) == 202) { // память уже калибрована EEPROM.get(ADDR_K, k); // 4 байта одним вызовом EEPROM.get(ADDR_B, b); } Serial.print("start k="); Serial.print(k, 4); Serial.print(" b="); Serial.println(b, 1);} void loop() { bool btn = digitalRead(PIN_BTN) == LOW; if (btn && !prevBtn) { // фронт нажатия long raw = analogRead(PIN_POT); if (points == 0) { x1 = raw; points = 1; digitalWrite(PIN_LED, HIGH); Serial.print("tochka1 raw="); Serial.println(raw); } else if (points == 1) { if (raw < x1 + 50) { // база опор шире шума Serial.println("tochki slishkom blizko"); } else { k = 100.0 / (raw - x1); // (y2-y1)/(x2-x1) b = 0.0 - k * x1; // y1 - k*x1 EEPROM.put(ADDR_K, k); EEPROM.put(ADDR_B, b); EEPROM.update(ADDR_MAGIC, 202); digitalWrite(PIN_LED, LOW); points = 2; Serial.print("k="); Serial.print(k, 4); Serial.print(" b="); Serial.println(b, 1); } } else { points = 0; // третье нажатие — новая калибровка } delay(30); } prevBtn = btn; long raw = analogRead(PIN_POT); float t = k * raw + b; // единственное место пересчёта Serial.print("raw="); Serial.print(raw); Serial.print(" t="); Serial.println(t, 1); delay(400);}Порядок старта: magic в ячейке 20 говорит, записана ли калибровка; если да — EEPROM.get достаёт оба float одним вызовом, раскладкой по байтам занимается библиотека, как в уроке про put и get. На чистой плате magic не находится — работают заводские дефолты k = 100/1023 и b = 0, то есть «весь диапазон на весь АЦП»: прибор сразу показывает что-то разумное, не дожидаясь калибровки. Тот же паттерн «magic и дефолты» разобран в уроке про magic — здесь он привешен не к настройкам, а к коэффициентам.
Запись происходит один раз — при выходе из калибровки: два put и один update, девять байт и около 30 мс. Ресурс от этого не шелохнётся даже при перекалибровке каждый месяц: дюжина записей в год против сотен тысяч паспортных циклов — расчёт темпов из урока про частоту записи здесь даёт срок, который скучно считать. В этом калибровка — идеальный клиент EEPROM: читается на старте, пишется по событию человеческой редкости.
Опоры снимайте на краях рабочего диапазона — чем шире база x2 − x1, тем меньше в k пузырится шум АЦП. Середина для второй точки — худший выбор: коэффициент взлетает, и каждый код превращается в градусы; в скетче стоит простая страховка — вторая точка принимается только если она ушла от первой хотя бы на 50 кодов.
| Ситуация | Offset, одна точка | k и b, две точки |
|---|---|---|
| Врёт одинаково по всему диапазону | лечит полностью | тоже лечит, избыточно |
| Врёт сильнее к краям: масштаб | не лечит — правит только сдвиг | лечит: k тянет наклон прямой |
| Место в EEPROM | 4 байта, один float | 8 байт, два float |
| Опор при калибровке | одна сверка | две, разнесённые по диапазону |
Пересчёт: одно место на весь скетч
Строка t = k·raw + b живёт в одной функции рядом с чтением датчика; пороги, экран и лог едят готовое значение и про сырые коды не знают. Это та же дисциплина, что спасала от двойного смещения в offset-уроке: поправка, применённая в двух местах, удваивается, и никакая точность k её не компенсирует. Термостат с гистерезисом из урока про термостат просто читает t — для него калибровка невидима, и это цель: коэффициенты не должны расползаться по скетчу.
Восемь байт под два float — не единственный расклад. Если память на счету, хранят k×1000 и b×10 целыми: четыре ячейки вместо восьми, плюс одна арифметика при чтении и восстановление знака — приём побайтовой упаковки из урока про int и байты. Именно он работает в симуляторе ниже: put с float движок не поддерживает, и упакованные целые — честная замена, при которой логика калибровки не меняется ни на йоту.
В мониторе стенда сценарий виден строками: start k=0.0978 b=0.0 — заводские дефолты; tochka1 raw=312 -> etalon 0.0 C — первая опора зафиксирована, светодиод горит; после второго нажатия k=0.1739 b=-54.2 zapisano v EEPROM — прямая ваша, и строка raw= t= уже идёт по ней. Третье нажатие сбрасывает счётчик точек, и прибор готов к следующей паре опор: перекалибровка не требует перепрошивки и не боится питания — коэффициенты уже в EEPROM.
На живой плате к механике добавляется терпение. Лёд лучше брать в пакетике: вода не замыкает контакты датчика, а тающий лёд держит ноль сколько угодно долго. В каждой точке ждите стабилизации — пока показания не перестанут ползти, обычно полминуты-минута; поправка, снятая на переходном процессе, унаследует его наклон. Кипяток — с оглядкой на пар: он греет всё вокруг, включая соседнюю электронику, и к окну высоты над морем стоит прибавить собственную погрешность эталона.
- Две опорные точки дают прямую y = k·x + b: k = (y2−y1)/(x2−x1) — масштаб, b = y1 − k·x1 — сдвиг
- Offset правит только вертикальную часть вранья; k и b выпрямляют ещё и наклон — датчик, вравший по-разному на краях, выравнивается по обоим концам
- k и b лежат в EEPROM восемью байтами (два float) или четырьмя (упакованные целые); пишутся один раз при выходе из калибровки
- Опоры снимают на краях диапазона: узкая база раздувает шум АЦП в градусы, поэтому скетч требует x2 не меньше x1 + 50
- Пересчёт t = k·raw + b живёт в одном месте скетча; всё остальное читает готовую температуру
- 1. Сырые коды опор: x1 = 100 (лёд) и x2 = 600 (кипяток). Чему равен k?
- 2. Чем двухточечная калибровка отличается от offset?
- 3. Вторая опорная точка снята в 20 кодах от первой. Что будет?
- 4. Сколько ячеек EEPROM занимают k и b, записанные как два float?
- 5. Как часто двухточечная калибровка тревожит ресурс EEPROM?
Частые вопросы по теме урока
- Сырые коды опор: x1 = 100 (лёд) и x2 = 600 (кипяток). Чему равен k?
- k — наклон прямой: перепад эталонов, делённый на перепад сырых кодов. 100 градусов на 500 кодов — 0,2 градуса на код; b дальше считается как y1 − k·x1. (100 − 0)/(600 − 100) = 0,2 градуса на код
- Чем двухточечная калибровка отличается от offset?
- Одна точка даёт только сдвиг. Две точки, разнесённые по диапазону, дают ещё и наклон — датчик, который врал по-разному на краях, выравнивается по обоим концам сразу. она правит и масштаб, и сдвиг — по двум точкам диапазона
- Вторая опорная точка снята в 20 кодах от первой. Что будет?
- Узкая база делит шум АЦП на малое число: k = 100/20 = 5 градусов на код, и две-три единицы шума растягиваются в десяток градусов. Правильные опоры — на краях диапазона; скетч страхуется порогом в 50 кодов. k получится огромным, и погрешность одного кода превратится в градусы
- Сколько ячеек EEPROM занимают k и b, записанные как два float?
- float занимает 4 байта, два коэффициента — 8 ячеек. Экономный вариант — упакованные целые k×1000 и b×10 по два байта каждый, как в симуляторе этого урока. восемь — по четыре на каждый
- Как часто двухточечная калибровка тревожит ресурс EEPROM?
- Коэффициенты пишутся один раз при завершении калибровки: дюжина записей в год против 100 000 паспортных циклов. Чтение ресурс не тратит вовсе. раз при выходе из калибровки — по событию, раз в месяцы