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

Dataflow in Practice

Most RTL is made of a small number of idioms that come up over and over: a gate, a carry pulled out of a wide add, a sign-extended operand, a mux chain. Once you know them you read RTL the way you read a schematic, and this article is the set, each written as an assign with the hardware it implies, followed by an ALU built from them.

Gates

assign y_not  = ~a;
assign y_and  = a & b;
assign y_or   = a | b;
assign y_xor  = a ^ b;
assign y_nand = ~(a & b);
assign y_nor  = ~(a | b);
assign y_xnor = ~(a ^ b);

One cell each, replicated across the width of the operands.

Reductions

wire [7:0] data_byte;

assign y_and8   = &data_byte;    // all ones
assign y_or8    = |data_byte;    // any one
assign y_parity = ^data_byte;    // odd number of ones
assign y_nor8   = ~|data_byte;   // all zeros: the zero flag

Trees of gates, $\log_2 N$ deep.

An adder with its carry

wire [7:0] a, b;
wire       cin;
wire [7:0] sum;
wire       cout;

assign {cout, sum} = {1'b0, a} + {1'b0, b} + {8'h00, cin};

Extend both operands to 9 bits, add, and split the 9-bit result into the carry and the sum with a concatenation on the left.

Comparators

assign equal   = (a == b);
assign gt_uns  = (a > b);
assign gt_sgn  = ($signed(a) > $signed(b));

Without the cast, 8'hFE is 254 and greater than 8'h01. With it, 8'hFE is $-2$ and less.

Muxes

assign mux2 = sel_1bit ? a : b;

assign mux4 = (sel_2bit == 2'b00) ? a :
              (sel_2bit == 2'b01) ? b :
              (sel_2bit == 2'b10) ? c : d;

Shifts

assign shl_fixed = data << 2;                 // wiring
assign sar_fixed = $signed(data) >>> 2;       // wiring, with the sign bit repeated
assign shl_var   = data << shift_amount;      // a barrel shifter

Extension, reversal, fields

assign sign_ext = {{8{narrow[7]}}, narrow};     // 8 to 16 bits, signed
assign zero_ext = {8'h00, narrow};              // 8 to 16 bits, unsigned
assign reversed = {narrow[0], narrow[1], narrow[2], narrow[3],
                   narrow[4], narrow[5], narrow[6], narrow[7]};
assign upper    = narrow[7:4];
assign replaced = {new_upper[3:0], narrow[3:0]};

All wiring, no gates.

An ALU

An ALU is where these come together, and it has the structure every ALU has: every operation computed all the time, a mux choosing one (Figure 1).

Dataflow ALU
Figure 1: Every unit computes; the mux selects

`timescale 1ns / 1ps
`default_nettype none

module alu_dataflow #(
    parameter WIDTH = 8
)(
    input  wire [WIDTH-1:0] a,
    input  wire [WIDTH-1:0] b,
    input  wire [2:0]       op,
    output wire [WIDTH-1:0] result,
    output wire             zero,
    output wire             carry,      // carry out of add; not-borrow out of subtract
    output wire             overflow    // signed overflow
);
    // every operation, all the time; add and subtract one bit wider to keep the carry
    wire [WIDTH:0]   add_result = {1'b0, a} + {1'b0, b};
    wire [WIDTH:0]   sub_result = {1'b0, a} - {1'b0, b};   // top bit is the borrow
    wire [WIDTH-1:0] and_result = a & b;
    wire [WIDTH-1:0] or_result  = a | b;
    wire [WIDTH-1:0] xor_result = a ^ b;
    wire [WIDTH-1:0] not_result = ~a;
    wire [WIDTH-1:0] shl_result = a << 1;
    wire [WIDTH-1:0] shr_result = a >> 1;

    // the selection
    assign result = (op == 3'd0) ? add_result[WIDTH-1:0] :
                    (op == 3'd1) ? sub_result[WIDTH-1:0] :
                    (op == 3'd2) ? and_result :
                    (op == 3'd3) ? or_result  :
                    (op == 3'd4) ? xor_result :
                    (op == 3'd5) ? not_result :
                    (op == 3'd6) ? shl_result :
                                   shr_result;

    // flags
    assign zero = ~|result;

    assign carry = (op == 3'd0) ?  add_result[WIDTH] :   // carry out
                   (op == 3'd1) ? ~sub_result[WIDTH] :   // 1 when no borrow, as in Module 2
                                   1'b0;

    // add: same-sign operands, result of the other sign
    // subtract: opposite-sign operands, result not the sign of a
    assign overflow =
        (op == 3'd0) ? (a[WIDTH-1] == b[WIDTH-1]) && (result[WIDTH-1] != a[WIDTH-1]) :
        (op == 3'd1) ? (a[WIDTH-1] != b[WIDTH-1]) && (result[WIDTH-1] != a[WIDTH-1]) :
                       1'b0;

endmodule

The parameter makes it any width. The extra bit on the add and subtract keeps the carry and the borrow. The result mux is an 8-way conditional chain, which the tool turns into a mux tree. The flags are the four of section 2.2, computed from the selected result.

A first look in simulation

`timescale 1ns / 1ps
`default_nettype none

module tb_alu_quick;
    reg  [7:0] a, b;
    reg  [2:0] op;
    wire [7:0] result;
    wire       zero, carry, overflow;

    alu_dataflow #(.WIDTH(8)) u_dut (
        .a(a), .b(b), .op(op),
        .result(result), .zero(zero), .carry(carry), .overflow(overflow)
    );

    initial begin
        a = 8'd100; b = 8'd50;  op = 3'd0; #10;    // 150: no carry, no overflow
        $display("ADD: %0d + %0d = %0d | zero=%b carry=%b overflow=%b", a, b, result, zero, carry, overflow);

        a = 8'd100; b = 8'd100; op = 3'd0; #10;    // 200: no carry; as signed, 100 + 100 = -56, overflow
        $display("ADD: %0d + %0d = %0d | zero=%b carry=%b overflow=%b", a, b, result, zero, carry, overflow);

        a = 8'd200; b = 8'd100; op = 3'd0; #10;    // 300 wraps to 44: carry; as signed, -56 + 100 = 44, no overflow
        $display("ADD: %0d + %0d = %0d | zero=%b carry=%b overflow=%b", a, b, result, zero, carry, overflow);

        a = 8'hAA;  b = 8'hF0;  op = 3'd2; #10;
        $display("AND: 0x%h & 0x%h = 0x%h | zero=%b", a, b, result, zero);

        a = 8'hAB;  b = 8'hAB;  op = 3'd4; #10;    // zero flag
        $display("XOR: 0x%h ^ 0x%h = 0x%h | zero=%b", a, b, result, zero);

        $finish;
    end
endmodule
iverilog -o alu_tb tb_alu_quick.v alu_dataflow.v && vvp alu_tb

The two middle additions are worth staring at. $100 + 100$ has no unsigned carry and does have signed overflow. $200 + 100$ has an unsigned carry and no signed overflow. Same adder, two questions.

Practice Exercise

Make the carry flag meaningful for the shifts: on a shift left it should be the bit that fell off the top, on a shift right the bit that fell off the bottom. Then add a negative flag.

Solution
assign carry = (op == 3'd0) ?  add_result[WIDTH] :
               (op == 3'd1) ? ~sub_result[WIDTH] :
               (op == 3'd6) ?  a[WIDTH-1] :          // shifted out of the top
               (op == 3'd7) ?  a[0] :                // shifted out of the bottom
                               1'b0;

assign negative = result[WIDTH-1];

The negative flag is the sign bit of the result, wires only, as in the ALU of Module 2.

The next article is about selection logic in dataflow style, muxes, priority encoders and decoders, and ends with a seven-segment decoder you can put on a board.

โ† Continuous Assignments Muxes, Encoders and Decoders โ†’