29 listopada, 2020 Autor admin Wyłączony

Arduino i sterowanie serwami

Przedstawię poniżej mój sposób sterowania serwami z wykorzystaniem Arduino. Czego użyłem do projektu?
1. Arduino Uno
2. PCA9685 – 16-kanałowy 12-bitowy sterownik PWM z linią I2C
3. Serwa modelarskie typu SG-90 (plastikowe tryby), SG-92R (węglowe tryby) lub MG-90s (metalowe tryby)
4. Moduł przekaźnika.

Arduino Uno opisywać chyba nie trzeba, ale układ PCA9685 może nie być wszystkim znany. Jest to 16-kanałowy sterownik PWM, co oznacza, że dla moich zastosowań może wysterować niezależnie 16 serw lub 16 przekaźników. Nie wdając się w szczegóły można stwierdzić, że rozdzielczość 12-bitowa jest wystarczająca do płynnego sterowania serwami. Linia I2C, zajmująca tylko 2 wyjścia Uno jest w tym wypadku bardzo korzystna.

Sam sterownik posiada masę wyjść, ale mnie interesują tylko te:
VCC – zasilanie „logiczne” 5V, pobrane z płytki Arduino. Tu należy zaznaczyć, że Arduino Uno podłączane na biurku do testów pod port USB pobierze z tego portu prąd maksymalny w granicach 500mA. Nie należy zbytnio przesadzać z obciążaniem portu USB. O ile samo Arduino Uno pobiera prąd w granicach kilkunastu mA, o tyle już układ PCA9685 z podłączonymi kilkoma serwami może mieć większe zapotrzebowanie prądowe. Dlatego też do testów i zabaw można zasilać całość z portu USB jeśli wykorzystuje się jedno (góra dwa) serwa. Przy większej ilości serw (przekaźników) dobrze jest zasilić układ z zewnętrznego źródła zasilania dodatkowym zasilaniem „sterującym”, wykorzystując do tego gniazdo śrubowe na płytce modułu PCA9685. Wtedy „plus” tego zasilania pojawia się na pinie V+ płytki oraz (o czym trzeba wiedzieć i pamiętać) na wyjściowych pinach V+, do których podłącza się serwa.

GND – wspólna masa zarówno dla zasilania VCC, jak i V+

SDA, SCL – linia I2C

PWM – wyjścia sterujące serw lub przekaźników

Sposób podłączenia modułu PCA9685 oraz serw i przekaźników przedstawia poniższy obrazek

Jest to konfiguracja, w której zasilanie układów PCA9586 oraz serw pobierane jest z płytki Arduino (piny VCC i V+ połączone) i może być zastosowana, gdy zasilamy Arduino Uno z zewnętrznego zasilacza 12V o większej wydajności prądowej, ok. 2-3A.
Jeśli jednak zachodzi konieczność zasilenia urządzeń podpiętych do PCA9586 większym napięciem, np. mamy do dyspozycji 12V przekaźniki, wejście (gniazdo śrubowe na płytce) V+ i GND podłączamy do drugiego zewnętrznego zasilacza. Wtedy zasilania na VCC i V+ powinny być dwoma odseparowanymi od siebie „plusami” różnych zasilań. Przy stosowaniu zasilania zewnętrznego i wykorzystywaniu wielu serw warto wlutować w miejsce oznaczone jako C2 dodatkowy kondensator podtrzymujący o wartości pow. 470uF i dopuszczalnym napięciu wyższym od napięcia zasilania serw (np. 470uF/25V). W praktyce jednak albo rzadko w jednym czasie przestawia się kilka serw na raz, albo można tego programowo unikać, więc stosowanie dodatkowego kondensatora nie jest konieczne.

Czy potrzebne są aż 2 zasilacze? Nie. W swoim projekcie użyłem jednego mocnego zasilacza DC 5V o wydajności 3-4A, podłączonego bezpośrednio do wejść VCC i GND płytki Arduino. Zasilanie to „rozpropagowane” jest dalej na płytki PCA9586 poprzez ich piny VCC i GND i jest też zasilaniem serw na liniach V+ i GND.

Mamy więc gotowy układ z dwoma modułami PCA9586, z których jeden w całości wykorzystany jest do sterowania 16 serwami a drugi w całości wykorzystany do sterowania 16 przekaźnikami polaryzującymi. Przekaźniki dostępne są w postaci pojedynczych modułów lub tych samych modułów zintegrowanych np. po 4 sztuki.

Sterowanie przekaźnika podłączamy do trzech goldpinów oznaczonych jako VCC, GND i trzeci pin sterujący. Z drugiej strony, do zewnętrznych złącz gniazda śrubowego podłączamy przewody zasilania torów. Środkowe złącze to wyjście na część dziobownicy rozjazdu, na którą podawana będzie odpowiednia polaryzacja, zgodna z przełożeniem rozjazdu.

Istotą sterowania jest to, aby Arduino Uno sterując serwem i zmieniając jego położenie, zmieniało również stan przekaźnika, podającego na dziób rozjazdu właściwą polaryzację. Tak więc zarówno serwo, jak i przekaźnik muszą działać razem, jednocześnie. Ustawienie pozycji serwa względem pozycji przekaźnika realizowane jest programowo, tak więc sposób podłączenia zasilania z torów (linia czerwona i czarna) nie ma większego znaczenia. Choć dobrze jest stosować jakiś jeden kanon, że np. stan spoczynkowy przekaźnika (cewka nie zasilana, przekaźnik wyłączony) powiązany jest z jazdą na wprost. Daje nam to oszczędność prądu i przekaźników jeśli stan załączenia przekaźnika jest stanem rzadziej używanym, np. przy rzadziej używanym zjeździe na bocznicę.

No dobrze, to jak to teraz zaprogramować? Program, jak każdy w Arduino IDE piszemy zgodnie z konwencją:
1. dołączanie bibliotek, deklaracja zmiennych i klas przez nie używanych
2. deklaracja zmiennych programu
3. funkcja setup(), czyli wykonanie niezbędnych czynności przed uruchomieniem programu właściwego
4. funkcja loop(), niekończąca się pętla działania naszego programu
I tak:
Ze strony pobieramy bibliotekę Adafruit_PWMServoDriver i instalujemy ją w Arduino IDE. Dołączamy do programu bibliotekę obsługującą moduły PCA9586 i konfigurujemy jej ustawienia i parametry działania.

#include <Adafruit_PWMServoDriver.h>    //uzywane przez PCA9685 (serwa, przekazniki)
Adafruit_PWMServoDriver pwm1 = Adafruit_PWMServoDriver(0x40);
Adafruit_PWMServoDriver rel1 = Adafruit_PWMServoDriver(0x41);
#define SERVOMIN  100 //min wychylenie serwa
#define SERVOMAX  580 //max wychylenie serwa

Pod zmiennymi (właściwie to zła nazwa bo nie są to zmienne, ale klasy) pwm1 i rel1 mamy zdefiniowane odwołania do dwóch modułów:
– pierwszy na adresie 0x40 do obsługi serw
– drugi na adresie 0x41 do obsługi przekaźników
Zapomniałem wspomnieć o adresowaniu układów PCA9586. Moduły posiadają zestaw pól lutowniczych oznaczonych A0-A5 do ustawiania indywidualnego adresu układu. O ile adresowanie, zapis do układu, odczyt z układu nas nie interesuje bo załatwia to za nas biblioteka, o tyle ważnym jest żeby biblioteka wiedziała o który układ nam chodzi definiując jego nazwy w programie. Fabrycznie każdy układ ma pola lutownicze rozwarte, co oznacza ustawiony adres 64 (szesnastkowo 0x40). Zwarcie pól A0-A5 (zlutowanie kroplą cyny) powoduje ustawienie jednego bitu adresu w bajcie adresowym zdefiniowanym bitowo jako: 1-A5-A4-A3-A2-A1-A0, czyli:
– pozostawiając pola niezlutowane adresem układu będzie 1 000000, czyli 64 (0x40),
– zlutowanie A0 – adres 1 000001, czyli 65 (0x41)
– zlutowanie A1 – adres 1 000010, czyli 66 (0x42)
– zlutowanie A0 i A1 – adres 1 000011, czyli 67 (0x43)
– zlutowanie A2 – adres 1 000100, czyli 68 (0x44)
– zlutowanie A0 i A2 – adres 1 000101, czyli 69 (0x44)
itd…
W tym przykładzie układ pwm1 posiada adres 0x40, czyli ma niezlutowane żadne pole, układ rel1 ma adres 0x41, czyli ma zlutowany pad A0.

Dalej przechodzimy do zdefiniowania obsługi transmisji danych do Uno – u mnie to programowy RS232 z wykorzystaniem biblioteki SoftwareSerial. Służy mi to do przesłania do Arduino zestawów komend sterujących serwami i przekaźnikami, ale równie dobrze sposób ten może być zastąpiony zwykłą obsługą przycisków podpiętych pod Arduino (na ile staczy nam wolnych pinów).

//definicje pinów seriala: BT bluetooth
#include <SoftwareSerial.h> //uzywane do komunikacji PC<->Uno
SoftwareSerial BT(0, 1); // RX | TX

Do obsługi serw wykorzystuję jeszcze dwie zmienne: trzymaj – przyjmującą dwie wartości 0/1 i określającą, czy po wykonaniu ruchu serwo ma podtrzymywać działającą siłę, czyli ma utrzymywać zadaną pozycję, co jest praktyczne przy źle zamontowanym serwie, nie dociskającym dobrze iglic po przełożeniu; srodek – zmienna ustalająca pozycję środkową serwa (z zakresu ruchu wcześniej zdefiniowanym w programie jako min-max, 100-500).

//zmienne obslugi serw
byte trzymaj = 0; //0 - wyłacza napięcie serwa po ustawieniu pozycji, 1 - caly czas trzyma napiecie serwa
uint16_t srodek;  //pozycja srodkowa serwa

//zmienne obslugi BT
byte dana_in[9];
byte dana_out[9];
uint16_t crc;
long czas;   //czas do badania timeouta braku odpowiedzi od zwrotnic

W funkcji setup() inicjujemy moduły i określamy parametry ich pracy – częstotliwość sygnału PWM

void setup()  
{
  pwm1.begin();
  pwm1.setPWMFreq(60);  // czestotliwosc serw ~60 Hz
  rel1.begin();
  rel1.setPWMFreq(60);  // czestotliwosc serw ~60 Hz
  //start BT HC-05
  Serial.begin(57600);
}

No i w głównej pętli programu loop() oczekujemy na jakieś zdarzenie zewnętrzne, które ma być powiązane z ruchem serwa. U mnie to nadejście serii bajtów odpowiednich komend dla serwa, ale może to być oczekiwanie na naciśnięcie przycisku podpiętego pod konkretny pin Arduino.

Pozostanę jednak przy przykładowym rozkazie przesyłanym dla serwa z komputera, bo wyjaśnia on takie sprawy jak regulacja szybkości przesuwu serwa, regulacja pozycji środkowej iglic i maksymalne wychylenia serwa (iglic).

void loop()  
{  
  //odebrane 9 bajtow z odbiornika BT
  if(Serial.available()>=9)
  {  
     //sprawdzenie komendy odebranej z PC przez BT i przeslanie jej do zwrotnic albo do semaforow
     //komenda dla serwa
    /*
     * dana[0] = nr serwa 101-155 minus 101, czyli 0-150
     * dana[1] = stan (lewo/prawo)
     * dana[2] = MIN (wychylenie min)
     * dana[3] = MAX (wychylenie min)
     * dana[4] = predkosc (predkosc obrotu)
     * dana[5] = hi srodek (gorny bajt pozycji srodka serwa)
     * dana[6] = lo srodek (dolny bajt pozycji srodka serwa)
     * dana[7] = hi CRC
     * dana[8] = lo CRC
     */

     //===SERWA NA 1 PCA NUMERY 0-15======
     if((dana_in[0]>=101) && (dana_in[0]<=116))
    {
      dana_in[0] -= 101;
      srodek = (dana_in[5]*256)+dana_in[6];
      if (dana_in[1]==0) //kierunek obrotu - druga zmienna
      {
        for (uint16_t pulselen = srodek-dana_in[2]; pulselen < srodek+dana_in[3]; pulselen=pulselen+2)
        {
          pwm1.setPWM(dana_in[0], 0, pulselen);
          delay(dana_in[4]);
        }
        delay(100);
        if(trzymaj==0) { pwm1.setPWM(dana_in[0],0,4095); }
        rel1.setPWM(dana_in[0],0,4095);   //ustawienie polaryzacji tylko dla EW1 - zwrotnice 1-13
      }
      else
      {
        for (uint16_t pulselen = srodek+dana_in[3]; pulselen > srodek-dana_in[2]; pulselen=pulselen-2)
        {
          pwm1.setPWM(dana_in[0], 0, pulselen);
          delay(dana_in[4]);
        }
        delay(100);
        if(trzymaj==0) { pwm1.setPWM(dana_in[0],0,4095); }
        rel1.setPWM(dana_in[0],4096,0);   //ustawienie polaryzacji tylko dla EW1 - zwrotnice 1-13
      }
    }  //end if dla PCA1
  }
}

Podałem tu znaczenie bajtów rozkazów przychodzących do Arduino. Przykładowa ramka danych rozkazu może mieć postać: 101, 1, 10, 15, 1, 84, x, y, co oznacza:
101 – komendy pow. 100 dotyczą rozjazdów, a po odjęciu 101 wskazują na serwo podpięte do pinów „0” układu PCA9586. Dla pinu „1”, komenda byłaby 102, itd…
1 – kierunek wychylenia serwa. Tu przyjmujemy konwencję 0-lewo, 1-prawo lub 0-góra, 2-dół.
10 – wartość wychylenia serwa w lewo licząc od pozycji środkowej przesyłanej w następnych bajtach rozkazu
15 – wartość wychylenia serwa w prawo. Oczywiście wiedząc po drugim bajcie w którą stronę ma się wychylić serwo zbędne jest wysyłanie wartości wychylenia w stronę, w którą serwo się nie obróci. Jednak tworzenie warunków i ustalanie wielkości obrotu w zależności od kierunku zajmuje więcej czasu niż przesłanie jednego więcej bajtu, nawet, gdy w danym momencie jest on zbędny.
0 – szybkość ruchu serwa. Tutaj maksymalna, tj. bez pauz między mikroruchami serwa.
1, 84 – górny i dolny bajt pozycji środkowej serwa. Po „sklejeniu” równy 340, co oznacza pozycję z zakresu wcześniej zdefiniowanych ruchów serwa (100-500). Pozycja środkowa ma na celu regulację środkowego położenia iglic rozjazdu i zapewnienia prawidłowego przełożenia przy jak najmniejszym ruchu serwa. Zapewnia też stabilizację pracy serwa i zapobiega „drganiom” serwa na starcie jego pracy, które wynikają z szukaniem pozycji startowej, przesunięciem się do niej a dopiero następnie ruchem do pozycji końcowej. W końcu pozycja środkowa podana wprost do serwa nie wymusza bardzo dokładnego montażu serwa pod rozjazdem z zachowaniem jego środkowego położenia. Przy drobnych przesunięciach bocznych pozycja środkowa koryguje drobne błędy montażu. Dlatego każde serwo może mieć inną wartość pozycji środkowej. Wartości te są pamiętane po stronie komputera i programu przesyłającego dane do Arduino.
x, y – niezbyt istotne bajty kontrolne pozwalające ocenić poprawność przesłanej do Arduino ramki danych i zgłosić przez nie błąd w przypadku błędów/zakłóceń transmisji.

Tak więc istotą obrotu serwa jest ten kawałek kodu programu w pętli for i zawarta w niej instrukcja pwm1.setPWM(dana_in[0], 0, pulselen) gdzie pulslen to czas trwania impulsu podawanego przez układ PCA9586 na serwo.

      if (dana_in[1]==0) //kierunek obrotu - druga zmienna
      {
        for (uint16_t pulselen = srodek-dana_in[2]; pulselen < srodek+dana_in[3]; pulselen=pulselen+2)
        {
          pwm1.setPWM(dana_in[0], 0, pulselen);
          delay(dana_in[4]);
        }
        delay(100);
        if(trzymaj==0) { pwm1.setPWM(dana_in[0],0,4095); }
        rel1.setPWM(dana_in[0],0,4096);   //ustawienie polaryzacji tylko dla EW1 - zwrotnice 1-13
      }

Wspominałem wcześniej o podtrzymaniu siły serwa w zależności od stanu zmiennej trzymaj. W normalnej pracy serwa przebieg PWM na wyjściu PCA9586 utrzymywany jest z wypełnieniem równym ostatniej wartości zmiennej pulslen po skończonej pętli for. Co oznacza, że na serwo podawany jest przebieg ustalający jego ostatnią pozycję, czyli serwo cały czas podtrzymuje pozycję. Jeśli zmienna trzymaj=0 na wyjście PWM podawany jest stan wysoki, co zakłóca pracę serwo i przestaje ono pracować.

Szybkość ruchu serwa regulowana jest zmienną dana_in[4], która jest wartością opóźnienia pomiędzy kolejnymi mikroruchami serwa z pozycji startowej ku końcowej.

W przypadku przekaźników, które nie są sterowane wypełnieniem PWM a jedynie stanem wysokim (PWM=100%) lub niskim (PWM=0%) wystarczy podać dwie wartości – minimalną z zakresu (czyli zero) lub maksymalną z zakresu 12-bitowego (czyli 4095), co wygląda tak:
rel1.setPWM(dana_in[0],0,4095); – stan wysoki, przekaźnik załączony
rel1.setPWM(dana_in[0],4096,0); – niski stan PWM, przekaźnik wyłączony.
Podanie tych komend w odpowiedniej korelacji z kierunkiem ruchu serwa zapewnia właściwe spolaryzowanie rozjazdu, zgodnego z przełożeniem iglic.

Na koniec pokażę obsługę samych przekaźników, niezależnych od rozjazdów i nie stosowanych do polaryzacji. Mogą one być wykorzystane do załączania np. punktów oświetlenia.

    //===OBSŁUGA DODATKOWYCH PRZEKAZNIKOW NA PWM1=====================
    if((dana_in[0]>=140) && (dana_in[0]<=155))
    {
      dana_in[0] -= 140;  //dana_in[0]=0 - nr przekaźnika na PCA9586
      if (dana_in[1]==1)
      {
        rel1.setPWM(dana_in[0],4095,0);  //OFF  
      }
      else
      {
        rel1.setPWM(dana_in[0],0,4095); //ON
      }
    }

I to by było na tyle. Życzę miłej zabawy 😉

Pełny kod przykładowego programu

#include <Adafruit_PWMServoDriver.h>    //uzywane przez PCA9685 (serwa, przekazniki)
Adafruit_PWMServoDriver pwm1 = Adafruit_PWMServoDriver(0x40);
Adafruit_PWMServoDriver rel1 = Adafruit_PWMServoDriver(0x41);
#define SERVOMIN  100 //min wychylenie serwa
#define SERVOMAX  580 //max wychylenie serwa

//definicje pinów seriala: BT bluetooth
#include <SoftwareSerial.h> //uzywane do komunikacji PC<->Uno
SoftwareSerial BT(0, 1); // RX | TX

//zmienne obslugi serw
byte trzymaj = 0; //0 - wyłacza napięcie serwa po ustawieniu pozycji, 1 - caly czas trzyma napiecie serwa
uint16_t srodek;  //pozycja srodkowa serwa

//zmienne obslugi BT
byte dana_in[9];
byte dana_out[9];
uint16_t crc;
long czas;   //czas do badania timeouta braku odpowiedzi od zwrotnic

void setup()  
{
  pwm1.begin();
  pwm1.setPWMFreq(60);  // czestotliwosc serw ~60 Hz
  rel1.begin();
  rel1.setPWMFreq(60);  // czestotliwosc serw ~60 Hz
  //start BT HC-05
  Serial.begin(57600);
}

void loop()  
{  
  //odebrane 9 bajtow z odbiornika BT
  if(Serial.available()>=9)
  {  
     //sprawdzenie komendy odebranej z PC przez BT i przeslanie jej do zwrotnic albo do semaforow
     //komenda dla serwa
    /*
     * dana[0] = nr serwa 101-155 minus 101, czyli 0-150
     * dana[1] = stan (lewo/prawo)
     * dana[2] = MIN (wychylenie min)
     * dana[3] = MAX (wychylenie min)
     * dana[4] = predkosc (predkosc obrotu)
     * dana[5] = hi srodek (gorny bajt pozycji srodka serwa)
     * dana[6] = lo srodek (dolny bajt pozycji srodka serwa)
     * dana[7] = hi CRC
     * dana[8] = lo CRC
     */

     //===SERWA NA 1 PCA NUMERY 0-15======
     if((dana_in[0]>=101) && (dana_in[0]<=116))
    {
      dana_in[0] -= 101;
      srodek = (dana_in[5]*256)+dana_in[6];
      if (dana_in[1]==0) //kierunek obrotu - druga zmienna
      {
        for (uint16_t pulselen = srodek-dana_in[2]; pulselen < srodek+dana_in[3]; pulselen=pulselen+2)
        {
          pwm1.setPWM(dana_in[0], 0, pulselen);
          delay(dana_in[4]);
        }
        delay(100);
        if(trzymaj==0) { pwm1.setPWM(dana_in[0],0,4095); }
        rel1.setPWM(dana_in[0],0,4095);   //ustawienie polaryzacji tylko dla EW1 - zwrotnice 1-13
      }
      else
      {
        for (uint16_t pulselen = srodek+dana_in[3]; pulselen > srodek-dana_in[2]; pulselen=pulselen-2)
        {
          pwm1.setPWM(dana_in[0], 0, pulselen);
          delay(dana_in[4]);
        }
        delay(100);
        if(trzymaj==0) { pwm1.setPWM(dana_in[0],0,4095); }
        rel1.setPWM(dana_in[0],4096,0);   //ustawienie polaryzacji tylko dla EW1 - zwrotnice 1-13
      }
    }  //end if dla PCA1
  }
}