Autor: Olek
Redakcja: Dondu
Kostka jest niezbędnym elementem wielu gier, zwłaszcza planszowych. Najczęściej używamy tej w formie sześcianu.
Możemy wykonać również jej wersję elektroniczną. W internecie oraz w czasopismach branżowych pojawiło się do tej pory wiele jej implementacji.
Ja oczywiście budując własny układ postanowiłem wykorzystać mikrokontroler. Ale postawiłem sobie za cel również użycie go jak najmniejszego, zarówno pod względem posiadanych zasobów jak i obudowy. Ta decyzja wymusiła zastosowanie nietypowych, aczkolwiek bardzo ciekawych i pouczających rozwiązań.
Mam nadzieję, że ten projekt Was zainspiruje do wykorzystywania zasobów mikroprocesorów w jak największym stopniu.
Założenia
1. Procesor. Wybór mikrokontrolera padł na ATtiny10. Jest to mikroprocesor z rodziny Atmel AVR o chyba najmniejszej obudowie, która posiada tylko 6 pinów. Z tych sześciu dwa tracimy na zasilanie. Pozostają więc tylko 4, z czego jeden to pin RESET. Zadanie wygląda więc dosyć karkołomnie.
2. Wyświetlanie. Gotowy projekt ma przedstawiać wyniki w formie klasycznego układu oczek reprezentującego wynik. Jak w innych tego typu konstrukcjach, Zastosowałem diody LED w takim oto ułożeniu:
Ułożenie diod LED w układzie elektronicznej kostki |
Jak widać na obrazku diod jest 7 sztuk. Tyle wystarczy, aby wyświetlić wszystkie 6 kombinacji. Pamiętajmy tylko, że mamy do dyspozycji właściwie 3 piny procesora.
3. Uruchamianie. Postanowiłem, aby układ był uruchamiany z wyłączenia przyciskiem chwilowym typu tact-switch. Układ ma także sam się wyłączyć po pewnym czasie, np 5 sekundach od wylosowania liczby. W czasie wyłączenia ma on pobierać minimalną ilość prądu.
4. Losowość. Kostka ma być maksymalnie uczciwa, a więc sposoby uzyskiwania przez nią wyniku muszą być możliwie wiarygodne. Mamy tutaj dwie drogi: za pomocą programu wykonujemy generator liczb pseudolosowych, lub wprowadzamy na układ jakąś wartość fizyczną, która jest możliwie nieprzewidywalna. Zdecydowałem się na tą drugą opcję.
5. Wymiary. Ponieważ używamy najmniejszego możliwie mikroprocesorka, warto pójść za ciosem i także cały układ wykonać możliwie mały. Zdecydowałem się więc na elementy w obudowach SMD. Największym elementem jest źródło zasilania. W moim modelu pracuje tutaj maleńkie ogniwo Li-Ion o pojemności (podobno) 240mAh, oznaczone jako 502030. Jego napięcie nominalne umożliwia bezpośrednie zasilenie całego układu. Aby ułatwić sobie lutowanie, a także wykorzystać posiadane części, użyłem obudów o wymiarach 1206. Ale śmiało można zastosować mniejsze, chociażby 0805, nawet bez przeróbek PCB. Aby nie zwiększać wymiarów gotowego urządzenia, nie przewidziałem zastosowania obudowy. Uznałem, że układ w "surowej" wersji będzie się prezentował ciekawiej.
6. Software. Program ma być możliwie krótki, optymalny i realizować prawidłowo cel w postaci wylosowania liczby z zakresu 1-6, oraz jej wyświetlenia w notacji znanej z klasycznych kości do gry w formie sześcianu.
Zastosowane techniki
Przy powyższych założeniach konstrukcja układu będzie wymagała trochę gimnastyki. Dlatego też przedstawię tutaj co ciekawsze elementy projektu:
- Charlieplexing (link Wikipedia),
- Użycie pinu RESET jako funkcjonalnego wejścia układu,
- Generowanie liczb losowych z wykorzystaniem ADC,
- Minimalizowanie zużycia pamięci RAM programu,
- Programowa zmiana częstotliwości taktowania procesora.
Schemat
Elektroniczna kostka - schemat ideowy |
Jak widać, schemat nie jest zbyt skomplikowany, a cały układ składa się z zaledwie 19 podzespołów, wliczając akumulator.
Kondensator C1 o pojemności 100nF, jak i rezystor R8 podciągający pin RESET do VCC są elementami użytymi zgodnie z prawidłowym minimalnym podłączeniem pinów mikroprocesora.
Ciekawą rzeczą jest natomiast sposób podłączenia diod LED. Na samym początku możemy wydzielić spośród 7 diod 4 sekcje. Diody w każdej z sekcji nigdy nie będą się zapalały osobno. Na poniższym rysunku oznaczyłem sekcje kolorami:
Sekcje diod LED |
Jak widać, jedna sekcja składa się z pojedynczej diody - tej, która jest na środku kostki. Diody w sekcjach połączyłem równolegle z użyciem rezystorów ograniczających prąd. Nie łączyłem ich szeregowo, aby móc wysterować sekcję za pomocą możliwie niskiego napięcia.
Sekcje diod LED zostały połączone wg techniki Charlieplexing. Sposób ten umożliwia zminimalizowanie liczby pinów potrzebnych do wysterowania diod LED. Umożliwia on przy sensownym sterowaniu zapalenie tylko jednej diody LED na raz. W naszym przypadku chodzi oczywiście o jedną sekcję. Wyboru sekcji dokonujemy podając odpowiednio stany wysokie, niskie lub wysokiej impedancji na poszczególne wyjścia.
W naszym przypadku możemy ograniczyć się tylko do stanów wysokiego i niskiego, aby uzyskać prawidłowe sterowanie. Jak widać, do wysterowania wszystkich diod wystarczą teraz tylko 3 piny mikroprocesora. Nie zaoszczędziliśmy wiele, bo tylko jeden pin w porównaniu do "zwykłego" sterowania czterema sekcjami, ale w naszym przypadku nawet ten jeden pin robi różnicę. W konsekwencji tego wyboru program odpowiedzialny za wyświetlanie trochę nam się skomplikuje, ale nie ma wyboru.
Zastosowałem nietypowy sposób włączania układu, za pomocą przycisku RESET. I nie chodzi tutaj o zmianę fuse-bitów, aby przejąć kontrolę nad tym pinem za pomocą rejestrów I/O. Po prostu: reset powoduje start programu od początku, program wykonuje zadanie po czym przechodzi w tryb POWER DOWN, z którego w tym wypadku obudzić może go już tylko reset. A pobór prądu przez mikrokontroler w tym trybie wynosi ok. 0,1µA, co i tak przy prądzie samorozładowania akumulatora nie ma większego znaczenia.
Program
Kod na mikroprocesor został napisany w języku C w środowisku Atmel Studio 6.1 i jest do pobrania. Ponieważ jest on króciutki, a za to opatrzony znaczną ilością komentarzy, pozwolę sobie go tutaj wkleić.
/* * tiny10_kostka.c * * Created: 2014-01-04 11:32:57 * Author: Olek */ //F_CPU=8 000 000 UL #include <avr/io.h> #include <inttypes.h> #include <util/delay.h> #include <avr/pgmspace.h> #include <avr/interrupt.h> #include <avr/sleep.h> //#include <stdlib.h> //If use atoi(); #include "cpuclk.h" /* Podłączenie ledów: Mamy 4 sekcje diod, z czego 3 zawierają po dwie diody, a jedna jedną (środkową). Poniżej zdefiniowaliśmy, jakie wartości należy podać na port, aby zapalić JEDNĄ, wybraną sekcję. Nazwy sekcji pochodzą od pierwszej diody LED, jaka się w niej znajduje wg schematu. Dodatkowo definiujemy LED0, które posłuży do wygaszenia diod. Oto rozmieszczenie sekcji: LED1 LED5 LED3 LED7 LED3 LED5 LED1 */ #define LED1 0x01 #define LED3 0x06 #define LED5 0x03 #define LED7 0x04 #define LED0 0x00 //Definiujemy wszystkie możliwości wejść ADC #define ADCA ADMUX = (0<<MUX1)|(0<<MUX0) #define ADCB ADMUX = (0<<MUX1)|(1<<MUX0) #define ADCC ADMUX = (1<<MUX1)|(0<<MUX0) //Tablica informuje, jaki stan trzeba ustawić na wyjściach, aby zapalić pojedynczą sekcję diod w konfiguracji charlieplexing const PROGMEM uint8_t led[] = {LED1,LED3,LED5,LED7}; //Tablica zawiera informacje "obrazki" wyników 1-6 w postaci 4 najmłodszych bitów // Bity: X X X X LED7 LED5 LED3 LED1 //Ustawienie bitów dla diod ma spowodować ich zapalenie podczas multipleksowania //Jest to niejako maska bitów. //Jest ona ściśle powiązana z poprzednią tablicą. /* * Przykład: Chcemy wyświetlić wynik "5", czyli musimy zapalić sekcje LED1, LED5 i LED7; sekcja LED3 ma być wygaszona * Tak więc wybieramy bity: X X X X 1 1 0 1 = 0x0D * Na tak skonstruowanym wyświetlaczu nie da się tego wyświetlić na raz, ale to już jest problem multipleksowania * Tak więc poniższa tablica koduje poszczególne "rysunki" na wyświetlaczu, od 1 do 6 */ const PROGMEM uint8_t anim[] = {0x08,0x01,0x09,0x05,0x0D,0x07}; //deklarujemy zmienną w sekcji noinit, aby zachować wynik poprzedniego losowania podczas resetu procesora volatile uint8_t losowa __attribute__ ((section (".noinit"))); //Do tej zmiennej trafi wynik losowania w postaci zdekodowanych sekcji diod ze zmiennej anim[] static volatile uint8_t wysw; int main(void) { clock_prescaler_select(CLKDF_1); //8MHz //ADC, losowanie liczby //Ustawiamy piny jako wejścia, które "wiszą w powietrzu" //Jest to celowe działanie mające sprowadzić na wynik jak największe szumy i przypadkowość //Dodatkowo wykorzystujemy tutaj niewielki, ale jednak empirycznie sprawdzony efekt //fotowoltaniczny diod LED //Wszystko po to, aby pomiar był jak najbardziej "błędny". DDRB = 0xF8; ADCA; ADCSRA = (1<<ADEN)|(1<<ADSC)|(1<<ADPS2)|(1<<ADPS1)|(0<<ADPS0); //Prescaler 64 //Odczekujemy na wykonanie się pomiaru //można to było zrobić sprawdzaniem flagi, ale już tak zostało ;) _delay_ms(1); //Wylosowana liczba zostaje dodatkowo pomnożona XOR'em z poprzednią wartością. //Należy pamiętać, iż losowa może przyjąć dowolne wartości, a nie tylko 1-6 losowa = (losowa ^ ADCL); //Wyłączamy ADC, nie będzie nam już potrzebny, a dbamy o pobór prądu w trybie POWERDOWN ADCSRA &= ~(1<<ADEN); DDRB = 0xFF; //Reszta z dzielenia przez 6 da nam odpowiedni zakres, który dekodujemy na wyświetlacz wysw = pgm_read_byte(&anim[losowa%6]); //Konfiguracja Timer0 TIMSK0 |= (1<<OCIE0A); //Timer0 Overflow interrupt enable OCR0A = 125; TCCR0B = (1<<WGM02)|(0<<CS02)|(1<<CS01)|(1<<CS00); //Timer0 ON, prescaler 64 sleep_enable(); set_sleep_mode(SLEEP_MODE_IDLE); sei(); while(1) { sleep_cpu(); } } // Ponieważ poza przerwaniem program już nic nie robi, // a dokładniej kręci się w nieskończonej pętli i wywołuje uśpienie // wtedy też nie korzysta z żadnych rejestrów cpu. // dlatego możemy darować sobie odkładanie na stos ich zawartość, // jak i późniejsze ich przywracanie. Oszczędzamy dzięki temu czas, // wielkość kodu, a przede wszystkim pamięć RAM zajmowaną przez stos. ISR(TIM0_COMPA_vect, ISR_NAKED) { // ~1000Hz //Przerwanie zajmuje się właściwie tylko wyświetlaniem wyniku na ledach. //Ze względu na ich podłączenie stosujemy multipleksowanie. //zmienna "krok" ustala, którą diodę mamy zapalić aktualnie static volatile uint8_t krok; // tą zmienną static volatile uint16_t slowtimer; if(krok == 4) krok=0; if(slowtimer == 5000) //jeżeli wynik wyświetlał się już przez 5 sekund { //wyłączamy co się da TIMSK0 = 0; TCCR0B = 0; PORTB = 0; //dobranoc. Teraz tylko reset może przywrócić program do działania. set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_cpu(); } if(wysw & (1<<krok)) PORTB = pgm_read_byte(&led[krok]); else PORTB = 0x00; krok++; slowtimer++; //Gdy korzystamy w przerwaniu z atrybutu "naked", sami musimy pamiętać o wywołaniu instrukcji asemblerowej "reti". asm volatile("reti\n"); }
Mikroprocesor ATtiny10 programujemy za pomocą interfejsu TPI. Ja wykorzystałem do tego celu klon popularnego programatora AVRISP MKII. Można też użyć USBASP - z najnowszym firmwarem i prostą przejściówką jest to także możliwe. Należy tylko pamiętać, że na czas programowania procesor musi być zasilany napięciem 5V. Procesor programujemy przed wlutowaniem w układ. Ja wykorzystałem do tego prostą płytkę - adapter, do której przykładam mikroprocesor bez lutowania:
Adapter do programowania TPI |
Płytka PCB
Elektroniczna kostka - schemat montażowy |
Płytka, jak i schemat zostały zaprojektowane w programie Eagle. Płytka jest jednostronna, wymaga jednak wykonania kilku połączeń od drugiej strony. Ponieważ połączenia przecinają się, należy użyć izolowanego przewodu. Ja użyłem emaliowanego drutu z rozebranego uszkodzonego transformatora. PCB została wykonana metodą termotransferu.
Początkowo płytka miała być dostosowana pod zamontowanie pod nią koszyczka na baterię CR2032, niestety autor biblioteki do Eagle pomylił się zamieniając złącza baterii. Z tego względu koszyczek nie schowa się pod płytkę, jak wcześniej planowałem. Jako przycisk użyłem tact-switch w wersji przewlekanej, lecz można spokojnie użyć wersji SMD. Można użyć różnokolorowych diod LED, trzeba tylko pamiętać o doborze rezystorów tak, aby ich jasność była równomierna. Ja użyłem diod LED w kolorze czerwonym, ponieważ mają one najniższe napięcie przewodzenia. Dzięki temu używając jako źródła zasilania baterii litowej, można ją bardziej "wydoić".
A oto, jak prezentuje się gotowy układ:
Do pobrania
Poniżej udostępniam pliki programu Atmel Studio i Eagle wraz z plikiem PDF gotowym do termotransferu:
Podsumowanie
Projektując tą konstrukcję odczułem dużą radość w sprawianiu, by była ona jak najmniejsza. Od pewnego czasu ATtiny10 to mój ulubieniec. Mam nadzieję, że projekt ma w sobie jakąkolwiek wartość edukacyjną. Starałem się użyć nietypowych rozwiązań, aby można było się z tego budowania jak najwięcej nauczyć.
Kostka stała się jedną z ulubionych zabawek mojej córki. Uważam że dorównuje ona klasycznej "mechanicznej" wersji; tak samo często ginie :-)
Zapraszam wszystkich do wykonania tego układu samemu.
Pozdrawiam,
Olek.
Czy myślałeś nad wprowadzeniem "animacji" rzutu kostką? Tj po przyciśnięciu przycisku wyświetlana liczba kilkanaście razy by się zmieniała, coraz wolniej aż do ustabilizowania się na ostatecznie wyrzuconej. Aby dodać jeszcze trochę realizmu można wprowadzić "pilnowanie", aby zmiany nie wykonywały się w sposób niemożliwy, tj na liczbę po przeciwległej stronie na normalnej kostce.
OdpowiedzUsuńNa to niestety nie wpadłem. Jest to generalnie możliwe, dopisując odpowiednie warunki w obsłudze przerwania timera. Nie wiem jednak, czy kod by się zmieścił, ponieważ trzeba by dodać przynajmniej jedną tablicę "przejść", oraz jakoś uzależnić animacje od wylosowanego wyniku. Warto też pamiętać, że zwiększy to zużycie prądu z baterii, ponieważ czasu na animację nie możemy poświęcić "kosztem" czasu na wyświetlanie wyniku. Kwestią gustu jest także dodawanie takiego "bajeru" i jego użyteczność praktyczna. Może niewielkie, ale jednak miałem jakieś doświadczenie z grami typu RPG, gdzie nieraz trzeba było wykonać kilka rzutów jeden po drugim. Wtedy taka animacja powodowałaby niepotrzebne zamieszanie, porównywalne do rzutu tradycyjną kostką przez pół pokoju ;)
OdpowiedzUsuńA jak ładujesz tego li-polka?
OdpowiedzUsuńOstatecznie reset pozostaje resetem? czyli fusebitów nie ruszamy?
niezły projekt :)
Z ładowaniem trzeba podpiąć się pod pady na płytce ;) Z resztą od czasu publikacji artykułu opracowałem już płytkę z gniazdem baterii CR1220, którą można wymieniać.
OdpowiedzUsuńZ fusami zgadza się - nie trzeba przeprogramowywać, reset pozostaje resetem.