Рассада на подоконнике выросла у меня до маленькой одержимости: хотелось знать, сколько градусов у самого стекла ночью. Ртутный градусник приходилось снимать и читать при свете телефона, модуль на DS18B20 откладывался «на потом», а термистор NTC — чёрная капля с двумя ножками — лежал в наборе с самого начала и стоил копейки. Первая версия термометра честно печатала в Serial числа 200–700, из которых я упорно пытался вычесть градусы на глаз. Не вычел. Градусник, кстати, врал и в обратную сторону: пока я нёс его к окну, он успевал нагреться в руках — с термистором такая наука не нужна, он смотрит сам.
В этом уроке сделаем настоящий термометр: сопротивление термистора посчитаем из показаний АЦП, а потом формулой Стейнхарта переведём омы в градусы Цельсия с десятыми долями. По дороге разберём три тупика, съевшие у меня вечер: целочисленное деление, обрыв делителя и логарифм не той природы. Считать будем float'ами — если типы из урока про переменные подзабылись, самое время освежить.
NTC: сопротивление падает, когда растет температура
Термистор NTC (negative temperature coefficient) — резистор, у которого сопротивление падает с нагревом. Распространённый номинал из наборов — 10 кОм при 25 °C: холодный, с балкона, мой показал около 33 кОм, разогретый пальцами уползал к 8, а в кипятке был бы порядка 3.6 кОм. Номинал в маркировке — это всегда сопротивление при 25 °C, а не «сколько намеряется при комнатной температуре, как повезёт».
Линейности здесь нет и в помине: зависимость экспоненциальная, поэтому «подобрать коэффициент умножением» не выйдет — нужна формула. Констант две: B (бета, у типовых 3950 или 3435 — смотрите даташит своего экземпляра) и R0 — сопротивление при 25 °C. А ещё есть точность: ±1–5% у обычных NTC, поэтому в конце мы будем калибровать поправкой по эталону, а не удивляться расхождению. Если даташита под рукой нет, начните с B = 3950: самый частый вариант в наборах, а калибровка в конце урока подправит остаток погрешности.
По схеме термистор — брат фоторезистора из прошлого урока: тоже «резистор, который сам меняется», тоже делитель с резистором-напарником 10 кОм, тоже analogRead. Меняется только физика: вместо света — тепло. Если ночник из прошлого урока ещё стоит на макетке, считайте, половина схемы уже собрана — поменяем одну деталь и займёмся математикой, которой у ночника не было. Пин A0 остаётся A0, резистор — тот же 10 кОм, отличается только расчёт и смысл чисел.
От raw к Омам: делим честно и защищаемся от нуля
Соберём так: NTC — между 5V и A0, резистор 10 кОм — между A0 и GND. Чем горячее термистор, тем меньше его сопротивление и тем больше напряжения остаётся на нижнем плече — raw растёт с температурой. Связь сырья и сопротивления выводится из пропорции делителя: R_ntc = R_fixed × (1023 − raw) / raw. Проверка контрольной точкой: при 25 °C оба плеча по 10 кОм, raw должен быть около 512, и формула возвращает примерно 9980 Ом — почти ровно 10 к. Сходится, схема живая. Обратите внимание, куда встала проверка: не «поверил формуле», а подставил контрольную точку и сравнил — так ловится добрая половина ошибок в скобках.
У формулы есть слепые зоны: при raw = 0 — деление на ноль, при raw = 1023 — тоже вырожденный случай. На живой схеме это не экзотика: вынутый из макетки термистор даст 0, замкнутый на 5V — 1023. Я получил «температуру минус 273 градуса» и минуту был уверен, что открыл абсолютный ноль на собственном балконе. Защита простая: если raw меньше 10 — печатаем «нет данных» и пропускаем расчёт.
Второй тупик прячется внутри первой же строки — целые числа. Если написать 1 / 298 без точки, C++ поделит целочисленно и вернёт ноль; то же касается 1023 без .0. Правило я вынес в шпаргалку: в температурных расчётах не бывает «просто чисел» — каждое либо float, либо константа с десятичной точкой. Откуда ноль в выводе — ищите деление, а не логарифм.
// NTC к 5V, резистор 10 кОм к GND, A0 в средней точкеvoid setup() { Serial.begin(9600);} void loop() { int raw = analogRead(A0); if (raw < 10) { Serial.println("NTC ne podklyuchen"); } else { float r = 10000.0 * (1023.0 - raw) / raw; // омы Serial.print("raw="); Serial.print(raw); Serial.print(" R="); Serial.println(r, 0); } delay(1000);}Формула Стейнхарта: логарифм и настоящие градусы
Формула Стейнхарта в бета-упрощении выглядит грозно, но собирается из трёх констант: 1/T = 1/T0 + ln(R/R0)/B. T0 — 298.15 кельвинов (те самые 25 °C), B — бета из даташита, R0 — номинал при 25 °C. Считаем T в кельвинах, вычитаем 273.15 — получаем Цельсия. Вся математика — одна строка и три константы с упаковки термистора. Полное уравнение Стейнхарта носит три коэффициента A, B, C и точнее, но для домашнего термометра бета-версии хватает с запасом.
Две детали, на которых спотыкаются. Первая: функция log() в Arduino — натуральный логарифм, то есть ln; отдельного написания ln в языке нет, а десятичный живёт в log10. Если подставить в формулу log10 — градусы поедут мимо: у меня «комнатная» температура вышла под шестьдесят. Вторая: результат храним во float, а не в int — иначе 24.9 превратится в 24 уже на этапе сохранения, до всякого Serial.
Печатать лучше с десятыми: Serial.println(c, 1) выводит один знак после запятой — движение термистора видно сразу, пока греете его пальцами. Выводить в Serial по-человечески мы учились в первом уроке про Serial Monitor, а здесь появляется новый нюанс: второй аргумент print() задаёт число знаков. Без него float печатается с двумя по умолчанию — тоже рабочий вариант, но столбец «24.87» дрожит сильнее, чем «24.9», и глаз цепляется за шум. Для табло на стене удобнее целые: Serial.println(c, 0) даст «25» без дробей — но инерцию пальцами вы так не увидите.
const float BETA = 3950.0; // бета из даташитаconst float R_NOMINAL = 10000.0; // R термистора при 25 Cconst float T0 = 298.15; // 25 C в кельвинах // r — сопротивление из шага 1float t = 1.0 / T0 + log(r / R_NOMINAL) / BETA;float c = 1.0 / t - 273.15; // кельвины -> ЦельсияSerial.print("T=");Serial.println(c, 1); // один знак после запятойconst float BETA = 3950.0;const float R_FIXED = 10000.0; // нижнее плечо делителяconst float R_NOMINAL = 10000.0; // R термистора при 25 Cconst float T0 = 298.15; void setup() { Serial.begin(9600);} void loop() { int raw = analogRead(A0); if (raw < 10) { Serial.println("NTC ne podklyuchen"); } else { float r = R_FIXED * (1023.0 - raw) / raw; float t = 1.0 / T0 + log(r / R_NOMINAL) / BETA; float c = 1.0 / t - 273.15; Serial.print("raw="); Serial.print(raw); Serial.print(" T="); Serial.println(c, 1); } delay(1000);}| Температура | R_ntc | raw в нашей схеме | Скетч напечатает |
|---|---|---|---|
| 0 °C | ≈ 33 000 Ом | ≈ 238 | T=0.4 |
| 25 °C | 10 000 Ом | ≈ 512 | T=25.0 |
| 50 °C | ≈ 3 600 Ом | ≈ 752 | T=49.9 |
- Сверьте с эталоном: положите NTC рядом с комнатным термометром, подождите минуту и сравните. Расхождение 1–2 °C — норма для бета-формулы; внесите поправку c = c - 0.5 прямо в код.
- Погрейте датчик пальцами, не отпуская, и следите за Serial: пара десятых градуса в секунду — инерция термистора; мгновенных прыжков ждать не стоит, и это нормально.
- Проверьте формулу на прочность: уберите .0 у 1023.0 в строке расчёта r и посмотрите, как сломается вывод. Верните точку — и убедитесь, что контрольная точка 25 °C восстановилась.
- Поменяйте плечи: NTC к GND, резистор к 5V. Формула станет R = R_FIXED × raw / (1023 − raw), а raw при нагреве начнёт падать; проверьте 25 °C по новой схеме.
- Сделайте сигнализацию перегрева: при c > 30 зажигайте светодиод на пине 9 — и сразу вспомните гистерезис из прошлого урока, иначе на границе тридцати он будет строчить.
- NTC 10 кОм — резистор с падающим от нагрева сопротивлением: около 33 кОм в холоде, 3.6 кОм в жаре; номинал в маркировке всегда про 25 °C.
- Сопротивление из АЦП: R_ntc = R_FIXED × (1023 − raw) / raw при NTC к 5V; зеркальная сборка переворачивает формулу и логику.
- Бета-формула Стейнхарта: 1/T = 1/T0 + log(R/R0)/B; в Arduino log() — натуральный логарифм, T в кельвинах, минус 273.15 — в Цельсия.
- Все температурные константы — с десятичной точкой, переменные float: целочисленное 1/298 обнуляет всю формулу.
- raw < 10 — признак обрыва: деление на ноль даёт мусор, защитный if обязателен; Serial.print(c, 1) печатает один знак после запятой, а println(c, 0) — целые градусы без дробей.
- 1. Термистор NTC нагрели от 25 до 50 °C. Его сопротивление...
- 2. Функция log() в Arduino и в нашем скетче считает...
- 3. Скетч печатает ровно −273.1 при любом нагреве. Первое место, куда смотреть?
- 4. NTC вынут из макетки, analogRead возвращает 0. Что посчитает R = 10000.0 × (1023 − raw) / raw?
Термометр получился из копеечной детали и одной строки математики — и это первый урок, где проводов оказалось меньше, чем формул. Базовый раздел подошёл к концу: вы умеете мигать, считать, ветвить, читать ручки и датчики, строить пороги с гистерезисом и пересчитывать сырые числа в физические величины. Дальше по плану раздела — звук: пьезозуммер, tone() и noTone(), сигналы и сирены. А термометр не разбирайте: он встретится снова — от теплицы до термостата, и термостат с гистерезисом из раздела про датчики — прямое продолжение сегодняшней формулы. И да, пересчитайте контрольные точки из таблицы выше руками на бумажке один раз — после этого формула перестанет быть магией.
Частые вопросы по теме урока
- Термистор NTC нагрели от 25 до 50 °C. Его сопротивление...
- NTC — negative temperature coefficient: с ростом температуры сопротивление падает, с 10 кОм при 25 °C до 3.6 кОм при 50 °C. Растёт оно при охлаждении. Упало с ~10 кОм до ~3.6 кОм
- Функция log() в Arduino и в нашем скетче считает...
- log() в C++ и Arduino — натуральный логарифм, именно он стоит в формуле. Десятичный живёт отдельно под именем log10 и градусы в бета-формуле ломает. Натуральный логарифм ln — как нужно в формуле Стейнхарта
- Скетч печатает ровно −273.1 при любом нагреве. Первое место, куда смотреть?
- Ровные −273.1 в выводе — знак, что 1/T получилось нулём из-за целочисленного деления. Все константы — с точкой, переменные — float. Константы без десятичной точки и int-переменные: целочисленное 1/298 даёт ноль
- NTC вынут из макетки, analogRead возвращает 0. Что посчитает R = 10000.0 × (1023 − raw) / raw?
- При raw = 0 знаменатель нулевой: результат непредсказуем, на реальной плате это мусор. Защита if (raw < 10) печатает «нет данных» вместо чепухи. Деление на ноль: мусор или бесконечность — нужен защитный if