[179] Pong na oscyloskopie cz. 5

05.png

Na koniec serii artykułów o oscyloskopowym Pongu zajmijmy się udźwiękowieniem gry. Mógłbym tutaj skorzystać z prostego piszczka, który mam na płycie edukacyjnej TME, wysyłając na niego krótkie impulsy napięcia, ale wolę mieć dostęp do wysokości dźwięków, a więc użyję metody bardziej subtelnej.

#include "pwm.h"                            // Biblioteka PWM dla architektury Uno R4
const byte pot = A1;                        // Port potencjometru sterującego prawą paletką (gracz).
PwmOut pwmX(10);                            // Obiekt sterujący sprzętowym PWM dla pinu 10 (oś X).
PwmOut pwmY(9);                             // Obiekt sterujący sprzętowym PWM dla pinu 9 (oś Y).
PwmOut pwmAudio(5);                         // Obiekt sterujący sprzętowym PWM dla pinu 5 (dźwięk).
const byte paddleLength = 8;                // Połowa szerokości paletki.
const byte paddleTop = 100 - paddleLength;  // Górna granica ruchu środka paletki.
byte paddleLCenterY = 50;                   // Środek lewej paletki (komputera).
byte paddleRCenterY = 50;                   // Środek prawej paletki (gracza).
int ballX = 50;                             // Pozycja X piłki.
int ballY = 50;                             // Pozycja Y piłki.
int ballXSpeed = 2;                         // Prędkość pozioma piłki i jej kierunek.
int ballYSpeed = 1;                         // Prędkość pionowa piłki i jej kierunek.
unsigned long prevMillis = 0;               // Czas poprzedniego stanu zegara systemowego.
const unsigned long refresh = 20;           // Czas odświeżania logiki gry w ms.
int ballCount = 0;                          // Licznik odbić piłki od paletek.
byte scoreL = 0;                            // Wynik lewej paletki (komputer)
byte scoreR = 0;                            // Wynik prawej paletki (gracz)
unsigned long pause = 0;                    // Pauza po zdobyciu punktu.
unsigned long soundEnd = 0;                 // Czas systemowy, gdy należy wyłączyć dźwięk.

#define XL 0                                          // Definicje położenia: Lewy punkt pierwszej cyfry.
#define XR 20                                         // Prawy punkt pierwszej cyfry.
#define YD 30                                         // Dolny punkt siatki odcinków.
#define YM 50                                         // Środkowy punkt siatki odcinków.
#define YU 70                                         // Górny punkt siatki odcinków.
const byte nodeX[7] = { 0, XL, XR, XL, XR, XL, XR };  // Mapowanie węzłów na współrzędne X...
const byte nodeY[7] = { 0, YU, YU, YM, YM, YD, YD };  // oraz Y
#define LU 1                                          // Definicje indeksów węzłów według klucza: Left-Up itd
#define RU 2
#define LM 3
#define RM 4
#define LD 5
#define RD 6
const byte vectors[10][8] = {
  { RD, LD, LU, RU, RD, 0, 0, 0 },    // Tablica połączeń węzłów dla kolejnych cyfr, 0
  { LM, RU, RD, 0, 0, 0, 0, 0 },      // 1
  { LU, RU, RM, LD, RD, 0, 0, 0 },    // 2
  { LU, RU, LM, RM, LD, 0, 0, 0 },    // 3
  { RU, LM, RM, RD, 0, 0, 0, 0 },     // 4
  { RU, LU, LM, RM, LD, 0, 0, 0 },    // 5
  { RU, LM, LD, RD, RM, LM, 0, 0 },   // 6
  { LU, RU, LD, 0, 0, 0, 0, 0 },      // 7
  { LM, LU, RU, RD, LD, LM, RM, 0 },  // 8
  { RM, LM, LU, RU, RM, LD, 0, 0 }    // 9
};
void setup() {
  pwmX.begin(480000.0f, 0.0f);    // Częstotliwość pracy X = 480 kHz = 100 kroków.
  pwmY.begin(480000.0f, 0.0f);    // Częstotliwość pracy Y = 480 kHz = 100 kroków.
  pwmAudio.begin(1000.0f, 0.0f);  // Inicjalizacja PWM audio.
  randomSeed(analogRead(A0));     // Inicjalizacja generatora losowego szumem z pinu A0.
}
void loop() {
  if (soundEnd != 0 && millis() >= soundEnd) pwmAudio.pulseWidth_us(0);  // Wyłącz dźwięk, jeśli przyszedł na to czas.
  unsigned long currentMillis = millis();                                // Sczytaj czas systemowy.
  if (currentMillis - prevMillis >= refresh) {                           // Jeśli minął czas trwania jednej klatki gry...
    prevMillis = currentMillis;                                          // Przenieś sczytany czas systemowy do zmiennej do porównań.

    paddleRCenterY = map(analogRead(pot), 0, 1023, paddleLength, paddleTop);  // Oblicz położenie środka prawej paletki (gracza).
    if (currentMillis >= pause) {                                             // Jeśli nie trwa pauza po zdobyciu punktu...
      ballX += ballXSpeed;                                                    // Wyznacz nowy adres X piłki.
      ballY += ballYSpeed;                                                    // Wyznacz nowy adres Y piłki.

      if (ballY > paddleLCenterY) {                                        // Jeśli piłka znajduje się powyżej środka lewej paletki...
        paddleLCenterY += random(1, 3);                                    // Przesuń lewą paletkę (komputera) w górę.
        if (paddleLCenterY > paddleTop) paddleLCenterY = paddleTop;        // Zabezpiecz się na wypadek przekroczenia górnego zakresu ekranu.
      } else if (ballY < paddleLCenterY) {                                 // Jeśli piłka znajduje się poniżej środka lewej paletki...
        paddleLCenterY -= random(1, 3);                                    // Przesuń lewą paletkę (komputera) w dół.
        if (paddleLCenterY < paddleLength) paddleLCenterY = paddleLength;  // Zabezpiecz się na wypadek przekroczenia dolnego zakresu ekranu (chroni przed znikaniem).
      }
      if (ballY >= 100) {          // Jeśli piłka osiągnęła górną krawędź...
        beep(2000, 1);             // Dźwięk górnej krawędzi.
        ballYSpeed = -ballYSpeed;  // Zmień kierunek poruszania się piłki w dół.
        ballY = 100;               // W razie przekroczenia zakresu ustaw piłkę na górze ekranu
      } else if (ballY <= 0) {     // Jeśli piłka osiągnęła dolną krawędź...
        beep(1000, 1);             // Dźwięk dolnej krawędzi.
        ballYSpeed = -ballYSpeed;  // Zmień kierunek poruszania się piłki w górę.
        ballY = 0;                 // W razie przekroczenia zakresu ustaw piłkę na dole ekranu.
      }
      if (ballXSpeed < 0 && ballX <= 2 && abs(ballY - paddleLCenterY) <= paddleLength) {          // Odbij piłkę jeśli leci w lewo i trafia w komputer...
        beep(1500, 1);                                                                            // Dźwięk odbicia piłki przez komputer.
        ballYSpeed = (ballY - paddleLCenterY + random(-4, 5)) / 2;                                // Wyznacz nowy kąt z przesunięcia od środka paletki komputera.
        hitBall();                                                                                // Dokończ działania w podprogramie.
      } else if (ballXSpeed > 0 && ballX >= 98 && abs(ballY - paddleRCenterY) <= paddleLength) {  // Odbij piłkę jeśli leci w prawo i trafia w gracza...
        beep(750, 1);                                                                             // Dźwięk odbicia piłki przez gracza.
        ballYSpeed = (ballY - paddleRCenterY) / 2;                                                // Wyznacz nowy kąt z przesunięcia od środka paletki gracza.
        hitBall();                                                                                // Dokończ działania w podprogramie.
      }
      if (ballX < 0 || ballX > 100) {  // Jeśli piłka nie została przechwycona paletką (znalazła się poza ekranem w osi X)...
        if (ballX < 0) {               // Jeśli piłka wypadła z lewej strony...
          scoreR++;                    // Punkt zdobywa gracz.
          beep(63, 250);               // Dźwięk zdobycia punktu przez gracza.
        } else {                       // Jeśli piłka wypadła z prawej strony...
          scoreL++;                    // Punkt zdobywa komputer.
          beep(31, 250);               // Dźwięk zdobycia punktu przez komputer.
        }
        if (scoreL > 9 || scoreR > 9) {  // Jeśli jeden z liczników przekroczył maksimum...
          scoreL = 0;                    // Zeruj oba.
          scoreR = 0;
        }
        ballYSpeed = random(-3, 4);  // Losuj kąt rzutu.
        ballCount = 0;               // Zresetuj licznik odbić piłki dla nowej rundy.
        if (ballX < 0) {             // Jeśli wygrał gracz po prawej stronie...
          ballX = 98;                // Ustaw piłkę przed paletką gracza.
          ballY = paddleRCenterY;    // Piłka startuje z wysokości środka paletki gracza.
          ballXSpeed = -2;           // Skieruj piłkę w lewo.
        } else {                     // Jeśli wygrał komputer po lewej stronie...
          ballX = 2;                 // Ustaw piłkę przed paletką komputera.
          ballY = paddleLCenterY;    // Piłka startuje z wysokości środka paletki komputera.
          ballXSpeed = 2;            // Skieruj piłkę w prawo.
        }
        pause = millis() + 1000;  // Aktywuj pauzę.
      }
    }
  }
  pwmX.pulse_perc(0);                                // Rysuj lewą paletkę (komputera, od góry do dołu).
  int n = paddleLCenterY + paddleLength;             // Oblicz najwyższy punkt lewej paletki.
  do {                                               // Rysuj serię kropek, które stanowią obraz paletki.
    pwmY.pulse_perc(n);                              // Wyślij adres Y kolejnej kropki paletki.
    delayMicroseconds(300);                          // Wprowadź opóźnienie dające wyraźny rysunek paletki.
  } while (--n >= (paddleLCenterY - paddleLength));  // Powtarzaj dopóki nie narysujesz całej paletki.

  pwmX.pulse_perc(ballX);   // Wyślij adres X pozycji piłki.
  pwmY.pulse_perc(ballY);   // Wyślij adres Y pozycji piłki.
  delayMicroseconds(1000);  // Wprowadź opóźnienie dające wyraźny rysunek piłki.

  pwmX.pulse_perc(100);                              // Rysuj prawą paletkę (gracza, od dołu do góry).
  n = paddleRCenterY - paddleLength;                 // Oblicz najniższy punkt prawej paletki.
  do {                                               // Rysuj serię kropek, które stanowią obraz paletki.
    pwmY.pulse_perc(n);                              // Wyślij adres Y kolejnej kropki paletki.
    delayMicroseconds(300);                          // Wprowadź opóźnienie dające wyraźny rysunek paletki.
  } while (++n <= (paddleRCenterY + paddleLength));  // Powtarzaj dopóki nie narysujesz całej paletki.

  drawDigit(scoreL, 15);  // Rysuj wynik komputera, pozycja X
  drawDigit(scoreR, 65);  // Rysuj wynik gracza, pozycja X

  drawCourt();  // Rysuj obrys boiska.
}
void hitBall() {
  ballXSpeed = -ballXSpeed;              // Zmień kierunek poruszania piłki.
  ballCount++;                           // Zwiększ licznik odbić.
  if (ballCount == 10) ballXSpeed *= 2;  // Jeśli to dziesiąte odbicie, podwój szybkość poruszania się piłki.
}
void drawCourt() {  // Rysuj obrys boiska.
  byte x = 50;      // Punkty startowe.
  byte y = 0;
  pwmX.pulse_perc(x);  // Środkowa pionowa w górę.
  do {
    pwmY.pulse_perc(y);
    delayMicroseconds(5);
  } while (++y <= 99);
  pwmY.pulse_perc(y);  // Lewa górna w lewo.
  do {
    pwmX.pulse_perc(x);
    delayMicroseconds(5);
  } while (x-- > 1);
  pwmX.pulse_perc(x);  // Lewa pionowa w dół.
  do {
    pwmY.pulse_perc(y);
    delayMicroseconds(5);
  } while (y-- > 1);
  pwmY.pulse_perc(y);  // Dolna w prawo.
  do {
    pwmX.pulse_perc(x);
    delayMicroseconds(5);
  } while (++x <= 99);
  pwmX.pulse_perc(x);  // Prawa pionowa w górę.
  do {
    pwmY.pulse_perc(y);
    delayMicroseconds(5);
  } while (++y <= 99);
  pwmY.pulse_perc(y);  // Prawa górna w lewo.
  do {
    pwmX.pulse_perc(x);
    delayMicroseconds(5);
  } while (x-- > 51);
}
void drawDigit(byte digit, byte xOffset) {                  // Odzyskaj cyfrę oraz pozycję rysowania.
  byte step = 0;                                            // Numer kolejnego odcinka.
  while (step < 7) {                                        // Analizuj po kolei wszystkie odcinki.
    byte beginNode = vectors[digit][step];                  // Węzeł, od którego należy rysować odcinek.
    byte endNode = vectors[digit][step + 1];                // Węzeł, do którego należy rysować odcinek.
    if (endNode == 0) break;                                // Jeśli węzeł docelowy równy jest zero, zakończ rysowanie.
    byte beginX = xOffset + nodeX[beginNode];               // Początek odcinka w osi X.
    byte beginY = nodeY[beginNode];                         // Początek odcinka w osi Y.
    byte endX = xOffset + nodeX[endNode];                   // Koniec odcinka w osi X.
    byte endY = nodeY[endNode];                             // Koniec odcinka w osi Y.
    for (byte nextStep = 0; nextStep <= 10; nextStep++) {   // Rysuj wybrany odcinek krok po kroku, dobierz współczynnik dla płynności.
      byte x = beginX + ((endX - beginX) * nextStep) / 10;  // Oblicz napięcie dla kolejnego punktu osi X
      byte y = beginY + ((endY - beginY) * nextStep) / 10;  // Oblicz napięcie dla kolejnego punktu osi Y
      pwmX.pulse_perc(x);                                   // Wystaw napięcie dla osi X
      pwmY.pulse_perc(y);                                   // Wystaw napięcie dla osi Y
      delayMicroseconds(25);                                // Wprowadź opóźnienie dające wyraźny rysunek cyfr.
    }
    step++;  // Analizuj kolejny odcinek.
  }
}
void beep(int frequency, int time) {   // Procedura odtwarzająca dźwięki: częstotliwość (Hz), czas (ms)
  int period = 1000000 / frequency;    // Przelicz częstotliwość na okres w us
  pwmAudio.period_us(period);          // Ustaw wysokość dźwięku.
  pwmAudio.pulseWidth_us(period / 2);  // Ustaw wypełnienie 50%
  soundEnd = millis() + time;          // Zaplanuj czas wyłączenia dźwięku na początku głównej pętli.
}

Powołamy do życia trzeci już kanał PWM, który będzie odpowiadał za emisję dźwięków. Na pin 5 wstawimy typowy pasywny piszczek, czyli tak zwany buzzer. Dla cichych efektów nie potrzeba wzmacniacza, dobrze jednak równolegle do niego podłączyć rezystor o wartości kilooma. Będzie nam potrzebna także kolejna zmienna czasu systemowego, do wyciszania dźwięków po zadanym czasie. Pamiętać trzeba o inicjacji kanału PWM, która w Uno R4 wygląda inaczej niż na klasycznym, ośmiobitowym Arduino. Częstotliwość nie ma tu znaczenia, ważne że wypełnienie równe jest zeru – żadnych dźwięków na razie słychać nie będzie.

Ich wydobywanie zaczniemy od napisania procedury, która będzie to robić na podstawie prostych danych: częstotliwości w hercach i czasu w milisekundach. Częstotliwość zostanie przeliczona na okres w mikrosekundach i załadowana do zmiennej systemowej odpowiedzialnej za długość okresu PWM. Do kolejnej, która wyznacza czas trwania wysokiego stanu, załadujemy połowę poprzedniej wartości. Da to nam wypełnienie 50 procent, czyli dźwięk najgłośniejszy. Na końcu wstawimy czas trwania dźwięku w zmienną związaną z zegarem systemowym.

Na początku głównej pętli pojawi się pułapka na wyłączenie dźwięku, jeśli przyjdzie na to czas, czyli gdy załadowana zmienna długości piszczenia zrówna się z czasem systemowym. Teraz już wystarczy powypełniać kluczowe momenty odwołaniem się do generacji dźwięku naszym podprogramem. Pragnę zwrócić uwagę, że takie właśnie wyłączanie piszczenia – systemowym zegarem – jest w pełni odporne na wszelkie opóźnienia, jeśli tylko będziemy w miarę szybko trafiać do głównej pętli.

Zidentyfikowałem sobie trzy pary zdarzeń: odbicie piłki, odbicie się piłki od krawędzi górnej i dolnej oraz przepuszczenie piłki. Dla każdego z nich zadałem nieco inne parametry, ale pozostające ze sobą w porządku muzycznym. Tak więc odbicia od krawędzi mają różnicę oktawy, a od paletek – kwint względem krawędzi. Brzmi to bardzo retro. Strata punktu będzie brzmieć nisko, więc ponuro i będzie trwać dłużej. Zdobycie – zabrzmi wyżej. Wszelkie eksperymenty szanownych czytelników są tu wskazane. Nasz Pong jest już ukończony – można zacząć się bawić.

Płytka edukacyjna TME-EDU-ARD-2Płytka edukacyjna TME-EDU-ARD-2

Inne artykuły z tej kategorii

Nasi partnerzy

TMETech Master EventTME EducationPoweredby
Copyright © 2026 arduino.pl