20.3 Mérés, validáció és a QEMU-alapú teszt reprodukálhatósága

20.3.1 Motiváció: a „működik nálam” nem bizonyíték

Az előző alfejezetben (driver és ISR integrálása) összeállt egy firmware, amelyben egy periféria-illesztő, az ISR, a vektorbejegyzés, az NVIC konfiguráció és a megosztott SPSC ring buffer egyetlen, QEMU-ban betölthető egésszé áll össze. A fejlesztő fejében innen gyakran az a mondat születik, hogy „kész, működik”. Egy mérnöki projektben ez nem elég: a „működik” egy személyes megfigyelés, amely nem mondja meg, milyen bemenetre, milyen sorrendben, milyen feltételek mellett, és mi az, ami nem működik. Az a080 célja, hogy a modult mérhető, önállóan reprodukálható egységgé zárja: olyan funkcionális tesztekkel, amelyeknek van elfogadási kritériumuk, és olyan latenciaméréssel, amelynek van dokumentált módszere és ismert korlátja.

A konkrét mérnöki feladat három, egymástól jól elkülöníthető kérdés megválaszolása. (1) Funkcionális: teljesíti-e a firmware az a078 követelményeit (K1–K6), és a kapacitáson belüli bemenet helyesen, sorrendhelyesen és veszteség nélkül jut-e a ring bufferből a fogyasztóhoz (F2), illetve a tele puffer a rögzített veszteségpolitika szerint viselkedik-e (F3)? (2) Teljesítmény: mennyi ciklust tölt az ISR törzse az első mérőponttól az utolsóig, és mekkora ennek a szórása? (3) Reprodukálhatóság: ha a jegyzőkönyvbe beírom a pontos parancsot és a bemenetet, egy másik mérnök ugyanazon a gépen ugyanabban az eredmény-tartományban kapja-e vissza ugyanazt? A fejezet ezt a hármat tanítja egymásra építve, mert a három közül bármelyik hiányában a másik kettő nem áll.

20.3.2 Definíció: funkcionális validáció, mérés, reprodukálhatóság

A fejezet három kulcsfogalmat vezet be, amelyeket élesen megkülönböztetünk.

Az a080 hatásköre szűk: nem írunk formális verifikációt (modell-ellenőrzés, szerződéses bizonyítás), és nem építünk RTOS-ütemezőt vagy kernel-szolgáltatásokat. A validáció célja a meglévő modul viselkedésének dokumentált bemutatása és a mérés megismételhetővé tétele — semmi ennél több.

A funkcionális validáció és a teljesítménymérés két különböző kérdést válaszol meg, és két különböző elfogadási kritériumot kap. Ez azért fontos, mert a gyakorlatban gyakran összekeverik őket: egy gyors, de hibás ISR nem „jobb”, és egy pontos, de lassú ISR nem „sikeres”. Az elfogadási kritérium mindig mindkét tengelyen megfogalmazandó.

20.3.3 A funkcionális tesztsorozat QEMU-ban

A funkcionális tesztek célja, hogy a driver-ISR-integrációt nem egyetlen „szép” futásra, hanem előre megfogalmazott esetekre próbálja ki. Két, egymástól független réteget használunk, és mindkettő futtatható, nem vázlat:

  1. Egységteszt a hoston (U1–U6). A ring buffer hardverfüggetlen modul (lab-m19-ring.c), ezért a host gcc-jével, hardver és emulátor nélkül tesztelhető. Esetek: alapeset és sorrendtartás (3 bájt be, ugyanaz ki, sorrendben), üres puffer (a pop nem olvas, a tail helyben marad), tele puffer (pontosan 63 bájt fér el, a többi az F3 politikája szerint elvész, és az overrun számolja), körbefordulás (5000 bájt kis adagokban, kapacitáson belül, veszteség nélkül), határ (az N−1 = 63. elem még befér, az N. elemre az előírt overflow következik be), és lassú fogyasztó (kettőt tesz be, egyet vesz ki: a kivett bájtok sorrendben egyeznek az elfogadottakkal, az overrun pontosan az elutasított push-ok száma). A tele puffer viselkedése itt determinisztikusan vizsgálható, a soros vonalon nem.
  2. Rendszerteszt soros bemenettel (K1–K6, F2, ismételt reset). A teljes firmware a QEMU-ban fut, a UART0-ját a -serial stdio a QEMU-folyamat stdin/stdout-jára köti. A hostoldali lab-m19-systest.py ide ír bájtokat: ez valódi RX-injektálás a periférián keresztül, és a firmware UART-ISR-je fogadja őket. A választ a kimeneten olvassa. A firmware a saját DATA-írásával nem tudna RX-et szimulálni: az a TX-irány.

20.3.3.1 Elfogadási kritérium megfogalmazása

Egy tesztsorozat csak akkor értékelhető, ha előre rögzítettük, mi számít sikernek. A kritériumokat a teszt futtatása előtt fogalmazzuk meg, és mindegyik egy követelményhez tartozik:

# Tengely Elfogadási kritérium Mérési mód
F1 Helyesség minden egység- és rendszerteszt PASS (a SKIP nem számít sikernek, de hibának sem) make test kimenete
F2 Veszteségmentesség a kapacitáson belül a legfeljebb 63 bájtos burst bájtjai sorrendben, hiánytalanul visszaíródnak rendszerteszt F2, egységteszt U1, U4, U6
F3 Veszteségpolitika tele puffernél az új bájt elveszik, az overrun pontosan az eldobottak számával nő egységteszt U3, U5, U6
P1 ISR-törzsidő stabilitása működő DWT mellett a min..max egy előre rögzített, platformonként megadott sávban; DWT nélkül SKIP M parancs (K5a formátum, K5b sáv)
T1 Időbázis a tickfrekvencia 100 Hz ±20% egy valós idejű futtatón; nem valós idejűnél SKIP rendszerteszt T1

Tele puffernél tehát nem írunk elő veszteségmentességet: az F2 csak a kapacitáson belüli esetekre szól, a tele puffert az F3 szabályozza. A kritérium rögzítése azért kötelező, mert utólag mindig könnyű „hozzáigazítani” az elvárást az eredményhez. Az előzetes kritérium az egyetlen védelem ez ellen.

20.3.4 A megszakítás-latencia mérése a modulon

Az a071 alfejezetben megismert módszert itt a konkrét modulra alkalmazzuk, de pontosan meg kell nevezni, mit mér. A lab-m19 UART-ISR-je az első végrehajtható utasításában és a visszatérés előtt is beolvassa a DWT CYCCNT számlálót. A különbség az ISR-törzsidő: a két mérőpont között eltelt ciklusszám. Ez nem belépési latencia:

A mintát az ISR csak elmenti egy tömbbe. A statisztikát (min, max, mean) és a kiírást a fő program végzi az M parancsra, a mért blokkon kívül, így a formázás és a kiírás nem torzítja a mérést.

A DWT jelenlétét és a CYCCNT haladását futás előtt ellenőrizni kell. A QEMU 8.2.2 mps2-an385 modelljében a számláló nem halad: itt nincs érvényes ciklusszám, a mérés SKIP. Más emulátoron a működő számláló sem jelent automatikusan cikluspontos modellt. Fizikai hardveren a mért ciklusszámot külön jegyzőkönyvezzük.

20.3.4.1 A latencia forrása — levezetés

Az eseménytől az ISR végéig eltelt idő több összetevőből áll:

Cesemény→vég=Cbelépés+Cprológus előtti rész+Ctörzs+Cmérési overhead C_\text{esemény→vég} = C_\text{belépés} + C_\text{prológus előtti rész} + C_\text{törzs} + C_\text{mérési~overhead}

A K5 tehát csak a Ctörzs+Cmérési overheadC_\text{törzs} + C_\text{mérési~overhead} részt adja. A teljes, eseménytől számított latenciához eseményoldali időbélyeg kell. Timer-megszakításnál ez rendelkezésre áll: a Timer0 VALUE regisztere a nullátmenet óta számol vissza, így az ISR elején kiolvasott RELOAD−VALUE\text{RELOAD} - \text{VALUE} az eseménytől eltelt timer-ütemek száma (ha a timer órajele a CPU-órajel). UART-nál a hardver nem ad ilyen időbélyeget; ott külső triggerre (logikai analizátor, GPIO-jel) van szükség, ami csak hardveren végezhető.

20.3.5 Kidolgozott példa: a teszt és a mérés futása

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

Az alábbi két program futtatható: mindkettőt lefuttattuk (a körülményeket lásd lent). A firmware forrásait és a Makefile-t az a079 adja; a make test mindkét réteget elindítja.

Egységteszt a hoston (make ringtest):

/* lab-m19-ring-test.c — a ring buffer egységtesztje a HOSTON (gcc), hardver nélkül.
 *   gcc -std=c11 -Wall -Wextra -o lab-m19-ring-test lab-m19-ring-test.c lab-m19-ring.c */
#include <stdio.h>
#include <string.h>
#include "lab-m19-ring.h"

static int fails;
#define CHECK(name, cond) do {                                      \
        int ok_ = (cond);                                           \
        printf("%s  %s\n", ok_ ? "PASS" : "FAIL", name);            \
        fails += !ok_;                                              \
    } while (0)

int main(void)
{
    ring_t r;
    uint8_t b, out[RING_CAP];

    /* U1 alapeset: 3 bájt be, ugyanaz a 3 ki, ugyanabban a sorrendben */
    memset(&r, 0, sizeof r);
    ring_push(&r, 0x41); ring_push(&r, 0x42); ring_push(&r, 0x43);
    int n = 0;
    while (ring_pop(&r, &b)) out[n++] = b;
    CHECK("U1 alapeset: count = 3, sorrend ABC",
          n == 3 && out[0] == 0x41 && out[1] == 0x42 && out[2] == 0x43);

    /* U2 üres puffer: nem olvas, a tail helyben marad */
    uint16_t tail = r.tail;
    CHECK("U2 üres puffer: pop = 0, tail változatlan", ring_pop(&r, &b) == 0 && r.tail == tail);

    /* U3 tele puffer: RING_CAP-1 fér el; a többi eldobva és számolva */
    memset(&r, 0, sizeof r);
    int accepted = 0;
    for (int i = 0; i < 100; i++) accepted += ring_push(&r, (uint8_t)i);
    CHECK("U3 tele: pontosan RING_CAP-1 = 63 bájt fér el", accepted == RING_CAP - 1);
    CHECK("U3 tele: overrun = 100 - 63 = 37", r.overrun == 100u - (RING_CAP - 1));
    n = 0;
    while (ring_pop(&r, &b)) out[n++] = b;
    int order = n == RING_CAP - 1;
    for (int i = 0; i < n; i++) order = order && out[i] == (uint8_t)i;
    CHECK("U3 tele: a megtartott bájtok az első 63, sorrendben (az újak vesznek el)", order);

    /* U4 körbefordulás: sok kis adag a puffer végén át, veszteség nélkül */
    memset(&r, 0, sizeof r);
    int ok = 1; uint8_t next_in = 0, next_out = 0;
    for (int round = 0; round < 1000; round++) {
        for (int k = 0; k < 5; k++) ok &= ring_push(&r, next_in++);
        for (int k = 0; k < 5; k++) ok &= ring_pop(&r, &b) && b == next_out++;
    }
    CHECK("U4 körbefordulás: 5000 bájt kapacitáson belül, veszteség és csere nélkül",
          ok && r.overrun == 0);

    /* U5 határ: N-1 elem még befér, az N. elemre előírt az overflow */
    memset(&r, 0, sizeof r);
    int all = 1;
    for (unsigned i = 0; i < RING_CAP - 1; i++) all &= ring_push(&r, (uint8_t)i);
    CHECK("U5 határ: az N-1 = 63. elem még befér", all && r.overrun == 0);
    CHECK("U5 határ: az N. elem elutasítva, overrun = 1",
          ring_push(&r, 0xEE) == 0 && r.overrun == 1);

    /* U6 lassú fogyasztó: kettőt tesz be, egyet vesz ki; a sorrend megmarad,
       a veszteség pontosan az elutasított push-ok száma */
    memset(&r, 0, sizeof r);
    unsigned pushed = 0, rejected = 0, popped = 0;
    int inorder = 1;
    uint8_t seq[512]; unsigned nseq = 0;            /* az elfogadott bájtok sorrendje */
    for (int round = 0; round < 200; round++) {
        for (int k = 0; k < 2; k++) {
            uint8_t v = (uint8_t)pushed++;
            if (ring_push(&r, v)) seq[nseq++ % 512] = v; else rejected++;
        }
        if (ring_pop(&r, &b)) { inorder &= b == seq[popped % 512]; popped++; }
    }
    while (ring_pop(&r, &b)) { inorder &= b == seq[popped % 512]; popped++; }
    CHECK("U6 lassú fogyasztó: kivett = elfogadott, sorrendben", inorder && popped == nseq);
    CHECK("U6 lassú fogyasztó: overrun = elutasított push-ok",
          r.overrun == rejected && rejected > 0);

    puts(fails ? "RINGTEST: FAIL" : "RINGTEST: PASS");
    return fails != 0;
}

Rendszerteszt host-oldali soros bemenettel (make systest). A teszt alapértelmezésben a QEMU-t indítja; a --runner kapcsolóval bármely más futtató megadható, amely a UART0-t a stdin/stdout-ra köti. A teszt három szabályt követ.

#!/usr/bin/env python3
"""lab-m19-systest.py — a lab-m19 alapfeladat rendszertesztje host-oldali soros bemenettel.

Használat:
  python3 lab-m19-systest.py                       # QEMU (alapértelmezett futtató)
  # más futtató, amely a UART0-t stdin/stdout-ra köti
  python3 lab-m19-systest.py --runner "<parancs>"
  További kapcsolók:
    --no-timing        a T1 frekvenciapróba SKIP (nem valós idejű futtatónál kötelező)
    --isr-band LO:HI   a K5b platformsáv: az ISR-törzsidő min..max ciklusban
    --reset-delay SEC  szünet a reset után (a K4 ütemezésfüggetlenségének ellenpróbája)
A firmware UART0-ját a futtató a folyamat stdin/stdout-jára köti; a teszt ide ír
bájtokat (RX-injektálás) és innen olvassa a választ. Kilépési kód: 0, ha nincs FAIL.

Két szint: a funkcionális próbák (K1-K4, K6, F2, K5a) a host időzítésétől
függetlenek; az időzítési próba (T1) és a hardveres sáv (K5b) külön eredmény.
"""
import queue
import re
import shlex
import subprocess
import sys
import threading
import time

QEMU = ("qemu-system-arm -machine mps2-an385 -cpu cortex-m3 -display none "
        "-monitor none -serial stdio -kernel lab-m19.elf")
TICK_HZ = 100
LAB_SAMPLES = 64
TICK_LINE = 'tick\r\n'
STATUS = r'status: ticks=(\d+) overrun=(\d+) raw=(\d+)\r\n'
# Heterogén, determinisztikus burst: a vezérlőparancsokat (S, R, M) és a "tick"
# részsztringet nem tartalmazó ábécéből, kapacitáson belül (50 < 63).
ALPHA = '0123456789ABCDEFGHJKLNOPQTUVWXYZ'
BURST = ''.join(ALPHA[(7 * i + 3) % len(ALPHA)] for i in range(50))


class Dut:
    def __init__(self, cmd):
        self.p = subprocess.Popen(shlex.split(cmd), stdin=subprocess.PIPE,
                                  stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
        self.q = queue.Queue()
        self.buf = ''
        threading.Thread(target=self._reader, daemon=True).start()

    def _reader(self):
        while True:
            b = self.p.stdout.read(1)
            if not b:
                return
            self.q.put(b.decode('latin-1'))

    def send(self, s):
        try:
            self.p.stdin.write(s.encode())
            self.p.stdin.flush()
        except (BrokenPipeError, OSError):
            # a futtató kilépett: a várakozó olvasás bukik el
            pass

    def read_until(self, pattern, timeout=10.0):
        """Vár, amíg a pattern megjelenik; a találat végéig fogyasztja a puffert.
        Visszaadja a (találat előtti szöveg, match) párt; időtúllépésnél match = None."""
        rx = re.compile(pattern)
        end = time.time() + timeout
        while True:
            m = rx.search(self.buf)
            if m:
                before, self.buf = self.buf[:m.start()], self.buf[m.end():]
                return before, m
            try:
                self.buf += self.q.get(timeout=max(0.0, end - time.time()))
            except queue.Empty:
                return self.buf, None

    def close(self):
        self.p.kill()


def strip_ticks(text):
    """Az aszinkron tick sorokat a protokoll bárhol beékelheti: ezeket eltávolítjuk."""
    return text.replace(TICK_LINE, '')


def framed_echo(dut, data):
    """Elküldi az adatot, majd egy 'S'-t; a status sorig érkezett bájtoknak (a tick
    sorok nélkül) PONTOSAN az adatnak kell lenniük: ez kizárja a hiányzó,
    duplikált, többlet- és felcserélt bájtot."""
    dut.send(data + 'S')
    before, m = dut.read_until(STATUS)
    got = strip_ticks(before)
    return got == data, got, m


def status(dut):
    dut.send('S')
    _, m = dut.read_until(STATUS)
    return int(m.group(1)) if m else None


def status_raw(dut):
    """(ticks, raw): a bázishoz mért és a nyers számláló ugyanabból a pillanatképből."""
    dut.send('S')
    _, m = dut.read_until(STATUS)
    return (int(m.group(1)), int(m.group(3))) if m else None


def main(argv):
    cmd = argv[argv.index('--runner') + 1] if '--runner' in argv else QEMU
    timing = '--no-timing' not in argv
    delay = float(argv[argv.index('--reset-delay') + 1]) if '--reset-delay' in argv else 0.0
    band = None
    if '--isr-band' in argv:
        lo, hi = argv[argv.index('--isr-band') + 1].split(':')
        band = (int(lo), int(hi))
    dut = Dut(cmd)
    results = []

    def check(name, ok, detail=''):
        results.append((name, 'PASS' if ok else 'FAIL', detail))

    def skip(name, detail):
        results.append((name, 'SKIP', detail))

    try:
        _, m = dut.read_until(r'lab-m19 boot\r\n')
        check('K1 boot', m is not None)

        ok, got, m = framed_echo(dut, 'ab')               # K2: echo, pontosan
        check('K2 echo pontosan "ab"', ok, repr(got) if not ok else '')

        ok, got, m = framed_echo(dut, BURST)               # F2: kapacitáson belüli burst
        check('F2 burst 50 heterogén bájt: pontos sorrend és hossz', ok,
              '' if ok else f'{len(got)} bájt érkezett')
        check('K3 status formátum, overrun=0', m is not None and m.group(2) == '0',
              m.group(0).strip() if m else 'nincs válasz')

        # K6: funkcionális, host-időzítéstől független
        t1 = int(m.group(1)) if m else None
        lines = 0
        for _ in range(2):
            _, mt = dut.read_until(re.escape(TICK_LINE), timeout=5.0)
            lines += mt is not None
        check('K6 periodikus tick sor (2 db)', lines == 2)
        t2 = status(dut)
        check('K6 két tick sor alatt a számláló legalább 100-zal nőtt',
              t1 is not None and t2 is not None and t2 - t1 >= TICK_HZ, f'{t1} -> {t2}')

        def reset_probe(name):                           # K4: a reset-válasz bázisa
            before = status_raw(dut)
            dut.send('R')
            _, mr = dut.read_until(r'reset ok base=(\d+)\r\n')
            time.sleep(delay)                            # ellenpróba: hostkésés
            after, later = status_raw(dut), status_raw(dut)
            base = int(mr.group(1)) if mr else None
            # A reset ténye a válaszban rögzített bázisból következik, nem abból, hogy
            # egy későbbi minta kisebb: a ticks = raw - bázis bármekkora késés mellett igaz.
            ok = (None not in (before, after, later, base) and base >= before[1]
                  and after[0] == after[1] - base and later[0] == later[1] - base
                  and later[1] >= after[1])
            fmt = lambda v: f'{v[0]}/{v[1]}' if v else '-'  # ticks/raw
            check(name, ok, f'bázis {base}, utána {fmt(after)}, később {fmt(later)}')

        reset_probe('K4 reset: ticks = raw - bázis, monoton')
        dut.read_until(re.escape(TICK_LINE), timeout=5.0)
        reset_probe('K4 ismételt reset: ugyanígy')

        if timing:                                       # T1: időzítési szerződés, külön
            ta, wa = status(dut), time.monotonic()
            time.sleep(2.0)
            tb, wb = status(dut), time.monotonic()
            rate = (tb - ta) / (wb - wa) if None not in (ta, tb) else 0.0
            check(f'T1 tickfrekvencia {TICK_HZ} Hz ±20% (host-órához mérve)',
                  0.8 * TICK_HZ <= rate <= 1.2 * TICK_HZ, f'{rate:.1f} Hz')
        else:
            skip('T1 tickfrekvencia', 'nem valós idejű futtató (--no-timing)')

        dut.send('M')                                    # K5: ISR-törzsidő
        _, m4 = dut.read_until(r'isr_body: ([^\r]*)\r\n')
        body = m4.group(1) if m4 else None
        if body is not None and body.startswith('SKIP'):
            skip('K5a ISR-törzsidő formátum', body)
            skip('K5b ISR-törzsidő platformsáv (P1)', body)
        else:
            mm = re.fullmatch(r'n=(\d+) min=(\d+) max=(\d+) mean=(\d+)'
                              r'(?: median=(\d+))?(?: sd=(\d+))?', body or '')
            ok = bool(mm)
            if ok:
                n, mn, mx, mean = (int(x) for x in mm.groups()[:4])
                med = mm.group(5)                    # a 2. feladat opcionális mezője
                ok = (0 < n <= LAB_SAMPLES and mn <= mean <= mx
                      and (med is None or mn <= int(med) <= mx))
            check('K5a ISR-törzsidő formátum: 0 < n <= 64, min <= mean <= max',
                  ok, body or 'nincs válasz')
            if band is None:
                skip('K5b ISR-törzsidő platformsáv (P1)', 'nincs --isr-band megadva')
            else:
                check(f'K5b ISR-törzsidő platformsáv (P1) {band[0]}..{band[1]}',
                      ok and band[0] <= mn and mx <= band[1], body or '')
    finally:
        dut.close()

    for name, res, detail in results:
        print(f'{res:4}  {name}' + (f'  [{detail}]' if detail else ''))
    fails = sum(r == 'FAIL' for _, r, _ in results)
    print('SYSTEST:', 'FAIL' if fails else 'PASS')
    return 1 if fails else 0


if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))
make test                        # ringtest + systest
qemu-system-arm --version | head -1   # a jegyzőkönyvbe
sha256sum lab-m19.elf                 # a jegyzőkönyvbe

20.3.6 Várható kimenet és ellenőrzési feltétel

A két teszt kimenete. Az egységtesztet a host gcc-jével (Ubuntu 24.04, gcc 13.3) futtattuk. A rendszertesztet QEMU helyett a tools/cm-run.py Cortex-M3 emulátorral (--no-timing --runner "python3 tools/cm-run.py lab-m19.elf --uart-stdin --max 5000000000"), a helyettesítő eszközlánccal (Clang + ld.lld) épített firmware-en; az emulátor a Timer0-t, a UART0 RX-et és a megszakítás-kézbesítést funkcionálisan modellezi, de nem valós idejű, ezért a T1 itt SKIP:

PASS  U1 alapeset: count = 3, sorrend ABC
PASS  U2 üres puffer: pop = 0, tail változatlan
PASS  U3 tele: pontosan RING_CAP-1 = 63 bájt fér el
PASS  U3 tele: overrun = 100 - 63 = 37
PASS  U3 tele: a megtartott bájtok az első 63, sorrendben (az újak vesznek el)
PASS  U4 körbefordulás: 5000 bájt kapacitáson belül, veszteség és csere nélkül
PASS  U5 határ: az N-1 = 63. elem még befér
PASS  U5 határ: az N. elem elutasítva, overrun = 1
PASS  U6 lassú fogyasztó: kivett = elfogadott, sorrendben
PASS  U6 lassú fogyasztó: overrun = elutasított push-ok
RINGTEST: PASS
PASS  K1 boot
PASS  K2 echo pontosan "ab"
PASS  F2 burst 50 heterogén bájt: pontos sorrend és hossz
PASS  K3 status formátum, overrun=0  [status: ticks=4 overrun=0 raw=4]
PASS  K6 periodikus tick sor (2 db)
PASS  K6 két tick sor alatt a számláló legalább 100-zal nőtt  [4 -> 200]
PASS  K4 reset: ticks = raw - bázis, monoton  [bázis 200, utána 0/200, később 0/200]
PASS  K4 ismételt reset: ugyanígy  [bázis 300, utána 0/300, később 0/300]
SKIP  T1 tickfrekvencia  [nem valós idejű futtató (--no-timing)]
SKIP  K5a ISR-törzsidő formátum  [SKIP (DWT CYCCNT nem halad)]
SKIP  K5b ISR-törzsidő platformsáv (P1)  [SKIP (DWT CYCCNT nem halad)]
SYSTEST: PASS

Ellenpróba: a teszt elkapja-e a hibát? A teszt saját ellenpróbájához egy hamis DUT-ot (a protokollt emuláló Python-programot) futtattunk a --runner helyén, szándékos hibákkal:

Hamis DUT hibája Eredmény
a burst egy bájtját kétszer küldi vissza (51 bájt) FAIL F2 … [51 bájt érkezett]
két szomszédos, eltérő bájtot felcserél FAIL F2 … [50 bájt érkezett]
egy bájtot elhagy FAIL F2 … [49 bájt érkezett]
200 Hz-es tick (kétszeres timerfrekvencia) FAIL T1 … [199.9 Hz]
isr_body: n=0 FAIL K5a …
isr_body: n=64 min=30 max=20 mean=25 FAIL K5a …
sávon kívüli mérés (max=90, --isr-band 15:40) FAIL K5b …
hibátlan, aszinkron tick sorokkal minden funkcionális próba PASS

Egy korábbi változat, amely 50 darab x-et küldött és regexszel kereste, az első két hibás DUT-ra hamis PASS-t adott. A firmware egy szándékosan elrontott változatában (az UART-ISR nem nyugtázza az INTSTATUS.RXINT-et) a megszakítás a visszatérés után azonnal újra kérődik, és a fő program nem jut processzorhoz. A rendszerteszt erre a K1 boot után minden sorban FAIL-t adott, SYSTEST: FAIL eredménnyel.

Az ellenőrzési feltétel a hallgató számára:

  1. minden teszteset PASS legyen (F1); a K5 QEMU-n SKIP;
  2. a kapacitáson belüli burst sorrendben és hiánytalanul visszaérkezzen (F2), a tele puffer viselkedését pedig az U3 igazolja (F3);
  3. működő DWT esetén az ISR-törzsidő min és max értéke egy előre rögzített, platformparaméterként (--isr-band LO:HI) megadott sávban legyen (P1, K5b). Nem működő számlálónál a ciklusmérés eredménye SKIP; a nullákból képzett statisztika nem érvényes mérés;
  4. a tickfrekvencia (T1) csak valós idejű futtatónál (QEMU valós idővel, illetve hardver) értékelhető; a funkcionális próbák eredménye ettől független.

Vázlat (nem futtatott). Az ISR-törzsidő-hisztogram a P1 kiértékelésének javasolt formája: a működő DWT-vel gyűjtött 64 minta eloszlása 1 ciklus felbontású sávokra bontva. Az alábbi ábra illusztratív, kitalált adatokat mutat, hogy a formátum és a szerkezet látható legyen; a tényleges értékeket hardveren kell feltölteni.

Illusztratív ISR-törzsidő-eloszlás (vázlat, nem mért) - Hipotetikus, nem mért érték. Valós ciklusadat csak működő DWT mellett gyűjthető; QEMU 8.2.2 AN385 alatt SKIP.

Az ugyanezen adat táblázatos formában (a forrás Markdownban is olvasható módon):

ISR-törzsidő [ciklus] Minták száma
18 4
19 9
20 15
21 17
22 11
23 5
24 2
25 1
Össz 64

20.3.7 Reprodukálhatóság: ugyanaz a parancs, ugyanaz a bemenet, ugyanaz a sáv

A reprodukálhatóság e fejezetben háromtényezős szerződés. A jegyzőkönyvben rögzíteni kell:

  1. A pontos parancsot. A make test önmagában nem elég, mert a Makefile-ban és a tesztben lévő QEMU-parancssor változhat. A jegyzőkönyvbe kerül a teljes hívási sor, vagy a makefile és a hívott bináris verziója.
  2. A pontos bemenetet. A teszt-bájtok sorrendje és értéke, valamint a hívások sorrendje. Determinisztikus tesztnél ez fix; ha a bemenet generált, a generátor seedje is rögzül.
  3. Az eredmény-sávot. Nem az eredmény bitre pontos értéke, hanem egy dokumentált tartomány: a funkcionális teszteknél PASS, a latenciánál min..max ciklussáv.

A reprodukálhatósághoz rögzítsük a QEMU-verziót, a machine-definíciót, a teljes parancssort, a firmware hash-ét és a beadott eseményeket. Ugyanazon verzió önmagában nem garantál determinisztikus időzítést; az UART-bemenet ütemezése és a virtuális óra beállítása is számít. A nem működő CYCCNT azonos nulla eredménye nem ismételhető ciklusmérés.

A reprodukálhatóság gyakorlati próbája: kétszer egymás után futtatjuk ugyanazt a parancsot (két külön QEMU-indítással), és a naplókat összehasonlítjuk. A naplóban a szerződés szerint változó mezők is vannak: a státuszsorok tickszámai, a reset előtti és utáni értékek, a T1 mért frekvenciája és a K5 számai a futtató ütemezésétől függnek, helyes működés mellett is. Ezeket az összevetés előtt normalizálni kell (például sed -E 's/[0-9]+/N/g' a […] részletekre), és a funkcionális rész csak így, a próbák neve és PASS/FAIL/SKIP eredménye szerint egyezzen bitre. A latenciarésznek a sávon belül kell maradnia. Ha a második futás más min/max-ot ad, az gyanús: vagy a QEMU nem determinisztikus (pl. több-szálas végrehajtás, ha a máshol emlegetett konfiguráció így van), vagy a modulban van nem determinisztikus viselkedés (pl. head és tail verseny egy véletlen ütemezett olvasóval).

20.3.7.1 Ellenőrzési igény

A QEMU mps2-an385 machine viselkedésének stabilitása verzióközi viszonyításban ebben a munkafolyamatban nem ellenőrzött. Ezért a jegyzőkönyvben a QEMU-verziót rögzítettként kell kezelni: a „reprodukálható” kijelentés csak ahhoz a verziohoz szól, nem „QEMU általában”. A dokumentációs kiindulópont a QEMU System Emulation kézikönyv, de a konkrét machine-definíció stabilitása ellenőrzést igényel az adott verzióra.

20.3.8 Fizikai STM32 Nucleo (opcionális)

Fizikai Cortex-M3 panelhez a startupot, a linker scriptet, a perifériacímeket és az IRQ-kiosztást a konkrét MCU-hoz kell igazítani, majd külön binárist építeni. A működő DWT számlálóval kapott eredmény valódi hardvermérés; a QEMU 8.2.2 AN385 esetében a ciklusmérés helyén SKIP szerepel. A fizikai értékeknél sem feltételezhető általánosan, hogy nagyobbak vagy kisebbek egy másik modell eredményénél.

A fizikai próba a perifériás működést és az időzítési követelmények teljesülését igazolhatja. A hiányzó emulált ciklusszámhoz nem számolunk gyorsulási vagy latenciaarányt; az emuláció funkcionális eredményeit külön hasonlítjuk össze.

20.3.9 Elfogadási feltétel kiértékelése

Az F1, F2, F3 és P1 kritériumokat a futás után értékeljük ki. Az értékelés a jegyzőkönyv záró szakasza:

Kritérium Elvárt Mért Értékelés
F1 minden teszt PASS (naplóból) PASS/FAIL
F2 sorrend- és hosszegyezés a kapacitáson belül (rendszerteszt, U1, U4) PASS/FAIL
F3 tele puffer: az új bájt elveszik, overrun pontos (U3) PASS/FAIL
P1 ISR-törzsidő min..max az előre rögzített sávban (M parancs) PASS/FAIL, DWT nélkül SKIP

A PASS/FAIL nem „igen/nem” jellegű eldöntés a fejlesztő fejében, hanem dokumentált, előre rögzített küszöbökkel. Ha valamelyik kritérium nem teljesül, a fejezet vége nem a modul „kudarca”, hanem egy hipotézis: a modul vagy a mérési módszer hibás. A tipikus hibák szakasz ennek szétválasztásában segít.

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

20.3.11 Feladatok

1. (magyarázó, alapszint — cél: megértés-ellenőrzés) Magyarázza meg, miért nem elég egyetlen PASS a funkcionális tesztből, és miért van szükség a P1 latenciastabilitási kritériumra is. Adjon példát egy olyan helyzetre, amikor a funkcionális teszt zöld, de a modul teljesítmény-szempontból nem elfogadható. Mutassa meg, az ön által adott példában melyik mérési pontok között kellene a latenciát definiálni.

2. (implementációs, középszint — cél: rutinépítés) Egészítse ki a lab-m19-main.c measure_line() függvényét úgy, hogy a min, max, mean mellett kiszámítsa a mediánt és a szórást (négyzetes átlageltérés, egész aritmetikával), és a meglévő UART-sorban írja ki. A minták az isr_body[] tömbben vannak; az UART-ISR (UART0_Handler) minden lefutásakor egy minta kerül bele, amíg az isr_body_n el nem éri a LAB_SAMPLES-t (64), utána a gyűjtés megáll. Oldja meg azt is, hogy az M parancs a jelentés után új mérési ablakot indítson, úgy, hogy az isr_body_n-t ne írja két kontextus versenyben. Adja meg a kimenet formátumát, és mutassa meg, hogy a rendszerteszt K5a/K5b próbája elfogadja.

3. (hibakeresési/mérési, középszint — cél: rutinépítés) Egy működő DWT-vel rendelkező futtatón a modul első futásán a mért min..max ciklussáv 18..26, a másodikon 30..52. Sorolja fel, milyen lépéseket tenne annak eldöntésére, hogy a különbség a futtatókörnyezetből, a firmware-binárisból vagy a mérési módszerből (a bemeneti terhelésből és a mintavétel ablakából) ered. Konkrét parancsokat adjon, és fogalmazza meg, mit rögzítene a jegyzőkönyvben a döntés után.

4. (integráló, elmélyült — cél: fejezetek összekapcsolása: a048 mérési módszertana + a071 többcsúcsú latencia) Az a048 alfejezet a NEON-mérésnél szándékosan a mediánt használja, mert az kiugró értékekre robusztus. Az a071 megmutatta, hogy egy latenciaeloszlásnak több csúcsa is lehet, és hogy a csúcsok okát kontrollkísérlet választja szét. Vizsgálja meg: alkalmas-e a measure_line() min/max/mean statisztikája, illetve a 2. feladat mean és median értékének különbsége arra, hogy egy kétcsúcsú eloszlást felismerjen? Indokoljon egy 64 elemű ellenpéldával, és adja meg, mivel lehet a kétcsúcsúságot valóban kimutatni, illetve mi kell ahhoz, hogy egy csúcsot egy mechanizmushoz kössünk.

20.3.12 Megoldások

1. megoldás — Segítség. Gondolj egy ISR-re, amely MINDIG helyesen viselkedik (soha nem veszít adatot), de néha sokáig tart — a funkcionális teszt ezt a „néha sokáig” tulajdonságot tudja-e észlelni?

1. megoldás — Teljes megoldás. A funkcionális teszt egy tulajdonságot (sorrend, hossz, nem-elveszés) ellenőriz, nem mond semmit arról, mennyi ideig tart az ISR. Egy helyes, de lassú ISR átengedheti a tesztet, miközben egy valós, kis órajelű rendszerben a következő megszakítást már nem tudja fogadni. A P1 éppen ezt a dimenziót fedi le: a latencia stabilitását. Példa: 1 kHz-es mintavételi ütem mellett egy 20 ciklusos belső munka rendben, de ha a latencia szórása 50%-os és a következő esemény 30 cikluson belül érkezik, elveszhet az adat. A mérési pontok itt: az ISR első utasítása és a head írás befejezése.

2. megoldás — Segítség. A mediánhoz rendezett mintasor kell: az isr_body[] egy helyi másolatán végezhetsz egyszerű rendezést. Az isr_body_n-t az ISR írja; ha a fő program is nullázza, a kettőnek nem szabad versenyben futnia. Melyik eszköz zárja ki, hogy a másolás és a nullázás közben az ISR lefusson?

2. megoldás — Teljes megoldás. A measure_line() helyére (a többi függvény változatlan):

/* Kiegészítés a lab-m19-main.c-hez (2. feladat): a measure_line() helyett. */
static uint32_t isqrt32(uint32_t v)           /* egész négyzetgyök, lefelé kerekítve */
{
    uint32_t r = 0, bit = 1u << 30;
    while (bit > v) bit >>= 2;
    while (bit) {
        if (v >= r + bit) { v -= r + bit; r = (r >> 1) + bit; }
        else r >>= 1;
        bit >>= 2;
    }
    return r;
}

static void measure_line(void)               /* K5, mediánnal és szórással */
{
    if (!lab_have_dwt) {
        uart_puts("isr_body: SKIP (DWT CYCCNT nem halad)\r\n");
        return;
    }
    uint32_t v[LAB_SAMPLES];
    uint32_t pm = crit_enter();              /* pillanatkép és új mérési ablak:      */
    uint32_t n = isr_body_n;                 /* az ISR ezalatt nem írhat              */
    for (uint32_t i = 0; i < n; i++)
        v[i] = isr_body[i];
    isr_body_n = 0;                          /* a következő M új ablakot mér          */
    crit_exit(pm);

    uart_puts("isr_body: n="); uart_put_u32(n);
    if (n) {
        for (uint32_t i = 1; i < n; i++) {   /* beszúró rendezés (n <= 64) */
            uint32_t x = v[i], j = i;
            for (; j > 0 && v[j - 1] > x; j--) v[j] = v[j - 1];
            v[j] = x;
        }
        /* floor(mean) a teljes uint32 tartományban, 64 bites osztás nélkül:
           sum(v) = n*sum(v/n) + sum(v%n); sum(v/n) <= max, sum(v%n) < n*n <= 4096 */
        uint32_t q = 0, r = 0;
        for (uint32_t i = 0; i < n; i++) {
            q += v[i] / n;
            r += v[i] % n;
        }
        uint32_t mean = q + r / n;
        /* páros n: a két középső elem egész átlaga lefelé kerekítve, túlcsordulás nélkül */
        uint32_t lo = v[(n - 1) / 2], hi = v[n / 2];
        uint32_t median = lo + (hi - lo) / 2u;
        uart_puts(" min=");    uart_put_u32(v[0]);
        uart_puts(" max=");    uart_put_u32(v[n - 1]);
        uart_puts(" mean=");   uart_put_u32(mean);
        uart_puts(" median="); uart_put_u32(median);
        /* a négyzetösszeg saját korlátja: |v - mean| <= max - min, így
           max - min < 8192 mellett n * 8191^2 < 2^32; felette az sd= mező elmarad */
        if (v[n - 1] - v[0] < 8192u) {
            uint32_t sq = 0;
            for (uint32_t i = 0; i < n; i++) {
                uint32_t d = v[i] > mean ? v[i] - mean : mean - v[i];
                sq += d * d;
            }
            uart_puts(" sd=");     uart_put_u32(isqrt32(sq / n));
        }
    }
    uart_puts("\r\n");
}

A minták átvétele és az isr_body_n nullázása egy kritikus szakaszban történik (crit_enter/crit_exit, a PRIMASK megőrzésével). Így az ISR nem írhat közben a tömbbe, és nem lesz két, versenyben lévő író; a következő M a nullázás óta érkezett, legfeljebb 64 mintát jelenti. A gyűjtés LAB_SAMPLES-nél megáll, ezért egy hosszú burst után a minták csak az első 64 UART-megszakításból származnak; az új ablakot ezért a mérendő terhelés előtt kell nyitni egy M-mel. A statisztika a teljes uint32_t mintatartományban helyes, és nem kell hozzá 64 bites osztás. Egy naiv uint32_t sum 64 darab 0x04000000-s mintánál éppen 2322^{32}-re érne, és nullára fordulna (mean=0 a helyes 67 108 864 helyett). 64 darab 0x80000000-s mintánál pedig az átlag és a páros medián (v[31] + v[32]) / 2 alakja is 0 lenne, ami a K5a min ≤ mean feltételét is megsértené. Ezért:

Ebből következik, hogy min ≤ mean ≤ max és min ≤ median ≤ max mindig teljesül; egyforma mintánál mindhárom egyenlő. A kimenet: isr_body: n=64 min=… max=… mean=… median=… sd=…. Hoston (gcc 13.3, tools/lab-m19/feladat2/) a lektori kontrollokra és néhány határesetre ezt adta. Utolsó sora 200 000 véletlen mintasort vet össze egy 64 bites referencia-átlaggal:

isr_body: n=64 min=14 max=14 mean=14 median=14 sd=0
isr_body: n=64 min=20 max=20 mean=20 median=20 sd=0
isr_body: n=64 min=14 max=20 mean=17 median=17 sd=3
isr_body: n=64 min=67108864 max=67108864 mean=67108864 median=67108864 sd=0
isr_body: n=64 min=2147483648 max=2147483648 mean=2147483648 median=2147483648 sd=0
isr_body: n=64 min=4294967295 max=4294967295 mean=4294967295 median=4294967295 sd=0
isr_body: n=64 min=4294967294 max=4294967295 mean=4294967294 median=4294967294 sd=0
isr_body: n=63 min=1 max=4294967295 mean=2113396605 median=1
isr_body: n=12 min=10 max=60 mean=16 median=10 sd=14
random mean-ellenőrzés: 0 eltérés / 200000

A rendszerteszt K5a próbája ezt is elfogadja, a mediánt is a min..max tartományban várja; a K5b ugyanúgy a min/max-ot veti össze a platformsávval. A kiegészítést a helyettesítő eszközlánccal Thumb-2-re, -nostdlib mellett és -lgcc nélkül lefordítottuk és linkeltük (-O0, -O1, -O2). Új külső szimbólum nem keletkezett, az osztások udiv utasítások lettek, és a rendszerteszt PASS-szal futott vele (a K5 a DWT hiánya miatt SKIP). A statisztikai részt a fenti hostpróba ellenőrzi; a 4. megoldás értékei is ebből származnak.

3. megoldás — Segítség. Rangsorold a lehetséges okokat aszerint, melyiket a legolcsóbb ellenőrizni (verziószám lekérdezése) és melyiket a legdrágább (kód újraírása) — és haladj ez alapján.

3. megoldás — Teljes megoldás. A döntési fa, a legolcsóbb ellenőrzéssel kezdve. (a) A futtatókörnyezet: a futtató és verziója (qemu-system-arm --version, illetve a panel és a mag típusa), és hogy működik-e a DWT; ha bármelyik eltér a jegyzőkönyvben rögzítettől, a különbség innen is eredhet. (b) A bináris azonossága külön, explicit ellenőrzés: sha256sum lab-m19.elf a két futás előtt. Ha a hash eltér, az arm-none-eabi-objdump -d lab-m19.elf összevetése a UART0_Handler két DWT_CYCCNT-olvasása közötti utasításokon mutatja meg, mi változott a mért szakaszban (readelf -h ehhez nem elég: a fejléc azonos lehet eltérő kód mellett is). (c) Ha a futtató és a bináris is azonos, a mérési módszer a gyanús. Az isr_body[] az UART-ISR első 64 lefutását gyűjti a boot vagy az utolsó M óta, utána a gyűjtés megáll. Ha a két futás bemenete eltért (más bájtszám, burst vagy egyenként küldött bájtok, eltérő overrun), az ISR más útvonalai kerültek a mintába: például tele pufferben a ring_push elutasító ága. A minták egy része akár egy korábbi terhelésből is származhat, ha a mérés előtt nem nyitottunk új ablakot. Mintatömb-túlcsordulás nincs: az ISR isr_body_n < LAB_SAMPLES mellett ír. A jegyzőkönyvbe a futtatót és verzióját, a firmware sha256sum-ját, a pontos bemeneti sorozatot, az n-t és a mérési ablak kezdetét rögzítjük, a „sáv eltolódott” megfigyelést a hipotézissel együtt.

4. megoldás — Segítség. Számold ki a mean-t és a mediánt egy olyan 64 elemű sorra, amelynek fele 14, fele 20. Hol van a medián, ha páros mintaszámnál a két középső elem átlagát vesszük? És mi történik egy egycsúcsú, de ferde (néhány nagy kiugró értékkel rendelkező) sornál?

4. megoldás — Teljes megoldás. Egyik összesítő sem bimodalitásjelző. Ellenpélda: 32 darab 14 és 32 darab 20 ciklusos minta. Az eloszlásnak két különálló csúcsa van, mégis mean = 17 és median = (14 + 20) / 2 = 17, a különbség nulla; a min = 14, max = 20 pedig egy 14 és 20 közötti egyetlen csúcsnál is ugyanennyi lehetne. Fordítva: egy egycsúcsú, de ferde, 12 elemű sor (tíz darab 10–12 ciklusos érték és két kiugró érték: 30 és 60) mean = 16, median = 10 eredményt ad, vagyis nagy különbséget, kétcsúcsúság nélkül. A 2. megoldás kódja hoston pontosan ezeket az értékeket adta. A |mean − median| tehát legfeljebb aszimmetriára figyelmeztető leíró jel. A kétcsúcsúságot a hisztogram mutatja meg (az isr_body[] értékeinek eloszlása ciklusonként, ahogy a P1 vázlata javasolja), vagy egy erre való eloszlásvizsgálat. Hogy egy csúcs mely mechanizmushoz tartozik (például láncolt belépéshez, eltérő kódúthoz vagy wait state-hez), azt egyik összesítő sem dönti el: ehhez az a071 3. megoldásának mintájára kontrollkísérlet kell, amelyben a feltételezett mechanizmus dokumentáltan jelen van, illetve hiányzik. Egy ferde, egycsúcsú eloszlás tehát nem bizonyítéka a tail-chainingnek. Az a048 azért használja a mediánt, mert az kiugró értékekre robusztus helyzetmutató, nem azért, mert a kétcsúcsúságot jelezné.

20.3.13 Rövid összefoglaló

Az a080 a modult mérhetővé és ellenőrizhetővé zárja. A funkcionális validáció két futtatható rétegből áll: a ring buffer hoston futó egységtesztjéből (alap, üres, tele, körbefordulás) és a teljes firmware soros bemenettel végzett rendszertesztjéből (K1–K6, F2). Az ISR-törzsidő-mérés a DWT CYCCNT-jével, a mért blokkon kívüli kiértékeléssel dolgozik, és nem azonos az eseménytől számított latenciával, a reprodukálhatóság pedig a háromtényezős szerződésen (parancs, bemenet, sáv) nyugszik. A működő számláló előfeltétel; QEMU 8.2.2 AN385 alatt a ciklusmérés SKIP, a fizikai host eredménye külön oszlop. Az elfogadási kritériumokat a futás előtt rögzítjük, nem utólag. Az eltérések és nem-determinizmus nem hiba, hanem jelzés: hipotézis, adatgyűjtés, egy változtatás, ellenőrzés.

Továbbhasznált definíciók: - funkcionális validáció: előre rögzített bemenet és elvárt tulajdonság (sorrend, hossz, nem-elveszés) összevetése; bináris eredmény. - mérési módszer: a mintavétel helyének, gyakoriságának, a gyűjtött statisztikának és a mértékegységnek a rögzített eljárása. - reprodukálhatóság: rögzített parancs, bemenet és eszközlánc-verzió mellett az eredmény egy dokumentált sávon belül marad.

20.3.14 Források és ellenőrzési hiányok

A linkek és a dokumentumverziók közös állapota az Olvasói útmutató forrásállapot-táblájában van; itt csak az alfejezet saját megállapításai szerepelnek.

Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is ténylegesen fennmaradnak, és amelyeket a jegyzőkönyvben jelölni kell: