Ohjelman toiminta

Yleistä:
Ohjelma jakaautuu toiminnallisesti kahteen pääkomponttiin. Toinen on pääohjelma, void main (), ja toinen on Timer/Counter 0:n ylivuotokeskeytys. Keskeytys tarkkailee ajankulua ja määrää kaikki ohjelman tapahtumat asettamalla lippuja laskureiden saavuttaessa niille määrättyjä raja-arvoja. Pääohjelma puolestaan tarkailee lippujen tiloja ja niiden perusteella tekee kaiken varsinaisen työn lukuunottamatta keskeytyksen hoitamaa näyttöjen ohjausta lukuunottamatta. Timer/Counter 0 on 8-bittinen laskuri ja siihen on liitetty 6-bittinen esijakaja (prescaler), joka jakaa laskurin arvon 64:llä, jolloin keskeytys suoritetaan 4.096 ms välein käytettäessä 4 Mhz:n kidettä.

Ohjelman toiminta jakautuu neljään eri moodiin. Käytössä oleva moodi on tallessa muuttujassa mode. Moodit ovat play, rest, high_score ja game_over. Varsinaisia päämoodeja ovat play ja rest. Rest-moodissa ohjelma on silloin, kun peli ei ole päällä eikä peli ole juuri hetki sitten loppunut. Pelattaessa ohjelma on luonnollisesti Play-moodissa. High_score- ja game_over-moodeja käytetään pelin loppuessa. Jos peli päättyy uuteen ennätystulokseen, mennään ennalta määritellyksi ajaksi high_score-moodiin, muussa tapauksessa käydään game_over-moodissa. Moodi määrää, kuinka keskeytysrutiinin tulee ohjata pisteiden esittämiseen tarkoitettuja 7-segmenttinäyttöjä ja vaikuttaa siihen, miten pääohjelma reagoi nappuloiden painalluksiin. Moodista riippumatta näyttöjen perustoiminta on sama.

Näyttöjen ohjaus:
Näyttöjen ohjaus tapahtuu keskeytysrutiini loppuosassa. Switch-rakenteella valitaan moodin perusteella, mitä näytössä esitetään ja välkkyykö näyttö ja jos välkkyy, niin miten. Koko ohjauslogiikka perustuu disp_counter-muuttujan tarkkailuun. Play-moodissa näytöt palavat jatkuvasti, rest-moodissa ne vilkkuvat hitaasti samaan tahtiin, high_score-moodi aiheuttaa nopean vilkkumisen samaan tahtiin siten, että palamisaika on pidempi kuin sammuksissaoloaika, ja game_over-moodi on samankaltainen kuin rest-moodi, mutta nopeampi.

Kumpikin näyttö palaa keskeytyksen suoritysvälin ajan eli 4.096 ms kerrallaan ja sen jälkeen on toisen näytön vuoro. Tämä vuorottelu saadaan aikaan disp_counter-muutujan avulla. Muuttujaa kasvatetaan yhdellä aina, kun keskeytysrutiinin suorittaminen alkaa, joten disp_counterin vähiten merkitsevä bitti on joka toisella suorituksella 0 ja joka toisella 1. Sytytettävä näyttö valitaan aina disp_counterin alimman bitin tilan mukaan, joten aina toisen näytön syttyessä toinen sammuu ja vastaavasti toisin päin. Näin näytöt käyvät päällä ja poissa 122 Hz taajudella, joka on niin korkea, että ihmisen silmä ei ehdi huomata välkkymistä, vaan luulee kummankin näyttöjen palavan jatkuvasti.

Kun keskeytysrutiini on selvittänyt, miten näyttöä pitää ohjata, pitää vielä hoitaa itse ohjaus. Sitä varten tallennetaan disp_data-muuttujaan koodi, joka kertoo, mitkä 7-segmenttinäytön segmentit on sytytettävä, jotta haluttu numero saadaan näkymään näytössä. Koodi haetaan ohjelmamuistissa olevasta taulukosta käyttäen indeksinä näyttöön haluttua numeroa. Sitten kutsutaan load_to_disp-funktiota, joka lataa koodin siirtorekisteriin (tarkempaa tietoa siirtorekisterin toiminnasta löytyy tämän pelin toimintaselostuksesta, piirin datalehdestä ja netistä). Lopuksi käytettään vielä makroa, joka kytkee valitulle näytölle virtaa antavalle transistorille ohjausvirran.

Nappulat:
Nappuloiden painalluksiin reagoidaan pääohjelmassa. Pääohjelma kutsuu jokaisella silmukkansa ajokerralla funktiota check_buttons, joka tarkistaa nappuloiden tilan, mikäli keskeytys on lipun asettamalla kertonut, että on sen aika. Nappuloiden tila luetaan, kun edellisestä lukukerrasta on kulunut 4.096 * button_counter_start ms. Button_counter_start-vakion arvo on oletuksena 10, jolloin nappulat tarkastetaan n. 40 ms välein. Tämä viive on tarpeettomaksi jäänyt viive pelin kehityksen alkuajoilta, jolloin nappuloilla ei vielä ollut suotokondensaattoreita ja suodatus tapahtui ohjelmallisesti tämän alipäästösuodattimena toimivan lukuvälin avulla. Viiveestä ei kuitenkaan ole mitään haittaa, joten sen säilyttäminen varmuuden vuoksi ei haittaa. Sen poistamista voi toki kokeilla, jos haluaa vaikkapa lisätä peliin uusia ominaisuuksia ja on tarvetta äärimmäisille koodin koon optimoinneille.

check_buttons-funktio lukee nappuloiden tilan ja valitsee palauttamansa arvon lukutuloksen ja nappuloiden edellisen tilan perusteella. Nappuloiden edellinen tila on tallessa muuttujassa pressed, johon myös tallennetaan uusi tila. Pressed-muuttujan arvo 4 tarkoittaa, että pääohjelma on käsitellyt edellisen lukutuloksen ja seuraavaksi odotetaan, että kaikki nappulat käyvät ylhäällä. Kahden painalluksen välissä on siis oltava hetki, jolloin kaikki nappulat ovat vapautettuina. Jos pressed on arvoltaan 4, check_buttons muuttaa sen arvoon 5, mikäli kaikki nappulat ovat ylhäällä. Muutoksen jälkeen palataan pääohjelmaan. Jos pressed onkin funktioon tultaessa arvoltaan 5, se tarkoittaa, että painalluksen jälkeinen kaikki-nappulat-vapautettuina -hetki on jo havaittu edellisessä tarkistuksessa, joten nyt tarkistetaan, onko jokin nappuloista painettuna. Jos on, niin sen indeksi väliltä 0 – 3 tallennetaan pressed-muuttujaan. Jos mikään nappula ei ole painettuna, jätetään pressed arvoon 5. Lopuksi palataan pääohjelmaan.

Jos check_buttons palauttaa jonkun nappulan indeksin, pääohjelma kutsuu painalluksen oikeellisuuden tarkastaaksen funktiota next_button, joka palauttaa sen nappulan indeksin, jota pelaajan olisi pitänyt painaa. Aluksi check_buttons tarkistaa button_indexin avulla, mistä valopuskurin tavusta sen osoittama nappula löytyy. Kyseinen tavu kopioidaan apumuuttujaan buf ja button_indeksiä vähennetään tarvittaessa niin, että se osoittaa vain tuon yhden valitun tavun alueelle. Muokattu button_index tallennetaan muuttujaan index. HUOM! Valopuskurista ja button_index:stä kerrotaan tarkemmin kohdassa Valot.

Sen jälkeen on vuorossa huima bitin pyörittely. Halutut 2 bittiä ovat tavun bitit 0 ja 1, 2 ja 3, 4 ja 5 tai 6 ja 7. Tavussa on siis neljä kahden bitin kenttää ja se pitäisi saada sellaiseen muotoon, että indexin osoittama kenttä olisi biteissä 0 ja 1 eli kahdessa vähiten merkitsevässä ja kaikki muut bitit olisivat nollia. Aluksi index kerrotaan kahdella. Sitten luodaan sopiva bittimaski muuttujaan mask shiftaamalla lukua 3 (eli vain kaksi alinta bittiä ykkösiä) indexin verran oikealle, jolloin maskissa on kaksi bittiä halutun bittikentän kohdalla. Tällä maskilla nollataan loogista and-operaatiota käyttäen buf-muuttujasta kaikki bitit, jotka eivät kuulu haluttuun bittikenttään. Lopuksi buf:ia shiftataan oikealle yhtä paljon kuin maskia shiftattiin aikaisemmin vasemmalle ja tuloksena on se, mitä haluttiin. Tämä arvo palautetaan pääohjelmaan.

Valot:
Valojen ohjaus ja se, mistä käsin valoja ohjataan, ovat riippuvaisia moodista. Play- ja rest-moodeissa valojen ohjauksesta huolehtii funktio new_light ja high_score- ja game_over-moodien valojen ohjaus on sisällytetty pelin päättyessä suoritettavaan finish_game-funktioon. Tässä kappaleessa käsitellään valojen ohjaus Play- ja rest-moodeissa ja muista moodeista kerrotaan enemmän edempänä.

Play- ja rest-moodeissa vain yksi valo palaa kerrallaan. Valojen palamisaikaa ohjataan muuttujan light_counter-muuttujan avulla, jota keskeytys vähentää jokaisella tai joka toisella ajokerrallaan moodista riippuen siten, että Play-moodissa vähennetään jokaisella ajokerralla ja rest-moodissa joka toisella. Vähennykset tehdään eri taajuksilla, jotta play-moodissa voitaisiin säätää palamisaika riitävän tarkasti ja rest-moodissa saataisiin pitkä palamisaika. Palamisajan säätäminen light_counterin avulla toimii siten, että muuttujaan tallennetaan haluttua palamisaikaa ilmaiseva luku. Kun light_counter saavuttaa nollan, keskeytys asettaa muuttujan light_flag arvoon 1, mistä pääohjelma tietää, että on aika kutsua new_light-funktiota.

Pelin vaikeutuminen pelin edetessä on luonnollisesti järjestetty lyhentämällä valojen palamisaikaa jatkuvasti. Muuttuja light_counter_start kertoo kauanko sytytettävän valon annetan palaa. Pelin ollessa käynnissä eli moodin ollessa play, light_counter_startin arvoa vähennetään aina kun uusi valo sytytetään. Arvoa vähennetään sitä enemmän, mitä suurempi se on, joten valojen palamisajan muuttuminen muistuttaa exponenttifunktiota. Tähän on syynä se, että jos aika muuttuu lineaarisesti, pelaajasta näyttää, kuin peli nopeutuisi ensin hitaasti ja yhtäkkiä, porrasmaisesti, alkaisi nopeutua nopeammin. Laittamalla peli todellisuudessa nopeutumaan ensin nopeasti ja kaiken aikaa hitaammin, saadaan aikaan vaikutelma, että peli nopeutuisi lineaarisesti.

Koska pelissä on oltava mahdollista jäädä valosta jälkeen, eli painallus on hyväksyttävä, vaikka sitä vastaava valon välähdys olisikin jo ohi, valojen syttymijärjestys on pidettävä muistissa. Tähän tarkoitukseen on kolme muuttujaa: light_buf_1, light_buf_2 ja light_buf_3. Koska valoja on neljä, tarvitaan 2 bittiä kertomaan, mistä valosta on kyse. Koska puskurimuuttujat ovat 8-bittisiä tavuja, puskuriin mahtuu 12 indeksiä. Kun seuraavaksi sytytettävä valo on arvottu, se laitetaan muistiin. Puskuri toimii siten, että sen alku on light_buf_1:n vähemmän merkitsevässä päässä ja loppu light_buf_3:n enemmän merkitsevässä päässä. Kun puskuriin tallenetaan uusi indeksi, täytyy kaikkia puskurissa ennestään olevia indeksejä siirtää pykälä loppuun päin, jotta alkuun saadaan tilaa uudelle indeksille. Tämä tehdään shiftaamalla puskuritavua kahden bitin verran vasemmalle ja sen jälkeen puskuritavujen 3 ja 2 tapauksessa kopioimalla edellisen puskuritavun (2 ja 1) kaksi ylintä bittiä shiftatun tavun alimmiksi biteksi. Shiftaus ja bittien kopiointi tehdään peräkkäin yhdelle tavulle kerrallaan, jotta yhtään indeksiä ei hukattaisi. Siirtojen yhteydessä myös kasvatetaan yhdellä muuttujaa button_index, joka voi saada arvoja välillä 0 – 11 ja joka kertoo, missä kohtaa puskuria on menossa se valo, jota vastaavaa nappulaa odotetaan painettavan seuraavaksi. Jos button_index pääsee kasvamaan arvoon 12 eli pelaaja jää jälkeen 12 valon väläystä jälkeen, tapahtuu puskuriylivuoto, jonka takia seuraava painallus tulkittaisiin todennäköisesti väärin. Siksi button_indexin saavuttaessa kielletyn arvon peli loppuu.

Nollattuaan light_flagin funktio new_light haarautuu heti aluksi light_moden arvon mukaan. Arvo 0 tarkoittaa, että kaikki valot ovat olleet sammuksissa ja arvo 1 kertoo, että yksi valo on palanut. Light_moden vaihdetaan ja light_counter käynnistetään uudelleen antamallasille arvoksi aika, jonka kuluttua valojen tilaa halutaan vaihtaa seuraavan kerran. Sen jälkeen joko sammutetaan valo tai sytytetään valo light_moden arvon mukaisesti. Valojen sammuttamiseen ei vaadita muuta kuin yksi makro, mutta valon sytyttäminen, tai tarkemmin sanottuna seuraavaksi sytytettävän valon valitseminen on hieman monimutkaisempaa. Ensin vähennetään light_counter_startia sen sen hetkisen arvon mukaisesti. Sitten järjestetään valopuskuri uudelleen edellisessä kappaleessa kuvatulla tavalla. Lopuksi sytytetään juuri arvottu valo kutsumalla light_on-funktiota parametrinä sytytettävän valon indeksi.

Satunnaislukugeneraattori:
Ohjelmaan kuuluu satunnaislukugeneraattori, jolla arvotaan sytytettävät valot. Siinä on kahdesta 8-bittisestä tavusta koostuva 16-bittinen sana, joka alustetaan aina peliä aloitettaessa sen hetkisellä Timer/Counter 2:n arvolla. Koska ei voida ennustaa, milloin peli aloitetaan ja mikä on Timer/Counter 2:n arvo tuolla hetkellä, saatava alustusarvo on käytännössä satunnainen. Satunnaislukua tarvittaessa otetaan vain tarvittavan määrä bittejä jostakin kohtaa sanaa. Tässä pelissä käytetään kahta alinta bittiä.

Kun yksi luku on otettu, pitää sanaa manipuloida sopivalla alogoritmilla ennen seuraavan luvun lukemista. jotta seuraavallakin kerralla saataisiin satunnainen luku. Manipulointi tapahtuu siten, että sanan biteille 15, 14, 12 ja 3 tehdään looginen xor-operaatio ensin pareittain ja sitten vielä saadulle kahdelle bitille. Käytännössä saatu bitti on 1, jos 4 bitin joukossa on pariton määrä nollia (tai ykkösiä), joten ohjelma hoitaa tehtävän xor-operaatioiden sijaan laskemalla nollien määrän. Saatu bitti laitetaan satunnaislukugeneraattorin sanan vähiten merkitseväksi bitiksi, kun sanaa on ensin shiftattu vasemmalle yhdellä bitillä.

Koska pelaaminen vaikenee pelin nopeuduttua todella paljon, jos sama valo voi välähtää peräkkäin monta kertaa. Näinhän voi käydä, jos arvontaa ei kontrolloida mitenkään. Siksi ohjelma laskee, montako kertaa sama tulos on tullut peräkkäin ja jos se huomaa tuloksen toistuneen 3 kertaa, se arpoo uusia lukuja, kunnes tulos muuttuu. Tässä on se vaara, että periaatteessa ohjelma voisi jäädä loputtomaan silmukkaan, jos tulos ei muuttuisikaan, mutta se on niin epätodennäköistä, että riskin voi huoletta ottaa.

Pelin aloitus:
Peli alkaa, kun nappulaa painetaan moodin ollessa rest eli kun peli ei ole ennestään käynnissä. Tällöin pääohjelma kutsuu funktiota start_game, joka huolehtii pelin alkamiseen liittyvistä valmisteluista ja alustuksista. Start_game kieltää heti aluksi keskeytysten suorittamisen, jotta ne eivät häiritsisi sen toimintaa, ja muuttaa asettaa moodiksi play. Sitten se lukee 16-bittisen Timer/Counter 1:n sen hetkisen arvon satunnaislukugeneraattorin siemenluvuksi. Light_counter_start asetetaan arvoon light_counter_play_start, joka on vakio, joka määrää ajan, jonka ensimmäisenä syttyvä valo palaa. Light_flag nollataan, jotta ensimmäinen valo syttyisi välittömästi pelin alettuaa. Button_indexille annetaan arvo 0xFF eli -1, jolloin se asettuu nollaksi osoittamaan ensimmäistä puskuripaikkaa, kun sitä kasvatetaan ensimmäistä valoa arvottaessa. Sitten nollataan vielä pisteet, ennen kuin sallitaan keskeytykset jälleen. Lopuksi start_game odottaa, kunnes kaikki nappulat ovat vapautettuna ja käynnistää yhden sekunnin mittaisen viiveen, jonka tarkoituksena on selventää pelin alkamista. Viive on toteutettu delay_counterin avulla. Keskeytys vähentää delay_counteria yhdellä joka kahdeksannella ajokerrallaan ellei se jo ole nolla, joten viiveen tekeminen on helppoa antamalla delay_counterille viiveen pituutta vastaava arvo ja odottamalla, kunnes se on jälleen nolla.

Tämä jälkeen ohjelma palaa pääohjelmaan ja jatkaa toimintaansa play-moodissa.

Pelin loppuminen:
Pelillä on kaksi loppumistapaa: virhepainallus ja valopuskurin ylivuoto pelaajan jäätyä jälkeen. Näistä virhepainallus on paljon todennäköisempi. Kun tulee tapahtuma, joka aiheuttaa pelin loppumisen, muuttuja end_game saa arvon yksi ja loppumisen johtuessa puskuriylivuodosta, ohjelma palaa välittömästi takaisin pääohjelmaan. Pääohjelman silmukan viimeinen tehtävä on tarkistaa end_gamen avulla, onko peli loppunut. Jos peli on loppunut, end_game palautetaan aluksi nollaksi. Sen jälkeen pääohjelma tutkii, onko tehty uusi ennätys. Lopuksi kutsutaan funktiota finish_game, joka huolehtii pelin loppumiseen liittyvistä toimenpiteistä. Funktiolle annetaan parametrinä tieto siitä, tehtiinkö uusi ennätys vai ei.

Ensitoimenaan finish_game laittaa moodiksi high_score tai game_over sen mukaan, mikä tulos oli. Sen jälkeen haaraudutaan moodin perusteella. Jos uutta ennätystä ei tullut, tehdään delay_counterin avulla vakion game_over_delay pituinen viive, jonka aikana vilkutetaan valoja samaan tahtiin siten, että ne ovat päällä pidempään kuin poissa. Vilkutuksen ajoitus hoidetaan light_counterilla ja päälläoloaika voidaan määrätä vakiolla light_counter_go_on_start ja sammuksissaoloaika vakiolla light_counter_go_off_start.

Jos tehtiin uusi ennätys, vilkutellaan valoja kuten normaalissakin pelin loppumisessa, mutta tehdään myös muuta. Ennen vilkuttelua tallennetaan uusi ennätys mikrokontrollerin EEPROM-muistiin, jotta se säilyisi tallessa, vaikka pelistä katkaistaisiin virta. Pisteiden tallentamisen ajaksi kielletään keskeysten suorittaminen, jotta ne eivät pääsisi pilaamaan tallentamista. Ennätyksen tallentamisen jälkeen tehdään viive vilkuttelua varten samoin kuin game_over-tapauksessakin, nyt tosin otetaan viiveen kesto vakiosta high_score_delay. Lisäksi vilkutusohjelma on erilainen. Valot syttyvät vuoronperään niin, että valo vaeltaa jatkuvasti rivin laidasta laitaan ja takaisin. Valon päälläoloaika määräytyy vakiolla light_counter_hs_start.

Lopuksi finish_game laittaa tarvittavat asetukset rest-moodia vastaavaan tilaan. Mode saa luonnollisesti arvokseen rest ja valojen ajoituksiin liittyvät muuttujat asetetaan uudelleen. Rest-moodissa valot vilkkuvat samoin kuin play-moodissa, mutta nopeus ei muutu. Light_counter_start saa vakion light_counter_rest_start mukaisen arvon ja light_counter luonnollisesti alustetaan samalla arvolla. Tämän jälkeen ohjelma palaa pääohjelmaan ja toiminta jatkuu rest-moodissa.

Laitteen käynnistyminen:
Kun laite käynnistyy, ohjelma aloittaa toimintansa funktiosta main, joka kutsuu välittömästi funktiota initialize. Sen tehtävänä on nimensä mukaisesti asettaa muuttujille tarvittavat alkuarvot. Aluksi ladataan EEPROM-muistista ennätyspisteet. Sitten kutsutaan funktiota check_eeprom_corruption, joka varmistaa, että lukutulos on oikea (enemmän tietoa löytyy kohdasta Pisteiden lasku). Seuraavaksi alustetaan joukko muuttujia. Pressed saa alkuarvokseen 4, joten yhtään painallusta ei hyväksytä ennen kuin kaikki nappulat ovat ollet ylhäällä kerran. Satunnaislukugeneraattori saa siemenluvun, joka muuttuu joka kerta, kun peli aloitetaan. Koska alkuasetuksissa asetettava luku on ennalta määrätty, valot vilkkuvat aina samassa järjestyksessä siihen asti, kunnes peli aloitetaan ensimmäisen kerran.

Sitten asetetaan moodiksi rest sekä siihen liittyen annetaan light_counter_startille alkuarvoksi vakio light_counter_rest_start ja light_counterille vakio light_counter_rst_start. Light_mode asetetaan nollaksi, jotta seuraava new_lightin ajo saa valon syttymään, ja IO-porttit laitetaan oikeisiin tiloihin. Lopuksi asetetaan Timer/Counterit 0 ja 1 toimintakuntoon ja merkitään Timer/Counter 0 ainoaksi sallituksi keskeytykseksi. Ennen paluuta pääohjelmaan sallitaan vielä keskeytykset.




Copyright Antti Gärding 2003, 2004