Sulautetun ohjelman toteutus¶
Osaamistavoitteet: Sulautetun ohjelman toteutus SensorTag-laitteessa.
Tässä materiaalissa perehdytään siihen, miten SensorTag:lla saadaan toteutettua ohjelma sen käyttöjärjestelmällä RTOS:lla. Ja vieläpä niin, että voimme laitteessa ajaa eri oheislaitteita yhtaikaa, esimerkiksi lukea sensoridataa, viestiä langattomasti ja päivittää näyttöä. RTOS:lle ohjelmien kirjoittaminen poikkeaa melkoisesti esim. Arduino-koodaamisesta, mutta samalla otamme askeleen kohti modernin (ja ok, astetta tehokkaamman) sulautetun järjestelmän sovelluskehitystä.
Laitteelle tehtävää¶
Kuten aiemmassa materiaalissa vihjailtiin, TI-RTOS:n kantava ajatus on että ohjelman toiminnallisuus toteutetaan erillisinä tehtävinä (
Task
-kirjasto) ja kirjoitetaan funktioina. Ohjelmoijalla on muuten vapaat kädet määritellä miten ohjelman jaetaan tehtäviin ja mitä ne sisäisesti tekevät. Yksinkertainen ohjelma voidaan toki toteuttaa yhdellä ainoalla taskilla, mutta heti monimutkaisempi ohjelma, joka käyttää vaikkapa useita eri oheislaitteita, ei helposti luonnistu yhdellä funktiolla. Tässä onkin suunnittelun paikka. Nyt ei ole mielekästä jokaista pikkuasiaa varten luoda omaa taskia, mutta silti taskin sisäinen toiminto on järkevää toteuttaa funktiona. Esimerkiksi, ympyrän pinta-alan laskemiseksi ei tarvita taskia, ihan vaan C-kielen funktio riittää, jota kutsumme taskista tarvittaessa. Taskin sisältä pitääkin kutsua omia tai API:n funktiota tarpeen mukaan.
Tyypillisesti sulatetut ohjelmat reagoivat syötteeseen esim. oheislaitteelta tuleva signaali, napin painallus, tms, jolloin syötteen odotteluun kuluvaa aikaa voidaan jakaa muille tehtäville. Myös oheislaitteiden kanssa kommunikointi on hidasta verrattuna laskennan suorittamiseen CPU:ssa. Tällöin on hyödyllistä, kun tehtävät ovat omia funktioitaan joita RTOS suorittaa tarpeen mukaan, jakamalla CPU:n resursseja parhaaksi katsomallaan tavalla. Esimerkiksi, SensorTagin sovelluksen yksi tehtävä voi lukea anturidataa vaikka sekunnin välein, toinen tehtävä näyttää uuden datan näytöllä ja kolmas tehtävä lähettää datan langattomasti verkon yli palvelimelle ja odottaa vastausta. Tällainen moniajo oikein toteutettuna nopeuttaa ohjelman suoritusta merkittävästi, koska tehtävien ei tarvitse odotella toisten valmistumista. Ohjelmakoodissa voimme toki ohjeistaa RTOS:ia yrittämään toteuttaa moniajoa haluamallamme tavalla.
Suoritusajan jakaminen voi perustua esimerkiksi tehtäville asetettuihin prioriteetteihin tai niiden saamiin ulkoisiin signaaleihin (ts. keskeytys, muistetaan aiemmin esitelty tietokoneen toiminnan kuvaus!). Tällaisessa ohjelmassa RTOS suorittaa tehtäviä niiden tai keskeytysten prioriteetin mukaisessa järjestyksessä. Toki, nyt RTOS tarvitsisi mekanismejä varmistamaan sovelluksen toimintaa, eli esimerkiksi korkean prioriteetin tehtävä ei saisi tukahduttaa alemman prioriteetin tehtäviä varaamalla kaiken suoritusajan. No, näistä lisää tulevilla kursseilla.
Task-kirjasto¶
Nyt, TI-RTOS mahdollistaa kuvatun moniajoympäristön SensorTag-laitteella tarjoamalla
Task
-kirjaston käyttöömme. Kirjaston avulla taskeja voidaan luoda funktioina ja ohjata niiden toimintaa. Esittelemme
Task
-kirjaston peruskäytön koodiesimerkkien avulla. #include <ti/sysbios/BIOS.h>
#include <ti/sysbios/knl/Task.h>
/* Vakiot ja globaalit muuttujat */
#define STACKSIZE 2048
// Taski tarvitsee oman pinomuistin
Char myTaskStack[STACKSIZE];
// Taskin toteuttava funktio
// prototyyppi on aina tämä: Void taskin_funktio(UArg arg0, UArg arg1)
Void myTaskFxn(UArg arg0, UArg arg1) {
// Tässä taskissa ei paljoa tehdä..
System_printf("Mun argumentit ovat %ld ja %ld.. heippa!\n", arg0, arg1);
System_flush();
}
int main(void) {
// Tietorakenne-muuttujia, joihin taskien tiedot tallennetaan
Task_Params myTaskParams;
Task_Handle myTaskHandle;
// Laitteen alustus
Board_initGeneral();
// Alustetaan taskin suoritusparametrit
Task_Params_init(&myTaskParams);
// Kerrotaan taskille sen pinomuisti
myTaskParams.stackSize = STACKSIZE;
myTaskParams.stack = &myTaskStack;
// Asetetaan taskin prioriteetti
myTaskParams.priority = 2;
// Argumentit taskille vain esimerkin vuoksi
myParams.arg0 = 127; // Argumentti 1
myParams.arg1 = 0xFFFF; // Argumentti 2
// Luodaan taski ja RTOS:lle taskin käsittellyyn kahva
myTaskHandle = Task_create((Task_FuncPtr)myTaskFxn, &myTaskParams, NULL);
if (myTaskHandle == NULL) {
System_abort("Task create failed");
}
// Terveisiä..
System_printf("Hello world!\n");
System_flush();
// Ohjelma käynnistyy
BIOS_start();
return (0);
}
Palastellaanpa tämä esimerkki.
Ensiksi, taskia varten meidän tulee määritellä sen suoritusparametrit (tietorakenne
Task_Params
), joista meidän ohjelmaa kiinnostaa prioriteetti (tästä oma kappale alla) ja argumenttien välittäminen taskille. Parametrit tulee ensin alustaa (Task_Params_init
-kutsu), joka asettaa parametreille oletusarvot, jonka jälkeen voimme asettaa omat halutut arvot halutuille parametrille. Huomataan, että tietorakenteessa määrittelemme jokaiselle taskille sen oman pinomuistin (tässä
myTaskStack
merkkimuotoisena taulukkona. Taskimme tarvitsee pinomuistia oman sisäisen toimintansa toteuttamiseen ja tässä ohjelmoija voi määrittää muistin koon tarpeen mukaan, josta syystä se tehdään taskin ulkopuolella. Nyt esimerkissä oleva 2kB
pinomuisti käy useimmissa tapauksissa. Nyt, SensorTag-laitteessa RAM-muistia on vain 20kB
, joten jokainen taski haukkaa siitä leijonanosan. Taskeille ei siis kannata turhaan ihan vaan varalta varata isoa pinomuistia. Lisäksi asetamme taskille kahvan (eng. handle), jolla taskiin voidaan viitata ohjelmassamme. Kahva siis on vain tunnus, joka yksilöi taskin. Tässä esimerkissä kahvaa (
Task_Handle
) käytetään vain tarkistamaan, että taskin luonti onnistui. Argumentteina taskille voidaan välittää kaksi unsigned int-tyyppistä (
typedef unsigned in UArg
) arvoa rakenteen jäsenissä arg0
ja arg1
. Tässä on RTOS siis käyttänyt typedef:iä omien muuttujatyyppien luomiseen. Noh, käytämme niitä kiltisti kuten RTOS haluaa, tosin argumenttien välittämistä taskeille ei välttämättä kurssilla tarvita, vaan käytämme esimerkiksi globaaleja muuttujia. Taski luodaan
Task_create
-funktiolla, jolle annetaan parametreiksi taskin suoritusfunktio (tyyppiä Task_FuncPtr
) sekä parametrirakenne (Task_Params). Taskin parametrit voidaan myös jättää määrittelemättä, jolloin argumentiksi annetaan NULL ja taski käyttää oletusparametreja. Huomataan myös System_abort
-funktio, jolla ohjelman suoritus voidaan ohjelmallisesti keskeyttää virhetilanteeseen ja tulostaa haluttu virheviesti konsoli-ikkunaan, tässä siis jos taski ei käynnisty, jos vaikka laitteen muisti on lopussa. Kurssin laboratorioharjoituksessa annetaan opiskelijoille ohjelma-aihio (engl. template), joka sisältää tarvittavia taskien toteutuksia ja parametrien määrityksiä valmiina, joten niitä ei tarvitse itse lueskella materiaalin ulkopuolelta.
Sulautetun laitteen ikuinen elämä¶
Nyt kun sulautetun laitteen toiminta perustuu syötteen odottamiseen, odotellessa se pyörii tyhjäkäynnillä (engl. idle) kunnes syöte/heräte saadaan. Ja sitten siihen tietysti reagoidaan ohjelmassa. Esimerkiksi, syöte voi olla start-käynnistyskomento mikroaaltouunin ohjauspaneelista.
Tyypillinen ratkaisu tässä on luoda ikuinen toistorakenne ohjelman
main
-funktioon, joka pitää ohjelmamme käynnissä. Ohjelmassa voidaan sitten joko odotella herätettä saapuvaksi (keskeytys) tai pollata (kysytään tilaa, engl. poll) oheislaitteilta yksi kerrallaan, siis niiltä oheislaitteilta, jotka eivät pysty lähettämään keskeytystä. Keskeytyksistä SensorTag-laitteessa opimme lisää tulevissa kappaleissa. SensorTagissa toistorakennetta main-funktiossa ei RTOS:n kanssa ole hyvä käyttää ikuisen silmukan ajoon, koska main-funktiossa vain alustetaan laitteen toiminta ja käynnistetään taskit. Joten .. simsalabim siirretään toistorakenne taskiin! Nyt joku tehtävistämme pysyy aina hengissä ja RTOS ja laite pyörii taustalla sitä kautta.
#include <ti/sysbios/knl/Clock.h>
...
// Taskifunktio
Void myTaskFxn(UArg arg0, UArg arg1) {
// Tämä taski ei lopu
while (1) {
if (onko_nappia_painettu()) {
tee_jotain();
}
// Vapautetaan MCU muille taskeille joksikin aikaa!
// n:n arvon voimme määritellä itse
Task_sleep( n / Clock_tickPeriod);
}
}
Nyt
myTaskFxn
käynnistetään aiemmin main-funktiossa, mutta sen suoritus ei lopu ennenkuin laitteesta katkaistaan virrat, johtuen aina totta olevasta while-toistorakenteesta. Lisäksi, olemme kohteliaita ja keskeytämme taskin suorituksen hetkeksi
Task_sleep
-kutsulla, joka vapauttaa mikrokontrollerin hetkeksi muihin hommiin. Sleep-funktion avulla siis jaetaan MCU:n prosessoriaikaa taskien kesken ja se pitäisi löytyä jokaisen taskin lopusta. RTOS sitten huolehtii taskimme seuraavasta suorituksesta ajallaan. Tähän sääntöön on yksi poikkeus!RTOS tarjoaa
Clock.h
-kirjastossa muuttujan Clock_tickPeriod
, joka kertoo meille kuinka monta mikrosekuntia yhteen tikitykseen (kellojaksoon, engl. tick) menee. Oletuksena tämä arvo on 10, eli yksi tikitys on 10 mikrosekuntia. Nyt siis kellojaksojen määrä saadaan kaavalla n / Clock_tickPeriod
, jossa n on haluttu aikaviive mikrosekunteina.Itseasiassa, hardisnikkareille tiedoksi, tässä RTOS:n kellojakso on ohjelmallinen vakio, eikä vastaa 1:1 laitteen kelloa.
Taskien priorisointi¶
RTOS antaa mahdollisuuden määritellä prioriteetti jokaiselle ohjelmamme taskille. Prioriteetti otetaan huomioon prosessoriaikaa jakaessa eli nyt (toteutuksesta riippuen) korkeamman prioriteetin taskit menevät edelle alemman prioriteetin taskeja. Prioriteetti voidaan määritellä laskevassa tai nousevassa suhteessa, ja SensorTagissa korkeampi lukuarvo antaa korkeamman prioriteetin.
taskParams.priority=2;
Prioriteetti, tietorakenteen jäsenessä
priority
, määritellään suhteessa toisiin taskeihin. Jos käytössä on useita taskeja, voimme prioriteeteillä antaa enemmän suoritusaikaa raskaammille taskeille. Kurssilla prioriteettitasoja ei tarvitse säätää, taso 1 käy kaikkeen. Vain jos teette monimutkaisempia ohjelmia, joissa on paljon I/O-liikennettä eri komponenttien välillä tms, niin useampia tasoja (1 ja 2) voi tarvittaessa käyttää. Huomataan, että ohjelman jossa on vain yksi taski, suoritus ei nopeudu prioriteettiä nostamalla, koska ei ole muita taskeja joiden kanssa suoritusaikaa jakaa..Alla koodiesimerkki miten prioriteetit on kurssin ohjelma-aihiossa määritelty valmiiksi main-funktiossa kahdelle taskille
taskFxn
ja commFxn
. Nyt siis kommunikaatiotaski ajetaan pienemmällä prioriteetillä, mutta asia ei ole ihan näin yksinkertainen.. // My own task
Void taskFxn(UArg arg0, UArg arg1) {
...
while (1) {
// DO SOMETHING HERE
// Task to sleep
Task_sleep(1000000 / Clock_tickPeriod);
}
}
// SensorTag communication task
Void commFxn(UArg arg0, UArg arg1) {
...
while (1) {
// THIS WHILE LOOP DOES NOT USE Task_sleep
}
}
Int main(void) {
...
/* My own task */
Task_Params_init(&taskParams);
taskParams.stackSize = STACKSIZE;
taskParams.stack = &taskStack;
taskParams.priority=2;
...
/* SensorTag communication task */
Task_Params_init(&taskCommParams);
taskCommParams.stackSize = STACKSIZE;
taskCommParams.stack = &taskCommStack;
// DO NOT CHANGE THIS PRIORITY
taskCommParams.priority=1;
...
}
Ensin main-funktiossa määritellään ns. ohjelmoijan omalle taskille
taskFxn, prioriteetti 2
ja sitten langattoman tiedonsiirron taskille commFxn, prioriteetti 1
. Nyt siis oma taskimme pyörii isommalla prioriteetillä, johon on ihan tietty syy joka selviää, kun katsotaan taskien toteutus-funktioita. Molemmissa taskeissa on siis ikuinen silmukka, mutta oman taskimme funktiossa taskFxn
while-loopissa taskin suoritus keskeytetään sleep-kutsulla. Sitten taas tiedonsiirtotaskin commFxn
ikuisessa silmukassa ei ole sleep-kutsua! Nyt, ylläkerrotun mukaisesti commTask siis varastaisi kaiken suoritusajan eivätkä muut taskit pääsisi suorittamaan tehtäviään! Mutta, koska annamme omalle taskille on isomman prioriteetin, RTOS osaa jakaa sillekin suoritusaikaa prioriteettijärjestyksessä. Ja, jotta oma taski ei taas veisi kaikkea suoritusaikaa isomalla prioriteetillä, täytyy se keskeyttää välillä..
Tästä seuraakin yleissääntö kurssille:
- Omille taskeille aina prioriteetti 2 ja ne tulee aina keskeyttää sleep-kutsulla.
- Tiedonsiirtotaskin prioriteettiin 1 ei ole syytä koskea, eikä funktio tarvitse sleep-kutsua.
(Tämä on se ainoa yllämainittu poikkeus, jossa emme käytä
Task_sleep
-funktiota. Langattoman radion toiminnan vuoksi jouduttiin aihioon toteuttamaan tämä ratkaisu. Jos lähdette näitä muokkaamaan, niin todennäköisesti ohjelmassa ei eri toiminnallisuudet eivät pelaa rinnakkain!)Ilmainen vinkki 1: Jos laite menee ohjelmassa tukkoon, syynä on useimmiten liian raskas tai pitkäkestoinen taski tai unohtunut
Task_sleep
-kutsu taskin toteutusfunktiossa. Liian raskas taski taas on syytä suunnitella uusiksi, jakamalla se pienempiin osiin. Ilmainen vinkki 2: Toteutusfunktion sisälle ei kannata tehdä useita peräkkäisiä sleep-kutsuja, koska tämä ratkaisu menee usein pieleen siksi että kun lisäätte ohjelmaan toiminnallisuuta aiemmin mietityt ajoitukset menevät pieleen. Toteutusfunktioon aina vain yksi sleep-kutsu ja toiminnalisuudet ehtorakenteen ja tilakoneen taakse (kts. tuleva materiaali).
Lopuksi¶
Arduino-ohjelmoinnissa ohjelmassa on tyypillisesti yksi ikuinen
while
-looppi, jonka sisällä toteutetaan kaikki ohjelman toiminallisuus. Haasteena on sitten mm. ajoituksista huolehtiminen itse. Noh, tämä on ok ratkaisu sille laitealustalle, mutta SensorTag on laitteena huomattavasti monimutkaisempi, kaksi mikrokontrolleria ja niiden alijärjestelmät ym. Joten siinä on syytä hyödyntää RTOS:n moniajomahdollisuuksia.. Ilmainen vinkki 3: SensorTagin ohjelmaa ei todella kannata toteuttaa Arduino-tavalla, teette itselleen ison karhunpalveluksen. Eikä se tapa ei ole loppupeleissä yhtään sen helpompi, ohjelmaan tulee taatusti pientä ja isompaa ongelmaa..
Anna palautetta
Kommentteja materiaalista?