/*
Многофункциональный PID-контроллер температуры
Версия для эмулятора Wokwi с LCD 20x4 и 3 энкодерами
*/
// Подключение необходимых библиотек
#include <Arduino.h>
#include <Wire.h> // Для работы с I2C
#include <LiquidCrystal_I2C.h> // Библиотека LCD дисплея
#include <EncButton.h> // Библиотека для работы с энкодерами
#include <GyverPID.h> // PID-регулятор
#include <EEPROM.h> // Для сохранения настроек
// Настройки эмуляции (раскомментировать для Wokwi)
#define SIMULATION
// #define DEBUG_SERIAL // Отладочный вывод в Serial
// =============================================
// КОНФИГУРАЦИЯ ОБОРУДОВАНИЯ
// =============================================
// Пины управления нагревателями
#ifdef SIMULATION
#define HEATER1_PIN 5 // PWM-пины в Wokwi
#define HEATER2_PIN 6
#define HEATER3_PIN 7
#else
#define HEATER1_PIN 2 // Пины для реального ESP32
#define HEATER2_PIN 25
#define HEATER3_PIN 32
#endif
// Конфигурация энкодеров (CLK, DT, SW)
EncButton enc1(26, 27, 13); // Энкодер 1
EncButton enc2(14, 12, 15); // Энкодер 2
EncButton enc3(33, 34, 35); // Энкодер 3
// Параметры PID-регуляторов
GyverPID pid1(2.0, 0.5, 0.1); // Канал 1 (Kp, Ki, Kd)
GyverPID pid2(2.0, 0.5, 0.1); // Канал 2
GyverPID pid3(2.0, 0.5, 0.1); // Канал 3
// Конфигурация LCD дисплея
LiquidCrystal_I2C lcd(0x27, 20, 4); // Адрес 0x27, 20 символов, 4 строки
// =============================================
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ И НАСТРОЙКИ
// =============================================
// Температурные данные
double currentTemps[3] = {0}; // Текущие температуры
double targetTemps[3] = {25.0, 25.0, 25.0}; // Уставки
double pwmOutputs[3] = {0}; // Выходные значения PWM
// Системные переменные
uint32_t lastUpdateTime = 0; // Таймер обновления системы
bool systemActive = true; // Флаг активности системы
// Настройки энкодеров
#define ENC_FAST_START 5 // Количество шагов для активации ускорения
#define ENC_FAST_STEP 5 // Множитель скорости при ускорении
// Работа с EEPROM
#define EEPROM_SIZE 12 // Размер хранилища (3 значения double)
unsigned long lastEncoderActivity = 0; // Время последнего изменения
bool settingsNeedSave = false; // Флаг необходимости сохранения
// Бегущая строка
String statusMessage = ""; // Текст сообщения
int scrollPosition = 0; // Текущая позиция прокрутки
unsigned long lastScrollTime = 0; // Таймер прокрутки
const unsigned SCROLL_DELAY = 300; // Интервал прокрутки (мс)
// Пользовательский символ стрелки
byte arrowRight[8] = {
B00000, B00100, B00010, B11111,
B00010, B00100, B00000, B00000
};
// =============================================
// ПРОТОТИПЫ ФУНКЦИЙ
// =============================================
void initializeHardware(); // Инициализация оборудования
void handleEncoders(); // Обработка энкодеров
void processEncoder(EncButton &enc, uint8_t channel); // Обработка одного энкодера
void readTemperatureSensors(); // Чтение температурных датчиков
void computePID(); // Вычисление PID
void updateHeaters(); // Управление нагревателями
void safetyShutdown(); // Аварийное отключение
void refreshDisplay(); // Обновление дисплея
void updateStatusLine(); // Обновление строки состояния
void serialOutput(); // Вывод данных в Serial
void saveConfiguration(); // Сохранение настроек
void loadConfiguration(); // Загрузка настроек
// =============================================
// НАСТРОЙКА СИСТЕМЫ
// =============================================
void setup() {
#ifdef DEBUG_SERIAL
Serial.begin(115200); // Инициализация Serial
#endif
// Инициализация EEPROM
EEPROM.begin(EEPROM_SIZE);
loadConfiguration();
// Настройка выходов PWM
pinMode(HEATER1_PIN, OUTPUT);
pinMode(HEATER2_PIN, OUTPUT);
pinMode(HEATER3_PIN, OUTPUT);
analogWrite(HEATER1_PIN, 0);
analogWrite(HEATER2_PIN, 0);
analogWrite(HEATER3_PIN, 0);
// Инициализация LCD
Wire.begin(21, 22); // Настройка I2C (SDA, SCL)
lcd.init(); // Инициализация дисплея
lcd.backlight(); // Включение подсветки
lcd.createChar(0, arrowRight); // Регистрация символа
initializeHardware();
// Конфигурация PID-регуляторов
pid1.setDirection(NORMAL); // Направление регулирования
pid1.setLimits(0, 255); // Диапазон выходных значений
pid2.setDirection(NORMAL);
pid2.setLimits(0, 255);
pid3.setDirection(NORMAL);
pid3.setLimits(0, 255);
#ifdef DEBUG_SERIAL
Serial.println("Система инициализирована");
#endif
}
// =============================================
// ОСНОВНОЙ ЦИКЛ РАБОТЫ
// =============================================
void loop() {
// Опрос энкодеров
enc1.tick();
enc2.tick();
enc3.tick();
handleEncoders();
// Автоматическое сохранение настроек
if(settingsNeedSave && (millis() - lastEncoderActivity >= 2000)) {
saveConfiguration();
settingsNeedSave = false;
}
// Основной цикл обновления (каждые 100 мс)
if(millis() - lastUpdateTime >= 100) {
lastUpdateTime = millis();
readTemperatureSensors(); // Получение данных с датчиков
if(systemActive) {
computePID(); // Вычисление регулирования
updateHeaters(); // Обновление выходов PWM
} else {
safetyShutdown(); // Аварийное отключение
}
refreshDisplay(); // Обновление интерфейса
#ifdef DEBUG_SERIAL
serialOutput(); // Отладочный вывод
#endif
}
}
// =============================================
// РЕАЛИЗАЦИЯ ФУНКЦИЙ
// =============================================
/**
* Инициализация дисплея
*/
void initializeHardware() {
lcd.clear();
// Заголовки для каналов
for(int i = 0; i < 3; i++) {
lcd.setCursor(0, i);
lcd.print("T");
lcd.print(i+1);
lcd.print(": . ");
lcd.write(0); // Вывод символа стрелки
lcd.print("SP . OFF");
}
}
/**
* Обработка всех энкодеров
*/
void handleEncoders() {
processEncoder(enc1, 0);
processEncoder(enc2, 1);
processEncoder(enc3, 2);
}
/**
* Обработка одного энкодера
* @param enc - объект энкодера
* @param channel - номер канала (0-2)
*/
void processEncoder(EncButton &enc, uint8_t channel) {
// Обработка вращения
if(enc.turn()) {
int step = 1;
if(enc.fast()) { // Ускоренный режим
step = ENC_FAST_STEP;
if(enc.getClicks() > ENC_FAST_START) step *= 2;
}
// Изменение уставки
if(enc.left()) {
targetTemps[channel] -= 0.5 * step;
} else {
targetTemps[channel] += 0.5 * step;
}
// Ограничение диапазона
targetTemps[channel] = constrain(targetTemps[channel], 0, 300);
settingsNeedSave = true;
lastEncoderActivity = millis();
}
// Сброс уставки по клику
if(enc.click()) {
targetTemps[channel] = 25.0;
settingsNeedSave = true;
lastEncoderActivity = millis();
}
// Переключение системы по удержанию
if(enc.hold()) {
systemActive = !systemActive;
statusMessage = systemActive ?
" *** Система активна *** " :
" *** Система остановлена *** ";
scrollPosition = 0;
}
}
/**
* Чтение температурных датчиков (эмуляция)
*/
void readTemperatureSensors() {
#ifdef SIMULATION
// Генерация тестовых значений
for(int i = 0; i < 3; i++) {
currentTemps[i] = targetTemps[i] + random(-20, 20)/10.0;
currentTemps[i] = constrain(currentTemps[i], 0, 300);
}
#else
// Реальный код для работы с датчиками
#endif
}
/**
* Вычисление PID
*/
void computePID() {
pid1.setpoint = targetTemps[0];
pid1.input = currentTemps[0];
pwmOutputs[0] = pid1.getResult();
pid2.setpoint = targetTemps[1];
pid2.input = currentTemps[1];
pwmOutputs[1] = pid2.getResult();
pid3.setpoint = targetTemps[2];
pid3.input = currentTemps[2];
pwmOutputs[2] = pid3.getResult();
}
/**
* Управление нагревателями
*/
void updateHeaters() {
analogWrite(HEATER1_PIN, (int)pwmOutputs[0]);
analogWrite(HEATER2_PIN, (int)pwmOutputs[1]);
analogWrite(HEATER3_PIN, (int)pwmOutputs[2]);
}
/**
* Аварийное отключение
*/
void safetyShutdown() {
analogWrite(HEATER1_PIN, 0);
analogWrite(HEATER2_PIN, 0);
analogWrite(HEATER3_PIN, 0);
}
/**
* Обновление дисплея
*/
void refreshDisplay() {
// Обновление данных каналов
for(int i = 0; i < 3; i++) {
lcd.setCursor(0, i);
lcd.printf("T%d:%5.1f", i+1, currentTemps[i]);
lcd.write(0);
lcd.printf("SP%5.1f ", targetTemps[i]);
lcd.setCursor(17, i);
lcd.print(pwmOutputs[i] > 10 ? "ON " : "OFF");
}
// Обновление строки состояния
updateStatusLine();
}
/**
* Обновление бегущей строки
*/
void updateStatusLine() {
if(millis() - lastScrollTime >= SCROLL_DELAY) {
lastScrollTime = millis();
scrollPosition = (scrollPosition + 1) % statusMessage.length();
}
String displayText;
for(int i = 0; i < 20; i++) {
int idx = (scrollPosition + i) % statusMessage.length();
displayText += statusMessage.charAt(idx);
}
lcd.setCursor(0, 3);
lcd.print(displayText);
}
/**
* Вывод данных в Serial
*/
void serialOutput() {
static uint32_t timer;
if(millis() - timer >= 500) {
timer = millis();
Serial.println("\n=== Статус системы ===");
for(int i = 0; i < 3; i++) {
Serial.printf("Канал %d | Температура: %-6.1f | Уставка: %-6.1f | PWM: %3d%%\n",
i+1, currentTemps[i], targetTemps[i], (int)(pwmOutputs[i]/255*100));
}
}
}
/**
* Сохранение настроек в EEPROM
*/
void saveConfiguration() {
EEPROM.put(0, targetTemps);
EEPROM.commit();
#ifdef DEBUG_SERIAL
Serial.println("Настройки сохранены");
#endif
}
/**
* Загрузка настроек из EEPROM
*/
void loadConfiguration() {
EEPROM.get(0, targetTemps);
// Проверка на корректность данных
if(targetTemps[0] < 0 || targetTemps[0] > 300) {
for(auto &temp : targetTemps) temp = 25.0;
}
}