20.2 Megvalósítás: driver és ISR integrálása a minimal runtime-ba

20.2.1 Motiváció: a specifikációtól a működő firmware-ig

A Projektspecifikáció: periféria, megszakítás-logika és mérési terv alfejezetben elkészült a lab-m19 szerződése: egyetlen, QEMU-n teljesíthető alapfeladat (Timer0 időbázis, UART0 echo és parancsok; a GPIO-eseménybemenet külön hardveres szerződés), a K1–K6, F2, F3 követelmények, az interfész-szerződés, az ISR-mátrix, az erőforrás-budget és a mérési terv. A specifikáció azonban önmagában nem fut. Ebben az alfejezetben a papíron rögzített szerződést alakítjuk át olyan firmware-modullá, amely a Startup kód: .data másolása, .bss törlése alfejezetben épített minimál runtime-ra támaszkodik, és a Firmware build és QEMU-betöltés munkafolyamat lépéssorával reprodukálható módon elindul a qemu-system-arm -machine mps2-an385 emulátorban.

A mérnöki nehézséget nem az egyes driverek megírása jelenti — a Periféria-regiszter absztrakció C struktúrával, az Egyszerű UART driver és a Timer-alapú időzítés és periodikus feladatok már megadta a mintákat. A valódi feladat kettős: (1) a driverek és a hozzájuk tartozó ISR-ek illesztése a vektortáblába, a startup kódba és az NVIC-konfigurációba, valamint (2) azon a ponton kezelni a fő program és az ISR közötti megosztott állapotot, ahol a naiv implementáció csendben adatot veszít. Ez utóbbi a tipikus „majdnem jó” firmware hibamódja: a kód lefordul, elindul, látszólag működik, majd nagy terhelésnél vagy ritka ütemezésnél elveszít egy ticket vagy egy bemeneti bájtot. Az alábbiakban ezért az integráció mellett kiemelt hangsúlyt kap a versenyhelyzet (race condition) felismerése és a kritikus szakasz (critical section) fegyelmezett használata.

20.2.2 Definíció: mit integrálunk és mi a szerződés

driver-ISR integráció (driver-ISR integration): az a műveletsor, amelynek során egy periféria inicializáló és I/O-függvényei, a hozzá tartozó megszakításkezelő rutin, a vektortábla megfelelő bejegyzése, az NVIC engedélyezési és prioritásbeállítása, valamint a megosztott állapotot védő kritikus szakasz egyetlen, konzisztens egységbe kerül.

Az integrációnak négy, egymástól függetlenül ellenőrizhető szerződéselem felel meg:

Elem Hol él Mit garantál
Vektorbejegyzés vektortábla (.vectors) a kivétel belépési pontja a helyes ISR-re mutat
NVIC-engedélyezés NVIC_ISER0, NVIC_IPR a forrás képes megszakítást kérni, adott prioritással
ISR-törzs C függvény a forrás-flag törlése, az adat kiolvasása, a megosztott állapot frissítése
Kritikus szakasz fő program és ISR a megosztott állapot nem sérül a nem atomi hozzáférés miatt

Az integráció akkor tekinthető késznek, ha a fenti négy elem konzisztens: a vektorban szereplő szimbólum pontosan az a függvény, amelyet az NVIC engedélyez, és amely a megosztott állapotot ugyanazzal a védelmi szerződéssel használja, mint a fő program. Ha bármelyik elem elcsúszik — például a vektor egy Default_Handler-re mutat, miközben az NVIC engedélyezve van —, a hiba tünete „néma”: a megszakítás vagy nem fut le, vagy végtelen ciklusba esik.

20.2.3 A vektortábla kiegészítése

Az M-profile kivételmodellt a A Cortex-M kivételmodell: prioritás és vektortábla-bejegyzések tárgyalta. A vektortábla első szava nem utasítás, hanem az inicializáló veremmutató (_estack) értéke; a második szó a reset vektor. Ezután 14 további rendszerkivétel következik (NMI, HardFault, MemManage, BusFault, UsageFault, SVC, DebugMon, PendSV, SysTick és a fenntartott helyek), majd 16-tól kezdődnek a külső megszakítások (IRQ0, IRQ1, …). A lab-m19 esetében tehát a vektortáblát a 16. indextől kell kiegészíteni a Timer0 és UART0 bejegyzésekkel.

Az ARM MPS2+ AN385 board IRQ-számai board-specifikusak; a pontos értékeket a QEMU hw/arm/mps2.c forrása és az MPS2 technikai referenciája adja meg. Az ellenőrzött kiosztás szerint az UART0 RX IRQ0, a Timer0 IRQ8 — ezt a lab-m19-drivers.h UART0_IRQn/TIMER0_IRQn szimbolikus konstansai rögzítik, de a hallgatónak a saját QEMU-verziójával szemben ettől függetlenül meg kell erősítenie. A vektortáblában a bejegyzés helye 16 + IRQn; a két IRQ-szám közötti nem szomszédos pozíciókat (itt IRQ1–IRQ7) explicit nulla bejegyzésekkel kell kitölteni, különben a második kezelő rossz IRQ helyére kerülne.

Fontos részlet, hogy a vektortábla a flash (illetve az AN385 esetében a betöltött SRAM) legalján van, és a linker garbage collection (GC) hajlamos eldobni, ha nem hivatkozik rá szimbólum. Ezért a linker scriptben KEEP(*(.vectors)) szerepel, és a tábla .global szimbólummal exportált.

20.2.4 Az ISR-ek M-profile szerződése

Az A-profile-tól eltérően az M-profile megszakításkezelő rutinjai sima C függvények. A kivétel belépésekor a hardver automatikusan elmenti a hívó által használt regisztereket (R0–R3, R12, LR, PC, xPSR) az aktuális veremre, és az LR-be egy EXC_RETURN-értéket tölt. A visszatérést az váltja ki, hogy ez az érték Handler módban a PC-be kerül. A fordító szokásos epilógusa (BX LR, vagy hívó kezelőnél POP {..., PC}) ezt külön teendő nélkül elvégzi. Ezért a kezelő szignatúrája void f(void), kézi BX LR-zárás nem kell. A GCC __attribute__((interrupt)) Armv7-M-en érvényes, de nem szükséges: ott csak azt jelzi, hogy a függvény 4 bájtra igazított SP-vel is meghívódhat (a068). A CCR.STKALIGN = 1 alapállapot mellett a sima függvény is helyes. Az A-profile-os __irq kulcsszó ide nem tartozik.

Két következmény érdemel figyelmet. Egyrészt az ISR-nek nincs visszatérési értéke és nincs paramétere. Másrészt az ISR-ben nem szabad hosszú, blokkoló műveletet végezni: a megszakítás-latencia — amelyet a Megszakítás-latencia mérése és korlátai definiált — a teljes ISR futási idejével nő, és a fő program előremenetelét késlelteti. Az ISR tipikus feladata három lépés: a forrás-flag azonosítása és törlése, az adat kiolvasása/írása, valamint a megosztott állapot minimális frissítése.

20.2.5 Megosztott állapot: az SPSC ring buffer

A specifikáció K2 és F2/F3 követelménye szerint a fő program a beérkező UART-bájtokat dolgozza fel. Mivel a bájtokat az UART0 ISR veszi át, a fő program és az ISR között megosztott puffer kell. A legegyszerűbb helyes szerkezet az egytermelős–egyfogyasztós (single-producer single-consumer, SPSC) ring buffer: az ISR csak a head indexet írja, a fő program csak a tail indexet. A puffer akkor üres, ha head == tail; eggyel kevesebb slot használható, mint a tömb mérete, így a tele/üres állapot megkülönböztethető külön számláló nélkül.

Az SPSC minta kulcsfelismerése, hogy az indexek külön változók, és mindegyiket pontosan egy kontextus írja. Ekkor a volatile minősítő elegendő a láthatósághoz, és nincs szükség kritikus szakaszra a push/pop műveletekhez. Ennek feltétele, hogy az index típusa (itt uint16_t) egyetlen, atomi módon olvasható és írható gépi szón belül legyen, és hogy a termelő előbb írja az adatot, és csak utána a head-et. A puffer kódja hardverfüggetlen külön modul (lab-m19-ring.h/.c, a szerződést lásd az a078 alfejezetben), ezért a hoston egységtesztelhető (a080).

20.2.6 A race condition és a kritikus szakasz

Munkakönyvtár: ~/arm-labor/cm/zaro2/ — az alfejezet minden laborfájlja ide kerül, és minden parancs innen futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le). Ez a II. záróprojekt közös könyvtára: mind a négy alfejezete (a078–a081) itt dolgozik, és egymás fájljaira épít.

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

Tekintsük a timer oldalt. Az első, naiv tervben az ISR egy tick_flag jelzőt állít, a fő ciklus figyeli és törli:

/* HIBÁS példa – szemléltetésre: egybites jelző, a fő program olvassa és törli */
if (tick_flag) {          /* (A) olvasás */
    tick_flag = 0;        /* (B) törlés  */
    process_tick();
}

Egyetlen magon az ISR nem fut „párhuzamosan” a fő programmal: vagy a fő program egy utasítása fut, vagy az ISR egésze. Az ISR-t viszont a fő program bármely két utasítása közé beékelheti a hardver. A hiba ezért a fő program megszakítható (A) olvasása és (B) törlése közé beszúrt ISR-ből adódik. Végrehajtható idődiagram (minden sor egy lépés, felülről lefelé):

Lépés Fő program ISR tick_flag Eltelt tick Feldolgozott tick
0 – 1. tick: tick_flag = 1 1 1 0
1 (A) ldr → 1 – 1 1 0
2 – 2. tick: belépés az (A) és (B) közé, tick_flag = 1 1 2 0
3 (B) str ← 0 – 0 2 0
4 process_tick() – 0 2 1
5 (A) ldr → 0: nincs teendő – 0 2 1

A 2. lépés eseményét a fő program a 3. lépésben úgy törli, hogy sosem látta: ez a valódi read-modify-write verseny. Ha utána nem jön újabb tick, a fő program erről az eseményről soha nem tud. (A fordított irány, hogy a fő program az ISR tick_count++-ának betöltése és visszaírása közé ékelődne, egyetlen magon lehetetlen: a fő program az ISR alatt nem fut.)

A javított jelzőkezelés az olvasást és a törlést egyetlen kritikus szakaszba teszi, a jelző tesztjével együtt, így egyetlen koherens pillanatkép keletkezik:

uint32_t pm = crit_enter();
uint32_t flag  = tick_flag;      /* teszt ... */
uint32_t ticks = tick_count;
tick_flag = 0;                   /* ... és törlés ugyanabban a szakaszban */
crit_exit(pm);
if (flag)
    process_tick(ticks);

Ez koherens pillanatképet vesz, és a verseny ellen véd, de nem garantál egy feldolgozást minden tickre, mert nem számlál: ha a fő program lassú, és közben két tick történik, a jelző egyszer lesz 1, és a két tick egyetlen feldolgozásba olvad. Ez nem hiba, hanem a jelző természete; egy „valami történt” jelzéshez éppen ez kell. Négy dolgot érdemes tehát szétválasztani:

Fogalom Mit mutat Eszköz
eseményszámláló hány esemény történt összesen tick_count, csak az ISR írja
eseményjelző történt-e valami a legutóbbi ellenőrzés óta (összevonással) tick_flag
fogyasztói lemaradás hány eseményt nem dolgozott még fel a fő program tick_count − seen
RMW-race egy esemény nyoma elveszik a nem védett olvasás–írás miatt a fenti idődiagram

A lab-m19 K6 követelménye szerint minden ticket fel kell dolgozni, ezért a megvalósítás nem jelzőt, hanem számlálót használ: a fő program a saját seen változóját lépteti a tick_count-ig, és minden különbségre egy ticket dolgoz fel. A tick_count-ot csak az ISR írja; a K4 R parancs nem nullázza, hanem a fő program saját tick_base nullpontját állítja. Így a megosztott számlálón nincs második író, tehát nincs RMW-verseny sem. A seen != now összehasonlítás és a seen++ előjel nélküli, modulo 2322^{32} aritmetikában dolgozik, ezért a tick_count körbefordulása (100 Hz mellett kb. 497 nap után) nem hiba. A feldolgozás addig helyes, amíg a fogyasztói lemaradás kisebb 2322^{32} ticknél, és ez a körbefordulási feltétel része a szerződésnek. A kritikus szakasz (PRIMASK) csak a maszkolható megszakítások ellen véd: egy NMI- vagy HardFault-kezelő ezalatt is futhat, ezért ezek nem férhetnek hozzá a megosztott állapothoz.

A volatile önmagában nem oldja meg ezt. A volatile azt garantálja, hogy a fordító minden hozzáféréskor újraolvassa és kiírja a változót, és nem cache-eli regiszterben; nem garantál atomitást és nem zárja ki a kontextusváltást az RMW közben. A helyes megoldás a megszakítások letiltása a kritikus szakaszban. Az M-profile-on ezt a PRIMASK speciális regiszter szabályozza; a cpsid i utasítás letiltja a maskolható megszakításokat, a cpsie i engedélyezi. A helyes minta a korábbi állapot mentése és visszaállítása, hogy a beágyazott kritikus szakaszok működjenek:

/* lab-m19-crit.h – kritikus szakasz M-profile-on, a PRIMASK megőrzésével */
#ifndef LAB_M19_CRIT_H
#define LAB_M19_CRIT_H
#include <stdint.h>

static inline uint32_t crit_enter(void)
{
    uint32_t pm;
    __asm volatile ("mrs %0, primask" : "=r"(pm));
    __asm volatile ("cpsid i" ::: "memory");
    return pm;
}

static inline void crit_exit(uint32_t pm)
{
    __asm volatile ("msr primask, %0\n\tisb" :: "r"(pm) : "memory");
}
#endif

A "memory" clobber megakadályozza, hogy a fordító memóriahozzáféréseket mozgasson a kritikus szakaszba vagy ki belőle. A mentett PRIMASK visszaállítása azért fontos, mert a cpsie i feltétel nélküli engedélyezése egy külső kritikus szakasz belsejében hibásan korábban nyitná vissza a megszakításokat. Az msr primask után az ISB biztosítja, hogy a módosítás a következő utasításoktól érvényes legyen; ezért szerepel a crit_exit-ben.

A kritikus szakasz mindössze néhány utasítás: a megszakítás-letiltás ideje rövid, így a megszakítás-latencia növekedése elhanyagolható. A fő szabály: a kritikus szakaszban nem hívunk függvényt, nem írunk soros portra, nem várunk. Ha a védendő művelet hosszú, a mintát újra kell tervezni (például dupla pufferrel vagy atomi cserével).

20.2.7 Build-integráció

A lab-m19 build a korábbi fejezetek elemeit egyetlen projektbe fogja össze. Az alábbi táblázat a forrásokat és a hozzájuk tartozó korábbi alfejezetet rendeli:

Fájl / elem Szerep Előzmény
lab-m19.ld szekciók, _estack, _sidata, .data/.bss Linker script alapjai
lab-m19-startup.S reset vektor, .data másolás, .bss törlés, vektortábla Startup kód
lab-m19-drivers.h/.c UART0, Timer0 regiszterstruktúra, init-függvények, pollozó TX (a .c-t a Makefile önállóan fordítja, ezért a megosztott deklarációk a .h-ban élnek) Periféria-regiszter absztrakció, UART driver
lab-m19-ring.h/.c hardverfüggetlen SPSC ring buffer (F2, F3) a078 szerződése
lab-m19-crit.h kritikus szakasz a PRIMASK megőrzésével Megszakítás-latencia
lab-m19-isr.h/.c TIMER0_Handler, UART0_Handler, a megosztott (nem static) ISR-állapot, DWT-mintavétel (K5) ISR írása
lab-m19-main.c fő ciklus, parancsértelmezés (K1–K6), versenymentes alvás Polling versus interrupt-vezérelt I/O
lab-m19-ring-test.c, lab-m19-systest.py egységteszt (hoston) és rendszerteszt (soros bemenettel) a080
Makefile cross-build -lgcc-vel, run, ringtest, systest, test Firmware build

20.2.8 Kidolgozott példa: a lab-m19 firmware

20.2.8.1 Linker script és memóriaelrendezés

Az AN385 memóriatérképét a Memóriatérkép és a bare-metal futási modell vezette be. A projekt nem ír saját linker scriptet: a lab-m19.ld a II. rész kanonikus lab-mps2.ld-jének (Linker script alapjai) változatlan másolata. Így ugyanaz a szerződés érvényes, mint a korábbi laborokban: .vectors a 0x00000000-n, 4 MiB FLASH (betöltött SSRAM) és a 4 MiB-os kanonikus RAM (a 0x20400000-tól látszó tükör nélkül), szóhatárra igazított .data/.bss határok, és ASSERT a verem és az adatok ütközésére.

cp ~/arm-labor/cm/a065/lab-mps2.ld lab-m19.ld

20.2.8.2 Startup és vektortábla

A startup kód a Startup kód kanonikus lab-startup.s-e, egyetlen bővítéssel: a vektortábla a 16 rendszerbejegyzés után a projekt két külső megszakítását is tartalmazza. A Reset_Handler (elöltesztelő, előjel nélküli bhs ciklusok), a Default_Handler és a gyenge aliasok változatlanok:

/* lab-m19-startup.S — a kanonikus lab-startup.s, kiegészítve a lab-m19
 * külső megszakításaival (IRQ0 = UART0 RX, IRQ8 = Timer0). A Reset_Handler,
 * a Default_Handler és a gyenge aliasok változatlanok. */
    .syntax unified
    .cpu cortex-m3
    .thumb

    .section .vectors, "a", %progbits
    .align 2
    .global __vectors
__vectors:
    .word _estack                 /* 0x00: kezdeti SP (MSP)      */
    .word Reset_Handler           /* 0x04: Reset (bit0 = 1)      */
    .word NMI_Handler             /* 0x08: NMI                   */
    .word HardFault_Handler       /* 0x0C: HardFault             */
    .word MemManage_Handler       /* 0x10: MemManage             */
    .word BusFault_Handler        /* 0x14: BusFault              */
    .word UsageFault_Handler      /* 0x18: UsageFault            */
    .word 0, 0, 0, 0              /* 0x1C-0x28: fenntartott      */
    .word SVC_Handler             /* 0x2C: SVCall                */
    .word DebugMon_Handler        /* 0x30: DebugMonitor          */
    .word 0                       /* 0x34: fenntartott           */
    .word PendSV_Handler          /* 0x38: PendSV                */
    .word SysTick_Handler         /* 0x3C: SysTick               */
    /* 16-tól: külső IRQ-k. Az ellenőrzött AN385-kiosztásban UART0 RX = IRQ0,
       Timer0 = IRQ8; a kettő között 7 helyet (IRQ1-IRQ7) ki KELL tölteni,
       különben a második bejegyzés tévesen IRQ1 helyére kerülne, nem IRQ8-ra. */
    .word UART0_Handler           /* 0x40: IRQ0 (UART0 RX)       */
    .word 0, 0, 0, 0, 0, 0, 0     /* 0x44-0x5C: IRQ1-IRQ7        */
    .word TIMER0_Handler          /* 0x60: IRQ8 (Timer0)         */

    .text
    .thumb_func
    .global Reset_Handler
    .type   Reset_Handler, %function
Reset_Handler:
    /* 1. .data másolása FLASH (LMA) -> RAM (VMA), szavanként.
       Elöltesztelő ciklus, előjel nélküli összehasonlítással: üres
       szekciónál egyszer sem fut, és a végcímen pontosan megáll. */
    ldr   r0, =_sidata
    ldr   r1, =_sdata
    ldr   r2, =_edata
1:  cmp   r1, r2
    bhs   2f
    ldr   r3, [r0], #4
    str   r3, [r1], #4
    b     1b
    /* 2. .bss nullázása, szavanként. */
2:  ldr   r1, =_sbss
    ldr   r2, =_ebss
    movs  r3, #0
3:  cmp   r1, r2
    bhs   4f
    str   r3, [r1], #4
    b     3b
    /* 3. main(); ha visszatérne, végtelen ciklus. */
4:  bl    main
5:  b     5b
    .size Reset_Handler, . - Reset_Handler

    .thumb_func
    .global Default_Handler
    .type   Default_Handler, %function
Default_Handler:
    b     .
    .size Default_Handler, . - Default_Handler

    /* Gyenge aliasok: amit a firmware nem definiál, az a Default_Handlerre fut. */
    .macro lab_default_handler name
    .weak       \name
    .thumb_set  \name, Default_Handler
    .endm
    lab_default_handler NMI_Handler
    lab_default_handler HardFault_Handler
    lab_default_handler MemManage_Handler
    lab_default_handler BusFault_Handler
    lab_default_handler UsageFault_Handler
    lab_default_handler SVC_Handler
    lab_default_handler DebugMon_Handler
    lab_default_handler PendSV_Handler
    lab_default_handler SysTick_Handler

A TIMER0_Handler és UART0_Handler szimbólumok C függvények, amelyeket a linker old fel; mivel nem .weak-ként szerepelnek, ha valamelyik hiányzik, a linker hibát jelez (“undefined reference to …”), és a linkelés megszakad — a GNU ld alapértelmezett viselkedése a fel nem oldott, nem gyenge szimbólumokra a hibajelzés, nem a vektorbejegyzés csendes 0-ra állítása. A rendszer-kivételek (NMI_Handler, HardFault_Handler, SVC_Handler, PendSV_Handler, SysTick_Handler) ezzel szemben a fent látott .weak/.thumb_set Default_Handler mintát követik (a070 szerint): ha a firmware nem definiálja felül őket, a linker a Default_Handler-re köti, amely egy végtelen ciklus — ez debuggerben azonnal jelzi, hogy egy kezelt kivétel lefutott, de a tényleges logika hiányzik. Ezért érdemes a vektortáblát KEEP-pel védeni, hogy a linker a --gc-sections során ne dobja el a futásidőben szükséges, de a kódból közvetlenül nem hivatkozott bejegyzéseket.

20.2.8.3 Driverek

A periféria-regisztereket a Periféria-regiszter absztrakció C struktúrával mintájára struktúrába szervezzük. Az alábbi címek és bitmezők az MPS2 AN385 CMSDK perifériáinak jellemző értékei, de ellenőrizendők a QEMU hw/arm/mps2.c és a CMSDK dokumentáció alapján.

A struktúrákat és a függvényprototípusokat egy fejlécbe tesszük, nem a .c fájlba: a lab-m19 build minden .c fájlt önállóan fordít (lásd a Makefile-t), és ha egy .c fájlt egy másik #include-dal is beemelne, a megosztott szimbólumok (pl. uart0_init) kétszer definiálódnának, és a linker „multiple definition” hibával állna el.

/* lab-m19-drivers.h */
#ifndef LAB_M19_DRIVERS_H
#define LAB_M19_DRIVERS_H
#include <stdint.h>

typedef struct {
    volatile uint32_t DATA;       /* 0x000 */
    volatile uint32_t STATE;      /* 0x004 bit0 TXFULL, bit1 RXFULL */
    volatile uint32_t CTRL;       /* 0x008 bit0 TXEN, bit1 RXEN, bit3 RXINTEN */
    volatile uint32_t INTSTATUS;  /* 0x00C olvasás: bit0 TXINT, bit1 RXINT; írás: W1C */
    volatile uint32_t BAUDDIV;    /* 0x010 legalább 16 */
} cmsdk_uart_t;

typedef struct {
    volatile uint32_t CTRL;       /* 0x000 bit0 ENABLE, bit3 IRQEN */
    volatile uint32_t VALUE;      /* 0x004 */
    volatile uint32_t RELOAD;     /* 0x008 írása a VALUE-t is beállítja */
    volatile uint32_t INTSTATUS;  /* 0x00C olvasás: pending; írás: W1C (INTCLEAR) */
} cmsdk_timer_t;

#define UART0  ((cmsdk_uart_t *)0x40004000u)
#define TIMER0 ((cmsdk_timer_t *)0x40000000u)

#define UART0_IRQn   0u           /* AN385: UART0 RX */
#define TIMER0_IRQn  8u           /* AN385: Timer0   */

#define LAB_SYSCLK_HZ  25000000u  /* QEMU AN385; laborparaméter */
#define LAB_TICK_HZ    100u       /* 10 ms tick */

void uart0_init(uint32_t bauddiv);
void timer0_init(uint32_t reload);
void uart_puts(const char *s);    /* blokkoló, pollozó TX (az alapfeladat szerződése) */
void uart_putc(char c);
void uart_put_u32(uint32_t v);

#endif
/* lab-m19-drivers.c */
#include "lab-m19-drivers.h"

void uart0_init(uint32_t bauddiv)
{
    UART0->BAUDDIV = bauddiv;
    UART0->CTRL    = (1u << 0) | (1u << 1) | (1u << 3);   /* TXEN, RXEN, RXINTEN */
}

void timer0_init(uint32_t reload)
{
    TIMER0->CTRL      = 0;          /* előbb leállít */
    TIMER0->INTSTATUS = 1u;         /* függő kérés törlése (W1C) */
    TIMER0->RELOAD    = reload;     /* a VALUE-t is beállítja: az első periódus is pontos */
    TIMER0->CTRL      = (1u << 0) | (1u << 3);            /* ENABLE, IRQEN */
}

void uart_putc(char c)
{
    while (UART0->STATE & (1u << 0)) { }    /* amíg TXFULL = 1, várunk */
    UART0->DATA = (uint8_t)c;
}

void uart_puts(const char *s)
{
    while (*s) uart_putc(*s++);
}

void uart_put_u32(uint32_t v)
{
    char d[10];
    unsigned n = 0;
    do { d[n++] = (char)('0' + v % 10u); v /= 10u; } while (v);
    while (n) uart_putc(d[--n]);
}

A CMSDK UART DATA regiszter olvasása a STATE.RXFULL bitet üríti, de nem törli önmagában az INTSTATUS.RXINT megszakításjelzőt — azt külön, write-1-to-clear móddal kell törölni (lásd Egyszerű UART driver). A CMSDK timernek nincs önálló INTCLR regisztere: ugyanaz az INTSTATUS regiszter szolgál a pending állapot olvasására és az 1-et író, write-1-to-clear törlésre. A pontos bitmezők (TX/RX enable, interrupt enable) ellenőrzést igényelnek.

20.2.8.4 ISR-ek

Az ISR-ek sima C függvények. Az UART RX bájtot az SPSC ring bufferbe tesszük, a timer számlál. A megosztott állapotot (rx_ring, tick_count) a fő programnak is látnia kell, ezért ezek nem lehetnek static: a tényleges definíció itt, a deklarációjuk (extern) egy közös fejlécben van, amelyet a lab-m19-main.c is #include-ol. A DWT-mintavétel (K5) csak elmenti a törzsidőt; a statisztikát és a kiírást a fő program végzi, az ISR-en kívül.

/* lab-m19-isr.h */
#ifndef LAB_M19_ISR_H
#define LAB_M19_ISR_H
#include <stdint.h>
#include "lab-m19-ring.h"

extern ring_t rx_ring;                     /* termelő: UART0_Handler */
extern volatile uint32_t tick_count;       /* csak a TIMER0_Handler írja */

/* ISR-törzsidő-minták (DWT); a gyűjtés az ISR-ben, a kiértékelés a fő programban */
#define LAB_SAMPLES 64u
extern int lab_have_dwt;
extern volatile uint32_t isr_body[LAB_SAMPLES];
extern volatile uint32_t isr_body_n;

void UART0_Handler(void);
void TIMER0_Handler(void);
int  lab_dwt_start(void);
#endif
/* lab-m19-ring.c */
#include "lab-m19-ring.h"

int ring_push(ring_t *r, uint8_t b)
{
    uint16_t next = (uint16_t)((r->head + 1u) % RING_CAP);
    if (next == r->tail) {
        r->overrun++;                    /* tele: az új bájt elveszik, de számoljuk */
        return 0;
    }
    r->buf[r->head] = b;                 /* előbb az adat, ... */
    r->head = next;                      /* ... utána a közzététel */
    return 1;
}

int ring_pop(ring_t *r, uint8_t *out)
{
    if (r->tail == r->head) return 0;    /* üres: nem olvas, a tail helyben marad */
    *out = r->buf[r->tail];
    r->tail = (uint16_t)((r->tail + 1u) % RING_CAP);
    return 1;
}

int ring_empty(const ring_t *r) { return r->tail == r->head; }
/* lab-m19-isr.c */
#include "lab-m19-drivers.h"
#include "lab-m19-isr.h"

#define DEMCR      (*(volatile uint32_t *)0xE000EDFCu)
#define DWT_CTRL   (*(volatile uint32_t *)0xE0001000u)
#define DWT_CYCCNT (*(volatile uint32_t *)0xE0001004u)

ring_t rx_ring;
volatile uint32_t tick_count;
int lab_have_dwt;
volatile uint32_t isr_body[LAB_SAMPLES];
volatile uint32_t isr_body_n;

int lab_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;
}

void UART0_Handler(void)
{
    uint32_t t0 = lab_have_dwt ? DWT_CYCCNT : 0u;   /* ISR-TÖRZS eleje (nem az esemény ideje) */
    if (UART0->INTSTATUS & (1u << 1)) {             /* RXINT */
        UART0->INTSTATUS = 1u << 1;                 /* nyugtázás (W1C) */
        ring_push(&rx_ring, (uint8_t)UART0->DATA);  /* a DATA olvasása üríti az RXFULL-t */
    }
    if (lab_have_dwt && isr_body_n < LAB_SAMPLES)   /* csak mentés; formázás a fő programban */
        isr_body[isr_body_n++] = DWT_CYCCNT - t0;
}

void TIMER0_Handler(void)
{
    TIMER0->INTSTATUS = 1u;         /* nyugtázás (W1C) */
    tick_count++;                   /* egyetlen író: nincs RMW-verseny a fő programmal */
}

Az ISR-ben minden megosztott változó volatile, és minden változót pontosan egy kontextus ír: az UART-ISR a head-et és az overrun-t, a timer-ISR a tick_count-ot, a fő program a tail-t. A DWT-mérés két mérőpontja az ISR törzsén belül van, ezért a mért szám az ISR-törzsidő. Nem tartalmazza a hardveres belépést (az esemény és az első utasítás közti időt), és a fordító prológusának a mérőpont előtti részét sem (a080).

20.2.8.5 NVIC-konfiguráció és prioritás

Az NVIC engedélyezése és prioritásbeállítása a NVIC konfiguráció: engedélyezés és prioritás mintáját követi. A prioritásbitek száma implementációfüggő. A példában a nvic_enable már balra igazított, nyers IPR-bájtot kap; így nem feltételezünk négy implementált bitet. A 0x40 timer- és 0x60 UART-prioritás LAB_NVIC_PRIO_BITS = 3 mellett a 2-es és a 3-as logikai szint (a068); más hardveren ezt visszaolvasással is ellenőrizni kell.

/* lab-m19-main.c – NVIC részlet */
#define NVIC_ISER0 (*(volatile uint32_t *)0xE000E100u)
#define NVIC_IPR   ((volatile uint8_t  *)0xE000E400u)

void nvic_enable(uint32_t irqn, uint8_t raw_prio)
{
    NVIC_IPR[irqn] = raw_prio;              /* balra igazított IPR-bájt */
    NVIC_ISER0 = (1u << irqn);
}

A prioritási sorrend a lab-m19 esetében: a timer (magasabb prioritás, mert ő az időbázis), majd az UART RX (alacsonyabb). A prioritásértékek konkrét száma az implementált prioritásbitektől függ.

20.2.8.6 Fő program

A fő ciklus a ring buffert fogyasztja és a tickeket dolgozza fel; minden követelmény (K1–K6) a kommentekben megjelölt helyen valósul meg. A ciklus végén versenymentes alvás áll (a075): a feltétel vizsgálata és a WFI ugyanabban a kritikus szakaszban van, így egy közben érkező megszakítás nem veszhet el.

/* lab-m19-main.c — a lab-m19 alapfeladat (QEMU mps2-an385): UART0 + Timer0 */
#include <stdint.h>
#include "lab-m19-drivers.h"
#include "lab-m19-isr.h"
#include "lab-m19-crit.h"

#define NVIC_ISER0 (*(volatile uint32_t *)0xE000E100u)
#define NVIC_IPR   ((volatile uint8_t  *)0xE000E400u)

static void nvic_enable(uint32_t irqn, uint8_t raw_prio)
{
    NVIC_IPR[irqn] = raw_prio;               /* nyers, balra igazított IPR-bájt */
    NVIC_ISER0 = 1u << irqn;
}

static uint32_t tick_base, overrun_base;     /* 'R' nullpontja: csak a fő program írja */

static void status_line(void)                /* K3 */
{
    uint32_t pm = crit_enter();              /* koherens pillanatkép a két számlálóról */
    uint32_t t  = tick_count;
    uint32_t ov = rx_ring.overrun;
    crit_exit(pm);
    uart_puts("status: ticks=");
    uart_put_u32(t - tick_base);
    uart_puts(" overrun=");
    uart_put_u32(ov - overrun_base);
    uart_puts(" raw=");                      /* K4: a nyers számláló, a bázispróbához */
    uart_put_u32(t);
    uart_puts("\r\n");
}

static void measure_line(void)               /* K5 */
{
    if (!lab_have_dwt) {
        uart_puts("isr_body: SKIP (DWT CYCCNT nem halad)\r\n");
        return;
    }
    uint32_t n = isr_body_n, mn = 0xFFFFFFFFu, mx = 0, sum = 0;
    for (uint32_t i = 0; i < n; i++) {
        uint32_t v = isr_body[i];
        if (v < mn) mn = v;
        if (v > mx) mx = v;
        sum += v;
    }
    uart_puts("isr_body: n="); uart_put_u32(n);
    if (n) {
        uart_puts(" min="); uart_put_u32(mn);
        uart_puts(" max="); uart_put_u32(mx);
        uart_puts(" mean="); uart_put_u32(sum / n);
    }
    uart_puts("\r\n");
}

static void handle_byte(uint8_t c)
{
    if (c == 'S') {
        status_line();
    /* K4: új nullpont, nem a megosztott számlálók írása */
    } else if (c == 'R') {
        uint32_t pm = crit_enter();
        uint32_t base = tick_count;
        tick_base    = base;
        overrun_base = rx_ring.overrun;
        crit_exit(pm);
        uart_puts("reset ok base=");         /* a válasz a rögzített új bázist hordozza */
        uart_put_u32(base);
        uart_puts("\r\n");
    } else if (c == 'M') {
        measure_line();
    } else {
        uart_putc((char)c);                  /* K2: echo */
    }
}

int main(void)
{
    __asm volatile ("cpsid i" ::: "memory");
    uart0_init(LAB_SYSCLK_HZ / 115200u);
    lab_have_dwt = lab_dwt_start();
    timer0_init(LAB_SYSCLK_HZ / LAB_TICK_HZ - 1u);   /* 10 ms */
    nvic_enable(TIMER0_IRQn, 0x40u);
    nvic_enable(UART0_IRQn,  0x60u);
    uart_puts("lab-m19 boot\r\n");                    /* K1 */
    __asm volatile ("cpsie i" ::: "memory");

    uint32_t seen = 0;
    for (;;) {
        uint8_t c;
        while (ring_pop(&rx_ring, &c))
            handle_byte(c);
        uint32_t now = tick_count;           /* 32 bites, igazított olvasás: atomi */
        /* K6: minden tick feldolgozva, nem csak "volt-e" */
        while (seen != now) {
            seen++;
            if (seen % LAB_TICK_HZ == 0u)
                uart_puts("tick\r\n");
        }
        uint32_t pm = crit_enter();          /* versenymentes alvás */
        if (ring_empty(&rx_ring) && seen == tick_count)
            __asm volatile ("dsb\n\twfi" ::: "memory");
        crit_exit(pm);
    }
}

20.2.8.7 Build és reprodukálható QEMU-indítás

A lab-m19.elf buildje a GCC-driverrel linkel, és -lgcc-t ad hozzá: a fordító a C-kódhoz (például 64 bites osztáshoz) segédfüggvényeket hívhat, amelyeket közvetlen ld-hívás nem találna meg. A test cél a két tesztréteget futtatja (a080):

# Makefile — lab-m19 (II. záróprojekt): build, futtatás, tesztek
CROSS   ?= arm-none-eabi-
CC      := $(CROSS)gcc
SIZE    := $(CROSS)size
CFLAGS  := -mcpu=cortex-m3 -mthumb -ffreestanding -O1 -g -Wall -Wextra
LDFLAGS := -nostdlib -T lab-m19.ld -Wl,--gc-sections -Wl,-Map=lab-m19.map
LDLIBS  := -lgcc            # pl. 64 bites osztáshoz vagy más fordítói segédfüggvényhez
SRCS    := lab-m19-startup.S lab-m19-drivers.c lab-m19-ring.c lab-m19-isr.c lab-m19-main.c
OBJS    := $(patsubst %.S,%.o,$(SRCS:.c=.o))
HDRS    := $(wildcard lab-m19-*.h)
QEMU    := qemu-system-arm -machine mps2-an385 -cpu cortex-m3 \
           -display none -monitor none -serial stdio

all: lab-m19.elf

lab-m19.elf: $(OBJS) lab-m19.ld
    $(CC) $(CFLAGS) $(LDFLAGS) $(OBJS) $(LDLIBS) -o $@
    $(SIZE) $@

%.o: %.c $(HDRS)
    $(CC) $(CFLAGS) -c $< -o $@

%.o: %.S
    $(CC) $(CFLAGS) -c $< -o $@

run: lab-m19.elf
    $(QEMU) -kernel lab-m19.elf

ringtest: lab-m19-ring-test.c lab-m19-ring.c lab-m19-ring.h
    gcc -std=c11 -Wall -Wextra -o lab-m19-ring-test lab-m19-ring-test.c lab-m19-ring.c
    ./lab-m19-ring-test

systest: lab-m19.elf
    python3 lab-m19-systest.py $(SYSTEST_FLAGS)   # pl. --isr-band LO:HI, --no-timing

test: ringtest systest

clean:
    rm -f $(OBJS) lab-m19.elf lab-m19.map lab-m19-ring-test

.PHONY: all run ringtest systest test clean
cp ~/arm-labor/cm/a065/lab-mps2.ld lab-m19.ld   # ha még nincs meg
make            # lab-m19.elf + méretösszesítő
make run        # interaktív: a UART0 a terminál stdin/stdout-ja (kilépés: Ctrl-C)
make test       # ringtest (hoston) + systest (QEMU, host-oldali soros bemenettel)

A -serial stdio a UART0-t a QEMU-folyamat standard bemenetére és kimenetére köti; a -monitor none -display none miatt a terminálon csak a firmware kimenete jelenik meg. A qemu-system-arm az ELF szegmenseit a betöltési címükre tölti, és a resetet a vektortábla _estack és Reset_Handler bejegyzésével indítja.

20.2.8.8 Várható viselkedés

Indítás után a soros kimeneten megjelenik a lab-m19 boot, majd másodpercenként (100 tick) egy tick. A beírt bájtok visszaíródnak; az S a status: ticks=… overrun=… raw=…, az R a reset ok base=…, az M a K5 sort adja. A raw a nyers tick_count, a base a resetkor rögzített új nullpont; a ticks mindig raw − base. A helyettesítő eszközlánccal (Clang + ld.lld) épített firmware-t a tools/cm-run.py emulátorban futtattuk; ez a Timer0-t, a UART0 RX-et és a megszakítás-kézbesítést funkcionálisan modellezi. Az abSRSM bemenetre a kimenet:

lab-m19 boot
abstatus: ticks=0 overrun=0 raw=0
reset ok base=0
status: ticks=0 overrun=0 raw=0
isr_body: SKIP (DWT CYCCNT nem halad)
tick
tick

A teljes, automatizált ellenőrzést az a080 rendszertesztje végzi. A QEMU-ban az időzítés nem valós idejű, és a QEMU-s futtatás ebben a munkafolyamatban nem történt meg.

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

1. A volatile elhagyása. Tünet: a fő ciklus soha nem látja a tick_flag változást, vagy a rx_head értéke beragad. Hipotézis: a fordító a változót regiszterben tartja. Adatgyűjtés: objdump -d lab-m19.elf, és keresd a tick_flag betöltését a cikluson kívül. Javítás: volatile minősítő minden megosztott változóra.

2. Hiányzó vektorbejegyzés. Tünet: az első UART RX után a rendszer HardFault-ba esik. Hipotézis: az UART0_Handler szimbólum nem oldódott fel, a vektor 0-ra mutat. Adatgyűjtés: arm-none-eabi-nm lab-m19.elf | grep UART0_Handler, és a .map fájl vizsgálata. Javítás: a szimbólum exportálása, a linker script KEEP ellenőrzése.

3. Két író egy megosztott számlálón. Tünet: 'R' után a státusz ritkán inkonzisztens (a ticks már nullázott, az overrun még nem, vagy fordítva). Hipotézis: a fő program több megosztott változót egymás után ír, és egy ISR a két írás közé ékelődik; vagy a fő program is írja azt a számlálót, amelyet az ISR növel. Javítás: a megosztott számlálót csak az ISR írja, a fő program nullpontot tárol (tick_base), és a nullpontokat egyetlen kritikus szakaszban rögzíti.

4. Hosszú kritikus szakasz. Tünet: a megszakítás-latencia nő, az UART RX túlcsordul. Hipotézis: a kritikus szakaszon belül uart_puts-t hívunk. Javítás: a kritikus szakaszból csak az adatmozgatást tartsuk bent.

5. cpsie i a crit_exit-ben. Tünet: beágyazott kritikus szakaszban a megszakítások túl korán engedélyeződnek. Javítás: a mentett PRIMASK visszaállítása.

20.2.10 Feladatok

1. feladat (magyarázó). Magyarázd el, miért nem elegendő a volatile minősítő a naiv tick_flag-kezelésnél. Add meg utasításszinten (ldr/str) azt a sorrendet, amelyben egy tick nyoma elveszik, és jelöld, melyik két utasítás közé ékelődik az ISR. Indokold, miért nem lehetséges a fordított eset, hogy a fő program az ISR tick_count++-ának közepére ékelődik.

2. feladat (implementációs). Egészítsd ki a lab-m19 firmware-t egy TX ring bufferrel. A uart_puts ne várjon a TX pufferen, hanem a TX interruptra támaszkodjon. Integráld az új ISR-t a vektortáblába és az NVIC-be, és gondoskodj a termelő/fogyasztó indexek védelméről.

3. feladat (hibakeresési/mérési). Egy jelzőalapú változatban a tick sorok száma kevesebb, mint az eltelt tickeké. Tervezz mérést, amely szétválasztja a három lehetséges okot: (a) a jelző szándékos összevonása lassú fogyasztó mellett, (b) a fogyasztói lemaradás, (c) a védtelen olvasás–törlés versenye. Melyik mennyiséget méred melyikhez, és melyik változat (jelző védelem nélkül, jelző kritikus szakaszban, számlálókülönbség) mit mutat?

20.2.11 Megoldások

1. megoldás. A volatile csak azt biztosítja, hogy a fordító minden hozzáféréskor ténylegesen olvas és ír; atomitást nem ad. A naiv kód utasításszinten: ldr r0, [tick_flag] (A), feltételes ugrás, str r1(=0), [tick_flag] (B). Ha az ISR az (A) és a (B) közé ékelődik és tick_flag = 1-et ír, a (B) ezt az új értéket írja felül 0-ra, mielőtt a fő program látta volna: a tick nyoma elveszik (lásd az idődiagramot). A fordított eset azért lehetetlen, mert egyetlen magon az ISR a fő programot szakítja meg, és lefut a végéig, mielőtt a fő program folytatódna. Az ISR ldr/adds/str hármasa közé a fő program nem ékelődhet. (Egy magasabb prioritású másik ISR igen, de az a tick_count-ot nem írja.) Javítás: a teszt és a törlés egy kritikus szakaszban, vagy a jelző helyett számláló, egyetlen íróval.

2. megoldás. A TX oldalon az ISR akkor fut, amikor a TX puffer kiürül. Az ISR a TX ring bufferből betölti a következő bájtot, és a DATA regiszterbe írja; ha a puffer üres, letiltja a TX interruptot. A fő program a TX uart_puts-ban a puffert tölti, és engedélyezi a TX interruptot. Az indexek itt is SPSC mintát követnek: a tx_head-et a fő program, a tx_tail-t az ISR írja. A volatile és az atomi indexméret itt is követelmény.

3. megoldás. Három külön mennyiséget kell mérni. (1) Az eltelt tickek száma: a tick_count, amelyet csak az ISR növel. (2) A feldolgozott tickek száma: a fő program saját számlálója. A kettő különbsége a lemaradás. (3) Az összevont események száma: az ISR növelhet egy merged számlálót, ha belépéskor a jelző már 1. Ez nem bizonyít versenyt, csak azt, hogy a fogyasztó két tick között nem ért oda. A verseny bizonyítéka az, ha védtelen kódnál az eltelt − feldolgozott − összevont különbség pozitív, védett kódnál pedig nulla. Számlálókülönbséges változatnál a feldolgozott tickek száma a lemaradás ledolgozása után mindig megegyezik az eltelttel. A tesztnek ezt az azonosságot kell ellenőriznie, nem azt, hogy az összevonás „nullához közeli”: az összevonás mértéke a terheléstől függ, és nem hiba. QEMU-ban az időzítés nem valós idejű, ezért az arányok csak relatívak.

20.2.12 Rövid összefoglaló

A driver és ISR integrációja a vektortábla, az NVIC, az ISR-törzs és a megosztott állapot védelmének konzisztens egysége, és pontosan az a078 specifikációját (K1–K6, F2, F3) valósítja meg. Az M-profile ISR sima C függvény; a hardver menti a kontextust. Minden megosztott változót egyetlen kontextus ír; az SPSC ring buffer így kritikus szakasz nélkül működik, a tickeket számláló viszi át, és a kritikus szakasz csak a koherens pillanatképhez és a versenymentes alváshoz kell. A volatile láthatóságot ad, atomitást nem. A build a korábbi fejezetek elemeit egyetlen, QEMU-ban reprodukálható firmware-be fűzi.

Továbbhasznált definíciók: - driver-ISR integráció: periféria-driver, ISR, vektorbejegyzés, NVIC-beállítás és kritikus szakasz konzisztens egysége. - SPSC ring buffer: egytermelős–egyfogyasztós puffer, ahol a head-et csak az ISR, a tail-t csak a fő program írja. - kritikus szakasz: megszakítás-letiltással védett, rövid, nem blokkoló utasítássorozat a megosztott állapot atomi frissítésére.

20.2.13 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 (a hallgatónak kell megerősítenie): a közölt forrásokat a helyettesítő eszközlánccal (Clang + ld.lld) lefordítottuk, és a tools/cm-run.py Cortex-M3 emulátorban futtattuk, amely a Timer0-t, a UART0 RX-et és a megszakítás-kézbesítést funkcionálisan, nem cikluspontosan modellezi; a QEMU-s és a GNU-eszközláncos futtatás a laborban végzendő. Ezen felül: az MPS2 AN385 pontos IRQ-számai (TIMER0_IRQn, UART0_IRQn); a CMSDK UART és Timer regiszter-offszetek és bitmezők (DATA, STATE, CTRL, INTSTATUS); az NVIC_IPR prioritásbitszáma; a timer referencia-órajel és az 1 másodperces periódushoz tartozó RELOAD; az UART0 és TIMER0 báziscímek; valamint a QEMU verziófüggő -kernel betöltési és soros viselkedése. A fenti kód ezekre az értékekre nézve jelölt, nem ellenőrzött; a QEMU-emulációban mért periódus relatív, nem hardverhű abszolút idő.