[Edit of Image1]
Hey it's a me again @drifter1!
Today we continue with the Logic Design series on the Hardware Description Language (HDL) Verilog to cover how Combinational Logic is defined in Verilog. One way of specifying continuous assignments in Verilog is the assign statement, which works just like connecting components through wires in a breadboard. Another commonly used feature of Verilog is the procedural always block, which can be used for both combinational and sequential logic. In this point it will also be wise to talk about the various Control Blocks that Verilog provides (if, if-else, case), as well as the for loop. Verilog also provides gate-level primitives, which are useful for modeling simple hierarchical designs such as ripple-carry adders. Lastly, its also worth to cover the user-defined primitives that Verilog provides, which are useful for specifying logic using truth tables.
So, without further ado, let's dive straight into it!
Assign Statement (Data Flow Modeling)
Assign statements can be used for something known as Data Flow Modeling. When using this approach no direct gate structure needs to be defined, but only the function that specifies the result/output. Any signal of type wire (or similar data types), which is not a register requires a continuous assignment of a value. The assign statement provides exactly that. Any wire data type can be driven continuously with a value, which can be either a constant or an expression of various signals. This expression describes the function of the circuit.
An assign statement begins with the keyword assign, followed by a "net expression", an equal (=) sign and lastly the constant or expression of various signals that's to be assigned:
The following rules need to be followed whenever the assign statement is used:
assign <net_expression> = <assign_expression>;
- The left-hand side (LHS) can be a scalar, vector or combination of scalar and vector nets, but NOT scalar or vector registers
- The right-hand side (RHS) is allowed to contain both scalar or vector nets and registers
For example, let's consider the following circuit:
The result of the cicuit can be written, mathematically, as follows:
In Verilog, considering all the signals have been already declared as wires, the assign statement is:
assign o = (a | b) ^ (c & d);
Its also common to use the conditional operator in order to define small multiplexers using the assign statement, such as:
assign c = sel ? a : b;
- a, b : input
- sel : selection signal
- c : output
The always block is the most powerful block in Verilog. It can be used for both sequential and combinational logic, and most assign statements can be easily replicated using always blocks. All statements within an always block are executed sequentially and the sensitivity list of the block specifies when the block of code has to be executed.
Always Block Syntax
An always block begins with the keyword always followed by the @ symbol/operator and the conditions (sensitivity list) in parentheses.
The statements to be executed are enclosed within a begin and a end keyword.
When only one statement needs to be executed then those last two keywords are optional, as only the statement directly after the
@ (conditions) will be executed when the conditions are met.
As such, the two main ways of specifying an always block are:
always @ (sensitivity_list) [statement]
always @ (sensitivity_list) begin [multiple statements] end
In order to specify combinational logic using such a block, all signals that affect the result need to be in the sensitivity list, so that any change of their value "triggers" the always block. Assigning a value is a simple as:
and so the same as the assign statement, but without the keyword.
<net_expression> = <assign_expression>;
Note that this is called an blocking assignment (=) as it is executed after the procedural part within the always block. It's wise to always use such assignments when modeling combinational logic with always blocks.
The circuit from the previous example, can be re-written using an always block, in the following way:
where it's easy to see that all signals that affect the result are within the sensitivity list.
always @ (a or b or c or d) begin o = (a | b) ^ (c & d); end
Control Blocks (Behavioral Modeling)
Any hardware design needs a way of controlling the flow of logic using conditional statements. Such mechanisms specify whether certain statements have to be executed or not, in a certain moment. So, this is quite similar to programming languages such as C, where if-else statements control the flow of a program. Of course all the constructs and control blocks are statements of always blocks, and can't exist by themselves!
Conditional Statement (If-Else)
The conditional statement in Verilog is quite similar to the one in C. The if and else keywords are used in order to specify if-else statements with expressions in parentheses. The else part is of course optional. If-else statements can be nested in order to specify more conditions. When multiple statements need to be executed, those are then enclosed within a begin and end keyword (similar to the always block).
The syntax is as follows:
// if statement without else part if (expression) [statement]
// if statement with else part if (expression) [statement] else [statement]
// if-else statement with nested if-else if (expression) [statement] else if (expression) [statement] else [statement]
// if-else with multiple statements if (expression) begin [multiple statements] end else begin [multiple statements] end
When many different conditions have to be checked (for example, for a multiplexer) an if-else statement might not be suitable anymore. That's when the case statement comes into play. The case statement checks if a given expression matches an expression in a list of expressions, and executes the corresponding statement or multiple statements. The case statement begins with the case keyword and ends with the endcase keyword. The expression to be evaluated is put into parentheses directly after the case keyword. Multiple statements are put within begin and end keywords, and cases can be nested. If no case matches the given expression then the default case is executed. This case is completely optional, as nothing will be executed if no match is found, but some synthesis tools might give a warning.
The syntax of a case statement is:
In this example case 2 and case 3 are nested. Also, let's not forget that case 4 contains multiple statements.
case (<expression>) case_1 : [statement] case_2, case_3 : [statement] case_4 : begin [multiple statements] end default : [statement] endcase
Another commonly used building block of Verilog is the for loop:
for (<initial_assignment>; <condition>; <step_assignment>) begin [statements] end
- The initial assignment (or condition) specifies the initial values of signals
- The loop is executed as long as the condition expression evaluates to true
- The step assignment updates the so called control variable after each iteration
i++, but more of an
i = i + 1.
For example, a for loop that initializes the array
reg [7 : 0] ram [0 : 255] can be specified as follows:
Of course i is declared using
for (i = 0; i < 256; i = i + 1) begin ram[i] <= 0; end
integer iand the
ram[i] <= 0is a so called un-blocking assigment used for sequential logic, which will be covered thoroughly in a next part of the series.
Other constructs such as the initial block, while loop, forever loop, repeat loop etc. are only useful in testbenches, and so that's when they will be covered...
Full-on examples on all these topics, and the remaining ones will of course be in the next article, which will be a follow-up to this one!
Gate-Level Modeling and Primitives
Logic design is mostly done in higher levels of abstraction such as Register-Transfer Level (RTL), but sometimes smaller circuits might be more intuitive to build using combinational elements such as ANDs and ORs. This kind of modeling is called Gate-Level Modeling as it involves specifying the interconnection of various logic gates. Verilog provides primitive modules that implement all of the basic logic gates. The arguments in module instantiation begin with the output port, and for logic operations that may involve multiple inputs, any number of inputs is allowed. As such 3-, 4- and even 16-input AND gates can be easily instantiated.
Let's first cover how modules are instantiated. The syntax of module instantiation is:
where module_name is the name of the module to be instantiated. Multiple instances of the same module can be distinguished by an optional instance_identifier. Port mapping can be specified in two ways:
module_name <instance_identifier> (<port_mapping>);
- Explicit mapping
- Positional mapping
In explicit mapping the names of the ports of the module are used in combination with the signals that they are connected to.
As such if a module test_module_1 has an output called o, and the signal it should be driven to is called w, the port mapping is defined as
The complete module instantiation would be of the form:
test_module_1 u0 (.o(w), ...);
In positional mapping the signals are connected to the ports in the order that they are defined within the submodule. Therefore, only the identifiers of the signals within the module that instantiates the other module are specified. The module of the previous paragraph can be instantiated using positional mapping as follows:
test_module_1 u1 (w, ...);
Verilog's gate primitives are instantiated using the name of their logic function. The output is always at the beginning of port mapping, and so positional mapping is preferred. For example, to instantiate an AND gate with any number of inputs we write:
A list of some of the commonly used gate primitives is:
and (o, a, b, c, ...);
- and : AND gate with 1 output and any number of inputs
- or : OR gate -||-
- xor : XOR gate -||-
- nand : NAND gate -||-
- nor : NOR gate -||-
- nxor : NXOR gate -||-
- not : NOT gate with 1 input and any number of output drivers
- buf : BUFFER gate -||-
For example, the circuit of the previous examples can be easily defined using gate primitives with the following code within the module:
wire or_out, and_out;
or (or_out, a, b); and (and_out, c, d); xor (o, or_out, and_out);
User-Defined Primitives (Truth Table Modeling)
Let's lastly cover how truth tables can be used in Verilog in order to model low-level components. Verilog provides a means of defining so called user-defined primitives (UDP). Such primitives are instantiated in a similar to manner to gate primitives and any output has to be declared before the input. By default, the data type of the ports is considered to be wire.
A primitive has a similar syntax to a module, with the module and endmodule keywords being replaced by primitive and endprimitive respectively. The truth table is enclosed within the table and endtable keywords. The syntax of a UDP is:
The most common values used in UDP truth tables are:
primitive primitive_name (port_list); table [truth table] endtable endprimitive
- 0 : logic zero
- 1 : logic one
- x : unknown | don't care's (used for modeling states in sequential UDPs)
- ? or * : any change in input value
- - : no change (only used in the output)
For example, an 3-input AND gate, called and3 can be specified as follows:
It's also possible to shorten such tables, by specifying min-terms or max-terms only (1s or 0s in the output). But, that was already way too much for this article, so let's continue next time with full-on exercises!
primitive and3 (o, a, b, c); table // a b c o 0 0 0 : 0 0 0 1 : 0 0 1 0 : 0 0 1 1 : 0 1 0 0 : 0 1 0 1 : 0 1 1 0 : 0 1 1 1 : 1 endtable endprimitive
Previous articles of the series
- Verilog Introduction → Basic Syntax, Data Types, Operators, Modules
Final words | Next up
And this is actually it for today's post!
Next time we will get into various examples on Combinational Logic using Verilog...
Keep on drifting!