SystemVerilog

De la WikiLabs
Jump to navigationJump to search

Module (sintetizabile)

Limabjul Verilog este structurat pe module. Fiecare modul reprezintă un circuit care implementează o anume funcție. Spre exemplu un modul poate reprezenta un sumator, adică un circuit care are două intrări ce specifică cei doi operanzi și o ieșire ce reprezintă rezultatul adunării. Conținutul modulului reprezintă descrierea (structurală sau comportamentală) a porților care calculeaza suma celor două intrări. Prin urmare, definiția unui modul Verilog are două părți:

  • interfață - lista tuturor porturilor de intrare și ieșire ale circuitului, specificate prin nume și dimensiune;
  • implementare - descrierea efectivă a circuitului care se folosește de valorile de intrare pentru a calcula valorile de ieșire;

Interfața modulelor Verilog

Interfața modulului Adder este prezentată mai jos. Observație: Ca și în sistemul numeral zecimal, unde suma a două numere de n cifre are nevoie de n + 1 cifre (9 + 9 = 18), și în sistemul binar, suma a două numere de n biți va fi pe n + 1 biți.

Reprezentarea interfetei modulului "Adder" (black box)
module Adder(
    output [4:0] out,
    input  [3:0] in0,
    input  [3:0] in1
);

//implementare

endmodule

Cuvintele cheie module și endmodule sunt folosite pentru a începe și a încheia definirea unui modul. Imediat după cuvântul cheie module urmează numele modulului.

Convenție: Numele unui modul va începe cu literă mare.

După definirea numelui modulului, urmează lista de porturi, plasată între paranteze rotunde și separate prin virgulă. Cuvintele cheie acceptate sunt output (reprezentând un port de ieșire), input (reprezentând un port de intrare) și inout (reprezentând un port bidirecțional).

Sfat: Când aveți de ales în privința interfeței unui modul, se evită utilizarea semnalelor de tip inout. Acestea introduc elemente de tip Tri-state Buffer care sunt ineficiente. O alternativă mai eficientă este definirea a două porturi, unul de intrare și unul de ieșire, cu nume similare (ex: data_in și data_out).

Convenție: Întâi se definesc ieșirile, apoi intrările unui modul.

Convenție: Porturile unui modul se scriu unul sub altul, pe câte o linie, aliniate cu un tab la dreapta față de cuvântul cheie module.

După tipul portului, urmează dimensiunea acestuia, specificată în indecșii biților, unde cel mai puțin semnificativ bit are indexul 0. Spre exemplu, un semnal de 4 biți va avea următoarea specificație [3:0] (bitul cel mai semnificativ are indexul 3, cel mai puțin semnificativ 0, în total 4 biți). Observație: Semnalelor de un bit le lipsește specificația de dimensiune:

input oneBitSignal,

După lista de porturi aflată între paranteze, definirea interfeței se termină cu caracterul ;.

Implementarea modulelor Verilog

Implementarea modulelor Verilog se face prin blocuri. Aceste blocuri pot avea sau nu corespondent într-un ciruit fizic. Dacă toate blocurile unui modul au corespondent într-un circuit fizic, atunci modulul este sintetizabil și poate fi transformat într-un circuit fizic. Blocurile care pot genera construcții sintetizabile sunt de patru tipuri:

  • blocuri assign
  • blocuri always
  • blocuri de instanțiere
  • blocuri generate

În plus, în Verilog se pot defini fire (wire) și registre (reg).

Blocurile care sunt întotdeauna nesintetizabile și sunt folosite exclusiv pentru simulare:

  • blocuri initial
  • blocuri forever

Observație: Nu orice bloc assign sau always este sintetizabil. Există construcții valide sintactic dar care nu au corespondent în circuit. Aceste blocuri pot fi simulate dar nu pot fi utilizate pentru programarea unei plăci FPGA.

Observație: Ordinea blocurilor într-un modul nu contează.

Fire (wire) și registre (reg)

Firele sunt utilizate pentru legături între module și pentru asignarea de rezultate parțiale în circuite combinaționale, prin urmare:

Regulă: Un element de tip wire își schimbă valoarea doar în blocuri de tip assign sau ca ieșire a unui modul (niciodată ambele simultan).

Firele într-un modul Verilog se definesc în felul următor:

wire [3:0] fir;

Registrele sunt utilizate uzual pentru implementarea ciruitelor secvențiale, și atunci ele definesc registre fizice, dar:

Observație: Un element de tip reg nu se translatează neapărat într-un registru fizic. Translatarea lui depinde de modul în care se utilizează.

Un registru se definește în felul următor:

reg [3:0] registru;

Regulă: Un element de tip reg își schimbă valoarea doar în blocuri de tip always sau initial.

Regulă: Niciun element nu își poate schimba valoarea în mai mult de un bloc. Adică pentru un fir (wire), nu pot exista două blocuri assign în care acesta ia valori, iar pentru elemente reg, nu există decât un bloc always sau initial în care acesta își schimbă valoarea.

Observație: O intrare a unui modul este întotdeauna de tip wire. Adică declarația

    input [3:0] in0,

este echivalentă cu

    input wire [3:0] in0,


Observație: O ieșire a unui modul poate fi de tip wire sau reg. Dacă nu se specifică, ea este implict de tip wire. Adică declarația

    output [4:0] out,

este echivalentă cu

    output wire [4:0] out,

Blocuri assign

Modulul Adder implementat

Assign este un cuvănt cheie care generează circuite combinaționale. Cum sumatorul este un circuit combinațional, și ieșirea acestuia este implicit de tip wire, îl putem implementa cu un bloc assign:

module Adder(
    output [4:0] out,
    input [3:0] in0,
    input [3:0] in1
);

assign out = in0 + in1;

endmodule

Observație: În general, un bloc assign va genera un circuit sintetizabil. Există și excepții, atunci când operația dorită este prea complexă pentru a fi implementată printr-un circuit combinațional, în mod eficient. Spre exemplu:

assign out = in0 / in1;

nu este un cod sintetizabil pentru majoritatea tool-urilor de sinteză, dar funcționează perfect într-o simulare.

Blocuri always combinaționale

Întotdeauna, un bloc always e utilizat pentru a da valori unor semnale de tip reg. Un bloc always se poate traduce în circuite combinaționale sau circuite secvențiale, în funcție de lista acestuia de sensitivități. Formatul general pentru un bloc always este următorul:

always@(<lista_sensitivitati>) begin
    //...
end

După cum îi spune și numele, lista de sensitivități este lista semnalelor la care este sensibil, adică de care depind registrele descrise de blocul always. Dacă în lista de sensitivități este trecut doar numele unui semnal, fără nici un alt specificator suplimentar, atunci blocul este sensibil la orice schimbare a acestui semnal. Dacă un bloc always are mai multe semnale la care acesta este sensibil, ele se despart în lista de sensitivități folosind cuvântul cheie or.

Observație: Dacă lista de sensitivități conține doar semnale fără alți specificatori, atunci rezultatul sintezei blocului va fi un circuit combinațional.

În acest caz, putem reface implementarea sumatorului folosind un bloc always în felul următor:

module Adder(
    output reg [4:0] out,
    input [3:0] in0,
    input [3:0] in1
);

always@(in0 or in1) begin
    out = in0 + in1;
end

endmodule

Blocuri always secvențiale. Asignări non-blocante (non-blocking assignments)

Circuitele secvențiale sunt circuitele care sunt sincronizate de semnalul de ceas. Acest semnal este, de obicei, produs de un generator de ceas și se definește ca intrare pentru fiecare modul secvențial (în care există cel puțin un registru). Putem modifica exemplul anterior, astfel încât ieșirea modulului de sumare să fie sincronă (adică să se modifice doar pe frontul pozitiv de ceas). Astfel, în interfața modulului, apare și semnalul de ceas:

module SyncedAdder(
    output reg [4:0] out,
    input [3:0] in0,
    input [3:0] in1,
    input       clock
);

//implementation here

endmodule

Regulă: Pentru descrierea circuitelor secvențiale, se folosește întotdeauna un bloc always.

Modulul SyncedAdder

Blocul always care descrie un circuit secvențial are în lista de sensitivități numai semnalul de ceas iar el nu este sensibil la orice tranziție a ceasului ci numai la unul din fronturi (de obicei cel pozitiv). Astfel, putem implementa sumatorul sincron în felul următor:

module SyncedAdder(
    output reg [4:0] out,
    input [3:0] in0,
    input [3:0] in1,
    input clock
);

always@(posedge clock) begin
    out <= in0 + in1;
end

endmodule

Observație: Cuvântul cheie care specifică frontul pozitiv al unui semnal este posedge iar cel pentru frontul negativ al semnalului este negedge.

Observați operatorul <= folosit pentru atribuirea sumei registrului destinație. Acesta NU este operatorul logic de mai mic sau egal, ci reprezintă un mod de atribuire care se numește non-blocantă. Diferența dintre atribuirea blocantă (=) și cea non-blocantă (<=) este că cea de-a doua întâi evalueaza toate expresiile din partea dreaptă a operatorului de atribuire (în cazul de față, suma in0 + in1) și abia apoi asignează rezultatul registrului destinație. Spre exemplu, dacă dorim să inversăm valorile a două registre, reg0 și reg1, astfel încât reg0 să ia valoarea lui reg1 și invers, vom face în felul următor:

module RegSwapper(
    //...
);

reg [31:0] reg0;
reg [31:0] reg1;

//cod corect
always@(posedge clock) begin
    reg0 <= reg1;
    reg1 <= reg0;
end

endmodule

Modulul se va comporta conform așteptărilor. Operatorul <= nu va bloca restul evaluărilor din blocul always și prin urmare toate asignările se fac simultan, după evaluarea expresiilor din partea dreaptă a operatorului. Pe de altă parte, dacă facem asignarea blocantă:

module RegSwapper(
    //...
);

reg [31:0] reg0;
reg [31:0] reg1;

//cod incorect
always@(posedge clock) begin
    reg0 = reg1;
    reg1 = reg0;
end

endmodule

aceasta va bloca restul operațiilor până când asignarea se termină. În acest caz, după primul front de ceas, ambele registre vor conține valoarea inițială a lui reg1.

Regulă: Operatorul de asignare non-blocantă (<=) se folosește doar în blocuri always, initial sau forever.

Regulă: În același bloc always, se folosește același tip de operator de asignare pentru toate operațiile.

Sfat: Utilizați asignare non-blocantă pentru toate blocurile always care generează circuite secvențiale (sunt sincronizate de ceas) și asignare blocantă în toate celelalte cazuri.

Blocuri de instanțiere

Un sumator de patru numere format din trei sumatoare de două numere

Odată definit un modul, acesta poate fi folosit de oricâte ori pe parcursul unuia sau mai multor proiecte. Acest sistem se numește instanțiere. Să luam, ca exemplu, un modul care trebuie să facă suma a 4 numere pe 3 biți. Putem să folosim în acest scop modulul Adder pe 4 biți definit mai sus, în configurația din figură. Observați că se utilizează același modul de trei ori. Este ineficient și inutil să scriem de trei ori implementarea modulului, așa că vom recurge la metoda instanțierii. Orice instanță a unui modul trebuie să aibă un nume unic, după cum și într-un limbaj de programare, orice variabilă de același fel trebuie sa aibă un nume unic.

Se observă în continuare că există semnale (fire) în modulul Adder4, care nu sunt nici intrări, nici ieșiri, și sunt folosite doar pentru conectarea modulelor Adder (cel roșu și cel verde). Aceste semnale sunt fire (wire) și trebuie declarate ca atare.

Sintaxa pentru instanțierea unui modul este următoarea:

NumeModulInstantiat numeInstanta(
    .nume_intrare_sau_iesire_0(nume_semnal_legat_la_intrare_sau_iesire_0),
    .nume_intrare_sau_iesire_1(nume_semnal_legat_la_intrare_sau_iesire_1),
    //...
    .nume_intrare_sau_iesire_n(nume_semnal_legat_la_intrare_sau_iesire_1),
);

Module de test (nesintetizabile)

Sintaxa Verilog

Operatori

Blocuri condiționale