Gra PONG na bazie Arduino i wyświetlacza LED

Gra PONG na bazie Arduino i wyświetlacza LED

Pong nie jest może pierwszą grą elektroniczną, jaka ukazała się na świecie, jednak jest z pewnością pierwszą, która osiągnęła gigantyczny sukces komercyjny i uzyskała ogromną rozpoznawalność. Niech świadczy o tym fakt, że pojawiła się na automatach pół wieku temu, a właśnie czytacie artykuł o jej implementacji na nowoczesnym mikrokontrolerze…

Pong był stanowczo początkiem pewnego trendu. Chociaż nie była to pierwsza gra wideo, jaką kiedykolwiek wymyślono, z pewnością jest to ta, której przypisuje się początek szaleństwa gier wideo. Po raz pierwszy pojawił się na poziomie konsumenckim wraz z wydaniem Magnavox Odyssey w 1972 roku. Odyssey to konsola, która oferowała 12 różnych gier, ale tenis stołowy (czyli właśnie pong) był zdecydowanie najpopularniejszą z nich. Później koncepcja wideo-tenisa stołowego została dopracowana przez Atari do wersji, którą wszyscy znamy obecnie i wtedy też pojawiła się nazwa pong.

Jak pisze autor, od kilku lat ma lekką obsesję na punkcie tej gry. W związku, z czym wpadł na pomysł odtworzenia jej za pomocą prostej matrycy diod LED. Projekt ten miał oddawać w jakiś sposób hołd oryginalnej grze, jednocześnie nadając jej oryginalny styl.

Potrzebne elementy

Do budowy urządzenia potrzebne będą następujące elementy:

  • rezystor 10 kΩ (16 sztuk),
  • diody LED 5 mm (128 sztuk),
  • dwa enkodery obrotowe,
  • trzy rejestry przesuwne SN74HC595,
  • moduł Arduino Nano,
  • zasilacz 5 V,
  • żeńskie gniazdo zasilania,
  • taśma wieloprzewodowa (można odzyskać ją ze starych komputerów),
  • cienki drut – autor wykorzystuje przewody z kabli sieciowych,
  • 26 goldpinów w listwie,
  • płytka uniwersalna,
  • śruby – autor użył calowych śrub 4-40×1”, najbliższym metrycznym zamiennikiem będą M3×25 mm (4 sztuki),
  • śruby 4-40×5/8” lub M3×15 mm (2 sztuki),
  • nakrętki 4/40 lub M3 (10 sztuk),
  • wkręty do drewna 6×1/4" lub 3,5×6 mm lub zbliżone (2 sztuki).

Ponadto potrzebna będzie drukarka 3D z filamentem – autor używa czarnego PLA) oraz podstawowe narzędzia, takie jak lutownica czy zestaw wkrętaków.

Pierwsze testy

Z uwagi na poziom skomplikowania takiej konstrukcji, autor rozpoczął od prostych testów uproszczonego układu. Ten uproszczony układ to rząd 16 czerwonych diod LED, które sterowane były przez dwa rejestry przesuwne, połączone łańcuchowo i podłączone do Arduino Uno. Celem było po prostu zapalanie pojedynczej diody od lewej do prawej, po kolei. Na fotografii 1 pokazano testowy system z pojedynczym rzędem diod świecących.

Fotografia 1. System w czasie pierwszych testów

Za pomocą płytki stykowej do rejestrów podłączono linie zatrzaskiwania, zegara i danych z Arduino do pierwszego rejestru przesuwnego. Następnie tylko zatrzask i zegar z Arduino podłączono do drugiego rejestru. Dane są wysyłane do drugiego rejestru przesuwnego przez pin 9 pierwszego rejestru przesuwnego do pinu 14 drugiego. Oznacza to właśnie, że połączone są one ze sobą szeregowo. Dane są przesyłane do rejestrów przesuwnych w postaci bitów. Tak więc wysłanie 1 lub (00000001) do niego spowoduje, że pin 15 rejestru będzie w stanie wysokim, podczas gdy piny 1...7 pozostaną w stanie niskim. Wysłanie 2 lub 00000010 powoduje, że pin 1 przyjmie stan wysoki; cała reszta będzie natomiast stanie niskim. Jeśli wyślemy (szeregowo) liczbę 128 (czyli 10000000) pin 7 przyjmie stan wysoki; wszystkie inne niskie. Wysyłanie 13 lub (00001101) sprawi, że piny 15, 2, 3 będą miały stan wysoki, a wszystkie pozostałe niski.

Do testów użyto 2 rejestrów przesuwnych połączonych szeregowo razem. Oznacza to, że aby przesłać dane do drugiego rejestru, trzeba wysłać po kolei dwie liczby 8-bitowe. Pierwsza wysłana liczba trafia do pierwszego rejestru; następnie kolejne polecenie wysłane do połączonych rejestrów, przesuwa pierwsze słowo do drugiego rejestru. Aby zobrazować działanie rejestrów przesuwnych, autor podłączył do wyjść rejestrów 16 diod LED. Pozwoliło to zobrazować działanie rejestru i potwierdzić poprawność założeń konstrukcji.

Wyświetlacz macierzowy LED

Zazwyczaj obudowa jest ostatnim elementem, jaki wykonuje się w projekcie urządzenia elektronicznego, gdy cały obwód już działa. Jest to podyktowane faktem, że dopóki cała elektronika nie zadziała poprawnie, nie można mieć pewności, że zmiany w projekcie elektroniki, nie pociągną za sobą zmian mechanicznych. A nikt, jak zauważa autor konstrukcji, nie chce przeprojektowywać obudowy dla dopiero, co powstającego układu, bo trzeba było coś zmienić w elektronice… Jednak w przypadku tej konstrukcji znacznie łatwiej będzie zbudować matrycę 128 diod LED z obudową, niż podpinać diody do płytki stykowej w celu przetestowania. Obudowa wyświetlacza LED to 2 kawałki plastiku wydrukowane w 3D. Obudowę i projekt modułu pokazano na, odpowiednio, fotografii 2 i rysunku 2.

Fotografia 2. Matryca diod LED w obudowie z druku 3D

Pierwszy element ma 128 otworów o rozmiarze pasującym do diod LED. Każda dioda LED jest umieszczana w otworze pojedynczo, a następnie umocowana w miejscu kroplą kleju, aby upewnić się, że się nie poruszą. Następnie każde dodatnie wyprowadzenie każdej diody LED jest przylutowane do dodatniego wyprowadzenia diody LED znajdującej się tuż pod nią. W ten sposób otrzymujemy 16 kolumn po 8 diod LED z anodami zlutowanymi razem. Następnie za pomocą kabla taśmowego do każdej kolumny dołączony jest jeden z 16 przewodów (fotografia 2b).

Rysunek 2. Projekt obudowy dla matrycy diod LED

Następnie druga plastikowa płytka z wydruku 3D jest przymocowana z tyłu. Ta płytka ma w sobie 8 pionowych szczelin. Każda katoda diody LED przechodzi przez tę szczelinę. Taśma przylutowana do anod przeciągnięta jest pod spodem zamontowanego drugiego elementu obudowy. Na zakończenie należy połączyć dwie plastikowe części za pomocą śrub i nakrętek (fotografia 2c). Następnie zlutowano każde z ujemnych wyprowadzeń diod razem – poziomo. Na końcu każdego z 8 rzędów po 16 diod LED zlutowanych razem można zainstalować taśmę, co kończy montaż matrycowego wyświetlacza LED.

Wszystkie 24 przewody z matrycy zostały przylutowane do płytki uniwersalnej. Następnie podłączono do niej rezystory (do 16 dodatnich pinów z diod LED matrycy), a do rezystorów dołączono dwa, połączone szeregowo, rejestry przesuwne. Na koniec trzeci rejestr przesuwny został podłączony do 8 linii wspólnych katod.

Oprogramowanie

Po wykonaniu matrycy, potrzebny jest kod dla mikrokontrolera, który pozwoli nam zagrać w ponga. W tym celu trzeba oprogramować trzy rejestry przesuwne, które kontrolują diody LED. Dwa z nich połączone są szeregowo i obsługują 16 kolumn wyświetlacza. Działanie tego układu omówione zostało już powyżej – wpisujemy do niego szeregowo 16-bitową liczbę, co powoduje zapalenie się odpowiednich diod (odpowiadających jedynkom w dwójkowym zapisie tej liczby). Trzeci rejestr przesuwny w systemie, który kontroluje wiersze, działa w odwrotny sposób. Jako że jego wyjścia podłączone ą do anod diod, potrzebny jest stan niski, aby dioda zapaliła się – gdyby do rejestru wysłać 255 (0b11111111) żadna dioda by się nie zapaliła, a gdyby 0 (0b00000000) to zapaliłyby się wszystkie
W kodzie programu macierz diod LED zapisana jest, jako dwuwymiarowa tablica binarna. Domyślnie inicjalizowana jest tak, jak pokazano na listingu 1.

Listing 1. Macierz diod LED zapisana jest, jako dwuwymiarowa tablica binarna, zainicjowana w następujący sposób

int leds[8][16]={{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}};

Zero oznacza, że dana dioda jest wyłączona, a jedynka oznacza, że dioda jest zaświecona. Gra w ponga ma 3 różne elementy, które się poruszają, gracz pierwszy, gracz drugi i piłka. Za każdym razem, gdy jeden z nich się porusza, tablica jest aktualizowana, aby pokazać bieżącą lokalizację – widać je nawet w takiej postaci, jak na listingu bez wyświetlania na macierzy diod. Dane z tablicy są przesyłane do rejestru przesuwnego po jednym wierszu na raz. Do pierwszych trzech wysyłamy 1 lub (0b000000001), ponieważ jedynka jest tylko na pozycji pierwszej diody LED. Na listingu 2 umieszczono kluczowe fragmenty kodu urządzenia.

Listing 2. Kluczowe fragmenty kodu urządzenia

void loop() {
 // Rysowanie gracza 1
 leds[player1][0]=1;
 leds[player1+1][0]=1;
 leds[player1+2][0]=1;  
 // Rysowanie gracza 2
 leds[player2][15]=1;
 leds[player2+1][15]=1;
 leds[player2+2][15]=1;
 
 if(row>7){
   row=0;    // Resetowanie wyświetlania (powrót do 0 rzędu)
   loops++;  // Po narysowaniu ekranu jest zliczana jedna pętla
 }

 while (row < 8){  
   //used to remove ghosting is the next columns
   //or else it will light up a second led in a column
   digitalWrite(LatchPin, LOW);//low to write data
   shiftOut(DataPin,ClockPin,MSBFIRST,0);//last shift register
   shiftOut(DataPin,ClockPin,MSBFIRST,0);//first shift register
   digitalWrite(LatchPin, HIGH);//sending written the data    
   digitalWrite(C_LatchPin, LOW);//low to write data
   //every row is high except for the one curently being drawin
   // that one is set to low
   shiftOut(C_DataPin,C_ClockPin,MSBFIRST,rowbase – mult[row]);
   digitalWrite(C_LatchPin, HIGH);//sending written the data
   //getting the number value to send to the shift reginters
   for(int i = 0; i<8; i++){
     first_shift_val += leds[row][i]*mult[7-i];
     second_shift_val += leds[row][15-i]*mult[i];
   }
   digitalWrite(LatchPin, LOW);//low to write data
   //last shift register
   shiftOut(DataPin,ClockPin,MSBFIRST,second_shift_val);
   //first shift register
   shiftOut(DataPin,ClockPin,MSBFIRST,first_shift_val);
   digitalWrite(LatchPin, HIGH);//sending written the data
   first_shift_val=0;
   second_shift_val=0;
   row++;
 }

 //*******************Kontrola gracza 1******************
 if (gameover != true) {
   p1eAnow = digitalRead(p1eA); // Odczytuje stan pinu A
   if((p1eAlast==HIGH)&&(p1eAnow==LOW)){
   if(digitalRead(p1eB) == LOW){
     if(player1>0){
     player1--; // Paletka gracza 1 przesuwa się w dół
     Serial.println("down down down");
     }
   }
   else{
     if(player1<5){
     player1++; // Paletka gracza 1 przesuwa się w górę
     Serial.println("up up up");
     }
    }
    player1change= true; // Flaga zmiany pozycji
   
    Serial.println(p1eAnow);  
   }
   p1eAlast=p1eAnow;
   if(player1change==true){  
   leds[0][0]=0;
   leds[1][0]=0;
   leds[2][0]=0;
   leds[3][0]=0;
   leds[4][0]=0;
   leds[5][0]=0;
   leds[6][0]=0;
   leds[7][0]=0;
   player1change=false;
   }
 }
 
 //*******************Kontrola gracza 2*********************
 //**************Analogicznie, jak dla gracza 1*************
 
 //*********************Ruch piłeczki***********************
 if (gameover != true){
   if(loops % ball_update == 0){
   // W pierwszej pętli rysujemy piłeczkę w domyślnym miejscu
   if(loops == 0) leds[ball_location[0]][ball_location[1]] = 1;
   else{
     // Kasujemy starą pozycję piłeczki
     leds[ball_location[0]][ball_location[1]] = 0;
     // Detekcja uderzenia w ścianę          
     if(ball_location[0]+ball_Vdir==8
       || ball_location[0]+ball_Vdir==-1)
       ball_Vdir *=-1;
     // Zderzenie z graczem 1
     if(ball_location[1] == 1){
     if(ball_location[0] == player1){
       ball_Hdir *= -1;
       ball_Vdir = 1;
       ball_location[1] = 1;
     }
     else if(ball_location[0] == (player1+1)){
       ball_Hdir *=-1;
       ball_Vdir=0;
     }
     else if(ball_location[0] == (player1+2)){
       ball_Hdir *=-1;
       ball_Vdir=-1;
     }
     else{ gameover = true;
     }
     }
     // Zderzenie z graczem 2
     if(ball_location[1] == 14){
     if(ball_location[0] == player2){
       ball_Hdir *= -1;
       ball_Vdir = 1;
     }
     else if(ball_location[0] == (player2+1)){
       ball_Hdir *=-1;
       ball_Vdir = 0;
     }
     else if(ball_location[0] == (player2+2)){
       ball_Hdir *= -1;
       ball_Vdir = -1;
     }
     else{
       gameover = true;
       ball_location[1] = 15;
     }
     }
     ball_location[0] += ball_Vdir;
     ball_location[1] += ball_Hdir;
     // Wstaw nową lokalizację piłeczki  
     leds[ball_location[0]][ball_location[1]] = 1;
   }
   }
 }
 if (gameover == true){
   if (gameover_start_timer == 0 ){
     gameover_start_timer = loops;
     gameover_end_timer = loops + 1000;
   }
   else if(loops >=  gameover_end_timer){
     gameover = false;
     gameover_start_timer = 0;
     
   }
 }
}

Ruch

W grze w ponga jest dwóch graczy, więc urządzenie musi umożliwić im obu kontrolowanie ruchu swoich paletek. W tym celu zastosowano dwa enkodery obrotowe – po jednym dla każdego gracza. Gracz jest reprezentowany przez 3 diody LED z każdej strony ekranu. Obracanie enkodera obrotowego zgodnie z ruchem wskazówek zegara przesuwa te 3 diody LED w dół, a obracanie enkodera przeciwnie do ruchu wskazówek zegara przesuwa diody LED w górę.

Piłka aktualizuje swoją lokalizację, co czterdziestą pętlę; jak wskazuje autor, wartość ta dobrana została eksperymentalnie i wydaje się być dobra dla zapewnienia średniego poziomu trudności gry. Jeśli chcemy podnieść poziom trudności, można aktualizować jej pozycję częściej – przyspieszy to ruch piłki. Kiedy piłka uderza gracza, zmienia kierunek w zależności od tego, w którą część diod gracza uderza piłka. Kiedy piłka uderzy w dolną diodę LED gracza, piłka będzie odbijać się w górę, jeśli uderzy w środkową diodę LED, nie ma żadnej zmiany ruchu pionowego, a jeśli uderzy w górę, piłka będzie kierować się w dół.

Montaż elektroniki

Do testowania oprogramowania zastosowano moduł Arduino Uno, ponieważ jest łatwy w użyciu w prototypach. Ma wyprowadzone piny, więc dodanie do niego nowego komponentu jest bardzo łatwe. Po fazie testów autor przeszedł jednak na Arduino Nano. Ta płytka ma taki sam rozkład wyprowadzeń jak Uno, więc nawet nie wymaga zmian oprogramowania.

Rysunek 3. Schemat ideowy układu

Na rysunku 3 pokazano schemat układu. Lutowanie należy zacząć od modułu Arduino i rejestrów przesuwnych. Autor zastosował płytkę uniwersalną, więc połączenia pomiędzy poszczególnymi elementami realizowane są za pomocą przewodów.

Następnie do płytki dolutowano dwa enkodery obrotowe. Gdy wszystko zostało połączone, system można zasilić i przetestować. Gotowe urządzenie pokazano na fotografii 3.

Fotografia 3. Zmontowany prototyp urządzenia

Obudowa

Gdy już cała elektronika działa poprawnie, można urządzenie umieścić w docelowej obudowie. Autor zaprojektował prostą obudowę w Fusion 360. Jest ona przystosowana do zamykania na wcisk, dzięki czemu nie trzeba jej skręcać – upraszcza to montaż całości. Ponieważ wcześniej powstała już obudowa matrycy LED, pierwszą rzeczą, nad którą pracował autor, jest sposób przymocowania jej do głównego korpusu. Wszystko, co było potrzebne, to dodanie kolejnego wspornika, który wychodzi za matrycę. Służy on również do zakrycia przewodów i wyprowadzeń z diod LED. Matryca w obudowie montowana jest za pomocą śrub, wkręcających się w nakrętki umieszczone w elemencie plastikowym. Wkręconą w uchwyt matrycę pokazano na fotografii 4.

Fotografia 4. Matryca LED w uchwycie

Następną plastikową częścią jest korpus główny. Zawiera on płytkę drukowaną, gniazdo zasilania i 2 enkodery obrotowe. Ma również miejsce na nakrętki, pozwalające na umocowanie matrycy w obudowie, jak pokazano na fotografii 5a. Na tylnej ścianie obudowy znajduje się jedyne gniazdo, które służy do zasilania konsoli (fotografia 5b).

Fotografia 5. Zmontowana konsola: a) widok od boku; b) widok od tyłu

Kolejne elementy z druku 3D to pokrętła dla enkoderów obrotowych, które kontrolują ruch paletek. Zostały one wydrukowane z kontrastującego, szarego filamentu. Pokrętła są dopasowane do enkoderów i przeznaczone do montażu na wcisk, ale autor sugeruje użycie kleju do ich lepszego umocowania na osi enkodera. Ostatni wydrukowany i zamontowany element to wieczko, które zakrywa główny korpus. To prosta osłona dla elektroniki. Do tego elementu nie używa się śrub ani kleju – jest wpasowany na wcisk. Element ten może wymagać dopasowania papierem ściernym, gdyż nie każda drukarka 3D zapewni odpowiednie tolerancje.

Podsumowanie

Ostatnie, co pozostaje, to już tylko podłączyć grę do prądu i zacząć grać. Jak wygląda rozgrywka? Autor uważa, że świetnie. Każdy z graczy kontroluje jeden z enkoderów, przesuwając paletkę w górę i w dół. Urządzenie działa szybko, każda paletka jest łatwa do kontrolowania, piłka odbija się dokładnie tak, jak zamierzono, a enkodery obrotowe świetnie sprawiły się w roli kontrolerów. Jedynym problemem jest to, że gra jest zdecydowanie zbyt prosta. Matryca ma tylko 8 diod LED, co oznacza, że aby zdobyć punkt, piłka musi trafić w miejsce, którego nie zajmuje inny gracz, ale że paletka ma szerokość równą trzem diodom LED, to niewiele wolnej przestrzeni pozostaje – w dowolnym momencie każdy gracz przykrywa prawie 40% swojej strony pola gry. Tak więc bardzo trudno jest nie trafić w piłkę.

Nikodem Czechowski, EP

Bibliografia:

Artykuł ukazał się w
Elektronika Praktyczna
grudzień 2022

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik kwiecień 2024

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio marzec - kwiecień 2024

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje marzec 2024

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna kwiecień 2024

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich kwiecień 2024

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów