Minesweeper

Introducere

Nume: Toader Ana-Maria
Grupa: 334CB

Joc Minesweeper pe un LCD.

  • jucătorul controlează mișcările prin intermediul unui joystick și a două butoane
  • un board (solvable) randomizat este generat pentru fiecare nou joc
  • un buzzer emite sunete la realizarea unei mișcări greșite sau la câștigarea jocului
  • fiecare joc are o limită de timp de 5 minute; la expirarea timpului, jocul este pierdut

Scopul proiectului este realizarea unui joc entertaining.

Descriere generală

Implementarea jocului Minesweeper pe un LCD. Controlul se va face printr-un joystick și butoane. Va avea un timer și un buzzer care scoate sunete la câștigarea / pierderea jocului.

Placa de dezvoltare compatibilă cu Arduino UNO (ATmega328p) controlează buzzer-ul și display-ul, primind input de la două butoane și un joystick. Pentru comunicarea cu LCD-ul folosește protocolul SPI. Prin joystick se controlează mișcările jucătorului pe tabla de joc, iar prin cele două butoane se poate selecta tipul celulei: clear sau flagged. Obiectivul jocului este descoperirea tuturor celulelor libere. Atunci când pe tablă rămân doar mine nedescoperite, jocul este câștigat, iar buzzer-ul va emite un sunet specific. Alternativ, la o mișcare greșită, jocul se încheie, iar sunetul emis de buzzer anunță înfrângerea.

Hardware Design

Schema electrică:

Lista de componente:

Nume componentă Link achiziție Cantitate Preț unitar
Arduino UNO R3 Placă de dezvoltare 1 39,37 lei
2.8” SPI LCD module cu controller ILI9341 Display 1 69,99 lei
Modul joystick biaxial Joystick 1 5,35 lei
Modul cu buzzer activ Buzzer 1 2,99 lei
Push button Buton 2 1,99 lei
Rezistor 10kΩ Rezistor 10kΩ 6 0.10 lei
Rezistor 100kΩ Rezistor 100kΩ 3 0.10 lei
Diodă 1N4007 Diodă 1N4007 2 0.49 lei
Breadboard HQ (400 points) Breadboard 2 4,56 lei
Fire rigide Set fire rigide 1 12,49 lei
Fire tată-tată Set fire tată-tată 2 2,85 lei
Cost total: 150,87 lei

Cablaj final:

Software Design

Setup

Mediu de dezvoltare: Visual Studio Code + PlatformIO
Librării:

  • SPI
    • oferă suport pentru resursele hardware care folosesc protocolul SPI
  • Adafruit_GFX
    • oferă o serie de primitive grafice (linii, forme geometrice)
  • Adafruit_ILI9341
    • folosită pentru interfațarea cu display-ul care folosește chip-ul ILI9341
    • oferă o serie de funcții pentru randarea formelor geometrice, textului și a imaginilor, color management, touchscreen integration

Implementare

Logica jocului
Tabla de joc este definită ca un array bidimensional în care fiecare celulă poate lua una dintre valorile predefinite ce simbolizează elementele de joc (BLANK, BOMB, RED_BOMB, FLAG, EMPTY, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT).

extern unsigned int board[ROWS][COLUMNS];

Pentru implementarea logicii jocului am scris funcții pentru diverse funcționalități:

  • inițializarea bombelor în poziții random pe grid (tabla este randomizată la fiecare joc nou)
  • indicarea poziției pe grid prin highlight-ul unei celule
  • input handling pentru acțiunile jucătorului (apăsarea butoanelor, mișcarea joystick-ului sau apăsarea switch-ului de pe joystick)
  • descoperirea unei singure celule ce conține un număr sau a tuturor celulelor libere adiacente în cazul unei celule goale
  • amplasarea de flag-uri
  • afișarea unui timer care se actualizează la interval fix de o secundă
  • resetarea jocului la apăsarea switch-ului de pe joystick (cu reinițializarea tuturor parametrilor jocului, a timer-ului și randomizarea tablei)
  • verificarea condițiilor de win / lose și în funcție de caz:
    • afișarea mesajelor specifice (GAME OVER sau CONGRATULATIONS)
    • emiterea unor sunete distinctive acționând buzzer-ul
    • la finalul jocului singura acțiune care mai este responsive este apăsarea switch-ului de pe joystick care duce la începerea unui joc nou


Grafică
Pentru a crea imaginile distinctive jocului pentru fiecare celulă posibilă am desenat imagini de 20x20px pe care le-am convertit folosind un tool online în bitmap-uri grayscale de 8biți per pixel. Le-am afișat la poziții corespunzătoare pe ecran folosind funcția void Adafruit_GFX::drawBitmap(int16_t x, int16_t y, const uint8_t bitmap[], int16_t w, int16_t h, uint16_t color) din biblioteca Adafruit_GFX. Am adăugat culori prin setarea foreground color.

const unsigned char bomb[] PROGMEM = {
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0xf0, 0x00, 0x0b, 0xfd, 0x00, 0x07,
	0xfe, 0x00, 0x16, 0x7e, 0x80, 0x0e, 0x7f, 0x00, 0x0f, 0xff, 0x00, 0x1f, 0xff, 0x80, 0x1f, 0xff,
	0x80, 0x0f, 0xff, 0x00, 0x0f, 0xff, 0x00, 0x17, 0xfe, 0x80, 0x07, 0xfe, 0x00, 0x0b, 0xfd, 0x00,
	0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};


Buzzer
La sfârșitul jocului, buzzer-ul emite sunete distinctive. Am ales două melodii din arduino-songs pentru cazurile de victorie / înfrângere.

Timer
Microcontroller-ul ATmega328p conține 3 unități de timer, două pe 8 biți (Timer0 și Timer2) și unul pe 16 biți (Timer1).
Am folosit Timer1 pentru a genera întreruperi la intervale fixe de 1 secundă, pentru a afișa un timer pe display. La expirarea timpului înainte ca jocul să se fi încheiat, jucătorul pierde.

Am folosit formula de calcul:

timer_count = clock_frequency / (prescaler * interrupt_frequency) - 1

Conform datasheet-ului ATmega328p:

  • pentru setarea prescaler-ului am setat bitul CS12 din registrul TCCR1B

  • pentru setarea întreruperilor am activat bitul OCIE1A din registrul TIMSK1

  • am setat pragul numărătorii la care se va declanșa întreruperea (timer_count) în registrul OCR1A

Frecvența de funcționare a procesorului este 16 MHz.

#define CLOCK_FREQUENCY 16000000    /* 16 MHz */
#define INTERRUPT_FREQUENCY 1       /* 1Hz corresponds to 1 second period */
#define PRESCALER 256
#define TIMER_COUNT (CLOCK_FREQUENCY / (PRESCALER * INTERRUPT_FREQUENCY) - 1)
void init_timer1() {
    /* Reset control registers for Timer 1 */
    TCCR1A = 0;
    TCCR1B = 0;
 
    /* The prescaler value is 256 -> set the CS12 bit */
    TCCR1B |= (1 << CS12);
 
    /* Make the comparator trigger Timer/Counter1 input capture interrupt
    -> set the OCIE1A bit in the timer interrupt mask register (TIMSK1). */
    TIMSK1 |= (1 << OCIE1A);
 
    /* Set the threshold value (timer_count) for Timer 1 */
    OCR1A = TIMER_COUNT;
}


Întreruperi
La inițializare, am activat mecanismul de întreruperi prin activarea bitului I din registrul SREG.

/* Activate interrupts */
sei();

Am definit două rutine pentru tratarea întreruperilor externe (pentru apăsarea butoanelor și pentru apăsarea butonului de la joystick) și una pentru tratarea întreruperilor interne folosind Timer1.

Configurarea componentelor care vor trimite întreruperi:

  • cele două butoane sunt conectate la același pin BUTTON_INTERRUPT pentru întreruperi
/* initialize button pins */
pinMode(BLUE_BUTTON, INPUT);
pinMode(RED_BUTTON, INPUT);
 
pinMode(BUTTON_INTERRUPT, INPUT);
attachInterrupt(digitalPinToInterrupt(BUTTON_INTERRUPT), ISR_button, RISING);

Am folosit funcția attachInterrupt pentru a atașa rutina ISR_button evenimentelor de pe pinul corespunzător butoanelor. Parametrul RISING definește momentul în care va fi declanșată întreruperea - atunci când valoarea pinului trece de la LOW la HIGH (la apăsarea unuia dintre butoane).

  • pinul SW al joystick-ului este conectat la pinul JOYSTICK_INTERRUPT de pe plăcuță, căruia i-am asociat rutina ISR_joystick
/* initialize joystick pins */
pinMode(JOYSTICK_X, INPUT);
pinMode(JOYSTICK_Y, INPUT);
 
pinMode(JOYSTICK_INTERRUPT, INPUT);
digitalWrite(JOYSTICK_INTERRUPT, HIGH);
attachInterrupt(digitalPinToInterrupt(JOYSTICK_INTERRUPT), ISR_joystick, RISING);

Toate variabilele care vor fi modificate într-o rutină de tratare a întreruperilor trebuie marcate ca volatile pentru a indica compilatorului să nu treacă variabila prin cache. Orice acces la o variabilă volatile se va face prin RAM.

/* Initialize volatile variables used with button interrupts */
volatile bool blueButtonFlag = false;
volatile bool redButtonFlag = false;
 
volatile unsigned long lastPressRed = 0;
volatile unsigned long lastPressBlue = 0;
 
volatile bool joystickButtonFlag = false;

Definirea rutinelor de tratare a întreruperilor:

  • pentru butoane - declanșarea întreruperii are loc la apăsarea unuia dintre butoane, moment în care se verifică care dintre butoane a fost apăsat și se setează flagul corespunzător acestuia (blueButtonFlag sau redButtonFlag. Pentru a trata existența zgomotului, am implementat debouncing software pentru butoane.
void ISR_button() {
    buttonPressTime = millis();
    if (digitalRead(BLUE_BUTTON) && buttonPressTime - lastPressBlue > debounceTime) {
        lastPressBlue = buttonPressTime;
        blueButtonFlag = true;
 
    } else if (digitalRead(RED_BUTTON) && buttonPressTime - lastPressRed > debounceTime) {
        lastPressRed = buttonPressTime;
        redButtonFlag = true;
    }
}
  • pentru joystick - declanșarea întreruperii are loc la apăsarea switch-ului, iar rutina de tratare a acesteia setează flag-ul joystickButtonFlag care va fi folosit în logica programului
void ISR_joystick() {
    joystickButtonFlag = true;
}
  • pentru Timer1 - întreruperea este declanșată la intervale fixe de o secundă; în rutina de tratare se resetează registrul counter la zero pentru a putea relua numărătoarea, se decrementează valoarea timer-ului și se setează flag-ul corespunzător timerFlag pentru marcarea faptului că a avut loc întreruperea în fluxul principal al programului
ISR(TIMER1_COMPA_vect){
    TCNT1 = 0;   /* Reset counter register */
 
    timer--;
    timerFlag = true;
}

Parametrul TIMER1_COMPA_vect indică faptul că se face Compare Match cu pragul A al timerului.

Rezultate Obţinute

  • inițializarea jocului, cu setarea timer-ului la 5 minute

  • eliberarea unui grup de celule

  • sfârșitul jocului

  • resetarea jocului la apăsarea switch-ului de pe joystick, cu resetarea timer-ului la 5 minute

Concluzii

Un proiect interesant, mă bucur că am obținut ceva funcțional.

Deși am ales un proiect simplu, cu puține componente hardware (pentru a nu avea mari bătăi de cap :-)) am avut mari bătăi de cap m( încercând să fac display-ul să funcționeze la tensiunea de alimentare de 3v3.

Download

Jurnal

  • 01/05/2024 - alegere temă proiect
  • 04/05/2024 - finalizarea documentației + schema bloc
  • 07/05/2024 - testarea componentelor hardware
  • 12/05/2024 - finalizare hardware design
  • 17/05/2024 - Milestone 2 (hardware)
  • 21/05/2024 - start code development
  • 24/05/2024 - Milestone 3 (software)

Probleme întâmpinate:​

  • Plăcuța de dezvoltare Arduino UNO R3 are doar doi pini digitali ce suportă întreruperi externe (D2, D3). Aveam nevoie să configurez 3 componente pentru a folosi întreruperi (două butoane și un joystick), dar doar doi pini disponibili. Am folosit două diode pentru a multiplexa butoanele pe același pin.
  • Alimentarea modulului LCD funcționează la tensiunea de 3.3V, iar tensiunea de funcționare a plăcii de dezvoltare este de 5V. Am încercat inițial să folosesc un translator de nivel logic. După mult timp pierdut (și un display ars :-\) nu am reușit să îl fac să funcționeze. În urma indicațiilor laborantului,​ am ales să introduc în circuit rezistențe de 10kΩ.
  • Active buzzer module pe care intenționam să îl folosesc inițial nu funcționa, l-am înlocuit cu un buzzer pasiv de 5V.

Bibliografie/Resurse

pm/prj2024/azamfir/ana_maria.toader02.txt · Last modified: 2024/05/27 03:27 by ana_maria.toader02
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0