Combinational circuits#
Learning goals#
Apply basic features of the language to describe combinational logic
Introductory problem#
Implement a circuit with the following requirements:
The first output
two_are_high
must be high if exactly two of the inputsi{0..3}
are high.The second output
three_are_low
must be low if exactly three of the inputsi{4..7}
are low.
First test the circuit using a testbench. Then transfer the circuit to the board using:
Slide switches for inputs
An LED for each output
Tasks#
Read sections 3 and 4 of SV Guide.
Quiz#
Mini-lecture#
What is the typical difference of SystemVerilog to traditional programming languages?
What is the difference between Verilog and SystemVerilog?
Different abstraction levels#
gate-level
register-transfer-level (RTL)
structural
behavioral
What does the synthesis step involve in an FPGA development tool?
What does the implementation step involve in an FPGA development tool?
Generally we should not use gate level modeling in abstract modeling. What is the purpose of this low-level of abstraction then?
Data types and data objects#
Definitions:
A set of values and a set of operations that can be performed on these values.
Note that a data type is not only defined by possible values but also the operations that we can apply on them. Data types are used (1) to declare data objects or (2) other data types.
A named entity that is associated with a data value and type, e.g., a parameter, variable or net.
So a data object has always have a data type.
x
unknown logicz
high impedance0
logic zero or false condition1
logic one or true condition
SV allows only these four states of values. The name of the basic 4-state data type is logic
.
logic
can be used to construct other data types. There are data types which are not 4-state, e.g., 2-state values, e.g., int
, bit
.
Integer data types#
2-state:
shortint
,int
,longint
: 16, 32, 64 bit signed integer, respectivelybyte
: 8 bit signed integer or ASCII characterbit
: user-defined vector size, unsigned
4-state:
logic
,reg
: user-defined vector size, unsignedinteger
, 32-bit signed integertime
, 64-bit signed integer
We can still make a logic
array signed by augmenting with the keyword signed
: logic signed[15:0]
.
For hardware signals we should use 4-state types, because this aids the designer to debug initialization problems. 2-state variables are useful for:
simulation code
For declaring loop variables which are only for describing repetitive logic
Constants
It would be more convenient to use 4-state logic only, however 2-state values require less memory in a typical simulator, so this may be the reason why they exist in the language.
The SV simulator Verilator uses mostly two-states for speed.
When you search for SV code on the web, you will very often find people using reg
for signals. You should prefer using logic
which was introduced 2005 to the language:
reg
vs logic
reg
and logic
denote the same type, however logic
is a more descriptive term because reg
does not always describe a register and thus does not accurately describe the writer’s intent.
Arrays#
Indexing and slicing of arrays#
Probably you are used to the slicing concept from other languages, e.g., arr[1:4]
. SystemVerilog allows slicing relative to an anchor.
An expression can select part of a packed array, or any integer type, which is assumed to be numbered down to 0.
The term part-select refers to a selection of one or more contiguous bits of a single-dimension packed array.
logic [63:0] data;
logic [7:0] byte2;
byte2 = data[23:16]; // an 8-bit part-select from data
The term slice refers to a selection of one or more contiguous elements of an array.
…
The size of the part-select or slice shall be constant, but the position can be variable.
int i = bitvec[j +: k]; // k is a constant
int a[x:y], b[y:z], e;
a = {b[c -: d], e}; // d is a constant
[j +: k]
is a constant width (of k
) slice beginning at j
. This construct can be used whenever we want to select a fixed width according to a dynamic signal, e.g., an address:
module m;
byte bit_array = 'b11_00_10_10;
int address;
logic [1:0] current_bit_pair;
assign current_bit_pair = bit_array[2*address+:2];
initial begin
for (address = 0; address < 4; ++address) #1 $displayb(current_bit_pair);
$finish;
end
endmodule
🟩 Simulation start
10
10
00
11
- m.sv:9: Verilog $finish
Modules#
The logic
data type#
logic
data type is 4-state. What does this mean?
What do the values
X
andZ
mean?
Why should we not use int
to model signals?
What is the difference between wire
and var
(variable)?
All port signals are wires as default. So we need to augment an output port with logic
if we want to use this port in a procedural block, e.g.:
module m(
input a, b, enable,
output logic c // without logic it is an error
);
always_comb begin
c = a & b;
if (!enable)
c = 0;
end
endmodule
Try it yourself.
Is logic
a net or variable?
Why is it sufficient to use the data type logic
in FPGA design?
Module instantiation#
In the example we see that and
, or
and not
are instantiated even they were not defined before. How is this possible?
How can we connect the ports of a module during instantiation? Show some of them and explain how they work.
Literals#
Integer literals
most basic form:
10
. This is a 32 bit signed decimal.providing the size:
2'10
providing the base:
2'b10
, (h
,o
,d
also exist)signedness:
2'sb10
More details are in SV2017-5.7.1.
Filling an array with the same value: x = '1
All the bits of x
are assigned to 1
.
Parameters#
param
is a compile-time module constant. It is typically used to design modules that support variable input and output sizes.param
can be provided when a module is instantiated.localparam
is a compile-time constant. It is used for constants used in the design.
RT-level combinational modeling#
Continuous assignments#
assign a = b;
Conditional operator#
Ternary operator:
output = condition ? output_if_true : output_if_false;
How can we describe a 2-to-1 multiplexer (two data inputs a
, b
, select signal sel
, output signal o
using a continuous assignment?
Bitwise operators#
&
^
|
~
How can we shorten the following code?
assign
x[0] = ~y[0],
x[1] = ~y[1],
x[2] = ~y[2];
Logical operators#
Write code for the following as continuous assignment: x
must be assigned 2
if the conditions a
and b
are true, and 3
otherwise.
Reduction operators#
You have five two-way switches placed on different walls of your room that can control your room light. Every switch can toggle the light. Write code using a reduction operator that models this behavior. The switches are modeled by the signal logic [4:0] switches
and the light as logic light
.
Arithmetic operators#
Shift operators#
<<
: logical<<<
: arithmetic
Difference between a logical and arithmetic shift
The arithmetic shift takes the sign of the number into account by extending the sign bit (the leftmost bit in a signed number). For example if 4 bit -2
= 1110
is logically shifted to the right, then we get 0111
= 7
. In case of arithmetic right shift, which is the same as division by two: we get: 1111
= -1
.
In arithmetic shift, the sign is extended — so called sign extension. For example we extend the sign bit of -1
= 1110
, which is 1
, and get 1111
, when we shift to the right. Without a sign-extension we would have 0111
.
Comparison operators#
Concatenate and replicate operators#
concatenate:
{a, b, c}
replication:
{3{x, z}}
=> {x, z, x, z, x, z}
You have the signals a
, b
, c
. Create the concatenation of signals aabcabcabcb
and assign to f
.
Loop statements#
for
forever
repeat
while
do ... while
foreach
Always block for combinational design#
always
can be used both for combinational and sequential designalways_comb
only for combinationalalways_ff
only for sequential
But what is the advantage of using an always_comb
statement over a simple always
or a continuous assignment?
always
statement typically needs a sensitivity list.always_comb
has an implicit sensitivity list for all signals that are read in the block.always @*
is an alternative and executes the block whenever a read signal inside the block changes. Howeveralways_comb
has many advantages overalways @*
. For examplealways @*
may not be executed in the beginning of a simulation which may lead to errors if some signals are initialized with constants. Constants are not signals, so they won’t trigger thealways
block. Another advantage is that the variables on the left-hand side of the assignments may not be written by otheralways
blocks which may lead to errors. For other differences refer to LRM 2017-9.2.2.2.2.Advantage over continuous assignment: We can use procedural code like if/else statements which are more general than the ternary operator in continuous assignments.
Blocking assignment#
a = b
: blockinga <= b
: non-blocking
Generally we use blocking assignments for combinational circuit design and non-blocking assignments for sequential circuits.
When we describe combinational logic procedurally, we typically want to break a long chain into smaller pieces with temporary variables, where every piece may be described using a single assignment. In this case we want a behavior like in C
, every statement should be executed immediately.
When we describe sequential logic, we typically want to assign a signal value a
to another signal value b
if an event happens. Even we can use blocking assignments for basic code, using them can lead to problems like this: https://i.stack.imgur.com/rrgHa.png
If statements#
Case statements#
Note that a multiplexer does not always resemble an if-else or switch statement from general programming languages.
A compiler infers a multiplexer only in the following case:
For every case an output
y
is assigned to an input signal.All conditions are mutually exclusive, in other words, two cases cannot be true at the same time.
Unique case#
The intent of unique case
is a multiplexer, where an if-else statement actually describes a priority encoder
Case statement is like if/else and can lead to a priority encoder if the compiler misses the conditions for a multiplexer. unique case
will try to a multiplexer (and will probably warn if not possible), e.g., even not all cases are complete. For example:
logic [1:0] sel;
always_comb
unique case (sel)
'b00: f = a;
'b01: f = b;
endcase
For other cases of sel
, f
can be assigned to any value.
Assertions#
Immediate assertions#
Deferred assertions#
assert #0 (expression) action_block
assert final (expression) action_block
As with all immediate assertions, a deferred assertion’s expression is evaluated at the time the deferred assertion statement is processed. However, in order to facilitate glitch avoidance, the reporting or action blocks are scheduled at a later point in the current time step.
In the following example we see an assignment from i
to o
. First browse briefly the code. The discussion follows after the code.
module tb;
logic i, o;
assign o = i;
initial begin
i = 0;
#1 $display("After #1 delay:");
i = 1;
$display("%d: o: %d", $time, o);
$strobe("%dstrobe: o: %d", $time, o);
assert (o == 1)
else $display("Immediate assert failed");
assert final (o == 1)
else $display("Final deferred immediate assert failed");
assert #0 (o == 1)
else $display("Observed deferred immediate assert failed");
#0 $display("After zero delay:");
$display("%d: o: %d", $time, o);
$strobe("%dstrobe: o: %d", $time, o);
assert (o == 1)
else $display("Immediate assert failed");
assert final (o == 1)
else $display("Final deferred immediate assert failed");
assert #0 (o == 1)
else $display("Observed deferred immediate assert failed");
#1 $display("After #1 delay:");
$display("%d: o: %d", $time, o);
assert (o == 1);
$finish;
end
endmodule
Due to the sequential nature of the simulation, the new value of i
requires time to be visible at o
even we are in the same cycle. assert
could be scheduled before o
gets updated, so we may assert
an intermediate value of o
. The solutions are:
Wait
#0
or more before checkingo
.A deferred assertion as above.
For example the output can be:
After #1
1: o: 0
Immediate assert failed
After zero delay
1: o: 1
1strobe: o: 1
1strobe: o: 1
After #1
2: o: 1
** Note: $finish : testbench.sv(32)
$strobe
acts as a deferred $display
call. We see that all #0
, assert #0
and assert final
solve the issue.
Warning
Currently, Verilator does not support deferred assertion. So the above output is from Questa on EDAplayground.
As a workaround the following macro can be used which I included in my util.sv
:
`ifdef VERILATOR
// _verilator does not support assert final
// https://github.com/verilator/verilator/issues/5081
// Use #1 delay instead.
`define ASSERT_FINAL(arg) #1 assert (arg)
`else
`define ASSERT_FINAL(arg) assert final (arg)
`endif
This macro can be used as follows:
din = test_word;
din_addr = 1;
wen = 1;
// Dout should *not* be available in this clock cycle.
`ASSERT_FINAL(test_word != dout);
@(negedge clk);
There is a slight difference between the observed and final assertions. They will be scheduled in the reactive and postponed region of the simulation, respectively.
The value that assert #0
sees may still not be the latest value, because:
Note that if code in the Reactive region modifies signals and causes another pass to the Active region to occur, this still may create glitching behavior in observed deferred assertions, as the new passage in the Active region may re-execute some of the deferred assertions with different reported results. In general, observed deferred assertions prevent glitches due to order of procedural execution, but do not prevent glitches caused by execution loops between regions that the assignments from the Reactive region may cause.
However assert final
cannot glitch because this statement is processed in the last and non-iterative step of a simulation cycle.
… Due to their execution in the non-iterative Postponed region, final deferred assertions are not vulnerable to the potential glitch behavior previously described for observed deferred assertions.
Currently I don’t have an example where assert #0
would be more beneficial than assert final
, so I would recommend using the final deferred assertion assert final
in cases where an assertion must wait the checked signal to settle.
Solution for the introductory problem#
module m #(
localparam int NInp = 8
) (
input [NInp-1:0] i,
output two_are_high,
three_are_low
);
logic [NInp/2-1:0] lower_slice, higher_slice;
assign lower_slice = i[3:0], higher_slice = i[7:4];
assign two_are_high =
lower_slice == 4'b1100 |
lower_slice == 4'b0110 |
lower_slice == 4'b0011 |
lower_slice == 4'b1001 |
lower_slice == 4'b0101 |
lower_slice == 4'b1010
? 1 : 0;
assign three_are_low =
higher_slice == 4'b1000 |
higher_slice == 4'b0100 |
higher_slice == 4'b0010 |
higher_slice == 4'b0001
? 1 : 0;
endmodule
module tb;
localparam int NInp = 8;
logic [NInp-1:0] i;
logic [NInp/2-1:0] lower_slice, higher_slice;
logic two_are_high, three_are_low;
m m_i (.*);
assign i = {higher_slice, lower_slice};
initial begin
$dumpfile("signals.fst");
$dumpvars();
repeat (2 ** $size(
lower_slice
)) begin
#1++lower_slice;
end
repeat (2 ** $size(
higher_slice
)) begin
#1++higher_slice;
end
$finish;
end
endmodule
Homework#
Design a calculator that supports addition and multiplication on unsigned integers. Use the following interface:
module calculator_simple #(
int unsigned WIDTH_IN = 4
) (
input multiply_instead_of_add,
input [WIDTH_IN-1 : 0] i1,
i2,
output [2*WIDTH_IN-1 : 0] o
);
Implementation on the board:
Use an input width of 4.
Use the left-most 8 LEDs and slide switches for the two 4 bit inputs.
Use the right-most 8 LEDs as the output.
Use the right-most slide switch for
multiply_instead_of_add
.