sobota, 23 stycznia 2016

[21] Arduino - Regulator PID

Tym razem przedstawię sposób wykonania regulatora PID w oparciu o płytkę Arduino Uno. Całość została opracowana z pomocą biblioteki PID.

[Źródło: https://store.arduino.cc/usa/arduino-uno-rev3]


Biblioteka


Dostępne są następujące zdefiniowane funkcje:

PID()


Jest to dokładnie konstruktor, który zostaje zdeklarowany poprzez podanie jego nazwy oraz poszczególnych wartości. Poniżej przedstawiłem wygląd jego definicje w bibliotece.

  1. PID::PID(double* Input, double* Output, double* Setpoint,
  2.         double Kp, double Ki, double Kd, int ControllerDirection)
  3. {
  4.     PID::SetOutputLimits(0255);            
  5.  
  6.     SampleTime = 100;
  7.     PID::SetControllerDirection(ControllerDirection);
  8.     PID::SetTunings(Kp, Ki, Kd);
  9.  
  10.     lastTime = millis()-SampleTime;    
  11.     inAuto = false;
  12.     myOutput = Output;
  13.     myInput = Input;
  14.     mySetpoint = Setpoint;
  15.  
  16. }

Pierwsze trzy parametry dotyczą wartości jakie zostaną odczytane z wejścia wyjścia oraz jaka wartość jest oczekiwana. Następnie definiowane są poszczególne składowe proporcjonalna (Kp), całkująca(Kd), oraz różniczkująca (Kd). W ostatniej części zdefiniowany jest sposób działania układu. Można wyróżnić dwa rodzaje tego parametru DIRECT, czyli wartość wyjściowa zostaje zwiększana gdy błąd będzie dodatni. Drugim parametrem jest wartość REVERSE, czyli parametr będzie zwiększany gdy błąd będzie ujemny,

Następnie zdefiniowany został parametr wartości wyjściowej od 0 do 255. Jest on zależny od maksymalnej dopuszczalnej wartości PWM. Dalej ustawiono czas próbkowania na 0.1 sekundy. 

Całość deklarujemy od wpisania nazwy konstruktora, patem nazwy nowo zdeklarowanego elementu. Następnie w nawiasach podawane są wartości dla poszczególnych zmiennych. Deklaracja może wyglądać tak jak w przykładzie poniżej:

  1. PID nowePID(&Input, &Output, &SetPoint, Kp, Ki, Kd, DIRECT);

Każdy z poszczególnych wartości członów Kp, Ki oraz Kd spełnia określoną funkcję i jest ze sobą w pewien sposób połączony. Kp czyli członu proporcjonalnego wykorzystuje się w celu zmniejszenia czasu narastania oraz zmniejszenia błędu prze-regulowania. Członu całkującego Ki używa się aby zminimalizować błąd, który stopniowo zwiększa sie w trakcie wykonywania programu i regulacji nastaw. Ostatni człon różniczkujący Kd, jest wykorzystywany aby zredukować odchylenie od wartości zakładanej, oraz w celu zmniejszenia czasu jaki jest potrzebny na ustabilizowanie przebiegu.


Compute()



W tej funkcji znajduje się główny algorym PID. Przeprowadza on obliczenia poszczególnych cześci co określoną wartość czasu. Powinien być wywołany w każdej pętli.

Funkcje Compute przedstawiłem poniżej wraz z wymaganym komentarzem.

  1. void PID::Compute()
  2. {
  3.    //Sprawdzenie wartosci zmiennej inAuto
  4.    if(!inAuto) return;
  5.    //Deklaracja zmiennej
  6.    unsigned long now = millis();
  7.    int timeChange = (now - lastTime);
  8.    //Jeśli minął dłuższy czas niż czas przetwarzania
  9.    //wtedy wykonaj funckję
  10.    if(timeChange>=SampleTime)
  11.    {
  12.       //Obliczenie wszystkich błędów
  13.       //Deklaracja zmiennych
  14.       double input = *myInput;
  15.       double error = *mySetpoint - input;
  16.       //Obliczenia część całkująca
  17.       ITerm+= (ki * error);
  18.       //Jeśli wartość jest większa od maksymalnej
  19.       //wtedy przypisz do niej wartość outMax
  20.       if(ITerm > outMax) ITerm= outMax;
  21.       else if(ITerm < outMin) ITerm= outMin;
  22.       double dInput = (input - lastInput);
  23.  
  24.       //Obliczenie wartości wyjsciowej dla PID
  25.       double output = kp * error + ITerm- kd * dInput;
  26.  
  27.       if(output > outMax) output = outMax;
  28.       else if(output < outMin) output = outMin;
  29.       *myOutput = output;
  30.  
  31.       //Zapisanie zmiennych do czasu ponownego wywołania funckji
  32.       lastInput = input;
  33.       lastTime = now;
  34.    }
  35. }

W pierwszej linijce sprawdzana jest wartość ustawiona zmiennej bool inAuto. Jeśli jest to false wtedy następuje wyjście z funkcji. Dopiero gdy ta wartość będzie prawdziwa to nastąpi wykonanie całości. Czyli gdy został wybrany tryb automatyczny.

SetMode()


Funkcja ta pozwala na wybranie trybu pracy układu. Do wyboru jest automatyczny lub manualny.

  1. void PID::SetMode(int Mode)
  2. {
  3.     bool newAuto = (Mode == AUTOMATIC);
  4.     if(newAuto == !inAuto)
  5.     {   //Zmiana z manualnego trybu pracy na automatyczny
  6.         PID::Initialize();
  7.     }
  8.     inAuto = newAuto;
  9. }

Domyślnie wybierany jest tryb manualny. Dopiero po wywołaniu funkcji z odpowiednim parametrem nastąpi zmiana trybu pracy na automatyczny. Dla trybu automatycznego przypisana została 1 lub AUTOMATIC, dla manualnego 0 oraz MANUAL. 


SetOutputLimits()



Pozwala na definiowanie wartości granicznych przedziału. Domyślnie przypisana jest wartość od 0 do 255. Ten parametr zależny jest od PWM, który może przyjmować wartości takie jak domyślne. Limit ten można zmniejszyć, natomiast zwiększenie nie spowoduje żadnego efektu.

  1. void PID::SetOutputLimits(double Min, double Max)
  2. {
  3.    if(Min >= Max) return;
  4.    outMin = Min;
  5.    outMax = Max;
  6.  
  7.    if(inAuto)
  8.    {
  9.     if(*myOutput > outMax) *myOutput = outMax;
  10.     else if(*myOutput < outMin) *myOutput = outMin;
  11.  
  12.     if(ITerm > outMax) ITerm= outMax;
  13.     else if(ITerm < outMin) ITerm= outMin;
  14.    }
  15. }

SetTunings()


Pozwala na zdefiniowanie dynamiki zmian kontrolera PID, dokładnie chodzi o szybkość zmian czy występowania oscylacji. Jako parametry podawane są poszczególne wartości dla odpowiednich członów PID.

  1. void PID::SetTunings(double Kp, double Ki, double Kd)
  2. {
  3.    //Jeśli któryś z czasów jest mniejszy od zera wtedy przerwij
  4.    //wykonywanie programu
  5.    if (Kp<0 || Ki<0 || Kd<0) return;
  6.  
  7.    //Przypisanie wartości wprowadzonych do
  8.    //zmiennych przechowujących dane
  9.    dispKp = Kp; dispKi = Ki; dispKd = Kd;
  10.  
  11.    //Wyznaczenie wartości czasu w sekundach
  12.    double SampleTimeInSec = ((double)SampleTime)/1000;
  13.  
  14.    //Obliczenie poszczególnych członów
  15.    kp = Kp;
  16.    ki = Ki * SampleTimeInSec;
  17.    kd = Kd / SampleTimeInSec;
  18.  
  19.    //Jeśli odwrotny tryb pracy, wtedy zmiana znaku dla
  20.    //obliczonych wartości
  21.    if(controllerDirection ==REVERSE)
  22.    {
  23.       kp = (0 - kp);
  24.       ki = (0 - ki);
  25.       kd = (0 - kd);
  26.    }
  27. }

SetSampleTime()


Pozwala na ustawienie okresu w milisekundach dla którego przeprowadzane są obliczenia. Funkcja ustawiająca ten czas wygląda następująco. Domyślnie ustawiona jest wartość 0.1 ms.

  1. void PID::SetSampleTime(int NewSampleTime)
  2. {
  3.    if (NewSampleTime > 0)
  4.    {
  5.       double ratio  = (double)NewSampleTime
  6.                       / (double)SampleTime;
  7.       ki *= ratio;
  8.       kd /= ratio;
  9.       SampleTime = (unsigned long)NewSampleTime;
  10.    }
  11. }

SetControllerDirection()


Pozwala na ustawienie trybu pracy, direct lub reverse.

  1. void PID::SetControllerDirection(int Direction)
  2. {
  3.    if(inAuto && Direction !=controllerDirection)
  4.    {
  5.    kp = (0 - kp);
  6.       ki = (0 - ki);
  7.       kd = (0 - kd);
  8.    }
  9.    controllerDirection = Direction;
  10. }

Display Functions


Ostatnim elementem tej biblioteki są funkcje pozwalające na wyświetlanie poszczególnych ustawień kontrolera takich jak: tryb pracy, kierunek, czy poszczególne wartości nastaw.

  1. double PID::GetKp(){ return  dispKp; }
  2. double PID::GetKi(){ return  dispKi;}
  3. double PID::GetKd(){ return  dispKd;}
  4. int PID::GetMode(){ return  inAuto ? AUTOMATIC : MANUAL;}
  5. int PID::GetDirection(){ return controllerDirection;}

Przykłady


Ta część będzie zawierała szybkie przykłady obrazujące sposób działania PID w oparciu o udostępnioną bibliotekę.

Program 1


Pierwszy program będzie modyfikacją przykładu basic, który został dołączony do biblioteki PID. Odczytywane będą wartości z dwóch pinów analogowych. Na podstawie tych wartości będą ustawiane parametry sygnału PWM generowanego na pinie 3 oraz 5.

  1. #include <PID_v1.h>
  2.  
  3. //Definicja zmiennych
  4. double Setpoint;
  5. double Input;
  6. double Output;
  7.  
  8. //Definicja kolejnych zmiennych
  9. double Setpoint1;
  10. double Input1;
  11. double Output1;
  12.  
  13. //Definicja wartosci nastaw dla poszczegolnych czlonow nowe PID
  14. const double Kp = 2;
  15. const double Ki = 5;
  16. const double Kd = 1;
  17.  
  18. //Definicja wartosci nastaw dla poszczegolnych czlonow drugie PID
  19. const double Kp1 = 1;
  20. const double Ki1 = 7;
  21. const double Kd1 = 2;
  22.  
  23. //Definicja poszczególnych parametrów
  24. PID nowePID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
  25. PID drugiePID(&Input1, &Output1, &Setpoint1, Kp1, Ki1, Kd1, DIRECT);
  26.  
  27. void setup()
  28. {
  29.   //Inicjalizacja portu szeregowego
  30.   Serial.begin(9600);
  31.  
  32.   //Inicjalizacja zmiennej Input dla pinu analogowego
  33.   Input = analogRead(0);
  34.   Input1 = analogRead(1);
  35.  
  36.   //Wartosc jaka zostaje wprowadzona jako punkt wyzwalania
  37.   Setpoint = 100;
  38.   Setpoint1 = 200;
  39.  
  40.   //Wlaczenie automatycznego trybu ustawiania wartosci wyjsciowej
  41.   nowePID.SetMode(AUTOMATIC);
  42.   drugiePID.SetMode(AUTOMATIC);
  43.  
  44.   Serial.print("Wartosci nastaw nowePID- Setpoint: ");
  45.   Serial.print(Setpoint);
  46.   Serial.print(", Kp: ");
  47.   Serial.print(Kp);
  48.   Serial.print(", Ki: ");
  49.   Serial.print(Ki);
  50.   Serial.print(", Kd: ");
  51.   Serial.println(Kd);
  52.  
  53.   Serial.print("Wartosci nastaw drugiePID- Setpoint: ");
  54.   Serial.print(Setpoint1);
  55.   Serial.print(", Kp: ");
  56.   Serial.print(Kp1);
  57.   Serial.print(", Ki: ");
  58.   Serial.print(Ki1);
  59.   Serial.print(", Kd: ");
  60.   Serial.println(Kd1);
  61. }
  62.  
  63. void loop()
  64. {
  65.   //Odczytanie wartości z portu analogowego
  66.   Input = map(analogRead(0)010230(Setpoint+50));
  67.   Input1 = map(analogRead(1)010230(Setpoint1+50));
  68.  
  69.   //Wyswietlenie wartosci z ADC
  70.   Serial.print("Wartosc z ADC: ");
  71.   Serial.print(Input);
  72.   Serial.print("  , ");
  73.   Serial.println(Input1);
  74.  
  75.   //Dokonanie obliczeń
  76.   nowePID.Compute();
  77.   drugiePID.Compute();
  78.  
  79.   //Wyświetlenie wartosci wyjsciowej
  80.   Serial.print("Wartosc wyjsciowa: ");
  81.   Serial.print(Output);
  82.   Serial.print("  , ");
  83.   Serial.println(Output1);
  84.  
  85.   //Przypisanie wartosci do PWM
  86.   analogWrite(3, Output);
  87.   analogWrite(5, Output1);
  88.  
  89.   //Petla opóźniająca
  90.   delay(1000);
  91. }

Poniżej przedstawię algorytm dokonywanych obliczeń w funkcji compute. Ta część będzie rozwinięciem wcześniejszego opisu.

  • Do wartości input zostanie przypisana wartość odczytana z ADC.
  • Zmienna error dostaje wartość różnicy pomiędzy daną wyzwalaną a wejściową.
  • Zmienna Iterm zostaje powiększona o wartość członu całkującego wymnożonego z otrzymaną wartością błędu.
  • Sprawdzenie czy wartość obliczona w punkcie c, jest większa od największej zdeklarowanej wartości (domyślnie 255). Jeśli tak to zostaje ona zamieniona na maksymalną dopuszczalną wartość.
  • Jeśli Iterm jest mniejsze od dopuszczalnej minimalnej wartości, wtedy zostaje przypisana do tej zmiennej najmniejsza dopuszczalna wartość.
  • W kolejnym kroku obliczona zostaje różnica pomiędzy wartością wejściową zmierzoną, a tą zapamiętaną z poprzednich pomiarów.
  • Następnie dokonywane jest obliczenie wartości wyjściowej. W tym celu wykorzystywany jest następujący wzór:

        Wartość wyjściowa = Kp * Błąd + ITerm - Kd * dInput

         Gdzie:   Kp – Wzmocnienie części proporcjonalnej, Kd – wzmocnienie części różniczkującej, dInput różnica pomiędzy wartością wejściową zmierzoną a poprzednią.

  • Jeśli wartość obliczona ze wzoru będzie większa niż dopuszczalna maksymalna wartość, wtedy zostaje ona zastąpiona. Tak samo dzieje się w przypadku gdy będzie ona poniżej wartości minimalnej.
  • Przypisanie do zmniennej wartości wyjściowej.
  • Zapamiętanie zmiennych z odbytego cyklu pomiarowego.

Program 2


Ten program będzie odczytywał wartości z czujnika temperatury DS18B20. W zależności od wartości jaka zostanie pobrana nastąpi regulacja wartości PWM, która będzie odpowiadała za sterowanie wiatrakiem komputerowym. 

Wiatrak potrzebuje większego prądu oraz napięcia niż takie jakie może zapewnić arduino. W związku z tym należy zastosować zewnętrzny zasilacz. Masy zasilacza oraz mikrokontrolera muszą być ze sobą połączone.

Poniżej przedstawiam pełny kod programu wraz z komentarzem.

  1. #include <OneWire.h>
  2. #include <DallasTemperature.h>
  3. #include <Wire.h>
  4. #include <PID_v1.h>
  5.  
  6. //Linia danych podpięta jest pod pin 2 Ardiuno
  7. #define ONE_WIRE_BUS 2
  8.  
  9. //Definicja zmiennych
  10. double Setpoint;
  11. double Input;
  12. double Output;
  13.  
  14. //Definicja kolejnych zmiennych
  15. double Setpoint1;
  16. double Input1;
  17. double Output1;
  18.  
  19. //Definicja wartosci nastaw dla poszczegolnych czlonow nowe PID
  20. const double Kp = 2;
  21. const double Ki = 5;
  22. const double Kd = 1;
  23.  
  24. PID nowePID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
  25. OneWire oneWire(ONE_WIRE_BUS);
  26.  
  27. //Przekazanie danych dotyczących One Wire do DallasTemperature
  28. DallasTemperature sensors(&oneWire);
  29.  
  30. void setup()
  31. {
  32.   //Inicjalizacja portu szeregowego
  33.   Serial.begin(9600);
  34.  
  35.   //Włączenie czujnika i biblioteki
  36.   sensors.begin();
  37.  
  38.   //Wysłania rządania pobrnaia temperatury
  39.   sensors.requestTemperatures();
  40.  
  41.   //Inicjalizacja zmiennej Input wartością odczytanej temperatury
  42.   Input = sensors.getTempCByIndex(0);
  43.  
  44.   //Wartosc jaka zostaje wprowadzona jako punkt wyzwalania
  45.   Setpoint = 30;
  46.  
  47.   //Wlaczenie automatycznego trybu ustawiania wartosci wyjsciowej
  48.   nowePID.SetMode(AUTOMATIC);
  49.  
  50.   Serial.print("Wartosci nastaw nowePID- Setpoint: ");
  51.   Serial.print(Setpoint);
  52.   Serial.print(", Kp: ");
  53.   Serial.print(Kp);
  54.   Serial.print(", Ki: ");
  55.   Serial.print(Ki);
  56.   Serial.print(", Kd: ");
  57.   Serial.println(Kd);
  58.  
  59.   nowePID.SetControllerDirection(REVERSE);
  60. }
  61.  
  62. void loop()
  63. {
  64.     sensors.requestTemperatures();
  65.     Input = sensors.getTempCByIndex(0);
  66.     Serial.print("Temp:   ");
  67.     Serial.println(Input);
  68.  
  69.     nowePID.Compute();
  70.  
  71.     Serial.print("PWM:    ");
  72.     Serial.println(Output);
  73.  
  74.     analogWrite(3, Output);
  75.  
  76.     //Petla opóźniająca
  77.     delay(1000);
  78. }

W miarę wzrostu temperatury, wartość PWM sterującego wzrasta. Cały program można dodatkowo wyposażyć w układ załączania grzania.