Dr Ryan Robucci
Objective
Verilog (IEEE 1364) is a hardware description language (HDL) that can be used to model electric systems for simulation and hardware synthesis and verification.
Logic Function of Hardware (modified from Patrick Schaumont, A Practical Introduction to Hardware/Software Codesign)
and Time
📝 concurrency
📝 parallelism
Concurrency and Parallelism Reference: Patrick Schaumont, A Practical Introduction to Hardware/Software Codesign
Structural models
Dataflow models
Behavioral models :
Register-Transfer Level (RTL)
Built-in primitives provide a starting set of building blocks for describing digital logic.
not
and
, or
: e.g. and myand1 (out,a,b);
nmos
, pmos
, supply1
, supply0
, etc…module mux_structural(d0,d1,sel,y); input d0,d1; input sel; output y; //defaults to type wire, which is suitable for // making connections between parts in netlists wire sel_n,w0,w1; not i0 (sel_n,sel); and i1 (w0,d0,sel_n); and i2 (w1,d1,sel); or i3 (y,w0,w1); endmodule
Line 1
Line 4-6
endmodule
keyword to conclude the module definitionLine 8
Line 9-12
Understand the concurrent description: at the base Verilog constructs are concurrent by default. In the example, the order of the gates provided in lines 8-11 do not matter
A testbench:
module tb(); //a testbench is typically self-contained and thus has no ports //Internal Signals reg d0,d1,sel; //reg: signals that will be assigned within the same // hierarchy level using procedural code wire y; //wire: signals for connections //In SystemVerilog, use: logic d0,d1,sel,y; //Instantiation of module, referred to as Device Under Test mux_structural dut(.*); //SystemVerilog default connection syntax // for standard Verilog use explicit port mapping // mux_structural dut(.d0(d0),.d1(d1),.sel(sel),.y(y)); integer count; //working integer variable initial begin //produral code is typically used to generate input stimulus count = 0; // unless a test-jig module forever #1 count++; // is instantiated to interact with the DUT end assign {d0,d1,sel}=count; initial #0 $display("d0 d1 sel y"); // zero-delay #0 ensures that printing // after circuit initialization is completed initial $monitor("%2b ",d0,"%2b ",d1,"%2b ",sel," ","%2b ",y); initial #7 $finish; //terminates simulation endmodule
The highlighted code lines with the DUT instantiation, the assign
statement, the run-once initial
lines, and the block of code in lines 14-17 may be provided in any order since the base level of code is concurrent.
The lines within begin end statements, lines 15-16, are sequntial code and only there does order matter.
The $monitor
is a printing task supporting multiple inputs, as well as formatting strings followed by the arguments satisfying inputs to the formatting string. The monitor task automatically re-triggers and prints when any mutable input changes.
Results: (iverilog ‘-Wall’ ‘-g2012’ design.sv testbench.sv && unbuffer vvp a.out)
d0 d1 sel y
0 0 0 0
0 0 1 0
0 1 0 0
0 1 1 1
1 0 0 1
1 0 1 0
1 1 0 1
1 1 1 1
Another Example Using Multi-Bit Vectors:
module mux_structural_2bit(d0,d1,sel,y);
input [1:0] d0,d1; //multi-bit input
input sel;
output [1:0] y; //multi-bit output
// making connections between parts in netlists
wire sel_n;
wire [1:0] w0,w1; //multi-bit internal buses
not i0 (sel_n,sel);
and i1 (w0[0],d0[0],sel_n);
and i2 (w1[0],d1[0],sel);
or i3 ( y[0],w0[0],w1[0]);
and i4 (w0[1],d0[1],sel_n);
and i5 (w1[1],d1[1],sel);
or i6 ( y[1],w0[1],w1[1]);
endmodule
module tb();
reg [1:0] d0,d1;
reg sel;
wire [1:0] y;
mux_structural_2bit dut(.*);
integer count;
initial begin
count = 0;
forever #1 count++;
end
initial $display("%3s ","sel","%3s ","d1","%3s ","d0"," ","%3s ","y");
initial $monitor("%3b ", sel ,"%3b ", d1 ,"%3b ", d0 ," ","%3b ", y );
assign {sel,d1,d0}=count;
initial #31 $finish;
endmodule
sel d1 d0 y
0 00 00 00
0 00 01 01
0 00 10 10
0 00 11 11
0 01 00 00
0 01 01 01
0 01 10 10
0 01 11 11
0 10 00 00
0 10 01 01
0 10 10 10
0 10 11 11
0 11 00 00
0 11 01 01
0 11 10 10
0 11 11 11
1 00 00 00
1 00 01 00
1 00 10 00
1 00 11 00
1 01 00 01
1 01 01 01
1 01 10 01
1 01 11 01
1 10 00 10
1 10 01 10
1 10 10 10
1 10 11 10
1 11 00 11
1 11 01 11
1 11 10 11
1 11 11 11
Dataflow models : assignment expressions
=
with either wire declaration or later with assign
+
, -
, *
, /
, %
, **
<
, >
,<=
, >=
==
, !=
, also ===
and !==
for metalogic support
==?
, !=?
&&
, ||
, !
->
, and logical equivalence <->
(see Verilog Manual)&
, |
, ^
, ~^
(xnor), ^~
(xnor)~
&
, ~&
, |
, ~|
, ^
, ~^
>>
, <<
>>>
, <<<
?
:
{
comma-separated list }
{
count{
comma-separated list }
}
One might consider Structural Verilog as inclusive of dataflow, since many of the dataflow operations that map directly to built-in logic primitives and specifications of net connections.
The following are the same in many contexts
and n0(x,a,b,c); //primitive instantiation
assign x = a & b & c; // dataflow
assign x = &{a,b,c}; // dataflow showing concatenation and reduction operator
The exact implementation and structure implied in the following is less certain unless we explicitly know the exact module that addition would map to with our synthesizer and library
assign x = a+b;
Dataflow modeling example
Module:
module mux_dataflow_2bit_enables(d0,d1,sel,enCh0,enCh1,enGlobal,y);
input [1:0] d0,d1;
input sel,enCh0,enCh1,enGlobal;
output [1:0] y;
wire sel_n;
wire w0,w1;
wire y0,y1;
assign w0 = (d0[0] & ~sel) | (d1[0] & sel); //NOT, ANDs, OR
assign w1 = sel ? d1[1] : d0[1]; // C-like conditional ternary operator
assign y0 = w0 & enCh0 & enGlobal; // string of binary (2-inputs) AND gates forms
// a three-input AND gate
assign y1 = & {w1,enCh1,enGlobal}; // reduction and compresses 3 bits to
// one resulting bit result using the AND operator
assign y = {y1,y0}; // concatenation operator
endmodule
Testbench:
module tb();
reg [1:0] d0,d1;
reg enCh0,enCh1,enGlobal;
reg sel;
wire [1:0] y;
mux_dataflow_2bit_enables dut(.*);
integer count;
initial begin
count = 0;
forever #1 count++;
end
initial $display("%8s ","enGlobal","%6s ","enCh1","%6s ","enCh0", "%3s " ,"sel","%3s ","d1","%3s ","d0"," ","| %3s","y");
initial $monitor("%8b ", enGlobal ,"%6b ", enCh1 ,"%6b ", enCh0 , "%3b " , sel ,"%3b ", d1 ,"%3b ", d0 ," ","| %3b", y );
assign {enGlobal,enCh1,enCh0,sel,d1,d0}=count;
initial #255 $finish;
endmodule
Output:
enGlobal enCh1 enCh0 sel d1 d0 | y
0 0 0 0 00 00 | 00
0 0 0 0 00 01 | 00
0 0 0 0 00 10 | 00
0 0 0 0 00 11 | 00
0 0 0 0 01 00 | 00
...
0 0 1 1 00 11 | 00
0 0 1 1 01 00 | 00
...
0 1 0 0 10 00 | 00
0 1 0 0 10 01 | 00
0 1 0 0 10 10 | 00
0 1 0 0 10 11 | 00
0 1 0 0 11 00 | 00
...
1 0 1 1 01 11 | 01
1 0 1 1 10 00 | 00
1 0 1 1 10 01 | 00
...
1 1 0 0 01 11 | 10
1 1 0 0 10 00 | 00
1 1 0 0 10 01 | 00
1 1 0 0 10 10 | 10
1 1 0 0 10 11 | 10
....
1 1 1 1 10 01 | 10
1 1 1 1 10 10 | 10
1 1 1 1 10 11 | 10
1 1 1 1 11 00 | 11
1 1 1 1 11 01 | 11
1 1 1 1 11 10 | 11
1 1 1 1 11 11 | 11
Behavioral code is implemented in procedural blocks that include one or several statements that describe an algorithm to define the behavior of a block of logic in a simulation or in hardware
A procedural block may include sequential statements from which the algorithm may be understood by beginning interpretation of statements one at a time (similar to traditional software coding languages) or
parallel statements intended to be interpreted in parallel.
The creation of behavioral code is sometimes characterized by a lack of regard for hardware realization
Synthesizable Behavioral Code is code that a given synthesizer can map to a hardware implementation
The definition of synthesizable is synthesizer dependant- some simple prodecural code constructs are universally
synthesizable by every synthesizer, while more complex code blocks and certain operators are not considered
synthesizable by many
Example :
x = myUINT8 >> 2;
// This is a shift by a constant implemented by a simple routing of bits.
// It is generally regarded as synthesizable
x = myUINT8 >> varShift;
// This is a variable shift with many possible implementations.
// It will simulate just fine, but at the synthesis step many
// synthesizers will throw an error saying that this is not synthesizable
Behavioral code may indeed describe behavior in such a way that is not directly synthesizable by almost any synthesizer (such as reading waveforms from a .txt file) – though what is “synthesizable” is always defined by the synthesizer tool being used
Initial and Always blocks will be the first two types of blocks we will discuss (tasks, functions)
Initial blocks are triggered once at the start of a simulation or in the case of some synthesis tools may be used to describe the power-up state of registers or may be used to describe the initial default value of an intermediate variable
Always blocks continually repeats its execution during a simulation, but optionally can be gated (paused) so as to describe evaluation triggered with change in one or more signals as provided in a sensitivity list.
Assuming no delay statements are included in the procedural code: the keywords begin
and end
may be used to encapsulate a description of an algorithm using a block of sequential code.The code is just a description of a desired behavior and does not necessarily mimic the implementation itself – the entire description is evaluated in one instant in time (takes 0 time to complete)
begin
and end
like { and } in CWhen describing clocked, register hardware that should updates its observable outputs only when the clock and/or aync control signals changes, provide only the change-invoking control signals with the appropriate edge selection in the sensitivity list
always @ (posedge clk, posedge reset)
SystemVerilog:
always_ff @ (posedge clk, posedge reset) begin...end
or always_latch
for latches
When describing only combinational hardware, which should update its observable output when data inputs change regardless of the direction of change, list all of the function dependencies in the sensitivity list.
always @ (a, b, c, d) begin...end
SystemVerilog:
always_comb begin...end
When providing a mix, the combinational results may not be used outside the block, and edge-selection must be used
RTL Verilog example:
module mux_rtl_2bit_enables(d0,d1,sel,enCh0,enCh1,enGlobal,y);
input [1:0] d0,d1;
input sel,enCh0,enCh1,enGlobal;
output reg [1:0] y;
wire sel_n;
wire w0, w1;
always @ (d0,d1,sel,enCh0,enCh1,enGlobal) begin: blk0
reg y1,y0;
y1 = 1'bx;
y0 = 1'bx;
if (enGlobal) begin
case (sel)
1'b0 : begin
{y1,y0} = { d0[1]&enCh1 , d0[0]&enCh0 };
y = {y1,y0};
end
1'b1 : y = d1 & {enCh1,enCh0}; //bit-wise AND, mult-bit assignment
endcase
end else begin
y = 2'b0;
end
end
endmodule
Testbench:
module tb();
reg [1:0] d0,d1;
reg enCh0,enCh1,enGlobal;
reg sel;
wire [1:0] y_rtl;
wire [1:0] y_df;
mux_dataflow_2bit_enables dut_df (.y(y_df) ,.*);
mux_rtl_2bit_enables dut_rtl(.y(y_rtl),.*);
integer count;
initial begin
count = 0;
forever #1 count++;
end
wire [1:0] match = y_df~^y_rtl;
initial $display("%8s ","enGlobal","%6s ","enCh1","%6s ","enCh0", "%3s " ,"sel","%3s ","d1","%3s ","d0"," ","| %6s","y_df"," %6s","y_rtl", "%6s" ,"match");
initial $monitor("%8b ", enGlobal ,"%6b ", enCh1 ,"%6b ", enCh0 , "%3b " , sel ,"%3b ", d1 ,"%3b ", d0 ," ","| %6b", y_df ," %6b", y_rtl , "%6b" , match );
assign {enGlobal,enCh1,enCh0,sel,d1,d0}=count;
initial #255 $finish;
endmodule
Output:
enGlobal enCh1 enCh0 sel d1 d0 | y_df y_rtl match
0 0 0 0 00 00 | 00 00 11
0 0 0 0 00 01 | 00 00 11
0 0 0 0 00 10 | 00 00 11
...
1 1 1 1 11 10 | 11 11 11
1 1 1 1 11 11 | 11 11 11
...
Previous versions of Verilog used or keyword in sensitivity list, which referred to a sensitivity to a change in any of a or b or c.
always @ (a or b or c)
This is not the same as being sensitive to a change in the result (a|b|c).
always @ (a|b|c); //⚠️ not the same, should not code this way
With Verilog 2001: Use a comma-separated sensitivity list
always @ (a, b, c, d)
always @ (posedge clk, posedge reset)
Shortcut for including all dependencies (inputs) found in the associated procedural code block:
always @ (*) begin...end
in SystemVerilog, replaced by always_comb:
always_comb begin...end
There are 2 kinds of assignment statements:
=
operator, and<=
operator.
When discussing a variable y declared using reg y
; refer to y as “reg” “y”, not as “register” “y”. You can also refer to it as “variable” “y”.
reg
keyword does not mean Register : Here is something to be cleared up right away when learning Verilog reg
is just a variable. In fact the type of the signal that is declared by using ref
is called a variable
in later versions of Verilog because the the name is so confusing. So, don’t let yourself be confused by the name, reg
does not necessarily register manifest as a register in hardware.Origin of the confusion: “Although the Verilog HDL is used for more than just simulation, the semantics of the language are defined for simulation, and everything else is abstracted from this base definition.” – page 64 of Verilog Standard
From a simulation perspective, a reg
is assigned through procedural code and in the execution of that code the simulator might require a memory in order to compute the resulting value of a reg
.
Implementing OR with a variable (reg) in simulation
The code below describes a signal y that is updated with the value a|b|c any time a,b,or c changes. Clearly this is a 3-input OR gate, which is combinatorial. However, the algorithm/code used to describe the function requires a memory of y to compute the resulting value of y. To a simulator, a type reg
is internally modeled distinctly than a wire
.
...
reg y;
always @(a,b,c) begin
y = a;
y = y|b;
y = y|c;
end
...
Implementing without variable in simulation
This code computes the result of the expression assigning a|b|c and after computing the result, independant of y, and schedules an assignment of that result to y.
wire y;
assign y = a|b|c;
A reg
is used in procedural-code as variables, which may end up being implemented in a synthesized circuit using sequential logic but just represent the output net of combinatorial logic otherwise.
Wires on the other hand are for structural connections (nets/wires) between modules or outputs of combinatorial expressions.
There is a simple rule to decide if you should use a reg
or a wire
: if the signal is assigned from a procedural block of code or not. It is so straightforward, that in SystemVerilog you can just use the datatype logic
and the signal type of reg or wire is effectively inferred from its use.
System Verilog Update:
.sv
rather than .v
to denote expected SystemVerilog interpretation of regTake care in specifying the sensitivity list : the contents of sensitivity list needs careful attention. A mistake here is a common cause for diferences between Verilog simulation and synthesized hardware.
always @ (a,b,c) begin
x = (c & a) | (~c & b);
end
always @ (a,c) begin
x = (c & a) | (~c & x);
end
always @ (a,b,c) begin
if (c) x = a;
else x = b;
end
always @ (c) begin
if (c) x = a;
else x = b;
end
always @ (a,b,c) begin
if c x = a;
end
Code interpretation for combinational logic
If results from any execution of the block directly rely on signals/results generated from a previous execution of the block then sequential logic is describe. This does not include the case when results are saved using external sequential logic.
Draw the truth table for each block with and without considering the sensitivity list, it should include a row for every possible input combination and the output variables should should never occur in the output columns
SystemVerilog always replacements always_ff, always_comb, always_latch
always_comb begin
x = (c & a) | (~c & b);
end
always @ (a,c) begin
x = (c & a) | (~c & x);
end
always_comb begin
if (c) x = a;
else x = b;
end
always_comb begin
if (c) x = a;
else x = b;
end
always_comb begin
if c x = a;
end
The stimulus (input)
$display
$monitor
or $strobe
statements to print result to screen or fileA common format for storing a waveform from simulation is a Value Change Dump file
Rather than store values of several variables at EVERY timestep, a VCD file has a record entry for only changes (pairs of time and value-canges )
Each entry is a time along with a list of changes
Time | Value Changes |
---|---|
0 ns | y1=0, y2=0 |
73ns | y1=1, y2=1 |
75ns | y2=0 |
90ns | y2=x |
$dumpfile("filename")
in a procedural code, commonly in an initial code block initial begin..end
$dumpvars;
with no arguments to dump all signals. Called from procedural code, commonly in initial begin..end
🫣 Controls for Dump
IEEE Std 1364™-2005
https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=1620780
$dumpfile("filename.vcd")
$dumpvars;
: dumps all variables$dumpvars(1);
: dumps all variables in the current scope$dumpvars (1, top)
: dump vars in module top
$dumpvars (0, top)
: dump vars in and below level top
$dumpvars (0, top.module1, top.module2.net1);
dump vars in and below level top.module1
as well as net1
in module2
$dumpoff;
disables dumping$dumpon;
enables dumping
initial begin
#10 $dumpvars( . . . );
#200 $dumpoff;
#800 $dumpon;
#900 $dumpoff;
end
$dumpall;
create checkpoint with the value of all selected variables$dumplimit ( filesize ) ;
maximum size before dumping is disabled$dumpflush
: flushes output, useful before crashing code, or perhaps before external library calls that potentially read dump fileExamples:
module top(led2,led1,led0, rst, clk);
input clk;
input rst;
ouput logic led2;
ouput logic led1;
ouput logic led0;
always_ff @ (posedge rst,posedge clk)
if (rst)
begin
led0 <= 1;
led1 <= 0;
led2 <= 0;
end
else
begin
led0 <= 0;
led1 <= led0;
led2 <= led2;
end
endmodule
module top_tb;
logic clk;
logic rst;
logic led2;
logic led1;
logic led0;
top DUT(
.led2(led2),
.led1(led1),
.led0(led0),
.rst(rst),
.clk(clk)
);
initial begin
$dumpfile("results.vcd");
$dumpvars;
rst = 1; clk = 0;
#10;
clk = 1;
#5;
clk = 0;
#5;
clk = 1;
#5;
clk = 0;
#5;
clk = 1;
#5;
clk = 0;
#5;
clk = 1;
#5;
clk = 0;
#20;
$finish;
end
endmodule
Possible Blueprint:
There are multiple ways to define the ports. It is not important right now to master all of these or is it nessisary to be familiar with them to begin design, but it is important to be aware of alternate syntax in case you encounter code using another style. (may show up in slides just becuase it is more compact for presentation)
🧐 Port Declaration Options
Verilog 2000 has New Port Declaration Options
Verilog 95 code:
module memory( read, write, data_in, addr, data_out);
input read;
input write;
input [7:0] data_in;
input [3:0] addr;
output [7:0] data_out;
reg [7:0] data_out;
After the port list, port direction must be declared to be input, output, or inout as well as the width if more than one bit
Type declaration: type is by default a wire unless another type is provided
Verilog 2k with direction and data type listed:
module memory(
input wire read,
input wire write,
input wire [7:0] data_in,
input wire [3:0] addr,
output reg [7:0] data_out
);
Verilog 2k with no type in port list
module memory(
input read,
input write,
input [7:0] data_in,
input [3:0] addr,
output reg [7:0] data_out
);
ports are declared as wire by default, override with keyword reg
keyword
However, there is a stylistic disadvantage to exposing type in port declaration. In the following two examples, the function is the same but the type for y must change.
Verilog 2000 – port y as reg
module dff2y(output reg qA,
output reg qB,
output reg y,
input dA,
input dB,
input en_n,
input clk)
always@(posedge clk)
if (~en_n) begin
qA <= dA;
qB <= dB;
end
always @(qA,aB) begin
y = 1’b0;
if (a&b) begin
y = 1;
end
end
end
Verilog 2000 – port y as wire
module dff2y(output reg qA,
output reg qB,
output wire y,
input dA,
input dB,
input en_n,
input clk)
always@(posedge clk)
if (~en_n) begin
qA <= dA;
qB <= dB;
end
assign y = qA&qB;
end
y is not the output of a register though declared as reg along with qA and qB in the left module declaration It is arguable that such an internal coding implementation detail does not belong in the presentation of an “external” interface
Verilog 2000 – hiding reg while exposing port direction using an intermediate signal
module dff2y( output qA,
output qB,
output y,
input dA,
input dB,
input en_n,
input clk)
reg qA_int,qB_int,y_int;
assign qA = qB_int;
assign qB= qA_int;
assign y= y_int;
always@(posedge clk)
if (~en_n) begin
qA_int <= dA;
qB_int <= dB;
end
always @(qA,aB) begin
y_int = 1’b0;
if (a&b) begin
y_int = a&b;
end
end
end
Implicit port mapping uses the order of the ports in the definition to imply connections to local nets
Instantiation with Implicit Port Mapping
module dff2y_en( qA, qB, y, dA, dB, en, clk) output qA, qB; output y; input dA, dB; input en; input clk; wire en_n = ~en; // wire en_n; // assign en_n = ~en; dff2y dff2yInstance( qA,qB, //dff out A B y, //and of qA&qB dA,dB, //dff inputs en_n, //input clk en clk //clk ); endmodule
Explicit port map uses the port names prefixed with .
and allows reordering, no-connect, and omission of ports
module dff2y_en( qA, qB, y, dA, dB, en, clk) output qA, qB; output y; input dA, dB; input en; input clk; dff2y dff2yInstance( .dA(dA), .dB(dB), .qA(), //qA not used (no-connect) //qB omitted (no-connect) .y(y), .en_n(~en), //⭐implicit net declaration .clk(clk)); //clk endmodule
⭐ Implicit net declaration of a net holding the result ~en
. This is NOT allowed if default net declaration is disabled. i.e. using `default net_type none
Verilog Supports more than the two logic values 0/Low and 1/High. The following Metalogic Values are important to be aware of since they may be encountered unintentionally in simulation.
z
: high-impedance (not driven)
x
: undetermined, can represent the following