Skip to content Skip 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
  • Использовать более хитрый алгоритм релейного управления
  • Использовать реле вместо мосфет-модуля для контроля сетевого нагревательного элемента
  • Использовать ПИД регулятор, так как мосфет-модуль и ШИМ сигнал позволяют это сделать

Видео


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

Похожие примеры
Подписаться
Уведомить о
14 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии