Strumenti Utente

Strumenti Sito


simon

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:

  • ci sono quattro bottoni di colore diverso che si illuminano e producono un suono abbinato al colore
  • il gioco propone una sequenza di colori/suoni
  • il giocatore deve ricordare e ripetere la sequenza premendo i bottoni in successione
  • se indovina si aggiunge un colore/suono alla sequenza
  • se sbaglia il gioco termina

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:

  • quattro LED di colore diverso
  • un buzzer (magnetico o piezoelettrico)
  • cinque bottoni (uno per avviare il gioco)
  • un microcontrollore che esegue il software che gestisce il gioco

Specifiche hardware e scelta componenti

Nel realizzare questo progetto bisogna considerare alcune limitazioni:

  • basso costo
  • PCB di piccole dimensioni (costa meno)
  • PCB single-layer e componenti a foro passante (per farlo a scuola)
  • alimentazione con batteria piccola e economica
  • materiale facilmente reperibile o disponibile a scuola

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:

  • funzionante a 3V e a basso consumo
  • con 5 ingressi per i pulsanti e 5 uscite per i quattro LED e il buzzer
  • economico e facilmente reperibile
  • facile da usare, possibilmente con ambienti di sviluppo che già conosciamo (IDE Arduino)
  • disponibile in un package a foro passante

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

  • basso consumo e alimentazione da 2V
  • 6 piedini utilizzabili sia come input che come output
  • economico (meno di 1€) e molto utilizzato
  • supportato da più ambienti di sviluppo, compreso quello di Arduino
  • disponibile nel package DIP8

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:

  • in ingresso troveremo una tensione al livello alto o basso generata dai 5 pulsanti
  • in uscita porremo un livello alto o basso di tensione per attivare i 4 LED colorati e il buzzer

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 5 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:

  • il circuito non è simulabile (non si simula un microcontrollore!)
  • il componente ATtiny13A non è disponibile in Multisim e va creato
  • tutti i componenti neri non hanno un footprint e non vengono considerati quandi si passa Ultiboard per progettare il PCB; bisogna assegnargli un footprint o sostituirli con componenti blu o verdi
  • il colore dei LED non è importante perché in fase di montaggio si possono montare in qualunque ordine (i resistori sono tutti uguali)

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

  • creare i footprint di alcuni componenti (portabatteria, buzzer, pulsanti)
  • creare i componenti ATtiny13A, portabatteria, pulsante6)
  • assegnare i footprint corretti agli altri componenti, se possibile (LED e buzzer non lo consentono e bisogna farlo in Ultiboard)

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

  • resistori: RES10
  • LED: LED5R2_5V, da cambiare in Ultiboard
  • ATtiny13A: PDIP-8(P), da indicare quando si crea il componente
  • pulsanti: footprint custom TACT-SPST da creare in Ultiboard
  • portabatteria: footprint custom DS1092-05 da creare in Ultiboard
  • buzzer: footprint custom LD-BZPN-1307 da creare in Ultiboard

Fatte queste considerazioni e creati i componenti ATtiny13A, portabatteria e buzzer si ottiene il circuito finale:

Creare un componente in Multisim

Si usa una procedura guidata:

  • scegliere dal menu Tools|Component Wizard
  • step 1: indicare nome, autore (produttore), funzione svolta e selezionare Layout Only
  • step 2: selezionare un package dal database Master (o uno custom dal database User se lo si è creato), lasciare single section, e indicare il numero dei pin
  • step 3: accettare, copiare o modificare il simbolo del componente (c'è un apposito editor)
  • step 4: indicare il nome dei pin in base a quanto riportato nei datasheet (la tipologia è poco importante)
  • step 5: associare correttamente i pin del simbolo a quelli del footprint
  • step 6: creare una nuova famiglia in una gruppo del database User (o selezionarne una esistente) e cliccare su Finish

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:

  • step 1: ATtiny13A, ATMEL, microcontroller, layout only
  • step 2: selezionare PDIP-8(P) dal database Master, single section, 8 pin
  • step 3: accettare il simbolo proposto
  • step 4: nominare i pin come indicato a pagina 2 del datasheet
  • step 5: associare i pin usando il bottone auto-assign (controllare che la corrispondenza pin del simbolo e pin del footprint sia corretta ed eventualmente correggere)
  • step 6: creare la famiglia ATMEL nel gruppo MCU del database User e cliccare su Finish

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:

  • step 1: TACT-65R-F, NINIGI, Tactile Switch SPST-NO, layout only
  • step 2: footprint custom TACT-SPST da creare in Ultiboard copiandolo da SKHH1_1 (in Through Hole Technoly Parts|Buttons and Switches|Tact) e aggiustando la piedinatura, single section, 4 pin
  • step 3: copiare da DPST-2NO-DB (Electro mechanical|Supplementary switches) e modificare aggiustando il nome dei pin del simbolo
  • step 4: nominare i pin come indicato nel datasheet
  • step 5: associare i pin usando il bottone auto-assign (controllare che la corrispondenza pin del simbolo e pin del footprint sia corretta ed eventualmente correggere)
  • step 6: creare la famiglia SWITCH nel gruppo Basic del database User e cliccare su Finish

Indicazioni per la creazione del portabatteria DS1092-05:

  • step 1: DS1092-05, Connfly, CR2032 battery holder, layout only
  • step 2: footprint custom DS1092-05 da creare in Ultiboard secondo usando le informazioni contenute nel datasheet
  • step 3: copiare da 1028 (in Connectors|POWER)
  • step 4: accettare la piedinatura
  • step 5: associare i pin controllando la corrispondenza pin del simbolo e pin del footprint come indicata nel datasheet
  • step 6: creare la famiglia Connectors nel gruppo Basic del database User e cliccare su Finish

Layout del PCB

Requisiti per la realizzazione nella sala acidi della scuola:

  • single layer
  • piste da 1 mm
  • testo specchiato col nome dello studente
  • massimo 2 piste sotto l'integrato

Altri requisiti:

  • dimensioni massime 6 x 5 cm
  • pulsanti facilmente raggiungibili
  • minimizzare ingombri

Indicazioni varie:

  • imparare a usare i filtri di selezione!
  • ridimensionare il board outline prima di piazzare i componenti (selezionare il layer Board outline e attivare il filtro Enable selecting other objects per cambiare le dimensioni del rettangolo giallo che delimita il PCB)
  • posizionare e ruotare i componenti per facilitare lo sbroglio
  • disabilitare l'opzione Part Shoving dal menu Desing per poter avvicinare tra loro i componenti
  • impostare l'ampiezza delle tracce dalla spreadsheet view (selezionarle tutte dalla scheda Nete cambiare il campo width)
  • dove è possibile usare piazzole circolari con foro da 0,6mm e diametro 2,6mm
  • per i componenti con i pin troppo vicini tra loro usare piazzole ovali custom o piazzole rettangolari
  • imparare a creare footprint custom

Cominciamo così:

  • la prima volta che si passa da Multisim a Ultiboard si usa Transfer|Transfer to Ultiboard; viene generata una netlist, cioè un file che contiene le indicazioni su quali componenti piazzare nel PCB, quale footprint hanno i componenti e come sono collegati tra loro
  • se dopo aver creato il file Ultiboard si fanno modifiche nello schematico in Multisim bisogna riportarle usando Transfer|Forward Annotate to Ultiboard invece che Transfer|Transfer to Ultiboard; in questo modo non si perde il lavoro fatto ma si prosegue il lavoro con una nuova netlist che tiene conto delle modifiche fatte
  • la prima cosa da fare è guardare il layout di ogni componente per capire se è sbagliato (nel dubbio misurare o guardare il footprint nel database usando il pulsante Show Dimensions); i footprint sbagliati si cambiano dalla Spreadsheet View cliccando sulla casella corrispondente nella colonna Footprint della scheda Parts e selezionandoli dal database; vanno sicuramente sistemati i footprint di LED e buzzer perché non è possibile scegliere quelli giusti in Multisim

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:

  • bisogna fare più tentativi, spostando i componenti e ridisegnando le piste, per trovare la soluzione migliore
  • seguire le Ratsnest (linee gialle che indicano quali piedini vanno collegati tra loro) e tracciare le piste usando sempre il layer Copper Bottom (rosso) e la larghezza di 1 mm
  • fare attenzione a non sovrapporre piste/piazzole che non si devono toccare
  • verificare che la clearance (distanza minima da piste e piazzole) sia almeno di 0.5 mm (si può impostare in Options|PCB Options per segnalare automaticamente gli errori col DRC)
  • verificare che il PCB abbia l'aspetto previsto aiutandosi con il render 3D (View|3D Preview)
  • a sbroglio completato verificare che il progetto non contenga errori osservando il risultato nella scheda Results della Spreadsheet View degli strumenti:
    • Desing|Connectivity Check controlla che tutti i collegamenti previsti nello schematico siano stati realizzati nel PCB
    • Desing|DRC and netlist check controlla che non ci siano errori nel modo in cui è stato disegnato il PCB

Un possibile sbroglio è mostrato in figura:

sbroglio scheda simon

Il render 3D ha questo aspetto:

render 3D

Creare un footprint

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 che non è disponibile in questo database bisogna crearne uno custom e salvarlo nel database User. Si può:

  • creare il footprint da zero e salvarlo nel database User
  • copiare un footprint dal database master per poi modificarlo e salvarlo nel database User
  • importare un footprint da un file Ultiboard dal Database Manager

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:

  • dal menu Tools scegliere Database|Database Manager
  • nella vista ad albero a sinistra selezionare User database (in questa fase, volendo si può creare un gruppo/sottogruppo per catalogare i footprint con Create New Group o importare un footprint da un progetto esistente)
  • cliccare l'icona Create new part (tratteggiata) e selezionare PCB part per creare un footprint (Custom Pad Shape serve a creare una piazzola)
  • l'editor dei footprint mostrerà solo l'etichetta per Value e RefDes del componente8); questi elementi di testo possono essere spostati, resi invisibili o modificati (ad esempio J va bene per un connettore, U per un integrato, ecc.)
  • piazzare i pin con Place|Pins indicando posizione, distanza e disposizione (file/righe), tipo di piazzola
  • utilizzando il layer Silkscreen Top disegnare il layout (contorno) del componente usando gli strumenti del menu Place|Graphics
  • dopo aver controllato attentamente le dimensioni del layout9), la distanza tra i piedini e il posizionamento dei piedini nel layout del componente salvare il footprint nel database User (eventualmente in un gruppo adatto) dandogli un nome significativo

Creare una piazzola custom

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) quindi, se si vogliono piazzole ovali, bisogna creare delle piazzole custom.

Creare la piazzola custom:

  • dal menu Tools scegliere Database|Database Manager
  • nella vista ad albero a sinistra selezionare User database
  • cliccare l'icona Create new part (tratteggiata)
  • selezionare Custom pad shape
  • cliccare l'icona Rounded rectangle e disegnare la piazzola ovale (3 click)
  • aprire la finestra delle proprietà della piazzola e aggiustare larghezza, altezza e raggio così: larghezza 3mm, altezza 1.8 mm, raggio 0.9
  • impostare il centrare la piazzola usando come riferimento la croce bianca col cerchio (si possono usare anche le coordinate X e Y per centrare la piazzola impostandole a metà del valore della larghezza e altezza)
  • salvare la piazzola nello indicando un nome

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:

  • dal menu Tools scegliere Database|Database Manager
  • nella vista ad albero a sinistra selezionare User database
  • selezionare la piazzola e cliccando col tasto destro scegliere Edit
  • modificare la piazzola e salvare
  • se la piazzola non è centrata o è spostata rispetto al foro modificare la posizione del reference point da Desing|Set reference point
  • le modifiche non vengono applicate alle piazzole custom già inserite nel layout, bisogna selezionarle nuovamente e scegliere di nuovo la piazzola custom aggiornata

E' possibile importare una piazzola custom nel database User da un file Ultiboard selezionando la piazzola e scegliendo Tools|Database|Add selection to Database. In questo caso può darsi che il reference point sia nella posizione sbagliata. In questo caso modificare la piazzola importata riposizionando il reference point come indicato sopra.

Software

Il programma non usa nessuna delle funzioni di Arduino; questo permette di ridurre la dimensione del programma compilato e migliora nettamente le prestazioni ma il programma è decisamente più complicato da scrivere oltre ad essere specifico per un determinato tipo di microcontrollore.

I commenti spiegano come funziona il codice ma è necessario sapere

  • come funzionano gli operatori bitwise, il bit masking, lo shift a destra (») e sinistra («) e le forme
  • interpretare i datasheet di un microcontrollore, in particolare la parte che riguarda i registri, il nome dei bit nei registri e la funzione svolta (in questo caso è utile il sommario a pagina 158 del datasheet dell'ATtiny13A)

Ad esempio nel codice è facile trovare istruzioni tipo questa:

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

che si interpreta così:

  • ADSC, definito in una macro, corrisponde al numero 6 (secondo bit più significativo)
  • 1 « ADSC corrisponde a 01000000
  • ADCSRA (ADC Control and Status Register A) è uno dei registro a 8 bit che gestisce il convertitore analogico-digitale (vedere il datasheet)
  • quando la conversione analogico digitale termina il bit ADSC del registro ADCSRA passa a 0
  • allora l'operatore AND bitwise restituisce 00000000 e termina il ciclo while, che serviva ad attendere il completamento della conversione
/*
  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.
 
 
  **** COMMENTI E SPIEGAZIONI INTRODUTTIVE ****
 
  SCOPO DEL PROGRAMMA
  Il codice implementa il classico gioco Simon con 4 LED, 4 pulsanti
  e una sequenza da ricordare; la sequenza casuale è generata quando 
  il pulsante start/reset avvia il gioco; la sequenza si allunga ogni
  volta che si indovina; il gioco si interrompe quando si sbaglia; il
  punteggio massimo viene salvto su EEPROM (si può cancellare premendo
  il pulsante del LED rosso durante lo start) 
 
  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 (con i "fuses") per andare a 1.2MHz (il ckock
  interno ha due possibili frequenze + un prescaler per ridurre la 
  frequenza) o l'esecuzione non sarà corretta come tempi
  Altre opzioni: BOD disabled, LTO enabled
 
  DATASHEET E REGISTRI PIU' IMPORTANTI UTILIZZATI
  I numeri di pagina nei commenti fanno riferimento al datasheet 
  dell'ATtiny13A. Nella sezione sui registri c'è il link alle pagine 
  con una descrizione e i seguenti valori di default:
  DDRB =   0b00000000 -> tutti ingressi
  PORTB =  0b00000000 -> pull-up disabilitato (tri-state)
  ADCSRA = 0b00000000 -> ADC spento
  ADMUX =  0b00000000 -> ADC0 selezionato
 
  DDRB imposta come uscite i bit a 1 (pagina 50)
  Tutti i pin sono ingressi tranne quando viene chiamata play(); il 
  pin 5 (PB5) viene usato come:
  * RESET attivo basso (start)
  * ADC0 scollegato  per generare il seed con l'ADC all'avvio
  Sul pin 6 (PB1/OC0B) si genera l'onda quadra per il buzzer usando il
  timer/counter e la modalità waveform generator.
  Per la gestione del timer/counter si usano i registri:
  - TCNT0 che contiene il valore del conteggio
  - OCR0A e OCR0B con dei valori da comparare con TCNT0 (valore 
  massimo che fissa la frequenza e valore 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 nei pin OC0A e OC0B (collegato al buzzer)
  Il timer/clock si ferma se non si seleziona una sorgente di clock
  il modo di funzionamento si imposta con WGM0[2:0] e COM0x[1:0]:
  - normal: conta in su fino a 0xFF poi interrupt e ricomincia
  - CTC: conta fino a OCR0A
  - fast PWM: va basso su match con OCR0A alto su 0xFF (fig. 11-6)
  - phase correct PWM: conta in su e in giù
*/
 
 
// LIBRERIE
 
// sleep mode per ridurre il consumo. Al termine di ogni partita
// il MCU va in POWER-DOWN per non consumare e si sveglia col reset
#include <avr/sleep.h>
 
// come la funzione delay() di Arduino: cicli di CPU buttati ma 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 con clk 1.2MHz 
// e t=25000 si calcola (1/clock)*4*t = 83ms
#include <util/delay_basic.h>
 
// per salvare l'high score nella EEPROM
#include <avr/eeprom.h>
 
// DICHIARAZIONE VARIABILI
 
// NB le variabili dichiarate e non inizializzate valgono zero, vedi
// www.microchip.com/webdoc/AVRLibcReferenceManual/FAQ_1faq_varinit.html
 
// i quattro elementi impostano come uscite PB1 (il buzzer) e uno tra
// PB3, PB2, PB0 e PB4 per accendere rispettivamente i LED 1, 4, 3 e 2
// nella fase di gioco in cui viene mostrata la sequenza
const uint8_t buttons[4] = {
  0b00001010, 0b00000110, 0b00000011, 0b00010010
};
 
// array con quattro valori usati dal waveform generator per produrre 
// le note: re# sol# do re# (2.5, 3.3, 4.2 e 5 kHz)
// 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 dei pulsanti
uint8_t lvl = 0; // livello (parte da 0 -> sequenza con un solo LED)
uint8_t maxLvl;  // livello massimo
 
// variabili per pseudo-random generator LFSR
uint16_t seed; // generato con l'ADC su un pin scollegato (e mischiato)
uint16_t ctx; // valori successivi generati a partire dal seed
 
// variabili volatile modificate dalla ISR del watchdog
volatile uint8_t nrot = 8; // quante volte si mischia il seed nella ISR
                           // usando il valore del timer
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 (pag. 53)
  PORTB = 0b00000000; // tri-state (disabilita la resitenza 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 è usanto nella funzione delay
  // per ritardare di (4/1.2M)*t sec (45000 -> il delay è di 150ms)
 
  // con i pin impostati come ingressi modifico PORTB sapendo che:
  // 0 -> se è un input disabilita la resistenza di pull-up
  // 0 -> se è un output imposta il livello basso di tensione
  // questa operazione serve nel passaggio da ingresso con pull-up a
  // uscita ed è indicata a pagina 51
  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 e sul
  // buzzer viene generato il segnale PWM 
  DDRB = buttons[i];  
 
  // assegno a OCR0A il valore massimo del conteggio del timer/counter
  // in modo da modificare la frequenza; da pag. 68 si capisce che la 
  // frequenza minima si otterrebbe senza usare OCR0A (quindi contando
  // sempre fino a 0xFF - 255) f = 1.2M/(255+255)= 1200000/510 = 2.3kHz
  // con i valori in tones si ottiene ad es. f=1.2M/(239+239)=2.5kHz
  // gli altri valori sono 3.3, 4.2 e 5 kHz (re# sol# do re#)
  OCR0A = tones[i];   // ad esempio 239 -> 11101111 per tones[0]
  // e a OCR0B la sua metà
  OCR0B = tones[i] >> 1; //shiftR di 1: 01110111 (239 diviso 2: 119)
 
  // TCCR0A/B sono impostati nel main prima di chiamare play() in modo
  // che il waveform generator funzioni in modalità PWM phase correct
  // WGM02 imposta il conteggio fino a OCR0A, CS01 il prescaler a 8
  TCCR0B = (1 << WGM02) | (1 << CS01);
 
  // suona la nota e tiene il LED acceso per 150ms
  _delay_loop_2(t);
 
  // spegne il timer/counter interrompendo il segnale PWM
  TCCR0B = 0b00000000; 
 
  // spegne il LED e disabilita il buzzer (tutti i pin come ingressi)
  DDRB = 0b00000000;
  // abilita le resistenze di pul-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); // write best score
    //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() {
  // linear-feedback shift register (Galois)
  // per una spiegazione di come funziona vedi il progetto originale
  // o la pagina wikipedia (e l'AN della Maxim linkata):
  // it.wikipedia.org/wiki/Registro_a_scorrimento_a_retroazione_lineare
  // modifica ctx e restituisce un indice tra 0 e 3 per i LED
  // la sequenza pseudo-casuale di valori di ctx si ripete sempre 
  // uguale se non cambia il seed -> riportando ctx al valore del seed
  // si può ripetere la sequenza e confrontarla con la pressione dei 
  // pulsanti
 
  // 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)
    }
  }
  // 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 da avr-libc
  // eseguita ogni volta che scade il timer (16ms)
  // ogni 16ms viene incrementato il valore della variabile usata per 
  // gestire il poweroff dopo 64s di inattività e il debouncing
  time++; // increase each 16 ms
  // dopo ogni reset viene mischiato il seed per 8 volte, il seed non
  // cabia più fino al reset successivo
  if (nrot) {
    nrot--;
    // mischia il seed facendo uno shift a sinistra e XOR con valore
    // del timer/counter (8bit senza prescaler 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 (tutti ingressi di default)
  PORTB = 0b00011101;
 
  // GENERAZIONE DEL SEED
  // ADC
  // Il pin PB5/ADC0 (start/reset) viene usato per generare il seed 
  // attivando l'ADC quando è scollegato per ottenere un valore casuale
  // il valore viene poi mischiato otto volte nella ISR usando il timer
  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 a 128kHz (il timer 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
  // imposta il timer/counter in normal mode (WGM[2:0] a zero) e senza 
  // prescaler (CS00 a uno): conta fino a 0xFF poi ricomincia e segnala
  // con TOV0 (pag. 64)
  // il timer comincia a contare quando si imposta una sorgente per il 
  // clock (pag. 60)
  TCCR0B = (1 << CS00);
 
  // il watchdog timer scade otto volte e la ISR mischia il seed 
  while (nrot); 
 
  // IMPOSTAZIONE TIMER/COUNTER per generare il segnale PWM del buzzer
 
  // cambia la modalità di funzionamento del timer che passa da normal 
  // a PWM phase correct (WGM0[2:0]=001) dove conta in su e in giù fino
  // a 0xFF (nella funzione play() conterà fino al valore contenuto nel
  // registro OCR0A per modificare la frequenza e produrre più note))
  // Si imposta il compare output mode del waveform generator in modo 
  // che il pin del buzzer PB1/OC0B (COM0B[1:0]=10) vada basso sul
  // match col valore del registro OCR0B mentre conta in su e alto 
  // mentre conta in giù (OCR0B è impostato nella funzione play() come
  // OCR0A) generando un onda quadra con la frequenza desiderata
  // NB il segnale PWM è presente solo se il pin è impostato come uscita
  // (pag 68) quindi il buzzer non suona finche non si chiama play()
  TCCR0A = (1 << COM0B1) | (0 << COM0B0) | (0 << WGM01)  | (1 << WGM00); 
 
  // LETTURA O RESET DELL'HIGH SCORE DA EEPROM PREMENDO START+ROSSO
 
  // maxLvl viene scritto e letto negato (con l'operatore ~) perché il 
  // valore di default della EEPROM è sempre 0xFF (255 e non zero)
  // (uint8_t*) 0 è un puntatore a un byte che parte dall'indirizzo 0
  maxLvl = ~eeprom_read_byte((uint8_t*) 0); 
 
  // legge lo stato dei pin della port B e lo confronta con la maschera
  switch (PINB & 0b00011101) {
    // AND tra gli ingressi e la maschera sopra; 0 significa premuto, 
    // PB1 è il buzzer e PB5 non conta (è un reset attivo basso ma in 
    // questo punto del programma sarà già alto); i pin dei pulsanti 
    // non premuti sono al livello alto (pull-up) quindi controlliamo 
    // se PB3 (LED rosso) è premuto e nel caso resettiamo l'high score
    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; valore massimo 65536 (2^16)
      // -> 218ms poi scende se aumenta lvl
      _delay_loop_2(4400 + 489088 / (8 + lvl));
      // accende il LED con indice da 0 a 3 restituito da simple_random4()
      // generato come resto della divisione per 4 di ctx, il cui valore
      // viene aggiornato ad ogni chiamata
      play(simple_random4());
    }
 
    // INSERIMENTO SEQUENZA DA PARTE DEL GIOCATORE
 
    // azzera il conteggio che porta al poweroff
    time = 0;
    // imposta lastKey a un valore che non corrisponde a nessun pulsante
    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++) {
      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 succede niente poweroff
        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 Uno come programmatore come descritto in questo tutorial.

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

come collegare Arduino e ATtiny13 per programmarlo

Una soluzione più robusta e senza cavi volanti è quella di produrre uno shield11) con zoccolo ZIF dedicata:

shield per programmare gli ATtiny13A

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

  • caricare sulla scheda lo sketch ArduinoISP (da File → Esempi → 11. ArduinoISP)
  • inserire l'URL https://mcudude.github.io/MicroCore/package_MCUdude_MicroCore_index.json nel campo URL aggiuntive per il gestore schede delle impostazioni
  • installare il pacchetto MicroCore che serve per utilizzare gli ATtiny13A nell'ambiente di sviluppo Arduino da Gestore schede… nel menu Strumenti → Scheda

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 una scheda Arduino) scegliendo, dal menu Strumenti:

  • Scheda: ATtiny13
  • BOD: BOD disabled
  • Compiler LTO: LTO enabled
  • Clock: 1.2 MHz internal osc.
  • Porta: quella della scheda Arduino rilevata sopra
  • Programmatore: Arduino as ISP

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 sugli ATtiny13A per utilizzarli al posto di una scheda Uno - ma ancora più limitata. 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:

  • spazzolare la scheda per togliere il photoresist e facilitare la saldatura
  • forare con punta da 0.8mm
  • come saldare (vedi immagine)
  • montare i LED rispettando la polarità
  • montare lo zoccolo nel verso giusto
  • montare il portabatteria nel verso giusto
  • il micro e la batteria si installano dopo (lato piatto della batteria in alto!)

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)
il punto interrogativo indica dove comparirà un numero progressivo o un valore
9)
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
10)
LED e resistori non servono ma permettono di testare velocemente se la programmazione funziona caricando sul micro uno sketch che fa lampeggiare il led
This website uses cookies for visitor traffic analysis. By using the website, you agree with storing the cookies on your computer.More information
simon.txt · Ultima modifica: 2020/07/03 17:58 (modifica esterna)