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.
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.
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.
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.
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).
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/zaro2Tekintsü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
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
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");
}
#endifA "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).
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 |
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.ldA 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_HandlerA 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.
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.
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).
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.
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);
}
}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 cleancp ~/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.
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.
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.
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?
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.
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.
Az alábbiak dokumentációs kiindulópontok (forrásállapot: nem megnyitott, lásd az Olvasói útmutató jelöléseit):
PRIMASK, kivétel-belépés/visszatérés.mps2-an385 machine, -kernel betöltés,
-nographic soros I/O.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ő.