17.3 ISR írása: C és assembly vegyesen

17.3.1 Motiváció

Tegyük fel, hogy az MPS2-AN385 (Cortex-M3) panelen egy 10 kHz-es mintavevő rutint kell futtatnunk: a periféria időzítője periodikusan megszakítást kér, a kezelőrutin kiolvassa az adatregisztert és beírja egy puff erbe, a főciklus pedig közben a puff ert dolgozza fel. A kérdés nem az algoritmus, hanem a szerződés: amikor a processzor a főciklus kellős közepén megszakítja önmagát, ki menti el azokat a regisztereket, amelyekben a megszakított kód éppen dolgozott? Ha ezt nekünk kellene megtenni, minden ISR (interrupt service routine, megszakításkezelő rutin) egy assembly trampolinnal kezdődne. A Cortex-M kivételmodellje ezt a munkát hardverre bízza — ennek pontos tartalma és következményei a jelen alfejezet tárgya.

17.3.2 Definíció és a vektortábla-bejegyzés szerződése

Definíció (ISR). Az ISR olyan, a megszakított végrehajtási folyamattól függetlenül lefutó vezérlési szál, amelyet a processzor kivétel-belépés mechanizmusa hív meg, és amelynek visszatérése nem BL-hez, hanem egy speciális, hardver által előkészített visszatérési mechanizmushoz kötődik. Thumb-2/Armv7-M ISA kontextusban az ISR „argumentumai” nincsenek: a hívó fél (a megszakított kód) regiszterállapota a verembe kerül, nem paraméterlistába.

Definíció (vektortábla-bejegyzés). A vektortábla egy, a kódtartomány legelején elhelyezkedő, 4 bájtos szavas tömb, amelynek nn-edik szava a nn sorszámú kivétel belépési pontjának címe. Az n = 0 slot nem cím, hanem a kezdeti veremmutató (_estack) értéke; az n = 1 a reset-vektor. A rendszerkivételek 1–15 sorszámot kapnak, az externálisan kért megszakítások (IRQ) sorszámozása 16-tól indul, azaz az IRQn számú megszakítás táblaindexe 16+IRQn16 + IRQn, bájt-offszete pedig:

offset(IRQn)=4⋅(16+IRQn) \text{offset}(\text{IRQn}) = 4 \cdot (16 + \text{IRQn})

Ez az összefüggés tisztán levezethető: minden bejegyzés egy 32 bites szó, és előtte 16 rendszerkivétel-slot áll. Például az MPS2 IRQ8 (CMSDK Timer0) bejegyzése a 4⋅24=96=0x604 \cdot 24 = 96 = \text{0x60} bájt-offszetre esik.

A tábla tartalma és a C függvénynév között nincs mágikus kapcsolat: a linker annyit lát, hogy az egyik fordítási egységben (a startup assembly-fájlban) egy .word TIMER0_IRQHandler direktíva egy szimbólumra hivatkozik, másutt pedig egy C függvény definiálja ugyanezt a szimbólumot. A tábla csak a szimbólum címe iránt érdeklődik. Ez a név–cím kapcsolat a regisztrálás teljes mechanizmusa. A jellemző gyakorlat a gyenge szimbólum (weak symbol) és a Default_Handler-re mutató alias:

Az alábbi részlet csak a .vectors szekció, nem önálló fájl. Az IRQ8-ig szóról szóra teljes: minden indexnek valódi szó felel meg. A hozzá tartozó Reset_Handler, Default_Handler és a gyenge aliasok a kanonikus lab-startup.s-ben vannak (Startup kód), és a kidolgozott példa pontosan ezekkel a sorokkal bővíti azt:

/* Vektortábla-részlet (csak a .vectors szekció) — Thumb-2 / Armv7-M ISA */
.section .vectors,"a",%progbits
.align 2
    .word   _estack              /* 0  (0x00): kezdeti SP          */
    .word   Reset_Handler        /* 1  (0x04)                      */
    .word   NMI_Handler          /* 2  (0x08)                      */
    .word   HardFault_Handler    /* 3  (0x0C)                      */
    .word   MemManage_Handler    /* 4  (0x10)                      */
    .word   BusFault_Handler     /* 5  (0x14)                      */
    .word   UsageFault_Handler   /* 6  (0x18)                      */
    .word   0, 0, 0, 0           /* 7-10 (0x1C-0x28): fenntartott  */
    .word   SVC_Handler          /* 11 (0x2C)                      */
    .word   DebugMon_Handler     /* 12 (0x30)                      */
    .word   0                    /* 13 (0x34): fenntartott         */
    .word   PendSV_Handler       /* 14 (0x38)                      */
    .word   SysTick_Handler      /* 15 (0x3C)                      */
    .rept   8
    .word   Default_Handler      /* 16-23 (0x40-0x5C): IRQ0-IRQ7   */
    .endr
    .word   TIMER0_IRQHandler    /* 24 (0x60): IRQ8 — ide kerül a C függvény címe */

/* gyenge alapértelmezés: ha a felhasználó nem definiálja, ide köt */
.weak   TIMER0_IRQHandler
.thumb_set TIMER0_IRQHandler, Default_Handler

Két részlet magyarázatra szorul. Egyrészt a 4 bájtos szóba kerülő érték nem a függvény „nyers” címe, hanem annak Thumb-bittel jelölt változata; a Thumb-2 szimbólumok címe eleve páratlan (LSB = 1), és a GAS .thumb_set illetve a C fordító által előállított szimbólumok ezt automatikusan hordozzák. Ha kézzel írnánk be páros címet, a processzor a kivétel belépésekor hardverhibába (HardFault) futna. Másrészt a .weak + .thumb_set minta megvalósítja azt, hogy a vektortáblát ne kelljen újrafordítani, amikor valamelyik kezelőt implementáljuk: a linker a gyenge szimbólumot a valódi definícióval cseréli. Az alapértelmezett kezelő tipikusan egy végtelen ciklus, ami rögtön megmutatja a hibát debugging közben.

17.3.3 Az automatikus context-mentés

Kivétel belépésekor a hardver, mielőtt egyetlen utasítást is végrehajtana a kezelőből, nyolc regisztert a verembe ír. A verem a CONTROL.SPSEL és a belépéskori mód függvényében az MSP vagy a PSP — a pontos választás az EXC_RETURN értékéből olvasható vissza. A keret elrendezése fix, a veremmutató a legalacsonyabb címre mutat:

Offszet az SP-től Tartalom Megjegyzés
+0x00 R0 argumentum / munkaregiszter
+0x04 R1 argumentum / munkaregiszter
+0x08 R2 argumentum / munkaregiszter
+0x0C R3 argumentum / munkaregiszter
+0x10 R12 scratch (IP)
+0x14 LR (R14) a megszakított kód visszatérési címe
+0x18 PC (R15) folytatási cím: ahol a visszatérés után a végrehajtás folytatódik
+0x1C xPSR állapotjelzők + IPSR kivételszám

Az SP tehát 32 bájttal csökken. Ha a belépéskori SP nem volt 8 bájt szerint igazítva, a mag további 4 bájtot hagy ki: ez az igazítási szó a keret fölött, a magasabb címen (+0x20), az xPSR és a belépés előtti SP között van. A fenti offsetek ezért ettől nem változnak, a kihagyást pedig a mentett xPSR 9. bitje jelzi, hogy a visszatérés pontosan visszaadhassa. Ez az elrendezés architekturális szabály, nem QEMU- vagy hardverfüggő részlet.

A mentett PC általánosan folytatási cím. Megszakításnál ez a következő, még végre nem hajtott utasítás. Szinkron hibánál gyakran maga a hibát okozó utasítás, de nem mindig: egy pontatlan (imprecise) BusFault esetén a mentett PC egy később végrehajtott utasításra mutathat. A Cortex-M3-nak nincs FPU-ja, ezért itt mindig ez a 8 szavas keret jön létre. A lebegőpontos regisztereket is tartalmazó kiterjesztett keret csak FPU-s magokon (Cortex-M4F, M7) fordul elő.

Levezetés: miért pontosan ez a nyolc regiszter? Az AAPCS szerint a hívott függvény szabadon felülírhatja az R0–R3 és R12 regisztereket (argumentum- és scratch-regiszterek), valamint az LR-t; a hívó felelőssége ezek megőrzése, ha értékükre a hívás visszatérése után szüksége van. A hardver által mentett halmaz pontosan ez a caller-saved halmaz, kiegészítve a PC-vel és az xPSR-rel, amelyek a megszakított folyamat folytatásához elengedhetetlenek. Az R4–R11 (callee-saved) regisztereket nem menti a hardver, mert azokat — az AAPCS szerint — úgyis annak kell megőriznie, aki használja: vagyis az ISR-nek, a fordító által generált szokásos prológusban. Ez a megfigyelés az egész alfejezet kulcsa.

Definíció (automatikus context-mentés, stacking). A kivétel belépésének mikroarchitekturális lépéssorozata, amely a caller-saved regisztereket és a folytatási állapotot a verembe írja, majd az EXC_RETURN kódot az LR-be tölti, és a PC-t a vektortábla bejegyzésére állítja.

17.3.4 Miért írható a legtöbb ISR tisztán C-ben

Az automatikus mentés miatt a kezelő belépésekor a regiszterállapot már „mentve van” pontosan azon a szerződésen keresztül, amit a C fordító a függvényhívásoknál amúgy is elvár. A C-ben megírt ISR számára a helyzet megegyezik egy közönséges függvényhívással: az R0–R3 és R12 szabadon használható, az R4–R11-et használat előtt meg kell őrizni (ezt a fordító megteszi), az LR-t pedig csak akkor kell menteni, ha az ISR további függvényt hív. Nem kell assembly trampolin, mert a hardver már elvégezte a hívói oldal munkáját. A visszatérést sem kell kézzel megírni: a fordító szokásos epilógusa az LR-ben kapott EXC_RETURN értéket juttatja a PC-be, és ez indítja el a kivétel-kilépést.

Az EXC_RETURN fogalmi szinten a következő: kivétel belépésekor az LR nem visszatérési címet, hanem egy fenntartott bitmintát kap, például 0xFFFFFFF9 (Thread mód, MSP), 0xFFFFFFFD (Thread mód, PSP) vagy 0xFFFFFFF1 (Handler mód, MSP). Amikor a kezelő az LR-en keresztül tér vissza, a processzor felismeri ezt a mintát, és nem ugrást, hanem kivétel-kilépést hajt végre: visszatölti a nyolc regisztert, és a megszakított ponton folytatja. Az ISR-t tehát nem kell „különleges” módon megírni. A kivétel-kilépést bármely, Handler módban megengedett PC-író utasítás kiváltja, ha az EXC_RETURN-értéket tölti a PC-be: a bx lr, a pop {..., pc}, az ldm a PC-vel a listában vagy egy ldr pc, [...]. A levélfüggvényként fordított kezelő tipikusan bx lr-rel zárul, a más függvényt hívó kezelő push {..., lr} / pop {..., pc} párral; egyiket sem kell kézzel írni. A GCC __attribute__((interrupt)) Armv7-M-en érvényes, de nem kötelező: a kivételtípust figyelmen kívül hagyja, és azt jelzi, hogy a függvény csak 4 bájtra igazított SP-vel is meghívódhat, ezért a prológus 8 bájtra igazít. A belépéskori hardveres igazítás (CCR.STKALIGN = 1, a Cortex-M3 r2p0-tól alapértelmezés) mellett a sima void f(void) is helyes.

Az alábbi ábra a teljes belépés–futás–kilépés ciklust foglalja össze, a később bemutatott TIMER0_IRQHandler példáján:

Megszakítás be- és kilépés: automatikus mentés és EXC_RETURN - A Thumb-2/Armv7-M kivétel-belépés és -kilépés lépéssorozata.

A mentett regiszterek mennyisége jól szemlélteti, mennyit spórolunk. Az alábbi ábra és táblázat levezetett értékeket mutat: a hardveres keret fix 32 bájtja fölé a fordító csak azt teszi, amit az ISR ténylegesen használ.

Levezetett, illusztratív veremhasználat ISR-belépéskor (nem mért érték) - Levezetett értékek AAPCS64-hez analóg Armv7-M szabályokból: 1 = csak hardveres mentés; 2 = +LR mentés és 8 bájtos igazítás; 3 = +négy callee-saved regiszter (R4-R7) mentése. Nem mért, nem hardverhű abszolút érték.
Eset Hardver Fordító Összesen
Üres ISR (nem hív más függvényt) 32 B 0 B 32 B
ISR, amely C függvényt hív 32 B LR + 4 B igazítás = 8 B 40 B
ISR, amely R4–R7-et is használ és hív 32 B 5 szó → 8-ra kerekítve = 24 B 56 B

17.3.5 A pending-jelző törlése: két különböző réteg

Itt rejlik az alfejezet leggyakoribb gyakorlati csapdája, és fontos szétválasztani két fogalmat. Az NVIC pending bit (a korábbi alfejezetben tárgyalt ISPR/ICPR regiszterpár) a megszakításkérésnek a processzor oldali tárolója. Külső, periféria által kért megszakítás esetén ezt a bitet a hardver automatikusan törli, amikor a kivételt belépésként elfogadja — tehát az ISR-nek ezzel nem kell foglalkoznia. A periféria saját megszakításjelzője viszont egyáltalán nem törlődik magától: amíg a periféria jelzője be van állítva, addig a kéréssor továbbra is aktív, és az NVIC a kezelő visszatérése után azonnal újra pending állapotba kerül.

Definíció (periféria pending-jelző). A periféria saját állapotregiszterében tárolt bit (gyakran INTSTATUS/RIS/MIS néven), amely a kérést addig tartja, amíg azt szoftveresen nem törlik (írás-1-hatású INTCLR regiszterbe, vagy a status regiszter olvasásával).

A következmény drámai: elfelejtett törlés esetén az ISR visszatér, a processzor azonnal újra belép ugyanabba a kezelőbe, és a főciklus soha nem kap processzort. Ezt nevezzük megszakítási viharnak (interrupt storm). A tünet jellegzetes: a rendszer „él” (az óra jár, a LED villog, ha az ISR-ben van a villogtatás), de a főciklus első utasítása után mindig megszakadás következik, és a megszakításszámláló a valóságosnál nagyságrendekkel gyorsabban nő.

Ezért az ISR szerkezeti vázlata mindig: adat kiolvasása → a periféria pending-jelzőjének törlése → feldolgozás. A törlést érdemes a feldolgozás elé tenni (a lehető leghamarabb az adat kiolvasása után), hogy egy hosszabb feldolgozás közben bekövetkező újabb esemény ne vesszen el.

17.3.6 Kidolgozott példa: Timer0 ISR C-ben az MPS2-AN385-ön

Munkakönyvtár: ~/arm-labor/cm/a070/ — az alfejezet minden laborfájlja ide kerül, és minden parancs innen futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le).

mkdir -p ~/arm-labor/cm/a070
cd ~/arm-labor/cm/a070

A linker script és a startup a II. rész kanonikus párja (Linker script alapjai, Startup kód), a vektortábla ellenőrzője pedig a Vektortábla alfejezet lab-check-vectors.py-ja. A linker script változatlan; a startup másolatának vektortábláját egyetlen, gépileg alkalmazható lépéssel bővítjük a Timer0 (IRQ8) bejegyzésével:

cp ~/arm-labor/cm/a065/lab-mps2.ld .
cp ~/arm-labor/cm/a066/lab-startup.s lab-a070-startup.s
cp ~/arm-labor/cm/a064/lab-check-vectors.py .
cat > lab-a070-irq.inc <<'EOF'
    /* 16-tól: külső IRQ-k; a Timer0 = IRQ8 a 24. index, előtte 8 helyet ki kell tölteni */
    .rept 8
    .word Default_Handler         /* 0x40-0x5C: IRQ0-IRQ7 (itt nem használt) */
    .endr
    .word TIMER0_IRQHandler       /* 0x60: IRQ8 (Timer0)         */
EOF
sed -i '/\.word SysTick_Handler/r lab-a070-irq.inc' lab-a070-startup.s   # a SysTick sor után
# gyenge alias: kezelő nélkül is linkelhető
echo '    lab_default_handler TIMER0_IRQHandler' >> lab-a070-startup.s
# ellenőrzés: a bővített tábla vége
sed -n '/\.word SysTick_Handler/,/^$/p' lab-a070-startup.s

A sed mintája csak a vektortábla .word SysTick_Handler sorára illeszkedik, a fájl végén lévő lab_default_handler SysTick_Handler aliasra nem. A fel nem használt IRQ-helyek a kanonikus Default_Handler-re mutatnak. Az echo sor a kanonikus lab_default_handler makróval a fájl végére egy gyenge, Default_Handler-re mutató aliast fűz a használt kezelőhöz is. A C-fájl erős definíciója ezt felülírja, de ha egy build a kezelőt nem tartalmazza, a KEEP-pel megtartott vektortábla hivatkozása akkor sem marad feloldatlan. Ezek valódi táblaszavak, nem megjegyzések, mert csak így kerül a használt kezelő a megfelelő indexre (16+IRQn16 + \text{IRQn}).

A példa a CMSDK APB Timer0-t használja. A regisztercímek a QEMU mps2-an385 machine dokumentált memóriatérképéből származnak, de a pontos offszetek és a CTRL mezőkiosztás a QEMU-verzió és a CMSDK specifikáció függvényében ellenőrzendő (lásd a záró szakaszt).

/* lab-a070_isr.c  — Thumb-2 / Armv7-M ISA */
#include <stdint.h>

#define CMSDK_TIMER0_BASE     0x40000000UL   /* MPS2-AN385, CMSDK APB0 */
#define CMSDK_TIMER0_CTRL     (*(volatile uint32_t *)(CMSDK_TIMER0_BASE + 0x00))
#define CMSDK_TIMER0_RELOAD   (*(volatile uint32_t *)(CMSDK_TIMER0_BASE + 0x08))
/* Nincs külön INTCLR regiszter: az INTSTATUS-ba írt 1 törli a pendinget (write-1-to-clear). */
#define CMSDK_TIMER0_INTSTATUS (*(volatile uint32_t *)(CMSDK_TIMER0_BASE + 0x0C))

#define NVIC_ISER0           (*(volatile uint32_t *)0xE000E100UL)
#define TIMER0_IRQn          8u

#define TIMER_CTRL_ENABLE    (1u << 0)
#define TIMER_CTRL_IRQEN     (1u << 3)      /* bitpozíció: CMSDK spec. ellenőrizendő */

volatile uint32_t g_tick;       /* eseményszámláló: CSAK az ISR írja, monoton nő */
/* a főciklus által feldolgozott események; GDB-vel figyelhető */
volatile uint32_t g_events;

/* A szimbólumnév a vektortábla 0x60 offszetén lévő .word operandusa.   */
void TIMER0_IRQHandler(void)    /* nincs assembly trampolin, nincs prolog */
{
    CMSDK_TIMER0_INTSTATUS = 1u; /* 1. periféria pending-jelző törlése (write-1-to-clear) */
    g_tick++;                   /* 2. az esemény rögzítése; a főciklus ebből értesül */
}

int main(void)
{
    CMSDK_TIMER0_RELOAD  = 0x00FFFFFFu;         /* lassú, szemléltető periódus */
    CMSDK_TIMER0_CTRL    = TIMER_CTRL_ENABLE | TIMER_CTRL_IRQEN;
    /* esetleges korai pending törlése (write-1-to-clear) */
    CMSDK_TIMER0_INTSTATUS = 1u;
    NVIC_ISER0           = (1u << TIMER0_IRQn); /* engedélyezés, lásd a069 */

    uint32_t seen = 0;          /* a főciklus saját állapota: eddig látott események */
    for (;;) {
        uint32_t now = g_tick;  /* egyetlen igazított 32 bites olvasás */
        if (now != seen) {
            g_events += now - seen; /* az előző kör óta érkezett MINDEN esemény */
            seen = now;
        }
    }
}

A build és futtatás:

# ~/arm-labor/cm/a070 — target: qemu-system-arm (MPS2+ AN385)
# fájlok: lab-mps2.ld, lab-a070-startup.s (+ lab-a070-irq.inc), lab-a070_isr.c, lab-check-
# vectors.py
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -ffreestanding -nostdlib \
  -Wall -Wextra -g3 -O1 -T lab-mps2.ld \
  lab-a070-startup.s lab-a070_isr.c \
  -o lab-a070.elf
python3 lab-check-vectors.py lab-a070.elf 1=Reset_Handler 24=TIMER0_IRQHandler
qemu-system-arm -machine mps2-an385 -cpu cortex-m3 -nographic \
  -gdb tcp::3333 -S -kernel lab-a070.elf

A -S és a GDB-kapcsolat lehetővé teszi, hogy a main-be állított törésponton megállva ellenőrizzük a vektortábla tartalmát (x/32xw 0x00000000) és a keretet (info registers, majd a kezelő belépésekor x/8xw $sp). A g_tick változó növekedése a main ciklusából figyelhető meg — de csak akkor, ha a pending-jelző törlése megtörténik.

Várható viselkedés (ellenőrzési feltétel). A helyes változatban g_tick a megszakítási periódusnak megfelelő, kiszámított ütemben nő, és a main ciklus rendszeresen lefut (a g_events értéke időnként frissül). Hiba esetén: g_tick ~100%-ban nő, a g_events soha nem frissül — ez a törölt pending-jelző hiányának tipikus tünete. Megjegyzendő, hogy a ciklusszámra vonatkozó abszolút értékek QEMU-ban nem hardverhűek, ezért itt csak a relatív viselkedés (rendszeresen fut-e a főciklus) az elfogadási feltétel; a tényleges latenciamérést a Megszakítás-latencia mérése és korlátai tárgyalja.

Miért számláló, és miért nem jelző? Egy if (g_pending) { g_pending = 0; … } minta két művelet: olvasás, majd törlés. Ha a kettő közé befut egy újabb megszakítás, és 1-et ír a jelzőbe, a főciklus ezt a frissen beírt 1-et törli: az esemény elveszik. Ez akkor elfogadható, ha a szerződés csak összevont értesítést ígér („volt legalább egy esemény”); veszteségmentes eseményátvitelre nem az. A fenti minta ezt kerüli el. A g_tick-et egyetlen kontextus, az ISR írja, a főciklus csak olvassa, és a saját seen értékével számolja a különbséget. Nincs tehát olyan pont, ahol a főciklus az ISR írását felülírná, és a now - seen különbség az átfordulás után is helyes (modulo 2322^{32}). Determinisztikus próbával igazoltuk: ha egy második eseményt pontosan az olvasás és a törlés közé injektálunk, a jelzős minta 2 eseményből 1-et dolgoz fel, a számlálós mind a kettőt. A főciklus haladását a g_events mutatja; ez külön mérőpont, mert egy csak lokálisan írt, fel nem használt változót a fordító el is hagyhat.

Az igazított 32 bites g_tick olvasása Cortex-M3-on egyetlen LDR, ezért atomi: a főciklus nem láthat félig frissített értéket. Ez azonban csak az egyes hozzáférésre igaz. Egy több műveletből álló protokoll (olvasás–vizsgálat–törlés, vagy több változó együttes frissítése) ettől még nem atomi. A 64 bites vagy több szóból álló adat egyetlen olvasása sem az. Ilyenkor vagy egyetlen író mintát kell választani, mint itt, vagy rövid, a PRIMASK-ot megőrző kritikus szakaszt (Záróprojekt II).

17.3.7 Tipikus hibák és ellenpróbák

Tünet Hipotézis Adatgyűjtés Egy változtatás
A főciklus soha nem fut le a periféria pending-jelzőjének törlése hiányzik, vagy nem hat (rossz cím vagy bit) GDB: töréspont az ISR-ben, g_tick monotón nő; az INTSTATUS visszaolvasása a törlés után INTSTATUS = 1 (write-1-to-clear) a kivételből való visszatérés előtt (a példában az ISR elején)
Megszakítás nem indul el szimbólumnév nem egyezik a vektortábla operandusával, ezért a Default_Handler fut nm lab-a070.elf \| grep TIMER, x/32xw 0 a név és a .weak/.thumb_set összehangolása
HardFault az első megszakításkor a vektortáblába páros cím került; a bejegyzések nincsenek szóhatáron; áthelyezett (VTOR) táblánál a bázis nem éri el a szükséges igazítást GDB: x/2xw 0 (illetve a VTOR által mutatott cím) és az IPSR értéke; lab-check-vectors.py Thumb-bit és .align 2 a bejegyzésekre; áthelyezett táblánál a bázis legalább 128 bájtra és a táblaméretnél nem kisebb kettőhatványra igazítva (a064); a kanonikus 0x00000000 bázis ezt teljesíti
A főciklus nem látja a g_flag változást hiányzó volatile, a fordító regiszterben tartja objdump -d a main-en, a g_flag betöltése volatile minősítő hozzáadása
Ritkán, véletlenszerűen elszállt adat vagy elveszett esemény több műveletből álló protokoll (pl. jelző olvasása, majd törlése) vagy egy szónál szélesebb megosztott adat megszakítás közben GDB: az olvasás és az írás közötti utasítások; szándékosan a résbe injektált esemény egyetlen író, monoton számláló és főciklusbeli seen (mint a példában), vagy rövid, PRIMASK-ot megőrző kritikus szakasz

17.3.8 Feladatok

  1. (magyarázó) Az MPS2-AN385-ön az UART0 vételi (RX) megszakítása az IRQ0 vonalon érkezik; a TX az IRQ1, az IRQ12 pedig a UART0–2 összevont túlcsordulási vonala. Számítsd ki az UART0 RX vektortábla-bejegyzésének indexét és bájt-offszetét, és magyarázd el, milyen érték kerül a szóba akkor, ha a kezelőt UART0_Handler néven definiáljuk C-ben, és miért nem kell ehhez assembly kód.
  2. (implementációs) Egészítsd ki a példát úgy, hogy az ISR egy 64 elemű volatile uint32_t gyűrűpuff erbe írjon mintát, a főciklus pedig a puff erből olvasson. Az ISR maradjon tisztán C, assembly trampolin nélkül. Indokold, hol kell a pending-jelzőt törölni, és miért nem elegendő az NVIC_ICPR írása.
  3. (hibakeresési/mérési) Egy kolléga a példa ISR-jében a CMSDK_TIMER0_INTSTATUS = 1u; sort az ISR végére tette, és azt állítja, hogy emiatt a main ciklusa soha nem fut le, mert a megszakítás „viharként” újra és újra belép. Döntsd el, igaz-e ez a lassú, 0x00FFFFFF-es periódus és a háromsoros ISR mellett. Fogalmazz hipotézist a tünet valódi okára, tervezz GDB-alapú adatgyűjtést (mely regisztereket és memóriaterületeket vizsgálod), és javasolj egyetlen változtatást az ellenőrzéssel együtt.

17.3.9 Megoldások

1. megoldás. Táblaindex 16+0=1616 + 0 = 16, offszet 4⋅16=64=0x404 \cdot 16 = 64 = \text{0x40}. Ugyanezt a vonalat használja az UART-driver (UART0_IRQn = 0) és a Záróprojekt II vektortáblája (0x40: IRQ0 (UART0 RX)); a lab-check-vectors.py lab-uart.elf 16=UART0_Handler ellenőrzés ugyanezt a slotot vizsgálja. A szóba a UART0_Handler függvény címe kerül (a Thumb-bittel együtt, amit a szimbólum hordoz). Nem kell assembly, mert a hardveres automatikus context-mentés pontosan a caller-saved regiszterhalmazt menti, azaz a C fordító számára az ISR belépése egy szokásos függvényhívásnak látszik; a visszatérést a fordító epilógusa (bx lr vagy pop {..., pc}) végzi, amely az LR-ben kapott EXC_RETURN-t a PC-be juttatja.

2. megoldás. A periféria pending-jelzőjét a kivételből való visszatérés előtt kell törölni; a pontos helyet a periféria eseményprotokollja dönti el. A timernél nincs kiolvasandó adat, a törlés az ISR-ben bárhol állhat a visszatérés előtt (a példában az elején). Az UART vételénél a sorrend számít: előbb az INTSTATUS törlése, utána a DATA olvasása (UART-driver). Az NVIC_ICPR írása azért nem helyettesíti ezt, mert a kérés forrása a periféria jelzője: amíg az be van állítva, a kéréssor továbbra is aktív, és az NVIC pending bitje azonnal újra beáll. A gyűrűpuff erhez írás- és olvasásindexet külön kell kezelni (a volatile önmagában nem biztosít atomitást több mezőre), ezért az írásindex csak az ISR-ben módosul, az olvasásindex csak a főciklusban.

3. megoldás. Az állítás ebben a formában nem igaz. A CMSDK Timer0 megszakításkérése szintvezérelt: addig áll fenn, amíg az INTSTATUS jelző 1. Ha az ISR a jelzőt a kivételből való visszatérés előtt bárhol törli, a kérés a visszatéréskor már nem aktív, és az NVIC nem állítja újra pendingbe; a kezelő egyszer fut le eseményenként. Vihar csak akkor keletkezik, ha (1) a törlés egyáltalán nem történik meg vagy nem hat (rossz cím, rossz bit), (2) az ISR futása hosszabb, mint a timer periódusa, így a visszatérés előtt újabb esemény érkezik, vagy (3) a törlő írás a visszatérés pillanatában még nem ért el a perifériáig (írási puffer). Az utolsó esetre egy dsb a törlés után a szokásos óvintézkedés. Itt egyik sem áll fenn: a periódus 0x00FFFFFF ciklus, az ISR néhány utasítás, tehát a késői, de visszatérés előtti nyugtázás szabályos. Hipotézis a tünetre: a törlés nem hat (például rossz regisztercím), vagy hiányzik. Adatgyűjtés: töréspont a TIMER0_IRQHandler utolsó utasításán, ott az INTSTATUS (x/1xw 0x4000000C) és az IPSR kiolvasása, a g_tick és a g_events figyelése. Ha az INTSTATUS a törlés után is 1, a törlés nem hat. Egy változtatás: a törlő írás címének és bitjének javítása (szükség esetén dsb-vel utána). Ellenőrzés: a main ciklus g_events értéke nő.

17.3.10 Rövid összefoglaló

Az ISR regisztrálása pusztán szimbólum- és címkérdés: a vektortábla 4⋅(16+IRQn)4 \cdot (16 + \text{IRQn}) offszetén lévő szó a kezelő szimbólumának címét tartalmazza. Kivétel belépésekor a hardver automatikusan a verembe menti az R0–R3, R12, LR, PC és xPSR regisztereket (32 bájt), és az LR-be EXC_RETURN kódot tölt. Mivel ez pontosan a caller-saved halmaz, az egyszerű ISR tisztán C-ben megírható, assembly trampolin nélkül. A periféria pending-jelzőjét viszont az ISR-nek kell törölnie; elmaradása végtelen megszakítási vihart okoz.

Továbbhasznált definíciók: - ISR: hardveresen meghívott, EXC_RETURN-nel visszatérő kezelőrutin. - automatikus context-mentés: a caller-saved regiszterek és a folytatási állapot hardveres verembe írása kivétel belépésekor. - EXC_RETURN: az LR-be töltött fenntartott bitminta; ha Handler módban egy megengedett utasítás (bx lr, pop {..., pc}, ldm, ldr pc) a PC-be tölti, kivétel-kilépés történik.

17.3.11 Források és ellenőrzési hiányok

Az alábbiak dokumentációs kiindulópontok (forrásállapot: nem megnyitott, lásd az Olvasói útmutató jelöléseit):

A laborkönyvtár összeállítását a helyettesítő eszközlánccal (Clang + ld.lld) ellenőriztük: egy üres könyvtárban, kizárólag a Vektortábla, a Linker script alapjai, a Startup kód és ennek az alfejezetnek a fájljaiból, a fenti cp/cat/sed lépésekkel a labor fordul és linkelődik, és a lab-check-vectors.py a Reset- és a használt IRQ-bejegyzést is a várt szimbólumra mutatónak találja. A .rept blokk nélküli táblánál ugyanez az ellenőrzés HIBA [24]-gyel bukik. Ellenőrzési hiányok: (1) A CMSDK Timer0 regiszteroffszetek (INTSTATUS, RELOAD) és a CTRL ENABLE/IRQEN bitpozíciói a QEMU konkrét verziójától és a CMSDK specifikációtól függően ellenőrzendők — az itteni konstansok szemléltetők. (2) Az IRQ8 hozzárendelés és a perifériacímek a QEMU mps2-an385 machine definíciójából ellenőrzendők. (3) A -nographic és -kernel viselkedése, valamint a UART-alapú kimenet megjelenítése verziófüggő; a bemutatott parancssor várható, de nem e folyamatban validált. (4) A QEMU-emuláció ciklusviszonyai nem hardverhűek, ezért a példában csak a viselkedésalapú elfogadási feltétel szerepel, és minden ciklusszám-jellegű adat levezetettként, nem mértként jelölt.