19.3 Timer-alapú időzítés és periodikus feladatok

19.3.1 Motiváció

Az előző alfejezetben (Egyszerű UART driver: inicializálás, küldés/vétel megszakítással) a vétel már megszakítás-vezérelt volt, de a kimenet és a feladatütemezés továbbra is polling-alapú maradt. Egy tipikus firmware-ben viszont nemcsak eseményre kell reagálni, hanem időben is: szenzort kell periodikusan mintavételezni, LED-et villogtatni, watchdogot rúgni, állapotgépet léptetni. Az első, naiv megoldás a busy-wait késleltetés, amely a CPU-t teljesen lefoglalja a várakozás alatt. Ez a mérnöki döntés azonban közvetlenül mérhető költséggel jár: a processzor nem tud más feladatot végezni, az energiafogyasztás nem csökken, és a késleltetés nem skálázódik. A timer-megszakítás-alapú (timer interrupt-driven) időzítés ezzel szemben a periódusos eseményt a hardverre bízza, és a CPU-t csak a megszakításkezelő rutin (ISR) futása idejére köti le.

Ebben az alfejezetben megírjuk a CMSDK APB timer inicializáló rutinját és megszakításkezelőjét, összekötjük a minimal runtime vektortáblájával, majd levezetjük és numerikusan összevetjük a két késleltetési stratégia CPU-kihasználtságát.

19.3.2 Definíció: timer-periféria, periódus, periodikus feladat, busy-wait

Timer-periféria (timer peripheral): memóriába leképzett (memory-mapped) számlálóegység, amely egy programozható kezdőértékről indulva minden órajel-ciklusban (vagy előosztóval osztott ciklusban) csökkenti a számlálóját; amikor nulla alá csökken, újratölti magát a kezdőértékből, és – ha engedélyezett – megszakítást kér.

Periódus (TT): két egymást követő megszakításkérés (illetve számláló-újratöltés) között eltelt idő:

T=RELOAD+1ftimerT = \frac{\text{RELOAD} + 1}{f_{\text{timer}}}

ahol ftimerf_{\text{timer}} a timer órajel-frekvenciája [Hz], RELOAD pedig a betöltési regiszterbe írt érték. A +1+1 onnan ered, hogy a számláló a RELOAD értéktől nulláig bezárólag számol.

Periodikus feladat (periodic task): olyan műveletsor, amelyet a rendszer fix TT időközönként, a feladat előző futásától függetlenül hajt végre. A timer-megszakítás ezt a kiváltó eseményt szolgáltatja; a feladat maga az ISR-ben vagy egy ISR által jelzett háttérkontextusban fut.

Busy-wait késleltetés: olyan várakozás, amely egy ciklusban aktívan fogyasztja a CPU-ciklusokat, amíg a kívánt idő el nem telik; eközben más munka nem végezhető el.

Fontos terminológiai szétválasztás: a timer a hardver objektum, az időzítés a periódus számszerű előírása, a periodikus feladat pedig a végrehajtandó logika. A QEMU mps2-an385 machine-en a timer-periféria a CMSDK (Cortex-M System Design Kit) része, tehát a regiszterszerződés nem az ARM A-profile architektúrából, hanem a platform leírásából származik.

19.3.3 A CMSDK APB timer alapregiszterei

A timer csak néhány regiszterből áll; ezek adják a teljes programozási felületet. A báziscím az ARM MPS2 referenciadokumentációjában rögzített perifériatartományból származik (itt TIMER0), és a QEMU mps2-an385 machine memóriatérképén ellenőrizendő.

Offset Regiszter Irány Szerep
0x000 CTRL R/W engedélyezés, megszakítás-engedélyezés
0x004 VALUE R/W aktuális számlálóérték; írása az aktuális értéket állítja be
0x008 RELOAD R/W újratöltési érték; írása az aktuális értéket is beállítja
0x00C INTSTATUS R/W olvasás: megszakítás-státusz (bit 0: függő megszakítás); írás: 1 a megfelelő bitre törli a kérést (write-1-to-clear) — nincs külön INTCLR regiszter

A CTRL releváns bitjei (a részletes bitmezőket a platform specifikációjából kell visszaigazolni):

Bit Név Jelentés
0 ENABLE a számláló fut / áll
3 IRQEN engedélyezi a megszakításkérést a nullaátlépéskor

Ennél a CMSDK APB timernél nincs külön mód-választó bit: a számláló mindig automatikusan újratöltődik a RELOAD értékből, amikor eléri a nullát, azaz a viselkedés mindig a fentebb „periodikusnak” nevezett mintát követi, szabadon futó (wrap) mód nélkül. A konkrét periféria fontos mellékhatása (CMSDK műszaki leírás, 4-4. táblázat): a RELOAD írása az aktuális értéket (VALUE) is beállítja. Ezért az inicializálásban a RELOAD beírása után az első periódus is pontosan RELOAD+1\text{RELOAD}+1 ciklus, külön VALUE-írás nélkül. A VALUE írása tudatos külön lépés lehet (például ha az első periódust szándékosan rövidebbre akarjuk), de az első periódus helyességéhez nincs rá szükség. Ugyanez a mellékhatás azt is jelenti, hogy egy futó timer RELOAD-jának átírása azonnal újraindítja az aktuális periódust. Más időzítők (például a SysTick, ahol a SYST_RVR írása nem tölti a számlálót) másként viselkednek, ezért a sémát mindig a konkrét periféria leírásából kell venni.

19.3.4 Kidolgozott példa: timer inicializálás, ISR és periodikus feladat

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

A regiszterabsztrakciót a korábban bevezetett struktúra-mintával végezzük (Periféria-regiszter absztrakció C struktúrával). A startup és a linker script a II. rész kanonikus párja (a065, a066); a vektortáblát a Timer0 bejegyzésével bővítjük:

cp ~/arm-labor/cm/a065/lab-mps2.ld .
cp ~/arm-labor/cm/a066/lab-startup.s lab-a077-startup.s
cp ~/arm-labor/cm/a064/lab-check-vectors.py .
cat > lab-a077-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-a077-irq.inc' lab-a077-startup.s   # a SysTick sor után
# gyenge alias: kezelő nélkül is linkelhető
echo '    lab_default_handler TIMER0_IRQHandler' >> lab-a077-startup.s

A két változat ugyanazt a bővített startupot használja, de TIMER0_IRQHandler-t csak az IRQ-s C-fájl definiálja. Ezért az echo sor a kanonikus lab_default_handler makróval gyenge, Default_Handler-re mutató aliast ad hozzá. Az IRQ-s buildben az erős C-definíció felülírja, a busy-wait buildben a vektortábla IRQ8-szava a Default_Handler-re mutat. A busy-wait változat a Timer0 megszakítását nem engedélyezi, így ez a vektor ott nem is hívódik. Alias nélkül a busy-wait linkelése undefined symbol: TIMER0_IRQHandler hibával állna le, mert a .vectors szekciót a linker KEEP-pel megtartja.

A két változat teljes forráslistája:

Változat Források IRQ8-vektor (24. index)
IRQ-s lab-a077-startup.s, lab-a077-irq.c, lab-a077-common.c (+ lab-a077-timer.h) TIMER0_IRQHandler (erős definíció a lab-a077-irq.c-ben)
busy-wait lab-a077-startup.s, lab-a077-busywait.c, lab-a077-common.c (+ lab-a077-timer.h) Default_Handler (a gyenge alias célja)

A két összehasonlítandó program ugyanazt a feladatot végzi: 10 ms-onként egy tick; minden tickben ugyanaz a rögzített mennyiségű „hasznos munka” (lab_work_step); és minden 100. tickben (1 s) ugyanaz az állapotváltozás és ugyanaz a kimeneti sor (lab_tick_task). A különbség csak az, hogy a tick honnan jön: a timer-megszakításból vagy egy busy-wait késleltetésből. A közös rész:

/* lab-a077-timer.h — CMSDK APB timer és a két változat közös része */
#include <stdint.h>

typedef struct {
    volatile uint32_t CTRL;       /* 0x000 */
    volatile uint32_t VALUE;      /* 0x004 — írás: az aktuális érték beállítása */
    volatile uint32_t RELOAD;     /* 0x008 — írás: a RELOAD ÉS az aktuális érték is */
    volatile uint32_t INTSTATUS;  /* 0x00C — olvasás: INTSTATUS; írás: INTCLEAR (W1C) */
} cmsdk_timer_t;

#define TIMER0_BASE  ((uintptr_t)0x40000000u)
#define TIMER0       ((cmsdk_timer_t *)TIMER0_BASE)
#define TIMER0_IRQ   8u

#define TIMER_CTRL_ENABLE   (1u << 0)
#define TIMER_CTRL_INTEN    (1u << 3)

/* A timer órajel-frekvenciája platformfüggő laborparaméter; 25 MHz illusztratív. */
#define TIMER_FREQ_HZ       25000000u
#define LAB_TICK_MS         10u        /* egy tick */
#ifndef LAB_TICKS_PER_LINE
#define LAB_TICKS_PER_LINE  100u       /* kimeneti periódus és mérési ablak: 100 tick = 1 s */
#endif

static inline uint32_t reload_for_ms(uint32_t ms)
{
    return (TIMER_FREQ_HZ / 1000u) * ms - 1u;   /* T = (RELOAD+1)/f */
}

extern volatile uint32_t g_ticks;    /* tickszámláló */
extern volatile uint32_t g_blink;    /* a periodikus feladat állapota */

void lab_uart_init(void);
void lab_uart_puts(const char *s);
void lab_tick_task(uint32_t tick);   /* AZONOS állapotváltozás és kimenet mindkét változatban */
void lab_work_step(void);            /* AZONOS hasznos munka tickenként mindkét változatban */
/* lab-a077-common.c — a két változat közös feladata */
#include "lab-a077-timer.h"

#define UART0_DATA    (*(volatile uint32_t *)0x40004000u)
#define UART0_STATE   (*(volatile uint32_t *)0x40004004u)
#define UART0_CTRL    (*(volatile uint32_t *)0x40004008u)
#define UART0_BAUDDIV (*(volatile uint32_t *)0x40004010u)

volatile uint32_t g_ticks;
volatile uint32_t g_blink;
static volatile uint32_t g_work_acc;

void lab_uart_init(void)
{
    UART0_BAUDDIV = 16u;
    UART0_CTRL    = 1u;                  /* TXEN */
}

void lab_uart_puts(const char *s)
{
    while (*s) {
        while (UART0_STATE & 1u) { }     /* amíg TXFULL = 1, várunk */
        UART0_DATA = (uint8_t)*s++;
    }
}

void lab_tick_task(uint32_t tick)
{
    if (tick % LAB_TICKS_PER_LINE == 0u) {   /* 100 tickenként (1 s): */
        g_blink ^= 1u;                       /*   állapotbillentés     */
        lab_uart_puts("tick\n");             /*   és egy kimeneti sor  */
    }
}

void lab_work_step(void)
{
    uint32_t acc = g_work_acc;               /* rögzített mennyiségű "hasznos munka" */
    for (uint32_t i = 0; i < 64u; i++)
        acc = acc * 1664525u + 1013904223u;
    g_work_acc = acc;
}

A timeres változat a polling és interrupt összevetésénél bemutatott versenymentes alvási mintát követi: a tickeket a főciklus dolgozza fel, és ha nincs feldolgozatlan tick, WFI-vel alszik. Az ISR a kérést mindenekelőtt az INTSTATUS/INTCLEAR címre írt 1-gyel nyugtázza, különben a megszakítás folyamatosan újra kérődik. A LAB_MEASURE ág DWT-vel három különböző mennyiséget mér (lásd a levezetést):

/* lab-a077-irq.c — timer-megszakításos változat, versenymentes alvással.
 * LAB_MEASURE = 1 mellett KÜLÖN méri (1) az ISR törzsidejét a DWT CYCCNT-tel,
 * (2) egy közös, LAB_TICKS_PER_LINE tickes ablak teljes idejét és (3) az
 * ugyanebben az ablakban alvással töltött időt. A (2) és (3) időalapja a
 * szabadon futó TIMER1: ugyanazon az APB-órán jár, mint az ébresztő TIMER0. */
#include "lab-a077-timer.h"

#ifndef LAB_MEASURE
#define LAB_MEASURE 1
#endif

#define DEMCR       (*(volatile uint32_t *)0xE000EDFCu)
#define DWT_CTRL    (*(volatile uint32_t *)0xE0001000u)
#define DWT_CYCCNT  (*(volatile uint32_t *)0xE0001004u)
#define TIMER1      ((cmsdk_timer_t *)(uintptr_t)0x40001000u)   /* időalap, IRQ nélkül */

static int g_have_dwt, g_have_t1;                 /* futásidőben eldöntve */
static volatile uint32_t g_isr_body, g_isr_n;     /* ISR-törzs összesen [CYCCNT-ciklus] */
static uint32_t g_sleep;                          /* alvás a futó ablakban [TIMER1-ciklus] */

void timer0_init_periodic(uint32_t reload)
{
    TIMER0->CTRL      = 0;          /* állj, megszakítás tiltva */
    TIMER0->INTSTATUS = 1;          /* függő kérés törlése (INTCLEAR, W1C) */
    TIMER0->RELOAD    = reload;     /* a RELOAD írása az aktuális értéket is beállítja,
                                       ezért az első periódus is (reload+1) ciklus */
    TIMER0->CTRL      = TIMER_CTRL_ENABLE | TIMER_CTRL_INTEN;
}

void TIMER0_IRQHandler(void)
{
    uint32_t t_in = g_have_dwt ? DWT_CYCCNT : 0u;   /* mérőpont: a törzs eleje */
    TIMER0->INTSTATUS = 1;          /* nyugtázás - ELŐBB, mint a munka */
    g_ticks++;
    if (g_have_dwt) {
        g_isr_body += DWT_CYCCNT - t_in;            /* mérőpont: a törzs vége */
        g_isr_n++;
    }
}

static int dwt_start(void)
{
    DEMCR    |= 1u << 24;           /* TRCENA */
    DWT_CYCCNT = 0;
    DWT_CTRL |= 1u;                 /* CYCCNTENA */
    __asm volatile ("dsb\n\tisb" ::: "memory");
    uint32_t before = DWT_CYCCNT;
    for (int i = 0; i < 64; i++) __asm volatile ("nop");
    return DWT_CYCCNT != before;    /* aktív futás alatt halad-e? (alvásról nem mond semmit) */
}

static int timebase_start(void)     /* TIMER1: szabadon futó, 32 bites lefelé számláló */
{
    TIMER1->CTRL   = 0;
    TIMER1->RELOAD = 0xFFFFFFFFu;
    TIMER1->CTRL   = TIMER_CTRL_ENABLE;              /* megszakítás nélkül */
    uint32_t before = TIMER1->VALUE;
    for (uint32_t limit = 100000u; limit; --limit)   /* korlátos várakozás az első lépésre */
        if (TIMER1->VALUE != before)
            return 1;
    return 0;
}

static void put_u32(uint32_t v)
{
    char buf[11], *p = buf + 10;
    *p = '\0';
    do { *--p = (char)('0' + v % 10u); v /= 10u; } while (v);
    lab_uart_puts(p);
}

/* Egy teljes ablak jelentése. win és sleep ugyanabból az időalapból és ugyanarra
 * a [kezdet, vég) ablakra vonatkozik, ezért 0 <= sleep <= win. */
static void report(uint32_t win, uint32_t sleep, uint32_t cyc)
{
    const uint32_t expect = LAB_TICKS_PER_LINE * (reload_for_ms(LAB_TICK_MS) + 1u);
    if (win < expect - expect / 10u) {          /* az ismert ébresztési időközhöz képest */
        lab_uart_puts("SKIP: a TIMER1 alvas kozben nem jar, alvasmeres nincs\n");
        return;
    }
    uint32_t per_mille = (win - sleep) / (win / 1000u);
    if (per_mille > 1000u) per_mille = 1000u;
    lab_uart_puts("ablak=");          put_u32(win);
    lab_uart_puts(" alvas=");         put_u32(sleep);
    lab_uart_puts(" aktiv_ezrelek="); put_u32(per_mille);
    if (g_have_dwt) {
        lab_uart_puts(" isr_torzs_atlag="); put_u32(g_isr_body / g_isr_n);
        lab_uart_puts(cyc + win / 10u < win ? " cyccnt_alvasban=megall"
                                            : " cyccnt_alvasban=jar");
    }
    lab_uart_puts("\n");
}

int main(void)
{
    lab_uart_init();
    g_have_dwt = LAB_MEASURE && dwt_start();
    g_have_t1  = LAB_MEASURE && timebase_start();
    if (LAB_MEASURE && !g_have_dwt)
        lab_uart_puts("SKIP: DWT CYCCNT nem halad, ISR-torzsmeres nincs\n");
    if (LAB_MEASURE && !g_have_t1)
        lab_uart_puts("SKIP: a TIMER1 idoalap nem jar, ablak- es alvasmeres nincs\n");

    timer0_init_periodic(reload_for_ms(LAB_TICK_MS));
    *(volatile uint32_t *)0xE000E100u = 1u << TIMER0_IRQ;   /* NVIC_ISER0 */

    uint32_t seen = 0, win_t1 = 0, win_cyc = 0;
    int have_win = 0;
    for (;;) {
        while (seen != g_ticks) {           /* minden elmaradt tickre ugyanaz a munka */
            seen++;
            lab_tick_task(seen);
            lab_work_step();
            if (g_have_t1 && seen % LAB_TICKS_PER_LINE == 0u) {
                uint32_t now = TIMER1->VALUE;
                uint32_t now_cyc = g_have_dwt ? DWT_CYCCNT : 0u;
                if (have_win)                /* az első, részleges ablakot eldobjuk */
                    /* lefelé számol; modulo 2^32 */
                    report(win_t1 - now, g_sleep, now_cyc - win_cyc);
                /* az új ablak kezdete: mindkét mennyiség nulláról */
                win_t1 = now;
                win_cyc = now_cyc;
                g_sleep = 0;
                have_win = 1;
            }
        }
        __asm volatile ("cpsid i" ::: "memory");   /* versenymentes alvás (a075) */
        if (seen == g_ticks) {
            uint32_t t0 = g_have_t1 ? TIMER1->VALUE : 0u;
            __asm volatile ("dsb\n\twfi" ::: "memory");
            if (g_have_t1) g_sleep += t0 - TIMER1->VALUE;   /* még az ISR előtt (PRIMASK = 1) */
        }
        __asm volatile ("cpsie i" ::: "memory");
    }
}

A busy-wait változat ugyanazt a ticket, kimenetet és munkát a CPU-val állítja elő:

/* lab-a077-busywait.c — busy-wait változat: ugyanaz a tick, kimenet és munka */
#include "lab-a077-timer.h"

/* Iterációk egy tickhez. Az iteráció költsége (subs + bne + nop, plusz a
   betöltések) nem egy ciklus, és fordítófüggő: LAB_DELAY_ITERS = T * f / c,
   ahol c-t a disassemblyből becsüljük, majd hardveren DWT-vel mérjük. */
#ifndef LAB_DELAY_ITERS
#define LAB_DELAY_ITERS  (TIMER_FREQ_HZ / 1000u * LAB_TICK_MS / 4u)   /* c = 4 feltételezés */
#endif

static void delay_busywait(uint32_t n)
{
    while (n--) {
        __asm volatile ("nop");     /* a volatile asm miatt a ciklus nem tűnik el */
    }
}

int main(void)
{
    lab_uart_init();
    for (;;) {
        delay_busywait(LAB_DELAY_ITERS);    /* ~10 ms, ha c helyes */
        g_ticks++;
        lab_tick_task(g_ticks);             /* ugyanaz, mint az IRQ-változatban */
        lab_work_step();
    }
}

A késleltető ciklus egy iterációja nem egy ciklus. A helyettesítő eszközlánccal (Clang -O1) a ciklusmag subs + nop + bne lett; Cortex-M3-on ez a vett ág pipeline-újratöltése miatt nagyjából 4 ciklus. Ezért a LAB_DELAY_ITERS alapértéke T⋅f/4=62500T \cdot f / 4 = 62\,500. Ez feltételezés, nem mérés: más fordító vagy kapcsoló más ciklusmagot ad, a pontos cc értéket hardveren DWT-vel kell megmérni. A busy-wait periódusa ráadásul nem 10 ms, hanem a késleltetés plusz a tick-feladat, a munka és a kiírás ideje, ezért a késleltetés kalibrálásához a teljes ciklushosszt kell mérni, nem a késleltetőét.

Build és futtatás:

CF="-mcpu=cortex-m3 -mthumb -ffreestanding -nostdlib -O1 -T lab-mps2.ld"
arm-none-eabi-gcc $CF -o lab-a077-irq.elf \
    lab-a077-startup.s lab-a077-irq.c lab-a077-common.c
arm-none-eabi-gcc $CF -o lab-a077-busywait.elf \
    lab-a077-startup.s lab-a077-busywait.c lab-a077-common.c
python3 lab-check-vectors.py lab-a077-irq.elf      1=Reset_Handler 24=TIMER0_IRQHandler
python3 lab-check-vectors.py lab-a077-busywait.elf 1=Reset_Handler 24=Default_Handler
qemu-system-arm -machine mps2-an385 -cpu cortex-m3 -nographic -kernel lab-a077-irq.elf
qemu-system-arm -machine mps2-an385 -cpu cortex-m3 -nographic -kernel lab-a077-busywait.elf

Kimenet. Mindkét változat másodpercenként egy tick sort ír, és ugyanannyi munkát végez tickenként. A QEMU alapúton a DWT CYCCNT nem halad (a071, a080), ezért ott az ISR-törzs mérése SKIP. Az ablak- és alvásmérés időalapja a TIMER1, amelyet az AN385 modellje tartalmaz; az ilyenkor kiírt értékek azonban a QEMU emulált idejét mutatják, nem hardverhű mérést:

SKIP: DWT CYCCNT nem halad, ISR-torzsmeres nincs
tick
ablak=… alvas=… aktiv_ezrelek=…
...

Mit mér a program, és mivel? A CYCCNT-et csak az aktív ISR-törzs mérésére használjuk. Az, hogy egy NOP-ciklus alatt halad, csak az aktív futásról mond valamit. Alvás közben a mag órajele a megvalósítás és az alvási mód szerint megállhat, és vele a számláló is: ilyenkor a WFI előtti és utáni különbség nem a falióra szerinti alvásidő. Ezért az alvást és a mérési ablakot egy olyan időalapból mérjük, amelynek alvás közben is járnia kell. Ez a szabadon futó TIMER1, ugyanazon az APB-órán, mint a TIMER0, amely a magot felébreszti. Hogy valóban jár-e, azt a program maga ellenőrzi: egy ablak teljes ideje az ismert ébresztési időköz, LAB_TICKS_PER_LINE · (RELOAD + 1). Ha a mért ablak ennél 10%-nál többel rövidebb, az időalap alvás közben nem járt, és a sor SKIP, nem 0 alvásidő. Működő DWT mellett a sor végén azt is jelzi, hogy a CYCCNT az ablak alatt a TIMER1-gyel együtt járt-e (cyccnt_alvasban=jar vagy megall); ez a választott számláló alvásbeli viselkedésének külön próbája. A 25 MHz-es AN385-ön a két óra azonos frekvenciájú, más panelen az arányukkal kell számolni.

Közös ablak. Az ablak és az alvas ugyanarra a [kezdet, vég) intervallumra vonatkozik: mindkettő a LAB_TICKS_PER_LINE-adik tick feldolgozásakor nullázódik és indul újra, és az első, részleges ablak eredményét a program eldobja. Az alvási szakaszok így mindig az ablak részintervallumai, ezért 0 ≤ alvas ≤ ablak, és az aktív hányad (aktiv_ezrelek = 1000 · (ablak − alvas) / ablak) 0 és 1000 között marad. A TIMER1 lefelé számol, az ablak kezdet − vég különbsége egyetlen átfordulásig modulo 2322^{32} helyes; 25 MHz-en ez 171 s, egy 1 s-os ablaknál bőven elég. Az aktív hányad a CPU-idő aránya, nem az energiafogyasztás mértéke: az alvási mód áramfelvétele és az ébredés költsége külön, hardveres mérést igényel.

Működő DWT-vel (hardveren) a sorok végén az isr_torzs_atlag=… és a cyccnt_alvasban=… mező is megjelenik. A helyettesítő eszközlánccal mindkét programot lefordítottuk, és a Unicorn-alapú tools/cm-run.py emulátorban futtattuk; ez a TIMER0 megszakítását kézbesíti, a TIMER1-et szabadon futó időalapként modellezi, a DWT viszont nem halad benne. A funkcionális tulajdonságokat így ellenőriztük. Az ablak pontosan 100 · 250 000 = 25 000 000 ciklus lett. -DLAB_TICKS_PER_LINE=200u-val kétszer ekkora, a normalizált aktiv_ezrelek változatlan. Az alvás közben megállított TIMER1-gyel (--timer1-stops-in-wfi) a sor SKIP: a TIMER1 alvas kozben nem jar…. Magát az aktív hányad számértékét az emulátor nem méri hardverhűen: utasításcsomagonként lépteti az időt. Fontos: a QEMU emulált ideje nem valós idejű; a periódusok tényleges hossza emulációban nem tekinthető hardverhű mérésnek, csak a viselkedés (megszakítás kiváltódik, ISR lefut, számláló nő) igazolható.

19.3.5 Levezetés: CPU-kihasználtság összevetése

A CPU-kihasználtság definíció szerint az adott feladatra fordított idő és a periódus aránya; itt mindkét mennyiség idő:

U=tfeladatT×100%U = \frac{t_{\text{feladat}}}{T} \times 100\%

A költségeket a továbbiakban ciklusban adjuk meg (CentryC_{\text{entry}}, CISRC_{\text{ISR}}, CexitC_{\text{exit}}, WW, P‾\bar{P}). A periódusra jutó elérhető ciklusszám T⋅fcpuT \cdot f_{\text{cpu}} (s · ciklus/s = ciklus), ezért ciklusokkal ugyanez az arány U=Cfeladat/(T⋅fcpu)U = C_{\text{feladat}} / (T \cdot f_{\text{cpu}}), mert tfeladat=Cfeladat/fcput_{\text{feladat}} = C_{\text{feladat}} / f_{\text{cpu}}. Egy képleten belül minden összeadandó azonos egységű: vagy mind ciklus, vagy mind idő. Időben megadott költséget előbb fcpuf_{\text{cpu}}-val szorozva ciklusra váltunk, és fordítva.

Busy-wait eset: a késleltetés teljes hosszában ciklusokat fogyaszt, tehát a CPU folyamatosan aktív:

Ubusy=100%U_{\text{busy}} = 100\%

A tickenkénti hasznos munka (lab_work_step) mindkét változatban ugyanannyi, ezért az összevetés a várakozás költségéről szól. Busy-wait esetén a várakozás alatt más munka nem végezhető, és a mag nem alhat. Ha egy iteráció cc ciklust vesz igénybe, az iterációszám N=T⋅fcpu/cN = T \cdot f_{\text{cpu}} / c. A cc nem becsülhető egyetlen NOP-ból: a fenti ciklusmagban három utasítás és egy vett ág van, és az értéket fordítóverzió és kapcsoló is befolyásolja.

Timer-megszakítás eset: két külön mennyiséget kell számolni. Az időzítés és a megszakítás többletköltsége csak a belépést, az ISR magját és a visszatérést tartalmazza:

Oirq=Centry+CISR+CexitT⋅fcpu×100%O_{\text{irq}} = \frac{C_{\text{entry}} + C_{\text{ISR}} + C_{\text{exit}}}{T \cdot f_{\text{cpu}}} \times 100\%

A teljes CPU-kihasználtsághoz hozzá kell adni a főciklus tickenkénti aktív munkáját is: a lab_work_step WW ciklusát és a tick-feldolgozás, valamint a kiírás tickenkénti átlagát (P‾\bar{P}; a tick sor csak minden 100. tickben íródik ki, ezért a költségét 100 tickre kell szétosztani):

Uirq=Centry+CISR+Cexit+W+P‾T⋅fcpu×100%U_{\text{irq}} = \frac{C_{\text{entry}} + C_{\text{ISR}} + C_{\text{exit}} + W + \bar{P}}{T \cdot f_{\text{cpu}}} \times 100\%

A számláló ciklus, a nevező ciklus, a hányados dimenziótlan; a százalékhoz 100-zal szorzunk.

A busy-wait változat ugyanezt a W+P‾W + \bar{P} munkát végzi, és a periódus maradékát pörgéssel tölti, ezért Ubusy=100%U_{\text{busy}} = 100\%, a várakozás többletköltsége pedig Obusy=(Tfcpu−W−P‾)/(Tfcpu)O_{\text{busy}} = (T f_{\text{cpu}} - W - \bar{P}) / (T f_{\text{cpu}}).

A 12 ciklus a Cortex-M3 implementáció adata ideális belépési körülmények között (nulla várakozási ciklusú memória, nincs folyamatban lévő többciklusos utasítás vagy buszhozzáférés). Nem az egész M-profile ISA általános garanciája: más mag, más memóriarendszer vagy tail-chaining más értéket ad. A kilépésre a modell szintén 12 ciklust feltételez; ez egyszerűsítés. Egyik szám sem tartalmazza az ISR utasításait.

Számpélda: megnevezett egyszerű költségmodell (levezetett, nem mért). Feltételezzük: ftimer=fcpu=25MHzf_{\text{timer}} = f_{\text{cpu}} = 25\,\text{MHz}, T=10msT = 10\,\text{ms}, belépés és kilépés 12–12 ciklus (a fenti modell), az ISR törzse 20–24 ciklus. Egy periódusra T⋅fcpu=250000T \cdot f_{\text{cpu}} = 250\,000 ciklus jut. A középértékkel

Centry+CISR+Cexit≈12+22+12=46 ciklus,C_{\text{entry}} + C_{\text{ISR}} + C_{\text{exit}} \approx 12 + 22 + 12 = 46 \text{ ciklus},

Oirq=46250000×100%=0,0184%.O_{\text{irq}} = \frac{46}{250\,000} \times 100\% = 0{,}0184\%.

A tartományt a törzsidő-feltevés adja: 20 ciklusnál 44/250000=0,0176%44/250\,000 = 0{,}0176\%, 24-nél 48/250000=0,0192%48/250\,000 = 0{,}0192\%. A 46 tehát ennek a modellnek az eredménye, nem egy mérésé. Ha egy teljes 10 ms-os várakozás előtt és után olvassuk a ciklusszámlálót, az eltelt ciklusszám ~250 000 lesz, akármilyen rövid az ISR. Az az eseményig eltelt időt méri, nem az ISR CPU-terhelését. A kettő külön mennyiség, és a fenti program ezért külön méri őket: az ISR-t közvetlenül körülvevő CYCCNT-mérőpontok a törzsidőt, a TIMER1 egy közös ablak teljes idejét és az ugyanebben az ablakban WFI-ben töltött időt. A törzsidő-mérés a hardveres be- és kilépést nem tartalmazza; azt a modell 12 + 12 ciklusa közelíti.

Ez a 0,0184% tehát a megszakítás modellbeli többletköltsége, nem a megszakításos program teljes CPU-terhe. A teljes terhet a közös munka határozza meg. Feltételezve (nem mérve), hogy a lab_work_step 64 iterációja iterációnként kb. 4 ciklus (W≈256W \approx 256), és a tick-feldolgozás a kiírással együtt tickenként átlagosan P‾≈10\bar{P} \approx 10 ciklus:

Uirq≈46+256+10250000×100%=312250000×100%=0,1248%≈0,125%.U_{\text{irq}} \approx \frac{46 + 256 + 10}{250\,000} \times 100\% = \frac{312}{250\,000} \times 100\% = 0{,}1248\% \approx 0{,}125\%.

Időben számolva ugyanez adódik: 312/(25⋅106ciklus/s)=12,48μs312 / (25 \cdot 10^6\ \text{ciklus/s}) = 12{,}48\,\mu\text{s}, és 12,48μs/10ms=0,1248%12{,}48\,\mu\text{s} / 10\,\text{ms} = 0{,}1248\%.

A busy-wait ugyanezen periódus alatt folyamatosan aktív (100%), a megszakításos változat a várakozás nagy részét alvással tölti. A teljes kihasználtság aránya ebben a feltételezett esetben kb. 100/0,125=800100 / 0{,}125 = 800, nem a többletköltségek 100/0,0184≈5400100 / 0{,}0184 \approx 5400-as aránya. Ha a lab_work_step iterációszámát százszorosára (6400-ra) növeljük, W≈25600W \approx 25\,600, és Uirq≈10,3%U_{\text{irq}} \approx 10{,}3\% lesz, miközben OirqO_{\text{irq}} változatlanul 0,0184%. A két metrika tehát mást mér: az OO az időzítési mód költsége, az UU a program teljes terhe.

Az alábbi ábra a három értéket veti össze; mivel a különbség extrém, a numerikus táblázat is szerepel, hogy a forrás szöveg önmagában is olvasható legyen.

Illusztratív, levezetett példa: CPU-terhelés 10 ms periódusnál - Levezetett, nem mért: 25 MHz feltételezett órajel, 46 ciklusos megszakítás-költségmodell (12 + 22 + 12), feltételezett W ≈ 256 és P ≈ 10 ciklus tickenkénti közös munka. QEMU-emulációban nincs hardverhű abszolút időzítés, ezért az érték illusztratív.
Késleltetési mód Mennyiség Ciklus / periódus Arány
Busy-wait teljes kihasználtság 250 000 100%
Timer-megszakítás teljes kihasználtság (feltételezett W+P‾≈266W + \bar{P} \approx 266) 46+266≈31246 + 266 \approx 312 kb. 0,125%
Timer-megszakítás csak a megszakítás többletköltsége 46 (modell; 44–48 a 20–24 ciklusos törzsfeltevéssel) 0,0184% (0,0176–0,0192%)

19.3.6 Mikor éri meg a timer-megszakítás extra komplexitása?

A timer-alapú megoldás nem ingyenes: extra regiszterkonfiguráció, vektortábla-bejegyzés, ISR-kód, megszakítás-latencia és annak szórása (jitter), valamint az osztott állapot (g_ticks) miatti versenyhelyzet-veszély. A döntési szempontok:

A Cortex-M magon a SysTick a maghoz tartozó periféria, de memóriába leképzett regiszterekkel: a System Control Space-ben, a 0xE000E010–0xE000E01F tartományban van (memóriatérkép). Ugyanezt a mintát kínálja beépítve (a SYST_RVR írása viszont nem tölti a számlálót, ezért ott a SYST_CVR törlése külön lépés); az itt tanult szerkezet (reload, engedélyezés, ISR, nyugtázás) a SysTick-re közvetlenül átvihető.

19.3.7 Kapcsolat a XIX. záróprojekthez

Ez a mintázat közvetlenül driverré alakul a Záróprojekt II: interrupt-vezérelt Cortex-M firmware-modul keretében: ott a timer-inicializálás, az ISR és a main ciklus közötti kapcsolat már a lab- előtagú driver interfészévé szerveződik (pl. timer_init(period_ms), timer_get_ticks()), a UART driverrel együtt egy időzített, megszakítás-vezérelt I/O-modulban. A mostani g_ticks számláló a projektbeli időbázis (tick clock) alapja lesz.

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

Tünet Hipotézis Adatgyűjtés Egy változtatás Ellenőrzés
Az ISR folyamatosan, azonnal újra lefut INTSTATUS-ba írt törlés hiányzik vagy a munka előtt/után rossz helyen van INTSTATUS olvasása; breakpoint az ISR elején INTSTATUS = 1 (write-1-to-clear) minden ISR-bevezetéskor Egy megszakítás → egy ISR-futás
Az ISR soha nem fut NVIC-engedélyezés hiányzik, vagy rossz IRQ-szám NVIC_ISER0 státusz; IRQ-szám ellenőrzése a machine-térképen helyes IRQ-engedélyezés ISR belép
HardFault az első megszakításkor a vektortábla bejegyzése nem a Thumb-kezelőre mutat objdump -s -j .vectors; a vektor LSB-je helyes szimbólum/réteg a vektorban megszakítás kezelt, nem HardFault
Az első periódus eltér a többitől a timert a RELOAD írása előtt engedélyeztük, így a számláló a régi aktuális értékről indult; vagy a RELOAD utáni szándékos VALUE-írás más értéket adott a CTRL és a RELOAD írásának sorrendje; VALUE visszaolvasása az engedélyezés előtt előbb RELOAD (ez az aktuális értéket is beállítja), utána CTRL az első periódus is a várt TT
A periódus „újraindul”, amikor futás közben módosítjuk a CMSDK timernél a RELOAD írása az aktuális értéket is felülírja VALUE olvasása a RELOAD-írás előtt és után a periódusváltást a tick-határhoz (az ISR-hez) igazítani a váltás utáni első periódus a teljes új TT
A busy-wait ciklus eltűnt volatile/__asm volatile hiánya, -O2 objdump a delay_busywait-en volatile a ciklusváltozón ciklus megmarad

19.3.9 Feladatok

  1. (magyarázó) Vezesd le, mekkora RELOAD érték ad 50 ms periódust ftimer=25MHzf_{\text{timer}} = 25\,\text{MHz} esetén. Magyarázd meg, mi történik az első periódussal, ha a RELOAD beírása után nem írod külön a VALUE-t, és mi történik, ha a timert a RELOAD írása előtt engedélyezed.
  2. (implementációs) Módosítsd a timer-inicializálást 1 Hz-es periódusra, és egészítsd ki az ISR-t úgy, hogy egy g_seconds számlálót növeljen. Számítsd ki levezetéssel az ISR-hez tartozó CPU-kihasználtságot, feltételezve ~46 ciklus ISR-költséget.
  3. (hibakeresési/mérési) Az ISR a vártnál kétszer sűrűbben fut. Írd le a tünet→hipotézis→adatgyűjtés→egy változtatás→ellenőrzés lépéseket, és add meg, melyik regisztert olvasnád vissza gdb-vel a hipotézis igazolásához.
  4. (mérési, összehasonlító) Készíts mérést, amely a timer-IRQ változatban külön méri az ISR törzsidejét (DWT CYCCNT), valamint egy közös ablak teljes idejét és az abban alvással töltött időt (alvás közben is járó időalappal), a busy-wait változatban pedig a teljes ciklushosszt (késleltetés + munka + kiírás). Ha a CYCCNT nem halad (QEMU-alapút), a mérés eredménye SKIP, nem nulla.

19.3.10 Megoldások

  1. T=50ms=0,05sT = 50\,\text{ms} = 0{,}05\,\text{s}, T⋅f=1250000T \cdot f = 1\,250\,000 ciklus, tehát RELOAD=1250000−1=1249999\text{RELOAD} = 1\,250\,000 - 1 = 1\,249\,999. Ha a RELOAD beírása után nem írod külön a VALUE-t, semmi nem változik: a CMSDK timernél a RELOAD írása az aktuális értéket is beállítja, ezért az első periódus is pontosan 12500001\,250\,000 ciklus. Ha viszont a timert a RELOAD írása előtt engedélyezed, a számláló a korábbi aktuális értékről indul. Ilyenkor az első megszakítás a régi érték szerint jön, amíg a RELOAD írása újra nem tölti, utána minden periódus pontos. (Egy olyan időzítőnél, amelynek reload-írása nem tölti a számlálót, mint a SysTick, a VALUE/CVR külön kezelése valóban szükséges; a CMSDK timernél nem.)
  2. 1 Hz-hez T⋅f=25000000T \cdot f = 25\,000\,000 ciklus, RELOAD=24999999\text{RELOAD} = 24\,999\,999. Az ISR-költség változatlan (~46 ciklus), ezért a megszakítás többletköltsége O=46/25000000×100%≈0,00018%O = 46 / 25\,000\,000 \times 100\% \approx 0{,}00018\%. A teljes kihasználtsághoz ehhez hozzá kell adni a periódusonként elvégzett munkát. Ha ez is másodpercenként egyszer fut, a terhe hasonlóan elenyésző; ha a munka nem a periódustól függ, a teljes arány a munka mennyiségétől függ. Az ISR-ben g_seconds++ történik; az INTSTATUS = 1 (write-1-to-clear) maradjon a visszatérés előtt (a példában az első utasítás). A tanulság: hosszabb periódusnál a megszakítás relatív többletköltsége tovább csökken.
  3. Tünet: kétszeres ISR-gyakoriság. Hipotézis: a megszakításkérés nem törlődik (hiányzó INTSTATUS = 1 write-1-to-clear), vagy a megszakítás forrása két entitás (pl. a TIMER0 és a TIMER1 ugyanarra a vektorra mutat). Adatgyűjtés: gdb alatt breakpoint az ISR-be, majd print/x *(uint32_t*)0x4000000C (a TIMER0->INTSTATUS) és a CTRL visszaolvasása. Egy változtatás: INTSTATUS = 1 beírása az ISR első utasításaként (illetve a két forrás regiszter-szintű szétválasztása). Ellenőrzés: a másodpercenkénti ISR-futások száma a periódus reciprokával egyezik.
  4. A mérés a fenti lab-a077-irq.c LAB_MEASURE ága. Először a program ellenőrzi, hogy a CYCCNT (0xE0001004) aktív futás alatt halad-e (DEMCR.TRCENA, DWT_CTRL.CYCCNTENA, majd két olvasás egy rövid ciklus körül); ha nem, az ISR-törzsmérés SKIP, ahogy a QEMU-alapúton, összhangban az a071 és az a080 mintájával. Ez a próba az alvás alatti viselkedésről semmit nem mond, ezért az alvásidőt nem a CYCCNT, hanem a TIMER1 adja. Három külön szám születik. (1) Az ISR törzsideje: az ISR elején és végén vett CYCCNT-mérőpontok különbségének átlaga, ez a modell 22 ciklusának mért párja, a hardveres be- és kilépés nélkül. (2) Egy LAB_TICKS_PER_LINE tickes ablak teljes ideje a TIMER1-gyel; ez az ismert ébresztési időközzel vethető össze, és ha alvás közben nem jár, SKIP. (3) Az ugyanebben az ablakban alvással töltött idő, szintén a TIMER1-gyel. Az aktív hányad 1−alvás/ablak1 - \text{alvás}/\text{ablak}, amely ugyanabból az ablakból számolva mindig 0 és 1 közé esik, és nem függ attól, hány tickes az ablak. Egy tickes periódus és egy 100 tickes alvásösszeg hányadosa értelmetlen lenne. A busy-wait változatban a ciklusonként mért teljes hossz (késleltetés + munka + kiírás) mutatja, hogy a periódus a munkával együtt csúszik. A kalibráláshoz ez kell, nem a késleltető ciklus becsült hossza.

19.3.11 Rövid összefoglaló

A timer-megszakítás-alapú periódusos feladatot a timer CTRL, VALUE, RELOAD és INTSTATUS regisztereinek konfigurációja, valamint az ISR-ben elvégzett nyugtázás és a tényleges munka adja. A periódus T=(RELOAD+1)/ftimerT = (\text{RELOAD}+1)/f_{\text{timer}}. A busy-wait késleltetés 100%-ban lefoglalja a CPU-t, míg a timer-IRQ a periódus ciklusainak csak egy töredékét áldozza az ISR-re, így más munka párhuzamosan folyhat. A nagyobb komplexitás (vektortábla, ISR, jitter) hosszú periódus és párhuzamos feladatok esetén térül meg. Ez a minta a záróprojektbeli időbázis alapja.

Továbbhasznált definíciók: - periodikus feladat: fix TT időközönként végrehajtandó műveletsor, amelyet timer-megszakítás vált ki. - RELOAD: a periódust meghatározó újratöltési érték; a számláló innen indul nulláig. - busy-wait késleltetés: CPU-ciklusokat aktívan fogyasztó várakozás, amely alatt más munka nem végezhető.

19.3.12 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 (nyitott): - A TIMER0 báziscíme, a CTRL pontos bitmezői és az IRQ-szám a platform dokumentációjából visszaigazolandók; a QEMU mps2-an385 memóriatérképén ellenőrizendők. - A timer órajel-frekvenciája (TIMER_FREQ_HZ) machine-függő; a fenti 25 MHz csak illusztratív, nem ellenőrzött érték. - A 12 ciklusos belépés a Cortex-M3 implementáció ideális körülményekre vonatkozó adata, a 12 ciklusos kilépés és a 20–24 ciklusos ISR-törzs a modell feltevése; a 46 ciklus és a 0,0184% (a megszakítás többletköltsége) ebből levezetett érték, nem mérés. A teljes kihasználtság példájában a lab_work_step és a tick-feldolgozás ciklusszáma (W≈256W \approx 256, P‾≈10\bar{P} \approx 10) feltételezés; a hardveren a LAB_MEASURE ág aktív hányada méri. - A közölt programokat (lab-a077-irq.c, lab-a077-busywait.c, lab-a077-common.c, a kibővített kanonikus startup) a helyettesítő eszközlánccal (Clang + ld.lld) lefordítottuk, mindkét ELF-et tiszta könyvtárból, pontosan a közölt cp/cat/sed/echo lépésekkel és buildparancsokkal linkeltük, és a lab-check-vectors.py az IRQ-s változatban TIMER0_IRQHandler-t, a busy-wait változatban Default_Handler-t találta az IRQ8 helyén. A gyenge alias nélkül a busy-wait linkelése undefined symbol: TIMER0_IRQHandler hibával leállt. A tools/cm-run.py emulátorban (amely a Timer0-t és az IRQ-kézbesítést is modellezi) mindkét változat tick sorokat írt, az IRQ-s változat előtte a DWT-ellenőrzés SKIP-sorát. A qemu-system-arm alatti futás és a DWT-mérés hardveren ellenőrizendő. - A RELOAD-írás mellékhatását a CMSDK műszaki leírás 4-4. táblázata írja le (a lektor ellenőrzése alapján javítva); a QEMU timermodelljének ezzel egyező viselkedését ebben a munkafolyamatban nem futtattuk. - A QEMU-emulációban nincs valós idejű órajel; minden periódus- és ciklusszám-adat relatív/illusztratív, fizikai hardveren mérendő.