Volume 02 Beginner 5 sub-modules ~55 min read

Synthesizable Combinational Logic & Arithmetic Datapaths

Volume 01 was about writing Verilog that means what you think it means. This volume is about architecture: two circuits can compute the same function and differ by an order of magnitude in speed. Every structure here exists because someone refused to accept a delay that grew linearly with width.

The one idea behind this entire volume Naive combinational logic tends to be O(N) deep - a chain. Almost every trick in real datapath design converts that chain into a tree, making it O(log N) deep at the cost of area. Carry lookahead, barrel shifters, Wallace trees, population counters: all the same move.

2.1 Multiplexers, decoders & priority encoders

The multiplexer is the most important primitive in digital design. Not because it is clever, but because everything becomes one: an if statement, a case statement, a ternary operator, a register with an enable. Learn to see MUXes in your RTL and you can estimate area before you ever run synthesis.

Three ways to write the same MUX

All three of these synthesize to identical hardware. Pick based on readability, not on some belief that one is "more efficient".


// ---- Style A: indexed part-select. Scales to any N without editing. ------
module mux_n #(
  parameter W = 8,          // width of each channel
  parameter N = 4           // number of channels
) (
  input  wire [N*W-1:0]       din,   // flattened: { ch3, ch2, ch1, ch0 }
  input  wire [$clog2(N)-1:0] sel,
  output wire [W-1:0]         dout
);
  // "Starting at bit sel*W, take W bits going up." Synthesizes to a MUX.
  assign dout = din[sel*W +: W];
endmodule


// ---- Style B: explicit case. Clearest when channels have real names. ----
module mux4_case #(
  parameter W = 8
) (
  input  wire [W-1:0] d0, d1, d2, d3,
  input  wire [1:0]   sel,
  output reg  [W-1:0] y
);
  always @(*) begin
    case (sel)
      2'd0:    y = d0;
      2'd1:    y = d1;
      2'd2:    y = d2;
      2'd3:    y = d3;
      // sel is 2 bits, so all four values are already covered. The default
      // is unreachable - assigning X tells synthesis "I do not care",
      // which lets it optimise. It also guarantees no latch (Volume 1.5).
      default: y = {W{1'bx}};
    endcase
  end
endmodule


// ---- Style C: structural tree, built from 2:1 primitives ----------------
module mux2 #(
  parameter W = 8
) (
  input  wire [W-1:0] a, b,
  input  wire         s,
  output wire [W-1:0] y
);
  assign y = s ? b : a;
endmodule

module mux4_tree #(
  parameter W = 8
) (
  input  wire [W-1:0] d0, d1, d2, d3,
  input  wire [1:0]   sel,
  output wire [W-1:0] y
);
  wire [W-1:0] lo, hi;

  // Both first-level MUXes evaluate CONCURRENTLY - 2 levels deep, not 3.
  mux2 #(W) u_lo  (.a(d0), .b(d1), .s(sel[0]), .y(lo));
  mux2 #(W) u_hi  (.a(d2), .b(d3), .s(sel[0]), .y(hi));
  mux2 #(W) u_out (.a(lo), .b(hi), .s(sel[1]), .y(y));
endmodule

Tree vs cascade: same gate count, different depth

Here is where architecture starts to matter. A 4:1 MUX built from three 2:1 MUXes uses three MUXes either way - but wiring them as a chain makes the path three levels deep, while wiring them as a tree makes it two. At 4:1 that is a rounding error. At 64:1 it is 63 levels versus 6.

A 4-to-1 multiplexer built as a balanced tree compared with the same multiplexer built as a serial cascade TREE - 2 LEVELS DEEP d0 d1 d2 d3 y sel[0] sel[0] sel[1] delay = 2 x t_mux N:1 costs log2(N) levels CASCADE - 3 LEVELS DEEP d0 d1 sel[0] d2 sel[1] d3 sel[2] y delay = 3 x t_mux N:1 costs N-1 levels
Figure 2.1 - Three MUXes either way. The tree finishes in two gate delays; the cascade needs three because d0 must physically pass through every MUX. Scale to 64:1 and the gap becomes 6 levels versus 63.
Does synthesis fix a cascade for you? For a plain MUX described behaviourally, usually yes - the tool is free to rebalance. But the moment you write a priority structure (an if/else if chain), you have specified that earlier conditions win, and that ordering constraint is real logic the tool must preserve. Priority costs depth. If your conditions are mutually exclusive, use case and say so.

Priority encoders

A priority encoder answers "which is the highest-numbered active request?" - the core of every interrupt controller and bus arbiter. The casez construct expresses it almost literally, with ? meaning "don't care".


module priority_encoder_8to3 (
  input  wire [7:0] req,
  output reg  [2:0] grant,
  output wire       valid    // 0 => no request, grant is meaningless
);

  assign valid = |req;       // OR-reduction: any bit set?

  always @(*) begin
    // casez treats ? in the PATTERN as don't-care. The first matching
    // branch wins, which is exactly the priority semantics we want.
    casez (req)
      8'b1???????: grant = 3'd7;   // bit 7 highest priority
      8'b01??????: grant = 3'd6;
      8'b001?????: grant = 3'd5;
      8'b0001????: grant = 3'd4;
      8'b00001???: grant = 3'd3;
      8'b000001??: grant = 3'd2;
      8'b0000001?: grant = 3'd1;
      8'b00000001: grant = 3'd0;
      default:     grant = 3'd0;   // req == 0; `valid` tells you to ignore it
    endcase
  end

endmodule
Use casez, never casex casex treats X in the input as a wildcard too. One uninitialised bit arriving at a casex can match a branch it has no business matching, and the mismatch between simulation and synthesized hardware is brutal to debug. casez only wildcards Z, which effectively never appears in internal logic. Industry style guides ban casex outright.
Interview grilling - "Build a 4:1 MUX from 2:1 MUXes. Then build a 2:1 MUX from NAND gates only."

Part 1 - 4:1 from 2:1. Three MUXes in a tree, as in Figure 2.1: two select between {d0,d1} and {d2,d3} using sel[0], and the third picks between those results using sel[1]. Say "two levels deep" out loud - that is the half they are actually testing.

Part 2 - 2:1 from NAND. Start from the Boolean expression:

2:1 multiplexer Y = (A · ~S) + (B · S)

Apply De Morgan to turn the OR of two ANDs into a NAND of two NANDs:

Y = NAND( NAND(A, ~S) , NAND(B, S) )

And ~S = NAND(S, S). So the answer is four NAND gates: one to invert S, two for the product terms, one to combine them.


module mux2_nand (input wire a, b, s, output wire y);
  wire s_n, n1, n2;

  nand (s_n, s,  s);    // inverter built from a NAND
  nand (n1,  a,  s_n);
  nand (n2,  b,  s);
  nand (y,   n1, n2);
endmodule

The follow-up: "Why does anyone care about NAND-only?" Because NAND is functionally complete and, in static CMOS, cheaper than AND or OR - an AND gate is physically a NAND followed by an inverter. Standard cell libraries are NAND/NOR heavy for exactly this reason.

2.2 Adders, subtractors & carry propagation networks

Addition is the hardest cheap thing in digital design. The logic per bit is trivial; the problem is that bit i cannot finish until it knows the carry from bit i-1. That dependency is the whole subject.

The full adder

Full adder S = A ⊕ B ⊕ Cin
Cout = (A · B) + (Cin · (A ⊕ B))

The Cout form above deliberately reuses A ⊕ B, which the sum already needs - sharing that term is why the standard cell version is compact.


module full_adder (
  input  wire a, b, cin,
  output wire sum, cout
);
  assign sum  = a ^ b ^ cin;
  assign cout = (a & b) | (cin & (a ^ b));
endmodule


module ripple_carry_adder #(
  parameter W = 8
) (
  input  wire [W-1:0] a, b,
  input  wire         cin,
  output wire [W-1:0] sum,
  output wire         cout
);
  wire [W:0] c;
  assign c[0] = cin;

  genvar i;
  generate
    for (i = 0; i < W; i = i + 1) begin : g_bit
      full_adder u_fa (
        .a    (a[i]),
        .b    (b[i]),
        .cin  (c[i]),      // <-- THE PROBLEM: stage i waits for stage i-1
        .sum  (sum[i]),
        .cout (c[i+1])
      );
    end
  endgenerate

  assign cout = c[W];
endmodule

Simple, tiny, and slow. The critical path runs from a[0] all the way to cout, through every stage. Delay is O(N). A 64-bit ripple carry adder is roughly 128 gate delays - utterly unusable at any serious clock frequency.

Carry lookahead: computing all carries at once

The insight is that each bit position can decide two things without knowing its incoming carry:

Per-bit signals, all computed in parallel Gi = Ai · Bi     (this bit generates a carry regardless)
Pi = Ai ⊕ Bi     (this bit propagates an incoming carry)

Every G and P depends only on the inputs, so all of them settle after one gate delay, simultaneously. Now the carry recurrence Ci+1 = Gi + Pi·Ci can be unrolled into flat expressions with no chaining at all:

C1 = G0 + P0C0
C2 = G1 + P1G0 + P1P0C0
C3 = G2 + P2G1 + P2P1G0 + P2P1P0C0
C4 = G3 + P3G2 + P3P2G1 + P3P2P1G0 + P3P2P1P0C0
Ripple carry adder serial carry chain compared with a carry lookahead adder computing all carries in parallel RIPPLE CARRY - CARRY MUST TRAVEL THROUGH EVERY STAGE FA3 FA2 FA1 FA0 c0 c4 c1c2c3 s0s1 s2s3 critical path = N carry delays → O(N) CARRY LOOKAHEAD - EVERY CARRY DERIVED IN PARALLEL G0 P0 G1 P1 G2 P2 G3 P3 all four settle simultaneously - 1 gate delay CARRY LOOKAHEAD NETWORK c1c2 c3c4 O(log N) depth
Figure 2.2 - The ripple carry adder's carry is a serial dependency (crimson). The carry lookahead adder breaks it: G and P depend only on the inputs, so every carry is a flat sum-of-products that resolves in a fixed number of gate levels no matter how wide the adder gets.

module cla_4bit (
  input  wire [3:0] a, b,
  input  wire       cin,
  output wire [3:0] sum,
  output wire       cout
);

  wire [3:0] g = a & b;     // generate: this bit makes a carry on its own
  wire [3:0] p = a ^ b;     // propagate: this bit passes a carry through
  wire [4:0] c;

  assign c[0] = cin;

  // Each carry is a FLAT expression of g, p and cin - no chaining.
  // Every one of these four lines evaluates at the same time.
  assign c[1] = g[0] | (p[0] & c[0]);

  assign c[2] = g[1] | (p[1] & g[0])
                     | (p[1] & p[0] & c[0]);

  assign c[3] = g[2] | (p[2] & g[1])
                     | (p[2] & p[1] & g[0])
                     | (p[2] & p[1] & p[0] & c[0]);

  assign c[4] = g[3] | (p[3] & g[2])
                     | (p[3] & p[2] & g[1])
                     | (p[3] & p[2] & p[1] & g[0])
                     | (p[3] & p[2] & p[1] & p[0] & c[0]);

  // Once the carries exist, the sums are one XOR each.
  assign sum  = p ^ c[3:0];
  assign cout = c[4];

endmodule
Why nobody builds a flat 64-bit CLA Look at c[4]: five product terms, the widest with five inputs. Extend the pattern to bit 63 and you need a 64-input AND gate, and p[0] must drive 64 separate loads. Both are physically impossible. Real adders build CLA in blocks - 4-bit groups with their own group-generate and group-propagate, then a second level of lookahead across the groups. That hierarchy is what actually delivers the log-depth.

The adder family in practice

Architecture Delay Area Where it is used
Ripple Carry O(N) Smallest Narrow adders, non-critical paths, counters
Carry Select O(√N) ~2× Computes both carry-in cases, MUXes the winner
Carry Lookahead O(log N) Large The classic textbook fast adder
Kogge-Stone O(log N), minimum depth Largest - heavy wiring High-frequency CPU datapaths
Brent-Kung O(log N), ~2× depth of K-S Much smaller than K-S Area- and power-sensitive designs
What you should actually write Write assign sum = a + b;. Modern synthesis tools have every one of these architectures in their arithmetic library and will pick one based on your timing constraint - ripple if the path is slack, Kogge-Stone if you are pushing the clock. Hand instantiating a CLA is almost always a mistake. Know the structures so you can read timing reports and explain them in interviews, not so you can type them out.
Interview grilling - "Turn your adder into a subtractor without adding a second adder."

Two's complement negation is "invert every bit, then add one". The adder already has a spare input that can supply that one: cin.

A - B = A + (~B) + 1

module add_sub #(parameter W = 8) (
  input  wire [W-1:0] a, b,
  input  wire         sub,      // 0 = add, 1 = subtract
  output wire [W-1:0] result,
  output wire         cout
);
  wire [W-1:0] b_in = b ^ {W{sub}};   // XOR with 1 inverts, with 0 passes
  assign {cout, result} = a + b_in + sub;
endmodule

The cost is W XOR gates, nothing more. Note the trick on line 7: b ^ {W{sub}} - replicating the control bit to full width turns a conditional inversion into a single XOR, no MUX required.

Say this too: "For subtraction, cout is a not-borrow flag - it is 1 when A ≥ B unsigned. That is exactly how the unsigned less-than comparison gets computed for free."

2.3 Arithmetic logic units & barrel shifters

An ALU is a MUX wrapped around an adder. The interesting engineering is not the operation list - it is sharing one expensive adder across four different operations, and getting the condition flags right.

Overflow: the flag everyone gets wrong

Cout tells you about unsigned overflow. It says nothing useful about signed arithmetic. Signed overflow has its own condition:

Signed overflow, two equivalent forms V = Cin(MSB) ⊕ Cout(MSB)
V = (Amsb = Bmsb) · (Smsb ≠ Amsb)

The second form is the intuitive one: adding two numbers of the same sign must produce that sign. If it does not, the result wrapped around. Adding numbers of opposite signs can never overflow, which is why the condition requires the signs to match.

4-bit example Binary Cout V Reading
7 + 1 = 8 0111 + 0001 = 1000 0 1 Signed overflow (+7+1 gave -8); unsigned fine
15 + 1 = 16 1111 + 0001 = 0000 1 0 Unsigned overflow; signed fine (-1+1 = 0)
3 + 2 = 5 0011 + 0010 = 0101 0 0 Correct in both interpretations
The line that wins interviews "The adder hardware is identical for signed and unsigned - two's complement is designed so that one adder serves both. Signedness only exists in how you interpret the result, which is why the ALU produces Cout and V as separate flags and lets the instruction decide which one matters."

A 32-bit RISC-V style ALU


module alu #(
  parameter W = 32
) (
  input  wire [W-1:0] a,
  input  wire [W-1:0] b,
  input  wire [3:0]   op,
  output reg  [W-1:0] y,
  output wire         zero,
  output wire         negative,
  output wire         carry,
  output wire         overflow
);

  localparam ALU_ADD  = 4'd0, ALU_SUB  = 4'd1, ALU_SLL = 4'd2,
             ALU_SLT  = 4'd3, ALU_SLTU = 4'd4, ALU_XOR = 4'd5,
             ALU_SRL  = 4'd6, ALU_SRA  = 4'd7, ALU_OR  = 4'd8,
             ALU_AND  = 4'd9;

  localparam SHW = $clog2(W);          // 5 bits of shift amount for W = 32

  // ---- ONE shared adder serves ADD, SUB, SLT and SLTU ------------------
  wire is_sub = (op == ALU_SUB) | (op == ALU_SLT) | (op == ALU_SLTU);

  // Conditional inversion, no MUX: replicate the control bit and XOR.
  wire [W-1:0] b_in  = b ^ {W{is_sub}};
  wire [W:0]   sum_x = {1'b0, a} + {1'b0, b_in} + is_sub;
  wire [W-1:0] sum   = sum_x[W-1:0];

  assign carry    = sum_x[W];
  assign overflow = (a[W-1] == b_in[W-1]) & (sum[W-1] != a[W-1]);

  // Both comparisons fall out of the subtraction for free.
  wire lt_signed   = sum[W-1] ^ overflow;   // two's complement less-than
  wire lt_unsigned = ~carry;                // no carry out == borrow

  wire [SHW-1:0] shamt = b[SHW-1:0];

  always @(*) begin
    case (op)
      ALU_ADD, ALU_SUB : y = sum;
      ALU_SLL          : y = a << shamt;
      ALU_SLT          : y = {{(W-1){1'b0}}, lt_signed};
      ALU_SLTU         : y = {{(W-1){1'b0}}, lt_unsigned};
      ALU_XOR          : y = a ^ b;
      ALU_SRL          : y = a >> shamt;              // logical: shifts in 0
      ALU_SRA          : y = $signed(a) >>> shamt;    // arithmetic: sign extends
      ALU_OR           : y = a | b;
      ALU_AND          : y = a & b;
      default          : y = {W{1'b0}};   // every path assigns y -> no latch
    endcase
  end

  assign zero     = (y == {W{1'b0}});
  assign negative = y[W-1];

endmodule
>>> only sign-extends on a signed operand Writing a >>> shamt where a is a plain wire [31:0] performs a logical shift - the operand is unsigned, so there is no sign to extend, and the >>> silently behaves like >>. You must write $signed(a) >>> shamt, as above. This is one of the most common silent bugs in student ALUs.

The barrel shifter

A shift-by-k where k is a runtime value cannot be wires alone - it needs a real circuit. The naive approach shifts one position per clock, taking up to N cycles. A barrel shifter does any shift in a single cycle by decomposing the shift amount into its binary digits and building one stage per digit.

An 8-bit logarithmic barrel shifter performing a right shift by five as three cascaded stages of four, two and one RIGHT SHIFT BY 5 - shamt = 101 binary din 1101 0110 = 214 decimal STAGE 2 shift by 4? shamt[2]=1 YES 8 two-to-one MUXes 0000 1101 STAGE 1 shift by 2? shamt[1]=0 NO pass straight through 0000 1101 STAGE 0 shift by 1? shamt[0]=1 YES 0000 0110 = 6
Figure 2.3 - Shifting by 5 = 4 + 1 costs three stages, not five steps. Each stage either shifts by its own power of two or passes through, controlled by one bit of shamt. Total cost: log₂(N) stages of N MUXes, and the delay never depends on how far you shift.

module barrel_shift_right #(
  parameter W   = 8,
  parameter SHW = 3            // $clog2(W)
) (
  input  wire [W-1:0]   din,
  input  wire [SHW-1:0] shamt,
  input  wire           arith,  // 1 = arithmetic (replicate sign), 0 = logical
  output wire [W-1:0]   dout
);

  wire fill = arith & din[W-1];   // what gets shifted in at the top
  wire [W-1:0] stage [0:SHW];     // stage[0] = input, stage[SHW] = output

  assign stage[0] = din;

  genvar i;
  generate
    for (i = 0; i < SHW; i = i + 1) begin : g_stage
      // Stage i shifts by exactly 2**i, or not at all. (1<<i) is an
      // elaboration-time constant, so this is a plain MUX per bit.
      assign stage[i+1] = shamt[i]
        ? { {(1<<i){fill}}, stage[i][W-1 : (1<<i)] }
        : stage[i];
    end
  endgenerate

  assign dout = stage[SHW];

endmodule
The same structure gives you a rotator Replace the fill bits with the bits that fall off the bottom and the barrel shifter becomes a barrel rotator - the primitive behind ROR/ROL instructions and every hash and crypto round function. One structure, three instructions.

2.4 Multipliers & divider architectures

Multiplication is addition done N times in parallel. The naive array multiplier generates N partial products and sums them - correct, but both large and slow. Two independent optimisations attack it: Booth encoding reduces how many partial products exist, and Wallace trees reduce how long they take to add.

Booth's algorithm: fewer partial products

Booth's insight is that a run of consecutive 1s can be replaced by one addition and one subtraction. 0111 (7) is 1000 - 0001 (8 - 1). Radix-2 Booth examines each bit alongside the bit to its right:

bi bi-1 Action Meaning
00no operationInside a run of zeros
01A = A + MEnd of a run of ones
10A = A - MStart of a run of ones
11no operationInside a run of ones
Booth handles signed operands for free This is the real reason it survives. Because the subtraction case naturally produces the two's complement behaviour at the sign bit, a Booth multiplier works on signed numbers with no special casing at all - unlike a plain array multiplier, which needs sign-extension logic bolted on.

module booth_multiplier #(
  parameter W = 8
) (
  input  wire                  clk,
  input  wire                  rst_n,
  input  wire                  start,
  input  wire signed [W-1:0]   multiplicand,
  input  wire signed [W-1:0]   multiplier,
  output wire signed [2*W-1:0] product,
  output reg                   busy,
  output reg                   done
);

  reg signed [W-1:0]  a;      // accumulator (high half of the result)
  reg signed [W-1:0]  q;      // multiplier, becomes the low half
  reg                 q_1;    // the phantom bit to the right of q
  reg signed [W-1:0]  m;      // multiplicand
  reg [$clog2(W):0]   count;

  // Combinational: decide this step's operation from the Booth pair.
  reg signed [W-1:0] a_next;
  always @(*) begin
    case ({q[0], q_1})
      2'b01:   a_next = a + m;   // end of a run of ones   -> add
      2'b10:   a_next = a - m;   // start of a run of ones -> subtract
      default: a_next = a;       // 00 or 11               -> nothing
    endcase
  end

  assign product = {a, q};

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      a <= 0; q <= 0; q_1 <= 1'b0; m <= 0;
      count <= 0; busy <= 1'b0; done <= 1'b0;
    end else begin
      done <= 1'b0;                       // single-cycle pulse

      if (start && !busy) begin
        a     <= 0;
        q     <= multiplier;
        q_1   <= 1'b0;
        m     <= multiplicand;
        count <= W;
        busy  <= 1'b1;
      end else if (busy) begin
        // ARITHMETIC shift right of the whole {a, q, q_1} register.
        // a's MSB is replicated - that is what keeps signs correct.
        a     <= {a_next[W-1], a_next[W-1:1]};
        q     <= {a_next[0],   q[W-1:1]};
        q_1   <= q[0];
        count <= count - 1'b1;

        if (count == 1) begin
          busy <= 1'b0;
          done <= 1'b1;
        end
      end
    end
  end

endmodule
Trace it by hand - 3 × (-2) with W = 4

M = 0011 (+3), Q = 1110 (-2). Expected product: -6.

Step q[0] q_1 Action A after shift Q after shift q_1
init - load000011100
10 0none000001110
21 0A - M111010111
31 1none111101011
41 1none111110101

Product = {A, Q} = 1111_1010. Interpreting as signed 8-bit: -6. ✅ Note that only one arithmetic operation ran in four steps - a plain array multiplier would have done four.

Radix-4 modified Booth: half the partial products

Production multipliers use radix-4, which inspects three bits at a time (overlapping by one) and processes two multiplier bits per step. That halves the partial product count from N to N/2 - the single biggest area win available.

b2i+1 b2i b2i-1 Partial product How it is formed
000, 1110Zero - no hardware cost
001, 010+MThe multiplicand as-is
011+2MShift left by 1 - free, just wiring
100-2MShift left, then invert and add 1
101, 110-MTwo's complement of the multiplicand
Why radix-4 and not radix-8 Every multiple in the radix-4 table is 0, ±1× or ±2× - and ±2× is just a shifted wire. Radix-8 would need ±3M, which is not a shift; it requires a real adder before the multiplier even starts. That extra adder usually costs more than the partial product it saves. Radix-4 is the sweet spot because 2 is a power of two and 3 is not.

Wallace trees: adding the partial products faster

Once you have the partial products, summing them with a chain of adders is O(N) again. A Wallace tree instead uses 3:2 compressors - ordinary full adders used as counters - to squash three rows into two, repeatedly, until only two remain. Those final two go into one fast carry-propagate adder.

Wallace tree reduction of eight partial products to two rows using three to two compressors WALLACE TREE REDUCTION - 8 PARTIAL PRODUCTS 8 rows partial products 3:2 compressors 6 4 level 1 level 2 2 → one fast CPA Each level cuts the row count by a factor of 3/2, so the depth is O(log N)
Figure 2.4 - A 3:2 compressor is just a full adder used as a counter: it takes three bits of equal weight and outputs a sum and a carry. Stacking them reduces 8 rows → 6 → 4 → 3 → 2 in four levels rather than seven sequential additions.

Division: the expensive one

Division has no Booth-style shortcut. The quotient bits are genuinely sequential - you cannot know bit i without having computed bit i+1. Restoring division is the direct hardware form of long division:


module divider_restoring #(
  parameter W = 8
) (
  input  wire         clk,
  input  wire         rst_n,
  input  wire         start,
  input  wire [W-1:0] dividend,
  input  wire [W-1:0] divisor,
  output wire [W-1:0] quotient,
  output wire [W-1:0] remainder,
  output reg          busy,
  output reg          done
);

  reg [W-1:0]       q;      // holds the dividend, becomes the quotient
  reg [W-1:0]       d;      // divisor
  reg [W:0]         r;      // remainder, one extra bit for the trial subtract
  reg [$clog2(W):0] count;

  // Shift the next dividend bit into the remainder, then TRY to subtract.
  wire [W:0] shifted = {r[W-1:0], q[W-1]};
  wire [W:0] diff    = shifted - {1'b0, d};

  assign quotient  = q;
  assign remainder = r[W-1:0];

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      r <= 0; q <= 0; d <= 0; count <= 0; busy <= 1'b0; done <= 1'b0;
    end else begin
      done <= 1'b0;

      if (start && !busy) begin
        r <= 0; q <= dividend; d <= divisor;
        count <= W; busy <= 1'b1;
      end else if (busy) begin
        if (diff[W] == 1'b0) begin      // no borrow: the subtraction fit
          r <= diff;
          q <= {q[W-2:0], 1'b1};        // quotient bit is 1
        end else begin                  // borrow: RESTORE the old remainder
          r <= shifted;
          q <= {q[W-2:0], 1'b0};        // quotient bit is 0
        end

        count <= count - 1'b1;
        if (count == 1) begin busy <= 1'b0; done <= 1'b1; end
      end
    end
  end

endmodule
Never write a / b in RTL Synthesis will accept it and build a full combinational divider - an enormous, painfully slow block that will single-handedly destroy your timing. Division by a constant power of two is fine (it is a shift). Everything else needs an explicit multi-cycle divider like the one above, or a reciprocal multiply. This is one of the fastest ways to blow a design review.

2.5 Comparators & parity generators

Two small structures that show up constantly, and one interview question that is asked almost every single time.

Magnitude comparators


module comparator #(
  parameter W      = 8,
  parameter SIGNED = 0
) (
  input  wire [W-1:0] a, b,
  output wire         gt, eq, lt
);

  assign eq = (a == b);

  generate
    if (SIGNED) begin : g_signed
      assign lt = ($signed(a) <  $signed(b));
      assign gt = ($signed(a) >  $signed(b));
    end else begin : g_unsigned
      assign lt = (a < b);
      assign gt = (a > b);
    end
  endgenerate

endmodule

Write it this way. Synthesis implements a < b as a subtractor and looks at the borrow - the same silicon you would have built by hand, but the tool gets to choose the adder architecture based on your timing constraint.

Equality is much cheaper than magnitude a == b is an XNOR per bit feeding one AND tree - O(log N) depth and no carry chain at all. a < b needs a full subtraction. If you only need to know whether two values differ, never write <.

Parity trees


module parity #(
  parameter W = 32
) (
  input  wire [W-1:0] data,
  output wire         even_parity_bit,  // append this to make the total even
  output wire         odd_parity_bit    // append this to make the total odd
);

  // ^ is XOR-reduction: 1 when data contains an ODD number of ones.
  // Synthesis builds a balanced XOR tree - log2(W) deep, not W.
  assign even_parity_bit = ^data;
  assign odd_parity_bit  = ~^data;

endmodule
What parity can and cannot do A single parity bit detects any odd number of bit flips and is blind to every even number - two flipped bits look identical to none. It also cannot correct anything. Real memories use SECDED ECC (single error correct, double error detect), typically Hamming plus one overall parity bit. Parity alone is for cheap link checks, not for protecting data you care about.
Interview grilling - "Count the 1s in a 32-bit vector with minimum logic delay."

This is the population count question, and it is asked constantly. The wrong answer is a loop that accumulates into a running total - that describes a 32-deep adder chain. The right answer is a balanced adder tree.

Population count implemented as a five level balanced adder tree 32 input bits 16 sums, 2 bits each 8 sums, 3 bits each 4 sums, 4 bits each 2 sums, 5 bits each 1 result, 6 bits 5 levels = log2(32) not 32
Figure 2.5 - Pair, add, repeat. Every level runs fully in parallel.

module popcount32 (
  input  wire [31:0] data,
  output wire [5:0]  ones
);

  wire [1:0] s1 [0:15];   // level 1: 16 two-bit sums
  wire [2:0] s2 [0:7];    // level 2:  8 three-bit sums
  wire [3:0] s3 [0:3];    // level 3:  4 four-bit sums
  wire [4:0] s4 [0:1];    // level 4:  2 five-bit sums

  genvar i;
  generate
    for (i = 0; i < 16; i = i + 1) begin : g_l1
      assign s1[i] = data[2*i] + data[2*i+1];
    end
    for (i = 0; i < 8;  i = i + 1) begin : g_l2
      assign s2[i] = s1[2*i] + s1[2*i+1];
    end
    for (i = 0; i < 4;  i = i + 1) begin : g_l3
      assign s3[i] = s2[2*i] + s2[2*i+1];
    end
    for (i = 0; i < 2;  i = i + 1) begin : g_l4
      assign s4[i] = s3[2*i] + s3[2*i+1];
    end
  endgenerate

  assign ones = s4[0] + s4[1];   // level 5

endmodule

The sentence that closes it: "Five levels instead of thirty-two, because the tree is balanced. And notice the widths only grow as needed - 1, 2, 3, 4, 5, 6 bits - so the early levels are nearly free."

Expect the follow-up: "What if you had to do this every cycle at 2 GHz?" Then you pipeline it - insert flops between the tree levels. Five levels makes for a natural 2-or-3-stage pipeline. Say that unprompted and you have answered the question they were about to ask.

Volume 02 recap

Structure The one thing to remember
MUX tree vs cascade Same gate count, log(N) depth vs N depth. Priority chains cost depth on purpose.
casez Use it for priority encoders. Never casex - it wildcards X on inputs.
Carry lookahead G and P depend only on inputs, so every carry resolves in parallel.
Subtraction a + (b ^ {W{sub}}) + sub - one XOR row, no second adder.
Overflow V = Cin ⊕ Cout at the MSB. Cout alone is unsigned-only.
Barrel shifter One stage per bit of shamt. Delay is independent of shift distance.
>>> Only sign-extends on a $signed() operand. Otherwise it is just >>.
Booth radix-4 Halves partial products; every multiple is 0, ±1× or ±2× - all free shifts.
Division Never write a / b in RTL unless the divisor is a constant power of two.
Population count Balanced adder tree: log₂(N) levels, not N.