Strojno-opisni jeziki ponujajo različne načine opisovanja oz. modeliranja logičnega vezja. V jeziku SystemVerilog ločimo:
Prvi trije modeli predstavljajo jezikovni opis sheme vezja, ki ga sestavljajo logična vrata, gradniki z digitalnimi operatorji ter hierarhično sestavljena vezja. Zadnji opis uporabljamo za visokonivojsko načrtovanje vezij s procesi, ki je podobno pisanju kode v programskem jeziku. Model obnašanja vezja bomo podrobneje predstavili v naslednjem poglavju: Model obnašanja.
Verilog ima vgrajene modele najbolj osnovnih sestavnih elementov logičnih vezij: logičnih vrat, ojačevalnikov, tranzistorjev ipd., s katerimi sestavimo opis vezja. To je najnižji način modeliranja vezij, kjer eksplicitno med seboj povezujemo osnovne elemente. Primer:
logic x, y;
logic z;
and(z, x, y); // vrata IN, izhod je z, vhoda sta x in y
not(z, x); // negator z izhodom z in vhodom x
Modeli logičnih vrat predstavljajo Boolove funkcije, pri katerih je prvi argument izhod, ostali pa so vhodi. Standardna logična vrata and, or, nor, xor, nand, xnor imajo dva ali več vhodov. Poleg teh so na voljo še modeli različnih vrst ojačevalnikov (buf, buif0, bufif1), ki le prenašajo vrednost iz vhoda na izhod. Gradnikom je mogoče določiti tudi časovne zakasnitve. Modeli vezja na ravni logičnih vrat z zakasnitvami niso primerni za sintezo vezja, zato jih ne bomo podrobneje razlagali.
Model pretoka podatkov obravnava digitalno vezje kot skupek operacij nad signali in prenosov podatkov med vhodi in izhodi vezja. Operacije opišemo s prireditvenimi stavki (assign), ki so precej bolj kompaktni kot opis vezja na nivoju logičnih vrat. Predstavili bomo nekaj primerov.
Polni seštevalnik opišemo z dvema stavkoma, enega potrebujemo za vsoto, drugega pa za izhodni prenos.
assign s = a ^ b ^ cin;
assign cout = (a & b) | (cin & (a ^ b));
Stavek assign imenujemo kontinuirani prireditveni stavek. Simulator izvede ta stavek vsakokrat, ko se spremeni katerikoli signal na desni strani prireditvenega operatorja. Izraz na desni strani prireditve postane logični izhod (driver) za signal na levi strani.
Opis polnega seštevalnika z logičnimi vrati bi zahteval pet stavkov:
xor(x, a, b);
xor(s, x, cin);
and(c1, a, b);
and(c2, cin, x);
or(cout, c1, c2);
Logični operatorji jezika SystemVerilog so zelo uporabni za modeliranje Boolovih funkcij in transformacij podatkov s kombinacijskimi vezji. Poglejmo še nekaj praktičnih primerov vezij, ki jih opišemo s kontinuiranim prireditvenim stavkom in operatorji.
Večbitni seštevalnik opišemo preprosto z operatorjem seštevanja. Izračunana vsota vsebuje dodaten bit, ki ga lahko izkoristimo za izhodni prenos (carry).
module adder
(
input logic [7:0] a, b,
output logic [7:0] sum,
output logic carry
);
assign {carry, sum} = a + b;
endmodule
Izbiralnik ima en izhodni signal, na katerega poveže izbrani podatkovni vhod, ki ga določa
vrednost na izbirnem vhodu sel. Najmanjši izbiralnik 2-1 ima dva podatkovna vhoda: in0 in in1. Logično
funkcijo izbiralnika opišemo s stavkom: assign out = (in1 & sel) | (in0 & ~sel);
Bolj enostaven in razumljiv je opis izbiralnika 2-1 s ternarnim operatorjem ?:. V tem
primeru so podatkovni vhodi in izhod lahko tudi večbitne vrednosti. Primer izbiralnika 4-bitnih vrednosti:
module mux2_1
(
input logic sel,
input logic [3:0] in0, in1,
output logic [3:0] out
);
assign out = sel ? in1 : in0;
endmodule
Še bolj učinkovit pa je opis izbiralnika z operatorjem indeksiranja. Podatkovni vhod deklariramo kot vektorski signal, vrednost izbirnega vhoda pa uporabimo kot indeks za izbiro vhodnega bita, ki ga priredimo izhodu. Primer izbiralnika 8-1:
module mux8_1
(
input logic [2:0] sel,
input logic [7:0] in,
output logic out
);
assign out = in[sel];
endmodule
Razdeljevalniki izvajajo inverzno funkcijo: en vhod razdelijo na dva ali več izhodov. Razdeljevalnik s pozitivno logiko deluje tako, da je na izbranem izhodu vrednost vhoda, ostali pa so na 0. Narejeni so iz logičnih vrat AND in negatorjev. Za vsak izhodni signal je potrebno določiti logično funkcijo v obliki kontinuirane prireditve, lahko pa izhode združimo v vektor in zapišemo le en prireditveni stavek:
module demux1_4 // razdeljevalnik s štirimi izhodi
(
input logic in,
input logic [1:0] s,
output logic [3:0] out
);
assign out = {in & ~s[0] & ~s[1],
in & s[0] & ~s[1],
in & ~s[0] & s[1],
in & s[0] & s[1]};
endmodule
Še bolj učinkovito pa opišemo razdeljevalnik z operatorjem pomikanja: assign out = in << sel;
Osnovna enota opisa vezja je modul. Modul lahko vsebuje druge enote (module), ki jih vključimo in povežemo v zapisu:
<ime_modula> <ime_instance>(<povezave_signalov>);
Vključenemu modulu pravimo inštanca. Vsaki inštanci določimo ime za identifikacijo in razlikovanje med več enotami, saj lahko vključimo več modulov enake vrste. V oklepaju navedemo povezovalne signale, ki so signali deklarirani na ravni modula v katerega vključujemo drug modul.
Primer opisa štiribitnega seštevalnika, ki je sestavljen iz štirih polnih seštevalnikov (za lažje razumevanje dodajamo tudi celoten opis polnega seštevalnika full_adder, ki je sicer v svoji datoteki):
module full_adder(
input logic a,
input logic b,
input logic cin,
output logic s,
output logic cout
);
assign s = a ^ b ^ cin;
assign cout = (a & b) | (cin & (a ^ b));
endmodule
module adder(
input logic [3:0] a,
input logic [3:0] b,
output logic [3:0] sum
);
logic c0, c1, c2;
full_adder u0 (a[0], b[0], 1'b0, sum[0], c0 );
full_adder u1 (a[1], b[1], c0, sum[1], c1 );
full_adder u2 (a[2], b[2], c1, sum[2], c2 );
full_adder u3 (a[3], b[3], c2, sum[1], );
endmodule
Polni seštevalnik vključimo v strukturni opis vezja tako, da navedemo ime modula full_adder, enolično oznako, npr. u1 in povezave signalov v oklepaju. Povezave signalov so v obliki seznama, kjer je povezava določena z zaporednim mestom signala v oklepaju. Ta oblika opisa povezav se imenuje pozicijski zapis, ki je krajši, vendar postane nepregleden pri večjem številu priključkov in lahko vodi do napak. Če nek signal ni povezan, kot v primeru izhodnega prenosa zadnjega seštevalnika, moramo vseeno narediti vejico.
SystemVerilog pozna tudi daljši (imenski) zapis, pri katerem eksplicitno določimo kateri priključek vključenega modula je povezan z določenim signalom vezja. Primer takšnega zapisa za prvi polni seštevalnik:
full_adder u0 (
.a(a[0]),
.b(b[0]),
.s(sum[0]),
.cin(1'b0),
.cout(c0)
);
Eksplicitno povezovanje zahteva več pisanja, vendar je manj podvrženo napakam in ne zahteva določitve vseh povezav, ki so lahko zapisane v poljubnem vrstnem redu.
Visokonivojsko načrtovanje vezja predstavlja model obnašanja (behavioral), ki je najbolj podoben pisanju kode v nekem programskem jeziku. Kljub temu pa so pomembne razlike med strojno-opisnim jezikom, ki opisuje paralelne strukture vezja in običajnim programskim jezikom, ki določa zaporedje izvajanja ukazov.
Kombinacijska vezja opišemo z blokom always_comb.
Prireditveni stavki znotraj bloka, ki je omejen z begin in end so zapisani z operatorjem =
brez rezervirane besede assign.
Primer opisa polnega seštevalnika:
module adder(
input logic a,
input logic b,
input logic cin,
output logic s,
output logic cout
);
always_comb
begin
s = a ^ b ^ cin;
cout = (a & b) | (cin & (a ^ b));
end
endmodule
Pogojni stavek ima obliko: if (<pogoj>) stavek1; else stavek2;, pri čemer lahko else izpustimo. Stavek je lahko tudi nov pogojni
stavek, kar je uporabno za opis zaporednih primerjav. Če naj se ob pogoju izvede več
stavkov, jih zapišemo med begin in end.
Primer opisa vezja, ki da na izhod maksimalno vrednost:
module max(
input logic [3:0] a,
input logic [3:0] b,
output logic [3:0] m
);
always_comb begin
if (a>b) m = a;
else m = b;
end
endmodule
V opisu kombinacijskih vezij je potrebno izhod določiti ob vseh pogojih, kar pomeni za if in za else. Če izpustimo stavek else, moramo predhodno določiti privzeto vrednost signalov, sicer se bo vrednost ohranjala in bi dobili sekvenčni zapah:
always_comb begin
y = 1'b0; // privzeta vrednost kadar pogoj ni izpolnjen
yn = 1'b1;
if (trig) begin
y = x;
yn = ~x;
end
end
Stavek case uporabljamo za pogojno izvedbo stavkov ob naštetih vrednostih signala ali izraza. Uporaben je za opis dekodirnikov, naprimer
dekodirnika iz 4-bitne bcd v sedem segmentno kodo:
always_comb begin
case (bcd)
4'h0: seg = 8'b11111100;
4'h1: seg = 8'b01100000;
4'h2: seg = 8'b11011010;
4'h3: seg = 8'b11110010;
4'h4: seg = 8'b01100110;
4'h5: seg = 8'b10110110;
4'h6: seg = 8'b10111110;
4'h7: seg = 8'b11100000;
4'h8: seg = 8'b11111110;
4'h9: seg = 8'b11110110;
default: seg = 8'b00000000; // ugasni segmente če ni BCD koda
endcase
end
Naštejemo lahko tudi več vrednosti, ki jih med seboj ločimo z vejico. Primer vezja za detekcijo, ali je BCD vrednost praštevilo:
always_comb begin
case (bcd)
1,2,3,5,7: prime = 1'b1; // BCD je praštevilo
default : prime = 1'b0;
endcase
end