Huśtawka #4 – PID

Wracamy do huśtawki z nowym podejściem – sterowaniem z wykorzystaniem PID-a. Niestety, konstrukcja mechaniczna uniemożliwiła poprawną pracę układu… trzeba ponownie popracować nad mocowaniem ramienia łączącego serwo z pierścieniem rury.

Warto przeczytać ten artykulik ze wprowadzeniem do PID-a:

TECH ME MICRO

(c) K.G. 2022

2x BT master i slave + Maskotka + RGB

Wojny robotów:

Prace podzieliły się na dwa tory – niezależnie powstaje sterowanie, niezależnie poznawane są możliwości kolorowych LED-ów WS2812B. Dzięki takiemu podziałowi projekt brnie do przodu, choć przyznać trzeba, że powstanie tylko jedno sterowanie – bez konkurencji, więc… oby było dobre 😉

BT master

Pan Rafał postanowił wykorzystać dwa moduły Bluetooth (BT) aby komunikować się pomiędzy pojazdem a kontrolerem (bez czekania na wykorzystanie radiówek nRF24L – szkoda… ale do tego pewnie jeszcze wrócimy).

Pojazd (z lewej, moduł BT MASTER) oraz „kontroler” do sterowania (z prawej, BT SLAVE)

Dotychczas to telefon z aplikacją na Androida był MASTEREM (kontrolerem) łączącym się z pojazdem, czyli BT SLAV-em. Teraz jednak używamy dwóch BT w tym celu – choć przyznać trzeba, że prościej, efektywniej i taniej wydaje się zastosowanie wspomnianych na wstępie modułów nRF24L.

Kontroler ma podłączony moduł JOY-stica oraz moduł BT, który wysyła odczyty. Odbiorcą tych pakietów jest moduł BT w pojeżdzie, który łączy się z naszym kontrolerem. Aby to było możliwe należy MASTERA skonfigurować tak, aby po jego uruchomieniu „szukał” kontrolera i się z nim łączył. Realizuje się to przez tryb komend AT.

W tym celu podłączamy BT z dodatkowym pinem ustawionym na HIGH (poniżej pin #6 z Arduino, nazwany KEY, połączony z pinem STATUS w module XM-15B) i programikiem według poniższego schematu:

Wejście w tryb komend AT.

Jak widać program czyta bajty z portu szeregowego PC-ta i wysyła je do modułu BT podłączonego do Arduino (metody readString() oraz write()). Sprawdzamy, czy mamy wszystko poprawnie działające wywołując komendę AT+NAME. Powinna pojawić się nazwa naszego modułu (uwaga: nawet niepoprawnie wyświetlona) ORAZ nowa linia z napisem OK. Jeśli tak nie jest, trzeba sprawdzić podłączenia.

Moduł BT w trybie komend AT. Zwróć uwagę na znacznik końca linii: NL+CR.

Tryb komend AT umożliwia nam zmianę nazwy urządzenia BT, zmianę hasła czy innych parametrów komunikacji (np. szybkość transmisji szeregowej). Ale my wykorzystujemy go najpierw do odczytania numeru identyfikacyjnego urządzenia BT SLAVE – naszego kontrolera. Parowanie urządzeń BT odbywa się przez ten numer (a nie przez przyjazne nazwy, jak w Androidzie). Wydajeny komendę AT+ADDR i mamy odpowiedź: u nas 001135938823. Teraz wychodzimy z tryb AT dla kontrolera (BT SLAVE) i wgrywamy ten sam program na Arduino z BT MASTEREM.

Pojazd (BT MASTER) ma się łączyć z konkretnym modułem BT – znamy jego ID. Musimy więc najpierw ustawić go w tryb MASTER-a (komenda AT+ROLE zwróci 0 gdy moduł jest SLAVE-em, a 1 gdy MASTER-em; zmiana trybu odbywa się komendą AT+ROLE=1). Następnie zapisujemy adres kontrolera poleceniem AT+BIND=001135938823. Teraz można już wyłączyć program dla modułów BT pracujących w trybie AT i wgrać nasz własny programik na Arduino, które przez UART będzie odczytywać dane przesyłane przez kontroler modułami BT (oczywiście – jeśli włączymy pojazd, a kontroler nie będzie włączony – nie nastąpi komunikacja i pojazd nie będzie otrzymywać instrukcji sterująych).

Kto ma być MASTER, kto ma być SLAVE?

Pan Rafał zdecydował, że kontroler będzie SLAVE, a pojazd MASTEREM. Czy to dobrze? Można było by zrobić na odwrót… I takie właśnie rozwiązanie jest chyba lepsze, bo pojazd jako SLAVE czeka na dane wysyłane przez… dowolne sparowane urządznie! Może to być dedykowany kontroler (który właśnie tu powstaje) ale także jakaś apka na Androidzie – wówczas telefon jako MASTER może sterować naszym pojazdem. Takie rozwiązanie jest bardziej uniwersalne, więc pewnie w przyszłości będzie zastosowane.

Można też zajrzeć do opisu łączenia modułów BT master <-> slave na tej stronie – polecam.

WS2812B

Pan Tomasz postawił sobie zadanie płynnego przejścia kolorów: od niebieskiego do czerwonego w module WS2812B. Te dwa kolory mają być odpowiedzialne za wartości odczytanego pola magnetyczna magnesu. Zapis kolorów w systemie RGB nie jest tutaj najwygoniejszy, więc wybór padł na zastosowanie modelu HSV.

Model kolorów RGB. Przejście od czerwieni do niebieskiego to jednoczesna zmiana dwóch amplitud kolorów: R i B.
Model kolorów HSV. Droga od czerwieni do niebieskiego to zmiana jednego parametru – „kąta”, czyli wartości H.

Więcej…

Fajny link: https://www.arduinoslovakia.eu/blog/2018/4/neopixel-ring-hsv-test?lang=en

Maskotka – sterowanie.

Sam PID nie wystarczył. Moduł JOY-sticka ma mechaniczną „wadę”, że wychyla się góra/dół (lewo/prawo) mniej niż „pod kątem”. Maksymalne odczyty (1023 w przypadku Arduino UNO) otrzymuje się przy skrajnym wychyleniu w górę/prawo (dół/lewo) ale wychylając joy „po skosie” mamy (teoretycznie) o pierwiastek z dwóch więcej! Dlatego też to właśnie wychylenia „pod kątem” mają wartości 1023 a skrajne pionowe/poziome uzyskują te wartości jeszcze przed swoim maksymalnym wychyleniem – dalsze wychylanie nic już nie daje (mamy ciągle 1023 – zakres ruchu joya „zmarnowany”). Aby to zniwelować Pan Bartek znalazł ciekawe mapowanie kwadratu na kółko:

Odwzorowanie kwadratu na okrąg (kliknij obrazek aby przejść do źródła).

Taka modyfikacja sterowania pozytywnie wpłynęła na kontrolowanie Maskotki. Gratulacje!

Kolejne spotkanie: czwartek 6 maj 2021, godz. 15:15.

Wojny robotów („kanapka”) + problem: BT + serwo

Wojny robotów: problem – serwa i Bluetooth (BT)

Pan Rafał oprogramowywał sterowanie swojej „kanapki” z wykorzystaniem modułu BT. Nie jest to może najlepsze rozwiązanie sterowania, ale szybkie i bezproblemowe (jasne, jasne – czytaj dalej). Proste, po włączamy dwie standardowe biblioteki Servo.h oraz SoftwareSerial.h i piszemy prosty kod.

#include <Servo.h>
#include <SoftwareSerial.h>

SoftwareSerial bt(8, 9);//RxD, TxD
Servo silnik;

void setup() {
  Serial.begin(9600);
  bt.begin(9600);
  silnik.attach(3);
  Serial.println("start!");
}

void loop() {
  if (bt.available()){
    char komenda=bt.readString());
    Serial.print("odebrano= ");
    Serial.println(komenda);//dla sprawdzenia

    switch (komenda){
      case 'W': silnik.writeMicroseconds(1000); break;  //przod
      case 'S': silnik.writeMicroseconds(2000); break;  //tyl
      case 'X': silnik.writeMicroseconds(1500); break;  //stop
    }//switch
}

A tu klops! Ilekroć przesyłane są dane przez moduł BT, to serwa „wariują”, zachowują się zupełnie nieoczekiwanie. Można to sprawdzić powyższym kodem gdzie przesyłamy 'W’ przez Bluetooth i silnik kręci się na maksa do przodu, a następnie wysyłamy z apki kolejne, nic nie znaczące znaczki (wciskamy inne, zdefiniowane przyciski, np. 'A’, 'D’ itd). Te komendy powinny zostać zignorowane (brak odpowiednich instrukcji w switch-u), a serwo powinno utrzymywać swój dotychczasowy ruch. Tak się jednak nie dzieje…

Jak zawsze najpierw należało sprawdzić przewody połączeniowe, ale to nie one okazały się przyczyną problemów. Grzebanie w internecie okazało się sukcesem – biblioteka SoftwareSerial.h korzysta z tego samego układu czasowego (timera) w Artudino co biblioteka Servo.h, do sterowania PWM. Chodzi o to, ze Arduino (a dokładniej ATmega 328P) posiada trzy niezależne timery: dwa 8-bitowe oraz jeden 16-to bitowy. Właśnie z tego ostatniego korzystają dwie wspomniane biblioteki i powstaje konflikt!

Rozwiązaniem może być alternatywna biblioteka do obsłui serw: ServoTimer2. Już sama nazwa wskazuje, że korzysta ona z drugiego timera – nie powinno być więc konfliktu z SoftwareSerial.h. Lekko zamieniamy kod programiku i sprawdzamy zachowanie serw podczas przesyłania danych z BT. Klops: jednak nie pomogło.

Jeśli nie biblioteka serw, to szukamy zamiennika do drugiej biblioteki – komunikacji szeregowej. Tutaj trafiamy na AltSoftSerial i nasze nadzieje nie gasną 😉 Ponownie modyfikujemy kod (uwaga: pinami do komunikacji są „na sztywno” pin 9 oraz 10 – nie ma możliwości wyboru, jak w przypadku biblioteki SoftwareSerial.h).

Nie poddajemy się i stosujemy dwa zamienniki bibliotek jednocześnie – bum! mamy to! działa!

#include <ServoTimer2.h>
#include <AltSoftSerial.h>

AltSoftSerial bt;//Arduino UNO -> RxD=9, TxD=10, nie ma wyboru!
ServoTimer2 silnik;

void setup() {
  Serial.begin(9600);
  bt.begin(9600);
  silnik.attach(3);
  Serial.println("start!");
}

void loop() {
  if (bt.available()){
    char komenda=bt.readString());
    Serial.print("odebrano= ");
    Serial.println(komenda);//dla sprawdzenia

    switch (komenda){
      case 'W': silnik.write(1000); break;  //przod
      case 'S': silnik.write(2000); break;  //tyl
      case 'X': silnik.write(1500); break;  //stop
    }//switch
}

Niewielkie zmiany, ale istotne. Warto zwrócić uwagę, że funkcja write() dla obiekty ServoTimer2 zapisuje mikrosekundy wypełnienia, czyli dokładnie to co robi funkcja writeMicroseconds() z obiekty Servo (biblioteka Servo.h), co jest trochę mylące. Wystarczy mieć to na uwadze i będzie OK.

Jeszcze jedno: wspomniany problem nie występuje na starszych wersjach środowiska i bibliotek Arduino IDE. Warto sprawdzić.

Wojny robotów: trzecia „kanapka” i korki (przymyślenia).

Pan Kacper buduje trzeci pojazd „kanapka”. Mamy nowe, trochę zmodyfikowane tymczasowe koła – choć drukarka 3D pracuje powoli, to i tak trzeba się natrudzić przy składaniu całej konstrukcji (jakieś 3h?).

Trzeci pojazd. Nowe koła. Ba(k)teria na ścisk!

Myślałem, że wykorzystanie korków od wina jest oryginalne – a tu się okazało, że takie zabiegi były znane i (zapewne) stosowane przynajmniej od roku 1200+. Źródła historyczne donoszą, że prawdziwy podróżnik awanturnik na szlaku musiał posiadać zestaw korków – pewnie w celach majsterkowania, bo po cóż innego miał on je w swoim ekwipunku?

Ekwipunek Geralta: korki to podstawa! Witcher 3, CDPR

Wojny robotów: szybkość obrotowa (RPM) silczka

Pan Tomasz z kolei zawziął się na zbadaniu prędkości obrotej kół (RPM = Revolutions Per Minute). Prosty układ pomiarowy (tymczasowy) pozwolił mu zmierzyć tą prędkość. Układ składa się z potencjometru do sterowania prędkością serwa Feetech FT90R (z prawej strony), oraz czujki szczelinowej (umieszczonej na – oczywiście – korku do wina) wraz z przymocowanym kołem zębatym (druk 3D).

Pomiar prędkości obrotowej serwa.

Pan Tomasz wykorzystał przerwania z Arduino UNO (a dokładniej jedno, ze zboczem narastającym). cdn…

Maskotka

BB zakończył prace nad płytą główną do Maskotki – aktualnie przygotowywane jest lepsze sterowanie prędkością kół – kontroler PID.

Kolejne spotkanie: czwartek 15:00

PID oraz podstawy: PWM + (mikro)serwo

Podstawy Arduino

Piny cyfrowe PWM z multimetrem i LED-em, a potem serwosilniczek (Serwo.h, silnik.attache(pin), silnik.write(stopnie).

Maskotka

BB: regulator PID dla 1 koła: poniżej wynik działania algorytmu regulującego pracę silnika: kolor czerwony to krzywa pożądana (nastaw użytkownika, potencjometrem), kolor niebieski to aktualna praca koła. Oś Y na wykresie to RPS, czyli obroty na sekundę.

Praca regulatora PID ma polegać na tym, że RPS koła ma podążać za ustaloną wartością niezależnie od zewnętrznych czynników (np. koło na lodzie=kręci się praktycznie bez oporów, lub koło w błocie =ciężko mu wyjechać). Najważniejsze współczynniki regulatora PID Pab bartek dobrał metodą „prób i błędów” (plus własne, wcześniejsze doświadczenie) i są dobrane dla konkretnego silnika (dlatego ich tutaj nie podaję). Zauważ, jak szybko silnik dopasowuje się do pożądanej wartości). Przy okazji: wykres ze „starej” wersji algorytmu, na zajęciach Pan Bartek go udoskonalił i reakcja koła jest jeszcze szybsza!

Opis algorytmu PD

Z tego wpisu dowiesz się:

  • Jak płynnie podążać za linią?
  • Jak przetwarzać sygnały z czujników by sterować szybkością poszczególnych silników?
  • Co to jest tzw. „pętla zwrotna”?

A wszystko po to, by oprogramować Line Followera (w skrócie LF)!

Regulator PD to “ucięta” wersja regulatora PID (ang. Proportional-Integral-Derivative). Posłuży nam on do płynnego sterowania linefollower’em. Regulator ten posiada 3 człony:
liniowy (P),
całkowy (I),
różniczkowy (D).

Człon P odpowiada za to, jak szybko silniki LF’a będą się obracać na podstawie położenia linii. Jeżeli linia będzie po prawej stronie czujnika to robot “wie”, że musi skręcić w tym kierunku i zmniejszy szybkość prawego silnika (lub zwiększy prędkość lewego, ale z reguły lepiej zastosować to pierwsze rozwiązanie). Jednak sama zależność liniowa nie wystarcza, żeby płynnie podążać za linią (chyba, że bardzo powoli). Żeby “przewidzieć” zmiany w kierunku ruchu i odpowiednio na nie zareagować przydaje się nam człon D regulatora. Jest to prosta różnica pomiędzy poprzednią a aktualną pozycją linii. W przypadku prostej linii różnica będzie nieznaczna, a podczas nagłej zmiany kierunku robot szybciej zmieni szybkość silników. Człon I pomijamy, bo nie jest on zbyt ważny w przypadku linefollower’ów (z doświadczenia).

Może i jest to na początek trochę skomplikowane, ale implementacja jest bardzo prosta i nie wymaga zbyt wiele pisania:

error_P = pozycja_linii * KP; // zależność liniowa
error_D = (pozycja_linii - poprzednia_pozycja_linii) * KD; // różnica pomiędzy pomiarami
Error = error_P + error_D; // suma członów regulatora
poprzednia_pozycja_linii = pozycja_linii;

Uzyskany w ten sposób Error możemy bezpośrednio wykorzystać do ustawienia prędkości silników. Współczynniki KP i KD określają jaki wpływ mają poszczególne człony na ruch LF’a i będą się znacząco różnić w zależności od algorytmu wykrywania pozycji linii czy samej budowy robota (np. pozycji czujnika). Właśnie w konkretnych wartościach KP, KD leży sekret działania dobrego regulatora PID. Żeby uzyskać płynny ruch trzeba się trochę “pobawić” z tymi współczynnikami i znaleźć najlepsze wartości. Można znaleźć wiele metod strojenia takiego regulatora (np. Metoda Zieglera-Nicholsa), jednak w tym przypadku najprościej jest to zrobić ręcznie czyli metoda prób i błędów, eksperymentalnie zmieniając KP i KD i sprawdzając na torze jak to wpływa na ruch LF.

Na początku ustawiamy KD = 0, i szukamy najmniejszego KP, żeby robot przy małej prędkości w miarę dobrze podążał po prostej linii lub lekkim łuku. Po znalezieniu odpowiedniego KP możemy stopniowo zwiększać współczynnik KD i prędkość, do uzyskania płynnego ruchu. Jako, że różnice w pomiarach są stosunkowo małe w porównaniu do położenia linii, często następuje zależność: KD > KP.

Określanie pozycji linii, wspólne działanie 5-ciu czujników

Jak przy pomocy wielu czujek jednoznacznie określić, gdzie znajduje się linia? Każda czujka zwraca tylko informację o ilości odbitego światła (wartości od 0 do 1023). Można by zastosować algorytm który wybiera jedną pozycję z największą wartością ale prowadzi on tylko do niepłynnego ruchu. Powinniśmy zastosować bardziej precyzyjny algorytm wykrywania linii ze średnią ważoną.

Każdemu czujnikowi przypisujemy wagi, na których postawie będziemy określać pozycję (np. -2000, -1000, 0, 1000, 2000 dla kolejnych czujek). Następnie czytamy wartości z czujników, mnożymy je razy wagę i sumujemy. Na koniec dzielimy otrzymaną wartość przez sumę wartości z czujek. W ten sposób dokładną informację nawet kiedy linia znajduje się na granicy dwóch czujników.

pozycja = (cz1*waga1+cz2*waga2+cz3*waga3+cz4*waga4+cz5*waga5)/(cz1+cz2+cz3+cz4+cz5)

W przypadku zastosowania wag: -2000, -1000, 0, 1000, 2000 dla kolejnych czujników, po przeliczeniu średniej ważonej otrzymamy jedną wartość od -2000 do 2000 (dla -2000 linia znajduje się po lewej stronie, dla 0 – na środku, a dla 2000 – po prawej).

Sterowanie prędkością silników

Jak wykorzystać wcześniej otrzymany Error do sterowania prędkością silników? Do ustawienia prędkości pojedynczego silnika używamy sygnału PWN. Będziemy tą wartość zmieniać w zależności od uzyskanego Error’a.

Jeżeli wartość Error’a jest ujemna to w naszym przypadku pozycja linii znajduje się po lewej stronie LF’a. Aby skorygować ruch będziemy zmniejszać prędkość lewego silnika. Analogicznie postępujemy w przypadku dodatniej wartości:

if(Error > 0){
pwn1=maxSpeed; //maxSpeed to wczesniej ustalona maksymalna wartość PWN LF’a
pwn2=maxSpeed-Error;
} else {
pwn1=maxSpeed+Error;
pwn2=maxSpeed;

Warto pamiętać, że Error może być znacznie większy od wartości maxSpeed. Jeżeli wartość nowego pwn1 lub pwn2 jest mniejsza od 0 to musimy ją ustawić na 0 (pwn powinien wynosić od 0 do 255). Po tych wszystkich operacjach uzyskujemy pwn’y do sterowania silnikami.

Mój LF w akcji:

(c) Bytler, 2019