Mérés laboratórium 3 - 3. mérés ellenőrző kérdései
1. Harvard vagy Neumann architektúrájú-e az ATmega128 (avagy külön vagy egy memóriában található-e a programkód és az adat)?
Harvard, mivel külön memória van az adatoknak, és külön memória van a kódnak.
2. Az ATmega128-ban (mint minden mikrovezérlőben) a központi feldolgozóegységen kívül találhatóak egyéb perifériák. Soroljon fel közülük hármat!
Pl.: általános célú portok, időzítő, USART, ADC
3. A közvetlen I/O utasításokon kívül még hogyan érhetőek el az I/O regiszterek?
Az adatmemóriába is le vannak képezve a 0x20 címtől kezdve.
4. Minden általános célú I/O port kezeléséhez három regiszter szükséges. Melyek ezek, és mik a funkcióik?
A digitális I/O portok kétirányúak, tehát be-és kimenetként is működhetnek. Minden porthoz 3 regiszter tartozik, egy adatregiszter (PORTx), egy irányregiszter (DDRx), és egy, csak olvasható regiszter (PINx). Az irány-és adatregiszter bitenként programozható.
Ha a DDRx (ahol x: A…G) értéke 1, akkor kimenetként, ha DDRx értéke 0, akkor bemenetként definiáltuk a portot. Kimeneti port esetén a PORTx-re írjuk a kimeneti értéket, bemeneti port esetén a felhúzó ellenállást (pull-up) szabályozzuk. (Ha a bemeneti portokat nem húzzuk fel logikai 1-be, akkor lebegni fognak, ezáltal a környezetben fellépő zajok miatt kiszámíthatatlan lesz a bemenetek értéke.) Bemeneti port esetén a PINx regiszterekből a bemeneten lévő érték olvasható ki, kimeneti port esetén a PORTx értéke olvasható ki egy órajellel később. (Utóbbit ritkán használjuk.)
5. Létezik-e a megszakításokat globálisan tiltó ill. engedélyező utasítás?
Igen (cli, sei).
6. Adott egy tetszőleges periféria, amelyik képes megszakítás kérésére. Miket kell ahhoz tenni, hogy engedélyezve legyen az adott megszakítás?
Engedélyezni kell mind az adott perifériára specifikusan és globálisan is a megszakításokat.
7. Miután megírtunk egy IT rutint, hogyan gondoskodunk arról, hogy a hozzá tartozó megszakítás beérkezésekor a rákerüljön a vezérlés?
A programmemória elején található IT tábla adott megszakításhoz tartozó sorában elhelyezünk egy ugróutasítást az ISR-ünkre.
Atmel AVR ATmega128 (programozása C alatt):
A következők innen idézve: [[1]]
8. Milyen tulajdonságát használhatjuk ki a mikrovezérlőnek ahhoz, hogy standard C eszközökkel is hozzá tudjunk férni az I/O regisztereihez?
Az adatmemória elejére (egész pontosan a 0x20 címtől kezdődően) le vannak képezve az I/O regiszterek. Ennél fogva memóriacímzéssel elérhetővé válnak számunkra. Tekintsük az alábbi kódrészletet:
- define SREG (*(volatile unsigned char *)(0x3F + 0x20))
Példának okáért a státusz regiszter a 0x3F I/O címen található. Az adatmemóriában ez a 0x5Fnek felel meg. Ezt az értéket ha átkasztoljuk egy megfelelő mutatóvá, máris képesek leszünk a regisztert elérni.
9. AVR-GCC fordító alatt milyen nyelvi kiterjesztés segítségével oldható meg a megszakítások globális engedélyezése ill. tiltása?
A megszakítások engedélyezésére / tiltására egyes C nyelvi kiterjesztések tartalmaznak utasításokat. Az AVR-GCC azonban nem. Viszont létezik egy másik, igen gyakran alkalmazott, hatékony kiegészítése a C nyelvnek: inline assembly. Azaz lehetőségünk van assembly utasítások közvetlen elhelyezésére a C kódban. Így például az alábbi makrók segítségével tudjuk C-ből engedélyezni ill. tiltani a megszakításokat AVR-GCC alatt:
- define sei() asm _volatile_ ("sei" ::)
- define cli() asm _volatile_ ("cli" ::)
Anélkül, hogy a GCC fordítók inline assembly szintaktikáját alaposabban elemeznénk, jól látszik, hogy a sei() és cli() makrók a nekik megfelelő assembly utasítást tartalmazzák. A megszakításkezelő rutinunkat megírhatjuk egy függvényben, de valahogy jelezni kell a fordítónak, hogy ez nem egy hagyományos rutin, hanem egy ISR, és ehhez mérten olyan bevezető és záró kódot főzzön hozzá, amit ilyen esetekben kell (a szükséges regiszterek mentése, visszaállítása, return from interrupt utasítás). Ehhez ismét egy nyelvi kiegészítés lesz segítségünkre. A GCC fordítóknál ugyanis lehetőség nyílik különféle attribútumokat főzni egy függvényhez annak deklarációja során. Ekképp az alábbi makrót definiálhatjuk ISR bevezetésére:
- define ISR(vector)
void vector (void) attribute ((signal, used));
void vector (void)
A signal egy AVR specifikus attribútum, ami olyan megszakításkezelő rutinként jelöli meg a függvény, amelynek belsejében a megszakítások tiltva vannak. (Létezik egy interrupt attribútum is, ennek hatására olyan kódot generál a fordító, amely engedélyezi a rutin belsejében a megszakításokat.) Az used pedig azt mondja a fordítónak, hogy a függvényre szükség van, és ne optimalizálja ki például abból az okból, hogy a kódból sehonnan sem hívják meg. (Vegyük észre, hogy az ISR-nek nincs se bemenő, se kimenő paramétere. Ha jobban belegondolunk, ez logikus is.) Azt pedig, hogy a megszakításkezelő rutinunk melyik megszakításhoz tartozik (a vektortáblán belül hova helyezze el a fordító a rutinunkra ugró utasítást), úgy tudjuk megmondani neki, hogy speciális nevet adunk az ISR-ünknek (pl. INT4_vect a 4-es külső megszakításnak, vagy ADC_vect az analóg-digitális átalakítónak). Azaz például:
ISR(INT4_vect) { // Do something...; }
Szerencsére ezen makrókat sem kell nekünk létrehozni, mert a standard C könyvtárral együtt kapott <avr/interrupt.h> fejléc fájl már tartalmazza. (Ha egy megszakításhoz nem tartozik ilyen speciális névvel rutin, akkor a fordító alapértelmezésként a megszakítás tábla megfelelő helyére egy, a reset címre ugró utasítást helyez el.)
Röviden: Az inline assembly segítségével.
10. A C standard I/O szolgáltatásainak milyen alapfüggvényekre van szükségük egy adott perifériára használatához?
A mérőpanelon található egy 4x20 karakteres LCD kijelző. Rövidebb üzenetek megjelenítésére ez is alkalmas. Először is írnunk kell olyan rutinokat, amelyek képesek egy karaktert kiírni vagy beolvasni az adott perifériákból. Ha ez megvan, akkor "össze kell főznünk" ezen alaprutinokat a standard I/O függvényekkel. A programozás megkönnyítése végett a mérőpanelhez készültek különböző API-k (így a soros port és az LCD kezeléséhez is). Az API-k biztosítják az alapvető függvényeket egy karakter beolvasására és kiírására, valamint az adott perifériákhoz tartozó I/O stream(ek)et is:
int serial_putc(char character, FILE * stream) { ... }
int serial_getc() { ... }
int LCD_putc (char character, FILE * stream) { ... }
FILE serial_stdout = FDEV_SETUP_STREAM(serial_putc,NULL,_FDEV_SETUP_WRITE);
FILE serial_stdin = FDEV_SETUP_STREAM(NULL,serial_getc,_FDEV_SETUP_READ );
FILE LCD_stdout = FDEV_SETUP_STREAM(LCD_putc, NULL, _FDEV_SETUP_WRITE);
Felhasználói program írásakor az API-t tartalmazó fejléc fájlokra (<board/serial.h>, <board/lcd.h>) történő hivatkozáson kívül csak az összerendeléseket kell megtenni a kívánt periféria és az stdin vagy stdout között:
stdout = &LCD_stdout;
printf("A message on LCD.");
Röviden: Az adott perifériára egy karaktert kiírni ill. beolvasni képes függvényekre.
Beágyazott operációs rendszerek:
11. Mi az a három alapvető állapot, amelyben a taszkok tartózkodhatnak? Továbbá milyen átmenetek lehetségesek (preemptív ütemező esetén)?
A taszkoknak (letlégyen szó bármely operációs rendszerről) alapvetően három állapota van: fut, futásra kész és várakozik. Egy taszk akkor van a várakozó állapotban, ha valamilyen esemény bekövetkeztére vár (pl. adott idő leteltére, egy szemaforra, stb.). Amint ez az esemény bekövetkezik, a taszk futásra kész állapotba kerül. Azonban futni csak akkor fog, ha a futásra készek közül ő rendelkezik a legmagasabb prioritással. Futó állapotban csak egy taszk tartózkodhat (általános esetben pedig a processzorok számával megegyező). Ha létezik "fut -> futásra kész" állapotátmenet is, akkor preemptív ütemezőről beszélünk (a továbbiakban ezt feltételezzük), ellenkező esetben nempreemptívről. Ha egy prioritás több taszkhoz is rendelhető, az azonos prioritásúak között időosztással (time-slicing) körforgó (round-robin) ütemezést szoktak megvalósítani.
12. Ismertesse a végtelen ciklusú taszk felépítését!
A hagyományos taszk felépítés alapvetően egy végtelen ciklus, amit esetleg egy inicializációs szakasz előz meg. Annak érdekében, hogy a vezérlés átkerülhessen másik taszkra is, a végtelen ciklusban el kell helyezni olyan OS hívásokat, amelyek várakozó állapotba juttatják a taszkot.
13. Ismertesse a single-shot taszk felépítést!
Létezik az ún. single-shot felépítés is. Itt nincs végtelen ciklus. A taszk elindul és feladatának befejezéséig fut, amikor is törli magát. A törlés előtt azonban még kiválthat olyan eseményeket, amelyek más taszkok elindulását eredményezik. Ekképp a taszkok futása láncszerűen fog kinézni.
Vagy: A taszk az elejétől a végéig lefut. Menet közben kivált olyan eseményeket, melyek más taszkot triggerelnek. A végén törli magát.
14. Mit értünk egy taszk környezete (kontextusa) alatt?
Minden taszknak létezik egy környezete (kontextusa): a processzor regisztereinek értéke, verem, veremmutató, programszámláló... Hagyományos programozás esetén is léteznek ezek, de természetesen ekkor mindenből csak egy van. Viszont több taszkos rendszerben gondoskodni kell arról, hogy minden taszknak meglegyen a saját környezete. Az egyik taszk környezetének elmentését majd egy másik taszk környezetének betöltését nevezzük kontextusváltásnak.
15. Miért van szükség arra, hogy az OS tudja, mikor járunk ISR kódban?
Beágyazott operációs rendszer alkalmazása esetén a hagyományos módon megírt megszakítások nem várt működésre vezetnének. Azt várnánk, hogy ha az ISR befejeződése után még mindig a megszakított taszk prioritása a legmagasabb a futásra készek közül, akkor oda térne vissza a vezérlés. Ha viszont az IT rutin hatására futásra kész állapotba került taszk prioritása nagyobb, mint a megszakítotté, akkor pedig ahhoz a taszkhoz. Sajnos azonban a hagyományos módon megírt megszakítás nem így működik. Az operációs rendszer ugyanis nem tudja, hogy most épp megszakítás kódból hívták meg az egyik szolgáltatását. Ennél fogva az újraütemezés azonnal meg fog történni. Két megoldás lehetséges a problémára. Az egyik az, hogy az operációs rendszer hivatott a megszakításokat "elkapni", és a hozzá tartozó, általunk megírt ISR-t ő fogja ezután meghívni. A másik lehetséges megoldás pedig az, ha a saját ISR-ünkre ugrik egyből a vezérlés, de úgy írjuk meg, hogy az elején értesítjük az operációs rendszert az ISR kódba lépésről, a végén pedig annak elhagyásáról.
16. Preemptív ütemező esetén lehetséges, hogy nem az eredetileg megszakított taszk kapja vissza a vezérlést az ISR lefutása után?
Igen. Ha az ISR olyan OS hívást hajt végre, amelynek eredményeképpen egy magasabb prioritású taszk lesz futásra kész, akkor az ISR után (lásd az előző kérdést), arra fog térni a vezérlés, és nem az eredetileg megszakított taszkra.
17. Ha az egyes taszkok rendelkeznek közös memóriával, könnyen kommunikálhatnak egymással. Mi a hátránya a közös memóriának (ált. esetben közös erőforrásnak)?
A probléma pedig abból áll, hogy ha az egyes futási egységek (taszk vagy ISR) nem kezelik atomikus módon az adott erőforrást, akkor egymás futását meg-megszakítva előfordulhat, hogy az erőforrás inkonzisztens állapotban látszódik. Például egy megszakítás időnként frissít egy 16 biten ábrázolt számot. Egy taszk pedig ezt a számot folyamatosan beolvassa, és értékétől függően folytatja tovább a feldolgozást. Ám ha a platform 8 bites, a 16 bites szám beolvasása egy assembly művelettel nem lehetséges. És ha épp a beolvasás közben érkezik a megszakítás, akkor a taszk által kapott érték egyik fele a régi, másik fele az új adatot tartalmazza. Ha programunkat valamilyen magas szintű nyelven írjuk, akkor ráadásul nem is látszik, hogy az általunk kiadott értékadás valójában több assembly utasításra fordítódik le.
18. Egy ISR és egy taszk globális változón keresztül kommunikálnak egymással. Hogyan küszöbölhető ki ebben az esetben a közös erőforrások problémája?
A módszer a megszakítások tiltása ill. engedélyezése. Ha az egyik résztvevő fél egy ISR, csak ezzel a módszerrel élhetünk.
19. Mire kell ügyelni, ha a megszakítások tiltását ill. engedélyezését választjuk egy adott esetben a közös erőforrások problémájának megoldására?
Vigyáznunk kell, hogy minél tovább tiltjuk a megszakításokat, annál nagyobb lesz az az idő, amin belül képes a rendszerünk reagálni rájuk. Márpedig a megszakításokba épp azon feladatok kerülnek, amelyek végrehajtása időkritikus.
20. Két taszk globális változón keresztül kommunikál egymással. Hogyan küszöbölhető ki ebben az esetben a közös erőforrások problémája?
Ha csak taszkok között merülhet fel a probléma, lehetőség van az ütemező tiltására ill. engedélyezésére is. Létezik viszont egy egyszerű elv, ami szintén megoldást nyújt a problémára: a lock-bit. A bit egyik állása jelképezi, hogy szabad az erőforrás, másik állása pedig azt, hogy már használatban van. Ezek után a kritikus szakasz elején megvizsgáljuk, hogy szabad-e az erőforrás. Ha nem, várakozunk. Ha igen, átbillentjük a bitet (test-and-set), és belépünk a kritikus szakaszba. A szakasz végén pedig visszaállítjuk szabad állapotba a bitet.
21. Mi a szemafor, és mit kell tudni a működéséről?
Nevét a vasúti pályaszakaszokat őrző berendezésről kapta. Két alapvető művelete a P() illetve a V() operáció. Az előbbi voltaképp a TAS műveletnek felel meg (nevezik még Wait()-nek vagy Pend()-nek is). A második pedig a szabadba állítás (nevezik még Signal()- nak vagy Post()-nak is). A szemafor értékét a P() operáció csökkenti eggyel, ha előzőleg nem nulla volt. A V() operáció pedig növeli eggyel. Ha a szemafor maximális értéke 1, akkor bináris (binary) szemaforról beszélünk (ez felel meg tulajdonképpen a lock-bit-nek). Ha maximális értéke egynél nagyobb, számláló (counting) szemaforról beszélünk. Vigyáznunk kell azonban, ugyanis a szemaforok alkalmazásával is elkövethetünk hibákat:
- elfelejtjük elkérni / visszaadni,
- rossz szemafort kérünk el / adunk vissza,
- holtpont (deadlock, deadly embrace) alakul ki,
- túl sokáig birtokoljuk (kiéheztetés),
- prioritás inverzió (priority inversion).
22. [Kiegészítő anyag] Mi a prioritás inverzió jelensége?
A szemaforok alkalmazásának egyik nemkívánatos mellékhatása a prioritás inverzió. Adott egy alacsony, egy közepes és egy magas prioritású taszk. Tételezzük fel, hogy először az alacsony prioritású lesz futásra kész. Miután elindult, egy adott ponton lefoglalja a szemafort, majd fut tovább. Ekkor azonban futásra kész lesz a magas prioritású taszk is. A (preemptív) kernel ennek megfelelően ütemezni is fogja. Egy adott ponton a magas prioritású feladat is le szeretné foglalni a szemafort. De mivel az már foglalt, ezért várakozó állapotba kerül. A vezérlés pedig visszaadódik az ekkor egyetlen futásra kész taszkra, az alacsony prioritásúra. Némi idő eltelte után a közepes fontosságú feladat is futásra kész lesz, a kernel ütemezi is, majd futása végeztével folytatódik tovább az alacsony prioritású taszk futása. Egészen addig, amíg el nem engedi a szemafort. Ez a lépés ugyanis futásra kész állapotba hozza a magas prioritású feladatot, amit a kernel ütemez is. Futása során elvégzi azon műveleteket, melyhez a szemafor kellett, majd elengedi azt. Ekkor nem történik semmi, lévén nem vár magasabb prioritású taszk a szemaforra. Miután befejezi futását, visszatér a vezérlés az alacsony prioritású feladatra, így az is be tudja fejezni működését. Figyeljük meg, hogy az alacsony prioritású taszk részben, a közepes prioritású pedig teljes egészében képes volt "beelőzni" a magas prioritásút. Sőt, ha több köztes prioritású feladat is lenne, legrosszabb esetben mindegyik olyan időzítéssel lesz futásra kész, hogy mindegyik hamarabb fog lefutni, mint a legmagasabb prioritású. Ezért nevezik ezt a jelenséget prioritás inverziónak.
23. [Kiegészítő anyag] Hogyan mőködik a prioritás plafon protokoll?
A protokoll úgy működik, hogy megnézzük, mely taszkok használnak egy adott szemafort. Kiválasztjuk a legmagasabb prioritásút, és ezt a prioritást (nevezzük prioritás plafonnak) hozzárendeljük a szemaforhoz. A normál szemafor működéshez képest annyi az eltérés, hogy ha egy már birtokolt szemafort egy magasabb prioritású taszk szeretne elkérni, a birtokos feladat prioritását megnöveljük a szemaforhoz rendelt prioritás plafonra. Amint elengedi a szemafort, prioritása visszaáll az eredetire. A közepes prioritású taszk most már nem előzte meg a magas prioritásút. Egy dolgot nem sikerült kiküszöbölni (és igazándiból nem is lehet): az alacsony prioritású taszk arra az időre, amíg birtokolja a szemafort, blokkolja a magas prioritásút. Azon szemaforokat, melyekre megoldják valamilyen protokoll segítségével a prioritás inverzió problémáját, kölcsönös kizárású szemaforoknak, angolul mutual exclusion semaphore (vagy röviden mutex) nevezzük.
24. Az OS időkezelés céljából egy periodikus időzítőt használ (heartbeat timer). Az időzítő két jelzése közt eltelt időt nevezzük óraütésnek. Mekkora pontosság érhető el az OS időkezelő szolgáltatásai által?
Egyfelől minél kisebb az óraütés, annál pontosabbak lesznek az időzítőn alapuló szolgáltatások. Másfelől minél nagyobb az óraütés, annál kevesebb overheadet jelent a timer megszakítás kezelése. Értéke általában 10-100 ms szokott lenni (avagy másodpercenként 100-10 óraütés keletkezik).
25. Ha legalább n óraütésre szeretnénk késleltetni, mennyi óraütést kell paraméterül adni a késleltető szolgáltatást nyújtó függvénynek?
Ha nekünk legalább n óraütésre kell várnunk, a késleltető függvénynek n+1 paramétert kell adni!
26. Milyen frekvencia mondható tipikusnak a heartbeat timer esetén?
10-100 Hz
27. Hasonlítsa össze a beágyazott és a hagyományos OS-t programszervezés és indulás szempontjából!
Hagyományos | Beágyazott | |
Indulás | először az OS indul, és az alkalmazásokat ő tölti majd be a memóriába | először (az OS-sel együtt fordított) alkalmazás indul, majd az inicializációs szakasz után elindítja az OS ütemezőjét |
Programszervezés | külön az OS, külön az alkalmazások | az OS és az alkalmazás szerves egészet alkot |
C/OS:
28. A C/OS taszk állapotátmeneti gráfjában található két plusz állapot a három alapvető mellett. Melyek ezek, és mit kell tudni róluk?
A három alapvető állapot (RUNNING, READY, WAITING). Van azonban két speciális állapot is:
- DORMANT: "szunnyadó", akkor van ebben az állapotban a taszk, amikor a memóriában ugyan megtalálható, de az ütemező hatáskörében nincs benne (nem hozták még létre -> OSTaskCreate(), vagy törölték -> OSTaskDel()),
- INTERRUPTED: a taszkot megszakította egy interrupt rutin. Ha a rutinnak nincs olyan mellékhatása, amely egy magasabb prioritású taszk futásra késszé tételét eredményezné, akkor rutin végeztével a megszakított taszk RUNNIG állapotba kerül vissza (OSIntExit()). Ha azonban a rutin során egy magasabb prioritású taszk futásra kész állapotba került, akkor visszatéréskor az fog futni és a megszakított taszk pedig a
READY állapotba kerül (OSIntExit()*).
Ha nincs egy futásra kész taszk sem, akkor OSTaskIdle() fut.
29. Lehet-e C/OS alatt több taszk is egy prioritási szinten?
A C/OS ütemezője preemptív. Összesen 64 prioritási szint lehetséges (0..63). Szintenként maximum egy taszk megengedett (ez azt is jelenti, hogy a prioritások alkalmasak a taszkok azonosítására).
Egyéb kérdések:
30. Kb. mennyi utasítást tud végrehajtani az ATmega128 1 óraütés alatt, ha 100 óraütés érkezik másodpercenként (a panelon található mikrovezérlő frekvenciája 11,0592 MHz, és az ATmega128 utasításai 2-3 órajel alatt hajtódnak végre)?
Egy óraütés alatt 11 059 200 / 100 periódusnyi órajel fut le. Ha várható értékben 2,5 periódusnak tekintjük az egy utasítás végrehajtásához szükséges időt, akkor végül ezt kapjuk eredményül: 11 059 200 / 100 / 2,5 ~ 45 000.
31. Kb. mennyi utasítást tud végrehajtani az ATmega128 a benne található A/D átalakító konverziós ideje alatt (az A/D konverter 128-al leosztott órajelet kap, 13 periódus kell az átalakításhoz az így leosztott órajelből és az ATmega128 utasításai 2-3 órajel alatt hajtódnak végre)?
Egy átalakítás alatt 128 * 13 processzor órajel történik. Ezt ismét 2,5-el osztva: ~ 650. Látható, hogy nem túl sok utasításra hagy időt az ADC, ha free-running üzemmódra van állítva. Hiába a legnagyobb (128) előosztó.
32. Mit takar a C nyelv volatile kulcsszava?
A volatile azt jelenti, "illékony". A C nyelvben ezzel azt mondhatjuk meg a fordítónak, hogy a változó tartalma nem csak a programkód futásának hatására változhat. Ez azért fontos, mert normális esetben ilyen nem fordulhat elő, ezért a fordító kioptimalizálhatja.
33. Írja föl bináris formában a 0xBC számot!
B | C |
1011 | 1100 |
34. Írja föl hexadecimális formában a 0b10111100 számot!
1011 | 1100 |
B | C |