Logic Design - Combinational Logic in Verilog


[Edit of Image1]

Introduction

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.

Assign Syntax

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:

assign <net_expression> = <assign_expression>;
The following rules need to be followed whenever the assign statement is used:
  • 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
Whenever a value in the RHS changes in value, the LHS is updated with the new value.

Example

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);

Conditional Operator

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


Always Block

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

Combinational Logic

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:

<net_expression> = <assign_expression>;
and so the same as the assign statement, but without the keyword.

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.

Example

The circuit from the previous example, can be re-written using an always block, in the following way:

always @ (a or b or c or d) begin
    o = (a | b) ^ (c & d);
end
where it's easy to see that all signals that affect the result are within the sensitivity list.


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

Case Statement

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:

case (<expression>)
    case_1 : [statement]
    case_2,
    case_3 : [statement]
    case_4 : begin
            [multiple statements]
            end
    default : [statement]
endcase
In this example case 2 and case 3 are nested. Also, let's not forget that case 4 contains multiple statements.

For loop

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
Let's note that Verilog doesn't have the ++ operator like C, and so don't expect to see something like 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:

for (i = 0; i < 256; i = i + 1) begin
    ram[i] <= 0;
end
Of course i is declared using integer i and the ram[i] <= 0 is 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.

Module Instantiation

Let's first cover how modules are instantiated. The syntax of module instantiation is:

module_name <instance_identifier> (<port_mapping>);
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:
  1. Explicit mapping
  2. Positional mapping

Explicit 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 .o(w). The complete module instantiation would be of the form:

test_module_1 u0 (.o(w), ...);

Positional Mapping

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, ...);

Gate Primitives

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:

and (o, a, b, c, ...);
A list of some of the commonly used gate primitives is:
  • 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 -||-
The buffer simply transfers the value of the input to the output (or multiple outputs).

Example

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:

primitive primitive_name (port_list);
    table
        [truth table]
    endtable
endprimitive
The most common values used in UDP truth tables are:
  • 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)

Example

For example, an 3-input AND gate, called and3 can be specified as follows:

 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
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!


RESOURCES:

References

  1. http://www.asic-world.com/verilog/veritut.html
  2. https://www.chipverify.com/verilog/verilog-tutorial
  3. https://www.javatpoint.com/verilog

Images

  1. https://www.flickr.com/photos/creative_stock/5227842611

Previous articles of the series


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...

See Ya!

Keep on drifting!

H2
H3
H4
3 columns
2 columns
1 column
Join the conversion now