Ohjelmiston suunnittelu aloitettiin tärkeimmästä laiteohjaimesta, LCD-näyttöjä päivittävästä rutiinista. Koska sen rakenteella oli vaikutuksia laitteiston suunnitteluun, täytyi se laatia jo hyvin varhaisessa vaiheessa (yksityiskohdat tosin täydentyivät myöhemmin). Kuten edellä on kerrottu, johtaa LCD-päivitysvaatimuksista poikkeaminen näyttöjen vaurioitumiseen pysyvästi, joten päivitysprosessin vaatimukset vaikuttavat myös koko muun ohjelmiston suunnitteluun.
Näyttöjen laiteohjaimen jälkeen laadittiin muut laiteohjaimet, koottiin tarvittavat aliohjelmakirjastot ja viimeiseksi laadittiin pääohjelma. Seuraavissa kappaleissa tarkastellaan vielä ohjelmiston rakenteeseen vaikuttaneita päätöksiä ennen ohjelmiston toteutuksen suppeaa esittelyä.
Ohjelmiston aikakriittiset toiminnot on laadittu assembler-kielellä, kuten osa muustakin ohjelmistosta (mm. valmiina käytetty liukulukukirjasto). C-kieltä on käytetty sellaisten osien tekemiseen, joiden suoritusajalla ei ole suurempaa väliä. Ohjelmankehitystyökalujen valinnasta on oma kappaleensa.
Jäljempänä esitetyt ohjelmaesimerkit on otettu suoraan käytössä olevasta ohjelmistosta, mutta pieniä muutoksia on tehty esityksen helpottamiseksi (mm. symbolisia vakiota on korvattu absoluuttisilla arvoilla).
Kaikkien 68HC11 CPU:n suorittamien käskyjen suoritusaika on ilmoitettu datakirjoissa. Käskyjen suoritusajat ovat myös suoritusjärjestyksestä tms. riippumatta samat, joten ohjelman tarvitsema kokonaissuoritusaika on helposti laskettavissa.
Yleisimpien käskyjen suoritusajat vaihtelevat 2-7 kellojakson välillä, joten yllä olevat vaatimukset ovat varsin tiukkoja (muutamat poikkeukset, esim. jakolaskukäskyt, käyttävät enemmän aikaa).
Buffaloon tehtiin pieniä muutoksia sen sovittamiseksi toimimaan tällä kortilla (alkuperäinen versio oli tehty käytettäväksi Motorolan 68HC11 evaluointikortilla).
Tärkeät toiminnot, joita ei ohjata "ulkoisilla ärsykkeillä", toteutettiin ajastimella tuotettavia keskeytyksiä hyväksi käyttäen. LCD-päivityksen, jonka toimivuus on ehdottomasti varmistettava, käyttämälle keskeytykselle annettiin muita korkeampi prioriteetti (68HC11:n sisäisten I/O-toimintojen generoimista keskeytyksistä (15 erilaista) yksi voidaan määrätä muita tärkeämmäksi. Muilla on kiinteä, ennaltä määrätty prioriteettijärjestys). Myös kellonajan päivittäminen ratkaisiin ajastimen avulla (reaaliaikakellopiiriltä voidaan lukea aika, mutta mitään ilmoitusta ajan muuttumisesta ei siltä saada).
Asynkroninen sarjaliitäntä (SCI) antaa keskeytyksen, kun uusi merkki on vastaanotettu tai edellinen merkki on lähetetty. Myös anturin pulssien laskennasta saadaan keskeytyksiä, koska se on liitetty ajastimeen sisältyvään laskuriin.
Muut laiteohjaimet sekä prosessit käsitellään keskeytyspalveluiden ulkopuolella. Laiteohjaimistakin osa voitaisiin käsitellä prosesseina.
Ensimmäinen arvio oli, että MCX11 sopisi tähän projektiin varsin hyvin. Sen käyttämisellä olisi kuitenkin ollut haittavaikutuksia, jotka arveltiin saavutettavia hyötyjä suuremmiksi.
MCX11:n prosesseilla on kiinteä prioriteettijärjestys, joka toimii samalla prosessien numerointina (jokaisella prosessilla on siis muista eroava prioriteetti). Siten ajovuoron saa aina prosessi, jota suuremmalla prioriteetilla olevat prosessit eivät ole ajovalmiita. Ajovuoro vaihtuu välittömästi, kun jokin suurempiprioriteettisista prosesseista saavuttaa valmiuden.
Edellä luetelluista ohjelmiston osista olisi ollut helppoa muodostaa ainakin 4-6 prosessia. Myös prosessien prioriteetit olisi ollut mahdollista määrätä kiinteästi (alimmalla prioriteetilla toimisi mittaustulosten laskenta- ja esitysprosessi, koska se on käytännössä koko ajan ajossa), joskin prioriteetiltaan samanarvoisista prosesseista olisi voinut olla hyötyä (toiseksi matalan prioriteetin prosessiksi olisi sopinut sarjaliitäntäprosessi).
Useista prosesseista seuraa muistin käytön kannalta haittavaikutuksia: jokaisella prosessilla on oma pino, jolle täytyy varata tilaa muistista. Lisäksi MCX11 tarvitsee oman pinonsa ja prosessien hallintaa varten muutaman tavun muistia/prosessi (jälkimmäinen tilantarve on pieni).
Oman pinon varaaminen jokaiselle prosessille kasvattaa muistin tarvetta suhteellisen paljon: Jos oletetaan, että keskimäärin pinolle riittää esim. 50 tavua, tarvitaan kuudelle prosessille 300 tavua muistia, n. 30% käytettävissä olevasta muistista (MCX11:n pinon ja muuttujien lisäksi). Vaikka vaatimusten määrittelyssä arvioitujen tarpeiden mukaan (tarvearvio 0.5kt, kontrollerissa on 1kt) muistia pitäisi olla riittävästi, kannattaa yrittää säästää muistia mahdollisia myöhempiä ohjelmiston laajennuksia varten.
Muistin säästämiseksi ohjelmisto päätettiin toteuttaa yhtenä prosessina, jossa toteutetaan aiemmin esitetty jako prosesseihin tilakoneiden avulla. Tilakonetoteutuksesta on suhteellisen helppo siirtyä erillisten prosessien käyttämiseen jälkikäteen, jos siihen löytyy tarvetta.
Edellä kuvattu prosessimalli toteutetaan kuvaamalla jokainen prosessi tilakoneilla. Prosessin toteuttava aliohjelma laaditaan siten, että se ei missään vaiheessa jää odottamaan mitään tapahtumia eikä suorita kertatoimenpiteitä, jotka eivät valmistu kohtuullisessa ajassa. Varsinainen pääohjelma vastaa kernelin skeduleria, se vain kutsuu ikisilmukassa jokaista prosessialiohjelmaa.
Tilakonetoteutuksen erona perinteisiin prosessitoteutuksiin verrattuna on siis tietynasteinen käänteisyys: "Normaali" prosessi voi jäädä odottamaan tapahtumaa esim. tekemällä käyttöjärjestelmäkutsun, josta palataa vasta kun prosessi on taas ajovalmis. Tilakonealiohjelmana toteutettu prosessi taas suorittaa normaalin aliohjelmasta paluun, kun sen täytyy odottaa tapahtumaa. Asiaa voidaan kuvata vaikka sanomalla, ettei käyttöjärjestelmä ole sovelluksen alla vaan päällä.
Tätä prosessimallia käytettäessä kullekin prosessille kuuluvat yksityiset muuttujat varataan yhteisestä muistista kiinteästi, koska prosessien vaihtojen yli säilyvää lokaalia muistia (joka yleensä varataan pinosta) ei ole. Tarvittavan muistin määrää voidaan pienentää käyttämällä samaa muistialuetta eri tarkoituksiin prosessin tilan määräämänä. C-kielen union-tyypi sopii tähän tarkoitukseen erittäin hyvin.
Prosessien laatiminen muuttuu myös yksinkertaiseksi yhteisten tietorakenteiden käsittely osalta, koska kyseessä ei ole keskeyttävä moniajo eikä yhteisiä tietorakenteita siten tarvitse lukita. Keskeyttävän moniajon puuttumisen takia on noudatettava tiukasti edellä mainittuja aliohjelmien suunnittelusääntöjä.
Käyttöliittymän ohjaustietojen kuvaamiseen on käytetty struktuuria ui_sel, joka sisältää mm. tiedot ohjausnappien käyttötarkoituksista, sekä pointterin näytön yläosan päivittämiseen tarvittavaan funktioon.
/* uistruct.h */ #define NUM_BUTTONS 4 struct ui_sel { char *mode_name; int (*update_screen)(); int flags; struct button { char *description; struct ui_sel *(*funct)(); void *arg; } button[NUM_BUTTONS]; }; extern struct ui_sel *ui_state;Käyttöliittymän tilan määrää yksikäsitteisesti pointteri ui_state, joka osoittaa kulloinkin aktiivisena olevaan struktuuriin. Alla on struktuurien initialisointikoodista pala, joka kuvaa ajanottofunktion käyttöliittymää. Kannattaa huomioida, että nämä struktuurit sijoitetaan kokonaisuudessaan ROM-muistiin. Tässä ei kuitenkaan käytetä const-sanaa määrittelyn yhteydessä, koska käytetty C-kääntäjä on ns. vanhan määrittelyn mukainen [Ker78] eikä tue ANSI C-standardin mukanaan tuomia laajennuksia.
struct ui_sel ui_m_timer = { NULL, ui_tmr_update_screen, DISPLAY_BUTTON_TEXTS|REP_CALL_UPDATE_FUNCT, {{ " =0 ", ui_tmr_clear_timer, &ui_m_timer }, { "käy ", ui_tmr_start_timer, &ui_m_timer }, { "seis", ui_tmr_stot_timer, &ui_m_timer }, { "-->>", ui_select_segdisp_mode, SEGDISP_MODE_1 }} };Kun yhtä käyttöliittymän napeista painetaan, kutsutaan funktiota ui_select_function, joka saa parametrikseen painetun napin numeron (1..4):
ui_select_function(key) unsigned int key; /* 1..NUM_BUTTONS (1..4) */ { struct ui_sel *(*f)(); if(--key < NUM_BUTTONS) { if(f = ui_state->button[key].funct) { ui_state = (*f)(ui_state, ui_state->button[key].arg); update_screen(); } } }Tämän funktion koodi käännettiin kahdella erilaisella C-kääntäjällä. Syntyneen koodin määrästä on yhteenveto alla olevassa taulukossa. Assembleriksi käännettyjen tiedostojen käännöslistaukset on esitetty liitteessä, koska ne vaativat suhteellisen paljon tilaa.
Kääntäjä | Koodin koko tavuina |
---|---|
GCC, ei optimointia | 190 |
GCC, optimointi (-O) | 102 |
5+c, ei optimointia | 131 |
Kääntäjistä on lisää tietoja kappaleessa ohjelmistotyökalujen valinta.
LCD-näyttöjä käsittelevän osuuden jälkeen selostetaan hieman I2C-väylän simulointia ohjelmallisesti. Laitteistorajoitteiden (esim. tässä tapauksessa I2C-väylän tuen puuttumisen) kiertäminen ohjelmallisesti on käytännön toteutuksissa hyvinkin yleistä, muttei aina suoraviivaista.
Ajastin on ohjelmoitu antamaan keskeytys säännöllisin välein. Samanaikaisesti saadaan ajastimesta ulos signaali (OC2), joka ohjaa näyttöjen Lp-signaalia (Lp siirtää siirtorekisterin sisällön LCD-elementtiä ohjaavan piirin ulostuloihin). Ajastimen antaman keskeytyksen palvelurutiini siirtää tarvittavan määrän tavuja näyttöjen siirtorekistereihin odottamaan seuraavaa Lp-pulssia (joka annetaan seuraavan keskeytyksen yhteydessä).
Menettelytavasta seuraa, että ajastimen antama keskeytys voidaan palvella missä vaiheessa vain näyttöjen päivityssykliä, kunhan keskeytysrutiini ehditään suorittaa kokonaisuudessaan ennen seuraavaa samasta syystä aiheutuvaa keskeytystä (etteivät näytöt vaurioidu).
Alla on esitetty näyttöjen päivitykseen liittyvä ohjelmakoodi kokonaisuudessaan.
Ajastimen ja SPI-väylän alustus
SPI-liitäntä initialisoidaan:
initspi ldaa #$54 ; SPI:n tila= ei keskeytyksiä, SPI enabloidaan, ; SPI:n master mode, polariteetti näytöille sopivasti ; ja SPI:n kello = CPU-kello / 2 staa SPCR ; asetetaan em. ehdot voimaan ldaa SPSR ; nollataan mahdollisesti päällä oleva SPIF-lippu ldaa SPDR ; poistetaan vanha data, jos sellaista oliAjastimen alustus:
inittmr ldaa #$60 ; estetään muut ajastimen keskeytykset paitsi staa TMSK1 ; TOC2:lta (näytöt) ja TOC3:lta (500ms) tulevat ldaa #$FF ; kuitataan mahdollisesti voimassa olevat staa TFLG1 ; keskeytykset ldaa #$80 ; Ulostulojen toiminta: OC2=nollataan vertailun staa TCTL1 ; seurauksena, OC3 on tavallinen I/O-linja
Keskeytysaliohjelma
Ensimmäiseksi keskeytysrutiini asettaa OC2-ulostulon takaisin 1-tilaan. I/O-linjaan ei voi vaikuttaa suoraan (portin A kontrollointirekisterien kautta), koska se on initialisoinnissa annettu ajastimen kontrolloitavaksi.
intr_TOC2 ldaa #$C0 staa TCTL1 ; alustetaan OC2 asetettavaksi ldaa #$40 staa CFORC ; Pakotetaan TOC2-vertailu: 1 -> OC2-pinniSeuraavaksi asetetaan ajastin keskeyttämään ja nollaamaan OC2 uudestaan oc2delay:n määräämän ajan kuluttua.
ldaa #$80 ; asetetaan OC2 nollattavaksi, staa TCTL1 ; kun vertailu tapahtuu ldd TOC2 ; otetaan edellinen vertailuaika, addd oc2delay ; lisätään siihen viive, std TOC2 ; ja talletetaan takaisin vertailurekisteriin ldaa #$40 staa TFLG1 ; Nollataan ajastimen keskeytyspyyntöAjastin on nyt alustettu seuraavaa keskeytystä varten.
Seuraavaksi valmistaudutaan lähettämään näyttöjen siirtorekistereihin edellisen datan komplementti.
ldx #seg_img ; X osoittaa lähetettävän datan puskuriin ldy #4*7-1 ; Y on lähetettävien tavujen lukumäärä - 1 ; 4 näyttöä, 7 tavua / näyttö com sencpl ; komplementoidaan lippu, joka määrää bmi int_scpl ; lähetettävän datan komplementoinnistaOhjelman suorituksen jatkuessa suoraan lähetetään puskurin sisältö sellaisenaan. Joka toisella kerralla hypätään osoitteeseen int_scpl, jolloin lähetetään data komplementoituna (alempana).
Lähetysrutiineja on siis kaksi, joista toinen näyttää turhalta - olisihan toki mahdollista sisällyttää lähetysrutiiniin XOR-operaatio yllä olevan sencpl-muuttujan kanssa, jolloin pärjättäisiin yhdellä lähetyssilmukalla. Selitys on varsin yksinkertainen: Ensimmäinen rutiini, joka lähettää datan komplementoimatta, on nopein mahdollinen toteutus datan lähettämiseksi. SPI käyttää kunkin tavun lähettämiseen juuri saman määrän kellojaksoja jonka kukin silmukan kierros kestää. Siten siirron valmistumisesta kertovaa SPIF-lippua (SPI Interrupt complete) ei tarvitse testata, mutta SPSR (SPI status register) täytyy lukea, jotta seuraava tavu voidaan lähettää.
spinxt1 ldaa ,x ; 4 ; otetaan tavu puskurista ldab SPSR ; 4 ; luetaan SPSR SPIF-lipun nollaamiseksi staa SPDR ; 4 ; lähetetään tavu inx ; 3 ; osoitetaan seuraavaa dey ; 4 ; ja vähennetään jäljelläolevia bne spinxt1 ; 3 ; hyppää jos Y != 0Numero kommenttikentän alussa kertoo käskyn käyttämien CPU-kellojaksojen lukumäärän.
Viimeinen tavu lähetetään erikseen. Koodia syntyy 8 tavua enemmän, mutta suoritusaikaa säästyy 10 kellojaksoa. Ajan säästäminen kannattaa, koska CPU:n kellotaajuus on tarkoitus laskea niin alas että tämän keskeytyksen palveleminen vie suhteellisen suuren osan kokonaisajasta.
ldaa ,x ; 4 ; viimeinen puskurista ldab SPSR ; 4 ; nollaa SPIF staa SPDR ; 4 ; ja matkaan rti ; palataan keskeytyksestäViimeisenä on jäljellä komplementoidun datan lähettäminen. Nyt aikaa kuluu 2*28 kellojaksoa edellistä enemmän coma-käskyjen takia.
Komplementoitu data voitaisiin myös muodostaa etukäteen RAM-muistiin (jota kuluisi silloin 28 tavua enemmän), mutta komplementointi ajon aikana on turvallisempaa: Jos joku muu osa ohjelmistota korruptoi lähetyspuskuria, mutta tämä keskeytyspalvelu toimii yhä (ja vaikka se olisi ainoa ohjelmiston toimiva osa), eivät LCD-näytöt vaurioidu.
int_scpl spinxt2 ldaa ,x ; 4 coma ; 2 ; komplementoi ldab SPSR ; 4 staa SPDR ; 4 inx ; 3 dey ; 4 bne spinxt2 ; 3 ldaa ,x ; 4 coma ; 2 ldab SPSR ; 4 staa SPDR ; 4 rtiKeskeytyspalvelun suoritusaika
Edellä olevan rutiinin kokonaissuoritusajaksi saadaan 670 kellojaksoa, kun data lähetetään sellaisenaan ja 726 kun se invertoidaan. Ajat eivät sisällä keskeytyksen käsittelyyn tarvittavaa aikaa. Yleisemmin suoritusajaksi saadaan invertoimattomassa tapauksessa
Pahimmassa tapauksessa muut keskeytykset ovat estettynä siis n. 750 CPU-kellojakson ajan.
Rutiinin suoritusajan laskemisen jälkeen voitiin määrittää tarvittava kellotaajuus (kts. edellinen luku).
Segmenttitietojen muodostaminen
Ohjelman kannalta on yksinkertaisinta, jos näytölle voidaan kirjoittaa ASCII-merkkejä. Silloin sama merkkijono voidaan tulostaa sellaisenaan myös alfanumeeriselle LCD-näytölle tai lähettää sarjalinjan kautta.
Tässä käytettyjä moduleita ohjataan kuitenkin segmenteittäin. LCD-näyttöjä ohjaaviin siirtorekistereihin lähetetään bittijono puskurista seg_img. Puskurissa on näyttöä kohti 56 bittiä, joista 49 ohjaa näyttömodulin segmenttejä ja yksi näytön backplanea (6 on käyttämättömiä). Segmenttejä ohjaavien bittien sijainti lähetyspuskurissa riippuu vain siirtorekisterien kytkennästä näyttöön, eli piirilevyn rakenteesta. Järjestys on siten lähes satunnainen.
Näytöille kirjoittamisen helpottamiseksi tehtiin merkkigeneraattori, joka muodostaa taulukon avulla ASCII-merkeistä 7-segmenttinäytöille sopivaa dataa (eli täyttää em. seg_img-puskurin). ASCII-merkkejä käytetään, koska silloin sama tulostettava data voidaan näyttää segmenttinäytöillä, lähettää sarjalinjasta ulos tai näyttää alfanumeerisella LCD-modulilla (vaikka 7-segementtinäytöt on tarkoitettu ensisijaisesti numeroiden näyttämiseen, voidaan suurin osa kirjaimistakin esittää ymmärrettävästi).
Datalinjan kaksisuuntaisuus on piirre, joka vaikuttaa ohjelmiston rakenteeseen. Kellopiiri nimittäin vaihtaa oman linjansa suuntaa antaakseen ACK-pulsseja (0-tila) aina vastaanotettuaan 8 bittiä. Väärin tehdyllä ohjelmistolla on teoriassa mahdollista vahingoittaa joko kontrolleria tai kellopiiriä.
68HC11:n I/O-portit sisältävät, kuten monien muidenkin kontrollerien ja I/O-laitteidenkin, erilliset rekisterit dataa ja linjojen suuntia varten. Linja käyttäytyy eri tavalla riippuen siitä onko se määritelty tuloksi vai lähdöksi. Jos linja määrätään lähdöksi, se joko syöttää tai nielee virtaa riippuen datarekisterin tilasta (1/0). Tuloksi määriteltäessä ei virtaa juurikaan kulje.
Ongelmia saattaa syntyä, jos I/O-portin datalinjaksi määrätty linja on tilassa 1 kun kellopiiri vetää datalinjan tilaan 0 ACK-pulssia varten (tällöin kulkee tuntemattoman suuruinen virta mikrokontrollerista kellopiiriin).
Oikea tapa hoitaa kommunikaatioita on siksi ohjelmoida I/O-portin lähtö olemaan aina 0, ja vaihtaa lähdön tilan sijasta linjan suuntaa, kun halutaan lähettää 1-bittejä. Kontrollerin ja kellopiirin välisissä kytkennöissä on ulkoiset ylösvetovastukset, jotka huolehtivat siitä, että linjan tila tulkitaan 1:ksi jos molempien piirien portit ovat input-tilassa.
Toinen erikoisuus, jonka kytkentäkaaviota tarkkaan lukeva huomaa, on I2C-väylälle käytettyjen I/O-linjojen käyttäminen myös lisämuistipiirin ylimpinä osoitelinjoina. Tällä menettelyllä säästetään kaksi I/O-linjaa, joilla tuntuu olevan yleisenä taipumuksena loppua viimeistään laitteiden jatkokehityksen yhteydessä. I/O-linjat voidaan tässä tapauksessa käyttää kahteen tarkoitukseen pyörittämällä ne sopivan sekvenssin kautta oikeaan tilaan kun niitä tarvitaan osoitelinjoina: I2C-väylän osoitteellisuus takaa, että kellopiiri ei sotkeennu asiaan, kun ei anneta sen osoitetta, ja tämän kyseisen piirin spesifikaatioiden sallima minimisiirtonopeus (0Hz) antaa mahdollisuuden jättää linjat oikeaan tilaan osoitelinjoina käyttämistä varten.
[versio 1.6, 1996/03/10 20:08:32] [Lähetä palautetta Laurille]