Уличный термометр на балконе я собрал за час и сдал в эксплуатацию, а ночью поймал позор: за окном минус семь, на индикаторе горело 249. Полчаса я подозревал датчик, потом провода, потом сам индикатор — пока не открыл тип переменной. Температура у меня лежала в byte, а байт минус не умеет: минус семь в нём обернулось числом 249, и дисплей честно показал то, что в него положили. Датчик был ни при чём с самого начала: он выдал минус семь, как умеет, а потерялась она на пути к экрану — ровно то место, которое я в тот вечер не проверял. Второй сюрприз ждал утром: после включения из розетки первые секунды на экране висел мусор — датчик ещё не отвечал, а скетч печатал то, что вернуло первое чтение.
В этом уроке соберём термометр правильно: чтение DHT по millis-интервалу в две секунды, проверка каждого чтения через isnan, вывод с минусом через showNumberDec и знак градуса через сырые байты. Про сам датчик, его три провода и подтяжку — в уроке про подключение DHT11, первый скетч с библиотекой DHT — в разборе первого чтения; здесь — связка датчика с индикатором и все места, где связка ломается.
Чтение DHT: две секунды и первый NAN
DHT не любит частых вопросов: DHT11 выдаёт новое значение не чаще раза в секунду, DHT22 — раза в две; спрашивать чаще — получать повтор старого показания. Отсюда интервал: читаю датчик раз в 2000 мс по условию if (millis() - lastRead >= 2000) — тот же тик, что и у экрана LCD. Сам обмен по проводу короткий, но библиотека Adafruit в примере называет до 250 мс на чтение с учётом всех пауз: при двухсекундном интервале это восьмая часть бюджета, терпимо. Если на плате живут срочные дела — кнопки, сигнализация — держите их вне кадра с чтением и разведите по времени.
Первые чтения после подачи питания часто возвращают NAN: датчику нужно одна-две секунды, чтобы выйти на режим, у меня мусор висел ровно столько после каждого включения. Ловится одной строкой: float t = dht.readTemperature(); if (isnan(t)) — пропускаем вывод, а на индикатор рисуем прочерк из четырёх байтов 0x40. Показывать NAN нельзя вовсе: приведение NAN к int даёт ноль или мусор, и прибор начинает врать молча, с уверенным видом. Если NAN не только на старте, а вечно — это провода, питание или подтяжка, и у меня есть отдельный урок про диагностику NAN у DHT.
Разница датчиков сводится к цифрам: DHT11 — точность плюс-минус два градуса и опрос раз в секунду, DHT22 — плюс-минус полградуса и раз в две, а читается он тем же вызовом readTemperature. Я начинал с DHT11 для комнаты, а на улицу ставлю DHT22: на морозе полградуса погрешности виднее, чем в квартире, хотя каркас скетча у обоих общий до последней строки.
Минус, градус и типы переменных
Минус рисуется сам — если переменная правильная. showNumberDec(-7) выводит знак минус в разряде левее цифр, прижав всё вправо: на экране -7, а не 249. Вся ловушка — в типах, и она стоит ночи позора: температура из readTemperature приходит float, и если сложить её в byte или unsigned int, минус потеряется ещё до экрана — байт превратит минус семь в 249, знаковый int удержит. Моё правило после той ночи: температура живёт в float до последнего момента и режется до int только в строке вывода, а тип byte для любых измерений с возможным минусом я не использую вовсе. Проверьте себя один раз за проект: присвойте переменной минус семь и посмотрите на экран ещё до подключения датчика — такой тест выявил бы мой байт за минуту, а не за полчаса с мультиметром.
С разрядами посчитайте заранее: минус семь занимает два разряда — знак и цифра; минус двенадцать — три; минус сто двадцать пять в окно с градусом уже не влезет. Для бытовой квартиры и улицы за окном хватает диапазона от -40 до +60 — три разряда с минусом, и всё влезает в четыре без десятых. Десятые я сознательно не вывожу: умножение на десять дало бы 235 без точки, потому что на модуле с двоеточием десятичной точки нет, а точность DHT11 — плюс-минус два градуса; знак после запятой тут врёт громче самой погрешности.
Знак градуса рисуется байтом 0x63 через setSegments — таблицу байтов по сегментам я приводил в вводном уроке про индикатор TM1637. Приём комбинирования кадра: число — через showNumberDec(t, false, 3, 1) в правых трёх разрядах, градус — setSegments(gradus, 1, 0) в крайнем левом. Получается 23 со значком — читается мгновенно и не требует пояснений. Букве C места уже нет: четыре разряда кончились, поэтому на TM1637 я ограничиваюсь градусом, а единицу измерения держу на шильдике корпуса. Если буква всё-таки нужна — берите байт 0x39, это латинская C: на паре модулей я так показывал eC для эконом-режима, но чаще значок градуса закрывает вопрос единиц измерения сам.
Следите за хвостами при переключении режимов: после кадра влажности в левом разряде висит градус прошлого кадра, и следующий вывод числа его не тронет — разряды вне окна библиотека не перерисовывает. Мой ход — гасить левый разряд нулевым байтом каждый раз, когда кадр не занимает весь экран: одна строка, а на экране не всплывают призраки прошлых режимов.
| Что выводим | Как | Нюанс |
|---|---|---|
| Целая температура | showNumberDec(t, false) | минус рисуется сам, если температура дожила до вывода в int, а не byte |
| Температура с градусом | showNumberDec(t, false, 3, 1) + setSegments(0x63, 1, 0) | число в правых трёх разрядах, градус в левом |
| Прочерк вместо NAN | setSegments из четырёх 0x40 | честная заставка «датчик не готов» на старте |
| Влажность | showNumberDec(h, false) + гашение левого разряда | после кадра с градусом в левом разряде висит хвост |
Обновление раз в две секунды без delay
Каркас скетча — тот же тик, что и у всех моих приборов: в loop() сравниваем millis() с lastRead, прошло две тысячи миллисекунд — читаем датчик и рисуем кадр. Между тиками loop() свободен: кнопка переключения температуры и влажности опрашивается каждый проход и срабатывает с первого нажатия, а не с десятого. Почему не delay(2000): паузы я выгнал из дисплеев ещё на LCD — обновление по тику таймера — и здесь причины те же, плюс DHT сам ставит потолок в две секунды; отнимать у кнопок ещё и эти секунды расточительно.
Пины держу так: датчик на D2, кнопка режима на D3, дисплей на D4 и D5 — линии не пересекаются, и в симуляторе макетка соберётся ровно в этой конфигурации. Интервал 2000 мс — из паспорта датчика, а не из вкуса: у DS18B20 и BMP280 свои потолки, и я сводил интервалы опроса разных датчиков в отдельном уроке про интервал опроса. Датчик за окном — плюс: тепло от платы сдвигает показания на градус-другой, и выносной кабель длиной до метра для DHT11 живёт со штатной подтяжкой 10 кОм; на больших длинах ставлю 5-10 кОм и проверяю линию на NAN.
Первый кадр у меня стартует не сразу: заставка-прочерк держится до первого успешного чтения, и прибор с включения ведёт себя честно — сначала говорит, что думает, потом показывает температуру. За зиму на трёх сборках не было ни одной претензии от домочадцев, которые раньше каждое утро видели на экране случайные цифры.
// Термометр: DHT на D2, TM1637 на D4/D5, кнопка режима на D3#include <DHT.h>#include <TM1637Display.h> DHT dht(2, DHT11); // DATA на D2TM1637Display disp(4, 5); // CLK на D4, DIO на D5 const int btnPin = 3; // температура / влажностьconst long INTERVAL = 2000; // потолок DHT11, не чаще long lastRead = 0;int rejim = 0; // 0 — температура, 1 — влажностьint prevBtn = HIGH; byte gradus[1] = { 0x63 }; // знак градусаbyte tire[4] = { 0x40, 0x40, 0x40, 0x40 }; // прочерк void setup() { pinMode(btnPin, INPUT_PULLUP); dht.begin(); disp.setBrightness(3); disp.setSegments(tire, 4, 0); // заставка: датчик прогревается Serial.begin(9600);} void loop() { int btn = digitalRead(btnPin); if (btn == LOW && prevBtn == HIGH) { // фронт нажатия rejim = 1 - rejim; delay(40); // антидребезг } prevBtn = btn; if (millis() - lastRead >= INTERVAL) { // тик чтения lastRead = millis(); float t = dht.readTemperature(); float h = dht.readHumidity(); if (isnan(t)) { disp.setSegments(tire, 4, 0); // датчик ещё не готов Serial.println("dht=nan"); } else if (rejim == 0) { int celaya = t; // минус живёт в int disp.showNumberDec(celaya, false, 3, 1); disp.setSegments(gradus, 1, 0); Serial.print("temp="); Serial.println(celaya); } else { int vlag = h; disp.showNumberDec(vlag, false, 3, 1); byte pusto[1] = { 0x00 }; // гасим градус прошлого кадра disp.setSegments(pusto, 1, 0); Serial.print("vlazh="); Serial.println(vlag); } }}- DHT11 спрашивают не чаще раза в 1-2 секунды: интервал 2000 мс по millis() — из паспорта датчика, а не из вкуса
- isnan(t) перед каждым выводом: NAN на старте — норма, на экране ему место только в виде прочерка из 0x40
- Минус рисует showNumberDec сам — если температура дожила до вывода в int; byte превращает минус семь в 249
- Градус — байт 0x63 через setSegments в левый разряд, число — showNumberDec в правые три
- loop() без delay(): кнопка режима опрашивается каждый проход и срабатывает с первого нажатия
- 1. На индикаторе 249, а за окном минус семь. Где промах?
- 2. Почему интервал чтения DHT11 — две секунды, а не сто миллисекунд?
- 3. Каким байтом setSegments рисуют знак градуса?
- 4. Первое чтение после включения вернуло NAN. Что показать на экране?
- 5. Что покажет showNumberDec(-7, false, 3, 1)?
Частые вопросы по теме урока
- На индикаторе 249, а за окном минус семь. Где промах?
- Байт минус не умеет: минус семь в нём обернулось 249. Измерения с возможным минусом держите в float до самого вывода и режьте в int. температура лежала в byte — минус не влез
- Почему интервал чтения DHT11 — две секунды, а не сто миллисекунд?
- DHT11 отвечает не чаще раза в секунду, DHT22 — раза в две. Быстрее читать бессмысленно: повтор старого показания. датчик выдаёт новое значение не чаще 0,5-1 Гц — чаще приходит повтор
- Каким байтом setSegments рисуют знак градуса?
- 0x63 — градус, 0x3F — ноль, 0x40 — минус, 0x06 — единица. Таблица байтов лежит в шапке библиотеки. 0x63
- Первое чтение после включения вернуло NAN. Что показать на экране?
- Прочерк честно говорит, что датчик не готов. Ноль на морозе — враньё, а приведение NAN к int даёт непредсказуемое число. прочерк из четырёх 0x40 и ждать следующего тика
- Что покажет showNumberDec(-7, false, 3, 1)?
- showNumberDec принимает int и рисует знак минус в разряде левее цифр, прижав всё вправо — тип переменной должен быть знаковым. минус и семёрку — знак рисуется сам