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.
- PID::PID(double* Input, double* Output, double* Setpoint,
- double Kp, double Ki, double Kd, int ControllerDirection)
- {
- PID::SetOutputLimits(0, 255);
- SampleTime = 100;
- PID::SetControllerDirection(ControllerDirection);
- PID::SetTunings(Kp, Ki, Kd);
- lastTime = millis()-SampleTime;
- inAuto = false;
- myOutput = Output;
- myInput = Input;
- mySetpoint = Setpoint;
- }
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:
- 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.
- void PID::Compute()
- {
- //Sprawdzenie wartosci zmiennej inAuto
- if(!inAuto) return;
- //Deklaracja zmiennej
- unsigned long now = millis();
- int timeChange = (now - lastTime);
- //Jeśli minął dłuższy czas niż czas przetwarzania
- //wtedy wykonaj funckję
- if(timeChange>=SampleTime)
- {
- //Obliczenie wszystkich błędów
- //Deklaracja zmiennych
- double input = *myInput;
- double error = *mySetpoint - input;
- //Obliczenia część całkująca
- ITerm+= (ki * error);
- //Jeśli wartość jest większa od maksymalnej
- //wtedy przypisz do niej wartość outMax
- if(ITerm > outMax) ITerm= outMax;
- else if(ITerm < outMin) ITerm= outMin;
- double dInput = (input - lastInput);
- //Obliczenie wartości wyjsciowej dla PID
- double output = kp * error + ITerm- kd * dInput;
- if(output > outMax) output = outMax;
- else if(output < outMin) output = outMin;
- *myOutput = output;
- //Zapisanie zmiennych do czasu ponownego wywołania funckji
- lastInput = input;
- lastTime = now;
- }
- }
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.
- void PID::SetMode(int Mode)
- {
- bool newAuto = (Mode == AUTOMATIC);
- if(newAuto == !inAuto)
- { //Zmiana z manualnego trybu pracy na automatyczny
- PID::Initialize();
- }
- inAuto = newAuto;
- }
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.
- void PID::SetOutputLimits(double Min, double Max)
- {
- if(Min >= Max) return;
- outMin = Min;
- outMax = Max;
- if(inAuto)
- {
- if(*myOutput > outMax) *myOutput = outMax;
- else if(*myOutput < outMin) *myOutput = outMin;
- if(ITerm > outMax) ITerm= outMax;
- else if(ITerm < outMin) ITerm= outMin;
- }
- }
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.
- void PID::SetTunings(double Kp, double Ki, double Kd)
- {
- //Jeśli któryś z czasów jest mniejszy od zera wtedy przerwij
- //wykonywanie programu
- if (Kp<0 || Ki<0 || Kd<0) return;
- //Przypisanie wartości wprowadzonych do
- //zmiennych przechowujących dane
- dispKp = Kp; dispKi = Ki; dispKd = Kd;
- //Wyznaczenie wartości czasu w sekundach
- double SampleTimeInSec = ((double)SampleTime)/1000;
- //Obliczenie poszczególnych członów
- kp = Kp;
- ki = Ki * SampleTimeInSec;
- kd = Kd / SampleTimeInSec;
- //Jeśli odwrotny tryb pracy, wtedy zmiana znaku dla
- //obliczonych wartości
- if(controllerDirection ==REVERSE)
- {
- kp = (0 - kp);
- ki = (0 - ki);
- kd = (0 - kd);
- }
- }
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.
- void PID::SetSampleTime(int NewSampleTime)
- {
- if (NewSampleTime > 0)
- {
- double ratio = (double)NewSampleTime
- / (double)SampleTime;
- ki *= ratio;
- kd /= ratio;
- SampleTime = (unsigned long)NewSampleTime;
- }
- }
SetControllerDirection()
Pozwala na ustawienie trybu pracy, direct lub reverse.
- void PID::SetControllerDirection(int Direction)
- {
- if(inAuto && Direction !=controllerDirection)
- {
- kp = (0 - kp);
- ki = (0 - ki);
- kd = (0 - kd);
- }
- controllerDirection = Direction;
- }
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.
- double PID::GetKp(){ return dispKp; }
- double PID::GetKi(){ return dispKi;}
- double PID::GetKd(){ return dispKd;}
- int PID::GetMode(){ return inAuto ? AUTOMATIC : MANUAL;}
- 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.
- #include <PID_v1.h>
- //Definicja zmiennych
- double Setpoint;
- double Input;
- double Output;
- //Definicja kolejnych zmiennych
- double Setpoint1;
- double Input1;
- double Output1;
- //Definicja wartosci nastaw dla poszczegolnych czlonow nowe PID
- const double Kp = 2;
- const double Ki = 5;
- const double Kd = 1;
- //Definicja wartosci nastaw dla poszczegolnych czlonow drugie PID
- const double Kp1 = 1;
- const double Ki1 = 7;
- const double Kd1 = 2;
- //Definicja poszczególnych parametrów
- PID nowePID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
- PID drugiePID(&Input1, &Output1, &Setpoint1, Kp1, Ki1, Kd1, DIRECT);
- void setup()
- {
- //Inicjalizacja portu szeregowego
- Serial.begin(9600);
- //Inicjalizacja zmiennej Input dla pinu analogowego
- Input = analogRead(0);
- Input1 = analogRead(1);
- //Wartosc jaka zostaje wprowadzona jako punkt wyzwalania
- Setpoint = 100;
- Setpoint1 = 200;
- //Wlaczenie automatycznego trybu ustawiania wartosci wyjsciowej
- nowePID.SetMode(AUTOMATIC);
- drugiePID.SetMode(AUTOMATIC);
- Serial.print("Wartosci nastaw nowePID- Setpoint: ");
- Serial.print(Setpoint);
- Serial.print(", Kp: ");
- Serial.print(Kp);
- Serial.print(", Ki: ");
- Serial.print(Ki);
- Serial.print(", Kd: ");
- Serial.println(Kd);
- Serial.print("Wartosci nastaw drugiePID- Setpoint: ");
- Serial.print(Setpoint1);
- Serial.print(", Kp: ");
- Serial.print(Kp1);
- Serial.print(", Ki: ");
- Serial.print(Ki1);
- Serial.print(", Kd: ");
- Serial.println(Kd1);
- }
- void loop()
- {
- //Odczytanie wartości z portu analogowego
- Input = map(analogRead(0), 0, 1023, 0, (Setpoint+50));
- Input1 = map(analogRead(1), 0, 1023, 0, (Setpoint1+50));
- //Wyswietlenie wartosci z ADC
- Serial.print("Wartosc z ADC: ");
- Serial.print(Input);
- Serial.print(" , ");
- Serial.println(Input1);
- //Dokonanie obliczeń
- nowePID.Compute();
- drugiePID.Compute();
- //Wyświetlenie wartosci wyjsciowej
- Serial.print("Wartosc wyjsciowa: ");
- Serial.print(Output);
- Serial.print(" , ");
- Serial.println(Output1);
- //Przypisanie wartosci do PWM
- analogWrite(3, Output);
- analogWrite(5, Output1);
- //Petla opóźniająca
- delay(1000);
- }
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.
- #include <OneWire.h>
- #include <DallasTemperature.h>
- #include <Wire.h>
- #include <PID_v1.h>
- //Linia danych podpięta jest pod pin 2 Ardiuno
- #define ONE_WIRE_BUS 2
- //Definicja zmiennych
- double Setpoint;
- double Input;
- double Output;
- //Definicja kolejnych zmiennych
- double Setpoint1;
- double Input1;
- double Output1;
- //Definicja wartosci nastaw dla poszczegolnych czlonow nowe PID
- const double Kp = 2;
- const double Ki = 5;
- const double Kd = 1;
- PID nowePID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
- OneWire oneWire(ONE_WIRE_BUS);
- //Przekazanie danych dotyczących One Wire do DallasTemperature
- DallasTemperature sensors(&oneWire);
- void setup()
- {
- //Inicjalizacja portu szeregowego
- Serial.begin(9600);
- //Włączenie czujnika i biblioteki
- sensors.begin();
- //Wysłania rządania pobrnaia temperatury
- sensors.requestTemperatures();
- //Inicjalizacja zmiennej Input wartością odczytanej temperatury
- Input = sensors.getTempCByIndex(0);
- //Wartosc jaka zostaje wprowadzona jako punkt wyzwalania
- Setpoint = 30;
- //Wlaczenie automatycznego trybu ustawiania wartosci wyjsciowej
- nowePID.SetMode(AUTOMATIC);
- Serial.print("Wartosci nastaw nowePID- Setpoint: ");
- Serial.print(Setpoint);
- Serial.print(", Kp: ");
- Serial.print(Kp);
- Serial.print(", Ki: ");
- Serial.print(Ki);
- Serial.print(", Kd: ");
- Serial.println(Kd);
- nowePID.SetControllerDirection(REVERSE);
- }
- void loop()
- {
- sensors.requestTemperatures();
- Input = sensors.getTempCByIndex(0);
- Serial.print("Temp: ");
- Serial.println(Input);
- nowePID.Compute();
- Serial.print("PWM: ");
- Serial.println(Output);
- analogWrite(3, Output);
- //Petla opóźniająca
- delay(1000);
- }
W miarę wzrostu temperatury, wartość PWM sterującego wzrasta. Cały program można dodatkowo wyposażyć w układ załączania grzania.