//регулировка температуры от термодатчика Pt100 с помощью PID-регулятора //настройка параметров //эти настройки можно записать в энергонезависимую память //на дисплей выводятся следующие символы : // ! - введена поправка температуры //А - сработала сигнализация // (отображается даже если сигнализация на внешнее реле отключена) //Внимание! На клапане, которым управляет этот регулятор, //должны стоять конечные выключатели. #include <EEPROM.h> //библиотека работы с памятью #include <LiquidCrystal_I2C.h> //библиотеки работы с LCD через I2C #include <Wire.h> //Не забудь шунтировать выход от кнопок резистором 10 КОм const int ValveUpPin = 5; //пин реле на открытие клапана const int ValveDownPin = 6; //пин реле на закрытие клапана const int BtSettingPin = 7; //пин кнопки настроек. const int BtDownPin = 8; //пин кнопки Уменьшить const int BtUpPin = 9; //пин кнопки Увеличить const int BtExitPin = 10; //пин кнопки Выход const int BtManualPin = 11; //пин кнопки Ручной режим const int AlarmPin = 12; //пин реле сигнализации const byte Pt100Pin = A0; //аналоговый пин термодатчика Pt100 //пины модуля I2C LCD-дисплея : //SDA - A4 //SCL - A5 //питание от шилда int Init_Eeprom_Addr = 1000; //номер ячейки в памяти EEPROM (0-1023), куда мы запишем ключ о //самом первом запуске программы //это нужно, так как при первом запуске в памяти нет наших настроек //и нужно загрузить значения по умолчанию byte Init_Key = 228; //это ключ (0-255). //Если при пуске скетча данные из ячейки Init_Eeprom не совпадают //с Init_Key, вводим в настройки значения по умолчанию, //иначе берем настройки из памяти boolean LastButtonSetting = LOW; //предыдущее состояние кнопки boolean CurrentButtonSetting = LOW; //текущее состояние кнопки boolean LastButtonDown = LOW; boolean CurrentButtonDown = LOW; boolean LastButtonUp = LOW; boolean CurrentButtonUp = LOW; boolean LastButtonExit = LOW; boolean CurrentButtonExit = LOW; boolean LastButtonManual = LOW; boolean CurrentButtonManual = LOW; boolean FlagSettingLoop = true; //были ли мы в цикле нажатия кнопки Настройки boolean FlagDownLoop = true; //были ли мы в цикле нажатия кнопки Меньше boolean FlagUpLoop = true; //были ли мы в цикле нажатия кнопки Больше boolean FlagExitLoop = true; //были ли мы в цикле нажатия кнопки Exit boolean FlagManualLoop = true; //были ли мы в цикле нажатия кнопки Ручной Режим //нужна при нажатии и удерживании кнопки ВВЕРХ unsigned long ButtonHoldTimerUp = 0; //нужна при нажатии и удерживании кнопки ВНИЗ unsigned long ButtonHoldTimerDown = 0; //по прошествии этого времени кнопка ВВЕРХ или ВНИЗ //перейдет в режим удержания, мс const long ButtonHoldIncTime = 500; boolean FlagUpHold = true; //были ли мы в цикле удержания кнопки ВВЕРХ boolean FlagDownHold = true; //были ли мы в цикле удержания кнопки ВНИЗ float Pt100_Val = 0; //значение с термодатчика Pt100 (0...1023) //значение с термодатчика Pt100, преобразованное в температуру float Pt100_Temp = 0; float Pt100_Temp_Correction = 0; //температура в град с поправкой //данные для интерполяции датчика (значения по умолчанию) //нижняя граница вольтажа с аналогового порта переведенный в цифровой вид float Min_Volt = 39; //верхняя граница вольтажа с аналогового порта переведенный в цифровой вид float Max_Volt = 546; //соответствующая нижней границе температура float Min_Temp = 0; //соответствующая верхней границе температура float Max_Temp = 100; unsigned long currentTime = 0; //нужна, чтобы убрать delay() int INTERVAL_TIME = 2000; //интервал опроса датчика в мс const int INTERVAL_TIME_MAX = 10000; //верхний порог const int INTERVAL_TIME_MIN = 100; //нижний порог //интервал опроса датчика в сек, считается ниже автоматически //float intervalTime_sec = 1; //нужна для отсчета продолжительности открытия клапана unsigned long currentTime_Up = 0; //нужна для отсчета продолжительности закрытия клапана unsigned long currentTime_Down = 0; int VALVE_TIME = 43; //время открытия клапана в сек. от 0 до полного //замеряем секундомером или берем из мануала const int VALVE_TIME_MAX = 6000; //верхний порог const int VALVE_TIME_MIN = 1; //нижний порог int TOTAL_SPAN = 55; //диапазон регулирования (total span) в град., //т.е. максимальное изменение температуры от закрытого клапана до //полностью открытого. Замеряем или задаем на глаз. const int TOTAL_SPAN_MAX = 1000; //верхний порог const int TOTAL_SPAN_MIN = 1; //нижний порог int P_FACTOR = 100; //относительный диапазон (proportional band в %) const int P_FACTOR_MAX = 10000; //верхний порог const int P_FACTOR_MIN = 0; //нижний порог int I_FACTOR = 70; //время интегрирования (integral time) в сек. const int I_FACTOR_MAX = 10000; //верхний порог const int I_FACTOR_MIN = 1; //нижний порог int D_FACTOR = 15; //время дифференцирования (derivative time) в сек. //(обычно D_FACTOR=I_FACTOR/4...5) const int D_FACTOR_MAX = 10000; //верхний порог const int D_FACTOR_MIN = 1; //нижний порог float P_part = 0; //пропорциональная составляющая выходного сигнала в % float I_part = 0; //интегральная составляющая выходного сигнала в % float D_part = 0; //дифференциальная составляющая выходного сигнала в % float PID_output = 0; //выход с регулятора в % от полного времени //открытия клапана VALVE_TIME float PID_output_old = 0; //выход с регулятора старый float PID_error = 0; //ошибка регулятора (PV-SV) в % от диапазона //регулирования (TOTAL_SPAN) float PID_error_old = 0; //предыдущая ошибка регулятора int PID_SETPOINT = 45; //заданная температура в град. const int PID_SETPOINT_MAX = 1000; //верхний порог const int PID_SETPOINT_MIN = 0; //нижний порог int ALARM_MIN=20; //сигнализация по низкой температуре const int ALARM_MIN_MAX = 1000; //верхний порог const int ALARM_MIN_MIN = 1; //нижний порог int ALARM_MAX=60; //сигнализация по высокой температуре const int ALARM_MAX_MAX = 1000; //верхний порог const int ALARM_MAX_MIN = 1; //нижний порог int ALARM_DIFF=1; //диффиренциал сигнализации const int ALARM_DIFF_MAX = 100; //верхний порог const int ALARM_DIFF_MIN = 0; //нижний порог int TEMP_CORRECT=0; //поправка температуры датчика. const int TEMP_CORRECT_MAX = 100; //верхний порог const int TEMP_CORRECT_MIN = -100; //нижний порог boolean AlarmControlOn = true; //включена или нет сигнализация по температуре boolean ManualModeOn = false; //включен или нет ручной режим boolean FlagSettingIn = false; //находимся ли в настройках int SettingNumber = 0; //пункт меню настройки //счетчик циклов ожидания запуска программы. //Нужно при запуске для стабилизации температуры и выхода пока идет усреднение int Start_wait = 1; //индикатор движения клапана на открытие //если разница между старым и новым выходом больше 1 сек. boolean flag_valve_up = false; //индикатор движения клапана на закрытие //если разница между старым и новым выходом больше -1 сек. boolean flag_valve_down = false; float time_valve_up = 0; //время открытия клапана в мс. float time_valve_down = 0; //время закрытия клапана в мс. //номер информации для вывода во второй строке по кнопке Exit int LCD_second_string = 1; LiquidCrystal_I2C lcd(0x27,16,2); //Задаем адрес и размерность дисплея. byte degree[8] = { //рисуем пользовательский символ градуса B00110, B01001, B01001, B00110, B00000, B00000, B00000, B00000, }; byte arrow_up[8] = { //рисуем пользовательский символ стрелки вверх B00100, B01110, B10101, B00100, B00100, B00100, B00100, B00100, }; byte arrow_down[8] = { //рисуем пользовательский символ стрелки вниз B00100, B00100, B00100, B00100, B00100, B10101, B01110, B00100, }; void setup() //************************************************ { analogReference(INTERNAL); //устанавливаем внутреннее опорное напряжение 1,1 В pinMode (Pt100Pin, INPUT); //устанавливаем режимы входов платы (ввод или вывод) pinMode (AlarmPin, OUTPUT); pinMode (ValveUpPin, OUTPUT); pinMode (ValveDownPin, OUTPUT); pinMode (BtSettingPin, INPUT); pinMode (BtDownPin, INPUT); pinMode (BtUpPin, INPUT); pinMode (BtExitPin, INPUT); pinMode (BtManualPin, INPUT); //если запись в настройки когда-то была, то читаем их if (EEPROM.read(Init_Eeprom_Addr) == Init_Key) { EEPROM.get(0,TEMP_CORRECT); //для типа int нужны 2 ячейки памяти EEPROM.get(2,ALARM_MAX); EEPROM.get(4,ALARM_MIN); EEPROM.get(6,ALARM_DIFF); EEPROM.get(8,INTERVAL_TIME); EEPROM.get(10,VALVE_TIME); EEPROM.get(12,TOTAL_SPAN); EEPROM.get(14,P_FACTOR); EEPROM.get(16,I_FACTOR); EEPROM.get(18,D_FACTOR); EEPROM.get(20,PID_SETPOINT); EEPROM.get(24,Min_Volt); //для типа float нужны 4 ячейки памяти EEPROM.get(28,Min_Temp); EEPROM.get(32,Max_Volt); EEPROM.get(36,Max_Temp); } else //если записи не было, записываем в память значения по умолчанию { EEPROM.put(0,TEMP_CORRECT); //для типа int нужны 2 ячейки памяти EEPROM.put(2,ALARM_MAX); EEPROM.put(4,ALARM_MIN); EEPROM.put(6,ALARM_DIFF); EEPROM.put(8,INTERVAL_TIME); EEPROM.put(10,VALVE_TIME); EEPROM.put(12,TOTAL_SPAN); EEPROM.put(14,P_FACTOR); EEPROM.put(16,I_FACTOR); EEPROM.put(18,D_FACTOR); EEPROM.put(20,PID_SETPOINT); EEPROM.put(24,Min_Volt); //для типа float нужны 4 ячейки памяти EEPROM.put(28,Min_Temp); EEPROM.put(32,Max_Volt); EEPROM.put(36,Max_Temp); //и ставим метку, что запись в настройки произведена EEPROM.put(Init_Eeprom_Addr,Init_Key); } lcd.init(); //инициализация lcd lcd.backlight(); //включаем подсветку lcd.setCursor(0,0); //ставим курсор в начало первой строки lcd.print(" Temperature"); lcd.setCursor(0,1); //ставим курсор в начало второй строки lcd.print(" Monitor "); delay(1500); //пауза в мс lcd.createChar(0, degree); //прописываем пользовательский символ градуса lcd.createChar(1, arrow_up); //прописываем пользовательский символ стрелки вверх lcd.createChar(2, arrow_down); //прописываем пользовательский символ стрелки вниз lcd.setCursor(0,0); lcd.print(" Please Wait "); lcd.setCursor(0,1); lcd.print(" "); lcd.setCursor(0,1); lcd.print(" "); } void loop() //*************************************************** { //************************** Замеряем температуру ****************************** if(millis() - currentTime > INTERVAL_TIME) //опрос датчика через INTERVAL_TIME { currentTime = millis(); //считываем значение с датчика и усредняем Pt100_Val = expRunningAverage(analogRead (Pt100Pin)); //переводим в температуру Pt100_Temp = ((Pt100_Val - Min_Volt) * (Max_Temp - Min_Temp) / (Max_Volt - Min_Volt)) + Min_Temp; //добавляем поправку Pt100_Temp_Correction = Pt100_Temp + TEMP_CORRECT; if(Start_wait < 4) { lcd.print(">>>"); //для красоты } //это нужно, чтобы стабилизация температуры прошло до начала работы системы Start_wait = Start_wait + 1; if(Start_wait>4) { Start_wait = 10; //10 от балды, лиш бы была больше 4 //******** Расчет ошибки (отклонения от заданного) и выхода с регулятора *********** //ошибка в процентах от диапазона регулирования PID_error = 100*(PID_SETPOINT-Pt100_Temp_Correction)/TOTAL_SPAN ; //считаем ПИД-составляющие в % от времени полного хода клапана VALVE_TIME P_part = PID_error*100/P_FACTOR; I_part = I_part + INTERVAL_TIME*(PID_error+PID_error_old)/(1000*2*I_FACTOR); //ограничиваем интегральную составляющую. Это может понадобиться, например, //при отключенной подаче теплоносителя, когда I_part будет бесконечно расти if(I_part > 150) { I_part = 150; } if(I_part < -150) { I_part = -150; } D_part = 1000*D_FACTOR*(PID_error-PID_error_old)/INTERVAL_TIME; //итоговая реакция регулятора с дополнительным усреднением //в % от времени полного хода клапана VALVE_TIME PID_output = expRunningAverage2(P_part + I_part + D_part); PID_error_old = PID_error; //****************** Расчет времени движения клапана ***************************** //если мы НЕ в процессе движения клапана if(flag_valve_up == false && flag_valve_down == false) { //----------------------- на открытие --------------------- //если разница между старым и новым выходом больше 1 сек. //в процентах от времени полного хода клапана. //Это нужно, чтобы исключить слишком короткие включения реле //VALVE_TIME - 100% , поэтому 1 сек в % = 100%/VALVE_TIME if((PID_output-PID_output_old) > 100/VALVE_TIME) { flag_valve_down = false; flag_valve_up = true; //время открытия клапана в мс. //здесь 10 = 1000мс / 100% time_valve_up = abs(10*VALVE_TIME*(PID_output-PID_output_old)); //чтобы исключить слишком долгого движения клапана if(time_valve_up>3000) { time_valve_up = 3000; //здесь 0.1 = 100% / 1000мс PID_output_old = PID_output_old + 0.1*time_valve_up/VALVE_TIME; } else { PID_output_old = PID_output; } currentTime_Up = millis(); } //----------------------- на закрытие --------------------- //если разница между старым и новым выходом меньше -1 сек. if((PID_output-PID_output_old) < -100/VALVE_TIME) { flag_valve_up = false; flag_valve_down = true; //время открытия клапана в мс. time_valve_down = abs(10*VALVE_TIME*(PID_output-PID_output_old)); //чтобы исключить слишком долгого движения клапана if(time_valve_down>3000) { time_valve_down = 3000; PID_output_old = PID_output_old - 0.1*time_valve_down/VALVE_TIME; } else { PID_output_old = PID_output; } currentTime_Down = millis(); } } } //if(Start_wait>4) } //if(millis() - currentTime > INTERVAL_TIME) //это нужно, чтобы усреднение температуры прошло до начала работы регулятора if(Start_wait>4) { //****************** Вывод первой строки ****************************** //--------------- Текущая температура ---------------------- if(FlagSettingIn == false) //если мы не в настройках { lcd.setCursor(0,0); //прописываем первую строку lcd.print("PV="); //текущая температура lcd.print(Pt100_Temp_Correction); lcd.write((byte)0); //рисуем пользовательский символ градуса lcd.print("C "); //если мы ввели корректировку показания датчика, //за символом градуса будет выводиться восклицательный знак if (TEMP_CORRECT != 0) { lcd.print("!"); } else { lcd.print(" "); } } //---------------- Сигнализация на LCD --------------------- //если сработала сигнализация, выведется буква А (alarm) //даже, если сигнализация на реле отключена if (Pt100_Temp_Correction<=ALARM_MIN || Pt100_Temp_Correction>=ALARM_MAX) { lcd.print("A"); } //если темп. в пределах нормы + диффиренциал if (Pt100_Temp_Correction>ALARM_MIN+ALARM_DIFF && Pt100_Temp_Correction<ALARM_MAX-ALARM_DIFF) { lcd.print(" "); } //************** Открытие/закрытие клапанов *************************** //------------ ручной режим --------------------- if(ManualModeOn == true) { if(FlagSettingIn == false) //если мы не в настройках { //считываем состояние кнопки с устраненным дребезгом CurrentButtonDown = debounce(LastButtonDown, BtDownPin); //если нажата кнопка Вниз, а Вверх отжата if(CurrentButtonDown == HIGH && digitalRead(BtUpPin) == LOW) { digitalWrite(ValveUpPin, LOW); digitalWrite(ValveDownPin, HIGH); lcd.write((byte)2); //рисуем пользовательский символ стрелки вниз lcd.print(" "); } //считываем состояние кнопки с устраненным дребезгом CurrentButtonUp = debounce(LastButtonUp, BtUpPin); //если нажата кнопка Вверх, а Вниз отжата if(CurrentButtonUp == HIGH && digitalRead(BtDownPin) == LOW) { digitalWrite(ValveDownPin, LOW); digitalWrite(ValveUpPin, HIGH); lcd.write((byte)1); //рисуем пользовательский символ стрелки вверх lcd.print(" "); } //если обе кнопки отжаты if(digitalRead(BtUpPin) == LOW && digitalRead(BtDownPin) == LOW) { digitalWrite(ValveUpPin, LOW); digitalWrite(ValveDownPin, LOW); lcd.print(" "); } //если обе кнопки нажаты, защита от дурака if(digitalRead(BtUpPin) == HIGH && digitalRead(BtDownPin) == HIGH) { digitalWrite(ValveUpPin, LOW); digitalWrite(ValveDownPin, LOW); lcd.print(" "); } } //if(FlagSettingIn == false) } else //----------- автоматический режим --------------------- { if(flag_valve_down == true) //закрываем клапан { //в течении time_valve_down if(millis() - currentTime_Down < time_valve_down) { digitalWrite(ValveUpPin, LOW); digitalWrite(ValveDownPin, HIGH); if(FlagSettingIn == false) //если мы не в настройках { lcd.write((byte)2); //рисуем пользовательский символ стрелки вниз lcd.print(" "); } else { lcd.print(" "); } } else { digitalWrite(ValveDownPin, LOW); if(FlagSettingIn == false) //если мы не в настройках { lcd.print(" "); } flag_valve_down = false; } } if(flag_valve_up == true) //открываем клапан { //в течении time_valve_up if(millis() - currentTime_Up < time_valve_up) { digitalWrite(ValveDownPin, LOW); digitalWrite(ValveUpPin, HIGH); if(FlagSettingIn == false) //если мы не в настройках { lcd.write((byte)1); //рисуем пользовательский символ стрелки вверх lcd.print(" "); } else { lcd.print(" "); } } else { digitalWrite(ValveUpPin, LOW); if(FlagSettingIn == false) //если мы не в настройках { lcd.print(" "); } flag_valve_up = false; } } if(flag_valve_down == false && flag_valve_up == false) { lcd.print(" "); } } //********** Включаем или выключаем реле сигнализации ********************** if (AlarmControlOn == true) //если сигнализация включена { if (Pt100_Temp_Correction<=ALARM_MIN || Pt100_Temp_Correction>=ALARM_MAX) { digitalWrite(AlarmPin, HIGH); } if (Pt100_Temp_Correction>ALARM_MIN+ALARM_DIFF && Pt100_Temp_Correction<ALARM_MAX-ALARM_DIFF) { digitalWrite(AlarmPin, LOW); } } else { digitalWrite(AlarmPin, LOW); //если сигнализация отключена } //*** в автоматическом режиме кнопками вверх/вниз можно задать температуру ***** SValueSetting(); //********************* Нажата кнопка Настройки ************************ //при каждом нажатии кнопки Настройки //перескакиваем на следующий параметр в очереди. И так по кругу //считываем состояние кнопки с устраненным дребезгом CurrentButtonSetting = debounce(LastButtonSetting, BtSettingPin); if(CurrentButtonSetting == HIGH) //кнопка нажата { if (FlagSettingLoop == true) //эта переменная нужна для однократного нажатия кнопки, //даже если ее удерживать { FlagSettingLoop = false; FlagSettingIn = true; SettingNumber = SettingNumber + 1; if (SettingNumber > 19) { SettingNumber = 1; } } } else //кнопка отжата { FlagSettingLoop = true; } //обновляем текущее значение кнопки LastButtonSetting = CurrentButtonSetting; if (FlagSettingIn == true) //если мы в настройках { //----------------- Нажата кнопка Увеличить ---------------------- ButtonPlus(); //функция реакции на нажатие кнопки Увеличить //---------------- Нажата кнопка Уменьшить ----------------------- ButtonMinus(); //функция реакции на нажатие кнопки Уменьшить SettingsOnLCD(SettingNumber); //отображаем строку с настройками } //************* Нажата кнопка Manual (Ручной Режим) *********************** //считываем состояние кнопки с устраненным дребезгом CurrentButtonManual = debounce(LastButtonManual, BtManualPin); if(CurrentButtonManual == HIGH && FlagManualLoop == true) //если кнопка нажата { //входим или выходим из ручного режима if(ManualModeOn == false) { ManualModeOn = true; } else { ManualModeOn = false; } if(FlagSettingIn == true) //если были в настройках, то выходим из них { SetingExit(); } FlagManualLoop = false; } if(CurrentButtonManual == LOW) { FlagManualLoop = true; } LastButtonManual = CurrentButtonManual; //обновляем текущее значение кнопки //**************** Нажата кнопка Exit ************************* //считываем состояние кнопки с устраненным дребезгом CurrentButtonExit = debounce(LastButtonExit, BtExitPin); if(CurrentButtonExit == HIGH && FlagExitLoop == true) //если кнопка нажата { if(FlagSettingIn == false) //если мы не в настройках { if(ManualModeOn == false) //если мы не в ручном режиме { LCD_second_string = LCD_second_string+1; if(LCD_second_string > 5) { LCD_second_string = 1; } } } else //если мы в настройках, то выходим из настроек { SetingExit(); } FlagExitLoop = false; } if(CurrentButtonExit == LOW) { FlagExitLoop = true; } LastButtonExit = CurrentButtonExit; //обновляем текущее значение кнопки //**************** Рисуем вторую строку ************************* SecondStringDraw(); } //if(Start_wait>4) } //void loop() //********************** ФУНКЦИИ ********************************* //*********** Функция отображения настроек на LCD ************* void SettingsOnLCD(int SettingItem) { lcd.setCursor(0,0); switch (SettingItem) { case 1: //уставка температуры lcd.print("1 Setpoint (SV) "); lcd.setCursor(0,1); lcd.print(PID_SETPOINT); lcd.write((byte)0); lcd.print("C "); break; case 2: //коэф пропорциональности (proportional band в %) lcd.print("2 P-factor "); lcd.setCursor(0,1); lcd.print(P_FACTOR); lcd.print(" % "); break; case 3: //время интегрирования (integral time) в сек. lcd.print("3 I-factor "); lcd.setCursor(0,1); lcd.print(I_FACTOR); lcd.print(" sec "); break; case 4: //время дифференцирования (derivative time) в сек. lcd.print("4 D-factor "); lcd.setCursor(0,1); lcd.print(D_FACTOR); lcd.print(" sec "); break; case 5: //поправки температуры датчика lcd.print("5 Temp Correct. "); lcd.setCursor(0,1); lcd.print(TEMP_CORRECT); lcd.write((byte)0); lcd.print("C "); break; case 6: //сигнализация по максимальной температуре lcd.print("6 Alarm Max Temp"); lcd.setCursor(0,1); lcd.print(ALARM_MAX); lcd.write((byte)0); lcd.print("C "); break; case 7: //сигнализация по минимальной температуре lcd.print("7 Alarm Min Temp"); lcd.setCursor(0,1); lcd.print(ALARM_MIN); lcd.write((byte)0); lcd.print("C "); break; case 8: //диффиренциал сигнализации lcd.print("8 Alarm Dif Temp"); lcd.setCursor(0,1); lcd.print(ALARM_DIFF); lcd.write((byte)0); lcd.print("C "); break; case 9: //интервал опроса датчика в мс lcd.print("9 Sensor Time "); lcd.setCursor(0,1); lcd.print(INTERVAL_TIME); lcd.print(" ms "); break; case 10: //время открытия клапана в сек. от 0 до полного lcd.print("10 Valve Time "); lcd.setCursor(0,1); lcd.print(VALVE_TIME); lcd.print(" sec "); break; case 11: //диапазон регулирования lcd.print("11 Total Span "); lcd.setCursor(0,1); lcd.print(TOTAL_SPAN); lcd.write((byte)0); lcd.print("C "); break; //данные для калибровки датчика Pt100 //нижняя граница вольтажа с аналогового порта переведенный в цифровой вид case 12: lcd.print("12 Min_Volt "); lcd.setCursor(0,1); lcd.print(Min_Volt,0); lcd.print(" "); break; case 13: //соответствующая нижней границе температура lcd.print("13 MinPt "); lcd.setCursor(0,1); lcd.print(Min_Temp, 1); lcd.write((byte)0); lcd.print("C "); break; //верхняя граница вольтажа с аналогового порта переведенный в цифровой вид case 14: lcd.print("14 Max_Volt "); lcd.setCursor(0,1); lcd.print(Max_Volt,0); lcd.print(" "); break; case 15: //соответствующая верхней границе температура lcd.print("15 MaxPt "); lcd.setCursor(0,1); lcd.print(Max_Temp, 1); lcd.write((byte)0); lcd.print("C "); break; case 16: //включение-выключение сигнализации по температуре lcd.print("16 Alarm ON/OFF "); lcd.setCursor(0,1); if(AlarmControlOn == true) { lcd.print("Alarm ON "); } else { lcd.print("Alarm OFF "); } break; case 17: //чтение настроек из EEPROM lcd.print("17 Read Settings"); lcd.setCursor(0,1); lcd.print("Press ("); lcd.write((byte)1); lcd.print(") button"); break; case 18: //запись настроек в EEPROM lcd.print("18 Save Settings"); lcd.setCursor(0,1); lcd.print("Press ("); lcd.write((byte)1); lcd.print(") button"); break; case 19: //загрузка значений по умолчанию lcd.print("19 Load Default "); lcd.setCursor(0,1); lcd.print("Press ("); lcd.write((byte)1); lcd.print(") button"); break; } } //********* Функция кнопки Больше, Увеличить, Плюс и т.п. ************* void ButtonPlus() { CurrentButtonUp = debounce(LastButtonUp, BtUpPin); if(CurrentButtonUp == HIGH) //кнопка нажата { if (SettingNumber >= 1 && SettingNumber <= 15 ) //пункты настроек, где нужен разгон параметра в режиме удержания { if(FlagUpHold == true) //отсечка времени удержания кнопки { ButtonHoldTimerUp = millis(); FlagUpHold = false; } if(millis() - ButtonHoldTimerUp > ButtonHoldIncTime) //сначала однократное нажатие, но если держать кнопку более //ButtonHoldIncTime, она переходит в режим удержания { ButtonPlusAction(SettingNumber, true); } if(FlagUpLoop == true) //однократное нажатие, даже если удерживать кнопку, //пока не прошло ButtonHoldIncTime { FlagUpLoop = false; ButtonPlusAction(SettingNumber, false); } } else //пункты настроек, где нужно только однократное нажатие кнопки { if(FlagUpLoop == true) { FlagUpLoop = false; ButtonPlusAction(SettingNumber, false); } } } else //кнопка отжата { FlagUpLoop = true; FlagUpHold = true; } LastButtonUp = CurrentButtonUp; //обновляем текущее значение кнопки } //******** Функция изменения параметров при нажатии кнопки Больше ********** void ButtonPlusAction(int SettingItem, boolean IncTrigger) { switch (SettingItem) { case 1: //уставка температуры PID_SETPOINT = PID_SETPOINT + Inc_Val_Calc(IncTrigger,1); if(PID_SETPOINT > PID_SETPOINT_MAX) //верхний порог { PID_SETPOINT = PID_SETPOINT_MAX; } //при изменении уставки обнуляем интегральную часть и выход с регулятора, //чтобы "память" о старой уставке не мешала регулировать температуру I_part = 0; PID_output_old = 0; PID_output = 0; break; case 2: //коэф пропорциональности (proportional band в %) P_FACTOR = P_FACTOR + Inc_Val_Calc(IncTrigger,1); if(P_FACTOR > P_FACTOR_MAX) //верхний порог { P_FACTOR = P_FACTOR_MAX; } break; case 3: //время интегрирования (integral time) в сек. I_FACTOR = I_FACTOR + Inc_Val_Calc(IncTrigger,1); if(I_FACTOR > I_FACTOR_MAX) //верхний порог { I_FACTOR = I_FACTOR_MAX; } break; case 4: //время дифференцирования (derivative time) в сек. D_FACTOR = D_FACTOR + Inc_Val_Calc(IncTrigger,1); if(D_FACTOR > D_FACTOR_MAX) //верхний порог { D_FACTOR = D_FACTOR_MAX; } break; case 5: //поправки температуры датчика TEMP_CORRECT = TEMP_CORRECT + Inc_Val_Calc(IncTrigger,1); if(TEMP_CORRECT > TEMP_CORRECT_MAX) //верхний порог { TEMP_CORRECT = TEMP_CORRECT_MAX; } break; case 6: //сигнализация по максимальной температуре ALARM_MAX = ALARM_MAX + Inc_Val_Calc(IncTrigger,1); if(ALARM_MAX > ALARM_MAX_MAX) //верхний порог { ALARM_MAX = ALARM_MAX_MAX; } break; case 7: //сигнализация по минимальной температуре ALARM_MIN = ALARM_MIN + Inc_Val_Calc(IncTrigger,1); if(ALARM_MIN > ALARM_MIN_MAX) //верхний порог { ALARM_MIN = ALARM_MIN_MAX; } break; case 8: //диффиренциал сигнализации ALARM_DIFF = ALARM_DIFF + Inc_Val_Calc(IncTrigger,1); if(ALARM_DIFF > ALARM_DIFF_MAX) //верхний порог { ALARM_DIFF = ALARM_DIFF_MAX; } break; case 9: //интервал опроса датчика в мс INTERVAL_TIME = INTERVAL_TIME + Inc_Val_Calc(IncTrigger,1); if(INTERVAL_TIME > INTERVAL_TIME_MAX) //верхний порог { INTERVAL_TIME = INTERVAL_TIME_MAX; } break; case 10: //время открытия клапана в сек. от 0 до полного VALVE_TIME = VALVE_TIME + Inc_Val_Calc(IncTrigger,1); if(VALVE_TIME > VALVE_TIME_MAX) //верхний порог { VALVE_TIME = VALVE_TIME_MAX; } break; case 11: //диапазон регулирования TOTAL_SPAN = TOTAL_SPAN + Inc_Val_Calc(IncTrigger,1); if(TOTAL_SPAN > TOTAL_SPAN_MAX) //верхний порог { TOTAL_SPAN = TOTAL_SPAN_MAX; } break; //данные для калибровки датчика Pt100 //нижняя граница вольтажа с аналогового порта переведенный в цифровой вид case 12: Min_Volt = Min_Volt + Inc_Val_Calc(IncTrigger,1); if (Min_Volt >= Max_Volt) { Min_Volt = Min_Volt - Inc_Val_Calc(IncTrigger,1); lcd.setCursor(0,1); lcd.print("Min must < Max "); //выводим подсказку delay(2000); } break; case 13: //соответствующая нижней границе температура Min_Temp = Min_Temp + Inc_Val_Calc(IncTrigger,2); if (Min_Temp > 1000) { Min_Temp = 1000; } break; //верхняя граница вольтажа с аналогового порта переведенный в цифровой вид case 14: Max_Volt = Max_Volt + Inc_Val_Calc(IncTrigger,1); if (Max_Volt > 1023) { Max_Volt = 1023; } break; case 15: //соответствующая верхней границе температура Max_Temp = Max_Temp + Inc_Val_Calc(IncTrigger,2); if (Max_Temp > 1000) { Max_Temp = 1000; } break; case 16: //включение-выключение сигнализации по температуре if(AlarmControlOn == true) { AlarmControlOn = false; } else { AlarmControlOn = true; } break; case 17: //чтение настроек из EEPROM EEPROM.get(0,TEMP_CORRECT); //для типа int нужны 2 ячейки памяти EEPROM.get(2,ALARM_MAX); EEPROM.get(4,ALARM_MIN); EEPROM.get(6,ALARM_DIFF); EEPROM.get(8,INTERVAL_TIME); EEPROM.get(10,VALVE_TIME); EEPROM.get(12,TOTAL_SPAN); EEPROM.get(14,P_FACTOR); EEPROM.get(16,I_FACTOR); EEPROM.get(18,D_FACTOR); EEPROM.get(20,PID_SETPOINT); EEPROM.get(24,Min_Volt); //для типа float нужны 4 ячейки памяти EEPROM.get(28,Min_Temp); EEPROM.get(32,Max_Volt); EEPROM.get(36,Max_Temp); lcd.setCursor(0,1); lcd.print("Reading... OK "); delay(1500); break; case 18: //запись настроек в EEPROM EEPROM.put(0,TEMP_CORRECT); //для типа int нужны 2 ячейки памяти EEPROM.put(2,ALARM_MAX); EEPROM.put(4,ALARM_MIN); EEPROM.put(6,ALARM_DIFF); EEPROM.put(8,INTERVAL_TIME); EEPROM.put(10,VALVE_TIME); EEPROM.put(12,TOTAL_SPAN); EEPROM.put(14,P_FACTOR); EEPROM.put(16,I_FACTOR); EEPROM.put(18,D_FACTOR); EEPROM.put(20,PID_SETPOINT); EEPROM.put(24,Min_Volt); //для типа float нужны 4 ячейки памяти EEPROM.put(28,Min_Temp); EEPROM.put(32,Max_Volt); EEPROM.put(36,Max_Temp); //ставим метку, что запись в настройки произведена EEPROM.put(Init_Eeprom_Addr,Init_Key); lcd.setCursor(0,1); lcd.print("Saving... OK "); delay(1500); break; case 19: //загрузка значений по умолчанию TEMP_CORRECT = 0; ALARM_MAX = 60; ALARM_MIN = 20; ALARM_DIFF = 1; INTERVAL_TIME = 2000; VALVE_TIME = 43; TOTAL_SPAN = 55; P_FACTOR = 100; I_FACTOR = 70; D_FACTOR = 15; PID_SETPOINT = 45; Min_Volt = 39; Min_Temp = 0; Max_Volt = 546; Max_Temp = 100; lcd.setCursor(0,1); lcd.print("Loading... OK "); delay(1500); break; } } //********* Функция кнопки Меньше, Уменьшить, Минус или т.п. ************* void ButtonMinus() { CurrentButtonDown = debounce(LastButtonDown, BtDownPin); if(CurrentButtonDown == HIGH) //кнопка нажата { if (SettingNumber >= 1 && SettingNumber <= 15 ) //пункты настроек, где нужен разгон параметра в режиме удержания { if(FlagDownHold == true) //отсечка времени удержания кнопки { ButtonHoldTimerDown = millis(); FlagDownHold = false; } if(millis() - ButtonHoldTimerDown > ButtonHoldIncTime) //сначала однократное нажатие, но если держать кнопку более //ButtonHoldIncTime, она переходит в режим удержания { ButtonMinusAction(SettingNumber, true); } if(FlagDownLoop == true) //однократное нажатие, даже если удерживать кнопку, //пока не прошло ButtonHoldIncTime { FlagDownLoop = false; ButtonMinusAction(SettingNumber, false); } } else //пункты настроек, где нужно только однократное нажатие кнопки { if(FlagDownLoop == true) { FlagDownLoop = false; ButtonMinusAction(SettingNumber, false); } } } else //кнопка отжата { FlagDownLoop = true; FlagDownHold = true; } LastButtonDown = CurrentButtonDown; //обновляем текущее значение кнопки } //******** Функция изменения параметров при нажатии кнопки Меньше ********** void ButtonMinusAction(int SettingItem, boolean IncTrigger) { switch (SettingItem) { case 1: //уставка температуры PID_SETPOINT = PID_SETPOINT - Inc_Val_Calc(IncTrigger,1); if(PID_SETPOINT < PID_SETPOINT_MIN) //нижний порог { PID_SETPOINT = PID_SETPOINT_MIN; } //при изменении уставки обнуляем интегральную часть и выход с регулятора, //чтобы "память" о старой уставке не мешала регулировать температуру I_part = 0; PID_output_old = 0; PID_output = 0; break; case 2: //коэф пропорциональности (proportional band в %) P_FACTOR = P_FACTOR - Inc_Val_Calc(IncTrigger,1); if(P_FACTOR < P_FACTOR_MIN) //нижний порог { P_FACTOR = P_FACTOR_MIN; } break; case 3: //время интегрирования (integral time) в сек. I_FACTOR = I_FACTOR - Inc_Val_Calc(IncTrigger,1); if(I_FACTOR < I_FACTOR_MIN) //нижний порог { I_FACTOR = I_FACTOR_MIN; } break; case 4: //время дифференцирования (derivative time) в сек. D_FACTOR = D_FACTOR - Inc_Val_Calc(IncTrigger,1); if(D_FACTOR < D_FACTOR_MIN) //нижний порог { D_FACTOR = D_FACTOR_MIN; } break; case 5: //поправки температуры датчика TEMP_CORRECT = TEMP_CORRECT - Inc_Val_Calc(IncTrigger,1); if(TEMP_CORRECT < TEMP_CORRECT_MIN) //нижний порог { TEMP_CORRECT = TEMP_CORRECT_MIN; } break; case 6: //сигнализация по максимальной температуре ALARM_MAX = ALARM_MAX - Inc_Val_Calc(IncTrigger,1); if(ALARM_MAX < ALARM_MAX_MIN) //нижний порог { ALARM_MAX = ALARM_MAX_MIN; } break; case 7: //сигнализация по минимальной температуре ALARM_MIN = ALARM_MIN - Inc_Val_Calc(IncTrigger,1); if(ALARM_MIN < ALARM_MIN_MIN) //нижний порог { ALARM_MIN = ALARM_MIN_MIN; } break; case 8: //диффиренциал сигнализации ALARM_DIFF = ALARM_DIFF - Inc_Val_Calc(IncTrigger,1); if(ALARM_DIFF < ALARM_DIFF_MIN) //нижний порог { ALARM_DIFF = ALARM_DIFF_MIN; } break; case 9: //интервал опроса датчика в мс INTERVAL_TIME = INTERVAL_TIME - Inc_Val_Calc(IncTrigger,1); if(INTERVAL_TIME < INTERVAL_TIME_MIN) //нижний порог { INTERVAL_TIME = INTERVAL_TIME_MIN; } break; case 10: //время открытия клапана в сек. от 0 до полного VALVE_TIME = VALVE_TIME - Inc_Val_Calc(IncTrigger,1); if(VALVE_TIME < VALVE_TIME_MIN) //нижний порог { VALVE_TIME = VALVE_TIME_MIN; } break; case 11: //диапазон регулирования TOTAL_SPAN = TOTAL_SPAN - Inc_Val_Calc(IncTrigger,1); if(TOTAL_SPAN < TOTAL_SPAN_MIN) //нижний порог { TOTAL_SPAN = TOTAL_SPAN_MIN; } break; //данные для калибровки датчика Pt100 //нижняя граница вольтажа с аналогового порта переведенный в цифровой вид case 12: Min_Volt = Min_Volt - Inc_Val_Calc(IncTrigger,1); if (Min_Volt < 0) { Min_Volt = 0; } break; case 13: //соответствующая нижней границе температура Min_Temp = Min_Temp - Inc_Val_Calc(IncTrigger,2); if (Min_Temp < 0) { Min_Temp = 0; } break; //верхняя граница вольтажа с аналогового порта переведенный в цифровой вид case 14: Max_Volt = Max_Volt - Inc_Val_Calc(IncTrigger,1); if (Max_Volt <= Min_Volt) { Max_Volt = Max_Volt + Inc_Val_Calc(IncTrigger,1); lcd.setCursor(0,1); lcd.print("Max must > Min "); //выводим подсказку delay(2000); } break; case 15: //соответствующая верхней границе температура Max_Temp = Max_Temp - Inc_Val_Calc(IncTrigger,2); if (Max_Temp < 0) { Max_Temp = 0; } break; case 16: if(AlarmControlOn == true) { AlarmControlOn = false; } else { AlarmControlOn = true; } break; } } //**** в автоматическом режиме кнопками вверх/вниз можно задать температуру ***** void SValueSetting() { int sn = SettingNumber; if(ManualModeOn == false) { //если мы не в настройках и видим уставку температуры во второй строке if(FlagSettingIn == false && LCD_second_string == 1) { SettingNumber = 1; ButtonMinus(); //функция реакции на нажатие кнопки Уменьшить ButtonPlus(); //функция реакции на нажатие кнопки Уменьшить SettingNumber = sn; } //if(FlagSettingIn == false && LCD_second_string == 1) } //if(ManualModeOn == false) } //**************** функция рисования второй строки ************************* void SecondStringDraw() { if(FlagSettingIn == false) //если мы не в настройках { lcd.setCursor(0,1); //прописываем вторую строку if(ManualModeOn == true) //если включен ручной режим { lcd.print("Manual Mode "); } else { switch (LCD_second_string) { case 1: lcd.print("SV="); lcd.print(PID_SETPOINT); lcd.write((byte)0); //рисуем пользовательский символ градуса lcd.print("C "); break; case 2: lcd.print("Output="); lcd.print(PID_output); lcd.print("% "); break; case 3: lcd.print("P_part="); lcd.print(P_part); lcd.print("% "); break; case 4: lcd.print("I_part="); lcd.print(I_part); lcd.print("% "); break; case 5: lcd.print("D_part="); lcd.print(D_part); lcd.print("% "); break; } } } } void SetingExit() { //это нужно, чтобы остаться на том же пункте меню настроек SettingNumber = SettingNumber - 1; if(SettingNumber < 0) { SettingNumber = 0; } FlagSettingIn = false; } //*************** Функция устранения дребезга контактов *********** boolean debounce(boolean last, int ButtonPinDebounce) { boolean current = digitalRead(ButtonPinDebounce); //считываем текущее состояние кнопки if(last != current) //если оно иное, чем предыдущее... { delay(50); //ждем 50 мс current = digitalRead(ButtonPinDebounce); //считываем состояние снова } return current; //возвращаем текущее состояние кнопки } //************* функция усреднения в точке замера *************** float expRunningAverage(float newVal) { static float filVal = 0; float k; // резкость фильтра зависит от процента отклонения от предыдущего значения if (abs((newVal - filVal)/filVal) > 0.05) k = 0.9; else k = 0.1; filVal += (newVal - filVal) * k; return filVal; } //************* функция усреднения в точке замера *************** float expRunningAverage2(float newVal2) { static float filVal2 = 0; float k; // резкость фильтра зависит от процента отклонения от предыдущего значения if (abs((newVal2 - filVal2)/filVal2) > 0.05) k = 0.9; else k = 0.1; filVal2 += (newVal2 - filVal2) * k; return filVal2; } //*************** Функция разгона приращения параметра ************ float Inc_Val_Calc(boolean IncTrigger, int IncVariant) { static int count_inc = 0; //счетчик итераций //IncVariant - разные варианты разгона if (IncTrigger == true) //пришел сигнал на разгон { count_inc = count_inc + 1; if (IncVariant == 1) { //играя этими числами, вы можете подстраивать разгон параметра под себя if (count_inc > 0 && count_inc <= 30) { return 1; } if (count_inc > 30 && count_inc <= 60) { return 2; } if (count_inc > 60 && count_inc <= 90) { return 5; } if (count_inc > 90 && count_inc <= 120) { return 10; } if (count_inc > 120) { return 100; } } if (IncVariant == 2) { //играя этими числами, вы можете подстраивать разгон параметра под себя if (count_inc > 0 && count_inc <= 30) { return 0.1; } if (count_inc > 30 && count_inc <= 60) { return 1; } if (count_inc > 60 && count_inc <= 90) { return 5; } if (count_inc > 90) { return 10; } } } else { count_inc = 0; if (IncVariant == 1) { return 1; //приращение по умолчанию } if (IncVariant == 2) { return 0.1; //приращение по умолчанию } } }