Сыроварня поставила мне ультиматум: при 12 градусах термометр должен показывать плюс-минус полградуса, потому что на 13 камамбер уже не тот. Двухточечная калибровка из урока про k и b выпрямила середину диапазона честно — и так же честно уехала на краях: при 4 градусах прибор врал на четыре, при 30 — на три с хвостом. NTC-термистор нелинеен по природе, и никакая прямая через две точки не выпрямит экспоненту целиком, мы эту границу сами и зафиксировали в том уроке.
Решение старое, как аналоговые приборы: снять кривую в двадцати точках по всему диапазону и хранить их в EEPROM, а при чтении находить два соседних узла и дорисовывать прямую между ними. Двадцать точек по 4-5 градусов — и кривизна между узлами падает ниже дрожания самого АЦП: прибор начинает врать меньше, чем мерцает последний разряд. Вся таблица — 80 байтов, восемь процентов килобайта, а точность прирастает там, где двух точек физически не хватало.
В этом уроке раскладываем таблицу по ячейкам, пишем интерполяцию на целой арифметике без одного float, зажимаем края и делаем ремонт одной точки без перезаписи соседей. Восемь точек адресной арифметикой мы уже держали в уроке про массив в EEPROM — там правилась одна ячейка циклом; здесь добавляется вторая половина приёма: дорисовка значения между узлами при чтении.
Раскладка: 80 ячеек и две константы
Таблица — массив из двадцати пар: сырой отсчёт АЦП и температура с десятыми. Температуру храню как int, умноженную на десять: от 20 до 850 для диапазона плюс 2 — плюс 85. Дробную часть в EEPROM держать не за что — float разбирался ещё в уроке про int и float, а фиксированная точка дешевле и точнее ровно настолько, насколько снята кривая. Пара занимает четыре байта: два на raw, два на температуру, всего 80 байтов. База — адрес 20, до адреса 99: место выбрано после magic-байта и версии, чтобы никто из старых жильцов не наступил на таблицу.
Заливка при первом запуске выглядит как у любого другого блока: если маркер пуст, EEPROM.put кладёт оба массива целиком и ставит печать. Дальше таблица живёт своей жизнью: перекалибровка одной точки правит update-ом ровно два байта пары, соседи не трогаются. Это тот же принцип, что в уроке про массив — число точек в одной константе, адреса через i * 4, никаких ручных литералов. Добавите точку — константа сдвинет все границы сама, и держите число точек в одной константе во всех циклах.
Сколько точек брать — вопрос к кривой, а не к памяти. Мой шаг: по 4-5 градусов или по 50 квантов АЦП, что ближе к равномерности кривой. Пятнадцать точек дали бы 60 байтов и почти ту же точность; сто точек — 400 байтов и полкилобайта впустую, потому что между узлами по 10 квантов кривизна уже меньше собственного шума АЦП. Сначала снимите кривую на бумаге, потом режьте её на узлы. И проверьте хвост: граница таблицы обязана кончаться до EEPROM.length(), иначе она наступит на соседей по карте.
#include <EEPROM.h> // Таблица: 20 пар (raw, t*10), база 20, маркер в ячейке 5.// Пустая память читается 255 - по нему ловим первый запуск.const int BAZA = 20;const int TOCHKI = 20;const int MAGIC = 5; int x[TOCHKI]; // сырые отсчёты, по возрастаниюint y[TOCHKI]; // температуры, умноженные на 10 int zavodRaw[TOCHKI] = {1020, 968, 913, 856, 797, 738, 679, 622, 566, 513, 462, 414, 369, 328, 290, 255, 224, 196, 171, 149};int zavodT[TOCHKI] = {20, 70, 120, 170, 220, 270, 320, 370, 420, 470, 520, 570, 620, 670, 720, 770, 800, 820, 840, 850}; void zalitZavod() { EEPROM.put(BAZA, zavodRaw); EEPROM.put(BAZA + TOCHKI * 2, zavodT); EEPROM.update(MAGIC, 170); Serial.println("tabelica zalita zavodskaya");} void chitatTablicu() { EEPROM.get(BAZA, x); EEPROM.get(BAZA + TOCHKI * 2, y);} long tempDesyataya(int raw) { if (raw <= x[0]) { return y[0]; } // зажим слева if (raw >= x[TOCHKI - 1]) { return y[TOCHKI - 1]; } // зажим справа for (int i = 0; i < TOCHKI - 1; i++) { if (raw <= x[i + 1]) { // произведение - в long: до 300 * 1000, int не влезает return y[i] + (long)(y[i + 1] - y[i]) * (raw - x[i]) / (x[i + 1] - x[i]); } } return y[TOCHKI - 1];} void setup() { Serial.begin(9600); if (EEPROM.read(MAGIC) != 170) { zalitZavod(); } chitatTablicu();} unsigned long poslednij = 0; void loop() { if (millis() - poslednij >= 1000) { poslednij = millis(); int raw = analogRead(A0); long t = tempDesyataya(raw); Serial.print("raw="); Serial.print(raw); Serial.print(" t="); Serial.print(t / 10); Serial.print("."); Serial.println(t % 10); }}Арифметика между точками
Интерполяция — одна формула: найдя сегмент, где x[i] меньше или равен отсчёту, а x[i+1] — больше, добавляем к y[i] долю перепада температуры пропорционально месту отсчёта внутри сегмента. Поиск — 19 сравнений в худшем случае, на 16 МГц это микросекунды: вызывать можно хоть каждый проход loop, измерять производительность не придётся. Цикл поиска — обычный for из урока про циклы, ничего экзотического.
Отдельный плюс целой арифметики — отсутствие float вообще. На Uno нет аппаратной плавающей точки: одно умножение float стоит десятки микросекунд и раздувает скетч на пару килобайтов, целочисленный пересчёт — такты и ноль лишних библиотек. Десятые доли градуса здесь — вопрос масштаба, а не типа: держите температуры умноженными на десять до последней печати и делите на десять только при выводе.
Главная ловушка формулы — порядок операций. Произведение (y[i+1] - y[i]) * (raw - x[i]) при перепаде в 300 десятых и плече в 1000 квантов даёт до 300000 — int на Uno кончается на 32767, и старшие разряды молча отваливаются. Приведение к long перед умножением обязательно, деление — строго после: 300000 / 1000 честно даст 300, а деление до умножения съест всё до нуля. Это та же арифметика переполнения, что мы ловили со счётчиками, только здесь она врёт градусами, а не сутками.
Края таблицы — отдельная честность. За первым и последним узлом прямой не существует: экстраполяция выдумала бы температуру из ниоткуда, поэтому отсчёты вне диапазона получают значение крайнего узла, а светодиод включает признак зажима. Оператор сыроварни видит мигание и знает: датчик вышел за калибровку, верить числу можно с оговоркой. Тихий зажим без признака — худший вариант: прибор выглядит уверенным там, где он слеп.
Таблица живёт
Съём точки — самая скучная часть, и её нельзя спешить. Датчик и эталон должны стоять рядом минимум десять минут в устоявшемся режиме: термомасса ленивая, и первые замеры после перестановки отражают переезд, а не температуру. Каждую точку снимаю трижды с интервалом в минуту и беру среднее: АЦП Uno дрожит на плюс-минус два кванта, и одиночный замер ловит именно дрожь. Правка — update двух байтов пары, и таблица продолжает жить: перекалибровка по сезону не перезаписывает 80 байтов, только 4.
Съём кривой не требует лаборатории. Две опорные точки есть на любой кухне: стакан с тающим льдом держит ноль, кипящая вода при нормальном давлении — сотню; они ловят грубые промахи шкалы. Промежуточные узлы снимают по эталонному термометру, положив его рядом с датчиком — следите, чтобы оба смотрели в одну температуру, а не в разные углы коробки. Раскладку узлов запишите комментарием к скетчу: таблица без паспорта через полгода превращается в магию.
Двадцать точек — не универсальный ответ, а инструмент. Смещение на весь диапазон из урока про калибровку смещением стоит два байта и хватает, когда кривая почти прямая; две точки — восемь байтов и середина диапазона; таблица — восемьдесят байтов и весь диапазон с краями. Замеры по расписанию из урока про millis вместо delay не мешают ни одному из вариантов: таблица не тяжелее других в loop, она тяжелее только на этапе съёма кривой — один вечер с эталонным градусником.
Ресурс таблицы пересчитывается так же скучно, как и любой другой записи. Сыроварня правит три-четыре точки за сезон: по четыре байта каждая — с запасом стотысячного ресурса таблица переживёт не один прибор, а целую линейку. А вот журнал рядом с таблицей селить не стоит: у них разные ритмы записи, и частые правки журнала не должны проходить через те же ячейки, что и редкие, но драгоценные узлы кривой. Таблице — тихий квартал карты, журналам — своё.
| Подход | Байты в EEPROM | Где врёт |
|---|---|---|
| Смещение на весь диапазон | 2 | Края диапазона: кривая уходит от сдвинутой прямой |
| Две точки: k и b | 8 | Выпрямляет только середину, NTC на краях уезжает на 3-4 градуса |
| Таблица 20 точек | 80 | Между узлами меньше 0,2 градуса; за границами — только зажим |
| Полином третьей степени | 12-16 и float-арифметика | Осцилляции между узлами при неудачных коэффициентах |
- 1. Сколько ячеек EEPROM займёт таблица из 20 пар (int raw, int t*10)?
- 2. Где в формуле интерполяции обязано стоять приведение к long?
- 3. Отсчёт АЦП вышел за первый узел таблицы. Честное поведение прибора?
- 4. Перекалибровали точку номер 7. Сколько байтов реально перезапишет аккуратный скетч?
- 5. Как снять узел таблицы, чтобы он не запомнил дрожь АЦП?
- 20 пар по 4 байта — 80 ячеек, восемь процентов килобайта; база после magic-байта, шаг i * 4
- Температуру держите в int, умноженной на десять: float в EEPROM не за что уложить
- Произведение перепада на плечо — в long, деление последним: иначе сегмент считает ноль
- За краями таблицы прямой нет: зажим к крайнему узлу плюс световой признак, никаких экстраполяций
- Перекалибровка одной точки — update двух байтов; съём узла — три замера в устоявшемся режиме
Частые вопросы по теме урока
- Сколько ячеек EEPROM займёт таблица из 20 пар (int raw, int t*10)?
- Пара из двух int занимает 4 байта, двадцать пар — 80 ячеек. Это около восьми процентов килобайта Uno, но 40 и 60 — типовые промахи арифметики на пару. 80
- Где в формуле интерполяции обязано стоять приведение к long?
- Произведение до 300000 не влезает в int с его 32767. Приведение перед умножением сохраняет разряды, деление после — возвращает масштаб. Обратный порядок съедает всё до нуля. Перед произведением перепада на плечо
- Отсчёт АЦП вышел за первый узел таблицы. Честное поведение прибора?
- За границами таблицы прямой не существует, любая экстраполяция выдумывает температуру. Зажим со светодиодом или флагом честнее: оператор знает, что прибор вне калибровки. Возврат значения крайнего узла и признак зажима
- Перекалибровали точку номер 7. Сколько байтов реально перезапишет аккуратный скетч?
- Обычно правят температуру узла при прежнем raw: два байта. update пропускает неизменившиеся байты, соседние 19 пар не трогаются вовсе — в этом выгода раскладки по парам. 2 — только десятую долю температуры
- Как снять узел таблицы, чтобы он не запомнил дрожь АЦП?
- Термомасса ленива, первые замеры отражают переезд датчика. Три замера с минутой паузы в устоявшемся режиме дают устойчивое среднее; максимум выхватывает выброс, а не режим. Три замера с интервалом в устоявшемся режиме и среднее