Регулятор был задуман как замена штатного в электроконвектор на дачу, потому что встроенный не обеспечивал необходимой точности поддержания температуры и при перебоях в питании самостоятельно не включался в работу, что могло нехорошо отразится на заполненном водопроводе. Данный регулятор обеспечивает поддержание заданной температуры. Проработал зиму на даче, никаких замечаний к работе регулятора нет, температуру держет точно. В качестве датчика температуры используется терморезистор NTC 10кОм при 20С, выходным устройством являеться реле (в дальнейшем планирую поставить симистор) (организован медленный ШИМ). Схема включения терморезистора стандартная, кнопки (аналоговые). Из скетча должно быть понятно что куда цеплять. У меня девайс выполнен на своей плате, использован микроконтроллер ATMega8, тактирование от встроенного генератора 8МГц. Собрано всё в монтажной коробке 100х100х40. Схема устройства LCD и аналоговые кнопки не показаны. На данный момент схема немного изменена в части питания терморезистора (он запитан от шины +5В а не от пина микроконтроллера как в первой версии) Устройство проработало на ниже представленном коде. Код (C++): #include <TermoRes3950.h>//библиотека терморезистора //кнопки А0, реле 13,12 подсветка 10 #include <LiquidCrystal.h> #define key_in analogRead(0)//вход аналоговых кнопок #define HI_OFF digitalWrite(13,LOW)//отключить реле HI #define HI_ON digitalWrite(13,HIGH)//включить реле HI #define n_f 10.0//температура по умолчанию #define p_s 50//частота обновления экрана #define t_pwm 60000//период 100% мощности #define t_time 1000//период опроса температуры TermoRes3950 termo(1);//терморезистор на АЦП 1 //для ПИ регулятора float p,i; LiquidCrystal lcd(7,6,5,4,3,2); long time;//интервал замера температуры long lcd_time;//интервал отрисовки экрана long on_time=0;//для периода ШИМ int zad=0; int k_b[]={ 512,683,768};//аналоговые кнопки SET + - float temp=0.0;//текущее значение температуры float pre_temp=0.0;//предыдущее значение температуры int w=0; float ust=n_f;//termostate byte cel[] = { 0b01000, 0b10100, 0b01000, 0b00111, 0b01000, 0b01000, 0b00111 }; void setup(){ // Serial.begin(9600); pinMode(13,OUTPUT);//реле HI HI_OFF;//отключить реле HI lcd.begin(16, 2); analogWrite(10,250);//подсветка time = millis(); lcd_time = time; on_time = time; lcd.createChar(1, cel); } void loop(){ byte k; k=key();//получить код нажатой кнопки key_m(k);//выполнить действие согласно нажатой кнопке if (time>millis()){//защита от переполнения time=millis(); lcd_time=time; on_time=time; } //---------------чтение температуры--------------- if ((millis()-time)>=t_time){ time=millis(); temp=termo.getTemp();//получаем температуру temp=(pre_temp*0.2)+(temp*0.8);//фильтр // w=abs((ust-temp)*(pre_temp-temp));//время выхода на уставку pre_temp=temp; PIctl();//расчет мощности } //-------------------вывод на экран---------------- if ((millis()-lcd_time)>=p_s){//вывод на экран lcd_time=millis(); Screen_print(); } //---------------------управление мощностью-------------------------- //выдаем мощность if (millis()>3000){ if(millis()-on_time >= t_pwm){//период ШИМ on_time=millis(); } if (millis()<((zad*(t_pwm/100))+on_time)){ HI_ON; } else { HI_OFF; } } } void PIctl() { if(temp>(ust-0.5)&&temp<(ust+0.5))//зона нечувствительности +-0,5C { zad=i; } else { float e; e=(ust-temp); // расчет выходной мощности: p=65.0*e;//130 коффициент пропорциональности 130.0 if (p<0.0) { p=0.0;//ограничение P } i=(i+(e*0.014));//0.7 if (i>100.0) { i=100.0;//ограничение I } if (i<-100.0) { i=-50.0;//ограничение I } //расчет выходной мощности zad=p+i; } //ограничение управляющего сигнала //для исключения частого переключения реле //если твердотельное реле, можно не ограничивать if (zad<4){ zad=0; } if (zad>96){ zad=100; } //корректировка при отклонении температуры более чем на +-2С //для быстрого выхода на уставку if (temp<(ust-2.0))//если темп < уст на 2С { zad=100;//то мощность 100% i=0; } if (temp>(ust+2.0))//если темп > уст на 2С { zad=0;//то мощность 0% i=0; } } //----------------опрос клавиш------------------------------- byte key(){ int x; x = key_in; if (x>k_b[0]-5 && x<k_b[0]+5){//МЕНЮ UP_butt(); return 0; // key_m(0); } if (x>k_b[1]-5 && x<k_b[1]+5){// + UP_butt(); return 1; // key_m(1); } if (x>k_b[2]-5 && x<k_b[2]+5){// - UP_butt(); return 2; // key_m(2); } } //---------------проверка отпускания кнопки----------------- void UP_butt(){ while (key_in<1010){//ждем отпускания кнопки } } //-----------------обработка кнопок------------------------- void key_m(int x){ switch (x){ case 0://menu ust=10; break; case 1://кнопка + ust++; if (ust>35){ ust=35; } break; case 2://кнопка - ust--; if (ust<5){ ust=5; } break; } } void Screen_print() { byte a=0; tempPrint(temp,0,0); tempPrint(ust,0,1); lcd.setCursor(6, 0); lcd.print(zad); lcd.print("% "); varPrint(i,"I ",6,1); lcd.setCursor(10, 0); lcd.print(on_time); a=digitalRead(13); if (a==1){ lcd.setCursor(12, 1); lcd.print("HI "); } if (a==0){ lcd.setCursor(12, 1); lcd.print("OFF"); } } //-------------вывод температуры---------------------------- void tempPrint(double t, int x, int y){ lcd.setCursor(x, y); lcd.print(t); lcd.setCursor(x+4, y); lcd.print(char(1));//вывод градусов } //---------------вывод переменной------------------------- void varPrint (double v, char* t, int x, int y){ lcd.setCursor(x, y); lcd.print(v); lcd.print(t); } Устройство в работе показало себя отлично. Температуру держало точно.
На данный момент переписал код, добавил сохранение уставки в EEPROM, переписал функцию управления силовой частью. Теперь код выглядит так. Основной цикл Код (C++): #include <DigOut.h>//http://forum.amperka.ru/threads/%D0%9A%D0%BB%D0%B0%D1%81%D1%81%D1%8B-digout-%D0%B8-digin.8711/ #include <EEPROM.h> #include <LiquidCrystal.h> #include <TermoRes3950.h>//http://forum.amperka.ru/threads/%D0%91%D0%B8%D0%B1%D0%BB%D0%B8%D0%BE%D1%82%D0%B5%D0%BA%D0%B0-%D0%B4%D0%BB%D1%8F-%D1%82%D0%B5%D1%80%D0%BC%D0%BE%D1%80%D0%B5%D0%B7%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B0.8716/ DigOut ten(13,0);//инициализация тэна на 13 выходе, отключен TermoRes3950 termo(1);//терморезистор на АЦП 1 LiquidCrystal lcd(7,6,5,4,3,2); //кнопки А0, реле 13,12 подсветка 10 #define key_in analogRead(0)//вход аналоговых кнопок #define p_s 50//частота обновления экрана boolean ps=1; #define t_pwm 60000//период LoPWM #define t_time 1000//период опроса температуры //для ПИ регулятора float p,i; unsigned long time;//интервал замера температуры int8_t zad=0;//задание мощности на выход #define kw 10//ширина кнопки :) int k_b[]={ 93,49,0};//proteus key /*int k_b[]={ 512,683,768};//аналоговые кнопки SET + -*/ unsigned long currentMillis; float temp=0.0;//текущее значение температуры float pre_temp=0.0;//предыдущее значение температуры //int w=0; float ust; byte cel[] = { 0b01000, 0b10100, 0b01000, 0b00111, 0b01000, 0b01000, 0b00111 }; void setup(){ OCR0A = 0xAF;//настройки прерывания TIMSK0 |= _BV(OCIE0A);//по таймеру, для получения millis() if(!EEPROM_float_read(0)){//если уставка в памяти ust=EEPROM_float_read(0);//то загрузить из памяти } else { ust=10;//установить по умолчанию } // Serial.begin(9600); temp=termo.getTemp();//получаем температуру lcd.begin(16, 2); analogWrite(10,250);//подсветка currentMillis = millis(); lcd.createChar(1, cel); } void loop(){ byte k; k=key();//получить код нажатой кнопки key_m(k);//выполнить действие согласно нажатой кнопке //---------------чтение температуры--------------- if (currentMillis-time>=t_time){ time=currentMillis; temp=termo.getTemp();//получаем температуру temp=(pre_temp*0.2)+(temp*0.8);//фильтр pre_temp=temp; zad=PIctl(temp, ust);//расчет мощности ps=1;//обновить экран } //-------------------вывод на экран---------------- if(ps==1){ lcd.clear(); Screen_print(); ps=0; } //---------------------управление мощностью-------------------------- //выдаем мощность if (currentMillis>3000){ ten.blink(t_pwm,(zad*(t_pwm/100)));//медленный ШИМ на тен } }//end Loop Функции работы с EEPROM Код (C++): // чтение float EEPROM_float_read(int addr) { byte raw[4]; for(byte i = 0; i < 4; i++) raw[i] = EEPROM.read(addr+i); float &num = (float&)raw; return num; } // запись void EEPROM_float_write(int addr, float num) { byte raw[4]; (float&)raw = num; for(byte i = 0; i < 4; i++) EEPROM.write(addr+i, raw[i]); } Функция расчета мощности по ПИ закону Код (C++): //расчет мощности byte PIctl(float temp, float ust) { byte zad=0; static float i=0; if(temp>(ust-0.5)&&temp<(ust+0.5))//зона нечувствительности +-0,5C { return byte(i); //zad=byte(i);//если ошибка+-0.5С то мощность = И состовляющей } else//иначе { float e, p; e=(ust-temp);//вычисление ошибки регулирования // расчет выходной мощности: p=(65*e);//расчет пропорциональной состовляющей if (p<0.0)//мощность не может быть отрицательной { p=0.0;//ограничение P } i=(i+(e*0.014));//расчет интегральной составляющей = тепловым потерям if (i>100.0)//мощность не может быть больше 100% { i=100.0;//ограничение I } if (i<-100.0) { i=-50.0;//ограничение I } //расчет выходной мощности zad=byte(p+i); if(zad>100){ return 100; } if(zad<0){ return 0; } //корректировка при отклонении температуры более чем на +-2С //для быстрого выхода на уставку if (temp<(ust-2))//если темп < уст на 2С { zad=100;//то мощность 100% i=0; } if (temp>(ust+2))//если темп > уст на 2С { zad=0;//то мощность 0% i=0; } return zad; }/// } Функция опроса кнопок Код (C++): //----------------опрос клавиш------------------------------- byte key(){ int x; x = key_in; if (x>k_b[0]-kw && x<k_b[0]+kw){//SET UP_butt(); return 0; // key_m(0); } if (x>k_b[1]-kw && x<k_b[1]+kw){// + UP_butt(); return 1; // key_m(1); } if (x>k_b[2]-kw && x<k_b[2]+kw){// - UP_butt(); return 2; // key_m(2); } } //---------------проверка отпускания кнопки----------------- void UP_butt(){ while (key_in<1010){//ждем отпускания кнопки } } Функция получения текущего millis по прерыванию таймера Код (C++): SIGNAL(TIMER0_COMPA_vect) //получение времени один раз в 1 мс { currentMillis = millis(); } Функция реакций на кнопки Код (C++): //-----------------обработка кнопок------------------------- void key_m(int x){ switch (x){ case 0://SAVE EEPROM if (EEPROM_float_read(0)!=ust){ lcd.setCursor(10, 0); lcd.print(" SAVE "); delay(300); lcd.setCursor(10, 0); lcd.print(" "); EEPROM_float_write(0, ust);//запись новой уставки в память } //ust=10; break; case 1://кнопка + ust=var(ust,5,35,1,0); break; case 2://кнопка - ust=var(ust,5,35,-1,0); break; } } //--------------функция изменения переменной------------------- //х=var(переменная,минимум,максимум,шаг,циклически); int var(int v, int mn, int mx, int stp, boolean c){ v+=stp; switch (c){//c-1 циклически с-0 до пределов case 1:// if (v<mn){ v=mx; } if (v>mx){ v=mn; } break; case 0: if (v<mn){ v=mn; } if (v>mx){ v=mx; } break; } return v; } Функция вывода на экран Код (C++): void Screen_print() { byte a=0; tempPrint(temp,0,0); tempPrint(ust,0,1); lcd.setCursor(6, 0); lcd.print(zad); lcd.print("% "); varPrint(i,"I ",6,1); //раскоментировать для определения аналоговых кнопок //---------------------------------------- /* lcd.setCursor(10, 0); lcd.print(analogRead(0)); lcd.print(" ");*/ //---------------------------------------- if (ten.state()==1){//(a==1){ lcd.setCursor(12, 1); lcd.print("HI "); } if (ten.state()==0){//(a==0){ lcd.setCursor(12, 1); lcd.print("OFF"); } } //-------------вывод температуры---------------------------- void tempPrint(double t, int x, int y){ lcd.setCursor(x, y); lcd.print(t); if(t>0){ lcd.setCursor(x+4, y); } else { lcd.setCursor(x+5, y); } lcd.print(char(1));//вывод градусов } //---------------вывод переменной------------------------- void varPrint (double v, char* t, int x, int y){ lcd.setCursor(x, y); lcd.print(v); lcd.print(t); } Код еще будет дорабатыватся. Хочу установить LCD NOKIA 5210. Наладить двухстороннюю связь с народным мониторингом.
Немного обновил функцию ПИ регулятора. Теперь настройка ПИ регулятора стала более удобной. Код (C++): //расчет мощности по ПИ закону регулирования byte PIctl(float temp, float ust) { #define kP 65//коэффициент пропорциональности #define p_min 0.0//минимум П составляющей - не < 0 #define p_max 100.0//максимум П составляющей - не > 100 #define kI 0.014//коэффициент интегрирования #define i_max 50.0//минимум И составляющей #define i_min -50.0//максимум И составляющей #define no_ctl 0,5//зона от уставки нечувствительности +- #define d_ctl 2//зона +- от уставки - за пределами или 100% или 0% byte zad=0; static float i=0; if(temp>(ust-no_ctl)&&temp<(ust+no_ctl))//зона нечувствительности +-0,5C { return byte(i); } else//иначе { float e, p; e=(ust-temp);//ошибка регулирования // расчет выходной мощности: p=(kP*e);//расчет пропорциональной состовляющей (p<p_min)? p=p_min: (p>p_max)? p=p_max: p=p;//ограничение p снизу i=(i+(e*kI));//расчет интегральной составляющей = тепловым потерям (i>i_max)? i=i_max: (i<i_min)? i=i_min: i=i; zad=byte(p+i); (zad>100)? zad=100: (zad<0)? zad=0: zad=zad; //При отклонении температуры более чем на +-2С для быстрого выхода на уставку if (temp<(ust-d_ctl))//если темп < уст на 2С { zad=100;//то мощность 100% i=0; } if (temp>(ust+d_ctl))//если темп > уст на 2С { zad=0;//то мощность 0% i=0; } return zad;//возвращаем расчитаную мощность } } Напомню что в функцию передается текущее состояние (температура) и уставка (желаемое значение), функция возвращает вычисленную с учетом коэффициентов (индивидуальны для каждого объекта) необходимое воздействие в % необходимое для достижения цели. Кому не понятна запись типа (p<p_min)? p=p_min: (p>p_max)? p=p_max: p=p; поясню (условие)? если истина : если лож ;
Немного переписал функцию расчета мощности Код (C++): //расчет мощности по ПИ закону регулирования byte PIctl(float temp, int8_t ust)//возвращает необходимую мощность { #define kP 15//коэффициент пропорциональности #define p_min 0.0//минимум П составляющей - не < 0 #define p_max 100.0//максимум П составляющей - не > 100 #define kI 0.015//коэффициент интегрирования #define i_min -30.0//минимум И составляющей #define i_max 30.0//максимум И составляющей #define d_ctl 2.0//зона пропорциональности ust-d_ctl #define out_min 0//минимальный выходной % #define out_max 100//максимальный выходной % uint8_t out=0; static float i=0; float e, p; e=(ust-temp);//ошибка регулирования //расчет p p=(temp<ust-d_ctl)?p_max:(temp>ust)?p_min:(kP*e); //расчет i i=(i<i_min)?i_min:(i>i_max)?i_max:i+(kI*e); out=(p+i<out_min)?out_min:(p+i>out_max)?out_max:p+i; return out; } График выхода на уставку с 36С до 41С Синяя линия - текущая температура (С) Красная - уставка (С) Желтая - мощность (%) Зеленая - ошибка регулирования (С) Сиреневая - П составляющая (%) Голубая - И составляющая (%) Синяя - температура Красная - уставка Желтая - мощность