#include <avr/interrupt.h>
#include "wiring_private.h"
#define PORT PORTB
#define DDR DDRB
#define NBIT 0 // Il pin fisico D8 (Bit 0 della porta B)
#define NPIX 32 // Numero totale dei LED sulla matrice
#define RSPACE (NPIX*3) // 3 byte per ogni LED (Verde, Rosso, Blu)
uint8_t vpix[RSPACE]; // L'array che farà da "tavolozza" per i colori
//Variabile auto incrementante che farà da metronomo del sistema
volatile unsigned long contatore_ms = 0;
//Variabili per controllare il debounce del pulsante del giocatore 1
volatile unsigned long tempo_pressione_1 = 0;
volatile uint8_t flag_stato_pressione_1 = 0;
//Variabili per controllare il debounce e per il controllo della durata della pressione del pulsante dell'arbitro
volatile unsigned long tempo_pressione_2 = 0;
volatile uint8_t pressione_arbitro = 0;
volatile uint8_t flag_arbitro_set = 0;
volatile uint8_t flag_arbitro_reset = 0;
//Variabili per controllare il debounce del pulsante del giocatore 2
volatile unsigned long tempo_pressione_3 = 0;
volatile uint8_t flag_stato_pressione_3 = 0;
//Variabili per l'Automa del Gioco
uint8_t stato_gioco = 0;
uint8_t dado = 1;
unsigned long tempo_animazione_led = 0;
uint8_t stato_lampeggio_led = 0;
uint8_t turno_corrente = 1; // 1 = Tocca al G1, 2 = Tocca al G2
uint8_t lanci_effettuati = 0; // Conta quanti hanno lanciato (max 2 per round)
//Variabili dei punteggi dei giocatori
uint8_t valore_dado_1 = 0;
uint8_t valore_dado_2 = 0;
uint8_t punteggio_1 = 0;
uint8_t punteggio_2 = 0;
// Variabili per gestire i tempi delle animazioni dentro il gioco
unsigned long tempo_animazione = 0;
void setup() {
//Faccio il reset dei registri che mi servono, TIMER/COUNTER CONTROL REGISTER 0 A e TIMER/COUNTER CONTROL REGISTER 0 B
TCCR0A = 0;
TCCR0B = 0;
//Setto il fattore di divisione del prescaler a 64 usando i bit CS00, CS01, CS02 impostati ad 1, 1 e 0, ma dopo il reset di TCCR0B CS02 è già a zero quindi non ci serve riscriverlo.
TCCR0B |= (1 << CS00) | (1 << CS01);
//Imposto il valore iniziale del contatore
TCNT0 = 0;
//Imposto il valore che fungerà da soglia (TOP) dentro il registro OCR0A (OUPUT COMPARE REGISTER 0 A).
OCR0A = 249;
//Imposto la modalità CTC (Clear Timer on Compare Match mode) del registro TCCR0A usando i bit WGM00, WGM01 ed il bit WGM02 del registro TCCR0B.
//In questo caso dobbiamo impostare ad uno solo il bit di WGM01 perchè WGM00 e WGM02 sono già a zero grazie al reset dei registri fatto sopra.
TCCR0A |= (1 << WGM01);
//Ora, per far in modo che quando il valore di TCNT0 e quello di OCR0A combacino ci sia un evento di interrupt, usiamo il registro TIMSK0 (TIMER COUNTER 0 INTERRUPT MASK REGISTER)
//Per farlo, devo impostare ad 1 il bit OCIEA (OUTPUT COMPARE ENABLE 0 A).
TIMSK0 |= (1 << OCIE0A);
//Non ci rimane che rendere questo evento di interrupt un evento globale
//Usiamo l'istruzione sei() (SET GLOBAL INTERRUPT FLAG), cambiando il bit I del registro SREG per far accettare al microcontrollore il nostro interrupt globale.
sei();
//Avendo 3 pulsanti da premere che devono governare il gioco dei dadi che stiamo implementando, dobbiamo far in modo che il segnale che arriva al microcontrollore sia un segnale in entrata.
//Per farlo, usiamo il registro DDRB (Data Direction Register B) che memorizzerà la configurazione di direzione della corrente dei rispettivi pin (0 IN 1 OUT) usando la funzione cbi (clear bit 0).
cbi(DDRB, 1);
cbi(DDRB, 2);
cbi(DDRB, 3);
//Ci serve anche che il microntrollore fornisca il voltaggio necessario per attivare il meccanismo di pull-up dei pulsanti per controllarne lo stato logico: 1 (tensione stabile, il pulsante non è premuto) 0 (caduta di tensione, il pulsante è stato premuto).
//Per questo usiamo un altro registro, il PORTB (Port B Data Register), che se impostato ad 1, attiva la resistenza di Pull-Up interna. Questa debole resistenza "tira" la linea a 5 Volt in modo stabile.
//Il PINB (Port B Input Pins Address), ovvero il registro a sistema degli input che memorizza lo stato logico di ogni pin esterno, che opera come un voltmetro passivo, vedendo questa tensione stabile da PORTB verso tutti e tre i pulsanti, imposta lo stato come 1.
//Quando uno dei tre pulsanti viene premuto, il circuito si chiude verso la massa, facendo così crollare la tensione e facendo cambiare lo stato logico del pin a cui è collegato il pulsante dentro il registro PINB a 0.
//Usando la funzione sbi (SET BIT 1) sui bit relativi ai pin collegati a cui sono collegati i nostri pulsanti nel registro PORTB, possiamo attivare la nostra resistenza di Pull-up interna.
sbi(PORTB, 1);
sbi(PORTB, 2);
sbi(PORTB, 3);
//Ora, unisco l'evento dell'interrupt al registro dei pin PCICR (Pin Change Interrupt Control Register) che mi permette di abilitare o disabilitare l'evento di un interrupt in relazione allo stato di un pin
//Ricordando che il nostro blocco di pin appartiene alla zona 0, dobbiamo lavorare sul bit PCIE0 (Pin Change Interrupt Enable 0), che collegato alla nostra funzione sei() ci permette di abilitare un evento di interrupt localizzato soltanto nella zona dei nostri pin
PCICR |= (1 << PCIE0);
//Ora, abbiamo bisogno di dire al nostro registro PCICR che non tutti i pin della zona 0 ci servono per il nostro evento di interrupt, ma soltanto 3 a cui sono collegati i nostri pin.
//Per farlo, si usa in tandem il registro PCMSK0 (Pin Change Mask Register 0), relativo alla zona dei nostri pin, ed attivando solo i bit del registro relativi ai 3 pin a cui sono collegati i nostri 3 pulsanti.
PCMSK0 |= (1 << PCINT1) | (1 << PCINT2) | (1 << PCINT3);
// Impostazioni uscita per la matrice LED (Pin D8 / PB0)
sbi(DDR, NBIT);
cbi(PORT, NBIT);
}
//Definiamo la funzione ISR(TIMER0_COMPA_vect) che farà in modo che l'esecuzione della funzione di loop() venga sospesa immediatamente dal microcontrollore ogni millisecondo.
ISR(TIMER0_COMPA_vect) {
contatore_ms++;
}
//Ora passiamo al controllo sullo stato dei pulsanti.
// Per farlo non ci serve altro se non fare un confronto con lo stato logico del registro PINB (il nostro voltmetro) precedentemente spiegato.
//I nostri pulsanti sono collegati fisicamente ai pin della Porta B, che vanno da 0 a 7, di cui i nostri sono l'1, il 2 ed il 3 (lo 0 è riservato alla matrice //led).
//Ai nostri pin sono collegati dei canali di interrupt chiamati PCINT (PIN CHANGE INTERRUPT), in questo caso quelli che ci servono sono PCINT1, PCINT2 E
//PCINT3.
//Ora possiamo creare una condizione logica in cui, applicando una maschera bit a bit (operatore &) sul registro PINB tramite i nostri sensori PCINT, //riusciamo a isolare la lettura. Se il bit specifico risulta uguale a 0 (crollo di tensione = pulsante premuto), sappiamo esattamente quale pulsante ha //generato l'evento."
//Per evitare letture errate confrontiamo il contatore_ms di sistema con l'ultimo tocco registrato per applicare un filtro antirimbalzo software //(Debounce) di 30ms.
//Dopo questo controllo, possiamo impostare una variabile globale che comunicherà l'evento di interrupt all'automa principale all'interno del loop().
ISR(PCINT0_vect) {
//Metto il primo pulsante del giocatore 1 in ascolto usando il registro PINB come memoria dello stato del pin ed imposto il bit del pin PCINT1 a cui è collegato il pulsante ad uno per controllarne lo stato, se è stato premuto, controllo prima la durata della pressione con il debounce per non mandare false flag al microcontrollore, ed infine
//registro la pressione sovrascrivendo il tempo di pressione ed impostando il bit della flag dell'interrupt ad 1.
if ((PINB & (1 << PCINT1)) == 0) {
if (contatore_ms - tempo_pressione_1 >= 30) {
tempo_pressione_1 = contatore_ms;
flag_stato_pressione_1 = 1;
}
}
//Faccio lo stesso per il pin PCINT2 aggiungendo una seconda condizione per il pulsante dell'arbitro, che con una pressione prolungata causa il reset del gioco.
if ((PINB & (1 << PCINT2)) == 0) {
if (pressione_arbitro == 0) {
tempo_pressione_2 = contatore_ms;
pressione_arbitro = 1;
}
}
else {
if (pressione_arbitro == 1) {
unsigned long durata_pressione = contatore_ms - tempo_pressione_2;
if (durata_pressione >= 30) {
if (durata_pressione >= 2000) {
flag_arbitro_reset = 1;
} else {
flag_arbitro_set = 1;
}
}
pressione_arbitro = 0;
}
}
//Ed infine per il pin PCINT3 per il terzo pulsante del secondo giocatore.
if ((PINB & (1 << PCINT3)) == 0) {
if (contatore_ms - tempo_pressione_3 >= 30) {
tempo_pressione_3 = contatore_ms;
flag_stato_pressione_3 = 1;
}
}
}
void loop() {
dado++;
if (dado > 6) {
dado = 1;
}
if (flag_arbitro_reset == 1) {
flag_arbitro_reset = 0;
stato_gioco = 0;
punteggio_1 = 0;
punteggio_2 = 0;
lanci_effettuati = 0;
}
switch (stato_gioco) {
case 0:
if (flag_arbitro_set == 1) {
flag_arbitro_set = 0;
turno_corrente = (contatore_ms % 2) + 1;
lanci_effettuati = 0;
stato_gioco = 1;
}
break;
case 1:
//Un lampeggio a 4hz equivale ad un lampeggio ogni 125 millisecondi.
if (contatore_ms - tempo_animazione_led >= 125) {
tempo_animazione_led = contatore_ms;
if(stato_lampeggio_led == 0) {
stato_lampeggio_led = 1;
rappresenta_dado();
}
else {
stato_lampeggio_led = 0;
for (uint8_t i = 0; i < 32; i++) { imposta_colore_led(i, 0, 0, 0); }
pix_showI();
}
}
if (turno_corrente == 1) {
if (flag_stato_pressione_1 == 1) {
flag_stato_pressione_1 = 0;
tempo_animazione_led = contatore_ms;
stato_gioco = 2;
}
}
else if (turno_corrente == 2) {
if (flag_stato_pressione_3 == 1) {
flag_stato_pressione_3 = 0;
tempo_animazione_led = contatore_ms;
stato_gioco = 2;
}
}
break;
case 2:
rappresenta_dado();
if (contatore_ms - tempo_animazione_led >= 2000) {
if (turno_corrente == 1) {
valore_dado_1 = dado;
} else {
valore_dado_2 = dado;
}
lanci_effettuati++;
if (lanci_effettuati == 2) {
stato_gioco = 3;
} else {
if (turno_corrente == 1) { turno_corrente = 2; }
else { turno_corrente = 1; }
stato_gioco = 1;
}
}
break;
case 3:
if (valore_dado_1 > valore_dado_2) {
punteggio_1++;
} else if (valore_dado_2 > valore_dado_1) {
punteggio_2++;
}
if(punteggio_1 == 4 || punteggio_2 == 4) {
stato_gioco = 4;
} else {
stato_gioco = 0;
}
break;
case 4:
break;
}
}
void imposta_colore_led(uint8_t numero_led, uint8_t r, uint8_t g, uint8_t b) {
// L'hardware WS2812 vuole l'ordine GRB (Verde, Rosso, Blu)
vpix[numero_led * 3] = g;
vpix[(numero_led * 3) + 1] = r;
vpix[(numero_led * 3) + 2] = b;
}
void rappresenta_dado() {
// 1. Per prima cosa, conviene spegnere l'area del dado precedente
// per non sovrapporre i puntini. (Puoi fare un ciclo for che mette a 0 i led interessati)
uint8_t numero_led;
for (numero_led = 0; numero_led < 32; numero_led++) {
imposta_colore_led (numero_led, 0, 0, 0);
}
// 2. Logica del Giocatore 1 (In alto a sinistra)
if (turno_corrente == 1) {
switch(dado) {
case 1:
imposta_colore_led (25, 255, 0, 0);
break;
case 2:
imposta_colore_led (30, 255, 0, 0);
imposta_colore_led (20, 255, 0, 0);
break;
case 3:
imposta_colore_led (30, 255, 0, 0);
imposta_colore_led (25, 255, 0, 0);
imposta_colore_led (20, 255, 0, 0);
break;
case 4:
imposta_colore_led (30, 255, 0, 0);
imposta_colore_led (28, 255, 0, 0);
imposta_colore_led (22, 255, 0, 0);
imposta_colore_led (20, 255, 0, 0);
break;
case 5:
imposta_colore_led (30, 255, 0, 0);
imposta_colore_led (28, 255, 0, 0);
imposta_colore_led (25, 255, 0, 0);
imposta_colore_led (22, 255, 0, 0);
imposta_colore_led (20, 255, 0, 0);
break;
case 6:
imposta_colore_led (30, 255, 0, 0);
imposta_colore_led (28, 255, 0, 0);
imposta_colore_led (26, 255, 0, 0);
imposta_colore_led (24, 255, 0, 0);
imposta_colore_led (22, 255, 0, 0);
imposta_colore_led (20, 255, 0, 0);
break;
}
}
// 3. Logica del Giocatore 2 (In basso a sinistra)
else if (turno_corrente == 2) {
switch(dado) {
case 1:
imposta_colore_led (5, 0, 0, 255);
break;
case 2:
imposta_colore_led (10, 0, 0, 255);
imposta_colore_led (0, 0, 0, 255);
break;
case 3:
imposta_colore_led (10, 0, 0, 255);
imposta_colore_led (5, 0, 0, 255);
imposta_colore_led (0, 0, 0, 255);
break;
case 4:
imposta_colore_led (10, 0, 0, 255);
imposta_colore_led (8, 0, 0, 255);
imposta_colore_led (2, 0, 0, 255);
imposta_colore_led (0, 0, 0, 255);
break;
case 5:
imposta_colore_led (10, 0, 0, 255);
imposta_colore_led (8, 0, 0, 255);
imposta_colore_led (5, 0, 0, 255);
imposta_colore_led (2, 0, 0, 255);
imposta_colore_led (0, 0, 0, 255);
break;
case 6:
imposta_colore_led (10, 0, 0, 255);
imposta_colore_led (8, 0, 0, 255);
imposta_colore_led (6, 0, 0, 255);
imposta_colore_led (4, 0, 0, 255);
imposta_colore_led (2, 0, 0, 255);
imposta_colore_led (0, 0, 0, 255);
break;
}
}
cli(); // STACCA IL TELEFONO: Ferma il Timer e i bottoni
pix_showI(); // Manda i dati ai LED in modo perfetto e indisturbato
sei(); // RIATTACCA IL TELEFONO: Riaccendi gli allarmi!
}
volatile void pix_showI() {
uint8_t cb, buf, nb=RSPACE;
asm volatile (
"lwhile0:\n\t"
"ld %[buf], %a[vp]+ \n\t" // (c17-18) Z=vp, byte=*Z++
"ldi %[cb], 8 \n\t" // (c19) e contatore bit
"lwhile:\n\t"
"nop \n\t" // (C+1=20) Fine ciclo: tau = 62,5*20 = 1,25 us
"sbi %[port],%[nbit] \n\t"// (c1) inizia il segnale H
"rjmp .+0 \n\t" // (c3-4) delay 2
"rjmp .+0 \n\t" // (c5-6) delay 2
"sbrs %[buf], 7 \n\t" // (c7)Test bit 7, skip se 1
"cbi %[port],%[nbit] \n\t"// (c8) se bit 0 impulso corto
"add %[buf], %[buf] \n\t" // (c9) r24 <<=1;
"subi %[cb], 1 \n\t" // (c10) Decremento contatore bit
"breq vnextb \n\t" // (c11) ciclo lungo, son finiti i bit
"rjmp .+0 \n\t" // (c12,13) delay 2
"cbi %[port],%[nbit] \n\t"// (c14) se bit 1, impulso lungo da 13
"rjmp .+0 \n\t" // (c15,16) delay 2
"nop \n\t" // (C17)
"rjmp lwhile \n\t" // (c18,19) entra nel ciclo;
"vnextb:\n\t" // (c12)
"subi %[nb], 1 \n\t" // (c13) Decremento contatore byte
"cbi %[port],%[nbit] \n\t"// (c14) se bit 1, impulso lungo da 13
"brne lwhile0 \n\t" // (c15,16) ... ciclo lungo x cambio byte
//
: // output list
: [vp] "e" (vpix),
[port] "i" (_SFR_IO_ADDR(PORT)),
[nbit] "i" (NBIT),
// Sostituiti gli "r" con "d" per forzare i registri > 15
[cb] "d" (cb), [buf] "d" (buf), [nb] "d" (nb)
);
}