SensorTag ohjelman toteutus¶
Osaamistavoitteet: Sulautetun ohjelman toteutus SensorTag-laitteessa.
Tässä materiaalissa perehdytään siihen, miten SensorTag:lle laaditaan sulautettu ohjelma tehtäväpohjaisesti niin, että useita tehtäviä voidaan ajaa laitteen RTOS-käyttöjärjestelmällä rinnakkain.
Kuten aiemmassa materiaalissa vihjailtiin, TI-RTOS:n kantava ajatus on, että ohjelman toiminnallisuus toteutetaan erillisinä tehtävinä joita ajetaan laitteen suorituskyvyn mukaan rinnakkain. Ohjelmoijalla on muuten vapaat kädet määritellä miten ohjelman jaetaan tehtäviin ja mitä tehtävät 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ä taskilla. Tällainen moniajo oikein toteutettuna yleisesti nopeuttaa ohjelman suoritusta merkittävästi, koska tehtävien ei tarvitse odotella toistensa valmistumista.
Tässä onkin ohjelmoijalla suunnittelun paikka. Ei tietenkään ole mielekästä jokaista pikkuasiaa varten luoda omaa taskia, koska se kuluttaa RTOS:lle varattuja resursseja. Taskin sisäiset toiminnot on nekin järkevää toteuttaa funktiona. Esimerkiksi, ympyrän pinta-alan laskemiseksi ei tarvita taskia, ihan vaan C-kielen funktio riittää, jota kutsumme taskista tarvittaessa.
Jotta taskien rinnakkainen suoritus onnistuisi yhdellä mikrokontrollerilla, on sen reaalista suoritusaikaa jaettava tehtävien (ja RTOS:n) kesken. Suoritusajan jakaminen voi perustua esimerkiksi aikaperusteiseen jakoon, prioriteettien mukaiseen jakoon tai syötteiden / erilaisten signaalien reaktiiviseen käsittelyyn kun ne saapuvat laitteelle. Suoritusajan jaon yhteydessä on myös huolehdittava, ettei suoritukseen päässyt tehtävä rohmua kaikkea suoritusaikaa, jottei laitteen muu toiminta pysähdy pitkäksi aikaa. Tällainen tilanne voisi tulla esimerkiksi kun korkean prioriteetin tehtävä tukahduttaa alemman prioriteetin tehtäviä viemällä niiltä kaiken suoritusajan. Toinen vastaava tilanne voisi tulla eteen kun, tehtävä kommunikoi (suhteellisen) hitaan oheislaitteen kanssa ja itsekkäästi pitää suoritinta odotusaikana hallussaan.
Homma Sensortagissa¶
Tarkastellaanpa esimerkkikuvan avulla, miten eri tehtävät ((engl. Task) saadaan toimimaan rinnakkain SensorTagissa. Esimerkissä on toteutettu ohjelmaan:
- Kaksi ohjelmaan liittyvää taskia (engl. Task) eli Taskit 1 ja 2.
- Yksi valmis taski: Viestintätaski, joka oletetaan kirjaston mukana ohjelmaan tulevaksi taskiksi.
- Yksi keskeytyksen käsittelijä, joka toteuttaa napinpainallusten käsittelyn ohjelmassa.
Kuvassa taskeilla on siis jokin suoritusaika ja prioriteetti. Lisäksi huomataan, että joidenkin taskien suorituksen loputtua ne pistetään nukkumaan
sleep
-funktiokutsulla. Huomataan, että ohjelmataskit pyörivät korkeammalla prioriteetillä (2) kuin kirjastotaski (1) ja että keskeytysten prioriteetti on kaikkein korkein ("ääretön"). Katsotaanpa seuraavaksi mitä ohjelmassa tapahtuu:
- Ohjelman suoritus lähtee liikkeelle Taskista 1, joka suoritetaan loppuun ja asetetaan nukkumaan.
- Sen jälkeen suoritin vapautuu ja RTOS antaa suorituksen Taskille 2, joka sekin pistetään nukkumaan.
- Tämän jälkeen RTOS:lla on taas valta päättää mitä laitteessa tapahtuu, ja koska ohjelmassa ei ole sillä hetkellä hereillä olevia prioriteetin 2 taskeja, suorittaa RTOS tiedossa prioriteetin 1 taskin (viestintätaski).
- Kun viestintätaskin suoritus loppuu, RTOS huomaa taas hereillä olevan prioriteetin 2 Taski 1:sen ja suorittaa sen.
- Taski 1sen suorituksen aikana mikrokontrollerille tulee keskeytyssignaali, ja koska keskeytyksen prioriteetti on ääretön, siirrytään sen käsittelijä suorittamaan kesken Taski 1sen suorituksen!
- Kun keskeytyksen käsittelijäfunktio on suoritettu, RTOS jatkaa Taski 1sen suorittamista siitä mihin se jäi.
- Ja näin ohjelman suoritus jatkuu kolmen taskin ja yhden keskeytyskäsittelijän välillä hamaan ikuisuuteen..
Hetkinen, mitä keskeytyksessä tapahtuu? Nyt joudutaan menemään hieman asioiden edelle, eli keskeytys on mikrokontrollerille tuleva hardispohjainen-signaali, kun taas muuten taskien ajo on ohjelmiston (RTOS) toimintaa. Nyt (tässä) prioriteetti ääretön tarkoitti sitä, ettei RTOS pysty sanomaan mitään keskeytysten suorittamisesta vaan niiden käsittely hanskataan mikrokontrollerissa. (Tämä ei ollut nyt täydellinen selvitys keskeytyksien käytöstä, mutta palaamme asiaan myöhemmässä materiaalissa.)
Ok, yksi juttu vielä.. miksi viestintätaskin perässä ei ole
sleep
-kutsua? Tässä on se selitys, että SensorTagin langattoman radion toiminta (tai se miten se on meillä toteutettu) edellyttää, että taski on kokoajan hengissä eikä nukkuma-tilassa. Tästä syystä viestintätaskin prioriteetti on 1, kun se ylemmillä taskeilla on korkeampi, prioriteetti 2 jotta ne saisivat suoritusaikaa. Lisäksi meidän tulee tietää vielä yksi asia taskin suorittamisesta. Tästä esityksestä sai kuvan, että jokainen taski ajetaan kerran alusta loppuun liipaisemalla se aina käyntiin, mutta näin ei asia kurssilla ole. Itseasiassa käytetään taskien sisäistä superlooppia eli
while
-silmukkaa, jonka sisällä sleep
-funktiokutsu tehdään! Näin meillä ohjelmassa pysyy kerran luodut taskit käynnissä ja ne myös pitävät ohjelman suorituksen päällä kokoajan. Tämä siis vaatii sen että superloopin jokaisen iteraation lopussa kutsutaan sleep
-kutsua. Taskissa siis käynnistetään ajastin, jonka aikaa se on nukkumassa ja tällöin muut taskit pääsevät väliin ja saavat suoritusaikaa. Sopiva nukkuma-aika on sitten sovelusriippuvainen ja ohjelmoijan määritettävissä.Ok ok, mutta mitä tapahtuisi jos taskien sisällä ei olisi superlooppia? Koska taski on C-kielen funktio, se vain suoritettaisiin kerran ja näin koko sulautetun ohjelman suoritus loppuu, kun kaikki sen taskit olisi kerran suoritettu. Superloop taskeissa on siis oleellinen ratkaisu sille, että ohjelma pysyy laitteessa hengissä.
RTOS:n Task-kirjasto¶
TI-RTOS mahdollistaa kuvatunlaisen ohjelman toteutuksen ja moniajoympäristön SensorTag-laitteelle tarjoamallaan
Task
-kirjastolla. Kirjaston avulla taskeja voidaan luoda funktioina ja RTOS:n kautta ohjata niiden toimintaa. Katsotaan koodiesimerkkiä
Task
-kirjaston toiminnasta. #include <ti/sysbios/BIOS.h>
#include <ti/sysbios/knl/Task.h>
// Taski tarvitsee oman sisäisen pinomuistin
#define STACKSIZE 512
Char myTaskStack[STACKSIZE];
// Taskin toteutusfunktio
Void myTaskFxn(UArg arg0, UArg arg1) {
// Taskin ikuinen elämä
while (1) {
System_printf("Mun argumentit ovat %ld ja %ld\n", arg0, arg1);
System_flush();
// Kohteliaasti nukkumaan hetkeksi
Task_sleep(1000000L / Clock_tickPeriod);
}
}
int main(void) {
// Taskeihin liittyviä tietorakenteita
Task_Params myTaskParams;
Task_Handle myTaskHandle;
// Laitteen alustus
Board_initGeneral();
// Alustetaan taskin suoritusparametrit
Task_Params_init(&myTaskParams);
// Osoitetaan 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
myTaskHandle = Task_create((Task_FuncPtr)myTaskFxn, &myTaskParams, NULL);
if (myTaskHandle == NULL) {
System_abort("Task create failed");
}
// Terveisiä konsolille
System_printf("Hello world!\n");
System_flush();
// Ohjelma käynnistyy
BIOS_start();
return (0);
}
Palastellaanpa tämä esimerkki.
Taskin toteutusfunktiossa
myTaskFxn
meillä onkin äsken mainittu ikuinen silmukka ja sen lopussa kiltisti Task_sleep
-funktio, joka toteuttaa nukkumistoiminnon ja antaa vallan takaisin RTOS:lle jokaisen iteraation jälkeen. Sitten
main
-funktiossa uutena asiana taskin suoritusparametrien asetus tietorakenteeseen myTaskParams
joka on tyyppiä Task_Params
). Esimerkissä asetetaan taskille kolme asiaa, jotka on aina oltava erikseen jokaiselle taskille:- Kutsutaan
Task_params_init
-funktiota, joka asettaa kaikki parametrit oletusarvoon. - Asetetaan taskin pinomuisti (sovitaan tässä vaiheessa että pino on taskin oma sisäinen muistialue, josta meidän ei tarvitse välittää) jäseniin
.stack
ja.stackSize
. Jokaiselle taskille on oltava oma pinomuisti, eli samaa muistialuetta ei saa jakaa useille taskeille. - Pinomuistin koko on tässä 512 tavua, joka riittää useimmille taskeille. Poikkeukset on selitetty kurssimateriaalissa.
- Asetetaan taskin prioriteetti jäseneen
.priority
. - Ohjelman taskeille aina prioriteetti 2
- Viestintätaskille aina prioriteetti 1
- Jos tuntuu, että prioriteetteihin pitäisi koskea harjoitustyössä, kysykää ensin asiaa opettajalta.
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 int 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
) (tässä myTaskFxn
sekä parametrit myTask_Params
. 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. Kello käy¶
RTOS tarjoaa
Clock.h
-kirjastossa muuttujan Clock_tickPeriod
, joka kertoo meille kuinka monta mikrosekuntia yhteen systeemin kellojaksoon (engl. tick) menee. Oletuksena arvo on 10, eli yksi tick on 10 mikrosekuntia. Nyt siis kellojaksojen määrä saadaan kaavalla n / Clock_tickPeriod
, jossa n on haluttu aika mikrosekunteina.Itseasiassa, hardisnikkareille tiedoksi, tässä RTOS:n kellojakso on ohjelmallinen vakio, eikä vastaa 1:1 laitteen kellokomponenttia.
Lopuksi¶
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 (vapaaehtoista lisämateriaalia tarjotaan kyllä luettavaksi).
Ilmainen vinkki 1: Jos laite menee ohjelmassa tukkoon, syynä on useimmiten liian pitkäkestoinen taski tai unohtunut
Task_sleep
-kutsu toteutusfunktiossa. Tällainen taski taas on syytä suunnitella uusiksi, jakaa 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 silloin kun lisäätte ohjelmaan toiminnallisuuta ja nämä aiemmin säädetyt ajoitukset menevät pieleen. Toteutusfunktioon aina vain yksi
sleep
-kutsu ja toiminnalisuudet ehtorakenteen ja tilakoneen taakse (kts. tuleva materiaali). Ilmainen vinkki 3: Joskus näkee ratkaisuja, joissa on viritetty useita eri prioriteettitasoja. Tästä on todennäköisesti seurauksena laitteen toiminnan sekoaminen, jos viritettyyn ohjelmaan lisätäänkin jälkeenpäin toiminnallisuutta. Mieluummin pysytään prioriteettitasolla 2 ja mietitiin taskien toiminta uusiksi. Assistenteilta voi aina kysyä.
Ilmainen vinkki 4: SensorTagin ohjelmaa ei todella kannata toteuttaa Arduino-tavalla yhdellä taskilla ja superloopilla, teette itsellenne ison karhunpalveluksen kun ohjelman logiikassa toiminnallisuuksien välillä ja ajoituksissa mitä todennäköisimmin ilmenee ongelmia.
Anna palautetta
Kommentteja materiaalista?