[CZĘŚĆ 1] STM32Butterfly2: Tetris na STM32 – wprowadzenie do mechanizmu gry
Pierwsza pokazana funkcja o nazwie RCC_Configuration konfiguruje wewnętrzne generujące sygnały zegarowe. Ustawiono je tak by rdzeń mikrokontrolera taktowany był z maksymalną częstotliwością 72 MHz. Ponieważ na „motylu” znajduje się kwarc 25 MHz, należało użyć obu pętli PLL. Sygnał jest najpierw dzielony przez 5, następnie mnożony w pętli PLL2 razy 8, co daje wartość 40 MHz. Sygnał ten trafia na pętlę PLL1 gdzie jest ponownie dzielony przez pięć i mnożony razy 9, co daje wartość 72 MHz. Po tym zabiegu wystarczy użyć jako źródła sygnału zegarowego wyjścia pętli PLL1. Ponadto w procedurze ustawiane są opóźnienia dla pamięci Flash oraz dzielniki sygnału zegarowego dla magistrali systemowych.
Funkcja GPIO_Configuration konfiguruje GPIO, czyli wejścia/wyjścia mikrokontrolera. W mojej wersji procedury konfigurowany jest port E, do którego podłączony jest joystick LED.
Funkcja EXTI_Configuration konfiguruje przerwania pochodzące z GPIO. Wszystkie wejścia współpracujące z joystickiem są ustawione jako źródła przerwania. W dalszej części opiszę jak napisać funkcje obsługi tych przerwań.
Funkcja NVIC_Configuration konfiguruje kontroler przerwań NVIC. W tej wersji działanie funkcji sprowadza się do uruchomienia przerwań EXTI, czyli pochodzących z GPIO, do których podłączony jest joystick.
Kolejne zmiany, jakich dokonamy będą obejmowały plik stm32f10x_it.c znajdujący się w tym samym katalogu co nasz plik main.c. W pliku tym umieszczone są funkcje obsługujące przerwania, oczywiście w naszym nowym projekcie są one jeszcze puste. Brakuje tu jednak funkcji obsługujących przerwań z kontrolera EXTI, czyli tych które wywołane zostaną przez joystick. Należy więc dodać do pliku funkcje void EXTI9_5_IRQHandler(void) oraz void EXTI15_10_IRQHandler(void). Pierwsza z funkcji będzie wspólna dla przerwań z pinów 8 i 9 portu GPIOE, a druga dla przerwań z pinów 10, 11 i 12 tego portu. Musi więc istnieć mechanizm, który umożliwi nam sprawdzenie z jakiego pinu pochodzi przerwanie. Służy do tego funkcja EXTI_GetITStatus(). Przykładowo, aby sprawdzić czy przerwanie wywołał pin 8 stworzymy następujący warunek:
if(EXTI_GetITStatus(EXTI_Line8) != RESET)
Należy pamiętać, że sygnały przerwań pochodzące z kontrolera EXTI należy kasować ręcznie, wiec procedura wykonywana w razie zgodności warunku powinna się kończyć wywołaniem funkcji:
EXTI_ClearITPendingBit(EXTI_Line10);
W tym momencie mamy gotowy szablon projektu. Przygotowywana gra polega na takim układaniu spadających klocków, aby ich segmenty tworzyły ciągłe poziome linie na planszy. Każda ułożona linia znika robiąc miejsce na kolejne klocki. W mojej wersji gry plansza ma wymiary 10 na 20 pól. Planszę w pamięci mikrokontrolera będzie odzwierciedlała tablica zmiennych:
unsigned char plansza[10][20];
Każda komórka tej tablicy reprezentuje jedno pole planszy, w najprostszy możliwy sposób. Jeżeli komórka przechowuje 0 to znaczy, że pole jest puste. Wartość komórki równa 1 sygnalizuje, że w danym polu znajduje się segment klocka. Współrzędna 0,0 będzie wskazywała na pole w górny lewym narożniku planszy. Ktoś zaraz się wzburzy na widok tak beztrosko marnowanej pamięci. Jest to jednak działanie celowe. Mamy z tego dwie korzyści: znaczne uproszczenie programu, bowiem artykuł skierowany jest głównie do początkujących oraz możliwość przyszłej rozbudowy programu, np. klocki na kolorowym wyświetlaczu mogą mieć różne kolory.
Pierwszy fragment programu zeruje tablicę. Umieśćmy go w osobnej funkcji tak, aby było nam wygodnie wykonać tę czynność z dowolnego fragmentu naszej gry:
void CzyscPlansze(void) { unsigned char x,y; for(y=0;y<20;y++) //odliczanie wierszy for(x=0;x<10;x++) //odliczanie kolumn plansza[x][y] = 0; //wypełnianie komórek zerami }
W funkcji użyłem dwóch pętli for do odliczania współrzędnych x i y. Do każdej komórki jest zapisywane 0. W tablicy będzie odwzorowana tylko statyczna część planszy, czyli klocki, które już opadły i zatrzymały się na dnie planszy. Potrzebujemy jeszcze zmiennych, które będą przechowywały informacje o klocku, który spada. Do tego celu przeznaczono zmienne:
unsigned char klocekx, kloceky, klocekr;
przechowujące współrzędne x i y klocka na planszy oraz informacje o kącie, o jaki obrócony jest klocek. Zmienna klocekr może przyjmować jedną z czterech wartości, od 0 do 3, które odpowiadają kątom obrotu o 0, 90, 180 i 270 stopni. Wartość kolejnej zmiennej:
unsigned char kloceknr;
odpowiada jednemu z 7 różnych kształtów spadających klocków. Jak zatem program odwzorowuje kształt klocka na planszy? W pamięci programu znajduje się tablica stałych przechowująca współrzędne każdego segmentu każdego rodzaju klocka w każdym z 4 pozycji obrotu. Tablica ma postać pokazaną na rysunku 3.
Rys. 3. Opis współrzędnych segmentów klocków w zależności od ich pozycji
Na rysunku 4 pokazano fragment tablicy opisujący kształt jednego z klocków.
Rys. 4. Opis w tablicy jednego z klocków
W brew pozorom korzystanie z takiej tablicy w programie jest bardzo proste. Dodanie współrzędnej x danego segmentu odczytanej z tablicy do współrzędnej x całego klocka da nam położenie x tego segmentu na planszy. Dodatkowym ułatwieniem jest fakt, że ilość segmentów dla każdego z 7 rodzajów klocka jest stała i wynosi 4. Teraz wystarczy zmieniać współrzędne klocka, aby zmieniać położenie na planszy każdego z jego 4 segmentów. Np. aby klocek spadał w dół należy cyklicznie zwiększać o 1 jego współrzędną y. Jednak skąd będziemy widzieli kiedy klocek oprze się na istniejących już segmentach na planszy i nie powinien spadać dalej? Należy zbudować funkcję, która będzie sprawdzała czy wystąpiła kolizja klocka z segmentami na planszy. Trzeba sprawdzić kolejno każdy z segmentów klocka, do tego posłuży nam pętla:
for(i=0;i<4;i++) { ... }
W każdej iteracji pętli należy obliczyć współrzędne x i y danego segmentu klocka na planszy:
x=klocki[kloceknr][r][i][0]+kx-2; y=klocki[kloceknr][r][i][1]+ky-2;
gdzie kx, ky oraz r to położenie klocka i sprawdzić czy nie ma w tym miejscu położonego już na stałe segmentu:
if(plansza[x][y]) kolizja=1;