Kontroler bezprzewodowy zbudowany z pada do konsoli

Kontroler bezprzewodowy zbudowany z pada do konsoli

Witam,

Do tej pory na majsterkowie byłem tylko czytelnikiem, teraz nadszedł czas, żeby się spróbować jako autor. Podkreślam jednak, że nie mam dużego doświadczenia w obsłudze mikrokontrolerów, a języka C++ uczyłem się dawno i na co dzień go nie wykorzystuję.

Wstęp

Zabawę z Arduino zazwyczaj rozpoczyna się od migania diodą, brzęczenia buzzerem czy sprawdzania innych przykładów. W pewnym momencie jednak przychodzi czas na większe i poważniejsze projekty, np. może nam przyjść ochota na zbudowanie jakiegoś robota lub pojazdu. W takich, oraz innych przypadkach pojawia się potrzeba zdalnego sterowania naszym dziełem. Kiedy stanąłem przed takim wyzwaniem, pomyślałem, że wygodnie będzie zbudować sobie kontroler w obudowie od pada do PlayStation. Początkowo planowałem wyrzucić całe wnętrze i stworzyć je od podstaw, ale po co wyważać otwarte drzwi? Po krótkich poszukiwaniach odnalazłem opis gotowej biblioteki do obsługi pada od PS2. Do tego wystarczy dodać transmisję bezprzewodową, kilka elementów elektronicznych, trochę kodu i gotowe! Nie musimy się martwić o problemy związane np. z dokładnością pomiarów analogowych, zjawiskiem drgania styków itp. To robi za nas pad, a my tylko przechwytujemy wartości, opakowujemy po swojemu i wysyłamy do naszego odbiornika.

W Botlandzie można kupić gotowe urządzenie, ale ma ono zasięg do ok 8 metrów, co w większości przypadków może być niewystarczające. W moim projekcie w zależności od użytego nadajnika i odbiornika możemy uzyskać nawet kilkaset metrów zasięgu.

Założenia

  • korzystam z tanich elementów do komunikacji radiowej – opisane np. na https://majsterkowo.pl/jak-zaczac-z-rf/
  • prototypuję na Arduino…
  • … ale później projektuję i tworzę własną płytkę PCB, opartą na mikrokontrolerze ATmega
  • staram się pisać optymalny i porządny kod (postaram się przekazać kilka wskazówek w tym temacie)
  • kontroler ma być uniwersalny, wysyła stan przycisków i drążków, decyzja co później z nimi zrobić leży po stronie odbiornika
  • kontroler musi być “sparowany” z odbiornikiem, tak aby przy użyciu 2 kontrolerów można było niezależnie sterować 2 odbiornikami (wybrany przeze mnie sposób komunikacji nie pozwala na użycie osobnych kanałów)

Lista potrzebnych elementów

  • pad do PS2 (oryginalne, nawet używane, są dość drogie, kupiłem zamiennik za 15 zł, który niestety nie jest najwyższej jakości)
  • ATmega 8 lub mocniejsza
  • nadajnik i odbiornik RF
  • drobna elektronika (rezystor 10kΩ, rezystor 220Ω, dławik 10μH, kondensatory 100nF, kondensator elektrolityczny 22μF, dioda) – polecam kupić większą ilość. Są to elementy tanie i potrzebne do większości projektów.

Lista przydatnych elementów

  • dowolne Arduino do prototypowania oraz ewentualnie programator USBasp ISP
  • dodatki typu kabelki, płytka stykowa, gniazdo ISP 10 Kanda, łączniki, goldpiny itp.
  • multimetr – bardzo przydatne narzędzie, nawet w najtańszej wersji
  • narzędzia i elementy potrzebne do wykonania płytki PCB – nie będę tutaj omawiał wszystkich szczegółów, na majsterkowie jest sporo informacji na ten temat – np. tutaj i tutaj

Schemat

Bez zbędnej zwłoki przejdźmy do zaprojektowania schematu. W przypadku Arduino sprawa jest prosta, ponieważ w wersji minimalistycznej potrzebujemy tylko podłączyć przewody od pada oraz nadajnik RF.

Arduino schemat
Arduino schemat

W przypadku przeniesienia na mikrokontroler, potrzebujemy więcej elementów – musimy zadbać o poprawne podłączenie i filtrowanie. Jest to o tyle ważne, że brak filtrowania może negatywnie wpływać na działanie nadajnika. W moim przypadku filtruję nieco na wyrost, ponieważ nie będę używać przetwornika analogowo-cyfrowego, ale taki sposób podłączenia nie zaszkodzi, a w przyszłości może pozwolić na rozbudowę i np. bezpośrednie podpięcie się pod drążki i przyciski.

W tym miejscu miałem wstawić schemat podłączenia z programu Fritzing przy użyciu mikrokontrolera wraz z filtrowaniem, ale mimo że starałem się sensownie go narysować, to za każdym razem wychodził bardzo nieczytelnie. Jeżeli ktoś jest bardzo zainteresowany, to wstawiam jako odnośnik do obrazka, ale podkreślam że nie do końca można na nim polegać. Przede wszystkim kondensatory powinny być bliżej nóżek mikrokontrolera, zdecydowanie polecam obejrzenie schematu ideowego oraz projektu płytki PCB, umieszczonych niżej (Przygotowanie PCB).

Przed podłączeniem przewodów polecam ustalić ich kolejność, przez sprawdzenie który kolor łączy się z wyprowadzeniem we wtyczce:

Wyprowadzenie przewodów
Wyprowadzenie przewodów z pada
Wnętrze pada
Wnętrze pada

Praca z kodem

Zacznijmy od dołączenia do projektu dwóch potrzebnych bibliotek:

Uwaga. Biblioteka VirtualWire nie jest już rozwijana i została zastąpiona przez RadioHead – http://www.airspayce.com/mikem/arduino/RadioHead/.  Ponieważ jednak VirtualWire jest nieco mniejsza (a na początku miałem problem ze zmieszczeniem się na ATmega 8) to zdecydowałem się ją zostawić. Zachęcam do przejścia na nowszą i wspieraną bibliotekę. W kolejnych projektach na pewno spróbuję z niej skorzystać.

Biblioteki po pobraniu można dodać do dostępnych np. przez Arduino IDE Szkic→Dołącz bibliotekę→Dodaj bibliotekę .ZIP a następnie wskazać te biblioteki i dołączyć do programu. Na samym początku powinny pojawić się linijki:

Jak ma działać kontroler?

Kilka założeń:

  1. nie wysyłam stanu przycisków w każdej pętli, robię to tylko jeżeli coś się zmieniło – dzięki temu zdecydowanie ograniczam wykorzystanie radia, wydłużam czas pracy na baterii i daję więcej czasu na działanie odbiornikowi
  2. wysyłam tylko te wartości które uległy zmianie – ilość danych które mogę przesłać za jednym razem jest mocno ograniczona, najlepiej wysłać wszystko co potrzebne za jednym razem
  3. wprowadzam pewnego rodzaju “pseudo-kanał” aby dany nadajnik współpracował z wybranym odbiornikiem
  4. sprawdzam czy odbiornik jest w zasięgu działania nadajnika – wyobraźmy sobie sytuację, kiedy np. odbiornik jest zdalnie sterowanym samochodem. Zaczynamy jechać do przodu i samochód wyjeżdża poza nasz zasięg. Nie możemy już nim sterować, a on ciągle ma włączoną jazdę przed siebie i oddala się coraz bardziej. Dlatego dbam, żeby maksymalnie co ok. sekundę był wysyłany sygnał. Jeżeli odbiornik go nie otrzyma w zadanym czasie, to może zareagować (np. samochód się zatrzyma).

Sprawdzam jak działa biblioteka oraz jakie metody mi udostępnia i znajduję które będą mi potrzebne:

  • najpierw należy stworzyć instancję klasy PS2X
  • config_gamepad – wywoływana raz w setup() umożliwia podłączenie się do pada i jego konfigurację
  • read_gamepad – wywoływana w każdym przebiegu pętli odczytuje wartości przycisków i drążków
  • NewButtonState – pozwala sprawdzić czy zmienił się stan dowolnego lub konkretnego przycisku
  • Analog – pozwala odczytać wartości drążków

Całkiem nieźle, ale brakuje mi kilku rzeczy. Przede wszystkim nie jestem w stanie sprawdzić czy zmieniły się wychylenia drążków. Dodatkowo chciałbym sformatować wartości przed wysłaniem, tak żeby ułatwić sobie ich interpretację po stronie odbiornika. W związku z tym sensownym jest rozszerzenie klasy PS2X o własny kod.

TIP 1
Teoretycznie można dopisać kod bezpośrednio do biblioteki, ale nie jest to dobry pomysł. Przede wszystkim utrudni to zarządzanie własnym kodem a dodatkowo spowoduje problemy przy ewentualnym aktualizowaniu biblioteki do nowszej wersji. Zamiast tego skorzystamy z dziedziczenia. Tworzymy klasę Controller, przez którą będzie dostęp do wszystkich metod i pól publicznych w klasie PS2X

Zanim przejdziemy do napisania klasy, trzeba ustalić jakiś “protokół” wysyłania danych. Mamy 16 przycisków i 4 wartości dla drążków (wychylenie góra-dół i lewo-prawo dla każdego). Wymyśliłem, że każdej wartości przypiszę dwuliterowy skrót (np. se dla przycisku Select) i po dwukropku podam stan, a poszczególne rozdzielę pionową kreską | (ang. pipe). Na początku będzie wysłany “kanał” na którym działa nadajnik (dla odróżnienia skrótem jest jedna litera – c).  Przykładowa wiadomość do wysłania może wyglądać następująco:

i znaczy to: kanał 1, select został puszczony, strzałka w górę została wciśnięta, kwadrat został wciśnięty, położenie lewej gałki w osi y zmieniło się na 255.

Biblioteka udostępnia nam przyciski pod nazwami np. PSB_SELECTPSB_SQUAREPSS_LY itd, więc musimy je sobie jakoś przetłumaczyć na nasze nazwy skrócone. Najlepszy byłby słownik przechowujący te dane w postaci klucz – wartość, czyli tzw. hash table. Niestety musielibyśmy do tego wykorzystać dodatkowe biblioteki, albo samemu to oprogramować. W tym wypadku szybciej i bardziej wydajnie będzie zastosować 2 osobne tablice. Musimy tylko zadbać o poprawną kolejność i identyczną ilość elementów w obu. A nawet dwa takie zestawy, ponieważ osobno potraktujemy przyciski i gałki.

Nie przedłużając przedstawię kod całej klasy z dość rozbudowanymi komentarzami

Klasa gotowa. W zasadzie można ją nawet umieścić w osobnym pliku i włączyć do projektu przy użyciu tzw. pliku nagłówkowego, nie będę tego robić w tym przypadku.

Czas na ustawienie kilku stałych i zmiennych, oraz definicji pinów, które będą potrzebne do działania programu. Definiujemy je prawie na samym początku kodu (zaraz po włączeniu bibliotek), tak żeby działały jako globalne, czyli były dostępne z dowolnego miejsca

TIP 2
Bardzo często widzę definicję pinów jako np.
int ledPin = 13;
Oczywiście zadziała to bez problemów, ale nie powinno się tak robić. Przede wszystkim w 99,9% projektów nie będziemy tej wartości zmieniać (jeżeli nie wiesz czy potrzebujesz ją zmieniać, to znaczy że nie potrzebujesz) a w tej sytuacji można w trakcie programowania przypisać do niej inną wartość i program zacznie zachowywać się nieoczekiwanie. Dodatkowo taka zmienna zajmuje pamięć. Niby niewiele ale zawsze.
Jak widać powyżej, ja stosuję np.
#define ledPin 13
Plus jest taki, że wartość jest niezmienna przez czasu czas działania programu i zajmuje mniej miejsca. A tak naprawdę nie zajmuje wcale miejsca, ponieważ podczas kompilacji po prostu każde wystąpienie w kodzie ledPin jest zastępowane wartością 13.
Można jeszcze użyć słowa kluczowego const (uzyskując np. const int), które oznacza, że wartość jest stała. Zaletą tego rozwiązania jest zdefiniowanie jakiego typu jest wartość.
Dla testu napisałem prosty program, w którym zdefiniowałem 5 pinów (kolejno jako int, const int i #define), w setup() ustawiłem pinMode jako OUTPUT a w loop() je zapaliłem ustawiając stan wysoki. Wynik:
int – Program size: 756 bytes
const int – Program size: 704 bytes
#define – Program size: 704 bytes
Niektórzy mogli się dziwić dlaczego definiowałem numer kanału lub tekst ping na samej górze programu – takie rzeczy raczej się nie zmieniają i spokojnie można je podać w samym kodzie metod. Ale jak widać nie powoduje to w zasadzie żadnego narzutu na program, a dodatkowo wyodrębnia całą konfigurację w jednym miejscu. Jeżeli musimy coś zmienić, to chroni nas przed popełnieniem błędu.

Teraz utworzymy kilka metod pomocniczych, do wysyłania wiadomości, do wysłania komunikatu ping oraz do zebrania stanu przycisków, sformatowania go i przygotowania do wysyłki. W dwóch ostatnich metodach kryje się kilka ważnych rozwiązań i podjętych decyzji, więc na koniec podsumuję co tam zrobiłem.

TIP 3
W powyższym kodzie użyłem kilku dziwnych funkcji strcat, itoa i zamiast użyć “normalnych” Stringów, to korzystam z char. Po co to wszystko? Otóż użycie Stringów i proste dodawania ich znakiem +, lub nawet znalezienie funkcji formatującej jest bardzo wygodne, ale ma bardzo duży narzut. Po pierwsze jest dość wolne, ze względu na ciągłe użycie konstruktora, destruktora i przypisań pomiędzy. Dodatkowo może prowadzić do tzw. heap fragmentation i innych problemów z pamięcią. Generalnie Stringów najlepiej używać w środowiskach gdzie mamy dużo pamięci RAM i nie musimy się o nią martwić. Nie bardzo nadają się do użycia na mikrokontrolerach. Żeby tego było mało, to samo zadeklarowanie Stringa w kodzie, powoduje dołączenie do programu bibliotek, które zabierają sporo miejsca. Żeby nie być gołosłownym: zupełnie pusty projekt (tylko setup i loop) po skompilowaniu
Program size: 314 bytes
a po dodaniu jednej linijki String a = “a”;
Program size: 1 760 bytes

Teraz pozostało nam już tylko ustawienie kilku rzeczy w metodzie setup() a później cykliczne sprawdzanie co jest do wysłania w loop().

I to by było na tyle jeżeli chodzi o kod nadajnika. Całość można pobrać z githuba (ostatni commit przed publikacją to 6266d5c, mogą pojawiać się następne z uaktualnieniami i poprawkami):

https://github.com/Cube82/atmega_controller

Teraz jeszcze na szybko kod odbiornika

Kod jest dość uniwersalny, po prostu odbiera wiadomość, odczytuje poszczególne elementy i wyświetla je w oknie portu szeregowego. Utworzony jest zalążek klasy Receiver w której należy obsłużyć dalsze przetwarzanie danych w zależności od potrzeb.

Całość można pobrać z githuba (ostatni commit przed publikacją to 1e44fdf, mogą pojawiać się następne z uaktualnieniami i poprawkami):

https://github.com/Cube82/atmega_simple_receiver

TIP 4
Arduino IDE jest fajne, bo jest małe, darmowe i proste. Niestety ta prostota powoduje że większe projekty tworzy się z sporą uciążliwością. Każdy kto miał do czynienia z bardziej zaawansowanymi narzędziami wspomagającymi programowanie może z pewnym politowaniem patrzeć na mocno spartańskie środowisko pracy Arduino. Ale nie jesteśmy do niego ograniczeni. Ja osobiście używam Visual Micro – dodatek do Visual Studio (dostępne również w bezpłatnej wersji Community). Gorąco polecam, ponieważ daje nam nie tylko takie dodatki jak IntelliSense ale również debugowanie i całą masę innych ułatwień.

VS - Visual Micro
VS – Visual Micro

Kilka istotnych informacji o których trzeba wiedzieć (po resztę odsyłam do dokumentacji na stronie projektu):
– podczas instalacji Visual Studio należy zainstalować obsługę języka C++
– na komputerze na którym używamy Visual Micro musi być również zainstalowane Arduino IDE
– debugowanie jest dostępne w wersji Pro Visual Micro – licencja kosztuje od 29$, ale za darmo dostajemy 45 dni pełnej wersji i jeżeli nie chcemy płacić to spokojnie można używać wersji darmowej – nadal daje bardzo dużo ułatwień
– kod w wersji debug jest dużo większy od release – należy pamiętać aby ostateczną wersję kompilować w wersji release

Poniżej zrzut z Visual Studio na którym widać jak prezentują się wartości w oknie OutputSerial monitor, oraz najwygodniejszy w tym wypadku podgląd wyrażeń w oknie Expressions

Debugowanie
Debugowanie

Przygotowanie PCB

Ponieważ już dość mocno się rozpisałem, to z przedstawieniem przygotowania płytki PCB postaram się ograniczyć. Na początku zakładałem, że gotową płytkę schowam wewnątrz pada (są tam dwa silniczki, z których w tym przypadku nie będę korzystał i można je wyjąć), ale okazało się że bez korzystania z elementów SMD będzie to bardzo trudne. Tym bardziej, że założyłem możliwość łatwej aktualizacji oprogramowania w mikrokontrolerze i zamontowałem gniazdo ISP Kanda do podłączenia programatora (patrz np. tutaj).

Zacząłem od projektu układu w programie Eagle (kliknij aby powiększyć):

Schemat kontrolera
Schemat kontrolera

Później zaprojektowałem samą pytkę (kliknij aby powiększyć)

Kontroler płytka PCB
Kontroler – płytka PCB

Jeżeli ktoś jest zainteresowany przeniesieniem projektu na PCB, ale nie chce sam bawić się z Eagle, to poniżej pliki które drukowałem do metody termotransferu (warstwę opisową należy odbić przed wydrukowaniem):

PCB
PCB do termotransferu
PCB opis
PCB warstwa opisowa opis

 

 

 

 

 

 

 

Tak wyglądała płytka przed wytrawianiem

PCB
PCB przed wytrawieniem

A efekt końcowy wygląda tak:

Gotowe PCB
Gotowe PCB

Co można poprawić?

Powyżej opisałem pierwszą wersję kontrolera, jest jeszcze trochę rzeczy które można poprawić, usprawnić lub dodać. Pokrótce opiszę kilka pomysłów:

Kodowanie danych do wysłania

Wysyłane dane są zapisane w sposób ułatwiający zrozumienie co się w nich znajduje, np:

Z tym że długość wiadomości jest ograniczona do 30 znaków, a w powyższym przykładzie już jest użyte 26. Dlaczego tak to zostawiłem? Podczas testów zauważyłem, że zazwyczaj i tak się nie udaje wcisnąć kilku przycisków tak, żeby się razem wysyłały w jednym przebiegu pętli. Jeżeli jednak chcemy mieć pewność, że uda nam się przesłać wszystko za jednym razem, możemy użyć trochę magii – operatory bitowe. Mimo że jest to bardzo potężne, zwarte i szybkie narzędzie, to jest rzadko używane przez początkujących programistów. Dlaczego? Jak mówi stary dowcip jest 10 typów ludzi – ci którzy rozumieją system binarny i Ci którzy nie rozumieją. W naszym wypadku można zapisać do 8 wartości true/false w jednym bajcie! Nie rozpisując się w tym temacie za dużo, zaznaczę tylko że modyfikując założenia zapisu danych w wysyłanej wiadomości, moglibyśmy w niej zmieścić stan wszystkich przycisków, drążków i numer kanału. Dociekliwych odsyłam do analizy kodu biblioteki PS2X.

Parowanie urządzeń

Założyłem, że jednoczenie może pracować obok siebie więcej niż jedna para nadajnik-odbiornik (channel wysyłany na początku każdej wiadomości). Jest to jednak dość niewygodne, ponieważ numer kanału jest zapisany na stałe w programie. Żeby go zmienić trzeba przekompilować i wgrać nową wersję. Jak to poprawić? Sposobów może być wiele, np. możemy użyć zworek na płytce do wyboru kanału, lub dołączyć microswitch i zliczać ilość naciśnięć, lub użyć komunikacji dwukierunkowej i zaimplementować automatyczne parowanie.

Inny moduł radiowy

W tym projekcie użyłem najtańszych modułów radiowych jakie można znaleźć. Za nieco większą cenę możemy kupić bardziej zaawansowane produkty (np. nRF24L01), które mogą zapewnić nam większy zasięg, większą szybkość i niezawodność, oraz możliwość komunikacji dwukierunkowej i parowanie urządzeń.

Przejście na SMD

Zbudowany moduł nie jest duży, ale i tak nie chce się zmieścić do wnętrza pada. Gdyby przejść na SMD to płytka byłaby o wiele mniejsza i dało by się ją schować do środka. Na razie jednak nie czuję się na siłach do wykonania takiego projektu i zostanę przy montażu przewlekanym.

Wibracje

W tym projekcie nie bardzo było jak użyć wibracji dostępnych w padzie, ale przy wykorzystaniu komunikacji dwukierunkowej miałoby to już większy sens. Np. zdalnie sterowany samochód uderza o przeszkodę, odbiornik znajduje się poza zasięgiem, sterowane ramię wysięgnika osiągnęło maksymalne wychylenie itp.

Zasilanie

Kontroler jest zbudowany w oparciu o pad przewodowy, dlatego też nie jest wyposażony w żadne zasilanie. Sensownie byłoby włożyć do środka jakiś pakiet i umożliwić jego ładowanie. Ponieważ jednak ładowanie ogniw nie jest tematem prostym a może być dość niebezpieczne, to nie zdecydowałem się na to rozwiązanie w pierwszym projekcie. Póki co kontroler jest zasilany z powerbanku.

Ocena: 4.7/5 (głosów: 74)

Podobne posty

15 komentarzy do “Kontroler bezprzewodowy zbudowany z pada do konsoli

Odpowiedz

anuluj

Masz uwagi?