Hardware behavior cannot be implemented without conditional statements and other ways to control the flow of logic. Verilog has a set of control flow blocks and mechanisms to achieve the same.

if-else-if

This conditional statement is used to make a decision about whether certain statements should be executed or not. This is very similar to the if-else-if statements in C. If the expression evaluates to true, then the first statement will be executed. If the expression evaluates to false and if an else part exists, the else part will be executed.

Placing values onto nets and variables are called assignments. There are three basic forms:

Legal LHS values

An assignment has two parts - right-hand side (RHS) and left-hand side (LHS) with an equal symbol (=) or a less than-equal symbol (<=) in between.

Assignment type Left-hand side
Procedural
  • Variables (vector/scalar)
  • Bit-select or part-select of a vector reg, integer or time variable
  • Memory word
  • Concatenation of any of the above
Continuous
  • Net (vector/scalar)
  • Bit-select or part-select of a vector net
  • Concatenation of bit-selects and part-selects
Procedural Continous
  • Net or variable (vector/scalar)
  • Bit-select or part-select of a vector net

The RHS can contain any expression that evaluates to a final value while the LHS indicates a net or a variable to which the value in RHS is being assigned.

  
  
module tb;
	reg clk;
	wire a, b, c, d, e, f;
	reg  z, y;
	
	// clk is on the LHS and the not of clk forms RHS
	always #10 clk = ~clk;
	
	// y is the LHS and the constant 1 is RHS
	assign y = 1; 
	
	// f is the LHS, and the expression of a,b,d,e forms the RHS
	assign f = (a | b) ^ (d & e);

	always @ (posedge clk) begin
		// z is the LHS, and the expression of a,b,c,d forms the RHS
		z <= a + b + c + d;
	end
	
	initial begin
		// Variable names on the left form LHS while 0 is RHS
		a <= 0; b <= 0; c <= 0; d <= 0; e <= 0;
		clk <= 0;
	end
endmodule

  

Procedural Assignment

Procedural assignments occur within procedures such as always, initial, task and functions and are used to place values onto variables. The variable will hold the value until the next assignment to the same variable.

The value will be placed onto the variable when the simulation executes this statement at some point during simulation time. This can be controlled and modified the way we want by the use of control flow statements such as if-else-if, case statement and looping mechanisms.

  
  
	reg [7:0]  data;
	integer    count;
	real       period;
	
	initial begin
		data = 8'h3e;
		period = 4.23;
		count = 0;
	end
	
	always @ (posedge clk) 
		count++;

  

Variable declaration assignment

An initial value can be placed onto a variable at the time of its declaration as shown next. The assignment does not have a duration and holds the value until the next assignment to the same variable happens. Note that variable declaration assignments to an array are not allowed.

  
  
	module my_block;
		reg [31:0] data = 32'hdead_cafe;
	
		initial begin
			#20  data = 32'h1234_5678;    // data will have dead_cafe from time 0 to time 20
			                              // At time 20, data will get 12345678
		end
	endmodule

  
  
  
	reg [3:0] a = 4'b4;
	
	// is equivalent to 
	
	reg [3:0] a;
	initial a = 4'b4;

  

If the variable is initialized during declaration and at time 0 in an initial block as shown below, the order of evaluation is not guaranteed, and hence can have either 8'h05 or 8'hee.

  
  
	module my_block;
		reg [7:0]  addr = 8'h05;
		
		initial 
			addr = 8'hee;
	endmodule

  
  
  
	reg [3:0] array [3:0] = 0;           // illegal
	integer i = 0, j;                    // declares two integers i,j and i is assigned 0
	real r2 = 4.5, r3 = 8;               // declares two real numbers r2,r3 and are assigned 4.5, 8 resp.
	time startTime = 40;                 // declares time variable with initial value 40

  

Procedural blocks and assignments will be covered in more detail in a later section.

Continuous Assignment

Click here for a step-by-step simulation example !

This is used to assign values onto scalar and vector nets and happens whenever there is a change in the RHS. It provides a way to model combinational logic without specifying an interconnection of gates and makes it easier to drive the net with logical expressions.

  
  
    // Example model of an AND gate
	wire  a, b, c;
	
	assign a = b & c;

  

Whenever b or c changes its value, then the whole expression in RHS will be evaluated and a will be updated with the new value.

Net declaration assignment

This allows us to place a continuous assignment on the same statement that declares the net. Note that because a net can be declared only once, only one declaration assignment is possible for a net.

  
  
	wire  penable = 1;

  

Procedural Continuous Assignment

These are procedural statements that allow expressions to be continuously assigned to nets or variables and are of two types.

  • assign ... deassign
  • force ... release

assign deassign

This will override all procedural assignments to a variable and is deactivated by using the same signal with deassign. The value of the variable will remain same until the variable gets a new value through a procedural or procedural continuous assignment. The LHS of an assign statement cannot be a bit-select, part-select or an array reference but can be a variable or a concatenation of variables.

  
  
	reg q;
	
	initial begin
		assign q = 0;
		#10 deassign q;
	end

  

force release

These are similar to the assign - deassign statements but can also be applied to nets and variables. The LHS can be a bit-select of a net, part-select of a net, variable or a net but cannot be the reference to an array and bit/part select of a variable. The force statment will override all other assignments made to the variable until it is released using the release keyword.

  
  
	reg o, a, b;
	
	initial begin
		force o = a & b;
		...
		release o;
	end

  

Parameters are Verilog constructs that allow a module to be reused with a different specification. For example, a 4-bit adder can be parameterized to accept a value for the number of bits and new parameter values can be passed in during module instantiation. So, an N-bit adder can become a 4-bit, 8-bit or 16-bit adder. They are like arguments to a function that are passed in during a function call.

  
  
	parameter MSB = 7;                  // MSB is a parameter with a constant value 7
	parameter REAL = 4.5;               // REAL holds a real number
	
	parameter FIFO_DEPTH = 256, 
	          MAX_WIDTH = 32;           // Declares two parameters
	          
	parameter [7:0] f_const = 2'b3;     // 2 bit value is converted to 8 bits; 8'b3

  

Parameters are basically constants and hence it's illegal to modify their value at runtime. It is illegal to redeclare a name that is already used by a net, variable or another parameter.

There are two major types of parameters, module and specify and both accepts a range specification. But, they are normally made as wide as the value to be stored requires them to be and hence a range specification is not necessary.

Module parameters

Module parameters can be used to override parameter definitions within a module and this makes the module have a different set of parameters at compile time. A parameter can be modified with the defparam statement or in the module instance statement. It is a common practice to use uppercase letters in names for the parameter to make them instantly noticeable.

The module shown below uses parameters to specify the bus width, data width and the depth of FIFO within the design, and can be overriden with new values when the module is instantiated or by using defparam statements.

  
  
	// Verilog 1995 style port declaration
	module design_ip  ( addr,  
	                    wdata,
	                    write,
	                    sel,
	                    rdata);
	                    
	     parameter  BUS_WIDTH    = 32, 
	                DATA_WIDTH   = 64,
	                FIFO_DEPTH   = 512;
	                
	     input addr;
	     input wdata;
	     input write;
	     input sel;
	     output rdata;
	     
	     wire [BUS_WIDTH-1:0] addr;
	     wire [DATA_WIDTH-1:0] wdata;
	     reg  [DATA_WIDTH-1:0] rdata;
	     
	     reg [7:0] fifo [FIFO_DEPTH];
	                
	     // Design code goes here ...
	endmodule

  

In the new ANSI style of Verilog port declaration, you may declare parameters as show below.

  
  
module design_ip 
	#(parameter BUS_WIDTH=32, 
		parameter DATA_WIDTH=64) (	
		
		input [BUS_WIDTH-1:0] addr,
   	// Other port declarations
   );

  

Overriding parameters

Parameters can be overridden with new values during module instantiation. The first part instantiates the module called design_ip by the name d0 where new parameters are passed in within #( ). The second part uses a Verilog construct called defparam to set the new parameter values. The first method is the most commonly used way to pass new parameters in RTL designs. The second method is commonly used in testbench simulations to quickly update the design parameters without having to reinstantiate the module.

  
  
module tb;
	
	  // Module instantiation override
		design_ip  #(BUS_WIDTH = 64, DATA_WIDTH = 128) d0 ( [port list]);
		
		// Use of defparam to override
		defparam d0.FIFO_DEPTH = 128;
		
endmodule

  

Example

The module counter has two parameters N and DOWN declared to have a default value of 2 and 0 respectively. N controls the number of bits in the output effectively controlling the width of the counter. By default it is a 2-bit counter. Parameter DOWN controls whether the counter should increment or decrement. By default, the counter will decrement because the parameter is set to 0.

2-bit up-counter

  
  
module counter
  #( 	parameter N = 2,
   		parameter DOWN = 0)
   		
  ( input 							clk,
    input 							rstn,
    input 							en,
   	output 	reg [N-1:0] out);
  
  always @ (posedge clk) begin
    if (!rstn) begin
      out <= 0;
    end else begin
      if (en)
        if (DOWN)
          out <= out - 1;
        else
          	out <= out + 1;
      else
         out <= out;     
    end
  end
endmodule

  

The module counter is instantiated with N as 2 even though it is not required because the default value is anyway 2. DOWN is not passed in during module instantiation and hence takes the default value of 0 making it an up-counter.

  
  
module design_top (    input clk,
                input rstn,
                input en,
                output [1:0] out);

    counter #(.N(2)) u0 (	.clk(clk),
                          .rstn(rstn),
                          .en(en));
endmodule

  

See that default parameters are used to implement the counter where N equals two making it a 2-bit counter and DOWN equals zero making it an up-counter. The output from counter is left unconnected at the top level.

4-bit down-counter

In this case, the module counter is instantiated with N as 4 making it a 4-bit counter. DOWN is passed a value of 1 during module instantiation and hence an down-counter is implemented.

  
  
module design_top (    input clk,
                input rstn,
                input en,
                output [3:0] out);

    counter #(.N(4), .DOWN(1)) 
    		u1 (	.clk(clk),
              .rstn(rstn),
              .en(en));
endmodule

  

Specify parameters

These are primarily used for providing timing and delay values and are declared using the specparam keyword. It is allowed to be used both within the specify block and the main module body.

  
  
	// Use of specify block
	specify
		specparam  t_rise = 200, t_fall = 150;
		specparam  clk_to_q = 70, d_to_q = 100;
	endspecify
	
	// Within main module
	module  my_block ( ... );
	 	specparam  dhold = 2.0;
	 	specparam  ddly  = 1.5;
	 	
	 	parameter  WIDTH = 32;
	endmodule

  

Difference between specify and module parameters

Specify parameter Module parameter
Declared by specparam Declared by parameter
Can be declared inside specify block or within main module Can only be declared within the main module
May be assigned specparams and parameters May not be assigned specparams
SDF can be used to override values Instance declaration parameter values or defparam can be used to override

An array declaration of a net or variable can be either scalar or vector. Any number of dimensions can be created by specifying an address range after the identifier name and is called a multi-dimensional array. Arrays are allowed in Verilog for reg, wire, integer and real data types.

  
  
	reg        y1 [11:0];        // y is an scalar reg array of depth=12, each 1-bit wide
	wire [0:7] y2 [3:0]          // y is an 8-bit vector net with a depth of 4
	reg  [7:0] y3 [0:1][0:3];    // y is a 2D array rows=2,cols=4 each 8-bit wide

  

An index for every dimension has to be specified to access a particular element of an array and can be an expression of other variables. An array can be formed for any of the different data-types supported in Verilog.

Note that a memory of n 1-bit reg is not the same as an n-bit vector reg.

Assignment

  
  
	y1 = 0; 						// Illegal - All elements can't be assigned in a single go
	
	y2[0] = 8'ha2; 			// Assign 0xa2 to index=0 
	y2[2] = 8'h1c; 			// Assign 0x1c to index=2
	y3[1][2] = 8'hdd; 	// Assign 0xdd to rows=1 cols=2
	y3[0][0] = 8'haa; 	// Assign 0xaa to rows=0 cols=0

  

Example

The code shown below simply shows how different arrays can be modeled, assigned and accessed. mem1 is an 8-bit vector, mem2 is an 8-bit array with a depth of 4 (specified by the range [0:3]) and mem3 is a 16-bit vector 2D array with 4 rows and 2 columns. These variables are assigned different values and printed.

  
  
module des ();
  reg [7:0]  mem1; 							// reg vector 8-bit wide
  reg [7:0]  mem2 [0:3]; 				// 8-bit wide vector array with depth=4
  reg [15:0] mem3 [0:3][0:1]; 	// 16-bit wide vector 2D array with rows=4,cols=2
  
  initial begin
    int i;
    
    mem1 = 8'ha9;
    $display ("mem1 = 0x%0h", mem1);
    
    mem2[0] = 8'haa;
    mem2[1] = 8'hbb;
    mem2[2] = 8'hcc;
    mem2[3] = 8'hdd;
    for(i = 0; i < 4; i = i+1) begin
      $display("mem2[%0d] = 0x%0h", i, mem2[i]);
    end
    
    for(int i = 0; i < 4; i += 1) begin
      for(int j = 0; j < 2; j += 1) begin
        mem3[i][j] = i + j;
        $display("mem3[%0d][%0d] = 0x%0h", i, j, mem3[i][j]);
      end
    end
  end
endmodule

  
Simulation Log
ncsim> run
mem1 = 0xa9
mem2[0] = 0xaa
mem2[1] = 0xbb
mem2[2] = 0xcc
mem2[3] = 0xdd
mem3[0][0] = 0x0
mem3[0][1] = 0x1
mem3[1][0] = 0x1
mem3[1][1] = 0x2
mem3[2][0] = 0x2
mem3[2][1] = 0x3
mem3[3][0] = 0x3
mem3[3][1] = 0x4
ncsim: *W,RNQUIE: Simulation is complete.

Memories

Memories are digital storage elements that help store a data and information in digital circuits. RAMs and ROMs are good examples of such memory elements. Storage elements can be modeled using one-dimensional arrays of type reg and is called a memory. Each element in the memory may represent a word and is referenced using a single array index.

memory array in verilog

Register Vector

Verilog vectors are declared using a size range on the left side of the variable name and these get realized into flops that match the size of the variable. In the code shown below, the design module accepts clock, reset and some control signals to read and write into the block.

It contains a 16-bit storage element called register which simply gets updated during writes and returns the current value during reads. The register is written when sel and wr are high on the same clock edge. It returns the current data when sel is high and wr is low.

  
  
module des (    input           clk,
                input           rstn,
                input           wr,
                input           sel,
                input [15:0]    wdata,
                output [15:0]   rdata);

	reg [15:0] register;

	always @ (posedge clk) begin
    if (!rstn)
    	register <= 0;
    else begin
    	if (sel & wr) 
      	register <= wdata;
    	else
      	register <= register;
    end
	end

	assign rdata = (sel & ~wr) ? register : 0;
endmodule

  

The hardware schematic shows that a 16-bit flop is updated when control logic for writes are active and the current value is returned when control logic is configured for reads.

Array

In this example, register is an array that has four locations with each having a width of 16-bits. The design module accepts an additional input signal which is called addr to access a particular index in the array.

  
  
module des (    input           clk,
                input           rstn,
                input  [1:0]    addr,
                input           wr,
                input           sel,
                input [15:0]    wdata,
                output [15:0]   rdata);

reg [15:0] register [0:3];
integer i;

always @ (posedge clk) begin
    if (!rstn) begin
        for (i = 0; i < 4; i = i+1) begin 
            register[i] <= 0;
        end
    end else begin
        if (sel & wr) 
            register[addr] <= wdata;
        else
            register[addr] <= register[addr];
    end
end
 
assign rdata = (sel & ~wr) ? register[addr] : 0;
endmodule

  

It can be seen in the hardware schematic that each index of the array is a 16-bit flop and the input address is used to access a particular set of flops.

The primary intent of data-types in the Verilog language is to represent data storage elements like bits in a flip-flop and transmission elements like wires that connect between logic gates and sequential structures.

What values do variables hold ?

Almost all data-types can only have one of the four different values as given below except for real and event data types.

0 represents a logic zero, or a false condition
1 represents a logic one, or a true condition
x represents an unknown logic value (can be zero or one)
z represents a high-impedance state

The following image shows how these values are represented in timing diagrams and simulation waveforms. Most simulators use this convention where red stands for X and orange in the middle stands for high-impedance or Z.

verilog value system