.. _w06-sync-design: =============================================== WykĹad 6: Projektowanie ukĹadĂłw synchronicznych =============================================== Data: 14.11.2018 .. toctree:: .. contents:: Przetwarzanie potokowe ====================== WrĂłÄmy do tematu ukĹadu dzielÄ cego z pierwszego zadania. Zrealizowany kombinacyjnie ukĹad dzialÄ cy wyglÄ da mniej-wiÄcej tak:: input wire [BITS-1:0] A; input wire [BITS-1:0] B; output wire [BITS-1:0] Q; // A / B output wire [BITS-1:0] R; // A % B wire [BITS-1:0] tmp[BITS:0]; assign tmp[BITS] = A; assign R = tmp[0]; genvar i; generate for (i = BITS - 1; i >= 0; i = i - 1) begin // Wyznacza jednÄ cyfrÄ wyniku, odejmuje (bÄ dĹş nie) odpowiednio przesuniÄte B, wynik // przekazuje dalej. div1 #(.IDX(i)) dzielacz(.A_IN(tmp[i + 1]), .A_OUT(tmp[i]), .Q(Q[i]), .B(B)); end endgenerate ZaĹóşmy, Ĺźe chcemy mieÄ ukĹad synchroniczny, ktĂłry bÄdzie wykonywaĹ operacjÄ dzielenia. UĹźywajÄ c bezpoĹrednio powyĹźszego ukĹadu (dodajÄ c przerzutniki na wejĹciach i wyjĹciach), maksymalna czÄstotliwoĹÄ dziaĹania bÄdzie wynosiÄ okoĹo ``1/(BITS * t_div1 + t_ff)``, gdzie ``t_div`` to opóźnienia kombinacyjne na ukĹadzie ``div``, a ``t_ff`` to opóźnienie na przerzutniku. Jest to bardzo zĹy wynik. Nie jesteĹmy w stanie poprawiÄ czasu wykonywania pojedynczego dzielenia bez zmiany algorytmu. MoĹźemy jednak znacznie poprawiÄ czÄstotliwoĹÄ dziaĹania naszego ukĹadu, wstawiajÄ c mu przerzutniki miÄdzy ukĹady dzielÄ ce:: input wire [BITS-1:0] A; input wire [BITS-1:0] B; output wire [BITS-1:0] Q; // A / B output wire [BITS-1:0] R; // A % B input wire clk; wire [BITS-1:0] tmp_a[BITS:0]; wire [BITS-1:0] tmp_q[BITS:0]; wire [BITS-1:0] tmp_b[BITS:0]; assign tmp_a[BITS] = A; assign tmp_q[BITS] = 0; assign tmp_b[BITS] = B; assign R = tmp_a[0]; assign Q = tmp_q[0]; genvar i; generate for (i = BITS - 1; i >= 0; i = i - 1) begin wire q1; wire [BITS-1:0] out; reg [BITS-1:0] out_a; reg [BITS-1:0] out_q; reg [BITS-1:0] out_b; div1 #(.IDX(i)) dzielacz(.A_IN(tmp_a[i+1]), .A_OUT(out), .Q(q1), .B(tmp_b[i+1])); always @(posedge clk) begin out_a[i] <= out; out_q[i] <= tmp_q[i+1] | {q1, {{i}1'b0}}; out_b[i] <= tmp_b[i+1]; end assign tmp_a[i] = out_a; assign tmp_q[i] = out_q; assign tmp_b[i] = out_b; end endgenerate Teraz nasz ukĹad potrzebuje aĹź ``BITS`` cykli zegara na wykonanie jednego dzielenia zamiast jednego cyklu, ale za to jego maksymalnÄ czÄstotliwoĹciÄ jest teraz ``1/(t_div1 + t_ff)`` -- czyli czas wykonywania jednego dzielenia nie zmieniĹ siÄ aĹź tak bardzo (``t_ff`` jest prawdopdobnie znacznie mniejsze niĹź opóźnienia kombinacyjne). Co wiÄcej, znacznie wzrosĹa przepustowoĹÄ naszego ukĹadu -- wykonujemy teraz ``BITS`` dzieleĹ rĂłwnoczeĹnie (kaĹźde na innym etapie przetwarzania)! TakÄ optymalizacjÄ ukĹadu nazywa siÄ przetwarzaniem potokowym (pipeline). Jest to podstawa efektywnego projektowania ukĹadĂłw synchronicznych i pozwala uzyskaÄ wyĹźsze czÄstotliwoĹci zegara i przepustowoĹÄ za cenÄ zwiÄkszenia opóźnienia. ProjektujÄ c prawdziwy potok prawdopodobnie bÄdziemy potrzebowaÄ jeszcze dwĂłch rzeczy: - jakiegoĹ sposobu na oznaczenie, ktĂłre etapy potoku przetwarzajÄ dane w danym momencie, a ktĂłre sÄ nieaktywne (np. z powodu braku dostÄpnych danych w ĹşrĂłdle -- tzw. pipeline bubble) - jakiegoĹ sposobu na zatrzymanie potoku, gdy ukĹad docelowy nie jest w stanie teraz akceptowaÄ danych (prawdopodonie flaga clock enable na caĹy potok, ale sÄ moĹźliwe bardziej skomplikowane pomysĹy, szczegĂłlnie w poĹÄ czeniu z bÄ belkami) NajczÄsszy problem przy uĹźyciu potokĂłw do realizacji procesorĂłw polega na tym, Ĺźe dane wejĹciowe mogÄ zaleĹźeÄ od danych wyjĹciowych, ktĂłre wysĹaliĹmy wczeĹniej, ale jeszcze nie zostaĹy przetworzone (rozwaĹźmy dwie instrukcje dzielenia pod rzÄ d, przy czym druga instrukcja zaleĹźy od wyniku pierwszej) -- wtedy konieczne jest pamiÄtanie, ktĂłre dane sÄ juĹź obliczone i wstawianie bÄ belkĂłw do potoku aĹź do momentu policzenia wszystkich potrzebnych wejĹÄ. ZauwaĹźmy, Ĺźe przetwarzanie potokowe skomplikowaĹo nam ukĹad -- musimy teraz pamiÄtaÄ osobny stan ``B`` na kaĹźdy krok dzielenia (Ĺźeby kaĹźdy krok dziaĹajÄ cy na danej dzielnej widziaĹ ten sam dzielnik). GdybyĹmy spodziewali siÄ, Ĺźe dzielnik zmienia siÄ rzadko, moglibyĹmy wyeliminowaÄ osobne stany i wymuszaÄ przerwanie potoku w momencie zmian dzielnika (tak robi siÄ w procesorach z rzadko zmieniajÄ cym siÄ stanem, np. poziomem uprzywilejowania). Maszyny stanĂłw ============== Ponownie rozwaĹźmy nasz ukĹad dzialÄ cy. ZaĹóşmy, Ĺźe nie jest nam potrzebna duĹźa przepustowoĹÄ (wystarczy nam wykonywanie tylko jednego dzielenia na raz). MoĹźemy wtedy zredukowaÄ nasz ukĹad do pojedynczego bloku ``div`` i uĹźyÄ rejestru do pamiÄtania, ktĂłry etap dzielenia akurat wykonujemy:: input wire [BITS-1:0] A; input wire [BITS-1:0] B; output wire [BITS-1:0] Q; // A / B output wire [BITS-1:0] R; // A % B input wire input_vld; // 1 jeĹli ktoĹ przesyĹa nam nowe liczby do dzielenia output wire output_vld; // 1 jeĹli skoĹczyliĹmy dzielenie input wire clk; reg [$clog2(BITS)-1:0] bitidx = 0; reg active = 0; reg [BITS-1:0] tmp_a; reg [BITS-1:0] tmp_q; assign R = tmp_a; assign Q = tmp_q; assign output_vld = !active; wire q1; wire [BITS-1:0] out; div1 dzielacz(.A_IN(tmp_a[i]), .A_OUT(out), .Q(q1), .B(B), IDX(bitidx)); always @(posedge clk) begin if (!active) begin if (input_vld) begin tmp_a <= A; tmp_q <= 0; active <= 1; bitidx <= BITS - 1; end end else begin tmp_a <= out; tmp_q[bitidx] <= q1; if (bitidx == 0) active <= 0; bitidx <= bitidx - 1; end end Taka konstrukcja efektywnie jest maszynÄ stanĂłw (``BITS`` róşnych stanĂłw na kaĹźdy etap dzielenia + stan nieaktywny). Maszyny stanĂłw sÄ , obok potokĂłw, drugim podstawowym budulcem ukĹadĂłw synchronicznych. Kodowanie one-hot ----------------- RozwaĹźmy prostÄ maszynÄ stanĂłw rozpoznajÄ cÄ podciÄ g ``"ABC"`` na strumieniu wejĹciowym:: parameter NONE = 2'b00; parameter GOT_A = 2'b01; parameter GOT_AB = 2'b10; parameter GOT_ABC = 2'b11; parameter SYMBOL_A = 8'h41; parameter SYMBOL_B = 8'h42; parameter SYMBOL_C = 8'h43; input [7:0] wire data; input wire clk; output wire found; reg state [1:0] = NONE; assign found = (state == GOT_ABC); always @(podedge clk) begin case (state) NONE: state <= (data == SYMBOL_A) ? GOT_A : NONE; GOT_A: state <= (data == SYMBOL_B) ? GOT_AB : (data == SYMBOL_A) ? GOT_A : NONE; GOT_AB: state <= (data == SYMBOL_C) ? GOT_ABC : (data == SYMBOL_A) ? GOT_A : NONE; GOT_ABC: state <= (data == SYMBOL_A) ? GOT_A : NONE; default: state <= 2'bxx; endcase end MoĹźe wydawaÄ siÄ, Ĺźe uĹźycie 2-bitowego rejestru do zakodowania 4 stanĂłw jest efektywne -- w koĹcu zuĹźywa minimalnÄ liczbÄ bitĂłw. Okazuje siÄ jednak, Ĺźe dekodowanie tych stanĂłw (porĂłwnanie ``state == xxx`` zaszyte w ``case``) kosztuje nas znacznie wiÄcej niĹź bity w rejestrze. Znacznie efektywniejsze kodowanie jest nastÄpujÄ ce (nazywane one-hot -- w kaĹźdym momencie dokĹadnie jeden bit jest aktywny):: parameter NONE = 4'b0001; parameter GOT_A = 4'b0010; parameter GOT_AB = 4'b0100; parameter GOT_ABC = 4'b1000; reg state [3:0] = NONE; UĹźycie takiego kodowania eliminuje koszt dekodowania stanu (aby sprawdziÄ, czy jesteĹmy w stanie ``GOT_A``, wystarczy popatrzeÄ na bit 1) i upraszcza multiplexery kodujÄ ce nowy stan. PamiÄÄ RAM ========== Przy tworzeniu bardziej skomplikowanych ukĹadĂłw, przydaje siÄ moĹźliwoĹÄ zapisania wiÄkszej iloĹci danych. Do tego celu ĹluĹźy pamiÄÄ RAM. W ukĹadach FPGA najczÄĹciej mamy do czynienia z dwoma rodzajami RAMu: - distributed RAM -- wielkoĹÄ pojedynczego RAMu rzÄdu kilkudziesiÄciu bitĂłw, polega na uĹźyciu tablicy LUT elementu logicznego jako zapisywalnej pamiÄci. - block RAM -- wielkoĹÄ pojedynczego RAMu rzÄdu kilkunastu kilobitĂłw, dedykowany blok pamiÄci. RAM charakteryzuje siÄ nastÄpujÄ cymi parametrami: - synchroniczny bÄ dĹş asynchroniczny (w FPGA prawie zawsze znajdziemy tylko RAM synchroniczny, czyli wykonujÄ cy wszystkie operacje tylko na zboczach zegarowych) - szerokoĹÄ -- ile bitĂłw czytamy bÄ dĹş piszemy w jednym dostÄpie - wielkoĹÄ -- ile bitĂłw ma caĹy RAM - gĹÄbokoĹÄ -- ile róşnych adresĂłw mamy do dyspozycji (rĂłwne wielkoĹci podzielonej przez szerokoĹÄ) - liczba i typ portĂłw Port RAMu to interfejs, ktĂłrym moĹźemy dostaÄ siÄ do zawartych danych. Porty mogÄ byÄ do odczytu, do zapisu, bÄ dĹş do jednego i drugiego. WewnÄ trz FPGA spotkamy pamiÄÄ z jednym lub dwoma portami. Zdarza siÄ, Ĺźe porty majÄ róşnÄ szerokoĹÄ (czyli moĹźemy mieÄ np. pamiÄÄ, do ktĂłrej piszemy jednym portem po 8 bitĂłw, a czytamy drugim po 1 bicie). Zdarza siÄ teĹź, Ĺźe porty majÄ niezaleĹźne sygnaĹy zegarowe (pozwalajÄ c na dostÄp do tej samej pamiÄci z dwĂłch domen zegarowych). W bloku pamiÄci powinniĹmy siÄ spodziewaÄ nastÄpujÄ cych sygnaĹĂłw (po szczegĂłĹy odsyĹam do dokumentacji): - zegar - write enable (po jednym na port zapisujÄ cy) -- jeĹli 1, bÄdziemy wykonywaÄ zapis w danym cyklu - output enable (po jednym na port odczytujÄ cy, jeĹli port odczytujÄ cy jest synchroniczny) -- jeĹli 1, chcemy wykonywaÄ odczyt w danym cyklu - adres (po jednym na port) - dane wejĹciowe (po jednym na port zapisujÄ cy) - dane wyjĹciowe (po jednym na port odczytujÄ cy) PamiÄci moĹźemy uĹźyÄ na dwa sposoby: - rÄcznie wybraÄ bloki, ktĂłrych chcemy uĹźyÄ (https://www.xilinx.com/support/documentation/sw_manuals/xilinx11/spartan3e_hdl.pdf) i podĹÄ czyÄ odpowiednie sygnaĹy - napisaÄ tablicÄ rejestrĂłw w Verilogu i procesy, ktĂłre z niej czytajÄ i do niej piszÄ UĹźywajÄ c tego drugiego sposobu, zdajemy siÄ na nasze narzÄdzie do syntezy. Aby uĹźycie bloku pamiÄci siÄ udaĹo, naleĹźy zapewniÄ, Ĺźe wyspecyfikowany przez proces rzeczywiĹcie daĹo siÄ zrealizowaÄ sprzÄtowo: - nie moĹźemy mieÄ w kodzie wiÄcej zapisĂłw do pamiÄci, niĹź pamiÄÄ ma portĂłw do zapisu (i analogicznie z odczytem). - jeĹli chcemy uĹźyÄ portu zarĂłwno do odczytu jak i zapisu, musimy zapewniÄ, Ĺźe nie uĹźywamy zarĂłwno odczytu jak i zapisu w jednej ĹcieĹźce w naszym kodzie. JeĹli parametry pamiÄci dostÄpnej w FPGA nam nie odpowiadajÄ , moĹźemy zĹoĹźyÄ wiÄkszÄ pamiÄÄ z kilku blokĂłw: - aby otrzymaÄ szerszÄ pamiÄÄ, uĹźywamy kilku blokĂłw pamiÄci z wspĂłlnymi liniami adresowymi - aby otrzymaÄ gĹÄbszÄ pamiÄc, uĹźywamy kilku blokĂłw pamiÄci, multipleksera na portach odczytu, dekodera adresĂłw i maski sygnaĹu wĹÄ czajÄ cego na porcie zapisu - aby otrzymaÄ pamiÄÄ z wiÄkszÄ liczbÄ portĂłw odczytu, moĹźemy zduplikowaÄ caĹy blok pamiÄci razem z wszystkimi portami zapisu (doĹÄ drastyczne, ale bywa przydatne do plikĂłw rejestrĂłw w procesorach) Distributed RAM --------------- W ukĹadach Spartan 3E mamy do dyspozycji nastÄpujÄ ce rodzaje rozproszonego RAMu: - RAM16X1S -- 16-bitowy RAM o 1-bitowej szerokoĹci, jeden port (pozwalajÄ cy na odczyt i zapis). Zajmuje 1 blok logiki. - RAM16X2S -- 32-bitowy RAM o 2-bitowej szerokoĹci, jeden port (pozwalajÄ cy na odczyt i zapis). Zajmuje 1 blok logiki. - RAM32X1S -- 32-bitowy RAM o 1-bitowej szerokoĹci, jeden port (pozwalajÄ cy na odczyt i zapis). Zajmuje 1 blok logiki. - RAM64X1S -- 64-bitowy RAM o 1-bitowej szerokoĹci, jeden port (pozwalajÄ cy na odczyt i zapis). Zajmuje 2 bloki logiki. - RAM16X1D -- 16-bitowy RAM o 1-bitowej szerokoĹci, dwa porty -- jeden do odczytu i zapisu, drugi tylko do odczytu. Porty majÄ wspĂłlny zegar. Zajmuje 1 blok logiki. Zapis w pamiÄci rozproszonej dziaĹa synchronicznie (nastÄpuje na zboczu zegara), natomiast odczyt dziaĹa natychmiast (czyli moĹźemy przeczytaÄ coĹ z pamiÄci i natychmiast wykonaÄ na tym obliczenia). Rozproszony RAM jest uĹźywany do syntezy maĹych blokĂłw pamiÄci (poniĹźej kilobita) -- poniewaĹź elmenty sÄ bardzo maĹe, uĹźywa siÄ ich duĹźo i skĹada siÄ szersze i/lub gĹÄbsze pamiÄci. Z rozproszonego RAMu moĹźemy na przykĹad poskĹadaÄ plik rejestrĂłw w procesorze. Block RAM --------- W ukĹadach Spartan 3E mamy do dyspozycji 18-kilobitowe bloki RAMu. W naszym ukĹadzie mamy ich 4. Bloki RAMu majÄ dwa kompletnie niezaleĹźne porty, kaĹźdy dostÄpny do odczytu i zapisu. Porty mogÄ pracowaÄ na niezaleĹźnych zagarach i mieÄ róşnÄ szerokoĹÄ. SzerokoĹÄ kaĹźdego portu moĹźemy wybraÄ z nastepujÄ cych moĹźliwoĹci: - 1 bit - 2 bity - 4 bity - 8+1 bitĂłw - 16+2 bitĂłw - 32+4 bity ZarĂłwno zapis jak i odczyt sÄ synchroniczny (przy odczycie, podajemy adres w jednym cyklu i otrzymujemy wynik w kolejnym). Komunikacja miÄdzy domenami zegarowymi ====================================== MajÄ c w ukĹadzie synchronicznym wiele domen zegarowych napotykamy na problem przekazywania danych miÄdzy nimi. Dla pojedynczego sygnaĹu (np. linii przerwania) wystarczy synchronizator, ale przekazanie wiÄkszej iloĹci danych wymaga bardziej skomplikowanych ukĹadĂłw. Kod Graya --------- ZaĹóşmy, Ĺźe chcemy przekazaÄ miÄdzy dwiema domenami jakÄ Ĺ liczbÄ, ktĂłra moĹźe zmieniÄ siÄ co najwyĹźej o 1 (w gĂłrÄ bÄ dĹş w dĂłĹ) w kolejnych cyklach zegara. Przekazanie jej bezpoĹrednio przez tablicÄ synchronizatorĂłw nie zadziaĹa -- przy zmianie, zmiany róşnych bitĂłw mogÄ dojĹc w róşnych cyklach do nowej domeny zegarowej. Istnieje jednak kodowanie liczb, ktĂłre rozwiÄ zuje ten problem -- zapewnia, Ĺźe kaĹźde kolejne liczby sÄ kodowane do wektorĂłw bitowych róşniÄ cych siÄ w dokĹadnie jednej pozycji. Jest to kod Graya. Dla przykĹadu, kod 4-bitowy: - 0: 0000 - 1: 0001 - 2: 0011 - 3: 0010 - 4: 0110 - 5: 0111 - 6: 0101 - 7: 0100 - 8: 1100 - 9: 1101 - 10: 1111 - 11: 1110 - 12: 1010 - 13: 1011 - 14: 1001 - 15: 1000 Aby zakodowaÄ liczbÄ ``x`` do kodu graya, wystarczy policzyÄ ``x ^ (x >> 1)``. Dekodowanie jest trochÄ bardziej skomplikowane, ale doĹc efektywnie realizowalne w sprzÄcie. FIFO ---- Do przekazania duĹźej iloĹci danych miÄdzy domenami zegarowymi najczÄĹciej uĹźywa siÄ kolejek FIFO zrealizowanych za pomocÄ blokĂłw RAMu uĹźywanych jako buforĂłw cyklicznych: - jeden port dziaĹa tylko w trybie zapisu w domenie ĹşrĂłdĹowej - drugi port dziaĹa tylko w trybie odczytu w domenie docelowej - wskaĹşniki odczytu i zapisu sÄ przekazywane miÄdzy domenami zegarowymi w kodzie Graya