Sulautettu reaaliaikakäyttöjärjestelmä¶
Aloitamme sulauttujen laitteiden ohjelmointiin perehtymisen käymällä ensin läpi ohjelman rakenteen SensorTag-laitteessa. Valmistaja tarjoaa käyttöjärjestelmän (TI-RTOS), jonka kirjastoja ja palveluita käytämme kurssilla harjoitustyön toteuttamiseen. Materiaalissa esitetään ohjelmointiesimerkkejä, mutta parhaiten asioita oppii tietysti itse tekemällä.
Käyttöjärjestelmä on tietokoneen toimintaa pyörittävä ohjelmisto. Se tarjoaa suoritusympäristön sovelluksille ja hallinnoi tietokoneen resursseja sekä käyttöoikeuksia. Käyttöjärjestelmä tarjoaa yhtenäisen rajapinnan ja kirjastot kaikille sovelluksille mm. muistihallintaan, tiedostojärjestelmään, oheislaitteiden käyttöön ja ulkoiseen tiedonsiirtoon. Käyttöjärjestelmä myös kontrolloi sovellusten suoritusta tietokoneessa, jakaen prosessorin suoritusaikaa ja muistia sovellusten kesken. (Yleisesti käyttöjärjestelmiä käsitellään myöhemmin Käyttöjärjestelmät-kurssilla, joten ohitamme tässä osiossa asioiden kattavan esityksen.)
Reaaliaikakäyttöjärjestelmä (Real-time Operating System, RTOS) taas tarjoaa suoritusympäristön sulautettujen järjestelmien sovelluksille, joille on erityisen kovat vasteaikavaatimukset, kuten autoissa tai robotit tehdasympäristössä. Kuten aiemmin esitettiin, sulautettuja järjestelmiä on toki mahdollista ohjelmoida ilman käyttöjärjestelmää, mutta tällöin ohjelmoija on vastuussa laitteen komponenttien kytkemisestä MCU:hun ja tarvittavien palveluiden toteuttamisesta laitteelle. Reaaliaikakäyttöjärjestelmään on usein toteutettu valmista toiminnallisuutta optimoida kallisarvoisten laiteresurssien käyttöä. Tämä seikka vaikuttaa merkittävästi sulautetun ohjelman suunnitteluun, toteutukseen ja ylläpitoon
Sulautetut ohjelmat on myös usein helpompi jakaa modulaarisesti erillisiin tehtäviin, vaikkapa sensoridatan lukemiseen ja langattomaan tiedonsiirtoon, ja antaa käyttöjärjestelmän huolehtia niiden rinnakkaisesta toiminnasta. Esimerkiksi, ohjelman eri tehtävien prioriteettien mukainen suoritus olisi haastavaa toteuttaa itse!
RTOS:n esittely¶
Kuvassa lohkokaavio TI RTOS:n ja laiteajurien tarjoamista eri toiminnallisuuksista. RTOS tarjoaa rajapinnan ja piilottaa (abstrahoi) ohjelmoijalta alemman tason toteutuksen yksityiskohdat, kuten laite-ja komponenttiajurit, reaaliaika-ominaisuudet ja tiedonsiirron. Meitä tällä kurssilla kiinnostaa sovellustaso, jossa käytämme erilaisia kirjastoja rajapintojen kautta (engl. application programming interface, API). Kurssilla pysymme kuvassa punaisen alueen sisällä, jota syvemmälle laitteen toiminnassa ei mennä.
Mainitsemme joitakin näistä kirjastoista jo nyt, mutta niiden käyttöön ohjelmissa perehdymme yksityiskohtaisesti myöhemmin luentomateriaalissa.
(Kiinnostuneille: Osa RTOS:n lähdekoodista tulee CCS:n asennuspaketin mukana. Niistä voi löytyä paljon muutakin mielenkiintoista, johon ei tässä materiaalissa perehdytä).
Palvelut ja kirjastot¶
Seuraavia kirjastoja tulemme käyttämään ohjelmien toteuttamisessa.
- Pin Kirjasto mikrokontrollerin I/O-pinnien hallintaan. Sensortagin painonappeja ja ledejä ohjataan tätä kautta. Myös GPIO-kirjastolla voidaan ohjata I/O-pinnejä.
- driverlib Laiterekisterit ja bittimaskit.
- I2C Kirjasto i2c-tiedonsiirtoväylän käyttämiseen. SensorTagin antureihin päästään käsiksi tällä kirjastolla.
- UART Sarjaliikenneprotokolla oheislaitteiden kanssa kommunikointiin. Esimerkiksi PC:lle voidaan lähettää dataa tai laitteelle lähettää komentoja PC:ltä tällä kirjastolla.
- (PWM Kirjasto Pulssinleveysmodulaation (PWM) tuottamiseen SensorTagin I/O-pinneillä. PWM:llä voidaan digitaalipinnistä syöttää analogista jännitettä tietyllä taajuudella vaikkapa ledin kirkkauden säätämiseen.)
- Display Kirjasto tekstin ja grafiikan tuottamiseen LCD-näytölle.
- wireless_lib Kurssin henkilökunnan toteuttama kirjasto langattomaan tiedonsiirtoon 6LoWPAN-teknologialla.
(Kiinnostuneille: Tarkempaa tietoa kirjastosta löytyy TI-RTOS Kernel User Guide:sta ja TI RTOS User's Guide:sta.)
Ohjelman toiminta¶
SensorTag:lle RTOS:ia käyttävät C-kieliset ohjelmat kirjoitetaan lähdekoodi-moduleina ihan samoja periaatteita noudattaen kuin mitkä tahansa C-kieliset ohjelmat. Jokaisella ohjelmalla on yksi main-funktio, josta ohjelman suoritus lähtee. (Ok, RTOS tekee ensin omiaan piilossa taustalla ennen main-funktion suoritusta).
Alla esimerkki
main
-funktiosta:// SensorTagin kirjastot mukaan
#include <xdc/runtime/System.h>
#include <ti/sysbios/BIOS.h>
#include <ti/drivers/I2C.h>
int main(void) {
// Alustetaan laite
Board_initGeneral();
Board_initI2C();
// Mahdolliset I/O-määritykset,
// joilla otetaan oheislaitteita käyttöön
...
// Alustetaan sovelluksen eri tehtävät
...
// Kerrotaan ohjelmoijalle, että alustus onnistui
System_printf("Hello world!\n");
System_flush();
// Käynnistetään RTOS, jolloin tehtävät lähtevät
// suoritukseen
BIOS_start();
return (0);
}
Pureudutaanpas main-funktion toimintaan hiukan. Kuten huomataan, alussa ensin alustetaan itse laite
Board_initGeneral
-funktiolla. Tämä tarkoittaa sitä, että RTOS:lle ja ohjelmallemme kerrotaan, että se ajaa nyt itseään SensorTag-laitteessa ja osaa ottaa huomioon laitteen resurssit, fyysiset kytkennät ja laitekohtaiset I/O-määrittelyt.Lisäksi käytettävät ajurikirjastot tulee alustaa. Tässä tapauksessa haluamme käyttää I2C-kirjastoa kommunikointiin oheislaitteen kanssa, joten se alustetaan. Näiden alustusfunktioiden sisäisestä maailmasta meidän ei tarvitse tällä kurssilla huolehtia. Tarvittavat kirjastot tulee tietenkin esitellä ohjelmaamme esikääntäjän
#include
-lauseella. Sen, mitä ja missä olevia kirjastoja meidän tulee koodissa käyttää, selviää yleensä laitteen ja ohjelmointiympäristön manuaaleista.Ajurien alustamisen jälkeen alustetaan I/O, eli käytettävät I/O-pinnit ja -portit, kuten painonapit tai ledit, ja muut I/O-liitännät oheislaitteisiin. Näistä lisää myöhemmin.
Seuraavaksi alustetaan varsinaisen ohjelman toiminta. RTOS:n kantava ajatus on, että ohjelma jaetaan modulaarisesti tehtäviin (eng. task) joita suoritetaan rinnakkain (no, oikeasti peräkkäin yksi kerrallaan käyttöjärjestelmän päättämässä järjestyksessä). Meidän tulee suunnitella ohjelmamme toiminta tältä pohjalta mahdollisuuksien mukaan tehtäviin. Jokainen tehtävä on itseasiassa funktio ja niihin pätee (varauksin) samat säännöt kuin funktioiden käyttöön yleensäkin. Ohjelmoijalla on vapaat kädet määritellä taskit ja niiden sisäinen toiminta. Yksinkertainen ohjelma voidaan toteuttaa yhdellä ainoalla taskilla. Tietenkään ei ole mielekästä jokaista pikkuasiaa varten luoda omaa taskia. Tässä taski poikkeaa funktion käsitteestä, jossa vaikkapa usein yleisesti käytettävä toiminnallisuus on järkevää toteuttaa funktiona. Esimerkiksi, ympyrän pinta-alan laskemiseksi ei tarvita taskia, ihan vaan funktio riittää. Taskin sisältä voimme (ja pitääkin) kutsua omia tai API:n funktiota tarpeen mukaan.
Sitten olisi tilaa tehdä mahdollisia muita alustuksia ohjelman toiminnan ohjaamiseen. Esimerkiksi, jos käytämme ajastimia ohjaamaan taskien suoritusta, ne voidaan alustaa tässä.
Tämän jälkeen on hyvä lähettää terveisiä kehitysympäristön konsoli-ikkunalle, jotta näemme, että laitteen alustus on onnistunut. Konsoli-ikkunaan voidaan kirjoittaa
System_printf
-funktiolla, joka toimii (pienin poikkeuksin) samoin kuin C:n standardikirjaston printf-funktio. Itseasiassa System_printf kerää tulostukset puskuriin, joka tyhjennetään vasta System_flush
-kutsulla, joten käytämme sitä jokaisen tulostuksen yhteydessä. Syy tähän käyttäytymiseen on se, että itseasiassa kirjoitus konsolille on laitteemme kannalta erittäin hidas operaatio, joten näin ohjelmoija voi valita sopivan paikan ohjelmasta aikaavievää tulostusta varten!Viimeiseksi funktiossa käynnistetään taskien suoritus kutsulla
BIOS_start
. Kun kaikki taskit on suoritettu, palataan main-funktioon ja suoritetaan se loppuun. Tietenkin nyt sulautettujen tapauksessa tällä arvolla ei ole juuri merkitystä, koska olemme jo lopettaneet ohjelmamme suorituksen ja sen palautus ei enää kiinnosta. (Noo.. RTOS pystyy kyllä vielä reagoimaan tähän palautusarvoon, mutta jätetään tämä asia jatkokursseille).Yllä on nyt kuvattu main-funktion perusrakenne mille tahansa tälle laitteelle kirjoitettavalle C-kieliselle ohjelmalle.
Tehtävää riittää¶
Sulautettu ohjelmamme koostuu joukosta ohjelmoijan määrittämiä tehtäviä (engl. task) sekä taustalla pyörivistä RTOS:n tehtäviä. RTOS tarjoaa
Task
-kirjaston käyttöömme, jolla taskeja voidaan luoda ja ohjata. RTOS jakaa mikrokontrollerin vapaata suoritusaikaa tehtävien kesken, eli mahdollistaa moniajon SensorTag-laitteella. Tyypillisesti, sulatetut ohjelmat reagoivat syötteeseen esim. oheislaitteelta tuleva dataa, 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 edullista käyttää kommunikoinnin viiveaikaa jonkin toisen tehtävän suorittamiseen rinnakkain jakamalla CPU:n suoritusaikaa.
Esimerkiksi, sovelluksen yksi tehtävä voi lukea anturidataa tietyin väliajoin, toinen tehtävä näyttää datan laitteen näytöllä ja kolmas tehtävä lähettää datan langattomasti verkon yli palvelimelle. Näillä tehtävillä on ihan eri viiveajat, joten on edullista lomittaa niitä. Eikä tehdä toteutusta niin, että ne odottelisivat toistensa valmistumista. Moniajo oikein toteutettuna nopeuttaa ohjelman suoritusta merkittävästi, jossa meitä auttaa RTOS.
Jakaminen voi perustua esimerkiksi tehtäville asetettuihin prioriteetteihin tai niiden saamiin ulkoisiin signaaleihin (ts. keskeytys, muistetaan aiemmin esitelty tietokoneen toiminnan kuvaus!). RTOS suorittaa taskeja niiden tai keskeytysten prioriteetin mukaisessa järjestykessä. Tässä alhaisen prioriteetin taskit voivat tukehtua, joskun niitä ei suoriteta ollenkaan. Kurssilla ei ole syytä oma-alotteisesti säätää taskien prioriteettejä.
Task-kirjasto¶
Esittelemme
Task
-kirjaston peruskäytön koodiesimerkkien avulla. // Taskin toteuttava funktio
Void myTask(UArg arg0, UArg arg1) {
// Tässä taskissa ei paljoa tehdä..
System_printf("Argumentit ovat %ld ja %ld\n", arg0, arg1);
System_flush();
}
int main(void) {
// Tietorakenteita, joihin taskin tiedot tallennetaan
Task_Params myParams;
Task_Handle myHandle;
// Laitteen alustus
Board_initGeneral();
// Alustetaan taskin suoritusparametrit
Task_Params_init(&myParams);
myParams.priority = 1;
myParams.arg0 = 127; // Argumentti 1
myParams.arg1 = 0xFFFF; // Argumentti 2
// Luodaan taski ja RTOS:lle taskin käsittellyyn kahva
myHandle = Task_create((Task_FuncPtr)myTask, &myParams, NULL);
if (myHandle == NULL) {
System_abort("Task create failed");
}
// Ohjelma käynnistyy
BIOS_start();
return (0);
}
Palastellaanpa tämä esimerkki.
Ensiksi, taskia varten meidän tulee määritellä sen suoritusparametrit (rakenne
Task_Params
), joista meidän ohjelmaa kiinnostaa prioriteetti 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. Lisäksi asetamme taskille kahvan (eng. handle), jolla taskiin voidaan viitata ohjelmassamme. Tässä esimerkissä kahvaa (Task_Handle
) käytetään vain tarkistamaan, että taskin luonti onnistui. (Huomatkaa System_abort
-funktio, jolla ohjelman suoritus voidaan ohjelmallisesti keskeyttää virhetilanteeseen ja tulostaa haluttu virheviesti konsoli-ikkunaan.)Taskin prioriteetti, jäsenessä
Huomataan, että ohjelman jossa on vain yksi taski, suoritus ei nopeudu prioriteettiä nostamalla.
priority
, määritellään suhteessa toisiin taskeihin. Jos käytössä on useita taskeja, voimme prioriteeteillä antaa enemmän suoritusaikaa raskaammille taskeille. Meidän 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.
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. 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.Sulautetun laitteen ikuinen elämä¶
Usein sulautetun laitteen toiminta perustuu syötteen odottamiseen käyttäjältä, jonka ohjelmisto pyörii tyhjäkäynnillä ("idlenä") kunnes syöte/heräte saadaan, ja sitten siihen reagoidaan ohjelmassa. Esimerkiksi syöte voi olla käynnistyskomento mikroaaltouunin ohjauspaneelista.
Tyypillinen ratkaisu tähän 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. int main(void) {
// Ikuinen looppi: jaksaa jaksaa
while (1) {
if (onko_nappia_painettu()) {
tee_jotain();
}
}
return 0;
}
Tämä ratkaisu ei kuitenkaan RTOS:n tapauksessa ole hyvä, koska main-funktiossa vain alustetaan laitteen toiminta. Joten siirretään toistorakenne taskiin!
...
Void myTask(UArg arg0, UArg arg1) {
// Tämä taski ei lopu
while (1) {
if (onko_nappia_painettu()) {
tee_jotain();
}
Task_sleep( n / Clock_tickPeriod); // Vapautetaan MCU muille taskeille joksikin aikaa!
}
}
Nyt
myTask
käynnistetään 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. Tällä funktiolla siis jaetaan MCU:n prosessoriaikaa taskien kesken ja se pitäisi löytyä jokaisen taskin lopusta. RTOS sitten huolehtii taskimme seuraavasta suorituksesta ajallaan. RTOS tarjoaa 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.(Tässä kellojakso on RTOS määrittelemä ohjelmallinen vakio, eikä vastaa 1:1 laitteen kellokomponentin tikitystä.)
Lopuksi¶
Näin olemme tutustuneet Task-kirjastoon ja ohjelmien suorittamisen perusteisiin RTOS:ssa. Opettelemme RTOS:n käyttöä kurssin laboratorioharjoituksessa.
Anna palautetta
Kommentteja materiaalista?