DÚ: Akcelerace Euklidovské normy ve 2D pomocí CORDIC

Verze Logisimu

Pro tuto úlohu je potřeba Logisim alespoň verze 4.1.0. Připravené soubory nebudou v Logisimu 4.0.0 a starším fungovat! Aktualizujte svůj Logisim viz instalace a případně si stáhněte i novou template.

Používání AI

Zvažte prosím před nakopírováním celého zadání do AI, zda nezkusit udělat úkol úplně bez, anebo alespoň s minimálním použitím AI. Tato úloha je navržená tak, abyste dokázali postupným porozuměním problému dospět k řešení, které je ve srovnání se samotným problémem velmi jednoduché a elegantní. Nemám pochyby o tom, že AI bude schopná z kontextu zadání rovnou dospět k optimálnímu řešení - to je tím, že CORDIC je už desítky let podrobně studovaný a je o něm velká spousta literatury.

Cílem tohoto úkolu ovšem není jenom získat optimální řešení, ale vyzkoušet si proces práce s dokumentací a odbornou literaturou, a na základě ní řešení navrhovat. U více obskurních nebo nedávných problémů, kde AI nebude schopné úlohu řešit "na základě dlouholetého výzkumu", vám nezbyde nic jiného. Pokud si chcete udržet použitelnost i ve světě, ve kterém je AI všudypřítomné a používá se na vše, budete řešit právě tyto druhy problémů - všechny ostatní bude řešit AI. Bohužel takové obskurní úlohy vám nemohu úplně zadat za úkol na střední škole, takže úkoly budou z principu pomocí AI lépe či hůře řešitelné. Ale ke komplexním problémům, kde AI selhává, se musíte propracovat.

Ten, kdo bude umět řešit problémy pouze pomocí AI, bude tou AI nevyhnutelně nahrazen.

Vysvětlení problému a algoritmu

Navrhovaný modul bude umět spočítat euklidovskou normu 2D vektoru postupným zpřesňováním výsledku (tzv. iterativně), a to pouze za použití dvou sčítaček-odčítaček a dvou barrel shifterů (a jedné násobičky konstantou), pomocí algoritmu CORDIC.

Návod ke čtení

Tento popis postupuje od jednoduchých konceptů ke komplikovanějším, co nejvíce to jde, ale i tak se může stát, že vám nebude nějaká pasáž dávat smysl (nebo vám nepřijde důležitá a neporozumíte jí), než budete mít lepší přehled o celém problému. Doporučuji se u prvního čtení nesoustředit tolik na konkrétní detaily nebo matematiku, ale spíše na obecný princip. Poté se můžete ve druhém čtení vrátit ke komplikovanějším sekcím a začít řešit, proč to vlastně funguje.

Euklidovská norma

Spočítat euklidovskou normu vektoru znamená geometricky vzato spočítat jeho skutečnou "délku" v (euklidovském) prostoru. Normu značíme a pro 2D vektor ji spočítáme pomocí Pythagorovy věty: . Určovat normu vektoru je potřeba na nejrůznějších místech: v samotné matematice, ale i ve fyzice, při zpracování signálu a třeba i v počítačové grafice. Protože umocňování i odmocňování jsou náročné operace, existují algoritmy, které se k výsledku postupně dopočítají pomocí jednodušších operací. My si ukážeme algoritmus CORDIC.

COrdinate Rotation DIgital Computer (CORDIC)

Algoritmus CORDIC vzniknul v roce 1959 (ale podobné matematické techniky se používaly už v 17. století) pro výpočty při zaměřování v radarových systémech. Samotná implementace CORDICu je jednoduchá a kompaktní, proto se používá např. v kalkulačkách, a je hotová k dispozici pro všechna možná FPGA - jedná se o standardní aritmetický blok v hardwarových knihovnách.

Samotný CORDIC provádí velmi jednoduchou operaci: Na vstupech bere 2D vektor a úhel , a po několika cyklech vrátí otočený vektor o úhel proti směru hodinových ručiček, viz obrázek (zdroj):

Nezní to příliš zajímavě, ale pomocí této jednoduché operace a vhodně zvolených vstupů lze s vysokou přesností spočítat hodnoty nejrůznějších "strašidelných" funkcí:

Princip Rotačního CORDICu

Rotace vektoru pomocí rotační matice

Otočit vektor o úhel je velmi jednoduché, pokud umíme spočítat sin a cos a násobit vektor maticí. Vektor stačí zleva vynásobit tzv. Rotační maticí o úhel proti směru hodinových ručiček:

Tedy po roznásobení matice:

Spočítat a pro libovolné je obtížné. Ale pro některé je to jednoduché: např. pro víme, že a . Pokusíme se problém nějak převést na výpočet s pomocí pouze "známých úhlů", ke kterým známe odpověď.

Rozložení na více menších rotací

Pokud zvolíme nějaké a tak, že , můžeme místo jedné rotace o provést dvě rotace za sebou: prvně o a pak o . To lze vyjádřit i pomocí rotačních matic, postupným násobením:

Tento princip lze libovolně rozšířit pro malých rotací:

Pokud , pak

Na následujícím obrázku (zdroj) je vidět skutečný princip CORDICu. Začínáme vektorem , a chceme vektor otočený o . Tak prvně otočíme o nějaký menší úhel () a dostaneme . Pak dalším otočením () dostaneme . Sice jsme cílový úhel přestřelili, ale to nevadí, můžeme se vrátit otočením o záporný úhel (), získáváme . Mělo by být evidentní, že ze složek lze vyčíst a , a tohle je skutečně způsob, kterým se tyto funkce pomocí CORDICu počítají. Jde pouze ilustraci, ve skutečnosti se úhly volí jiným způsobem, nicméně princip je stejný: stačí vhodně zvolit rozložení na tak, aby šly matice jednoduše spočítat.

Z celé této sekce je opravdu nejdůležitější princip algoritmu, ne nutně konkrétní matematika.

Zjednodušení rotace pouze na tangens

Chceme tedy náš vektor vynásobit několika rotačními maticemi:

Protože budeme chtít úhly volit vůči trigonometrickým funkcím speciálně tak, aby nám vyšlo hezké číslo, kterým se bude dobře násobit, vadí nám, že máme ve výrazu dvě různé goniometrické funkce. Tyto dvě funkce totiž pro skoro všechny vstupy mají odlišné, navíc iracionální výstupy, kterými se težko násobí.

Tento problém se dá vyřešit následujícím "trikem": z i-té matice vytkneme :

Teď už máme v maticích, kterými se bude náš vektor násobit, pouze . Zůstal nám sice mimo matici, ale protože násobení matic skalárem ("číslem") je komutativní (samotné násobení matice maticí komutativní není!), můžeme všechny vytknout před samotné násobení maticemi, do samostatného součinu skalárů, který si označíme . (zdroj, zdroj, zdroj)

Dostaneme tedy:

Levý člen zatím vynecháme - ukáže se, že je konstantní a vůbec nezávisí na ani na samotných - budeme ho aplikovat až úplně na konci algoritmu jako korekci.

Postupné násobení matic si rozložíme na jednotlivé mezikroky a a zapíšeme jako rekurenci (tj. vzorec pro (i+1)-tý člen posloupnosti na základě i-tého):

Pozn.: S pomocí této rekurence můžeme konstatovat vztah -tého členu posloupnosti ke správnému výsledku:

Poté rozepíšeme maticové násobení a dostaneme následující vzorce pro a :

Až na ten tohle vypadá už docela implementovatelně. Zbývá nám tedy nějak zařídit, abychom nemuseli v hardwaru implementovat výpočet . To uděláme tak, že zvolíme "magicky" tak, abychom úplně bez počítání, na první pohled, věděli, kolik bude .

Volba vhodných úhlu pro tangens

A jak tedy zvolit tak, abychom hned věděli kolik je ? Jednoduše, pomocí k opačné funkce :

Zvolme , pak platí

.

Tedy pokud takto "kouzelně" zvolíme , pak . Např. . Není potřeba žádný tangens počítat. Sice jsme problém tangensu jenom přesunuli jinam, protože pokud by nás zajímala skutečná hodnota , museli bychom umět spočítat - ale najednou ne pro libovolný vstup, ale pouze pro konkrétní , kde jde od do . To se dá udělat například předpočítáním -řádkové tabulky, kterou budeme mít v nějaké paměti. My dokonce pro výpočet skutečnou hodnotu znát vůbec nepotřebujeme, takže tu tabulku nepotřebovat nebudeme. Budeme totiž používat CORDIC ve vektorovém režimu popsaném níže, kde nás úhel a tímpádem ani konkrétní hodnoty nezajímají.

V tomto místě je vhodné do výpočtu vnést i zmiňovaný "směr" rotace. Pokud je kladné, otáčíme proti směru hodinových ručiček. Pokud je záporné, použijeme identitu . Budeme tedy uvažovat, že samotný úhel je vždy kladný (je to pouze "velikost"), a směr budeme ovládat koeficientem , kde je proti směru hodinových ručiček (dopředu) a opačně (zpátky). Tedy pokud je směr , tak jenom prohodíme znaménka před v tom vzorci, nic dalšího není potřeba měnit.

Skutečný krok CORDICu a jeho implementace

Po dosazení do zmíněné rekurence

a zjednodušení a přidání směru tedy získáme skutečný jeden krok algoritmu CORDIC:

Povšimněte si, jak dobře se tyto matematické operace budou implementovat v hardwaru. Kvůli tomu jsme všechny ty úpravy a triky dělali.

Zaprvé, násobení číslem je to samé jako dělení číslem . Podobně jako dělení číslem v desítkové soustavě je tato operace triviální - jednoduše posun o číslic, ve dvojkové soustavě o bitů (tzn. barrel shifter). Tohle je mimochodem důvod, proč jsme zvolili , a ne něco jiného - fungovaly by i jiné hodnoty, ale hůř by se jimi násobilo.

Za druhé, protože je pouze nebo a hodnotu pak ihned přičítáme/odčítáme od něčeho jiného, není potřeba nic násobit, ani nic invertovat - stačí oba dva součty/rozdíly implementovat pomocí sčítaček-odčítaček - a pouze správně (podle ) rozhodnout o tom, zda se má sčítat nebo odčítat. Jenom to pro bude fungovat opačně než pro .

Určení směru rotace v každém kroku rotačního CORDICu

Jediné, co zbývá, je nějak určit ty tak, aby byl výsledek správně, tj., pokud se snažíme otočit o konkrétní úhel (to je co dělá rotační CORDIC), musíme ho nějak poskládat jako součet:

(fuj)

V praxi se to dělá tak, že během výpočtu, v -té iteraci, koukáme, jestli je zbývající větší nebo menší než (ty taháme z tabulky v paměti), podle toho určíme , a pak od odečteme, a se zbytkem pokračujeme dál do příští iterace. Tedy pro výpočet a pomocí CORDICu je potřeba tabulka , a právě kvůli tomu jsem vám záměrně zadal místo nebo právě výpočet normy, kde taková tabulka potřeba není.

Škálování výsledku

Pokud se rozhodneme pro iterací algoritmu, začneme s hodnotami (ze vstupu), a budeme iterovat, dokud nezískáme . Hodnota v určitém smyslu reprezentuje náš výsledek, ale zatím není úplný. Musíme do výsledku ještě zahrnout konstantní člen kosinů:

Tento člen koriguje skutečnost, že během "zjednodušeného" CORDIC algoritmu, kde jsme cosiny vynechali, se nám výsledek postupně více a více "natahuje" o faktor . Ovšem my umíme spočítat, a vynásobíme jím náš výsledek abychom ho zase "zkrátili" na správnou velikost:

Pro konkrétní iterací je vhodné použít přesně , protože to přesně odpovídá natažení, které způsobilo těch iterací. Ovšem pro je mezi jednotlivými už velmi malý rozdíl, proto často stačí jednoduše použít limitu :

Jakmile máme vhodné , které je konstantní pro CORDIC s konstantním počtem iterací, stačí naše vypočítané přeškálovat pomocí , čímž konečně získáme výsledek.

Zde je potřeba násobit - ovšem násobení konstantou lze implementovat efektivněji než obecné násobení, a násobit je potřeba pouze jednou, na konci algoritmu - násobení v tomto případě není bottleneck.

Vektorový CORDIC

U rotačního CORDICu se algoritmus snažil vektor otočit o konkrétní úhel. CORDIC lze ale použít i jinak, ve vektorovém režimu. Ten se snaží vstupní vektor natočit určitým směrem, a výstup je kromě otočeného vektoru i úhel, o který bylo potřeba vektor otočit (pokud nás zajímá). Standardně se provádí otáčení do směru vektoru (tj. na osu X), protože se jednoduše implementuje.

Vektorový CORDIC dostane na vstupu vektor s nenulovým , a snaží se ho postupně "sklopit" do vodorovné polohy na ose X tím, že se snaží dostat vektoru na nulu. To je jednodušší než rotační CORDIC - vektor otáčíme v -tém kroku o úhel , ale vždy jednoduše směrem k ose X: Pokud je , tak po směru hodinových ručiček ("dolů", ), a pokud , tak proti směru ("nahoru", ), viz následující obrázek (zdroj):

Výpočet normy pomocí vektorového CORDICu

Pokud vektor otočíme tak, aby byl vodorovně s osou X (), dostaneme právě vektor , což vychází z podobnosti trojúhelníků, viz obrázek:

Stačí tedy na zadaný vstupní vektor pustit vektorový CORDIC, a v souřadnici výsledného vektoru nalezneme "skoro" normu. Tu jenom přenásobíme , což nám dá skutečnou normu vstupního vektoru:

Nyní platí

Příklad výpočtu normy

Mějme , stejně jako na obrázku výše. Norma je . Pojďme se podívat, jestli CORDIC dospěje ke stejnému závěru:

i
04.00000+3.00000-11.000001.000004.00000
17.00000-1.0000010.500000.707114.94975
27.50000+2.50000-10.250000.632464.74342
38.12500+0.62500-10.125000.613574.98527
48.20313-0.3906210.062500.608834.99434
58.22754+0.12207-10.031250.608654.99945
68.23135-0.1350410.015630.607354.99933
78.23346-0.0642510.007810.607285.00001
88.23351+0.05790-10.003910.607264.99988
98.23374+0.02574-10.001950.607254.99998
108.23379+0.00965-10.000980.607255.00000
118.23380-0.0024110.000490.607255.00000

Všimněte si, jak rychle se CORDIC přiblížil ke správnému výsledku (tedy po přenásobení ). Obecně platí, že každá iterace CORDICU zvětší přesnost výsledku o 1 bit (pokud máme dost přesnou sčítačku atd.). Standardně tedy pokud CORDIC interně pracuje s 32-bitovými čísly, bude se používat cca. iterací, po nich už bude celý 32bit výsledek velmi přesný.

Desetinné čísla ve dvojkové soustavě

Protože CORDIC potřebuje pracovat s desetinnými čísly, musíme je nějak reprezentovat pomocí bitů. V praxi, např. při programování, jste se nejspíš setkali s čísly s plovoucí řádovou čárkou (floating-point numbers), nejspíš ve formě datových typů float a double. Ty jsou super, protože umí reprezentovat zároveň obrovská čísla a také velmi přesně čísla velmi blízko k nule, ovšem pro naše využití jsou zbytečně komplikované. Nám budou stačit čísla s tzv. pevnou řádovou čárkou (fixed-point numbers). Sice mají nějaké nedostatky, obzvlášť omezenou přesnost, ale zase je práce s nimi drasticky jednodušší.

Fixed-point numbers

Jak napovídá název, ve fixed-point desetinném čísle je na nějakém místě zafixovaná desetinná čárka, viz obrázek (zdroj).

Tato desetinná čárka se neukládá, je vlastností zvoleného číselného formátu. V obrázku tedy například pracujeme s čísly Q5.3: máme 5 bitů před čárkou a 3 bity za čárkou, dohromady 8 bitů. "Na drátě" existuje pouze těch 8 bitů, bez jakékoliv informace o tom, co znamenají, tj. jak se mají správně interpretovat. To je popsané někde v dokumentaci a ovlivňuje to, jak budeme s číslem pracovat.

Hodnotu čísla můžeme chápat jako . V této reprezentaci se už vyskytují pouze celé binární čísla, a po převedení do desítkové soustavy dostaneme .

Existuje i jiný způsob přemýšlení o fixed-point číslech. Jak jistě víte, násobením můžeme posouvat desetinnou čárku doleva nebo doprava (násobíme nebo dělíme řádem, funguje to úplně stejně v desítkové soustavě pro ). Tedy místo můžeme psát . Pracujeme tedy s číslem (to jsou surové bity tohoto fixed-point čísla na drátě), ale bokem si "pamatujeme", že toto číslo je -krát větší než opravdová hodnota. Tohle je, jak budeme k fixed-point číslům přistupovat my, protože nás to nechá se k nim chovat prostě jako k celým číslům, až do momentu, kdy je potřebujeme ukázat uživateli (v ten moment musíme umístit na správné místo desetinnou čárku).

Operace s fixed-point čísly

Pokud chci sečíst dvě fixed-point čísla stejného typu, stačí je reprezentovat výše zmíněným způsobem a vytknout těch :

Z toho plyne, že můžeme prostě sčítat bitové reprezentace stejných fixed-point čísel a dostaneme správné fixed-point číslo stejného typu.

U násobení budeme muset provést drobnou úpravu, protože budeme mít ve výrazu přebytečné :

Tedy po klasickém celočíselném vynásobení musíme jedno násobení aplikovat: posuneme výslednou hodnotu o bitů doprava (protože exponent je záporný). Tím dostaneme , což je správná reprezentace výsledku ve fixed-point číslech.

Zmiňované operace lze navrhnout i pro čísla různých typů, ale my budeme v celém obvodu pracovat pouze s čísly jednoho jediného fixed-point typu, Q12.16 (12 bitů před a 16 bitů za desetinnou čárkou = 28 bitů). Pro případ Q12.16 je a tedy pracujeme s čísly v podobě .

Záporná fixed-point čísla

Určitě jste zaznamenali, že CORDIC potřebuje pracovat i se zápornými čísly, musíme tedy naše fixed-point obohatit o znaménko, abychom měli čísla se znaménkem. To se dělá velmi jednoduše, stačí říct, že v našem desetinném čísle je reprezentováno ve dvojkovém doplňku. Dostáváme tedy něco, čemu můžeme říkat two's-complement signed fixed-point.

Pokud už si nepamatujete, jak funguje dvojkový doplňek, doporučuji si znovu projít příslušnou kapitolu skript. Nejdůležitější potřebné skutečnosti pro práci s ním shrnu tady:

  • . To stačí udělat na celou reprezentaci fixed-point čísla bez ohledu na jeho typ, protože platí, že . Z toho vyplývá i, že odčítání fixed-point čísel funguje identicky jako odčítání celých čísel (skripta).
  • Nejvyšší bit čísla je znaménko. Platí pro libovolné fixed-point číslo, nehledě na typ.
  • Násobení odpovídá shiftu doleva. Pokud vypadnou shiftem zleva nějaké nenulové bity, dochází k přetečení a výsledek nelze reprezentovat. Zprava hodnotu doplňujeme nulami.
  • !!! Násobení (tedy dělení ) odpovídá aritmetickému shiftu doprava. Ten se chová odlišně od logického (kde se doplňuje nulami). Při aritmetickém shiftu musí zůstat znaménko čísla zachované, tzn. bity zleva se doplňují hodnotou znaménka (a NE nulami!). Tedy pro kladná čísla se doplňuje nulami (sign bit je nula) a pro záporná čísla se doplňuje jedničkami (sign bit je 1). Více detailů a diagram je na wikipedii. Bity, které vypadávají zprava, pokud jsou jedničkové, znamenají ztrátu přesnosti (zaokrouhlení výsledku), ale to neznamená, že výsledek je nesprávný, jenom je nepřesný. V Logisimu můžete použít Shifter, pokud ho správně nastavíte.
  • Čísla ve dvojkovém doplňku nelze přímo násobit!!! Jak se to implementuje, aby to fungovalo, neprobíráme. V Logisimu můžete použít Multiplier v režimu 2's Complement.

V Logisimu

V Logisimu žádná konkrétní podpora pro fixed-point čísla není, budete muset správně manipulovat jejich bitovou reprezentaci (není to těžké). Ale, Logisim umí zobrazovat floating-point čísla. V projektovém souboru máte připravené moduly Q12_16_tofloat a Q12_16_fromfloat, které konvertují mezi reprezentací Q12.16 a 32-bit floating-point čísly. Pomocí nich, a inputů/outputů/probes s nastaveným radixem "Float" můžete při testování zadávat hodnotu jako (nepřesné) desetinné číslo, a také si ho jako desetinné číslo nechat zobrazit. To hodně pomahá, pokud se snažíte odladit, při kterém kroku váš modul udělal výpočetní chybu. Konverze z/do floatů jsou pouze přibližné, měli byste obvod otestovat i přímo poskytnutými testovacími vektory v Q12.16 formátu a ujistit se, že dostáváte úplně přesný výsledek.

Specifikace modulu













X_IN
 [27:0]



Y_IN
 [27:0]



START



CLK



OUTP
 [27:0]



DONE



DEBUG_X
 [27:0]



DEBUG_Y
 [27:0]



Struktura a chování

  • Plně sekvenční modul, synchronizovaný na nábežné hraně
  • Vstupy X_IN a Y_IN výstup OUTP
    • V signed two's-complement fixed-point formátu Q12.16 (28 bitů)
    • Výstup bude vzhledem k povaze výpočtu vždy kladný, ale stejně bude v signed formátu
  • Vstupy START, CLK a výstup DONE tvoří start-done interface modulu
  • Na výstupy DEBUG_X a DEBUG_Y přiveďte aktuální stav vašich registrů pro a , ve kterých provádíte mezivýpočty, aby bylo zvenku vidět, co modul dělá. Hodnota těchto výstupů se nekontroluje, slouží pouze pro ladění.
  • Modul spočítá pomocí CORDIC algoritmu
  • Po jednom úvodním načítacím cyklu (start=1) modul provede přesně 28 iterací CORDIC algoritmu
    • Výsledek 28. iterace CORDICu je potřeba přenásobit konstantou (stačí kombinačně na výstupu z modulu)
    • Použijte hodnotu zaokrouhlenou do formátu Q12.16:
    • Tato hodnota je ve skutečnosti přesně , ale to je ke dostatečně blízko
  • Modul se bude jmenovat NORM
  • Používejte logisimové matematické operátory
    • Pro implementaci stačí dvě sčítačky/odčítačky (můžete pro jednoduchost použít i separátní sčítačku+odčítačku+multiplexor), dva shiftery a jedna násobička. Pokud toho potřebujete víc, zkuste se vrátit k zadání.

Časování

Modul je plně synchronní, reaguje na vstup a mění svůj výstup tedy pouze s náběžnou hranou vstupu clk. Modul používá standardní start-done interface, jak jsme ho probírali. Po skončení výpočtu (done=1) výsledek musí zůstat konstantní až do příštího start=1. Detailní popis níže.

Standardní start-done interface

Pokud je na vstupu start=1, načte modul vstupy a připraví se na výpočet (toto může nastat i během již probíhajícího předchozího výpočtu, v takovém případě se tento výpočet ruší a připraví se nový podle vstupů). "Příprava na výpočet" může znamenat různé věci: pouhé uložení vstupních hodnot do registrů, nebo přímo provedení prvního kroku výpočtu podle vstupů a uložení částečného výsledku do registrů. V každém případě by se ale měla inicializovat řídící část obvodu - pokud obvod provádí výpočet v nějakých "krocích", nebo má v registrech jiné poznámky o výpočtu, je potřeba tyto inicializovat na hodnoty, které umožní začít výpočet od začátku.

Jakmile start=0, provede modul s každým clockem jeden krok výpočtu. Během tohoto musí být výstup done=0. Modul může počítat s tím, že po celou dobu jednoho výpočtu zůstanou vstupy A a B konstatní. Jakmile modul dokončil výpočet a na výstupu OUTP prezentuje správný výsledek, musí přestat počítat (výsledek musí zůstat konstantní, dokud nepřijde další start=1 signál). Zároveň toto musí indikovat pomocí done=1.

Warning

Pamatujte, že synchronní obvod reaguje na hodnoty na vstupu až při příští náběžné hraně, ne té, která změnu hodnoty způsobila!

Postup řešení

U celého vašeho řešení se bude hodnotit čitelnost a srozumitelnost - vše co odevzdáváte kromě řešení slouží jako dokumentace k projektu. Pokud vám dokumentace přijde nepřehledná, je nutné ji přepsat, přepracovat, nebo překleslit, aby byla. Toto platí obzvlášť pro diagram - např. škrtání a matoucí věci v diagramu budou penalizovány.

Pokud budete pracovat na papír, použijte kontrastní propisku, ne slabou tužku, a výsledek buď naskenujte, nebo s dobrým osvětlením, kolmo a ve vysokém rozlišení vyfoťte (vyhněte se tvrdým stínům ve fotce), a upravte, aby vypadal jako naskenovaný, zejména proveďte korekci zkosení (pro tento účel lze použít např. aplikaci Microsoft Lens, dříve Office Lens, na telefony). Odevzdání v nízké kvalitě (jak rozlišení tak vizuálně) bude vráceno k přepracování.

1. Zadání

Přečtěte si zadání, ujistěte se, že rozumíte CORDICu, postupu výpočtu, desetinným číslům a záporným číslům. Pochopení těchto konceptů může být zkoušeno v rámci hodnocení domácího úkolu.

2. Teoretická příprava

Buď na papír nebo digitálně (bude součástí odevzdání) si připravte (jako v předchozích úlohách)

  1. Stručný popis algoritmu, ať máte ujasněné, co implementujete. Tento popis je oproti uvedené teorii velmi jednoduchý, to je záměrně (úloha je navžená tak, aby byla implementace jednoduchá).
  2. Seznam registrů (stavu), jejich velikosti, stručný popis jejich funkce, pokud to není evidentní.
  3. Popis zápisů do registrů za určitých podmínek (při start=1, při done=0, etc.). Použijte syntaxi z předchozích úloh a obecně zažité výrazy/konstrukce z číslicového návrhu nebo programování. Cokoliv nejasného popište.
  4. Detailní simulaci příkladu nemusíte provádět, ani to moc bez programování nejde, stačí aby vám bylo jasno, jak zjistit že proběhlo přesně 16 iterací CORDICu. Pokud necháte proběhnout 15 nebo 17 iterací, nedostanete správný výsledek.
  5. Pro každý stavový registr rozepište, jaké různé hodnoty je potřeba do něj umět uložit.
  6. Uveďte, jak z aktuálního stavu vypočítáváte všechny kontrolní i datové signály, např. DONE, OUTP, všechny write-enably, a cokoliv jiného pojmenovaného, co používáte v popisu chování.

3. Blokový diagram

Buď na papír nebo digitálně (buď tabletem s tužkou nebo např. pomocí draw.io) nakreslete blokový diagram zapojení obvodu. Diagram musí odpovídat běžným elektronickým diagramům.

Zejména upozorňuji na:

  1. Datovou cestu se snažte zapojit přímým napojováním drátů, bez tunelů
  2. Pro kontrolní signály používejte tunely (pojmenované odkazy na jiné vodiče)
  3. U vícebitových drátů/registrů označte zavedeným způsobem šířku (počet bitů)
  4. V blokovém diagramu mohou být operace a bloky víc "high level" - jde o chování, ne o konkrétní způsob implementace (např. násobění dvěma v diagramu by se pak implementovalo shifterem)
  5. Diagram musí být přehledný (bez škrtání atd.) a smysluplně rozvržený - má sloužit jako dokumentace. Pokud je nepřehledný, musíte ho překreslit. Dokonce se dá počítat s tím, že vaše první iterace nebude dostatečně přehledná, a budete odevzdávat až druhou/třetí verzi.
  6. Označte všechny clock vstupy zavedeným symbolem. Dále není v diagramu potřeba clocky spojovat, clocky se předpokládají vždy spojené.

4. Zapojení v Logisimu

Stáhněte si 🔗 template pro tento úkol a do předpřipraveného modulu NORM podle vašeho blokového diagramu postavte řešení. Dbejte na následující:

  • Upravujte pouze modul NORM, modifikace ostatních modulů budou při odevzdání zahozeny!
  • V datové cestě modulu použijte:
    • 2x připravený modul ADDSUB
    • 1x Logisimovou celočíselnou 28-bitovou násobičku
    • Libovolný počet Logisimových shifterů (libovolné nastavení)
    • Libovolný počet Logisimových sčítaček jako inkrementorů
    • Libovolný počet Logisimových komparátorů pro rozhodování
    • Žádná další aritmetika není pro datovou cestu potřeba!
  • Snažte se, aby struktura vaší Logisimové implementace (hlavně datové cesty) odpovídala vašemu blokovému diagramu (stavte to podle něj!)
  • Není potřeba vytvářet žádné další moduly, všechno stačí zapojit do NORM
  • Pro debuggování můžete používat připravenou sadu Q12_16_* modulů spolu s Logisimovou Probe nastavenou na Radix Float - nechá vás to sledovat skutečnou (ale nepřesnou, kvůli konverzi na float) hodnotu fixed-point drátů živě, aniž byste museli ručně konvertovat. Příklad použití je v modulu main.
  • Testujte svůj modul proti testovacím vektorům v modulu main.
  • Před odevzdáním na svůj modul pusťte testovací baterii připravenou v modulu NORM_TEST_BATTERY, případně můžete zkusit jeden test ručně pomocí modulu NORM_GOLDEN_TEST. Červeně jsou vyznačeny parametry testu, které můžete měnit.

Ujistěte se, že jste nezměnili externí interface modulu NORM. Měl by vypadat následovně:

Info

Na výstupu DEBUG_X a DEBUG_Y přiveďte přímo hodnoty vašich registrů X a Y, ve kterých provádíte iterativní výpočet. Je to pro účely debuggování pro vás i pro mě. Hodnoty těchto výstupů se netestují. Testuje se pouze hodnota výstupu OUTP, jakmile done=1. Také se testuje, jestli váš modul dokončil výpočet (indikoval done=1) během nejvýše 40 cyklů, jinak je příklad hodnocen jako TIMEOUT.

5. Testování

Váš modul otestujte v main pomocí (některých z) následujících testovacích vektorů. Nemusíte mít výsledek úplně přesně jako je zde uvedeno, ale neměl by se lišit o více než 0.01%. V OUTPUT_ACTUAL jsem uvedl výsledek z mojí implementace. Pozn.: tyto testovací hodnoty odpovídají těm v modulu NORM_TEST_BATTERY.

Testovací vektory

NrXYOUTP_EXPECTEDOUTP_ACTUAL
000030000004000000500000050008
01001000000100000016A0A0016A0A
0200A0000000000000A000000A0009
03000000000A000000A000000A0008
04001BB670027311002FFFF0030008
05005000000C000000D000000D0008
06008000000F000001100000110009
07007000001800000190000019000A
080140000015000001D000001D000B
0900C0000023000002500000250006
100090000028000002900000290008
1101C000002D00000350000035000D
1200B000003C000003D000003D000B
13010000003F00000410000041000C
14021000003800000410000041000A
15030000003700000490000049000F
1600D000005400000550000055000D
17024000004D000005500000550010
18027000005000000590000059000F
19041000004800000610000061000C
20014000006300000650000065000F
2103C000005B000006D000006D0011
2200F0000070000007100000710011
2302C0000075000007D000007D0014
240580000069000008900000890015
250110000090000009100000910013
26018000008F000009100000910018
27033000008C000009500000950011
280550000084000009D000009D0015
29077000007800000A900000A90017
3003400000A500000AD00000AD0017
3101300000B400000B500000B50018
3203900000B000000B900000B9001B
33068000009900000B900000B9001A
3405F00000A800000C100000C10018
3501C00000C300000C500000C50019
3605400000BB00000CD00000CD001C
37085000009C00000CD00000CD001B
3801500000DC00000DD00000DD001F
3908C00000AB00000DD00000DD001C
4003C00000DD00000E500000E50020
4106900000D000000E900000E90021
4207800000D100000F100000F1001D
4302000000FF000010100001010021
440170000108000010900001090023
4506000000F7000010900001090023
460450000104000010D000010D0021
4707300000FC000011500001150024
480A000000E7000011900001190024
490A100000F0000012100001210026
50044000011D000012500001250026

Zde uvádím tabulku kompletního průběhu příkladu X=3 Y=4 na mé implementaci. Opět připomínám, že stačí, aby vaše hodnoty byly blízko k těm mým, s dostatečnou přesností.

Průběh X=3 Y=4 na mé implementaci

cyclestartXYdoneOUTP
0010000000000000000000000
010003000000400000001D25F
0200070000001000000044033
0300078000FFD800000048DED
0400082000FFF60000004EF16
050008340000064000004FB3B
0600083A40FFFE0C00004FF07
0700083B3A00022920004FF9F
0800083BC400001A60004FFF2
0900083BC7FFFF12F0004FFF4
1000083BD6FFFF96A0004FFFD
1100083BDAFFFFD8700050000
1200083BDBFFFFF9500050000
1300083BDC000009C00050001
1400083BDC000001900050001
1500083BDCFFFFFD800050001
1600083BDDFFFFFF800050002
1700083BDE000000800050002
1800083BDE000000000050002
1900083BDEFFFFFFC00050002
2000083BDFFFFFFFE00050003
2100083BE0FFFFFFF00050003
2200083BE1FFFFFFF00050004
2300083BE2FFFFFFF00050005
2400083BE3FFFFFFF00050005
2500083BE4FFFFFFF00050006
2600083BE5FFFFFFF00050007
2700083BE6FFFFFFF00050007
2800083BE7FFFFFFF00050008
2900083BE8FFFFFFF10050008

Poznámka k přesnosti

Na výše uvedeném průběhu si můžete všimnout, že správný výsledek jsem získali už po 11 cyklech, a pak se od správného výsledku zase vzdalujeme. Protože neznáme při výpočtu správný výsledek, a metoda v tomto případě trochu přestřeluje, nemáme tomu jak zabránit.

Nicméně, mohli bychom zastavit běh algoritmu, pokud Y=0 (nebo pokud je Y dostatečně blízko k nule), protože pak už výsledek přesnější nebude. V našem případě tím výsledek dokonce dále zhoršujeme, protože nemáme dost přesné čísla, abychom reprezentovali Y bližší k nule než 0xFFFFFFF (asi -0.000015), a kvůli povaze zaokrouhlování při prováděných operacích zůstaneme na Y=0xFFFFFFF, takže se Y nikdy k nule zase nedostane - do nekonečna budeme o cca 0x0000001 zhoršovat přesnost výsledku.

V praxi by bylo vhodné výpočet ve vhodný moment zastavit, v této úloze to není zadané, aby to nezvyšovalo komplexitu projektu, horší přesnost nám nevadí. Nicméně pokud chcete, můžete modul zastavit dříve, než přesně po zadaném počtu iterací - ale myslete na to, že abyste dostali přesný finální výsledek, musíte nakonec vynásobit správným podle počtu provedených iterací .

Test včetně kontroly přesnosti lze provést v modulu NORM_GOLDEN_TEST. Přesnost tam je zadaná jako 0x64 miliontin, což je 0.01%.

Finálně na svůj modul pusťte baterii testů v NORM_TEST_BATTERY a ověřte počet PASS/FAIL/TIMEOUT. Pokud budete mít nějaké selhání, můžete si nastavit konstantu tak, aby se tester zastavil. Proklikáním do modulů se pak můžete podívat na vaše hodnoty vs. očekávané, spočítanou chybu v přesnosti, a získat vstupy X a Y pro další debuggování selhávajícího příkladu. Pozn.: Odevzdávač testuje právě tuto baterii testů.

6. Odevzdávání

Nejspíše na Submitty, Logisim se bude kontrolovat automaticky, zbytek (přípravu na papír) budu hodnotit ručně. TBA.

FAQ

V případě, že máte nějakou otázku, se určitě neváhejte co nejdříve ptát. Pokud bude odpověď užitečná pro všechny, přidám ji zde do FAQ.

  • TBA
Last change: 2026-03-31, commit: 132e4ef