Janez Puhan
posodobljeno 9.11.2011

Primer uporabe preprostega operacijskega sistema

Izvorna koda je napisana za Philipsov mikrokrmilnik LPC2138, ki temelji na centralnem procesnem jedru ARM7TDMI-S. Prikazuje primer regulacije svetlosti žarnice. Svetlost žarnice se povečuje s povečevanjem kota odprtja, in zmanjšuje z zmanjševanjem kota odprtja triaka v pomožnem vezju (slika 1). Mikrokrmilnik ves čas pregleduje stanje treh tipk T0, T1 in T3, ki se nahajajo na osnovni plošči učnega razvojnega sistema Šarm. Ob pritisku na tipko T0 se prične svetlost zmanjševati, ob pritisku na T1 pa povečevati. Zmanjševanje, oziroma povečevanje traja toliko časa, dokler ni pritisnjena tipka T3, ki pomeni zaustavitev trenutnega ukaza. Tako mikrokrmilnik skrbi za pravilne trenutke odprtja triaka.


Slika 1: Pomožno vezje

Inicializacija mikrokrmilnika

Ob zagonu se izvrši ukaz na naslovu 0x00000000. Izvajati se prične koda, ki se nahaja na naslovu reset: v datoteki crt0.s. Najprej se koda programa prepiše iz pomnilnika flash v pomnilnik RAM, kar kasneje omogoči hitrejše delovanje. Vsa programska koda, ki se ne nahaja v datoteki crt0.s, bo tekla iz pomnilnika RAM. Da je to mogoče, morajo biti temu ustrezno napisana navodila povezovalniku (datoteka iSLPC2138_WinIDEA.ld). Če količina pomnilnika RAM ne zadostuje, mora programska koda pač teči iz pomnilnika flash. Da to dosežemo, je potrebno malce spremeniti navodila prevajalniku. In sicer je potrebno vse .text odseke izven datoteke crt0.s preseliti v pomnilnik flash. Spremenjen del datoteke iSLPC2138_WinIDEA.ld bi v tem primeru izgledal takole: .text :
{
  *(.text)
  *(.glue_7t) *(.glue_7)
} > code
. = ALIGN(4);
.rodata :
{
  *(.rodata)
} > code
. = ALIGN(4);
_etext = . ;
PROVIDE(etext = .);
_codesrc = . ;
.code : AT(_codesrc)
{
  _code = . ;
} > data
Koda v datoteki crt0.s se ne spremeni. Po prepisovanju programske kode je na vrsti postavljanje začetnih vrednosti kazalcev sklada. V našem primeru so postavljeni kazalci za vse načine delovanja, čeprav bomo uporabljali le irq in uporabniški način delovanja. Kazalci sklada v ostalih načinih delovanja so sicer inicializirani, vendar v našem primeru nikdar uporabljeni. Sledi skok v funkcijo start_up(), ki se nahaja v datoteki startup.c. S tem smo zapustili zbirnik in se preselili v programski jezik C.
Inicializacija mikrokrmilnika se nadaljuje s klicem funkcije init(). Argumenti funkcije podajajo želene hitrosti vodil, oziroma želeno frekvenco urinega signala, ter uporabljene vhodne in izhodne pine. Koda je parametrizirana, tako da lahko uporabnik omenjene parametre nastavlja na enem mestu v glavi datoteke. #define clock_rate  60    // Clock rate in MHz (12, 24, 36, 48 or 60)
#define vpb_div     cclk  // VPB divider (cclk, cclk_2 or cclk_4)
#define output_pins P0_5  // Output pins mask
Funkcija init() se nahaja v datoteki init.c in kliče ostale funkcije, ki inicializirajo posamezne sklope mikrokrmilnika (PLL - Phase Locked Loop, VPB - VLSI Peripherial Bus, MAM - Memory Acceleration Module in GPIO - General Purpose Input Output). Po končani inicializaciji se ob klicu funkcije sch_on() dvigne mehanizem operacijskega sistema, ki bo opisan v nadaljevanju. Sledi glavni program, ki pa ga v našem primeru pravzaprav ni. Namesto njega imamo neskončno zanko while(1);saj se funkcija start_up() nikdar ne konča.

Preprost operacijski sistem v realnem času

Koda operacijskega sistema se nahaja v datoteki rtos.c. Jedro sistema predstavlja enostaven razvrščevalnik, ki je napisan v funkciji sch_int(). Razvrščevalnik je klican v enakomernih časovnih intervalih s pomočjo prekinitev Timer0. Zagotavlja, da je vsako opravilo (opravila so funkcije navedene v urniku sch_tab) klicano enkrat v določenem časovnem intervalu. To pomeni, da naš operacijski sistem deluje v realnem času. Zaradi preprostosti sistema veljajo zanj štiri omejitve:
  1. vse časovne rezine so natanko enako dolge,
  2. vsa opravila se vedno zaključijo pred iztekom časovne rezine (če je opravilo predolgo, oziroma se pred iztekom časovne rezine ne zaključi, potem se operacijski sistem namenoma ujame v neskončni zanki - sistem zamrzne, ter tako označi omenjeno napako),
  3. urnik opravil je vnaprej določen in se med delovanjem ne spreminja, opravila v njem pa se izvajajo ciklično, in
  4. prekinitev, razen sistemske prekinitve Timer0, ni.
Delovanje operacijskega sistema v časovnem prostoru simbolično ponazarja slika 2.


Slika 2: Časovno rezinjenje

Opravila, ki jih razvrščevalnik poganja, so funkcije brez argumentov, ki ničesar ne vračajo (tip: void task()). Opravila so deklarirana v datoteki rtos_tasks.h in našteta v urniku opravil sch_tab v datoteki rtos_tasks.c. Urnik opravil je pravzaprav polje kazalcev na funkcije. V našem primeru imamo le dve opravili, in sicer sta to funkciji keys() in turn_on().

rtos_tasks.h: extern void keys();
extern void turn_on();
rtos_tasks.c: voidfuncptr sch_tab[] = {keys, turn_on};Operacijski sistem ne teče sam od sebe. Zagon operacijskega sistema se izvrši ob klicu funkcije sch_on(), ki je klicana iz funkcije start_up() v datoteki startup.c. Funkcija sch_on() z ustreznimi klici drugih funkcij poskrbi za nastavitve časovnika Timer0 in vektorskega nadzornika prekinitev (VIC - Vector Interrupt Controller). Morda najpomembnejše pa je, da s tem postavimo tudi dolžino ene časovne rezine, ki jo je mogoče nastavljati v glavi datoteke startup.c s parametrom #define timeslice 10   // Timeslice in microseconds V našem primeru je dolžina časovne rezine torej 10ms. Ker imamo dve opravili, je vsako na vrsti enkrat na Dt = 20ms.

Opravili

Imamo dve opravili, in sicer sta to funkciji keys() in turn_on(). Njuna koda se nahaja v datoteki main.c. Dolžina časovne rezine omejuje dožino opravil. Funkciji keys() in turn_on() se morata zato tudi v najslabšem primeru končati vedno prej kot v 10ms.
Funkcija keys() vsakič odčita stanje tipk T0, T1 in T3. Glede na zadnjo pritisnjeno tipko poveča, ali zmanjša vrednost globalne spremenljivke turn_on_level, ki določa kot odprtja triaka. Če je bila zadnja pritisnjena tipka T3, potem ostane spremenljivka turn_on_level konstantna. Da se kot odprtja nebi prehitro spreminjal, se povečevanje oziroma zmanjševanje zgodi le na vsakih keys_counter_stop klicev opravila (v našem primeru je keys_counter_stop = 500, opravilo pa je klicano na vsakih 20ms, torej se turn_on_level spremeni na vsakih 500 * 20ms = 10ms).
Drugo opravilo turn_on() na vhodnem pinu p0.4 zazna prehod omrežne napetosti preko ničle. Prehod preko ničle sproži začetek štetja števca turn_on_counter, ki šteje klice opravila. Torej se poveča za ena na vsakih 20ms. Ko števec doseže vrednost spremenljivke turn_on_level, funkcija s postavitvijo pina p0.5 na ena odpre triak, ki seveda ostane odprt do konca polperiode. Razmere ponazarja slika 3.


Slika 3: Delovanje opravila turn_on()