ECAD and Architecture Practical Classes
Verilog Tutorial Part 2 - Language Features
Boolean equations
Describing logic functions as a collection of instantiated primitive modules is obviously a lot of typing and it isn't particularly easy to read. So designers started to use Boolean equations instead, e.g.:
assign Cout = (A && B) || (A && Cin) || (B && Cin); assign S = A ^ B ^ C;
Where:
&&
is logical AND||
is logical OR^
is logical XORassign
is a keyword which indicates continuous assignment used in combinational circuit (i.e. it produces logic gates which continuously evaluate the inputs and updates the outputs accordingly).
These equations can then be processed by an automatic Boolean optimiser (inside the synthesis tool) which produces the netlist of gates for us.
Arithmetic operators
Whilst Boolean equations are a good first step, we often want to undertake arithmetic so the next obvious addition is, well, the addition operator "+". If we assume that A and B are two bit values, then we might write:
assign S = A+B;
So the tricky question is, what size is S? Since A and B are both 2 bits you might expect that S would also be 2 bits. But in fact the result (in SystemVerilog) is 3 bits in size where the upper bit is the carry output (Cout). So we could recode our two-bit full-adder as:
module two_bit_full_adder( // declare inputs: input Cin, input [1:0] A, input [1:0] B, // declare outputs: output Cout, output [1:0] S); // internal 3-bit sum wire [2:0] sum; assign sum = A+B+Cin; assign Cout = sum[2]; assign S = sum[1:0]; endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Here we've used sum[2]
to select the most
significant bit of sum
to output via Cout. Then we've
used sum[1:0]
to select a bit range from bit
1 downto bit 0 which is output as S
.
The subtract operator -
is of course
defined, as are multiply *
and divide
/
and modulo %
.
However, you should remember that when you use these operators you are
actually asking for hardware to be created. Multiply is quite
complex, but as you'll find out later, FPGAs have multiplies as
embedded hardware so they are not too expensive. Divide and modulo,
on the other hand, are very expensive and is likely to be very slow.
The exception to this is if you divide by a constant which is as power of 2,
as this can easily
be converted into a shift. Moreover, shifting by a constant amount
is just wiring, no logic is required.
Numbers and Operators
Unsigned and signed numbers
By default SystemVerilog treats numbers as unsigned. Signed numbers
can be declared by adding the signed
keyword, e.g:
input signed A; wire signed [2:0] sum;
2
3
There are also special functions $signed( )
and
$unsigned( )
to
convert between the two. Note that SystemVerilog is very lax when it
comes to type checking, so mixing signed and unsigned numbers can be
done freely without causing any errors (even though it might not do
what you intended!).
Bases and bit-widths
The number base is specified using a single letter prefix:
Prefix | Meaning |
b | binary (base 2) |
d | decimal (base 10) |
h | hexdecimal (base 16) |
o | octal (base 8) |
Bit-widths are specified by a width (in decimal) and a quote mark. So, for example, a four bit number in different bases:
4'b1010 == 4'd10 == 4'ha
Also, negative numbers are stored in twos complement, so, for example:
-8'd3 == 8'b1111_1101 == 8'hfd
Note that in the above example "_
" is used to partition the
binary number into two 4-bit quantities to make it easier to read and
is ignored by the synthesis system.
Bitwise vs. logical operators
Bitwise and logical operators are similar to other languages, e.g. Java:
Operator type | Symbols | Operation |
Bitwise | ~ | Bitwise NOT |
& | Bitwise AND | |
| | Bitwise OR | |
^ | Bitwise XOR | |
Logical | ! | NOT |
&& | AND | |
|| | OR | |
== | Logical equality | |
!= | Logical inequality |
Bit concatenation
We can concatenate bits together using braces { }
. For example, we
could declare a four-bit wire to hold the two two-bit inputs A and B:
wire [3:0] ab; assign ab = {A,B};
2
Alternatively we could have written:
wire [3:0] ab; assign ab[3:2] = A; assign ab[1:0] = B;
2
3
Parameters
SystemVerilog provides
the parameter
keyword
that is used in module declarations to allow parameterisation
of the module so that modules can be made more generic. For
example, a FIFO implementation could have parameters specifying
the FIFO depth and bitwidth.
parameter datawidth = 32; wire [datawidth-1:0] data;
The other way is to use compiler directives`define
to define a text macro, for example:
`define height 1024 `define Path "/home/somefile" `include `Path assign i = `height;
SystemVerilog adds another mechanism to declare any variable as a
constant, using the const
keyword, but we treat this as an
advanced topic which is not covered here.
High Level Language Constructs
Conditional operator
SystemVerilog, like Java and other languages, uses ? :
as a ternary
conditional operator of the form:
condition ? equation_if_condition_is_true : equation_if_condition_is_false
We could use this to describe a two-bit saturation adder,
remembering that if the third bit of the sum (i.e. sum[2]
) is set
then the sum must have rolled over and we would like to saturate instead.
Truth table for saturation-add
Inputs | Outputs | ||
A[1:0] | B[1:0] | S[1:0] | |
00 | 00 | 00 | |
00 | 01 | 01 | |
00 | 10 | 10 | |
00 | 11 | 11 | |
01 | 00 | 01 | |
01 | 01 | 10 | |
01 | 10 | 11 | |
01 | 11 | 11 | |
10 | 00 | 10 | |
10 | 01 | 11 | |
10 | 10 | 11 | |
10 | 11 | 11 | |
11 | 00 | 11 | |
11 | 01 | 11 | |
11 | 10 | 11 | |
11 | 11 | 11 |
Example implementation
wire [2:0] sum; assign sum = A+B; assign S = sum[2] ? 2'b11 : sum[1:0];
2
3
Note that here I've used sum[2]
rather than
sum[2]==1
, i.e. you can use a single bit as a Boolean.
Moreover, for a multi-bit number, zero is treated as False and any
non-zero number as True.
Encoding truth tables directly
Alternatively we can encode the truth table directly using nested
conditional operators. To encode saturation add, let's collect all
inputs together and then enumerate every input condition of interest
with its associated output. The last term is the default case. Here
we chose output 2'b11
as the default since it appears most
often in the table (i.e. we're trying to keep the description short).
wire [3:0] ab = {A,B}; assign S = (ab==4'b0000) ? 2'b00: (ab==4'b0001) ? 2'b01: (ab==4'b0010) ? 2'b10: // default case to output 2'b11 covers (ab==4'b0011) (ab==4'b0100) ? 2'b01: (ab==4'b0101) ? 2'b10: // default case to output 2'b11 covers (ab==4'b0110) and (ab==4'b0111) (ab==4'b1000) ? 2'b10: // default case for the rest of the table 2'b11;
2
3
4
5
6
7
8
9
10
11
Functions
Functions are provided in SystemVerilog.
A function can take a number of inputs but have only one output.
They are restricted to operations which semantically happen in zero time, with the result that
typically they are used to describe combinational logic.
Since they
are evaluated at compile time, they can be recursive, something that
would not be possible in hardware (unless you invent some magic
hardware which physically manufactures hardware as you recurse!).
Currently the automatic
keyword has to be used for reentrant
functions.
function automatic [15:0] factorial; input [2:0] n; if(n <=1) factorial = 1; else factorial = n * factorial(n-1); endfunction
2
3
4
5
6
7
Using this we might create a small table containing factorials where each factorial function is evaluated at compile/synthesis time and the result is a constant. This might be implemented as a ROM.
// version using the factorial function // call which executes at synthesis time wire [15:0] result = (n==3'd0) ? factorial(3'd1) : (n==3'd1) ? factorial(3'd1) : (n==3'd2) ? factorial(3'd2) : (n==3'd3) ? factorial(3'd3) : (n==3'd4) ? factorial(3'd4) : (n==3'd5) ? factorial(3'd5) : (n==3'd6) ? factorial(3'd6) : factorial(3'd7);
2
3
4
5
6
7
8
9
10
11
// equivalent version where the factorials // have been determined manually wire [15:0] result = (n==3'd0) ? 1 : (n==3'd1) ? 1 : (n==3'd2) ? 2 : (n==3'd3) ? 6 : (n==3'd4) ? 24 : (n==3'd5) ? 120 : (n==3'd6) ? 720 : 5040;
2
3
4
5
6
7
8
9
10
11
Enumeration
Enumerated types provide a means to declare an abstract variable that can have a specific list of valid values. Each value is identified with a user-defined name, or label. For example, the variable RGB can have the values of red, green and blue:
typedef enum {red, green, blue} RGB;
Enumerated types are variables or nets with a set of labeled values. The default base type for enumerated type is int, which is a 32-bit signed integer.
In order to represent hardware at a more detailed level, SystemVerilog
allows an explicit base type for the enumerated types to be declared.
Here we use the bit
type, i.e. a binary representation.
Example:
//enumerated type which is 1-bit wide, //2-state base type typedef enum bit {TRUE, FALSE} MyBoolean; //enumerated type which is 2-bits wide, //4-state base type typedef enum bit [1:0] {WAIT, LOAD, READY} state;
2
3
4
5
6
7
Structure
Design data often has logical groups of signals, for example all the control signals for a bus protocol. Structure is a convenient mechanism for collecting common signals into a group.
A structure is declared using the struct
keyword, structure
members can be any variable type, including user-defined types and any
constant type, for example:
//a MIPS R type instruction word typedef struct { bit [5:0] opcode; bit [4:0] rs, rt, rd; bit [4:0] shamt; bit [5:0] fn; } Instruction_Word
2
3
4
5
6
7