19.1 Polling versus interrupt-vezérelt I/O

19.1.1 Motiváció

A qemu-system-arm -machine mps2-an385 gépen futó firmware-ünknek egy soros porton másodpercenként néhány bájtot kell fogadnia, közben 1 ms-os időzítéssel mintát kell vennie, és alkalmanként egy GPIO-lábon gombnyomást kell érzékelnie. A főciklus while (1) testében vagy ismételten beolvassuk a periféria állapotregiszterét, vagy a periféria jelez a processzornak, amikor teendő van. Az első megoldás néhány sorból áll, de a CPU-t folyamatosan lefoglalja. A második kevesebb ciklust pazarol, viszont bonyolultabb: megszakítás-prioritás, ISR-szerződés, megosztott állapot és versenyhelyzetek kerülnek a képbe. Ez az alfejezet a két stratégia számszerűsíthető tradeoffjait adja meg, hogy a hallgató egy konkrét feladatról önállóan el tudja dönteni, melyik illik rá — és a fejezet három konkrét esetére konkrét választ kap.

19.1.2 Definíció és működés

Polling (lekérdezéses I/O): a CPU egy ciklusban ismételten beolvassa egy periféria állapotregiszterét (pl. STATE RXBF bitjét), és addig nem lép tovább a cikluson, amíg a várt feltétel nem teljesül. A periféria sosem kezdeményez; kizárólag a szoftver kérdez.

Interrupt-vezérelt I/O: a periféria kérésére az NVIC kivételt vált ki, a processzor az aktuális utasításfolyamot félreállítja, elmenti a szükséges regisztereket, majd az ISR (interrupt service routine) fut le. A vezérlést nem a szoftver kérdező mintája, hanem a hardver eseménye indítja. A belépés latenciáját és összetevőit a Megszakítás-latencia mérése és korlátai tárgyalta.

Hybrid minta: a szoftver egy rövid, jól definiált ablakban pollol (például miután az első bájt megérkezéséről megszakítás értesített), majd a maradék bájtokat interrupt-vezérelten szedi ki a FIFO-ból. Ez a minta a burstös eseményekre ad jó választ.

CPU-kihasználtság: az az arány, ameddig a CPU hasznos munkát végez az összes eltelt ciklushoz képest. Polling esetén a várakozó ciklusok is elhasználódnak.

Energiafogyasztás: az órajel aktív tartományában a CPU dinamikus teljesítményfelvétele az aktív ciklusok arányával skálázódik; az WFI/WFE utasításokkal alvó állapotba vitt mag kevesebbet fogyaszt. Az interrupt-vezérelt minta ezt a lehetőséget természetesen kihasználja, a tiszta polling nem.

A polling és a megszakítás nem ellentétpár, hanem a reakcióidő és az erőforrás-fogyasztás két különböző pontja ugyanazon a spektrumon.

19.1.3 A döntést befolyásoló szempontok

1) Latency (reakcióidő). Polling esetén a legrosszabb eset Lpollwc=TpollL_{poll}^{wc} = T_{poll}, ahol TpollT_{poll} a poll-ciklus periódusa (ideális esetben az az idő, amíg a while-test egy teljes iterációja újra a STATE olvasásához ér). Az átlagos késleltetés Lpollavg≈Tpoll/2L_{poll}^{avg} \approx T_{poll}/2, ha az esemény egyenletesen véletlen. Interrupt-vezérelt esetben a késleltetés lényegében a LentryL_{entry} belépési latencia plusz az ISR első utasításainak futási ideje. A polling ezt a paramétert tehát csak úgy tudja rövidíteni, ha a ciklusunk sűrűbb — ami arányosan több energiát és CPU-t visz el.

2) CPU-kihasználtság. A pollingnak két, külön számolandó modellje van. Folyamatos busy-poll esetén (mint a lenti poll_loop) a CPU minden várakozó ciklust elhasznál, a teher mindig a teljes órajel. Időszakos lekérdezés esetén a CPU TpollT_{poll} időközönként egyszer ellenőriz: TT idő alatt nagyjából T/TpollT/T_{poll} (darab) olvasás történik, akkor is, ha közben nem jött esemény. Interrupt-vezérelt esetben a processzor WFI-vel alhat, és csak eseményenként ébred: a pluszköltség eseményenként körülbelül Lentry+LISR+LexitL_{entry} + L_{ISR} + L_{exit}, ahol LexitL_{exit} a kilépési szekvencia. Nagy eseménysűrűségnél (N≫N \gg néhány ezer/másodperc) az interrupt-overhead eseményenként dominálhat, és polling olcsóbb lehet; ritka eseményeknél viszont az interrupt nyilvánvalóan nyer.

3) Energiafogyasztás. A két stratégia energiamérlege akkor nyílik szét igazán, ha a CPU-nak van alacsony fogyasztású alvó módja. A tiszta polling ciklus közben a mag aktív órajelet fogyaszt. Az interrupt-vezérelt minta tipikus szerkezete:

while (1) {
    process_pending_events();
    uint32_t saved = __get_PRIMASK();
    __disable_irq();
    if (!events_pending())
        __asm volatile ("dsb\n\twfi" ::: "memory");
    __set_PRIMASK(saved);
}

Az events_pending() az alkalmazás megosztott jelzőit vizsgálja; a CMSIS PRIMASK-műveletekkel védett újraellenőrzés és WFI megakadályozza az elveszett ébresztést. A főciklusba engedélyezett megszakításokkal lépünk.

A WFI (Wait For Interrupt) és a WFE (Wait For Event) részletek: az WFI bármely engedélyezett és prioritásban megfelelő megszakításra felébred; a WFE egy esemény-regiszter bitjére is (többprocesszoros szinkronizációhoz). Az alvó mag fogyasztása tipikusan töredéke az aktívénak, így ha az események ritkák, az interrupt-vezérelt minta energia szempontból egyértelműen kedvezőbb. Ha viszont az események sűrűn jönnek, az alvás és az ébredés közötti átmenetek tranzíciós veszteséget okoznak, és a CPU úgyis folyton fut.

4) Tervezési komplexitás. Az interrupt-vezérelt minta több hibalehetőséget hoz: megosztott állapotot kell védeni (kritikus szakasz), a prioritásokat helyesen kell beállítani (lásd NVIC konfiguráció: engedélyezés és prioritás), és figyelni kell arra, hogy az ISR rövid maradjon. A polling ezzel szemben lokálisan olvasható, szimulátorban egyszerűen követhető, és ismert ideig fut.

5) Determinisztikus időzítés. Valós idejű rendszerekben néha fontos, hogy a reakcióidő minden esetben ugyanannyi legyen. A polling ezt fix felső korláttal adja (a ciklus hossza), az interrupt LentryL_{entry}-je pedig worst-case analízist igényel (lásd a071). Egyik sem csodaszer: a mérnök feladata a feladat követelményéhez illeszteni.

19.1.4 Döntési táblázat a fejezet három esetére

A fejezet Egyszerű UART driver: inicializálás, küldés/vétel megszakítással és Timer-alapú időzítés és periodikus feladatok szakaszai három tipikus esetet mutatnak be; ezekre a döntés a következő.

Eset Eseménysűrűség Tipikus reakcióidő-igény Választandó stratégia Indoklás
UART RX, soronkénti bájt alacsony–közepes (10–10 000 B/s) néhány tíz µs interrupt (RXIE engedélyezve) ritka, kiszámíthatatlan; WFI-vel alhat a mag, alacsony CPU-teher
UART TX, folyamatos blokk magas (több 100 kB/s) ütemezett, tűrhető polling a küldési sor rövid ablakában, vagy DMA sűrű esemény; az interrupt-overhead eseményenként dominálhat
Timer-tick (pl. 1 kHz) ismert, periodikus kicsi, pontosan ismert interrupt (általában), polling csak ha a főciklus úgyis pontosan ekkor jár WFI integrálható; pontosan periodikus reakció; a főciklus nem garantáltan fut ekkor

A táblázat nem recept, hanem irányadó: a konkrét számok (ciklusidő, ISR-hossz, kívánt latencia) mindig felülírhatják.

19.1.5 Kidolgozott példa: UART RX polling vs interrupt

Munkakönyvtár: ~/arm-labor/cm/a075/ — 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/a075
cd ~/arm-labor/cm/a075

A példa a QEMU mps2-an385 machine CMSDK UART0 perifériáját használja. A báziscíme a QEMU/ARM MPS2 AN385 dokumentációja szerint 0x40004000; a regiszter-offszetek és a STATE/CTRL bitmezők a CMSDK UART dokumentációjából valók, és ellenőrizendők — a gyakorlatokban a hallgatónak a QEMU mps2-an385 forrásában vagy a hivatalos MPS2 dokumentációban vissza kell igazolnia. Az itt szereplő konstansok az elv bemutatására szolgálnak.

#include <stdint.h>
#include <stddef.h>

#define UART0_BASE  0x40004000u
typedef struct {
    volatile uint32_t DATA;       /* +0x00: olvasás RX adat, írás TX adat */
    volatile uint32_t STATE;      /* +0x04 */
    volatile uint32_t CTRL;       /* +0x08 */
    volatile uint32_t INTSTATUS;  /* +0x0C */
    volatile uint32_t BAUDDIV;    /* +0x10 */
} cmsdk_uart_t;

#define UART0 ((cmsdk_uart_t *)UART0_BASE)
#define STATE_RXBF   (1u << 1)   /* CMSDK RXFULL; bit0: TXFULL */
#define CTRL_RXEN    (1u << 1)
#define CTRL_RXIE    (1u << 3)

Polling változat. A főciklus minden iterációban megkérdezi a STATE-et; a bájtot a DATA beolvasása adja vissza (a RXBF tipikusan ezzel törlődik).

A process_rx_byte(b) az alkalmazás bájtfeldolgozó művelete. A polling közvetlenül hívja; az interrupt-változatban a főciklus fogyasztja az SPSC körpuffert, a telítettséget pedig rx_overrun számolja.

void uart_poll_forever(void) {
    while (1) {
        if (UART0->STATE & STATE_RXBF) {
            uint8_t b = (uint8_t)UART0->DATA;
            process_rx_byte(b);
        }
        /* más feladatok is futhatnak, de a CPU sosem alszik */
    }
}

A megfelelő betöltés–teszt–elágazás ciklus assembly-ben (Thumb-2, Cortex-M3):

/* Cortex-M3 (Thumb-2), polling wait loop.  A pontos címek ellenőrizendők. */
    .syntax unified
    .thumb
    ldr     r1, =0x40004004      @ UART0 STATE
    ldr     r2, =0x40004000      @ UART0 DATA
poll_loop:
    ldr     r3, [r1]             @ STATE betöltése (volatile olvasás)
    tst     r3, #2               @ RXFULL, a STATE bit1
    beq     poll_loop            @ nincs adat: újra
    ldrb    r3, [r2]             @ adat olvasása; RXBF törlése
    b       poll_loop

Interrupt-vezérelt változat. Az ISR egy soros portonkénti bájtot emel ki, és visszatér. A főciklus WFI-vel alhat.

static volatile uint8_t rx_buf[64];
static volatile uint32_t rx_head, rx_tail, rx_overrun;

/* Az attribútum Armv7-M-en érvényes, de nem kötelező (lásd a068): a GCC itt
   a kivételtípust figyelmen kívül hagyja, és csak azt kezeli, hogy a kezelő
   4 bájtra igazított SP-vel is meghívódhat. CCR.STKALIGN = 1 mellett a sima
   void uart0_isr(void) deklaráció is helyes. */
void uart0_isr(void) __attribute__((interrupt("IRQ")));

void uart0_isr(void) {
    if (UART0->INTSTATUS & (1u << 1)) {
        UART0->INTSTATUS = 1u << 1;       /* nyugtázás a DATA olvasása előtt */
        uint8_t b = (uint8_t)UART0->DATA;
        uint32_t next = (rx_head + 1u) & 63u;
        if (next == rx_tail) rx_overrun++;
        else {
            rx_buf[rx_head] = b;
            rx_head = next;
        }
    }
}

void uart_init_irq(void) {
    UART0->INTSTATUS = 1u << 1;
    UART0->CTRL |= CTRL_RXEN | CTRL_RXIE;
    /* NVIC_EnableIRQ(UART0_IRQn); lásd [NVIC konfiguráció](#a069) */
}

void drain_pending_events(void) {
    while (rx_tail != rx_head) {
        uint8_t b = rx_buf[rx_tail];
        rx_tail = (rx_tail + 1u) & 63u;
        process_rx_byte(b);
    }
}

void main_loop(void) {
    for (;;) {
        drain_pending_events();
        uint32_t saved = __get_PRIMASK();
        __disable_irq();
        if (rx_tail == rx_head)
            __asm volatile ("dsb\n\twfi" ::: "memory");
        __set_PRIMASK(saved);
    }
}

Elméleti összehasonlítás. A mennyiségek és mértékegységeik:

Jel Jelentés Mértékegység
fcpuf_{cpu} órajel-frekvencia ciklus/s
NN eseménysűrűség (beérkező bájtok) esemény/s
TpollT_{poll} két lekérdezés közötti idő (időszakos modell) s
cpollc_{poll} egy lekérdezés teljes költsége (ha egy timer ütemezi, az ébresztésé is) ciklus
cISR=Lentry+LISR+Lexitc_{ISR} = L_{entry} + L_{ISR} + L_{exit} az ISR teljes költsége eseményenként: belépés, törzs, kilépés ciklus/esemény
CC aktív CPU-teher ciklus/s

A WFI-ben töltött idő nem aktív ciklus. A három modell terhe:

Cbusy=fcpu,Cpoll=cpollTpoll,Cirq=N⋅cISR.C_{busy} = f_{cpu}, \qquad C_{poll} = \frac{c_{poll}}{T_{poll}}, \qquad C_{irq} = N \cdot c_{ISR}.

Egységvizsgálat: CbusyC_{busy} maga az órajel, ciklus/s. CpollC_{poll}: (ciklus/lekérdezés) / (s/lekérdezés) = ciklus/s. CirqC_{irq}: (esemény/s) · (ciklus/esemény) = ciklus/s. A három teher tehát közvetlenül összevethető, és fcpuf_{cpu}-val osztva a CPU-idő arányát adja.

Megtérülési pont (időszakos lekérdezés vs. interrupt). A két teher ott egyenlő, ahol N*⋅cISR=cpoll/TpollN^* \cdot c_{ISR} = c_{poll}/T_{poll}, azaz

N*=cpollTpoll⋅cISR[cikluss⋅ciklus/esemény=eseménys].N^* = \frac{c_{poll}}{T_{poll} \cdot c_{ISR}} \qquad \left[\frac{\text{ciklus}}{\text{s} \cdot \text{ciklus/esemény}} = \frac{\text{esemény}}{\text{s}}\right].

N<N*N < N^* esetén az interrupt, N>N*N > N^* esetén az időszakos lekérdezés használ kevesebb aktív ciklust. Az időszakos modell csak addig érvényes, amíg N≤1/TpollN \le 1/T_{poll}: egy egybájtos vételi regiszternél ennél sűrűbb eseményeknél a lekérdezés bájtot veszít, a latenciakövetelményről nem is beszélve. A folyamatos busy-poll külön eset: az mindig fcpuf_{cpu} terhet jelent, ezért ciklusszámban csak akkor éri meg, ha az interrupt is a teljes CPU-t kitöltené, azaz N≥fcpu/cISRN \ge f_{cpu}/c_{ISR}. Előnye a legrövidebb reakcióidő, nem a CPU-takarékosság.

Az alábbi illusztratív számok hipotetikusak: fcpu=25MHz=25⋅106f_{cpu}=25\,\text{MHz} = 25 \cdot 10^{6} ciklus/s, Tpoll=50μsT_{poll}=50\,\mu s, cpoll=6c_{poll}=6 ciklus, cISR=30c_{ISR}=30 ciklus/esemény, N=1000N=1000 esemény/s. A busy-poll egy iterációja szintén 6 ciklus. A belépési latencia a Megszakítás-latencia mérése 12 ciklusos referenciaértéke.

Mutató Folyamatos busy-poll Időszakos lekérdezés (Tpoll=50μsT_{poll} = 50\,\mu s) Interrupt
Aktív CPU-teher 25⋅10625 \cdot 10^{6} ciklus/s 6ciklus/50⋅10−6s=1,2⋅1056\ \text{ciklus} / 50 \cdot 10^{-6}\ \text{s} = 1{,}2 \cdot 10^{5} ciklus/s 1000esemény/s⋅30ciklus/esemény=3,0⋅1041000\ \text{esemény/s} \cdot 30\ \text{ciklus/esemény} = 3{,}0 \cdot 10^{4} ciklus/s
CPU-idő aránya (C/fcpuC/f_{cpu}) 100 % 1,2⋅105/25⋅106=0,481{,}2 \cdot 10^{5} / 25 \cdot 10^{6} = 0{,}48 % 3,0⋅104/25⋅106=0,123{,}0 \cdot 10^{4} / 25 \cdot 10^{6} = 0{,}12 %
Átlagos reakcióidő fél iteráció: 3ciklus/fcpu=0,12μs3\ \text{ciklus} / f_{cpu} = 0{,}12\,\mu s Tpoll/2=25μsT_{poll}/2 = 25\,\mu s belépési latencia: 12ciklus/fcpu≈0,48μs12\ \text{ciklus} / f_{cpu} \approx 0{,}48\,\mu s
Eseményenkénti feldolgozási CPU-idő – – cISR/fcpu=30/25⋅106s=1,2μsc_{ISR}/f_{cpu} = 30 / 25 \cdot 10^{6}\ \text{s} = 1{,}2\,\mu s (teljes ISR-költség, nem latencia)
Alvás lehetséges? nem csak a lekérdezések között, timer-ébresztéssel igen (WFI)

A 1,2μs1{,}2\,\mu s az ISR teljes költsége (belépés + törzs + kilépés), ezért nem reakcióidő. A reakcióidőt csak a belépés és az ISR első utasításai határozzák meg.

Illusztratív, számított példa: aktív CPU-idő aránya - Hipotetikus, levezetett érték: f_cpu = 25 MHz, c_poll = 6 ciklus, T_poll = 50 µs, c_ISR = 30 ciklus/esemény, N = 1000 esemény/s. Nem mért adat, QEMU-emulációban sem hardverhű.

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

19.1.7 Feladatok

1. (magyarázó) Soroljon fel három konkrét mérnöki szempontot, amelyben a polling és az interrupt-vezérelt I/O különbözik egy Cortex-M rendszerben, és mindegyikhez adjon egy példát, amikor a polling a jobb választás.

2. (implementációs) Az mps2-an385 CMSDK UART0 perifériáján írjon rövid C programot, amely lab-uart-poll.c néven polling alapján fogad bájtokat, és lab-uart-irq.c néven ugyanazt interrupt-vezérelten. Mindkettőben tegye láthatóvá a rx_head számlálót egy GPIO-n vagy printf-fel (ha a félig hosztolt mód elérhető), hogy össze lehessen hasonlítani.

3. (mérési/hibakeresési) Tegyük fel, hogy cISR=30c_{ISR}=30 ciklus/esemény, és egy lekérdezés cpoll=6c_{poll}=6 ciklus. Számítsa ki azt az N*N^* eseménysűrűséget (esemény/s), ahol az időszakos lekérdezés (Tpoll=20μsT_{poll}=20\,\mu s) és az interrupt ugyanannyi aktív CPU-ciklust használ másodpercenként, ha fcpu=25MHzf_{cpu}=25\,\text{MHz}. Minden köztes eredmény mellé írja ki a mértékegységet. Mi a helyzet a folyamatos busy-poll-lal? Ezután GDB és DWT_CYCCNT (lásd a071) segítségével igazolja, hogy az interrupt-overhead valóban néhány tíz ciklus nagyságrendű. Jegyezze meg, mennyiben tér el a QEMU-eredmény a fizikai Cortex-M3-tól.

19.1.8 Megoldások

1. Szempontok: (a) latencia — pollingnál a poll-periódustól függ, interruptnál a belépési latenciától; (b) CPU-kihasználtság — polling folyamatosan foglal, interrupt csak eseményenként; (c) energiafogyasztás — WFI csak interrupt-vezérelt szerkezetben hasznos. A polling jobb például gyors burstös kommunikációban, ahol a CPU úgyis folyton dolgozik, vagy nagyon rövid, garantáltan TpollT_{poll}-n belül befejeződő szekvenciákban (pl. soros konfigurációs regiszterek kiírása).

2. A polling változat lényeg a while ciklus; az interrupt változatban UART0->CTRL |= CTRL_RXEN | CTRL_RXIE, az ISR neve/címe a vektortáblában legyen, NVIC_EnableIRQ a [NVIC konfiguráció](a069.html) szerint. Ellenőrzés: objdump -d mutassa az ISR-t és a vektorbejegyzést; GDB breakpoint az ISR elején. Ha a DATA olvasása után nem törlődik az RXBF, az ISR újra belép — ezt a STATE-tel kell igazolni.

3. Az időszakos lekérdezés terhe cpoll/Tpoll=6ciklus/(20⋅10−6s)=300000c_{poll}/T_{poll} = 6\ \text{ciklus} / (20 \cdot 10^{-6}\ \text{s}) = 300\,000 ciklus/s, ami fcpuf_{cpu}-hoz képest 300000/25⋅106=1,2300\,000 / 25 \cdot 10^{6} = 1{,}2 %. A megtérülési pont N*=(cpoll/Tpoll)/cISR=300000ciklus/s/30ciklus/esemény=10000N^* = (c_{poll}/T_{poll}) / c_{ISR} = 300\,000\ \text{ciklus/s} / 30\ \text{ciklus/esemény} = 10\,000 esemény/s. Ez belül van az időszakos modell érvényességi határán (1/Tpoll=500001/T_{poll} = 50\,000 esemény/s). 1000010\,000 esemény/s alatt az interrupt, fölötte a 20 µs-os lekérdezés használ kevesebb aktív ciklust. A fcpuf_{cpu} ebben a számításban nem szerepel; csak a százalékos arányhoz kell. A folyamatos busy-poll mindig 25⋅10625 \cdot 10^{6} ciklus/s, vagyis 100 %. Ciklusszámban csak N≥fcpu/cISR=25⋅106/30≈8,3⋅105N \ge f_{cpu}/c_{ISR} = 25 \cdot 10^{6} / 30 \approx 8{,}3 \cdot 10^{5} esemény/s fölött versenyképes, amikor az interrupt is kitöltené a CPU-t; az előnye a reakcióidő. A hallgató a számítás után a GDB-vel és DWT_CYCCNT-vel ellenőrizheti a nagyságrendet; QEMU-ban a mért értékek relatív összehasonlításra jók, hardverhű abszolút ciklusszámra nem (lásd a fejezet korlátait).

19.1.9 Rövid összefoglaló

A polling és az interrupt-vezérelt I/O két különböző tervezési pont a reakcióidő–CPU-teher–energiafogyasztás háromszögben. A polling egyszerű és determinisztikus felső korlátú, de aktív ciklusokat emészt, és csak a poll-periódus felbontásával érzékel. Az interrupt-vezérelt I/O alacsony latenciát és WFI-alvást tesz lehetővé, cserébe ISR-szerződést, prioritáskezelést és versenyhelyzet-védelmet kíván.

Továbbhasznált definíciók: - polling: a CPU ismételten lekérdezi egy periféria állapotregiszterét, és addig vár, amíg a feltétel nem teljesül. - interrupt-vezérelt I/O: a periféria kérésére az NVIC kivételt vált ki, és az ISR kezeli az eseményt. - hybrid minta: rövid ideig tartó polling egy interrupt-értesítést követő ablakban.

19.1.10 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):

Ellenőrzési hiányok: (1) a CMSDK UART STATE/CTRL pontos bitmezői és a RXBF törlésének szabálya (olvasással vagy explicit írással) a QEMU mps2-an385 forrásában és a hivatalos MPS2 dokumentációban ellenőrizendő; (2) a WFI/WFE pontos ébredési feltételei és a QEMU ezzel kapcsolatos esetleges eltérései a fizikai Cortex-M3-hoz képest; (3) az L_{entry} tipikus ciklusszáma az a071-ben jelzett források szerint; (4) a táblázatban szereplő N=1000 B/s-os példa és a hozzá tartozó ciklusszámok hipotetikus illusztrációk, nem mért értékek.