Термометр для теплицы я завёл по формуле из урока про термистор и Штейнхарта: логарифм, возведение в степень, три коэффициента — на макетке работало. Но каждое измерение тянуло за собой float-арифметику, которой у ATmega328P нет в железе, а сотые доли градуса на экране всё равно никто не читал. Когда в скетч добавились Serial-печать и второй датчик, пришло время считать не десятичные знаки, а байты.
Выход, которым пользуются и производители датчиков, — таблица значений. Заранее считаете или замеряете пары «сырой код АЦП → градусы», кладёте в массив, а в скетче остаётся найти пару соседних точек и дотянуть линейной интерполяцией. Инструкция Adafruit по термисторам прямо предлагает таблицу как второй официальный путь после формулы: она дешевле по коду, прозрачнее при отладке и калибрует конкретный экземпляр датчика, а не абстрактный NTC из справочника.
Сами массивы с нуля разобраны в уроке про массивы и журнал показаний, сырой analogRead — в базовом уроке о потенциометре. Здесь третий маршрут: как устроена таблица с интерполяцией, почему она иногда точнее формулы и какой ценой покупается место в ОЗУ.
Кроме термистора таблицы выручают в двух местах: кривая яркости «свечи» без единого вызова sin и любые нелинейные пересчёты вроде процента заряда по напряжению. Везде схема одна и та же: измеренные узлы, поиск соседей, дотяжка. Научитесь один раз — применяйте на всём, что криво и не желает лезть в формулу: даже ёмкостный датчик влажности почвы, чей сырой код зависит от питания и качества контакта, калибруется таблицей честнее, чем любой коэффициент из чужого примера.
Узлы таблицы: поиск соседей и интерполяция
Таблица — два массива одинаковой длины: сырые коды RAW_T и температуры T_GR. У моего делителя (NTC 10 кОм на A2 и резистор 10 кОм вверх) коды при −20 °C давят к сотне, при +100 °C растут почти до тысячи — десять узлов с шагом градусов в десять покрывают весь бытовой диапазон. Числа у вас будут другими: они зависят от номиналов делителя, и в этом одна из причин держать таблицу в своём скетче, а не копировать из чужого.
Поиск простой: идём по массиву, пока raw лежит между двумя соседними узлами. Нашли пару — дотягиваем линейно: t = t0 + (raw − r0) × (t1 − t0) / (r1 − r0). Между узлами зависимость принимается прямой: у термистора кривая монотонная и плавная, и на шаге в десять градусов погрешность прямой укладывается в десятые доли градуса — для теплицы, где решает порог «пора полить», этого за глаза.
Края обрабатываем честно: код ниже первого узла — возвращаем крайнюю температуру, выше последнего — крайнюю, и печатаем пометку «вне диапазона». Термометр, молча отдающий мусор за краями таблицы, хуже термометра, который говорит, что ему не хватает таблицы: с мусором вы узнаете о проблеме по погибшей рассаде.
Одна деталь с последствиями: промежуточное умножение в интерполяции может переполнить int. Разность кодов до тысячи на разницу градусов в пятнадцать даёт 15 000 — влезает, но при другом шаге узлов вылезет за 32 767 молча. Промежуточное считайте в long: (long)(raw − RAW_T[i]) * dt — приведение стоит ноль байтов, а ловушку переполнения отсекает на корню; вся арифметика потолков разобрана в уроке о типах и переполнении.
Деление в конце — целочисленное: дробная часть отбрасывается, а не округляется. Если нужны десятые градуса, возвращайте температуру, умноженную на десять: t10 = t0 × 10 + (raw − r0) × (t1 − t0) × 10 / (r1 − r0), а точку ставьте только в печати — Serial.print(t10 / 10), затем точка, затем t10 % 10. Половина градуса теряется тихо: для порога полива это не страшно, а вот в инкубаторе масштаб ×10 уже обязателен.
const int PIN_SVECHA = 9; // на Uno ШИМ живёт на 3, 5, 6, 9, 10, 11// 12 уровней пламени, снятые «на глаз» с настоящей свечи; правятся за минутуconst byte PLAMYA[] = {40, 95, 150, 205, 240, 250, 235, 190, 145, 100, 60, 48}; void setup() { pinMode(PIN_SVECHA, OUTPUT);} void loop() { for (byte i = 0; i < 12; i++) { // разгорание analogWrite(PIN_SVECHA, PLAMYA[i]); delay(55); } for (byte i = 10; i > 0; i--) { // затухание без дубля нижнего уровня analogWrite(PIN_SVECHA, PLAMYA[i]); delay(55); }}Кривая свечи — пример, где таблица бьёт формулу вчистую. Синус на Uno — вызов float-библиотеки: у ATmega328P нет аппаратного умножителя дробных чисел, каждая волна — сотни тактов и лишние килобайты флеша. Таблица из двенадцати байтов даёт то же самое, плюс то, чего у формулы нет: кривую можно нарисовать руками. Мои сорок — двести пятьдесят — сорок сняты с реального пламени; добавьте в каждый уровень random(−15, 15), и дрожание станет живым.
Точно так же устроены ноты в уроке про мелодию на массиве и шаги бегущих огней: вместо вычислений — заранее подобранные значения, а скетч только гуляет по индексам. Плавное затухание без таблицы делается и счётчиком ШИМ с шагом — тот путь разобран в уроке про ШИМ и яркость, но кривую произвольной формы, с горбами и дрожанием, даёт только таблица.
Заметьте тип: byte, а не int. Яркость не выходит за 0–255, и двенадцать уровней занимают двенадцать байтов; в int они съели бы двадцать четыре. Это первая экономия — раньше, чем разговор про PROGMEM: половина расхода ОЗУ снимается правильным выбором типа, без всяких специальных знаний.
| Критерий | Таблица + интерполяция | Формула (Штейнхарт-Харт) |
|---|---|---|
| Код на плате | поиск соседей и одна дробь на целых числах | log, pow и деления на float |
| ОЗУ | 2 байта на точку: 10 узлов — 20 байтов | коэффициенты почти бесплатны |
| Точность | как у ваших замеров, между узлами прямая | ±0,1–0,5 °C у хорошего NTC |
| Калибровка экземпляра | вписывается прямо в узлы таблицы | нужно пересчитывать коэффициенты |
| Отладка | каждый узел проверяется печатью за минуту | коэффициенты не проверить без справочника |
Цена таблицы: байты, которых в ОЗУ нет
Массив-константа по умолчанию живёт в ОЗУ: при старте скетч копирует его из флеша в SRAM и дальше работает с копией. На Uno всего 2 КБ SRAM, и арифметика у таблицы жёсткая: int — два байта, десять точек — двадцать, пятьдесят точек — сто байтов. Для тепличного термометра это расход незаметный, но счёт ведите сразу: узлов × 2 для int и × 1 для byte, до компиляции, на салфетке.
Ловушка пахнет, когда таблица растёт: int-таблица на 256 точек — уже 512 байтов, половина килобайта и четверь всей ОЗУ; тысяча точек — 2048 байтов, то есть вся SRAM Uno целиком, и скетч не начинается вовсе. Полкило под таблицу кодов АЦП — типовая глупость при попытке засунуть весь диапазон 0–1023 с шагом в единицу: интерполяция придумана именно затем, чтобы этого не делать.
Три ступени экономии по возрастанию сложности: правильный тип (byte вместо int, когда значения 0–255), меньше узлов (десять точек с интерполяцией почти всегда достаточно) и PROGMEM — массив остаётся во флеше, чтение через pgm_read_word. Третья ступень разобрана в уроке про F() и PROGMEM-строки; моё мнение: пока таблица до пятидесяти точек, PROGMEM не нужен — это усложнение ради байтов, которых и так хватало.
Проверяйте расход до прошивки: Arduino IDE при сборке печатает строку о глобальных переменных и свободной ОЗУ. Если глобальные съели больше полутора килобайтов из двух, среда предупредит «Low memory» — но лучше ловить перебор в счётчике точек на этапе планирования, чем в предупреждении компилятора на этапе сборки.
Моё правило выбора: если зависимость можно снять измерениями и точность ±1–2 °C устраивает — таблица. Она калибрует конкретный экземпляр: у каждого NTC разброс в несколько процентов, и формула со «средними» коэффициентами меряет именно ваш термистор хуже, чем десять замеров, сделанных по нему самому и вписанных в узлы.
Формула выигрывает в трёх случаях: нужна дробная точность на хорошем датчике, нет эталонного термометра, чтобы снять точки, и когда диапазон шире таблицы разумной длины. У формулы есть и скрытый плюс: она не врёт между узлами. Если шаг таблицы десять градусов, а зависимость в этом месте крутая, интерполяция сгладит горб, который формула бы увидела.
Та же схема работает там, где формулы нет вовсе. Процент заряда литий-ионной банки по напряжению — кривая, зависящая от нагрузки и температуры; я держу таблицу из десяти пар «милливольты → проценты», снятых на своей банке в покое, и получаю процент без единого float. Форма не меняется: два массива, поиск соседей, дотяжка — меняется только физический смысл колонок.
Условие, при котором я меняю мнение: на ESP32 или Mega с десятками килобайт ОЗУ таблица почти всегда честнее по трудозатратам — двести точек int там стоят 400 байтов, которых никто не заметит. Водораздел не «таблица или формула», а «сколько у вас ОЗУ и есть ли чем калибровать»: на Uno считайте байты, на ESP32 просто снимайте точки.
- 1. Сколько ОЗУ займёт int-таблица на 50 узлов на Uno?
- 2. Что вернёт функция поиска при raw ниже первого узла таблицы?
- 3. Зачем в интерполяции приведение (long)(raw − RAW_T[i]) * dt?
- 4. Почему кривую свечи я делаю таблицей byte, а не формулой sin?
- 5. Когда формула честнее таблицы?
- Таблица — два массива: сырые коды и значения; между узлами работает линейная интерполяция t = t0 + (raw − r0) × (t1 − t0) / (r1 − r0).
- Считайте байты заранее: int — 2 байта на точку, byte — 1; int-таблица на 256 точек — половина килобайта из 2 КБ ОЗУ Uno.
- Края таблицы обрабатываются явно: за границей возвращайте крайний узел и помечайте это в печати.
- Промежуточное умножение в интерполяции считайте в long — молчаливое переполнение int ловится один раз и не там, где вы смотрите.
- Таблица калибрует конкретный экземпляр датчика; формула нужна при дробной точности, отсутствии эталона и огромном диапазоне.
Частые вопросы по теме урока
- Сколько ОЗУ займёт int-таблица на 50 узлов на Uno?
- int — 2 байта: узлов × 2 = 100 байтов из 2048 доступных. Таблицу всего диапазона 0–1023 с шагом 1 в int на Uno не засунуть — это все 2 КБ. 100 байтов
- Что вернёт функция поиска при raw ниже первого узла таблицы?
- Края обрабатываются явно: за границей возвращается крайний узел, лучше с пометкой в печати. Молча отдавать мусор за краями — худший вариант. крайнюю температуру первого узла
- Зачем в интерполяции приведение (long)(raw − RAW_T[i]) * dt?
- Разность кодов до тысячи на разницу градусов легко превышает потолок int 32 767. Промежуточное умножение в long отсекает молчаливое переполнение. чтобы промежуточное умножение не переполнило int
- Почему кривую свечи я делаю таблицей byte, а не формулой sin?
- sin на 8-битной плате — float-библиотека: сотни тактов и лишние килобайты флеша. Таблица из 12 байтов даёт кривую любой формы и правится за минуту. таблица дешевле по коду, и кривую можно нарисовать руками
- Когда формула честнее таблицы?
- Формула выигрывает при дробной точности, отсутствии эталонного прибора и широком диапазоне. При возможности калибровать и запасе ОЗУ таблица честнее. когда нужна дробная точность и нет эталона для замеров