Indice

Progetto Simon

Vogliamo realizzare un clone di un gioco elettronico famoso negli anni '80, il Simon della MB.

Il gioco Simon della MB(fonte Wikipedia)

Il gioco è molto semplice:

Per vedere come funzionava si può guardare lo spot trasmesso in TV in quegli anni o questo video che mostra bene il gameplay.

Abbiamo scelto di fare un clone di questo gioco perché è semplice da realizzare ma interessante sia per la parte di progettazione e scelta dei componenti che per quella del software1). Dal punto di vista dei componenti servono:

Un video che descrive il progetto:

Specifiche hardware e scelta componenti

Nel realizzare questo progetto bisogna considerare alcune limitazioni:

Alimentazione

Tenendo conto di questi requisiti si è scelto di alimentare la scheda con una batteria a bottone tipo CR20322). Sono batterie al litio da 3V piccole, economiche, facilmente reperibili e con buona autonomia. Disponendo di soli 3V bisogna scegliere con attenzione gli altri componenti: i LED devono avere una Vf3) al massimo di 2,5V4), il microcontrollore e il buzzer devono poter funzionare con soli 3V.

LED

Vanno bene anche i LED da 5mm del laboratorio ma bisogna controllare che la Vf non superi i 2,5V e che la luminosità sia adeguata anche con correnti basse.

Buzzer

Scegliamo un buzzer piezoelettrico perché è più facile da pilotare di uno magnetico - che richiede un condensatore in più e assorbe correnti più elevate - e perché li abbiamo già a disposizione in laboratorio. Per una semplice panoramica sulle due tipologie di buzzer si può leggere questo articolo dal blog di un produttore.

Microcontrollore

I requisiti sono tanti:

La scelta cade quasi obbligatoriamente su un prodotto della serie tinyAVR di Atmel: l'ATtiny13A. Le sue caratteristiche sono:

Schema elettrico

Considerazioni preliminari

L'uso del microcontrollore ATtiny13A è possibile perché i suoi pin di I/O possono essere utilizzati alnternativamente come ingressi o come uscite nello stesso programma cambiando la modalità con un'istruzione. In questo modo con soli 6 pin si riescono a gestire 5 pulsanti, 4 LED e un buzzer.

I segnali sono tutti digitali:

Coi pulsanti - che a tutti gli effetti sono dei contatti normalmente aperti - si può generare un segnale alto o basso utilizzando i 3 Volt di alimentazione e una resistenza di resistenza di pull-up. Queste resistenze sono già disponibili negli ingressi dell'ATtiny13 e devono solo essere attivate via software. Il problema del anti-rimbalzo, sempre presente ogni volta che ci sono parti mobili in un contatto, può essere gestito via software per risparmiare componenti.

I LED si collegano ai pin dei pulsanti corrispondenti. Quando i pin, usati come uscite, saranno al livello basso permetteranno alla corrente di scorrere dall'alimentazione a massa accendendo il LED. Naturalmente serve un resistore per limitare la corrente nel LED; il suo dimensionamento si fa considerando una Vf di 2 Volt uguale per tutti i LED e una corrente di 2 mA5):

`R = (V_(\C\C)-V_F)/(I_d) = (3 - 2)/0.002 = 500 \Omega`

Scegliamo il valore più vicino della serie commerciale E12, quindi 470Ω.

Il buzzer può essere collegato direttamente tra l'alimentazione e un'uscita del microcontrollore. Per produrre un suono di una certa tonalità (una nota) l'uscita dovrà commutare tra il valore alto e basso ad una determinata frequenza (ad esempio a 440Hz sentiremo la nota La).

Circuito di principio e circuito finale

Fatte queste considerazioni il circuito di principio si presenta così:

schematico di principio simon

Osserviamo che:

Ci sono allora una serie di modifiche da fare e bisogna:

Per risparmiare tempo è possibile utilizzare i due file per Multisim e Ultiboard disponibili tra le risorse a fondo pagina e importare simboli e footprint:

In alternativa si procede come descritto nel paragrafo seguente.

Dopo aver creato footprint (portabatteria, buzzer e pulsanti) e simboli dei componenti (portabatteria, microcontrollore e pulsanti) e dopo aver abbinato a tutti i componenti un footprint (che per resistori, buzzer e LED andrà cambiato in Ultiboard) si ottiene il circuito finale:

schematico simon

Creare un componente in Multisim

NB non è necessario se si importano i simboli del file Multisim disponibile tra le risorse

Prima di creare i componenti conviene creare i footprint in Ultiboard usando le informazioni contenute nei datasheet. L'abbinamento componenti-footprint è questo:

Si usa una procedura guidata:

Terminata la procedura è possibile piazzare il componente nello schematico. Controllare che la sigla associata (il RefDes) sia corretta7), altrimenti modificarla da Tools|Database|Database manager|scheda Family|Default prefix.

Indicazioni per la creazione del componente ATtiny13A:

Per creare correttamente i componenti portabatteria e pulsante bisogna prima aver creato i rispettivi footprint custom in Ultiboard. Per il pulsante c'è un problema in più: i 4 piedini sono collegati internamente due a due (vedi il datasheet a pag. 4) ma Ultiboard non permette di avere più piedini con lo stesso nome nei footprint. La soluzione più semplice è quella di usare un simbolo con 4 piedini e collegare le coppie di piedini nello schematico (vedi figura sopra).

Indicazioni per la creazione del pulsante TACT-65R-F:

Indicazioni per la creazione del portabatteria DS1092-04:

Layout del PCB

In questa fase di procede alla definizione delle dimensioni della scheda, poi al layout (posizionamento) dei componenti e infine allo sbroglio, cioè al disegno delle piste in rame che collegano i componenti che non devono mai toccarsi o incrociarsi.

Requisiti per la realizzazione nella sala acidi della scuola:

Altri requisiti:

Indicazioni varie:

Cominciamo così:

Andranno cambiati i footprint di:

Prima di procedere allo sbroglio è bene anche aggiustare se necessario le dimensioni delle piazzole:

Dopo aver ridimensionato il PCB si dovrebbe ottenere qualcosa di simile:

inizio sbroglio

Da qui si procede con lo sbroglio, piazzando i componenti e tracciando le piste. Qualche indicazione:

Un possibile sbroglio è mostrato in figura:

sbroglio scheda simon

Il render della vista 3D ha questo aspetto:

render 3D

Creare un footprint

NB non è necessario se si importano i footprint del file Ultiboard disponibile tra le risorse

Ultiboard organizza i footprint in più database; quello predefinito si chiama Ultiboard Master e contiene i footprint di migliaia di componenti. Quando un progetto include un componente il cui footprint non è disponibile in questo database bisogna crearne uno custom e salvarlo nel database User. Si può:

Il footprint di buzzer e portabatteria va creato da zero, quello dei pulsanti si può fare modificandone uno di Ultiboard.

Per creare un footprint custom:

Creare una piazzola custom

NB non è necessario da Multism 14.2 o superiore

I LED e l'ATtiny13A hanno dei pin molto vicini tra loro (100mil = 2,54mm) e non è possibili utilizzare piazzole rotonde delle dimensioni proposte sopra (2,6mm) perché si toccherebbero tra loro. Bisogna usare piazzole rettangolari o - meglio ancora - ovali. Ultiboard permette di creare piazzole ovali in maniera molto semplice (selezionandole e scegliendo round rectangle come forma) ma purtroppo non vengono stampate (è un bug del programma risolto da Multisim 14.2) quindi, se si vogliono piazzole ovali, bisogna creare delle piazzole custom.

Creare la piazzola custom:

Per utilizzare la piazzola custom dal layout aprire la finestra delle proprietà di una piazzola e selezionare il pulsante accanto alla scritta Custom. Poi scegliere la piazzola creata. Se la piazzola è orientata male (per esempio orizzontale invece che verticale) si può ruotarla selezionandola e impostando 90° nel campo Rotation della scheda General.

Modificare una piazzola custom:

E' possibile importare una piazzola custom nel database User da un file Ultiboard selezionando la piazzola e scegliendo Tools|Database|Add selection to Database. Nelle piazzole custom importate il reference point potrebbe essere nella posizione sbagliata; per riposizionarlo modificare la piazzola importata come indicato sopra.

Software

Il programma11) che caricheremo nel microcontrollore non usa nessuna delle funzioni di Arduino ma opera direttamente sui registri dell'ATtiny13A. Questa soluzione migliora nettamente le prestazioni e permette di ridurre la dimensione del programma compilato (quello che caricheremo occupa praticamente tutta la memoria disponibile) ma complica la scrittura del programma e lo rende incompatibile con altri tipi di microcontrollore.

Il programma non è spiegato nel dettaglio ma dai commenti si capisce come funziona.

Prerequisiti:

Il codice non è banale! E' facile imbattersi in istruzioni come questa:

  while (ADCSRA & (1 << ADSC));

che si interpreta così:

Insomma per capire cosa succede in questa istruzione, che serve ad attendere il termine della conversione analogico-digitale dell'ADC, bisogna conoscere i registri dell'ATtiny13A (le informazioni si trovano sul datasheet) e saper operare con i bit (bit-shift, operatori bitwise).

/*
  Codice originale (leggermente modificato per rientrare in 1kB di
  flash) e descrizione del progetto:
  https://hackaday.io/project/18952-simon-game-with-attiny13
 
  Copyright (c) 2016 Divadlo fyziky UDiF (www.udif.cz)
 
  Permission is hereby granted, free of charge, to any person
  obtaining a copy of this software and associated documentation
  files (the "Software"), to deal in the Software without
  restriction, including without limitation the rights to use,
  copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the
  Software is furnished to do so, subject to the following
  conditions:
 
  The above copyright notice and this permission notice shall be
  included in all copies or substantial portions of the Software.
 
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  OTHER DEALINGS IN THE SOFTWARE.
*/ 
 
/*
  **** SPIEGAZIONI INTRODUTTIVE ****
 
  SCOPO DEL PROGRAMMA
  Il codice implementa il classico gioco Simon con 4 LED, 4 pulsanti
  e una sequenza di luci e suoni da memorizzare e riprodurre.
  La sequenza casuale è generata quando il pulsante start/reset avvia
  il gioco e si allunga ogni volta che il giocatore la riproduce.
  Il gioco si interrompe quando si commette un errore.
  Il punteggio massimo viene salvto su EEPROM (per cancellare l'high-
  score tenere premuto il pulsante del LED rosso durante lo start). 
  Il watchdog timer viene usato per il debouncing, lo sleep-mode e all'
  avvio per il random generator.
  Il timer viene usato per generare il segnale PWM per il buzzer.
 
  PREREQUISITI
  - generalità sui microcontrollori e programmazione in C
  - saper leggere un datasheet lungo e complesso
  - bitwise operation e bit masking per la gestione dei registri
 
  CONFIGURAZIONE
  Il MCU va configurato per andare a 1.2MHz o l'esecuzione non sarà
  corretta come tempi.
 
  REGISTRI PIU' IMPORTANTI UTILIZZATI
  I numeri di pagina nei commenti fanno riferimento al datasheet 
  dell'ATtiny13A (rev. 8126F-AVR-05/12). Nella pagina "Register Summary" 
  c'è il link alle pagine che descrivono i registri e i loro valori di  
  default:
  DDRB   = 0b00000000 -> tutti ingressi
  PORTB  = 0b00000000 -> pull-up disabilitato (tri-state)
  ADCSRA = 0b00000000 -> ADC spento
  ADMUX  = 0b00000000 -> ADC0 selezionato
 
  GESTIONE INGRESSI E USCITE
  DDRB imposta come uscite i pin che hanno il bit corrispondente a 1
  (pagina 50). I pin sono sempre usati come ingressi tranne quando 
  viene chiamata la funzione play().
  Il pin 1 (PB5) è usato sia per il pulsante start (è un RESET attivo
  basso che come ingresso analogico (ADC0) scollegato, per generare
  il seed del random generator.
  Il pin 6 (PB1/OC0B) è usato per pilotare il buzzer con un'onda 
  quadra generata col timer/counter in modalità waveform generator.
 
  Per la gestione del timer/counter si usano i registri:
  - TCNT0 che contiene il valore del conteggio
  - OCR0A e OCR0B che contengono i valori da comparare con TCNT0 
  Questi due registri contengono il valore massimo, che fissa la
  frequenza, e quello in corrispondenza del quale c'è la commutazione
  tra livello alto e basso. Il waveform generator usa il risultato 
  della comparazione per produrre un segnale PWM nel pin OC0B (buzzer).
 
  Il timer/counter si ferma se non si seleziona una sorgente di clock.
  Ha più modi di funzionamento che dipendono da WGM0[2:0] e COM0x[1:0]:
  - normal: conta in su fino a 0xFF poi genera un interrupt e ricomincia
  - CTC: conta fino a OCR0A
  - fast PWM: va basso sul match con OCR0A e alto su 0xFF (fig. 11-6)
  - phase correct PWM: conta in su e in giù
  Nel programma si usa la modalità phase correct PWM per il buzzer.
*/
 
 
// LIBRERIE UTILIZZATE (da https://www.nongnu.org/avr-libc/ )
 
// sleep mode
// Al termine di ogni partita, se scade il watchdog timer il MCU va in
// POWER-DOWN per ridurre il consumo; si risveglia col un reset.
#include <avr/sleep.h>
 
// delay 
// funzione simile a delay() di Arduino (cicli di CPU buttati). Va 
// bene per piccole temporizzazioni, altrimenti meglio usare il timer.
// Il codice usa _delay_loop_2(t) dove t è un int a 16 bit; il ritardo 
// dipende dal clock del MCU e si calcola moltiplicando t per il tempo 
// di quattro cicli di clock. Ad esempio col clock a 1.2MHz e t=25000 
// l'attesa è (1/clock)*4*t = 83ms
#include <util/delay_basic.h>
 
// eeprom
// per salvare l'high-score nella EEPROM
#include <avr/eeprom.h>
 
// DICHIARAZIONE VARIABILI
 
// NB le variabili dichiarate e non inizializzate valgono zero, vedi
// https://www.nongnu.org/avr-libc/user-manual/FAQ.html#faq_varinit
 
// nella fase di gioco in cui viene mostrata la sequenza con la funzione
// play() i quattro elementi dell'array, applicati al registro PORTB,
// impostano come uscite PB1 (il buzzer) e uno tra PB3, PB2, PB0 e PB4
// (i LED 1, 4, 3 e 2)
const uint8_t buttons[4] = {
  0b00001010, 0b00000110, 0b00000011, 0b00010010
};
 
// array con quattro valori usati dal waveform generator per produrre 
// le note: re fa# la re
// vedere la funzione play() per interpretare i valori
const uint8_t tones[4] = {
  239, 179, 143, 119
};
 
uint8_t lastKey; // ultimo tasto premuto (per il debouncing pulsanti)
uint8_t lvl = 0; // livello (parte da 0 -> sequenza con un solo LED)
uint8_t maxLvl;  // livello massimo (high-score salvato su EEPROM)
 
// variabili per pseudo-random generator
uint16_t seed; // generato con l'ADC su un pin scollegato (poi mischiato)
uint16_t ctx; // valori successivi generati a partire dal seed
 
// variabili volatile modificate dalla ISR del watchdog
// vedi http://gammon.com.au/interrupts o la reference di Arduino per
// le motivazioni circa il qualificatore "volatile"
volatile uint8_t nrot = 8; // quante volte si mischia il seed nella ISR
volatile uint16_t time; // conteggio incrementato dall'ISR ogni 16ms
                        // usato per il debouncing dei pulsanti e per
                        // l'auto power-off dopo 64s (time = 4000)
 
// FUNZIONI 
 
void sleepNow() {
  // power-down riduce al massimo il consumo
  // il risveglio avviene col pulsant start/reset
  // di default tutti i pin sono ingressi; in power-down gli ingressi
  // flottanti è meglio che siano scollegati cioè tri-state (pag. 53)
  PORTB = 0b00000000; // tri-state (disabilita la resistenza di pullup)
  cli(); // disabilita gli interrupt
  WDTCR = 0; // spegne il Watchdog timer
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
  sleep_cpu();
}
 
void play(uint8_t i, uint16_t t = 45000) {
  // accende un LED e suona la nota corrisponente col buzzer
  // i è l'indice del LED da 0 a 3
  // t è usato nella funzione delay per ritardare di (4/1.2M)*t sec 
  // il valore di default (45000) corrisponde a 150ms
 
  // quando non viene eseguita la funzione play() i pin sono impostati
  // come ingressi con resistenza di pull-up interna
  // prima di passare da ingresso a uscita bisogna impostarli come 
  //tri-state (pagina 51) quindi si modifica PORTB sapendo che:
  // 0 -> come input disabilita la resistenza di pull-up (tri-state)
  // 0 -> come output imposta il livello basso di tensione
  PORTB = 0b00000000;
 
  // imposta come uscite il buzzer e uno dei pin collegati ai LED
  // con i bit di PORTB a 0 (livello basso) il LED si accende
  DDRB = buttons[i];  
 
  // generazione del segnale PWM per il buzzer con il waveform generator
  // modo PWM phase-correct (con TTCR0A e TTCR0B)
  // - OCR0A e OCR0B impostano la frequenza della nota e il duty-cicle
  // - bit di TTCR0B impostano il modo PWM e il prescaler
  // la frequenza vale (pag. 68): f=(1.2M/[(prescaler)x(OCR0A+OCR0A)]
  // con i valori in tones (239, 179, 143, 119) -> 314 419 524 e 630 Hz
  // che per qualche strana ragione che andrebbe investigata diventano
  // re, fa#, la, re (294 370 440 587 Hz)
 
  // assegno a OCR0A il valore massimo del conteggio del timer/counter
  // per modificare la frequenza (nota)
  OCR0A = tones[i];   // ad esempio 239 -> 11101111 per tones[0]
  // assegno a OCR0B la sua metà (duty-cycle 50%)
  OCR0B = tones[i] >> 1; //shiftR di 1: 01110111 (239 diviso 2: 119)
 
  // TCCR0A è impostato nel main prima di chiamare play(); si cambia 
  // TTCR0B in modo che il waveform generator funzioni in modalità PWM
  // phase correct e conteggio fino a ORC0A con prescaler 8
  TCCR0B = (1 << WGM02) | (1 << CS01);
  // TTCR0A = 00100001 e TTCR0B = 00001010 quindi:
  // WGM0[2:0] = 101 -> modo phase correct fino a OCR0A
  // CS0[2:1] = 010 -> prescaler 8
 
  // suona la nota e tiene il LED acceso per 150ms
  _delay_loop_2(t);
 
  // spegne il timer/counter (niente clock) interrompendo il segnale PWM
  TCCR0B = 0b00000000; 
 
  // spegne il LED e disabilita il buzzer (tutti i pin come ingressi)
  DDRB = 0b00000000;
  // abilita le resistenze di pull-up per i pulsanti
  PORTB = 0b00011101;
}
 
void gameOver() {
  // animazione e gestione high-score
  for (uint8_t i = 0; i < 4; i++) {
    play(3 - i, 25000); // all'indietro
  }
  // se si batte l'high score salva il livello raggiunto su eeprom
  if (lvl > maxLvl) {
    eeprom_write_byte((uint8_t*) 0, ~lvl); // high score complementato
    //animazione high score (3 volte l'animazione level up)
    for (uint8_t i = 0; i < 3; i++) { 
      levelUp();
    }
  }
  sleepNow();
}
 
void levelUp() {
  // animazione con i quattro LED in sequenza
  for (uint8_t i = 0; i < 4; i++) {
    play(i, 25000);
  }
}
 
uint8_t simple_random4() {
  // LFSR linear-feedback shift register (Galois)
  // per una spiegazione su come funziona vedi il progetto originale
  // o la pagina wikipedia (e l'Application Note della Maxim linkata):
  // it.wikipedia.org/wiki/Registro_a_scorrimento_a_retroazione_lineare
  // Modifica ctx e restituisce un indice tra 0 e 3 corrispondente a 
  // uno dei quattro LED
  // la sequenza pseudo-casuale di valori di ctx si ripete sempre 
  // allo stesso modo se non cambia il seed; riportando ctx al valore
  // del seed si può ripercorrere la sequenza e confrontarla con la
  // pressione dei pulsanti per capire se il giocatore ha sbagliato
 
  // servono due bit per un nuovo indice da 0 a 3 (00 -> 11)
  for (uint8_t i = 0; i < 2; i++) {
    uint8_t lsb = ctx & 1; // estrae il LSB da ctx
    ctx >>= 1; // shift a destra
    if (lsb || !ctx) { // se LSB (uscita) è 1 o se ctx contiene tutti 0
                       // (con tutti zeri la sequenza non cambia mai)
      ctx ^= 0xB400;   // inverte i bit secondo la maschera (XOR con 1)
                       // 0xB400 -> 1011010000000000 quindi XOR con i 
                       // bit 16, 14, 13 e 11, vedi su wikipedia en
                       // Linear-feedback_shift_register#Galois_LFSRs
    }
  }
  // l'AND con quella maschera dà il resto dopo aver diviso per 4
  // quindi un valore tra 0 e 3 che corrisponde al LED da accendere
  return ctx & 0b00000011;
}
 
ISR(WDT_vect) { 
  // Watchdog Timeout Interrupt Server Routine da avr-libc
  // eseguita ogni volta che scade il timer
  // ogni 16ms viene incrementato il valore della variabile time usata
  // per gestire il poweroff (64s di inattività) e il debouncing (16ms)
  time++; // increase each 16 ms
  // il watch timer viene usato anche all'avvio per mischiare 8 volte il
  // seed con l'ADC; il seed non cambia più fino al reset successivo
  if (nrot) {
    nrot--;
    // mischia il seed facendo uno shift a sinistra e XOR con il valore
    // del timer/counter che nella fase iniziale funziona da contatore
    // a 8bit senza prescaler, quindi a 1.2MHz
    seed = (seed << 1) ^ TCNT0;
  }
}
 
void resetCtx() {
  // riporta ctx al valore iniziale, quello del seed
  ctx = seed;
}
 
int main(void) {
  // pull-up sui 4 pulsanti dei LED (sono tutti ingressi di default)
  PORTB = 0b00011101;
 
  // GENERAZIONE DEL SEED
 
  // Il pin PB5/ADC0 (start/reset) viene usato per generare il seed 
  // attivando l'ADC sul pin scollegato per ottenere un valore casuale.
  // Il valore viene poi mischiato per otto volte nella ISR richiamata
  // allo scadere del watchdog timer
 
  // ADC
 
  ADCSRA |= (1 << ADEN); // abilita l'ADC (pagina 82 e 92)
  ADCSRA |= (1 << ADSC); // parte la conversione sul pin ADC0 scollegato
  // attende che termini la conversione ADC (ADSC vale 0 quando termina
  // la conversione (pagina 83)
  while (ADCSRA & (1 << ADSC)); 
  // l'ADC è un 4 canali a 10 bit ad approssimazioni successive; per 
  // ottenere 8 bit si considerano solo gli 8 bit meno significativi 
  // contenuti nel registro ADCL (gli altri due sono in ADCH)
  seed = ADCL;
  ADCSRA = 0b00000000; // spegne l'ADC
 
  // WATCHDOG TIMER
 
  // abilita l'interrupt del watchdog timer (interrupt mode pag 43)
  // il WDT usa un oscillatore separato che va a 128kHz (il timer invece
  // va col clock)
  // senza prescaling c'è un timeout ogni 16ms (2048 cicli, pag 43) e
  // viene eseguita la ISR
  WDTCR = (1 << WDTIE); // parte il watchdog timer con prescaler da 16ms
  sei(); // global interrupt enable (mette a 1 il bit I nello SREG)
 
  // TIMER/COUNTER per mischiare il seed
 
  // imposta il timer/counter in normal mode (WGM[2:0] a zero) e senza 
  // prescaler (CS00 a uno): conta fino a 0xFF, segnala con TOV0 e 
  // ricomincia il conteggio (pag. 64)
  // il timer comincia a contare quando si imposta una sorgente per il 
  // clock (pag. 60)
  TCCR0B = (1 << CS00);
  // TTCR0A = 00000000 e TTCR0B = 00000001 quindi:
  // - WGM0[2:0] = 000 -> normal mode (pag 73)
  // - CS0[2:0] = 001  -> no prescaling (clock a 1,2MHz)
 
  // il watchdog timer scade otto volte e la ISR mischia il seed 
  while (nrot); 
 
  // TIMER/COUNTER per generare il segnale PWM del buzzer
 
  // la modalità di funzionamento del timer cambia da normal mode a PWM
  // phase correct (WGM0[2:0]=001) dove conta in su e in giù fino
  // a 0xFF (tabella a pagina 73)
  // nella funzione play() invece, per cambiare la frequenza e produrre
  // note diverse, conterà fino al valore contenuto nel registro OCR0A 
  // Si imposta il compare output mode del waveform generator 
  // (COM0B[1:0]=10)in modo che il pin del buzzer PB1/OC0B vada basso 
  // sul match col valore del registro OCR0B mentre si conta in su e 
  // alto mentre si conta in giù; in questo modo si genera un onda
  // quadra con la frequenza desiderata
  // NB il segnale PWM è presente solo se il pin è impostato come uscita
  // (pag 68) e se è impostata una sorgente per il clock (pag 60) quindi
  // il buzzer non suona finche non si chiama play()
  TCCR0A = (1 << COM0B1) | (0 << COM0B0) | (0 << WGM01)  | (1 << WGM00); 
  // TTCR0A = 00100001 e TTCR0B = 00000001 quindi:
  // - WGM0[2:0] = 001 -> phase correct mode (pag 73)
  // - COM0B[1:0] = 10 -> reset up-counting, set down-counting (pag 72)
 
  // LETTURA DELL'HIGH SCORE DA EEPROM 
 
  // maxLvl viene complementato (con l'operatore ~) quando viene scritto
  // o letto perché il valore di default su EEPROM è 0xFF (255 e non 0) 
  // e con la EEPROM "vergine" il record sarebbe il livello 255
  // (uint8_t*) 0 è un puntatore a un byte che parte dall'indirizzo 0
  maxLvl = ~eeprom_read_byte((uint8_t*) 0); 
 
  // EVENTUALE RESET DELL'HIGH-SCORE PREMENDO START+ROSSO
 
  // legge lo stato dei pin della port B e lo confronta con la maschera
  // (dove 0 significa premuto)
  switch (PINB & 0b00011101) {
    // NB lo switch non è necessario; nel codice originale permetteva di 
    // attivare altre funzioni tenedo premuti altri pulsanti all'avvio
    // per ridurre le dimensioni dell'eseguibile le altre combinazioni
    // sono state eliminate
    // PB1 e PB5 non contano: uno è il buzzer e l'altro un reset attivo
    // basso (che a questo punto del programma sarà già alto); i pin dei
    // pulsanti non premuti sono al livello alto (pull-up) 
    // all'avvio controlliamo  se PB3 (LED rosso) è premuto 
    case 0b00010101: // PB3 premuto dopo il reset -> azzera high-score
      eeprom_write_byte((uint8_t*) 0, 255); // scrive 255 -> livello 0
      maxLvl = 0;
      break;
  }
 
  while (1) {
    // MOSTRA LA SEQUENZA
 
    // reimposta ctx al valore del seed; la sequenza di valori di ctx 
    // sarà sempre la stessa sequenza finché non si resetta
    resetCtx();
    // accende in sequenza un numero di LED pari al livello più uno
    for (uint8_t cnt = 0; cnt <= lvl; cnt++) { 
      // attesa tra un LED e il successivo
      // il valore iniziale è quello massimo (2^16 -> 65536 -> 218ms)
      // poi diminuisce all'aumentare del livello 
      _delay_loop_2(4400 + 489088 / (8 + lvl));
      // accende il LED con l'indice restituito da simple_random4(), un
      // numero tra 0 e 3 che è il resto della divisione per 4 di ctx
      // il valore di ctx viene aggiornato ad ogni chiamata
      play(simple_random4());
    }
 
    // INSERIMENTO SEQUENZA DA PARTE DEL GIOCATORE
 
    // azzera il conteggio che porta al power-off
    time = 0;
    // per il debouncing imposta lastKey a un valore che non corrisponde
    // a nessun pulsante (0-3)
    lastKey = 5;
    // riporta ctx al valore iniziale uguale al seed
    resetCtx();
    // tante volte quanti sono i LED da indovinare
    for (uint8_t cnt = 0; cnt <= lvl; cnt++) {
      // azzero pressed
      bool pressed = false;
      // finché non si preme un bottone
      while (!pressed) {
        // provo i quattro bottoni
        for (uint8_t i = 0; i < 4; i++) {
          // controlla se il pulsante i è premuto
          // buttons & maschera -> bit a 1 solo per il pin/pulsante "i"
          // PINB ha tutti i bit a 1 (resistenze di pull-up attivate
          // nella funzione play) tranne quello del pulsante premuto
          // l'espressione logica tra le parentesi interne vale 0 se
          // il pulsante i è premuto, poi viene complementata
          if (!(PINB & buttons[i] & 0b00011101)) {
            // DEBOUNCE
            // se sono passati 16ms (tempo max per eventuali rimbalzi) o
            // se è stato premuto un pulsante diverso (non è rimbalzo)
            if (time > 1 || i != lastKey) {
              // accendi il LED i e registra la pressione del tasto
              play(i);
              pressed = true;
              // genera di nuovo l'indice LED della sequenza riprodotta
              uint8_t correct = simple_random4();
              // se si è premuto il pulsante sbagliato
              if (i != correct) {
                // accendi il LED giusto tante volte pari al livello raggiunto
                for (uint8_t j = 0; j <= lvl; j++) {
                  _delay_loop_2(65536); // 33ms
                  play(correct, 45000); // 66ms
                }
                _delay_loop_2(65536); // 218ms
                // animazione game over
                gameOver();
              }
              // è stato premuto il pulsante giusto 
              time = 0; // azzera time
              lastKey = i; // memorizzo l'indice dell'ultimo LED
              break; // esce dal ciclo for e smette di controllare se è 
                     // premuto un pulsante; passa al LED successivo 
                     // della sequenza
            }
            time = 0; // azzera il conteggio per poweroff e debouncing
          }
        }
        // se passano 64s (time = 4000) e non viene premuto nessun 
        // pulsante si attiva il power-off
        if (time > 4000) {
          sleepNow();
        }
      }
    }
    _delay_loop_2(65536);
    // sequenza indovinata, aumenta il livello
    if (lvl < 254) {
      lvl++;
      levelUp(); // animazione levello completato
      _delay_loop_2(45000);
    }
  }
}

Programmazione del microcontrollore

Per programmare il micro ATtiny13A useremo una scheda Arduino Uno14) come programmatore come descritto in questo tutorial.

La soluzione più semplice è quella di usare una breadboard e sei cavi rigidi collegati come nella figura seguente15):

come collegare Arduino e ATtiny13 per programmarlo

Una soluzione più robusta e senza cavi volanti è quella di produrre uno shield custom 16) con zoccolo ZIF:

shield per programmare gli ATtiny13A

Per utilizzare una scheda Arduino e il suo ambiente di sviluppo per programmare gli ATtiny13 bisogna prima di tutto:

Fatto questo si inserisce il microcontrollore nello zoccolo ZIF (o nella breadboard con i collegamenti visti sopra) quindi si apre il programma che andrà caricato sull'ATtiny13A. Prima del caricamento bisogna modificare le impostazioni di default della scheda (che andranno ripristinate per tornare a utilizzare “normalmente” la scheda Arduino) scegliendo, dal menu Strumenti:

A questo punto è possibile caricare il programma sull'ATtiny13A scegliendo Carica tramite un programmatore dal menu Sketch (NB non con il pulsante Carica che useremmo per programmare la scheda Arduino!).

Terminato il caricamento si può montare il microcontrollore sulla scheda Simon.

Volendo è possibile caricare un bootloader sui microcontrollori ATtiny13A e utilizzarli nell'ambiente di sviluppo Arduino; per farlo si usa la voce Scrivi bootloader dal menu Strumenti. Nel nostro caso non è necessario perché il programma non fa uso delle librerie Arduino e ci interessa solo compilare e “flashare” il firmware sul microcontrollore.

Assemblaggio

Indicazioni sintetiche sul montaggio:

come saldare

La scheda assemblata avrà più o meno questo aspetto:

scheda simon assemblata

Risorse

Torna all'indice.

1)
anche il progetto finale di STA del 2012 era un clone del gioco ma realizzato su breadboard con Arduino
2)
vedere qui per una panoramica sulle batterie
3)
tensione diretta quando conducono
4)
quindi i LED blu non vanno bene
5)
la Vf dei quattro LED è diversa (dipende dal colore e dal materiale con cui è realizzato il LED) ma così scegliamo quattro resistori uguali e minimizziamo gli errori in fase di montaggio; la corrente è più bassa dei 20 mA standard per ridurre il consumo
6)
nessuno dei pulsanti disponibili in Multisim corrisponde a quelli in commercio
7)
J per il portabatteria, U per il microcontrollore, S per il pulsante
8)
per farlo aprire il file Ultiboard, selezionare il componente di cui si vuole importare il footprint poi dal menu Tools|Database selezionare “Add selection to Database” e salvarlo nello User Database eventualmente rinominandolo se ce n'è già un altro con lo stesso nome
9)
il punto interrogativo indica dove comparirà un numero progressivo o un valore
10)
le misure si possono fare cliccando in un punto e osservando in basso a destra il valore delle coordinate e delle distanze dX dY e L
12)
vedi anche questo tutorial dal sito di Arduino
13)
è definito in una macro, vedi qui ad esempio
14)
funziona anche con una Arduino Duemilanove ma bisogna collegare un condensatore da 10uF tra reset e ground altrimenti l'upload fallisce
15)
LED e resistori non servono ma permettono di testare velocemente se la programmazione funziona caricando sul micro uno sketch che fa lampeggiare il led