skip to primary navigationskip to content
 

Course pages 2025–26 (working draft)

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 XOR
  • assign 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
1
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;
1
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};
1
2

Alternatively we could have written:

wire [3:0] ab;
assign ab[3:2] = A;
assign ab[1:0] = B;
1
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]
0000 00
0001 01
0010 10
0011 11
0100 01
0101 10
0110 11
0111 11
1000 10
1001 11
1010 11
1011 11
1100 11
1101 11
1110 11
1111 11

Example implementation

wire [2:0] sum;
assign sum = A+B;
assign S = sum[2] ? 2'b11 : sum[1:0];
1
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;
1
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
1
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);
1
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;
1
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;
1
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     
1
2
3
4
5
6
7