Термостат с таймером
Задача
Разработать термостат с таймером для йогуртницы или других применений: термостат держит температуру только на протяжении заданного времени, всё остальное время (режим ожидания) – реле выключено
- Релейное регулирование с гистерезисом
- Бесшумное управление – используем нагрузку постоянного тока и МОСФЕТ модуль
- Рабочий цикл по таймеру (включили-регулируем-выключили)
- Управление энкодером
- Вывод на 7-сегментный дисплей
- Двоеточие мигает, если рабочий цикл запущен
- Звуковая индикация окончания рабочего цикла
Базовые уроки
- Термистор. Основы
- Энкодер. Основы
- Зуммер. Основы
- Как написать скетч
- Генерирование сигналов
- Релейное управление
Компоненты не из набора
- В моём проекте контроллера йогуртницы используется блок питания на 12V и нагревательный кабель
Подключение
- Энкодер: любые цифровые пины
- Зуммер: любой цифровой пин через ограничительный резистор 100 Ом
- Дисплей: любые цифровые пины
- Мосфет модуль: любой цифровой пин
- Термистор: любой аналоговый пин
- Внешнее питание 12V: пин Vin
Библиотеки
- EncButton – энкодер с кнопкой
- GyverTM1637 – дисплей
- GyverNTC – термистор
Программа
Инициализация
Компонентов много, так что для удобства задефайним все пины константами, и только потом подключим все библиотеки и создадим объекты:
// пины #define ENC_A 4 #define ENC_B 3 #define ENC_KEY 2 #define BUZZ_PIN 5 #define DISP_CLK 6 #define DISP_DIO 7 #define MOS_PIN 8 #define NTC_PIN 0 // ================ БИБЛИОТЕКИ ================ // библиотека энкодера #include <EncButton.h> EncButton<EB_TICK, ENC_A, ENC_B, ENC_KEY> enc; // библиотека дисплея #include <GyverTM1637.h> GyverTM1637 disp(DISP_CLK, DISP_DIO); // библиотека термистора #include <GyverNTC.h> GyverNTC ntc(NTC_PIN, 10000, 3950);
Нам понадобится несколько глобальных переменных (для простоты программы), обозначим флаги и переменные режимов:
- Нужен флаг, отвечающий за рабочий цикл: запущен таймер термостата, или нет. Соответственно реле управляется только при поднятом флаге, как и счёт оставшегося времени. Назовём его
state
: 0 – режим ожидания, 1 – “рабочий цикл”, когда идёт обратный отсчёт - Хочу выводить на дисплей оставшееся время, заданную температуру и текущую температуру датчика – имеем три режима отображения, пусть будет переменная
mode
- Понадобится переменная таймера
tmr
. Таймер мы сбросим при запуске рабочего цикла - Нам нужно хранить оставшиеся минуты рабочего цикла (они же – настроенное время работы), целевую температуру и текущее значение с датчика. Назовём всё соответственно:
// ================ ПЕРЕМЕННЫЕ ================ bool state; // статус (0 ожидание, 1 таймер запущен) byte mode; // вывод: 0 часы, 1 градусы, 2 датчик uint32_t tmr; // общий таймер работы int mins = 480; // минуты (по умолч 8 часов) int temp = 40; // целевая температура (по умолч 40 градусов) int sens = 0; // темепратура с датчика
В блоке setup()
сделаем нужные пины выходами, запустим и обновим дисплей:
// ================== SETUP ================== void setup() { // активные пины как выходы pinMode(BUZZ_PIN, OUTPUT); pinMode(MOS_PIN, OUTPUT); // на всякий случай очистим дисплей и установим яркость disp.clear(); disp.brightness(7); updDisp(); // обновить дисплей }
Обновление дисплея
Здесь updDisp()
– функция, которую я создаю для удобства, т.к. она будет вызываться и в других местах программы. Взглянем на неё поближе:
void updDisp() { switch (mode) { case 0: if (!state) { disp.point(1); disp.displayClock(mins / 60, mins % 60); } else { int curMins = mins - (millis() - tmr) / 60000ul; disp.displayClock(curMins / 60, curMins % 60); } break; case 1: if (!state) disp.point(0); disp.displayInt(temp); disp.displayByte(0, _t); break; case 2: if (!state) disp.point(0); disp.displayInt(sens); disp.displayByte(0, _S); break; } }
Вывод на дисплей зависит от текущего режима отображения mode
:
- При 0 мы выводим время при помощи
displayClock(часы, минуты)
. Я не просто так храню время в минутах, так удобнее с ним работать: проверять таймер и менять значение энкодером. Из общих минут можно получить часы, просто разделив на 60. При целочисленном делении дробная часть отсекается, то есть округление идёт вниз и мы всегда получим целые часы. Для получения минут – берём остаток от деления на 60, и всё! Что касается строчкиif (!state) disp.point(1);
– я хочу чтобы двоеточие на дисплее горело постоянно, если рабочий цикл не запущен. Поэтому принудительно включаем. В ждущем режиме мы просто выводим настроенные минутыmins
, а во время работы таймера – сколько времени осталось до конца. Это можно реализовать разными способами, я выбрал самый простой (это всё-таки урок, а не погоня за оптимальным кодом): считаем оставшиеся минуты, вычитая прошедшее с момента запуска таймера время из заданных минут. И заданные минуты мы всё ещё сможем менять, то есть заставить таймер сработать чуть раньше или позже – пусть это будет фишкой - При 1 выводим значение установленной температуры
temp
, а также выводим букву t в левое знакоместо дисплея. Выключаем двоеточие, если находимся в режиме ожидания - При 2 выводим значение с датчика
sens
, а также букву S
Main loop
Главный цикл программы выглядит вот так, в нём несколько функций, мы их рассмотрим ниже:
// =================== LOOP =================== void loop() { controlTick(); // опрос энкодера sensorTick(); // опрос датчика и работа реле if (state) timerTick(); // опрос таймеров (во время работы) }
Управление
Управление энкодером организуем следующим образом:
- Поворот энкодера: изменение текущего значения на дисплее: градусы на 1, время – на 10 минут. При любом повороте нам нужно обновить дисплей
- Удержание кнопки: запуск рабочего цикла:
- Поднимаем флаг рабочего режима
state = 1
- А также сбрасываем таймер обратного отсчёта
- Поднимаем флаг рабочего режима
- Клик по кнопке: переключение режимов отображения (фактически закольцованное изменение переменной
mode
от 0 до 2). Также обновляем дисплей
Реализация:
void controlTick() { enc.tick(); // опрос энкодера if (enc.turn()) { // если был поворот (любой) // при повороте меняем значения согласно режиму вывода if (enc.right()) { // вправо if (mode == 1) temp++; else if (mode == 0) mins += 10; } if (enc.left()) { // влево if (mode == 1) temp--; else if (mode == 0) mins -= 10; if (mins < 0) mins = 0; } updDisp(); // обновляем дисплей в любом случае } if (enc.held()) { // кнопка энкодера удержана if (!state) { // если таймер не запущен state = 1; // запускаем tmr = millis(); // и запоминаем время запуска } } if (enc.click()) { // клик по кнопке mode++; // следующий режим вывода if (mode >= 3) mode = 0; // закольцуем 0,1,2 updDisp(); // обновить дисплей } }
Регулирование
Здесь всё просто: взводим таймер на 1 секунду, по нему опрашиваем термистор. Мы задаём температуру в целых числах, поэтому для более корректной работы лучше математически округлить значение перед присваиванием к переменной temp
.
Если рабочий цикл запущен – идёт релейное регулирование с гистерезисом, оно разбиралось в отдельном уроке. Таким образом сигнал на реле рассчитывается раз в секунду и имеет дополнительную защиту от частых переключений в виде гистерезиса
void sensorTick() { static uint32_t tmrSens; // каждую секунду if (millis() - tmrSens >= 1000) { tmrSens = millis(); // получаем значение с датчика и округляем sens = round(ntc.getTempAverage()); // если главный таймер запущен - релейное регулирование // с гистерезисом (см. урок) if (state) { static bool relayState = 0; if (sens < temp - REL_HSTR) relayState = 1; else if (sens >= temp) relayState = 0; digitalWrite(MOS_PIN, relayState); } } }
Таймеры и индикация
Два раза в секунду хотим мигать двоеточием, а также обновлять дисплей (там ведь тикает время и меняются показания датчика). “Мигать” очень просто – заводим себе переменную-флажок, и каждый вызов просто его инвертируем и передаём в функцию управления двоеточием
static uint32_t tmrSec; // таймер 2 раза в секунду if (millis() - tmrSec >= 500) { tmrSec = millis(); // переключаем переменную 0,1,0,1... для мигания точками static bool dotsFlag = 0; dotsFlag = !dotsFlag; disp.point(dotsFlag, false); // обновляем точки updDisp(); // обновляем дисп // ждём после вывода данных, особенность дисплея }
Далее проверяем таймер рабочего цикла, не вышло ли время. Если вышло – пищим пять раз при помощи функции tone()
, а затем переходим в ждущий режим, обнулив state
. Всё!
// рабочий таймер,с момента запуска до "минут" if (millis() - tmr >= mins * 60000ul) { // время вышло! Суши вёсла digitalWrite(MOS_PIN, 0); // пропищать 5 раз for (int i = 0; i < 5; i++) { tone(BUZZ_PIN, 1000); // на частоте 1000 Гц delay(400); noTone(BUZZ_PIN); delay(400); } state = 0; // уйти в ждущий режим }
Полный код программы
Возможные доработки
- Использовать более точный датчик температуры ds18b20
- Использовать более хитрый алгоритм релейного управления
- Использовать реле вместо мосфет-модуля для контроля сетевого нагревательного элемента
- Использовать ПИД регулятор, так как мосфет-модуль и ШИМ сигнал позволяют это сделать