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