Skip to contentSkip to main navigation Skip to footer

Термостат с таймером

Задача


Разработать термостат с таймером для йогуртницы или других применений: термостат держит температуру только на протяжении заданного времени, всё остальное время (режим ожидания) – реле выключено

  • Релейное регулирование с гистерезисом
  • Бесшумное управление – используем нагрузку постоянного тока и МОСФЕТ модуль
  • Рабочий цикл по таймеру (включили-регулируем-выключили)
  • Управление энкодером
  • Вывод на 7-сегментный дисплей
    • Двоеточие мигает, если рабочий цикл запущен
  • Звуковая индикация окончания рабочего цикла

Базовые уроки


Компоненты не из набора


  • В моём проекте контроллера йогуртницы используется блок питания на 12V и нагревательный кабель

Подключение


  • Энкодер: любые цифровые пины
  • Зуммер: любой цифровой пин через ограничительный резистор 100 Ом
  • Дисплей: любые цифровые пины
  • Мосфет модуль: любой цифровой пин
  • Термистор: любой аналоговый пин
  • Внешнее питание 12V: пин Vin

Библиотеки


Программа


Инициализация

Компонентов много, так что для удобства задефайним все пины константами, и только потом подключим все библиотеки и создадим объекты:

// пины
#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;    // уйти в ждущий режим
}

Полный код программы

Полный код
// ================== НАСТРОЙКИ ==================
#define REL_HSTR 2  // ширина гистерезиса, градусов

// пины
#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);

// ================ ПЕРЕМЕННЫЕ ================
bool state;     // статус (0 ожидание, 1 таймер запущен)
byte mode;      // вывод: 0 часы, 1 градусы, 2 датчик
uint32_t tmr;   // общий таймер работы
int mins = 480; // минуты (по умолч 8 часов)
int temp = 40;  // целевая температура (по умолч 40 градусов)
int sens = 0;   // темепратура с датчика

// ================== SETUP ==================
void setup() {
  // активные пины как выходы
  pinMode(BUZZ_PIN, OUTPUT);
  pinMode(MOS_PIN, OUTPUT);

  // на всякий случай очистим дисплей и установим яркость
  disp.clear();
  disp.brightness(7);

  updDisp();  // обновить дисплей
}

// =================== LOOP ===================
void loop() {
  controlTick();  // опрос энкодера
  sensorTick();   // опрос датчика и работа реле
  if (state) timerTick();   // опрос таймеров (во время работы)
}

// ================== ФУНКЦИИ ==================
// опрос управления
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();                // обновить дисплей
  }
}

// всякие таймеры
void timerTick() {
  // переменные таймеров
  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();  // обновляем дисп
    // ждём после вывода данных, особенность дисплея
  }

  // рабочий таймер,с момента запуска до "минут"
  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;    // уйти в ждущий режим
  }
}

// опрос датчика, регулирование
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);
    }
  }
}

// обновить дисплей в зависимости от текущего режима отображения
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;
  }
}

 

Возможные доработки


  • Использовать более точный датчик температуры ds18b20
  • Использовать более хитрый алгоритм релейного управления
  • Использовать реле вместо мосфет-модуля для контроля сетевого нагревательного элемента
  • Использовать ПИД регулятор, так как мосфет-модуль и ШИМ сигнал позволяют это сделать

Видео


Полезный пример?

Похожие примеры
13 Комментариев
  • Привет!
    Как вывести показания с термистора на lcd дисплей 1602А? никакой информации нет…

  • Мне б для автоклава подобное устройство, но там и управление похитрее. Домашние консервы такие же вкусные как домашние йогурты, только из рыбы)))

  • Привет. Никак не могу понять, как остановить таймер по удержанию кнопки энкодера…

  • Пытаюсь подключить вместо термистора датчик ds18b20. После подключения библиотеки MicroDS18B20 вместо GyverNTC компилятор пишет: “error: could not convert template argument ‘0’ from ‘int’ to ‘uint8_t* {aka unsigned char*}’
    MicroDS18B20 sensor1;
    ^
    exit status 1
    could not convert template argument ‘0’ from ‘int’ to ‘uint8_t* {aka unsigned char*}’

  • Алекс,добрый день!При компиляции вылазит ошибка.no matching function for call to ‘GyverTM1637::point(bool&, bool)’ и подсвечивается строка в коде isp.point(dotsFlag, false); // обновляем точки библиотеки вроди бы все установлены,подскажи пжлста в чем проблема ?

  • доброе…
    Что нам требуется от нашего “Nano”?,
    1) чтобы выдавало 2 вывода в 1 килогерц последовательно и чтобы можно по влиять на скважности обеих выводов процента за счёт одной “стр627” (знаю что если с потенциометрами это возможно то и с оптопарои это должно быть еще проще ,,,),
    И вот таких комплекта операций должны быть 2 и двумя термометрами и какой не будь Пин на вентилятор(+50 Цельсия чтобы включался,,,) ,,,
    Для вас это просто ( а тут в селе это очень бы пригодилось, наконец то хоти какое то толк будет с ваших игрушек,,,),,,

    • Добрый день.
      Вы попробуйте хоть начать что то делать, и я уверен что у Вас начнет получатся.

  • Добрый день. Спасибо за Ваш труд.
    Переписал на датчик DS18B20, Вы правы, и показания точне, и легче код стал ( хотя это и было не критично). Хочу дописать что бы и гистерезис можно было выставлять с энкодера (если мозгов хватит), иногда очень даже не помешает. Еще раз спасибо.

  • Добрый день.
    при включении на дисплее загорается 8:00, энкодером могу прибавлять и убавлять 10 минут, при нажатии режимы не меняются, как только нажимаешь включается нагрев, когда должен включаться при длительном нажатии. после включения дисплей никак не реагирует на нажатия энкодера.
    плата Attiny88.
    буду благодарен за совет.

    • Могу предположить что с камнем на котором Вы делаете, некоректно могут работать библиотеки. Но это чисто моё предположение.

  • Дописал что бы была возможность изменять гистерезис, и добавил сохранение пороговой температуры и гестырезиса в память. Там стало удобнее, не нужно вносить каждый раз с нуля значения. Вы классно даете материал, просто супер, даже такой валенок как я разобрался как это сделать))))Спасибо Вам))

Оставить комментарий

Ваш адрес email не будет опубликован.