Термостат с таймером
Задача
Разработать термостат с таймером для йогуртницы или других применений: термостат держит температуру только на протяжении заданного времени, всё остальное время (режим ожидания) — реле выключено
- Релейное регулирование с гистерезисом
- Бесшумное управление — используем нагрузку постоянного тока и МОСФЕТ модуль
- Рабочий цикл по таймеру (включили-регулируем-выключили)
- Управление энкодером
- Вывод на 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
- Использовать более хитрый алгоритм релейного управления
- Использовать реле вместо мосфет-модуля для контроля сетевого нагревательного элемента
- Использовать ПИД регулятор, так как мосфет-модуль и ШИМ сигнал позволяют это сделать
