PID - jeszcze raz

Jan 22, 2026 Last reply: 5 miesięcy temu 59 Replies

Tylko nie wiem, czy zauważasz, że ładne działanie z 2:05 to on ma przy Kp=500, a kolejne oscylacje są przy Kp=10tys, to jest na mój gust spory przeskok parametrów. Na oko to jest tam dużo miejsca na dostrojenie.

No a to ostatnie pokazuje, że częstotliwość przeliczania wewnętrzego też istotna.

Hm, czy to wystarczy? Bo będzie prędkośc silnika większa, przesunie się np o 10 kroków w cyklu, z -5 zrobi się +5, i znów prędkość nie będzie minimalna. No dobra - cykl przetwarzania 200Hz, 5ms, to nie wiem, czy silnik da radę tak szybko zmienić kierunek ruchu.

No i .. jak częstotliwość duża, to różniczkowanie trzeba przemyśleć, bo różnica kolejnych odczytów, to będzie raczej typu 0, 1, 0, 0, 0 0,

0, 1, 0, 0, 0, 0, 0, 1.

A co to za sprzęt? Bo pod unixem sprzęt przecież chroniony przed uytkownikiem.

Wydajnio PIO może pozostać kiepska.

A enkoder jak obsługujesz? Bo tam łatwo przekroczyć możliwosci wolneog programu

J.

W dniu 27.01.2026 o 13:16, J.F pisze:

No więc jak to jest realizowane? Magia?

Nie ma tam żadnego Unixa. Raspberry Pi Pico. Przecież napisałem na początku.

Nie ma szans. Pisałem, że większość czasu zajmuje przeczytanie rejestru z wartością enkodera. To jest po prostu skopane w microPythonie. Reszta obliczeń zajmuje tyle co nic.

Pisałem, że program w asemblerze w PIO. On działa sobie zupełnie niezależnie od głównego rdzenia procesora i nie gubi żadnych kroków i nie ma żadnego wpływu na program w Pythonie.

Ciężko się z tobą pisze, ile razy można pisać to co było już napisane. Może nie ciąć w ogóle wypowiedzi? Będziesz czytał całość?

W dniu 27.01.2026 o 13:16, J.F pisze:

No właśnie nie ma. Przy Kp=10tyś powinno być przetłumienie, czyli powinien "hamować" o wiele za wcześnie i wolniutko dochodzić prawie do celu. Ale nie da się. Masz niedotłumiony, czyli przerzut jest nieakceptowalny, zwiększasz... zmniejsza się przerzut, ale nadal jest, zwiększasz dalej i zanim zniknie przerzut pojawiają się te drgania. To, że w miarę to wygląda przy 2:05 to jest tylko złudzenie. Gdyby było dobrze to by nie szukał więcej. U mnie też to tak wygląda niekiedy, ale jak zwiększę dystans to wychodzi znów przerzut. Przy dużo mniejszym wzmocnieniu da się wytłumić, ale znów błąd ustawiania jest ogromny.

Przy wysokim Kp układ się wzbudza i oscyluje - takie jest ogólne prawo automatyki :-)

I widziałes reguły Z-N - doprowadź układ do oscylacji w trybie P, po czym zmniejsz z Kp do 0.6 wartości krytycznej.

Trudno powiedzieć, czy tu Kp=10k jest wartością krytyczną, czy o wiele za dużo. Niby wystarczyło zwiększyć częstotliwość obliczen, i przestało oscylować, ale może wprowadziło to znacznie więcej zmian, niż się wydaje. No i oscylacje mierzymy tylko z członem P, bez ID. A tu były włączone.

J.

Trzeba trochę popracować nad programem. Zsumować z dłuższego okresu, nałożyć jakiś filtr uśredniający ...

OK.

Jeśli to jakiś rejestr uC, to dziwne, że sobie nie poradzili z szybkim czytaniem. No i czy Pico nie jest ogólnie jakiś powolny, biorąc pod uwagę, że program jest pamięci szeregowej ?

Przegapiłem "Pico", a wtedy PIO niewiele mówi.

J.

W dniu 27.01.2026 o 23:02, J.F pisze:

Zgadza się, gość zwiększył kp i zapewne miał przerzut i oscylacje i zaczął zwiększać kd żeby wytłumić i zaczęły się te drgania. Zupełnie tak jak u mnie.

Ja już przeskoczyłem i pisałem o kp a myślałem o kd.

Ale tu znalazłem ciekawy przykład:

formatting link
gość to robi zdecydowanie szybciej i coś z tego wychodzi. Jest nawet prawie cały kod widoczny - będzie się czym podpierać. Potem ma jeszcze drugi film z większym silnikiem.

Się czepiasz :) analizuje czy zbiera błędy z poprzednich kroków.

po prostu realizuje funkcje P,

To są te wyprzedzające.

No nie, zobacz w program, każdy człon ma swój rejestr w którym są najczęściej sumowane odchyłki. Potem te rejestry pomnożone przez współczynniki wzmocnienia każdego członu są sumowane i suma po normalizacji stanowi wynik pid-a. Więc nie można mówić tylko o jednej całce.

Ale w każdym kroku może go korygować, a że kroki coraz mniejsze to i korekcja dąży do 0.

na razie piszę tylko o automatycznym strojeniu - takie też są.

Bo tak im wygodniej przeliczać parametry?

To masz tu prosty przykładowy

formatting link

error = self.setpoint - input_ self._proportional = self.Kp * error #to nie wymaga żadnej historii

d_input = input_ - (self._last_input if (self._last_input is not None) else input_)

w skrócie d_input = input_ - self._last_input

self._derivative = -(self.Kd / self.unit) * d_input / dt # to wymaga poprzedniej wartośći.

self._integral += (self.Ki * self.unit) * error * dt self._integral = _clamp(self._integral, self.output_limits) # Avoid integral windup

#a to istotnie "zbiera" wartosć całki z poprzednich różnic.

output = self._proportional + self._integral + self._derivative

No chyba, że powyzsze sumowanie nazwiesz "zbieraniem".

Ale:

-kto powiedział, że całość wykona ten ostatni mały krok? wyjscie regulatora P może być za słabe, aby tarcie pokonać

-a co będzie, jak będzie jakies stałe obciążenie? Np winda z pasazerami? Trzeba podać jakies prąd/napięcie na silnik, aby winda stała, a regulator P bedzie miał niezerowe wyjscie, o ile błąd jest niezerowy.

Po to jest człon I.

J.

Załózmy, że pozycja wirnika odczytana w kolejnych pętlach obiegu regulatora jest

5 5 6 6 6 6 7 7 7 7 8 8 8 8 9 9 9 9 ...

czy to znaczy, że silnik sie kręci "skokami do przodu" ?

Niekoniecznie, może po prostu sie wolno kręci, lub regulator szybko działa i ma dużo petli na sekunde.

jak policzysz z tego pochodną metodą "nowa wartośc - poprzednia wartość", to wyjdzie

0 0 1 0 0 0 1 0 0 0 1 0 0 0 1 0 0 ...

no i to może byc niezbyt prawdziwe. A jak to regulator uwzględni, to istotnie wyjdą jakieś skoki.

Z drugiej strony - może ułatwic zatrzymanie po osiągnięciu pozycji, bo zapewne spowalnia napęd po każdym "kroku enkodera".

Z tego co mówi, to:

- różniczkowanie "wzmacnia" wysokie częstotliwości,

- on się boi wzbudzneania

Może nie jest potrzebny tak naprawde ... silnik dobrze filtruje :-)

A to zależy. Jakie mniej więcej masz tam parametry - ile kroków na obrót ma enkoder, jak szybki jest silnik, jakiej prędkości wymagasz tak jak na tym pierwszym filmie, że silnik wraca do położenia zadanego w mniej niż ~30 ms ?

Zwiększyć Ki ?

tu masz inny efekt - przez długi czas jest odchyłka, nawet duza, i całka rosnie. potrzebujesz tego "anti-windup", albo - zadawać rosnące pozycje, nie od razu 80k,

Albo niesymetria regulatora ... choć H-bridge powinieni być symetryczny.

J.

W dniu 31.01.2026 o 16:50, J.F pisze:

Ja się nie spodziewałem ale gość pokazuje, że mu działa.

Nie, to nie to. Wydaje mi się, że samo StateMachine.get() czyta z bufora o ile jest napełniony, jak jest pusty to czeka. Natomiast czemu StateMachine.exec("in_(x, 32)") nie napełnia go natychmiast to nie wiem. Sytuację trochę poprawiło umieszczenie tego w pętli kilka instrukcji wcześniej, ale na tym zaoszczędziłem niewiele. Obecny kod zmienia to radykalnie: bufor sam się napełnia jak jest zmiana, jak jest napełniony to czytam, jak nie to znaczy że nie było zmiany.

Tak, myślałem o: Integral = poprzednie_Integral + błąd*Ki i na końcu: I = Ki * Integral

Nie, bo z obliczonego wcześniej float robię int-a.

PWM jest 16-bitowe czyli 64k

A przy okazji - gość od tych filmików ma tam błąd, bo kierunek silnika uzależnia od znaku błędu, co zwykle zadziała ale nie zawsze. Ja ustawiam w zależności od znaku wyniku całego PID.

Przychodzi mi do głowy jeszcze jedna metoda, mianowicie jeśli sterowanie osiągnie maksimum to zerować wartość I. No bo tutaj nic nie ma do roboty

- silnik kręci się z maksymalną prędkością i nie ważne jak to długo trwa, póki samo P daje maksimum to nie ma co zbierać błędu. Może głupie, ale sprawdzę jak to się zachowa.

Tak prawdę mówiąc, to mało pokazuje. Jak masz np enkoder 2000 impulsów na obrót i do 200 obr/s, to spora częstotliwość wychodzi.

A jak np 180 imp/obr i maks 5 obr/s, to może i Python sobie poradzi.

Prawdopodobne. A jak tak długo czeka i wstrzymuje pętlę regulatora, to cuda mogą być. ale masz to rx_fifo().

Ja się tam jednak podejrzliwie patrzę na ten parametr. Jego dopiero trzeba zdekodować, co może być trudne lub łatwiejsze.

Ale ale - dokumentacja podaje, że można podać int zawierający gotowy kod instrukcji PIO.

formatting link

Nadal wydaje mi sie, że masz Ki podwójnie.

Werja 1 calka = calka+blad I = Ki* calka

wersja 2 calka - calka+blad*Ki I = calka

Pytanie, czy powinieneś tam uwzględniać krok czasu, czy nie trzeba, bo stały.

Ale wartość całki sumujesz na float?

Ale to własnie to wewnętrzną sumę całki chcesz ograniczyc, bo bedzie się długo utrzymywał błąd, wartość(suma) całki urośnie np do miliona, Ty ograniczysz wyjsciową wartość I ... ale ta wewnętrzna wartość całki bedzie potem dłuuugo wracała do sensownej wartości.

Ogólnie tak właśnie powinieś robić.

Pytanie, czy on tam aktywnie hamuje silnikiem, bo jak unika, to może mu działa - albo sprawia takie wrażenie. Bo nie pokazuje wykresu błędu :-)

No i przypadek dużego obciążenia zewnętrznego - powiedzmy, że jakiś cięzar czy sprężyna chce kręcić silnikiem w prawo, kontroler musi podawać prąd chcący kręcic w lewo, i to w miarę niezależnie od błędu.

Też bym o takim czymś myślał, bo jak silnik się kręci szybko w jedną stronę i nie nadąża, to nie ma co błędu całkować.

Ewentualnie ... to co Janusz pisał - nie skacz zadaną pozycją o dużą wartość, tylko zadawaj mu stosowną trajektorię - stopniowy przyrost pozycji, i jeszcze stopniowy wzrost i zmniejszenie prędkości. Regulator będzie to naśladował, i błąd ma szanse być niewielki ....

J.

W dniu 31.01.2026 o 19:40, J.F pisze:

Możliwe, że coś to przyspieszy, ale w tym momencie już bez znaczenia dla mnie, bo nie używam tej metody.

A "I = Ki*(poprzednie_I + błąd*Ki)" - lepiej?

Przecież w poprzednie_I już jest Ki a tak ma właśnie gościu na filmiku.

No, czyli moja pierwsza wersja na dwa sposoby.

No krok jest stały, ale jeśli chcielibyśmy stosować reguły Z-N to chyba trzeba.

Ja chcę zrobić regulator uniwersalny - chcę żeby mi to działało tak jak na tym pierwszym filmiku, który podlinkowałem. Widziałem jeszcze lepszy filmik, ale nie mogę go znaleźć, gdzie gość trzymał w ręce taki niewielki silnik z takim jakby patyczkiem jakby wskazówką. Gość wytrącał go palcem a ten wracał do pozycji jakby był na gumce i o coś uderzał. Potem zrobił kilka obrotów palcem i to samo - patyczek po prostu pojawiał się natychmiast na pozycji.

Paradoksalnie najgorzej mi się to zachowuje jak powoli zmieniam nastawę

- tzn, w pętli dodaję mu do pozycji stałą wartość - powinien jechać ze stałą prędkością a "kołysze" tą prędkością jak tylko trochę zwiększę Ki. Jest taki moment, że wydaje się, że już szybciej dociąga do celu i jest stabilny przy gwałtownych zmianach nastawy a przy powolnych zmianach jednak się kołysze.

Mam pid-a na avr-a i tam jest wg 1 bo po sumowanu trzeba sprawdzić czy nie wyszło poza zakres a potem dopiero mnożyć przez współczynnik. Funkcja cała pid-u tak wygląda

#include "pid.h" #include "stdint.h"

/*! \brief Initialisation of PID controller parameters. * * Initialise the variables used by the PID algorithm. * * \param p_factor Proportional term. * \param i_factor Integral term. * \param d_factor Derivate term. * \param pid Struct with PID status. */ void pid_Init(int16_t p_factor, int16_t i_factor, int16_t d_factor, struct PID_DATA *pid) // Set up PID controller parameters { // Start values for PID controller pid->sumError = 0; pid->lastProcessValue = 0; // Tuning constants for PID loop pid->P_Factor = p_factor; pid->I_Factor = i_factor; pid->D_Factor = d_factor; // Limits to avoid overflow pid->maxError = MAX_INT / (pid->P_Factor + 1); pid->maxSumError = MAX_I_TERM / (pid->I_Factor + 1); }

/*! \brief PID control algorithm. * * Calculates output from setpoint, process value and PID status. * * \param setPoint Desired value. * \param processValue Measured value. * \param pid_st PID status struct. */ int16_t pid_Controller(int16_t setPoint, int16_t processValue, struct PID_DATA *pid_st) { int16_t error, p_term, d_term; int32_t i_term, ret, temp;

error = setPoint - processValue; //1

// Calculate Pterm and limit error overflow if (error > pid_st->maxError){ p_term = MAX_INT; } else if (error < -pid_st->maxError){ p_term = -MAX_INT; } else{ p_term = pid_st->P_Factor * error; //2 }

// Calculate Iterm and limit integral runaway temp = pid_st->sumError + error; //3 if(temp > pid_st->maxSumError){ i_term = MAX_I_TERM; pid_st->sumError = pid_st->maxSumError; } else if(temp < -pid_st->maxSumError){ i_term = -MAX_I_TERM; pid_st->sumError = -pid_st->maxSumError; } else{ pid_st->sumError = temp; //4 i_term = pid_st->I_Factor * pid_st->sumError; //5 }

// Calculate Dterm d_term = pid_st->D_Factor * (pid_st->lastProcessValue - processValue);//6

pid_st->lastProcessValue = processValue; //7

ret = (p_term + i_term

  • d_term) / SCALING_FACTOR;//8 if(ret > MAX_INT){ ret = MAX_INT; } else if(ret < -MAX_INT){ ret = -MAX_INT; }

return((int16_t)ret); }

/*! \brief Resets the integrator. * * Calling this function will reset the integrator in the PID regulator. */ void pid_Reset_Integrator(pidData_t *pid_st) { pid_st->sumError = 0; }

a definicja pid.h tak

#ifndef PID_H #define PID_H

#include "stdint.h"

#define SCALING_FACTOR 128

/*! \brief PID Status * * Setpoints and data used by the PID control algorithm */ typedef struct PID_DATA{ //! Last process value, used to find derivative of process value. int16_t lastProcessValue; //! Summation of errors, used for integrate calculations int32_t sumError; //! The Proportional tuning constant, multiplied with SCALING_FACTOR int16_t P_Factor; //! The Integral tuning constant, multiplied with SCALING_FACTOR int16_t I_Factor; //! The Derivative tuning constant, multiplied with SCALING_FACTOR int16_t D_Factor; //! Maximum allowed error, avoid overflow int16_t maxError; //! Maximum allowed sumerror, avoid overflow int32_t maxSumError; } pidData_t;

/*! \brief Maximum values * * Needed to avoid sign/overflow problems */ // Maximum value of variables #define MAX_INT INT16_MAX #define MAX_LONG INT32_MAX #define MAX_I_TERM (MAX_LONG / 2)

// Boolean values #define FALSE 0 #define TRUE 1

void pid_Init(int16_t p_factor, int16_t i_factor, int16_t d_factor, struct PID_DATA *pid); int16_t pid_Controller(int16_t setPoint, int16_t processValue, struct PID_DATA *pid_st); void pid_Reset_Integrator(pidData_t *pid_st);

#endif

ograniczać zakres można i w wersji 2, pytanie tylko jak uwzględnic Ki w granicach. A może nie trzeba, bo to problem użytkownika, albo od strony wartości wyjsciowych to też jest dobre.

Tylko zauważ, że tu się boją, aby wynik obliczen nie wyskoczył poza zakres INT.

**** wow - patrząc niżej, to strasznie wielki ten MAX_I_TERM ( (MAX_LONG / 2) ).

A tu, w członie D, jakby mniej się boją przekroczenia zakresu ... ciekawe.

J.

W dniu 1.02.2026 o 15:26, J.F pisze:

Ale samo ograniczanie nie ma sensu, chodzi o tto że to jest zmienna word czyli 16bit i żeby sie nie "przekręciła"

Wiem o tym, o tym właśnie piszę wyżej. Mogli dać int32 ale chyba by wtedy za duże bufory kumulował. Stąd wpierw normalizują wynik a potem mnożą przez współczynnik.

31 bitowy :) zauważ że wszystkie sumy są takie, ale potem długo się zbiera jak za duży błąd skumuluje, ja zmniejszyłem do 16 bit jak robiłem regulację prądu prostownika na pid-ie.

Bo tu się za bardzo nic nie kumuluje, to jest tylko całka ostatniego kroku, więc nie ma obaw o przekroczenie zakresu. A nikt rozsądny nie robi współczynnika na tysiące czy więcej.

IMO - ograniczanie wartości całki jak najbardziej ma sens. Inaczej to np każesz wykonac dużo obrotów, i zanim tam dojedzie, to całka zakumuluje sporą wartość, a potem będzie długo przeszkadzać. Tu Mirek ma rację - przerwać czy nawet zerować całkę przy osiągnięciu maksymalnej prędkości, a moze nawet przy zbyt dużym błędzie, czy choćby ograniczyć jej możliwą wartość do sensownych wartosci.

Może o to chodzi. Ale ... lastProcessValue-processValue teoretycznie może przekroczyc MAX_INT. No dobra - w realu absolutnie nie powinna, bo to znaczy, że obiekt przeskoczył w jednej skrajnej wartosci w drugą, w jeden cykl. A to się nie powinno zdarzyć. Choć jak piszesz o regulowaniu prądu .. może. Prąd ma małą bezwładnosć :-)

J.

W dniu 2.02.2026 o 11:31, J.F pisze:

Zwykle jest lekki przerzut, czasem się zdarza że prawie trafi albo jest tak mały że go nie zdążę zauważyć. Nie mogę za CHRL uzyskać takiego przetłumienia, żeby hamował dużo wcześniej i potem dochodził. Tzn, mogę to zrobić z zerowym Ki i bardzo małym wzmocnieniem, tak że dochodzi łagodnie ale nie osiąga celu, błąd jest na końcu ogromny.

Co do integral, zrobiłem tak jak tu gość tłumaczy:

formatting link
przestaje dodawać błąd jak wynik jest nasycony i znak błędu i wyniku jest zgodny. Generalnie po strojeniu działa to nieźle, w miarę szybko kasuje błąd do zera, ale czasem występują denerwujące szarpnięcia

- bardziej je słychać niż widać po silniku. Wygląda na to, że odpowiedzialny za nie jest człon D. Jak zmniejszę Kd, szarpnięcia zanikają, ale zaczynają się oscylacje. Najciekawsze jest to, że jeśli robię jazdę np. od 0 do 50000, to jeśli zwiększam w tempie 1 na 100uS - jedzie gładko. 2- gładko... aż tak do 7- zaczynają się szarpania, przy 10 jest koszmar, a tak od 15 znowu szarpania zaczynają zanikać. Coś mam jeszcze nie tak. Próbowałem też filtr na samo D - efekt jest podobny, albo nie ma wpływu albo wprowadza niestabilność.

W dniu 3.02.2026 o 00:14, J.F pisze:

[ciach]

Na razie nie odniosę się do tego, bo walczyłem z innym problemem. Mianowicie nurtowało mnie, że przy wolnych jazdach, kiedy dodaję do zadanej pozycji np. 5 kroków na cykl obliczania PID, słychać jakby regularne stukanie - silnik się kręci w miarę równo, ale coś stuka. Po całej serii testów, wychodzi na to, że winny jest tu odczyt z enkodera i np. mamy w kolejnych odczytach przyrost:

5,5,4,6,4,5,5,5,4,5,6,4,5,6,5,5,56,4,5,6,5,5... dziwne jest to 56 prawda? Tak samo jak dodaję 7 kroków mamy: 7,7,6,6,8,7,6,7,6,7,6,87,7,6,8,6,7,7,7,7,6,7,7... Co jest do cholery grane? Pozycja enkodera to 32-bitowy signed int. Nie sądzę, żeby przy błędzie konwersji albo jakichś błędach bitów można było uzyskać taki efekt. Ta wartość wygląda na skumulowany zapis z dwóch odczytów - jakby ktoś nie zauważył. Podobnie dzieje się w drugą stronę tylko znak się zmienia. Ta błędna wartość wygląda na to, że występuje raz na ileś tam kroków - przy jeździe 5 kroków na 1ms pyka tak mniej-więcej 2 razy na sekundę. przy szybszej szybciej. Nie jest to wielokrotność obrotu enkodera. Co ciekawe również nie udało mi się zauważyć błędu (rozjechania się pozycji 0) enkodera. Pętla odczytująca też w miarę równo chodzi - testowałem ją przy pomocy micros() i odczyty są w granicach 997-1003us i nie ma jakichś anomalii przy tych zawyżonych odczytach.

Dziwne jest, że to się tak składa z cyfr, które w ciagu występują.

No własnie - moze to nie bląd enkodera, ani jakas kumulacja odczytów, bo chyba dane są binarne, tylko np błąd raportowania - gdzieś Ci znika separator, i zamiast 5, 6, jest 56. Albo jakies zajechanie/nakładanie buforów w pamięci ...

J.

W dniu 4.02.2026 o 22:01, J.F pisze:

To bym nie miał stukania. Stukanie brało się z tego, że działało Kd. Później zacząłem szukać zacząłem wypluwać na terminal różne dane. Kiedy wyplułem if DeltaError > 10: Print(DeltaError) wszystko stało się jasne

- zaczęło wypisywać w rytm stukania. Przy Kd=0 nie stuka, ale wypluwa rytmicznie dalej - czyli jest to przyczyna a nie skutek.

No ciężko mi to sobie wyobrazić jak by to miało działać. Jutro jak będę miał na to czas, to spróbuję wyłapać surowe odczyty a nie przyrosty i sprawdzę dokładnie czy nie przekłamuje enkoder przy kręceniu tylko w jedną stronę. Bo przy kręceniu tam i z powrotem błędy mogą się niwelować.

Join the Discussion

Have something to add? Share your thoughts — no account required.

Didn't find your answer?

Ask the community — no account required