Однажды в июле мой уличный термометр показал 75.20. Квартира в этот момент была тёплой, но не сауной: по Цельсию там стояло 24. Я полчаса лечил датчик — переставлял провод, сдувал пыль с решётки, подносил ладонь и смотрел, растёт ли число. Оно росло, от 75 и выше, и это окончательно сбивало с толку. Виноват оказался не датчик, а строка из чужого туториала: readTemperature(true). Аргумент true означает «отдай Фаренгейт», и 75.20 по Фаренгейту — это как раз мои 24 по Цельсию. Датчик был здоров; меня полчаса гонял один пропущенный в разборе аргумент. Монитор порта при этом работал исправно, интервал был ровно две секунды, и всё это делало картину ещё абсурднее.
С тех пор я начинаю разбор любой чужой DHT-строки с простого вопроса: что именно возвращает функция и в каких единицах. В этом уроке разберём обе функции чтения до дна: что лежит внутри float, откуда берутся и куда деваются десятые доли, как перевести показания в привычные вам единицы и зачем в библиотеке кэш, из-за которого два вызова подряд могут вернуть одно и то же число. Плюс одна функция, о которой туториалы молчат, — тепловой индекс.
Что именно возвращают функции чтения
readTemperature() и readHumidity() возвращают float — дробные числа с плавающей точкой. Первая отдаёт градусы Цельсия, вторая — проценты относительной влажности от 0 до 100. Float выбран не по прихоти автора библиотеки: протокол датчика — целые байты, но библиотека делит и пересчитывает их, а хранить результат в int значило бы терять десятые доли у DHT22, который честно отдаёт шаг в 0.1 градуса. Чем float устроен в памяти и почему его опасно сравнивать через двойное равенство — в уроке про типы переменных.
Теперь оговорка, которую я долго отказывался признавать: DHT11 физически не умеет десятые доли. В его протоколе температура — целый байт, 24 и есть 24, без хвоста. Все «24.30» из монитора у DHT11 — это нули, дописанные библиотекой, а не измерение. DHT22 возвращает шаг 0.1 °C, и только там десятые настоящие: шестнадцать битов температуры в его протоколе делятся на десять, и дробь выходит честная, зашитая в разрядность. Моё правило: у DHT11 в графиках и отчётах пишу целые числа, чтобы не обещать точность, которой в датчике нет. Если нужна настоящая дробная температура не из DHT — это либо DHT22, либо термистор NTC со своей таблицей пересчёта.
#include "DHT.h" #define DHTPIN 2#define DHTTYPE DHT22 // теперь десятые доли настоящие DHT dht(DHTPIN, DHTTYPE); void setup() { Serial.begin(9600); dht.begin();} void loop() { float t = dht.readTemperature(); // Цельсий float f = dht.readTemperature(true); // Фаренгейт float h = dht.readHumidity(); // проценты float hi = dht.computeHeatIndex(f, h); // тепловой индекс, F Serial.print("t = "); Serial.println(t); Serial.print("f = "); Serial.println(f); Serial.print("h = "); Serial.println(h); Serial.print("hi = "); Serial.println(hi); delay(2000);}| Вызов | Что возвращает | Формат и диапазон |
|---|---|---|
| readTemperature() | Температура, °C | DHT11: целые, 0…50; DHT22: шаг 0.1, −40…+80 |
| readTemperature(true) | Температура, °F | Те же данные в другой шкале: F = C × 9/5 + 32 |
| readHumidity() | Относительная влажность, % | float; DHT11: 20–80, DHT22: 0–100 |
| computeHeatIndex(f, h) | Тепловой индекс, «как ощущается» | По умолчанию °F; с третьим аргументом true — °C |
| isnan(значение) | Проверка сорванного чтения | true, если вернулось nan — об этом следующий урок |
Разберём пересчёт руками, потому что формулу полезно держать в голове: градусы по Фаренгейту — это Цельсий, умноженный на девять, делённый на пять и плюс тридцать два. В коде я всегда пишу t * 9.0 / 5.0 + 32.0 с явными точками: если написать t * 9 / 5, часть компиляторов посчитает деление в целых числах и потеряет дробь на каждом шаге. Один символ — а разница между 77.0 и 75.0 на графике. Три контрольные точки для проверки в уме: при 25.0 должно выйти ровно 77.0, при 20.0 — 68.0, при 30.0 — 86.0. Обратный ход тоже полезно помнить: из Фаренгейта в Цельсий — минус 32, умножить на 5 и разделить на 9.
computeHeatIndex(f, h) — «температура, как её ощущает человек при такой влажности». При 30 °C и 70 % влажности индекс покажет около 35 °C: испарению пота мешает мокрый воздух. Для теплицы, инкубатора и улицы функция полезная, для комнаты — избыточная; я включаю её только в проекты, где цифры читает человек. В комнате я печатаю только Цельсий: вторая шкала в одном графике легко превращается в бардак из рядов разного масштаба. А вот про округление запомните ловушку: трюк int(t + 0.5) корректен только выше нуля. Для минусовой температуры он врёт: int(-1.5 + 0.5) даст -1, хотя по правилам должно выйти -2. Если скетч поедет на улицу — берите round(t), она с минусом работает честно.
float t = dht.readTemperature(); // int(t + 0.5) честно только выше нуля:// int(-1.5 + 0.5) даст -1, а должно быть -2int tWhole = round(t); // round(-1.5) = -2, как и положено Serial.print("t = ");Serial.println(t);Serial.print("tR = ");Serial.println(tWhole);Кэш библиотеки: почему два вызова возвращают одно и то же
Самый неочевидный факт про библиотеку: между вашими вызовами она держит кэш. После каждого успешного обмена запоминается момент чтения, и если вы спросите температуру снова раньше, чем через две секунды, библиотека не станет трогать сенсор — вернёт прежнее значение. Отсюда два симптома, которые регулярно всплывают на форумах. Два вызова readTemperature() подряд в одном loop() возвращают одно и то же число — это не сенсор «застыл», это кэш. И опрос чаще двух раз в секунду не даёт свежих данных, сколько ни проси: библиотека молча отдаёт старое. Если данные уходят в журнал чаще двух секунд, вы пишете дубликаты: вес файла растёт, а информации не прибавляется.
Из кэша следует правильная архитектура: читать датчик по таймеру раз в две секунды, а результат класть в две переменные, с которыми loop() работает сколько угодно. Так собран любой приличный проект на DHT, включая термостат с гистерезисом из этого раздела, — схема чтения там та же, что мы ставили в первом уроке про подключение. В симуляторе ниже кэша нет, ползунки отвечают сразу, но привычку читать по таймеру ставьте с самого начала. Отсюда, кстати, и ответ на вопрос, почему в чужих скетчах стоит delay(2000): это не перестраховка автора, а обход кэша.
- Откройте Serial Plotter и медленно двигайте ползунок: три ряда t, f и h живут в разных шкалах и рвут график. Оставьте в печати один ряд — и сравните, как меняется картинка.
- Добавьте целочисленную переменную с округлением int(t + 0.5) и напечатайте её третьей парой. Проверьте при t = 25.4 и t = 25.6: должно выйти 25 и 26. Потом повторите то же через round() и сравните: выше нуля оба способа совпадут, а на минусе разойдутся.
- Посчитайте среднее из пяти чтений подряд и выведите парой avg = … Подсказка: копите сумму в переменной, делите на 5.0 с точкой — иначе потеряете дробь. Сравните среднее с одиночным чтением: разница покажет, насколько датчик шумит от раза к разу.
Эксперименты и проверка себя
- 1. Что вернёт readTemperature(true)?
- 2. У DHT11 в Serial стоит 24.30. Откуда десятые?
- 3. Два вызова readTemperature() подряд в одном loop() вернут:
- 4. Как округлить 24.6 до 25 так, чтобы минусовые температуры тоже округлялись правильно?
- 5. Какая формула переводит Цельсий в Фаренгейт?
- Обе функции чтения возвращают float: readTemperature() — Цельсий, readHumidity() — проценты.
- readTemperature(true) отдаёт Фаренгейт; пересчёт руками: F = C × 9.0 / 5.0 + 32.0, точки в числах обязательны.
- DHT11 не умеет десятые доли — его разрешение один целый градус; честные десятые даёт только DHT22.
- Библиотека кэширует чтение: чаще раза в две секунды значения повторяются. Читайте по таймеру в две переменные.
- Округление с минусом — только round(): трюк int(t + 0.5) работает лишь выше нуля.
Числа в Serial — это полдела; дальше их нужно куда-то девать. Если вместо числа иногда приходит nan — открывайте следующий урок про диагностику nan: там чек-лист от питания до таймингов. Если числа идут ровно, а вы хотите копить их в файл, который потом откроется таблицей, — пригодится урок про журнал данных на SD-карте. А для визуальной проверки единиц в реальном времени держите под рукой Serial Plotter: три пары из этого урока ложатся на график без единой правки. И если проект поедет на дачу без интернета, вспомните макрос F для строк — он бережёт RAM, которой в журнале всегда мало.
Частые вопросы по теме урока
- Что вернёт readTemperature(true)?
- Необязательный аргумент true переключает шкалу на Фаренгейт. Внутри это тот же пересчёт: F = C × 9/5 + 32. Градусы Фаренгейта
- У DHT11 в Serial стоит 24.30. Откуда десятые?
- Протокол DHT11 передаёт температуру целым байтом. Дробная часть в его показаниях — дописанные библиотекой нули, не измерение. Библиотека дописала нули: у DHT11 разрешение — целый градус
- Два вызова readTemperature() подряд в одном loop() вернут:
- Библиотека кэширует последний успешный обмен и не трогает сенсор чаще, чем раз в две секунды. Второй вызов получает то же число. Одно и то же значение из кэша библиотеки
- Как округлить 24.6 до 25 так, чтобы минусовые температуры тоже округлялись правильно?
- int отбрасывает дробь, трюк с +0.5 врёт ниже нуля. round() округляет по правилам и для отрицательных значений. round(t)
- Какая формула переводит Цельсий в Фаренгейт?
- Умножаем на девять, делим на пять, прибавляем тридцать два. В коде пишите 9.0 и 5.0 с точками, чтобы не потерять дробь. F = C × 9 / 5 + 32