A szoftverben a tesztvezérelt fejlesztés hétköznapi dolog. Megírod a bukó tesztet, megírod a kódot, ami zöldre fordítja, aztán refaktorálsz. Fejlesztők milliói csinálják nap mint nap.
A hardverben ritka, és ennek szerkezeti oka van: a visszacsatolási hurok hosszú, az utolsó iteráció pedig drága. Egy fordítási hiba másodpercekbe kerül. Egy szintézis-hiba percekbe. Egy legyártott chipen talált hiba egy shuttle-helybe és több hónapba. Vagyis amit a szoftverben stílus-kérdésként lehet kezelni, az a szilíciumban az egyetlen megfizethető munkamódszer.
A CLI-CPU egy processzor, ami a .NET CIL bájtkódot natívan futtatja, JIT és interpreter nélkül. Kezdettől teszt-előbb épült. Így néz ki ez a gyakorlatban, a repó mai állapotán mérve.
A teszt-készlet nem minőségi szertartás. Egy chip-projektben ez az egyetlen mechanizmus, ami megmondja, hogy az RTL még mindig azt jelenti-e, amit a specifikáció jelentett.
A specifikáció egy futtatható program
Még mielőtt egyetlen sor Verilog létezett volna, a CIL-T0 utasításkészletnek volt egy referencia-implementációja: a CilCpu.Sim, a Nano core C# szimulátora. Ez nem dokumentációs melléktermék. Ez a kanonikus definíciója annak, hogy melyik opkód mit csinál a kiértékelő veremmel, a keret-mutatóval, a hívási mélységgel és a trap-feltételekkel.
A szimulátor mögött ma 187 zöld xUnit teszt áll, a szimulátor-magra, a CIL-T0 linkerre, a futtatóra, a tracerre és a trap-szemantikára:
$ dotnet test src/CilCpu.Sim.Tests/CilCpu.Sim.Tests.csproj Passed! - Failed: 0, Passed: 187, Skipped: 0, Total: 187
Az arányok érdekesek. A szimulátor 3 684 sor C#; a teszt-készlete 3 391 sor. Nagyjából egy sor teszt minden sor implementációra — és a teszt volt előbb.
Az arany vektor: mihez van kötve az RTL
Amikor a Verilog már létezik, az érdekes kérdés nem az, hogy „átmennek-e az RTL tesztek". Hanem az, hogy „ugyanazt jelenti-e az RTL, mint a referencia". Ez két különböző állítás, és gyártás előtt csak a második számít.
Az arany vektor harness közvetlenül erre válaszol. A C# szimulátor egy JSONL végrehajtási nyomvonalat állít elő — utasításonként egy bejegyzést, még az utasítás végrehajtása előtt rögzítve. A cocotb testbench ugyanazt a programot lefuttatja a Verilog magon Verilator alatt, és minden ST_DECODE ciklusban hat architekturális jelet vet össze a nyomvonal megfelelő lépésével:
| RTL jel | Jelentés |
|---|---|
r_pc | utasításszámláló |
r_sp | veremmutató (a frame-end konvencióra normalizálva) |
r_fp | keret-mutató |
r_call_depth | hívási mélység |
r_arg_count | az aktív keret argumentum-száma |
r_local_count | az aktív keret lokális-száma |
Eltérés esetén a teszt mindkét oldalt kiírja — a nyomvonal pc/sp/fp/cd/ac/lc értékeit az RTL pc/sp/fp/cd/ac/lc értékeivel szemben, megnevezett lépésnél és ciklusnál. Nincs értelmezési lépés, nincs „nagyjából stimmel". Vagy ugyanazon az úton ment végig a hardver, mint a referencia, vagy a teszt megnevezi a pontos utasítást, ahol elváltak.
Ez az a mechanizmus, amitől a szimulátorból foggal-körömmel érvényesített specifikáció lesz. A CALL/RET arany teszt például egy 1 → 2 → 1 hívásmélység-átmenetet követ 0 → 2 → 0 argumentum-számmal, nyolc lépésen át. Bármelyik RTL-módosítás, ami csendben elrontja a keret-könyvelést, ott bukik el, azonnal, a hibás lépés megnevezésével.
Amit a hardveres készlet ma mér
Az RTL oldalt a cocotb verifikálja Verilator felett: 322 tesztfüggvény 24 testbench modulban, lefedve az ALU-t, a dekódert, a mikrokód-szekvenszert, a stack cache-t, a szorzót, az osztót, a QSPI vezérlőt, a mailboxot, a GPIO-t, a trace MUX-ot, az UART adót és vevőt, a boot vezérlőt, a decimális printert és az összeállított SoC-ot.
A méretarány az, ami a szoftver felől érkezőket meglepi:
| Réteg | Sor |
|---|---|
Verilog RTL (rtl/src/*.v) | 6 480 |
cocotb testbench-ek (rtl/tb/*.py) | 10 457 |
| C# referencia-szimulátor | 3 684 |
| C# teszt-készlet | 3 391 |
1,6-szor több testbench-kód van, mint RTL. Ez az arány nem a túltesztelés mellékterméke; ennyibe kerül futtatható formában kimondani, hogy egy hardver-darabnak mit kell csinálnia minden határeset minden ciklusában.
Ugyanaz a függvény, három rétegen át
A végponttól végpontig teszt szándékosan unalmas: egy Fibonacci függvény, hétköznapi C#-ban, a samples/PureMath-ban. Attól hasznos, hogy egyetlen forrásfájl táplál három független verifikációs réteget, mindegyik közelebb a fizikai valósághoz, mint az előző.
C# forrás (samples/PureMath, Math.cs)
↓ Roslyn
.dll (CIL bájtkód)
↓ TCliCpuLinker
CIL-T0 bináris
↓
│ 1. réteg C# referencia-szimulátor Assert.Equal(6765, result.Result)
│ 2. réteg Verilog core / Verilator return_value == 55 (rekurzív Fib(10))
└ 3. réteg XC7A200T FPGA, valódi UART 36 37 36 35 0D 0A = "6765\r\n"
Az 1. réteg a RunBinary_Fibonacci20_Returns6765 az xUnit készletben: az iteratív Fibonacci bináris a referencia-szimulátoron, 6765-öt állítva n = 20-ra.
A 2. réteg a rekurzív változatot veszi, ugyanazzal a toolchainnel linkeli, és a Verilog magon bootolja Verilator alatt, 55-ös visszatérési értéket követelve n = 10-re. A rekurzív alak itt szándékos választás: ez terheli legjobban a keret-könyvelést — a hívó kiértékelési mélységének megőrzése egymásba ágyazott CALL/RET között, a stack cache kiürítése és újratöltése, a fejléc fenntartott mezői. Egy mag, ami az iterációt hibátlanul kezeli, a rekurzióban még lehet csendben rossz — és ez a teszt az, ahol ez kiderül.
A 3. réteg a fizikai kártya: ugyanabból az osztályból származó bináris a konfigurációs flashben, valódi soros portra írva. Maga a decimális átalakítás külön RTL-tesztet kap — a printer modul a 6765-ből a "6765\r\n" bájtsorozatot állítja elő, cocotb-ban verifikálva, a fölötte lévő rétegektől függetlenül.
Egy függvény, három réteg. Ha egy változtatás elrontja a szemantikát, a legolcsóbb rétegben romlik el először — pontosan ez a lényeg.
Miért fizetődik ki a fegyelem
Egy hardveres hiba költsége nem lineáris. Lépcsős, és minden lépcső nagyjából egy nagyságrend:
| Hol bukkan fel a hiba | Költség | Iterációs idő |
|---|---|---|
| C# referencia-szimulátor | ~0 € | ~1 másodperc |
| cocotb / Verilator | ~0 € | másodpercek-percek |
| FPGA | ~0 € (a kártya megvan) | percek (újraszintézis + újraflashelés) |
| ASIC tape-out | 1 500–3 000 € (Tiny Tapeout shuttle) | hónapok |
Ezért teszi a projekt végrehajtási sorrendje az FPGA-validációt (F2.7) a szilícium-szintézis elé, és ezért mondja ki a roadmap nyíltan a szabályt: nincs szilícium tape-out olyan tervvel, ami nem futott FPGA-n. Az FPGA-n talált hiba ingyen van. Ugyanaz a hiba egy legyártott lapkán egy halott chip és egy elvesztett shuttle-hely.
Az első FPGA futás ezt kézzelfoghatóvá tette: három hiba, amin minden szimuláció tisztán átment, órákon belül felszínre került valódi hardveren — egy csak szintézisben számító part-select, egy időzítési kényszer, és egy flash chip, aminek a Quad Enable bitje gyárilag nulla. Egyik sem logikai hiba volt. Mindegyik feltételezés volt. A szimuláció a logikádat ellenőrzi, a hardver a feltételezéseidet — és a teszt-készlet az, amitől a kettőt meg tudod különböztetni.
Mit ad a TDD, és mit nem
Nem bizonyítja, hogy a terv helyes. Ezt egyetlen teszt-készlet sem teszi. Amit ad, az szűkebb és hasznosabb: ha valami elromlik, másodperceken belül tudod, melyik réteg rontotta el. Egy bukó xUnit teszt azt jelenti, hogy a specifikáció változott. Egy bukó arany vektor teszt azt, hogy az RTL elcsúszott a specifikációtól. Egy bukó FPGA futás azt, hogy a fizikai világról alkotott feltételezés volt hibás.
Egy kis csapatnak, ami nyílt toolchainnel és szerény büdzsével épít processzort, ez a szétválasztás nem luxus. Ez a különbség egy olyan terv között, amit még lehet változtatni, és egy olyan között, amit már csak csodálni lehet.
Nyílt forráskód
Az RTL, a referencia-szimulátor, a linker és a teljes teszt-készlet nyilvános. A cikkben szereplő minden szám reprodukálható a repóból.