Table of Contents

Minesweeper

Introducere

Nume: Toader Ana-Maria
Grupa: 334CB

Joc Minesweeper pe un LCD.

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:

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:


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:

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:

/* 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).

/* 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:

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;
    }
}
void ISR_joystick() {
    joystickButtonFlag = true;
}
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

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

Github repo

Jurnal

Probleme întâmpinate:​

Bibliografie/Resurse

Datasheet Arduino UNO R3
Datasheet ATmega328P
Datasheet ILI9341
Adafruit GFX Library
arduino-songs

Export to PDF