Module 4: Introduction to Verilog HDL ยท 4.5 Dataflow Modeling

Continuous Assignments

Section 4.4 described circuits by naming gates. This section describes them by writing equations. A gate on a schematic is always there, computing its output from its inputs from the moment power arrives. assign y = a & b; is that gate written down: not an instruction, but a statement that an AND gate exists with its output on y. That is dataflow modelling, and it is how nearly all combinational RTL is written.

The statement

assign wire_name = expression;

The right-hand side can use any operator from section 4.3, and whatever hardware the expression implies is what gets built:

assign sum        = a + b;                                   // an adder
assign {cout, s}  = {1'b0, a} + {1'b0, b};                   // the same adder, carry kept
assign y          = sel ? a : b;                             // a mux
assign eq         = (a == b);                                // an XOR tree

Read each one and ask what circuit it is. The answer is always the same kind of thing: some gates with inputs and an output wire.

The rules

It is continuous. Whenever anything on the right changes, the left changes, after a gate delay in silicon and instantly in an ideal simulation. Nothing polls and nothing is called.

It is concurrent. Every assign in a module is active at the same time, and the order they appear in is irrelevant. These are the same hardware:

assign sum = a + b;
assign eq  = (a == b);

assign eq  = (a == b);
assign sum = a + b;

The left side is a net. A reg cannot be driven by assign; that is the rule from section 4.2, and the compiler enforces it.

One driver per wire. Two assigns to the same wire are a conflict, an error in most tools and x in the rest. If a wire needs to take one of several values, that is a mux, and you write the mux:

assign y = sel ? a : b;

An assign can carry a delay, assign #5 y = a & b;, and the simulator honours it and the synthesiser ignores it, as section 4.1 said. It belongs in models, not in designs.

Two spellings

wire y;
assign y = a & b;    // explicit

wire y = a & b;      // declaration and assignment together

Both synthesise. The explicit form is the one to use in anything other than a throwaway, because the assign keyword is what a reader looks for.

What a set of assigns is

Four assigns describe a full adder bit:

assign p         = a ^ b;
assign g         = a & b;
assign sum_bit   = p ^ cin;
assign carry_out = g | (p & cin);

Every statement is a node in the schematic and its output wire; every name on the right is a wire from another node (Figure 1). The synthesiser maps each expression to cells of the library, and the names p, g, sum_bit and carry_out are the wires between them.

A full adder as four assigns
Figure 1: Each assign is a node; each name is a wire

assign or always @(*)

Both describe combinational logic and the netlist is the same. The choice is readability.

Logic Use
one expression, one output assign
a 2-to-1 or 4-to-1 mux assign with ?:
an adder, a comparator assign
several outputs from the same conditions always @(*)
a decode with many cases always @(*) with case
anything with a clock always @(posedge clk), the only option

If it fits in one expression, assign. If you find yourself four levels deep in ?: or writing the same condition for three outputs, an always block. A module mixes the two freely.

A half adder and a full adder

`timescale 1ns / 1ps
`default_nettype none

module half_adder (
    input  wire a,
    input  wire b,
    output wire sum,
    output wire carry
);
    assign sum   = a ^ b;
    assign carry = a & b;
endmodule

module full_adder (
    input  wire a,
    input  wire b,
    input  wire cin,
    output wire sum,
    output wire cout
);
    wire xor_ab;
    wire carry_1, carry_2;

    assign xor_ab  = a ^ b;
    assign sum     = xor_ab ^ cin;
    assign carry_1 = a & b;
    assign carry_2 = xor_ab & cin;
    assign cout    = carry_1 | carry_2;

endmodule

This is the five-gate full adder of the previous section, without naming a gate.

Practice Exercise

Write a 4-input priority encoder with assign only: input req[3:0], output grant[1:0] giving the index of the highest active request, and valid high when any request is active.

Solution
`timescale 1ns / 1ps
`default_nettype none

module priority_encoder_4to2 (
    input  wire [3:0] req,      // req[3] has the highest priority
    output wire [1:0] grant,
    output wire       valid
);
    assign valid = |req;

    assign grant = req[3] ? 2'd3 :
                   req[2] ? 2'd2 :
                   req[1] ? 2'd1 :
                            2'd0;
endmodule

The chain tests from the top down, so the highest active request wins. With no request, grant reads 0, the same as for req[0] alone, which is why valid exists: the reader of grant has to check it first.

The next article is a gallery: the common circuits of RTL, each as an assign, ending with a complete ALU in dataflow style.

โ† A 4-bit ALU from the Gates Up Dataflow in Practice โ†’