ПИ регулятор температуры

Тема в разделе "Глядите, что я сделал", создана пользователем yul-i-an, 16 июн 2016.

  1. yul-i-an

    yul-i-an Гик

    Регулятор был задуман как замена штатного в электроконвектор на дачу, потому что встроенный не обеспечивал необходимой точности поддержания температуры и при перебоях в питании самостоятельно не включался в работу, что могло нехорошо отразится на заполненном водопроводе.

    Данный регулятор обеспечивает поддержание заданной температуры.

    Проработал зиму на даче, никаких замечаний к работе регулятора нет, температуру держет точно.

    В качестве датчика температуры используется терморезистор NTC 10кОм при 20С, выходным устройством являеться реле (в дальнейшем планирую поставить симистор) (организован медленный ШИМ).
    [​IMG]
    Схема включения терморезистора стандартная, кнопки (аналоговые). Из скетча должно быть понятно что куда цеплять. У меня девайс выполнен на своей плате, использован микроконтроллер ATMega8, тактирование от встроенного генератора 8МГц. Собрано всё в монтажной коробке 100х100х40.
    [​IMG]
    Схема устройства LCD и аналоговые кнопки не показаны.
    [​IMG]
    На данный момент схема немного изменена в части питания терморезистора (он запитан от шины +5В а не от пина микроконтроллера как в первой версии)
    [​IMG]
    Устройство проработало на ниже представленном коде.
    Код (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);
    }
     
    Устройство в работе показало себя отлично. Температуру держало точно.
     
  2. yul-i-an

    yul-i-an Гик

    На данный момент переписал код, добавил сохранение уставки в 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. Наладить двухстороннюю связь с народным мониторингом.
     
  3. yul-i-an

    yul-i-an Гик

    Немного обновил функцию ПИ регулятора. Теперь настройка ПИ регулятора стала более удобной.
    Код (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; поясню
    (условие)? если истина : если лож ;
     
    Последнее редактирование: 22 июн 2016
    KellaKilla нравится это.
  4. yul-i-an

    yul-i-an Гик

    Немного переписал функцию расчета мощности
    Код (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С
    Синяя линия - текущая температура (С)
    Красная - уставка (С)
    Желтая - мощность (%)
    Зеленая - ошибка регулирования (С)
    Сиреневая - П составляющая (%)
    Голубая - И составляющая (%)

    [​IMG]


    Синяя - температура
    Красная - уставка
    Желтая - мощность
    [​IMG]
     
    Последнее редактирование: 6 июл 2016
    Nikolai1972, ИгорьК, arkadyf и ещё 1-му нравится это.