In the previous chapter we reviewed some of the main SystemVerilog operators that allow us to describe the operation of combinational logic circuits. In this chapter we will review some of the SystemVerilog constructs that allow us to describe parts of a combinational circuit: always blocks, routing structures, and conditional control constructs.
Table of Contents
-
- Always Block for a Combinational Circuit
- Procedural Assignment
- Examples of always Block
- Coding Guidelines
- If Statement
- Case Statement
- Case Statement With Do-Not-Cares
- Full Case and Parallel Case
- Routing Structures of Conditional Control Constructs
- Parameter and Constant
- Replicated Structure
- Reduced-xor circuit
- Source Files
- Conclusion
- SystemVerilog Study Notes Chapters
Chapters:
- Gate-Level Combinational Circuit
- RTL Combinational Circuit Operators
- RTL Combinational Circuit - Concurrent and Control Constructs
- Hex-Digit to Seven-Segment LED Decoder RTL Combinational Circuit
- Barrel Shifter RTL Combinational Circuit
- Simplified Floating Point Arithmetic. RTL Combinational Circuit
- BCD Number Format. RTL Combinational Circuit
- DDFS. Direct Digital Frequency Synthesis for Sound
- FPGA ADSR envelope generator for sound synthesis
- AMD Xilinx 7 series FPGAs XADC
- Building FPGA-Based Music Instrument Synthesis: A Simple Test Bench Solution
Always Block for a Combinational Circuit
The HDL body can be considered as a collection of concurrent constructs.
An always block is a "concurrent construct". It encapsulate procedural statements which are executed sequentially.
An always block can be used to infer a combinational circuit or a register.
Simplified syntax of an always block:
always @ ([sensitivity_list])
begin: [optional name]
[optional local variable declaration];
[procedural statement];
[procedural statement];
...
end
[sensitivity_list] : list of signals and events to which the always block responds. For a combinational circuit all the input signals should be included. It is a timing control construct.
The body is composed of any number of procedural statements.
An always block runs autonomously and continuously. When any signal of the sensitivity list changes (event) it is activated and executes the internal procedural statements sequentially. The always block "loops forever" and the initiation of each loop is controlled by the sensitivity list.
SystemVerilog introduces three variations of the always block:
- always_comb: to infer a combinational circuit
- always_ff: to infer a flip-flop or a register
- always_latch: to infer a latch
The always_comb procedure provides functionality that is different from the general purpose always procedure, as follows:
- There is an inferred sensitivity list.
- The variables written on the left-hand side of assignments shall not be written to by any other process.
- The procedure is automatically triggered once at time zero, after all initial and always procedures have been started so that the outputs of the procedure are consistent with the inputs.
Examples:
always_comb
a = b & c;
always_comb
d <= #1ns b & c;
System Verilog has many procedural statements. In these notes, we will focus on the ones marked in bold:
- blocking_assignment
- nonblocking_assignment
- procedural_continuous_assignment
- case_statement
- conditional_statement
- inc_or_dec_expression
- subroutine_call_statement
- disable_statement
- event_trigger
- loop_statement
- jump_statement
- par_block
- procedural_timing_control_statement
- seq_block
- wait_statement
- procedural_assertion_statement
- clocking_drive
- randsequence_statement
- randcase_statement
- expect_property_statement
Procedural Assignment
A procedural assignment can only be used within an always block or initial block. We have already used the initial blocks and the procedural assignments in the test-benches of the previous blogs.
There are two types of assignments:
- blocking assignment: the expression is evaluated and then assigned to the variable immediately, before the execution of the next statement
- nonblocking assignment: the evaluated expression is assigned at the end of the always block
Their basic syntax is
[variable_name] = [expression]; // blocking assignment
Examples of always Block
Poor 1-bit comparator implementation with one always block
Poor eq1 implementation to show the difference between blocking and nonblocking assignments
`timescale 1ns / 10ps module poor_eq1_always( input logic a, input logic b, output logic eq ); always_comb begin: equ1 // local variable logic tmp; // procedural statements executed sequentially in order tmp = 1'b0; tmp = tmp | (~a & ~b); tmp = tmp | (a & b); eq = tmp; end endmodule module poor_eq1_always_testbench; // signal declaration logic test_a, test_b; logic test_eq; // instantiate the circuit under test poor_eq1_always uut(.a(test_a), .b(test_b), .eq(test_eq)); // test vector generator initial begin // test vector 1 test_a = 1'b0; test_b = 1'b0; #200; // test vector 2 test_a = 1'b1; test_b = 1'b0; #200; // test vector 3 test_a = 1'b0; test_b = 1'b1; #200; // test vector 1 test_a = 1'b1; test_b = 1'b1; #200; $stop; end task display; #1 $display("i0:%0h, i1:%0h, eq:%0h", test_a, test_b, test_eq); endtask endmodule
Simulation
Implementation is correct. eq signal is true only when a and b are equal.
Inferred circuit
Lets make an error and use nonblocking assignments instead
PLEASE, DON'T DO THIS!!!!
`timescale 1ns / 1ps module poor_eq1_always_nonblocking( input logic a, input logic b, output logic eq ); always_comb begin: equ1 // local variable logic tmp; // DON'T DO!!!!!!!!: Using nonblocking assignments tmp <= 1'b0; tmp <= tmp | (~a & ~b); tmp <= tmp | (a & b); eq <= tmp; end endmodule
The circuit now is totally wrong. It is clearly not the desired circuit.
Again, please, don't do this.
Simulation: This is not what we wanted
Poor 2-bit Comparator Implementation With Two always Blocks
The two-bit comparator can be implemented by two always blocks and a continuous assignment.
`timescale 1ns / 10ps module poor_eq2_always( input logic [1:0] a, input logic [1:0] b, output logic eq ); logic eq0; logic eq1; // instantiate two one-bit always comparators poor_eq1_always eq_inst0(.a(a[0]), .b(b[0]), .eq(eq0)); poor_eq1_always eq_inst1(.a(a[1]), .b(b[1]), .eq(eq1)); // continuous assignment assign eq = eq0 & eq1; endmodule // 2-bit comparator module techbench module poor_eq2_always_testbench; // signal declaration logic [1:0] test_a, test_b; logic test_eq; // instantiate the circuit under test poor_eq2_always uut(.a(test_a), .b(test_b), .eq(test_eq)); // test vector generator initial begin // test vector 1 test_a = 2'b00; test_b = 2'b00; #200; // test vector 2 test_a = 2'b01; test_b = 2'b00; #200; // test vector 3 test_a = 2'b10; test_b = 2'b00; #200; // test vector 4 test_a = 2'b11; test_b = 2'b00; #200; // test vector 5 test_a = 2'b00; test_b = 2'b01; #200; // test vector 6 test_a = 2'b01; test_b = 2'b01; #200; // test vector 7 test_a = 2'b10; test_b = 2'b01; #200; // test vector 8 test_a = 2'b11; test_b = 2'b01; #200; // test vector 9 test_a = 2'b00; test_b = 2'b10; #200; // test vector 10 test_a = 2'b01; test_b = 2'b10; #200; // test vector 11 test_a = 2'b10; test_b = 2'b10; #200; // test vector 12 test_a = 2'b11; test_b = 2'b10; #200; // test vector 13 test_a = 2'b00; test_b = 2'b11; #200; // test vector 14 test_a = 2'b01; test_b = 2'b11; #200; // test vector 15 test_a = 2'b10; test_b = 2'b11; #200; // test vector 16 test_a = 2'b11; test_b = 2'b11; #200; $stop; end task display; #1 $display("i0:%0h, i1:%0h, eq:%0h", test_a, test_b, test_eq); endtask endmodule
Although our code style is very poor, we can see how the semantics of sequential execution work.
Again Vivado does a great job inferring the comparator circuit.
The code contains three concurrent constructs. The two instantiation blocks with always blocks for the two 1-bit comparators. When a or b changes, both always blocks are activated at the same time and run concurrently. Their internal procedural statements are executed sequentially to obtain the one-bit comparison results, which in turn activate the continuous assignment to obtain the final value.
The always_comb construct implicitly includes all input signals within the block.
Coding Guidelines
Simple guides to obtain reliable and robust codes:
- Separate memory components (registers) into an individual code segment.
- Use always_ff and nonblocking (deferred) assignments for a register.
- Use always_comb and blocking (immediate) assignments for a combinational circuit. In a combinational circuit, the output responds to an input change immediately. Although both blocking and nonblocking assignments can be used to describe the same circuit, nonblocking assignments can activate the same always block several times.
- Assign a variable only in a single always block. Assigning a variable in multiple always blocks leads to a race condition and does not describe any behavior existing in a physical circuit.
SystemVerilog does not specifies an "ordering mechanism" within the same region and thus the activities can be rendered in any order. The same SystemVerilog code may infer different circuits and all circuits are considered correct by the language standard.
In SystemVerilog, variables can be assigned in multiple always blocks. Example:
always_comb
if (reset) y = 1'b0;
always_comb
if (~reset) y = a & b;
The code is syntactically correct but leads to a race condition. No physical circuit exhibits this behavior.
We must group the assignment statements in a single always block
Think procedure statements in code as hardware parts, not as sequential C algorithm
always_comb
if (reset) y = 1'b0;
else y = a & b;
For a combinational circuit, the output is a function thus any change in an input signal should activate the circuit. The always_comb construct implicitly includes all input signals within the block.
If Statement
The simplified syntax of an if statement:
if [boolean_expr]
begin
[procedural statement];
[procedural statement];
...
end
else
begin
[procedural statement];
[procedural statement];
...
end
[boolean_expr]: Boolean expression that is evaluated first. If true the the statements in the following branch are executed. Otherwise, the statements in the else branch are executed. The else branch is optional.
Multiple if statement can be "cascaded":
if [boolean_expr]
...
else if [boolean_expr]
...
else if [boolean_expr]
...
else
...
Priority Encoder Using an if Statement
Four request priority encoder. The priority encoder has four request, request[4], request[3], request[2] and request[1] which are grouped in a single 4-bit request input.
A common use of priority encoders is for interrupt controllers, where we select the most critical out of multiple interrupt requests.
The output is the binary code of the highest order request.
Function table of the four-request priority encoder:
Input request |
Output highest_request code |
1 - - - |
1 0 0 |
0 1 - - |
0 1 1 |
0 0 1 - |
0 1 0 |
0 0 0 1 |
0 0 1 |
0 0 0 0 |
0 0 0 |
SystemVerilog code with testbench and wrapper for the Arty S7 50
`timescale 1ns / 10ps module prio_encoder_if( input [4:1] request, output logic [2:0] highest_request ); always_comb begin if (request[4]) begin highest_request = 3'b100; end else if (request[3]) begin highest_request = 3'b011; end else if (request[2]) begin highest_request = 3'b010; end else if (request[1]) begin highest_request = 3'b001; end else begin highest_request = 3'b000; end end endmodule module prio_encoder_if_wrapper( input logic [3:0] sw, output logic [3:0] led); prio_encoder_if prio_encoder_if_wrapper( .request(sw), .highest_request(led[3:1])); endmodule module prio_encoder_testbench; logic [4:1] test_request; logic [2:0] test_highest_request; prio_encoder_if uut(.request(test_request), .highest_request(test_highest_request)); initial begin test_request = 4'b1111; #20; test_request = 4'b1110; #20; test_request = 4'b1101; #20; test_request = 4'b1100; #20; test_request = 4'b1011; #20; test_request = 4'b1010; #20; test_request = 4'b1001; #20; test_request = 4'b1000; #20; test_request = 4'b0111; #20; test_request = 4'b0110; #20; test_request = 4'b0101; #20; test_request = 4'b0100; #20; test_request = 4'b0011; #20; test_request = 4'b0010; #20; test_request = 4'b0001; #20; test_request = 4'b0000; #20; $stop; end endmodule
Vivado simulation
Vivado Elaborated Design Schematic
The priority routing network is implemented by a sequence of 2-to-1 multiplexers.
Vivado Synthesized Design Schematic
Upload the code to the Digilent Arty S7 50 development bord
Binary Decoder Using an if Statement
An n-to-xy binary decoder asserts one bit of the 2^n-bit output according to the input combination.
Functional table of a 2-to-4 decoder with enable:
en | a(1) | a(0) | y |
0 |
- |
- |
0000 |
1 |
0 |
0 |
0001 |
1 |
0 |
1 |
0010 |
1 |
1 |
0 |
0100 |
1 |
1 |
1 |
1000 |
SystemVerilog code with testbench and wrapper for the Arty S7 50
`timescale 1ns / 10ps module decoder_2_4_if( input logic [1:0] a, input logic en, output logic [3:0] decode ); always_comb begin if(!en) begin decode = 4'b0000; end else if (a == 2'b00) begin decode = 4'b0001; end else if (a == 2'b01) begin decode = 4'b0010; end else if (a == 2'b10) begin decode = 4'b0100; end else begin decode = 4'b1000; end end endmodule module decoder_2_4_if_wrapper( input logic [3:0] sw, output logic [3:0] led); decoder_2_4_if uut(.en(sw[3]), .a(sw[1:0]), .decode(led)); endmodule module decoder_2_4_if_testbench(); logic [1:0] a; logic en; logic [3:0] decode; decoder_2_4_if uut(.*); initial begin en = 0; a = 2'b00; #10; en = 0; a = 2'b01; #10; en = 0; a = 2'b10; #10; en = 0; a = 2'b11; #10; en = 1; a = 2'b00; #10; en = 1; a = 2'b01; #10; en = 1; a = 2'b10; #10; en = 1; a = 2'b11; #10 $stop; end endmodule
The code checks whether en is not asserted. If false then tests the four binary combinations in sequence.
Simulation
Large propagation delays: The priority routing network is implemented by a sequence of 2-to-1 multiplexers. An if-else statement implies a priority routing network.
As we increment the if-else clauses the cascading chain increments and introduce a large propagation delay.
Vivado Synthesized Design Schematic
Case Statement
Simplified Syntax
The simplified syntax of a case statement is:
case [case_expr]
[case_item_expr]:
begin
[procedural_statement];
[procedural_statement];
end
[case_item_expr]:
begin
[procedural_statement];
[procedural_statement];
end
...
default:
begin
[procedural_statement];
[procedural_statement];
end
endcase
The case statement is a multiway decision statement that tests whether an expression matches one of a number of other expressions and branches accordingly.
The default statement is optional. Use of multiple default statements in one case statement is illegal. The [case_expr] and [case_item_expr] are not required to be constant expressions.
The [case_expr] is evaluated exactly once and before any of the [case_item_expr] . The [case_item_expr] are evaluated and then compared in the exact order in which they appear. If there is a default case_item, it is ignored during this linear search. During the linear search, if one of the [case_item_expr] matches the [case_expr], then the statement associated with that case_item is executed, and the linear search terminates. If all comparisons fail and the default item is given, then the default item statement is executed. If the default statement is not given and all of the comparisons fail, then none of the case_item statements are executed.
Apart from syntax, the case statement differs from the multiway if–else–if construct in two important ways:
- The conditional expressions in the if–else–if construct are more general than comparing one expression with several others, as in the case statement.
- The case statement provides a definitive result when there are x and z values in an expression.
In a [case_expr] comparison, the comparison only succeeds when each bit matches exactly with respect to the values 0, 1, x, and z. As a consequence, care is needed in specifying the expressions in the case statement. The bit length of all the expressions needs to be equal, so that exact bitwise matching can be performed. Therefore, the length of all the [case_item_expr], as well as the [case_expr], shall be made equal to the length of the longest [case_expr] and [case_item_expr]. If any of these expressions is unsigned, then all of them shall be treated as unsigned. If all of these expressions are signed, then they shall be treated as signed.
The reason for providing a case_expression comparison that handles the x and z values is that it provides a mechanism for detecting such values and reducing the pessimism that can be generated by their presence.
2-to-4 Decoder Using a case Statement
SystemVerilog code
`timescale 1ns / 10ps module decoder_2_4_case( input logic [1:0] a, input logic en, output logic [3:0] decode ); always_comb begin case({en, a}) 3'b000, 3'b001,3'b010,3'b011 : begin decode = 4'b0000; end 3'b100: begin decode = 4'b0001; end 3'b101: begin decode = 4'b0010; end 3'b110: begin decode = 4'b0100; end default: begin decode = 4'b1000; end endcase end endmodule module decoder_2_4_case_wrapper( input logic [3:0] sw, output logic [3:0] led); decoder_2_4_case uut(.en(sw[3]), .a(sw[1:0]), .decode(led)); endmodule module decoder_2_4_case_testbench(); logic [1:0] a; logic en; logic [3:0] decode; decoder_2_4_case uut(.*); initial begin en = 0; a = 2'b00; #10; en = 0; a = 2'b01; #10; en = 0; a = 2'b10; #10; en = 0; a = 2'b11; #10; en = 1; a = 2'b00; #10; en = 1; a = 2'b01; #10; en = 1; a = 2'b10; #10; en = 1; a = 2'b11; #10 $stop; end endmodule
Simulation
Vivado Elaborated Design Schematic. Now implemented as a RTL_ROM instead of cascading multiplexors
Vivado Synthesized Design Schematic.
But the final synthesized design is similar to the if-version
Four Request Priority Encoder Using a case Statement
We replicate the Four Request Priority Encoder now using a case statement
SystemVerilog code
`timescale 1ns / 10ps module prio_encoder_case( input [4:1] request, output logic [2:0] highest_request ); always_comb begin case (request) 4'b1000, 4'b1001, 4'b1010, 4'b1011, 4'b1100, 4'b1101, 4'b1110, 4'b1111: begin highest_request = 3'b100; end 4'b0100, 4'b0101, 4'b0110, 4'b0111: begin highest_request = 3'b011; end 4'b0010, 4'b0011: begin highest_request = 3'b010; end 4'b0001: begin highest_request = 3'b001; end default: begin highest_request = 3'b000; end endcase end endmodule module prio_encoder_case_wrapper( input logic [3:0] sw, output logic [3:0] led); prio_encoder_case prio_encoder_case_wrapper( .request(sw), .highest_request(led[2:0])); endmodule module prio_encoder_case_testbench; logic [4:1] test_request; logic [2:0] test_highest_request; prio_encoder_case uut(.request(test_request), .highest_request(test_highest_request)); initial begin test_request = 4'b1111; #20; test_request = 4'b1110; #20; test_request = 4'b1101; #20; test_request = 4'b1100; #20; test_request = 4'b1011; #20; test_request = 4'b1010; #20; test_request = 4'b1001; #20; test_request = 4'b1000; #20; test_request = 4'b0111; #20; test_request = 4'b0110; #20; test_request = 4'b0101; #20; test_request = 4'b0100; #20; test_request = 4'b0011; #20; test_request = 4'b0010; #20; test_request = 4'b0001; #20; test_request = 4'b0000; #20; $stop; end endmodule
Simulation
Case Statement With Do-Not-Cares
Two other types of case statements are provided to allow handling of do-not-care conditions in the case comparisons. One of these treats high-impedance values (z) as do-not-cares, and the other treats both high-impedance and unknown (x) values as do-not-cares. These case statements can be used in the same way as the traditional case statement, but they begin with keywords casez and casex, respectively.
Do-not-care values (z values for casez, z and x values for casex) in any bit of either the case_expression or the case_items shall be treated as do-not-care conditions during the comparison, and that bit position shall not be considered.
The syntax of literal numbers allows the use of the question mark (?) in place of z in these case statements.
Four Request Priority Encoder Using a casez Statement
SystemVerilog code
`timescale 1ns / 10ps module prio_encoder_casez( input [4:1] request, output logic [2:0] highest_request ); always_comb begin casez (request) 4'b1???: begin highest_request = 3'b100; end 4'b01??: begin highest_request = 3'b011; end 4'b001?: begin highest_request = 3'b010; end 4'b0001: begin highest_request = 3'b001; end default: begin highest_request = 3'b000; end endcase end endmodule module prio_encoder_casez_wrapper( input logic [3:0] sw, output logic [3:0] led); prio_encoder_casez prio_encoder_casez_wrapper( .request(sw), .highest_request(led[2:0])); endmodule module prio_encoder_casez_testbench; logic [4:1] test_request; logic [2:0] test_highest_request; prio_encoder_casez uut(.request(test_request), .highest_request(test_highest_request)); initial begin test_request = 4'b1111; #20; test_request = 4'b1110; #20; test_request = 4'b1101; #20; test_request = 4'b1100; #20; test_request = 4'b1011; #20; test_request = 4'b1010; #20; test_request = 4'b1001; #20; test_request = 4'b1000; #20; test_request = 4'b0111; #20; test_request = 4'b0110; #20; test_request = 4'b0101; #20; test_request = 4'b0100; #20; test_request = 4'b0011; #20; test_request = 4'b0010; #20; test_request = 4'b0001; #20; test_request = 4'b0000; #20; $stop; end endmodule
Full Case and Parallel Case
It is frequently desirable that the item expressions of a case statements meet the following conditions:
- all-inclusive: all possible binary values of the case expression are covered by the item expressions. This is call full case statement. For a combinational circuit we must use a full case statement since each input should have an output value.
- mutually-exclusive: a binary value appears in only one item expression. This is call a parallel case.
Routing Structures of Conditional Control Constructs
In a combinational circuit the constructs are realized by routing networks. All the expressions are evaluated concurrently and the routing network routes the desired result to the output.
Routing structures:
- Priority routing network: inferred by an if-else type statement.
- Multiplexing network: inferred by a parallel case statement.
Priority Routing Network
Consider the following module:
`timescale 1ns / 10ps module priority_routing_network( input logic m, n, a, b, c, output logic r ); always_comb begin if (m==n) r = a +b +c; else if(m>n) r = a -b ; else r= c+1; end endmodule module priority_routing_network_wrapper( input logic [3:0] sw, output logic [3:0] led); priority_routing_network r1(.m(sw[0]),.n(sw[3]),.r(led[0]),.a(sw[0]),.b(sw[1]),.c(sw[2])); endmodule
In the schematic the 2-to-1 multiplexes form the priority routing network and the other components implement various Boolean and arithmetic expressions.
- if the first condition (m==n) is true the result of a + b + c is routed to r.
- Otherwise data connected to I0 are passed to r.
- The next Boolean condition (m>n) determines whether the result of a-b or c+1 is routed to the output.
The conditional operator (?:) is like a simplified if-else statement and infers similar priority networks.
A nonparallel case statement sets a preference on the first matched item and thus also infers similar priority routing networks.
Multiplexing Networks
Consider the following module:
`timescale 1ns / 10ps
module multiplexing_network(
input logic [1:0] sel,
input logic a, b, c,
output logic r
);
always_comb
begin
case (sel)
2'b00: r = a + b + c;
2'b10: r = a - b;
default : r = c + 1;
endcase
end
endmodule
module multiplexing_network_wrapper(
input logic [3:0] sw,
output logic [3:0] led);
multiplexing_network uut(.sel(sw[1:0]), .r(led[0]),.a(sw[0]),.b(sw[1]),.c(sw[2]));
endmodule
The sel variable can assume four possible values: 00, 01, 10, and 11. It implies a 2^2-to1 multiplexes with sel as the selection signal.
- The evaluated result of a+b+c is routed to r when sel is 00
- The result of a-b is routed to r qhen sel is 10,
- and the result of c+1 is routed to r when sel is 01 or 11
Implementation of a parallel case statement:
The priority routing network is more effective when a preference is given under certain conditions.
The multiplexing network is more effective for a truth table or function table-based description.
Incomplete Branch and Incomplete Output Assignment.
In a combinational circuit the output is a function of current inputs. The circuit should not contain any internal state.
The standard specifies that a variable will keep irs previous value if it is not assigned a value in an always block.
During synthesis, this infers an internal state (via a closed feedback loop) or a memory element (such a latch).
Incomplete branch and incomplete output assignment causes some variables to remain unassigned and leads to unintended memory in a combinational circuit!
Coding guidelines
- Include all the branches of an if or case statement.
- Assign a value to every output signal in every branch.
One way to satisfy the two previous guidelines is to assign default values for outputs in the beginning of the always block.
Be aware of he type of routing network inferred by different control constructs.
Parameter and Constant
Constants are named data objects that never change. SystemVerilog provides three elaboration-time constants: parameter, localparam, and specparam. SystemVerilog also provides a run-time constant, const.
The parameter, localparam, and specparam constants are collectively referred to as parameter constants.
Parameter constants can be initialized with a literal.
localparam byte COLON_1= ":" ;
specparam DELAY = 10 ; // specparams are used for specify blocks
parameter logic FLAG = 1 ;
SystemVerilog provides several methods for setting the value of parameter constants. Each parameter may be assigned a default value when declared. The value of a parameter of an instantiated module, interface, or program can be overridden in each instance using one of the following:
- Assignment by ordered list (e.g., m #(value, value) u1 (...); )
- Assignment by name
The list_of_param_assignments can appear in a module, interface, program, class, or package or in the parameter_port_list of a module, interface, program, or class. If the declaration of a design element uses a parameter_port_list (even an empty one), then in any parameter_declaration directly contained within the declaration, the parameter keyword shall be a synonym for the localparam keyword. All param_assignments appearing within a class body shall become localparam declarations regardless of the presence or absence of a parameter_port_list. All param_assignments appearing within a generate block, package, or compilation-unit scope shall become localparam declarations.
The parameter keyword can be omitted in a parameter port list. For example:
class vector #(SIZE = 1); // size is a parameter in a parameter port list
logic [SIZE-1:0] v;
endclass
interface simple_bus #(AWIDTH = 64, type T = word) // parameter port list
(input logic clk) ; // port list
...
endinterface
Parameter
A parameter section can be added to a module in the header, before the port declaration. Its simplified syntax is:
module [module_name]
#(
parameter [parameter-name] = [default_value],
...
parameter [parameter-name] = [default_value]
)
(
... // IO port declaration
)
Adder Using Constants and Parameters
The code is for a 4-bit adder. The N parameter is declared with a default value of 4. If the adder is later used as a component in other code, we can assign a desired value to the parameter during component instantiation and override the default value.
`timescale 1ns / 10ps module adder_carry_para #(parameter N = 4)( input logic [N-1:0] a, input logic [N-1:0] b, output logic [N-1:0] sum, output logic carryOut ); localparam BIT_ZERO = 1'b0; logic [N:0] sum_ext; assign sum_ext = {BIT_ZERO, a} + {BIT_ZERO, b}; assign sum = sum_ext [N-1:0]; assign carryOut = sum_ext[N]; endmodule module adder_insta ( input logic [3:0] a4, input logic [3:0] b4, output logic [3:0] sum4, output logic c4, input logic [7:0] a8, input logic [7:0] b8, output logic [7:0] sum8, output logic c8 ); adder_carry_para #(.N(8)) unit1 (.a(a8), .b(b8), .sum(sum8), .carryOut(c8)); adder_carry_para #(.N(4)) unit2 (.a(a4), .b(b4), .sum(sum4), .carryOut(c4)); endmodule module adder_testbench(); logic [3:0] a4; logic [3:0] b4; logic [3:0] sum4; logic c4; logic [7:0] a8; logic [7:0] b8; logic [7:0] sum8; logic c8; adder_insta uut(.a4(a4), .b4(b4), .c4(c4), .sum4(sum4), .a8(a8), .b8(b8), .sum8(sum8), .c8(c8)); initial begin a4 = 4'b0001; b4 = 4'b0011; a8 = 8'b0011_1111; b8 = 8'b0000_1111; #10 a4 = 4'b1111; b4 = 4'b0001; a8 = 8'b1011_1111; b8 = 8'b1000_1111; #10 $stop; end endmodule
Simulation
A parameter provides a mechanism to create scalable code, in wich the "width" of a circuit can be adjusted to meet a specific need.
Th N parameter is declared with a default value of 4.
During the component instantiation we can override the default value. In the example we instantiate the circuit with N=8 and N =4
Replicated Structure
Generate Constructs
Generate constructs are used to either conditionally or multiply instantiate generate blocks into a model. A generate block is a collection of one or more module items. A generate block may not contain port declarations, specify blocks, or specparam declarations. Parameters declared in generate blocks shall be treated as localparams. All other module items, including other generate constructs, are allowed in a generate block. Generate constructs provide the ability for parameter values to affect the structure of the design. They also allow for modules with repetitive structure to be described more concisely, and they make recursive module instantiation possible.
There are two kinds of generate constructs: loops and conditionals. Loop generate constructs allow a single generate block to be instantiated into a model multiple times. Conditional generate constructs, which include if-generate and case-generate constructs, instantiate at most one generate block from a set of alternative generate blocks. The term generate scheme refers to the method for determining which or how many generate blocks are instantiated. It includes the conditional expressions, case alternatives, and loop control statements that appear in a generate construct.
The keywords generate and endgenerate may be used in a module to define a generate region. A generate region is a textual span in the module description where generate constructs may appear. Use of generate regions is optional. There is no semantic difference in the module when a generate region is used. A parser may choose to recognize the generate region to produce different error messages for misused generate construct keywords. Generate regions do not nest, and they may only occur directly within a module. If the generate keyword is used, it shall be matched by an endgenerate keyword.
Loop Generate Constructs
A loop generate construct permits a generate block to be instantiated multiple times using syntax that is similar to a for loop statement. The loop index variable shall be declared in a genvar declaration prior to its use in a loop generate scheme.
The genvar is used as an integer during elaboration to evaluate the generate loop and create instances of the generate block, but it does not exist at simulation time. A genvar shall not be referenced anywhere other than in a loop generate scheme.
Both the initialization and iteration assignments in the loop generate scheme shall assign to the same genvar. The initialization assignment shall not reference the loop index variable on the right-hand side.
Within the generate block of a loop generate construct, there is an implicit localparam declaration. This is an integer parameter that has the same name and type as the loop index variable, and its value within each instance of the generate block is the value of the index variable at the time the instance was elaborated. This parameter can be used anywhere within the generate block that a normal parameter with an integer value can be used. It can be referenced with a hierarchical name.
generate
genvar [index_variables];
for ([initial_assignment]; [expression]; [step_assignment])
begin [:optional_label]
[concurrent_constructs];
[concurrent_constructs];
...
end
endgenerate
Procedural-for Statements
The procedural for loop can only be used within an always block and its semantic
for ([initial_assignment]; [expression]; [step_assignment])
begin [:optional_label]
[concurrent_constructs];
[concurrent_constructs];
...
end
SystemVerilog consists of several types of procedural loop statements. Most of them cannot been be easily synthesized, most of them are for modeling and test-bench development.
The procedural for loop can only be used within an always block. For the synthesizable description, the loop boundaries has to be static. For synthesis purposes, it should be treated as a shorthand for repetitive procedural statements rather than a sequential control mechanism.
Reduced-xor circuit
Reduced-xor Circuit Using a Generate Statement
SystemVerilog has a built-in reduce-xor operator. We will use it to validate our designs.
The reduced-xor circut can be constructed with a linear cascading chain. The basic building bloc in a stage is a two-iput xor gate.
For the ith stage
p[i] = a[i] ^ p[ i - 1];
SystemVerilog code
`timescale 1ns / 10ps module reduced_xor_gen #(parameter N=8) ( input logic [N-1:0] a, output logic y ); // intermediate p signal for the N stages logic [N-1:0] p; // stage 1 assign p[0] = a[0]; generate genvar i; for ( i = 1; i < N ; ++i) begin // an xor gate in each state assign p[i] = a[i] ^ p[i - 1]; end endgenerate // connect output from the last stage assign y = p[N-1]; endmodule module reduced_xor_gen_testbench; logic [7:0] a; logic y; reduced_xor_gen #(.N(8)) uut(.*); initial begin a = 8'b0000_0000; #10; a = 8'b1111_1111; #10; a = 8'b0101_0101; #10; a = 8'b1010_1010; #10; a = 8'b1010_1011; #10; a = 8'b0000_0001; #10; $stop; end endmodule
Reduced-xor circuit
Reduced-xor Circuit Using a Procedural Loop Statement
We are going to do the same exercise, but this time using loop statements inside an always block.
SystemVerilog code
`timescale 1ns / 10ps module reduced_xor_loop #(parameter N=8) ( input logic [N-1:0] a, output logic y ); always_comb begin logic tmp; tmp = a[0]; for(int i = 1; i<N; i=i+1) begin tmp ^= a[i]; end y = tmp; end endmodule module reduced_xor_loop_testbench; logic [7:0] a; logic y; reduced_xor_loop #(.N(8)) uut(.*); initial begin a = 8'b0000_0000; #10; a = 8'b1111_1111; #10; a = 8'b0101_0101; #10; a = 8'b1010_1010; #10; a = 8'b1010_1011; #10; a = 8'b0000_0001; #10; $stop; end endmodule
Simulation
The result of the simulation is the same, as we expected.
Source Files
Conclusion
We've reviewed the main SystemVerilog control and concurrent constructs for combinational logic circuits, how they are synthesized from SystemVerilog, and some guidelines for making our code more robust and reliable.
We have the necessary knowledge to face simple design exercises. The next chapter of this series will be dedicated to small combinational logic design exercises.
SystemVerilog Study Notes Chapters
- Gate-Level Combinational Circuit
- RTL Combinational Circuit Operators
- RTL Combinational Circuit - Concurrent and Control Constructs
- Hex-Digit to Seven-Segment LED Decoder RTL Combinational Circuit
- Barrel Shifter RTL Combinational Circuit
- Simplified Floating Point Arithmetic. RTL Combinational Circuit
- BCD Number Format. RTL Combinational Circuit
- DDFS. Direct Digital Frequency Synthesis for Sound
- FPGA ADSR envelope generator for sound synthesis
- AMD Xilinx 7 series FPGAs XADC
- Building FPGA-Based Music Instrument Synthesis: A Simple Test Bench Solution