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.
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 (): két egymást követő megszakításkérés (illetve számláló-újratöltés) között eltelt idő:
ahol a timer órajel-frekvenciája [Hz], RELOAD pedig a betöltési regiszterbe írt érték. A 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 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.
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
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.
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/a077A 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.sA 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
.
Ez feltételezés, nem mérés: más fordító vagy kapcsoló más ciklusmagot
ad, a pontos
é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.elfKimenet. 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
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ó.
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ő:
A költségeket a továbbiakban ciklusban adjuk meg (, , , , ). A periódusra jutó elérhető ciklusszám (s · ciklus/s = ciklus), ezért ciklusokkal ugyanez az arány , mert . Egy képleten belül minden összeadandó azonos egységű: vagy mind ciklus, vagy mind idő. Időben megadott költséget előbb -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:
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ó
ciklust vesz igénybe, az iterációszám
.
A
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:
A teljes CPU-kihasználtsághoz hozzá kell adni a
főciklus tickenkénti aktív munkáját is: a lab_work_step
ciklusát és a tick-feldolgozás, valamint a kiírás tickenkénti átlagát
(;
a tick sor csak minden 100. tickben íródik ki, ezért a
költségét 100 tickre kell szétosztani):
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 munkát végzi, és a periódus maradékát pörgéssel tölti, ezért , a várakozás többletköltsége pedig .
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: , , belépés és kilépés 12–12 ciklus (a fenti modell), az ISR törzse 20–24 ciklus. Egy periódusra ciklus jut. A középértékkel
A tartományt a törzsidő-feltevés adja: 20 ciklusnál
,
24-nél
.
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
(),
és a tick-feldolgozás a kiírással együtt tickenként átlagosan
ciklus:
Időben számolva ugyanez adódik: , és .
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.
,
nem a többletköltségek
-as
aránya. Ha a lab_work_step iterációszámát százszorosára
(6400-ra) növeljük,
,
és
lesz, miközben
változatlanul 0,0184%. A két metrika tehát mást mér: az
az időzítési mód költsége, az
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.
| 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 ) | 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%) |
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:
main ciklusnak más dolga van (UART, szenzor, állapotgép), a
busy-wait kizárt, mert blokkolja a haladást. A timer viszont
párhuzamosítja az időzítést és a munkát.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ő.
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.
| 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 |
| 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 |
| A busy-wait ciklus eltűnt | volatile/__asm volatile hiánya,
-O2 |
objdump a delay_busywait-en |
volatile a ciklusváltozón |
ciklus megmarad |
RELOAD
érték ad 50 ms periódust
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.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.gdb-vel a hipotézis igazolásához.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.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
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.)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.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.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
,
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.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
.
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 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ő.
Az alábbiak dokumentációs kiindulópontok (forrásállapot: nem megnyitott, lásd az Olvasói útmutató jelöléseit):
SysTick.mps2-an385 machine memóriatérképe és a
-nographic/-kernel viselkedése.arm-none-eabi-gcc,
objdump, gdb viselkedése és a
vektortábla-generálás.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
(,
)
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ő.