.. _w02-logic: =============================================== WykĹad 2: Konstrukcja ukĹadĂłw cyfrowych, nMigen =============================================== Data: 27.10.2020 .. toctree:: .. contents:: ModuĹy ====== UkĹady cyfrowe skĹadajÄ siÄ z moduĹĂłw. ModuĹy to wydzielone obszary ukĹadu ukĹadajÄ ce siÄ w hierarchiÄ â tworzymy wiÄksze moduĹy przez instancjonowanie podmoduĹĂłw i opisanie logiki je ĹÄ czÄ cej. UkĹad cyfrowy ma ustalony "gĹĂłwny" moduĹ (top module), ktĂłry reprezentuje caĹy ukĹad. ModuĹy mogÄ zawieraÄ: - wejĹcia (sygnaĹy wchodzÄ ce do moduĹu) - wyjĹcia (sygnaĹy wychodzÄ ce z moduĹu) - sygnaĹy wewnÄtrzne - instancje podmoduĹĂłw - instancje prymitywĂłw - logikÄ opisanÄ w jÄzyku HDL ModuĹy w wiÄkszoĹci jÄzykĂłw mogÄ byÄ rĂłwnieĹź parametryzowane. Prymitywy sÄ czymĹ podobnym do moduĹĂłw â majÄ wejĹcia, wyjĹcia i parametry oraz sÄ instancjonowane podobnie do moduĹĂłw. SÄ jednak z punktu widzenia syntezy "czarnÄ skrzynkÄ " reprezentujÄ cÄ gotowy blok sprzÄtowy dostÄpny w danej technologii (rodzinie FPGA bÄ dĹş bibliotece komĂłrek ASIC). WiÄkszoĹÄ prymitywĂłw jest automatycznie wybierana przez proces syntezy, lecz niektĂłre trzeba zinstancjonowaÄ rÄcznie (np. generatory zegarĂłw). Proces syntezy skĹada siÄ z dwĂłch gĹĂłwnych etapĂłw: - elaboracja: zaczynajÄ c od moduĹu gĹĂłwnego, syntezator rekurencyjnie znajduje wszystkie moduĹy skĹadajÄ ce siÄ na ukĹad i instancjonuje je z odpowiednimi parametrami, tworzÄ c peĹnÄ sieÄ poĹÄ czeĹ - wĹaĹciwa synteza: caĹa logika opisana w jÄzyku wysokiego poziomu jest przetwarzana na prymitywy odpowiednie dla danej technologii Prymitywy dostÄpne na Zynq moĹźna przejrzeÄ tutaj: https://www.xilinx.com/support/documentation/sw_manuals/xilinx2019_1/ug953-vivado-7series-libraries.pdf ModuĹy w nMigen --------------- Aby napisaÄ moduĹ w nMigen, tworzymy klasÄ dziedziczÄ cÄ z ``Elaboratable``. JeĹli chcemy by nasz moduĹ byĹ parametryzowany, dodajemy odpowiednie parametry do konstruktora. W konstruktorze tworzymy rĂłwnieĹź sygnaĹy ktĂłre bÄdÄ interfejsem moduĹu. W nMigen nie oznacza siÄ specjalnie sygnaĹĂłw wejĹcia/wyjĹcia â aby poĹÄ czyÄ sygnaĹy miÄdzy moduĹami, wystarczy siÄgnÄ Ä do odpowiedniego atrybutu podmoduĹu. Ta klasa powinna teĹź mieÄ metodÄ ``elaborate``, ktĂłra stworzy wĹaĹciwy moduĹ (instancjÄ klasy ``Module``) i wypeĹni go logikÄ (sygnaĹami wewnÄtrznymi, podmoduĹami, przypisaniami, itp). PrzykĹadowy moduĹ:: # A counter that counts down to 0. class Counter(Elaboratable): # Width is the counter's width in bits. def __init__(self, width): self.width = width # Inputs. # Start trigger for the counter. self.start = Signal() # Start value of the counter. self.startval = Signal(width) # Counter enable â if 0, the counter will be paused. self.en = Signal(reset=True) # Outputs. # Set if the counter is done counting (the count is 0). self.done = Signal() def elaborate(self, platform): m = Module() val = Signal(self.width) with m.If(self.start): m.d.sync += val.eq(self.startval) with m.Elif(self.en & (val != 0)): m.d.sync += val.eq(val - 1) m.d.comb += self.done.eq(val == 0) return m Aby dodaÄ do moduĹu podmoduĹ rĂłwnieĹź napisany w nMigenie, dodajemy go do ``m.submodules`` w ``elaborate``, po czym moĹźemy po prostu uĹźywaÄ jego sygnaĹĂłw w naszej logice:: my_ctr = m.submodules.my_ctr = Counter(width=4) m.d.comb += my_ctr.start.eq(self.my_start_signal) JeĹli chcemy dodaÄ do moduĹu prymityw bÄ dĹş podmoduĹ napisany w innym jÄzyku, uĹźywamy klasy ``Instance``:: my_inst = m.submodules.my_inst = Instance("nazwa_moduĹu", p_nazwa_parametru=wartoĹÄ_parametru, i_nazwa_wejĹcia=self.sygnaĹ_wejĹciowy, o_nazwa_wyjĹcia=self.sygnaĹ_wyjĹciowy, ) W przypadku takich moduĹĂłw trzeba rÄcznie zdefiniowaÄ ich wejĹcia/wyjĹcia w konstruktorze, by nMigen znaĹ ich interfejs. SygnaĹy i przypisania ===================== SygnaĹy stanowiÄ odpowiednik zmiennych w jÄzykach programowania â sÄ nazwanymi wartoĹciami logicznymi (zmiennymi w czasie), ktĂłrych moĹźna uĹźyÄ w wyraĹźeniach logicznych do wyliczenia wartoĹci innych sygnaĹĂłw. SygnaĹy w nMigen tworzy siÄ nastÄpujÄ co:: # Jednobitowy sygnaĹ. a = Signal() # 4-bitowy sygnaĹ (reprezentujÄ cy wartoĹÄ bez znaku w przypadku wyraĹźeĹ # arytmetycznych), zakres 0..15 b = Signal(4) # 4-bitowy sygnaĹ, reprezentujÄ cy wartoĹÄ ze znakiem, zakres -8..7 c = Signal(signed(4)) # SygnaĹ majÄ cy tyle bitĂłw (3), ile potrzeba, by reprezentowaÄ liczby # z zakresu 0..5 d = Signal(range(6)) class Color(enum.Enum): RED = 0 GREEN = 1 BLUE = 2 # SygnaĹ majÄ cy tyle bitĂłw (2), ile potrzeba, by reprezentowaÄ wartoĹci # enumeracji Color. e = Signal(Color) # SygnaĹ o takim samym rozmiarze i znaku jak sygnaĹ b. f = Signal(b.shape()) # SygnaĹ majÄ cy 3 bity, o domyĹlnej wartoĹci 2. g = Signal(3, reset=2) Przypisania ----------- WartoĹÄ sygnaĹĂłw ustala siÄ przypisaniami:: # a jest rĂłwne 1 wtedy i tylko wtedy, gdy b jest rĂłwne 3 m.d.comb += a.eq(b == 3) # b w kaĹźdym cyklu jest zwiÄkszane o 1 m.d.sync += b.eq(b + 1) Przypisania i sygnaĹy naleĹźÄ do "domen" okreĹlajÄ cych, kiedy sÄ one przeliczane. DomyĹlnie istniejÄ dwie domeny: - ``comb``: sygnaĹ jest przeliczany ciÄ gle (zawsze gdy zmieni siÄ wartoĹÄ prawej strony lub warunku przypisania). W przypadku bezwarunkowego przypisania oznacza to, Ĺźe sygnaĹ staje siÄ efektywnie aliasem prawej strony przypisania. JeĹli Ĺźadne przypisanie sygnaĹu nie jest aktywne, sygnaĹ automatycznie przyjmuje swojÄ wartoĹÄ ``reset`` (domyĹlnie 0). - ``sync``: sygnaĹ jest przeliczany za kaĹźdym razem, gdy wystÄ pi rosnÄ ce zbocze zegara. Wszystkie przypisania synchroniczne w domenie nastÄpujÄ atomowo â wszystkie prawe strony przypisaĹ sÄ obliczane na podstawie wartoĹci przed zboczem zegara. Na przykĹad poniĹźszy kod spowoduje zamienienie miejscami wartoĹci sygnaĹĂłw ``x`` i ``y``:: m.d.sync += x.eq(y) m.d.sync += y.eq(x) Przed wystÄ pieniem pierwszego rosnÄ cego zbocza zegara, sygnaĹ ma swojÄ wartoĹÄ ``reset`` (domyĹlnie 0). JeĹli w danym cyklu zegara Ĺźadne przypisanie dla danego sygnaĹu nie byĹo aktywne, sygnaĹ zachowuje swojÄ wartoĹÄ z poprzedniego cyklu. MoĹźliwe jest zdefiniowanie dodatkowych domen przez uĹźytkownika (ktĂłre zachowujÄ siÄ jak domena ``sync``, ale z osobnym sygnaĹem zegarowym). Jest to jednak doĹÄ zaawansowany temat. Warunki ------- Przypisania mogÄ byÄ warunkowe (byÄ aktywne tylko, jeĹli dane wyraĹźenie jest niezerowe itp). W tym celu naleĹźy je umieĹciÄ w bloku ``m.If`` lub podobnym:: with m.If(a): m.d.sync += b.eq(0) with m.Elif(b == 3): # 3+1 is a bad value, we don't like it. Skip over it. m.d.sync += b.eq(5) with m.Else(): m.d.sync += b.eq(b + 1) Dla danego sygnaĹu, w kodzie mogÄ istnieÄ przypisania tylko z jednej domeny (nie wolno przypisywaÄ tego samego sygnaĹu z kilku domen). W przypadku, gdy kilka przypisaĹ do tego samego sygnaĹu jest aktywne w danym momencie, ostatnie przypisanie wygrywa. W przypadku, gdy Ĺźadne przypisanie nie jest aktywne: - dla sygnaĹĂłw przypisywanych z domeny ``comb`` (lub nigdzie nie przypisywanych), sygnaĹ jest ustawiany na wartoĹÄ domyĹlnÄ (``reset``) - w przeciwnym przypadku (sygnaĹ przypisywany z domeny synchronicznej), sygnaĹ nie zmienia wartoĹci. SygnaĹy przypisywane z domeny ``comb`` syntezujÄ siÄ do ukĹadu obliczajÄ cego odpowiednie prawe strony i warunki oraz drzewa multiplekserĂłw wybierajÄ cego aktywne przypisanie. SygnaĹy przypisywane z domen synchronicznych syntezujÄ siÄ do podobnego ukĹadu oraz zespoĹu przerzutnikĂłw przechowujÄ cych obecny stan sygnaĹu. Switch ------ Analogicznie do konstrukcji ``m.If`` istnieje rĂłwnieĹź konstrukcja ``Switch``:: with m.Switch(operation): with m.Case(0): m.d.sync += o.eq(x + y) with m.Case(1): m.d.sync += o.eq(x - y) with m.Case(2): m.d.sync += o.eq(x | y) with m.Case(3): m.d.sync += o.eq(x & y) Co wiÄcej, oprĂłcz w peĹni zdefiniowanych staĹych moĹźna uĹźywaÄ wzorcĂłw bitowych z "wildcardami":: with m.Switch(opcode): # Pasuje do 0, 1, 2, 3. with m.Case('0--'): m.d.sync += o.eq(x + opcode[0:2]) with m.Case('100'): m.d.sync += o.eq(x + y) with m.Case('101'): m.d.sync += o.eq(x - y) with m.Case('110'): m.d.sync += o.eq(x | y) with m.Case('111'): m.d.sync += o.eq(x & y) WyraĹźenia i operatory ===================== nMigen ma doĹÄ bogaty zbiĂłr wyraĹźeĹ, ktĂłre moĹźna skonstruowaÄ i uĹźywaÄ w logice. Podobnie jak sygnaĹy, wyraĹźenia majÄ rozmiar w bitach (ktĂłry moĹźna sprawdziÄ przez ``len(wyraĹźenie)``) i mogÄ byÄ ze znakiem lub bez (``wyraĹźenie.shape().signed``). Najprostszymi wyraĹźeniami sÄ same sygnaĹy oraz staĹe. StaĹe w nMigen moĹźna tworzyÄ jawnie (uĹźywajÄ c konstruktora ``Const``) lub niejawnie (po prostu uĹźywajÄ c liczby bÄ dĹş wartoĹci enumeracji w wyraĹźeniu). UĹźycie ``Const`` pozwala nam wybraÄ szerokoĹÄ staĹej (w przeciwnym wypadku jest wybierana automatycznie):: # StaĹa o wartoĹci 5, szerokoĹci 3 bitĂłw, bez znaku. Const(5) # StaĹa o wartoĹci 5, szerokoĹci 8 bitĂłw, bez znaku. Const(5, 8) # StaĹa o wartoĹci 5, szerokoĹci 8 bitĂłw, ze znakiem. Const(5, signed(8)) # StaĹa o wartoĹci -3, szerokoĹci 3 bitĂłw, ze znakiem Const(-3) # StaĹa o wartoĹci 1, szerokoĹci 2 bitĂłw, bez znaku. Const(5, 2) # StaĹa o wartoĹci 0, szerokoĹci 2 bitĂłw, bez znaku. Const(Color.RED) WyraĹźenia moĹźna teĹź tworzyÄ z innych wyraĹźeĹ uĹźywajÄ c operatorĂłw. Operatory arytmetyczne w nMigen automatycznie dobierajÄ szerokoĹÄ wyniku tak, by mĂłgĹ on reprezentowaÄ poprawny matematyczny wynik odpowiedniego obliczenia. DostÄpne operatory to: - ``a.as_signed()``, ``a.as_unsigned()``: rzutowanie surowych bitĂłw wartoĹci na wartoĹÄ ze znakiem / bez znaku (zmienia wĹaĹciwoĹci wartoĹci, nie zmienia bitĂłw). - ``a[2:4]``: wycina sygnaĹ 2-bitowy bez znaku z bitĂłw 2..3 wartoĹci ``a``. - ``Cat(a, b)``: skleja ze sobÄ bity dwĂłch wartoĹci w wiÄkszÄ wartoĹÄ (``a`` jest w niĹźszych pozycjach, ``b`` w wyĹźszych) - ``Repl(a, 3)``: rĂłwnowaĹźne ``Cat(a, a, a)`` - ``~a``, ``a & b``, ``a | b``, ``a ^ b``: operacje bitowe NOT, AND, OR, XOR, operujÄ ce na parach bitĂłw z ``a`` i ``b`` niezaleĹźnie i dajÄ ce wynik takiego rozmiaru jak wejĹcia. JeĹli wartoĹci ``a`` i ``b`` sÄ róşnych rozmiarĂłw, krĂłtsza jest rozszerzana do rozmiaru dĹuĹźszej. - ``a.all()``, ``a.any()``, ``a.xor()``: operacje bitowe AND, OR, XOR operujÄ ce na wszystkich bitach wartoĹci ``a``, dajÄ ce 1-bitowy wynik. - ``a.bool()``: rzutowanie ``a`` na 1-bitowÄ wartoĹÄ logicznÄ . 1 jeĹli ``a`` jest róşne od 0. W zasadzie rĂłwnowaĹźne ``a.any()``. - ``Mux(a, b, c)``: operator wyboru. JeĹli ``a`` jest prawdÄ , zwraca ``b``. W przeciwnym wypadku zwraca ``c``. RownowaĹźne operatorowi ``?:`` z jÄzyka C. JeĹli ``b`` i ``c`` majÄ róşnÄ dĹugoĹÄ, krĂłtsza jest rozszerzana do dĹuĹźszej. - ``a + b``, ``a - b``, ``-a``, ``a * b``: operatory arytmetyczne. Wynik ma dĹugoĹÄ takÄ , by zawsze zmieĹciÄ wynik operacji. - ``a == b``, ``a != b``, ``a < b``, ``a > b``, ``a <= b``, ``a >= b``: operatory porĂłwnania. ZwracajÄ 1-bitowy wynik. - ``a.bit_select(b, 3)``: jak ``a[b : b+3]``, ale dziaĹa rĂłwnieĹź, gdy ``b`` nie jest staĹÄ . - ``a.word_select(b, 3)``: jak ``a[b*3 : (b+1)*3]``, ale dziaĹa rĂłwnieĹź gdy ``b`` nie jest staĹÄ . - ``a << b``, ``a >> b``: operatory przesuniÄcia bitowego. Uwaga: podobnie jak przy operatorach arytmetycznych, wyraĹźenie ma takÄ dĹugoĹÄ, by wynik siÄ zawsze zmieĹciĹ. W przypadku przesuniÄcia w lewo jest to wykĹadnicze od dĹugoĹci ``b``. NaleĹźy zapewniÄ, Ĺźe ``b`` tylko tyle bitĂłw, ile jest konieczne. .. _ex-fsm: Maszyny stanĂłw ============== PoniewaĹź w ukĹadach cyfrowych nie moĹźna tak po prostu wyraziÄ pÄtli lub innych skomplikowanych pojÄÄ programowania imperatywnego, bardzo czÄsto spotyka siÄ maszyny stanĂłw, ktĂłre w zasadzie sĹuĹźÄ do rÄcznej realizacji tych konceptĂłw. nMigen ma natywne wsparcie dla maszyn stanĂłw. RozwaĹźmy nastÄpujÄ cy kod w jÄzyku imperatywnym:: while (1) { int num = get_input(); int sum = 0; while (num--) sum += get_input(); output(sum); W logice cyfrowej podobny schemat zrealizowalibyĹmy za pomocÄ maszyny stanĂłw w nastÄpujÄ cy sposĂłb:: with m.FSM() as fsm: with m.State('START'): m.d.comb += input_rdy.eq(1) with m.If(input_vld): m.d.sync += sum.eq(0) m.d.sync += num.eq(input) with m.If(input != 0): m.next = 'ACCUMULATE' with m.Else(): m.next = 'DONE' with m.State('ACCUMULATE'): m.d.comb += input_rdy.eq(1) with m.If(input_vld): m.d.sync += sum.eq(sum + input) m.d.sync += num.eq(num - 1) with m.If(num == 1): m.next = 'DONE' with m.State('DONE'): m.d.comb += output_vld.eq(1) m.d.comb += output.eq(sum) with m.If(m.output_rdy): m.next = 'START'