Skip to content

RTL Design Patterns · Chapter 5 · Arithmetic & Datapath Patterns

Multipliers

A single multiply operator becomes one of the largest, slowest structures in your block: an array of partial-product adders that grows with the square of the operand width, or a hard DSP block on an FPGA. The multiply is where two arithmetic lessons turn unforgiving. An N-bit number times an M-bit number needs N-plus-M bits of result, and if you declare the product any narrower the tool silently discards the top half, so the circuit is correct for small operands and quietly wrong at scale. Get the signedness wrong and the products are enormous and meaningless. You learn what the operator actually infers, why the product width and signedness must match the operands, and the trade-off between a wide combinational multiplier, a pipelined one, and a small sequential shift-add multiplier, all built and self-check-verified in SystemVerilog, Verilog, and VHDL.

Intermediate14 min readRTL Design PatternsMultiplierDatapathSigned ArithmeticProduct WidthShift-AddDSP Inference

Chapter 5 · Section 5.2 · Arithmetic & Datapath Patterns

1. The Engineering Problem

You are building the scaling stage of a small DSP datapath: each sample x arrives 16 bits wide, and you multiply it by a 16-bit coefficient k before it goes on to an accumulator. The RTL is one line — product = x * k — and in the first block-level test, with hand-picked small numbers like 3 * 5 and 100 * 7, every result is exactly right. The design passes review. Weeks later, in a system run with real signal data, the filter output is garbage on loud passages and fine on quiet ones. The bug is not in the accumulator, not in the coefficients, and not in the testbench's expected values. It is in that one line.

The problem is that a multiply grows the word, and by more than addition does. Adding two 16-bit numbers needs 17 bits — one carry-out bit. But multiplying two 16-bit numbers needs 32 bits: the product of an N-bit and an M-bit number occupies up to N+M bits, because the largest 16-bit value is about 65 000 and 65 000 × 65 000 is about 4.3 billion, which does not fit in 16 bits or in 17 — it needs 32. If you declared product as 16 bits (to "match the inputs"), the synthesis tool computed the full 32-bit result internally and then threw away the top 16 bits. Small operands whose true product fits in 16 bits pass; large operands overflow silently and the retained low half is meaningless. That is exactly the loud-passage failure: big samples times big coefficients overflow the truncated product.

Two forces converge here and both are traps. The first is width: * needs an N+M-bit destination or it truncates, and the truncation is invisible — it lints clean, it simulates clean on small numbers, and it only shows up at scale. The second is signedness: if x and k are signed but you multiply them with an unsigned operator, the tool treats a value like -3 as the huge unsigned number 65 533, and the "product" is nonsense. And behind both is cost: that one line is not free — at 16 bits it is a large array of adders and, at wide widths, one of the slowest paths in your design. This section is about getting all three right.

the-need.v — one line, three traps: width, signedness, cost
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // A DSP scaling stage: a 16-bit sample times a 16-bit coefficient.
   wire signed [15:0] x;      // sample  (SIGNED — can be negative)
   wire signed [15:0] k;      // coeff   (SIGNED)
 
   // WRONG — product declared operand-width: the top 16 bits are DISCARDED.
   //   wire signed [15:0] product = x * k;   // silent overflow at scale
   //   wire        [31:0] product = x * k;   // UNSIGNED dest of SIGNED data: wrong
 
   // RIGHT — full N+M = 16+16 = 32-bit product, SIGNED to match the operands:
   //   wire signed [31:0] product = x * k;   // no truncation, correct sign
   //
   // And it is not free: this `*` infers a 16x16 partial-product ARRAY
   // (or one FPGA DSP block) — one of the widest paths in the block.

2. Mental Model

Hold this model — a multiply is an adder array that grows the word to N+M and must match signedness — and the rest is mechanical. A shift-add multiplier is nothing but the repeated addition from 5.1 done N times, controlled by an FSM (4.6). On an FPGA the whole array collapses into one hard DSP block the tool infers from your *. And in every HDL the two rules are identical: SystemVerilog, Verilog, and VHDL all need the N+M-wide destination and all need the operand signedness stated explicitly.

3. Pattern Anatomy

The multiplier family is one arithmetic structure with three implementation shapes and two typing decisions.

The partial-product array — what * actually is. Long multiplication: for each bit b[j] of one operand, form the partial product a & {WIDTH{b[j]}} (the whole of a, or zero, depending on b[j]), shift it left by j, and sum all the partial products. For an N×M multiply that is N (or M) partial products fed into a tree of adders. This is why a multiplier is a long combinational path: the summing tree has depth on the order of log of the number of partial products, and the whole thing is wide — so both area and delay grow with operand width (area roughly as N×M, delay more slowly but relentlessly). A 32×32 multiply is not a gate; it is a small block.

The N+M product width — the signature trap. A partial product for an M-bit operand is at most N bits, shifted up by up to M-1 places, so the summed result occupies up to N+M bits. 8 × 8 → 16, 16 × 16 → 32, 18 × 18 → 36. Size the destination narrower and the low N+M bits you keep are only correct when the true product happened to fit — the trap in §1. Widths must be deliberate: name the product [AW+BW-1:0] and it can never overflow.

Signed vs unsigned — the two arrays. Unsigned multiply treats every partial product as positive. Signed (two's-complement) multiply must sign-extend the operands and subtract the partial products that correspond to the sign bit — it is a structurally different array. So the operands' signedness is load-bearing: in SystemVerilog and Verilog you force it with $signed(...) (and signed nets); in VHDL you type the operands as signed or unsigned from numeric_std and resize deliberately. Mixing a signed and an unsigned operand needs explicit handling — sign-extend the signed one into the unsigned domain (or vice versa) on purpose; never let the tool guess.

The multiplier family — one array, three shapes, two typings

data flow
The multiplier family — one array, three shapes, two typingspartial-productarrayAND-grid + adder tree; area ~ N*MN+M product widthAW+BW bits or it truncatessigned vsunsignedtwo different arrays; $signed / numeric_stdcombinational1 cycle, biggest area, longest pathpipelinedhigh throughput, +latency, +regssequentialshift-addan FSMD: N cycles, tiny area
Everything is the partial-product array. Its output is N+M bits (AW+BW) and its structure depends on signedness — get either wrong and the result is silently corrupt. The same array can be realized three ways: as one big combinational block (one cycle, largest area, longest path); pipelined (register the adder-tree ranks for high throughput at the cost of latency and flops); or as a sequential shift-add (an FSM+datapath doing repeated addition over N cycles — tiny area, low throughput). On an FPGA the whole combinational array is usually swallowed by one hard DSP block. Language-independent: the same width and signedness rules hold in SystemVerilog, Verilog, and VHDL.

The three implementation shapes — the area/throughput/latency trade-off. The same product = a * b maps to three very different circuits:

  • Combinational — the full array in one cycle. Lowest latency (one cycle), highest area, and the longest combinational path — at wide widths it can be the critical path. This is what a bare * infers.
  • Pipelined — the same array with registers inserted between ranks of the adder tree. A new multiply can start every cycle (high throughput) and the clock can run fast, but the result appears several cycles later (latency) and you pay the flops. This is what you want in a streaming datapath.
  • Sequential shift-add — an FSMD (Chapter 4.6): a controller plus a datapath that, over N cycles, examines one multiplier bit per cycle, conditionally adds the (shifted) multiplicand into an accumulator, and shifts. It is repeated addition — exactly the adder of 5.1 reused N times. Tiny area (one adder, some registers, a small FSM), but it takes N cycles per multiply. The classic trade: area vs throughput vs latency.

FPGA DSP-block inference. On an FPGA, a * (up to the DSP's native width, e.g. 18×18 or 27×18) is usually not built from fabric LUTs — the synthesis tool infers a hard DSP block (Xilinx DSP48, Intel DSP). This is faster and smaller than a LUT multiplier, but the DSP has inference constraints: it wants specific operand widths, it has optional built-in input/output pipeline registers you must place to hit timing, and if you exceed its native width the tool stitches several DSPs together (or falls back to fabric). Knowing those constraints is the difference between one DSP at high clock and a fabric multiplier that misses timing.

4. Real RTL Implementation

This is the core of the page. We build three things — (a) a combinational parameterized multiplier (unsigned and signed, full N+M product, explicit signedness), (b) a sequential shift-add multiplier (an FSMD — controller + shift/accumulate datapath, clocked), and (c) the product-width truncation bug (buggy operand-width product vs fixed N+M product) — each in SystemVerilog, Verilog, and VHDL, with an RTL block and a self-checking testbench. The testbenches check the full N+M-bit product against a reference across random and boundary operands, including the signed corners (-1 × -1, min × -1) that a naive test skips.

Two disciplines run through every block: the product is always declared N+M bits wide (AW+BW — never operand-width), and signedness is always explicit ($signed/signed nets in SV/Verilog; signed/unsigned from numeric_std with deliberate resize in VHDL). Miss the first and you get pattern (c)'s silent overflow; miss the second and a signed operand multiplies as a giant unsigned one.

4a. Combinational multiplier — unsigned and signed, full N+M product

Start with the everyday form: a bare *, sized correctly. The only real content is the two disciplines — the product is AW+BW bits, and the operands' signedness is stated. We show the unsigned and signed variants side by side because they are different hardware and the only source-level difference is the types.

mult_comb.sv — combinational N+M multiply, UNSIGNED and SIGNED variants
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // UNSIGNED: product is AW+BW bits, operands and result all unsigned.
   module mult_comb_u #(
       parameter int AW = 16,                         // width of a
       parameter int BW = 16                          // width of b
   )(
       input  logic [AW-1:0]      a,
       input  logic [BW-1:0]      b,
       output logic [AW+BW-1:0]   product             // N+M bits — cannot overflow
   );
       // Bare `*` infers the partial-product ARRAY (one DSP on FPGA). The dest is
       // AW+BW wide, so the full product is kept — no truncation.
       assign product = a * b;
   endmodule
 
   // SIGNED: same array shape logically, but two's-complement. Signedness is
   // EXPLICIT — signed ports AND a $signed multiply — so -3 stays -3, not 65533.
   module mult_comb_s #(
       parameter int AW = 16,
       parameter int BW = 16
   )(
       input  logic signed [AW-1:0]    a,
       input  logic signed [BW-1:0]    b,
       output logic signed [AW+BW-1:0] product        // SIGNED N+M result
   );
       // $signed() forces the signed array even if a/b were unsigned nets.
       assign product = $signed(a) * $signed(b);
   endmodule

The testbench checks the full product against SystemVerilog's own arithmetic used as the reference, sweeping random operands plus the boundary set — 0, 1, max — and, for the signed DUT, the signed corners -1 × -1 (must be +1), min × -1 (the value that overflows a naive product), and max × min.

mult_comb_tb.sv — self-check full N+M product over random + signed corners
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mult_comb_tb;
       localparam int AW = 16, BW = 16;
       // Unsigned DUT
       logic [AW-1:0]    ua, ub;   logic [AW+BW-1:0]        uprod;
       // Signed DUT
       logic signed [AW-1:0] sa, sb;   logic signed [AW+BW-1:0] sprod;
       int errors = 0;
 
       mult_comb_u #(.AW(AW), .BW(BW)) dut_u (.a(ua), .b(ub), .product(uprod));
       mult_comb_s #(.AW(AW), .BW(BW)) dut_s (.a(sa), .b(sb), .product(sprod));
 
       // Boundary operands (signed): min, -1, 0, 1, max
       localparam logic signed [AW-1:0] SMIN = -(1<<<(AW-1));
       localparam logic signed [AW-1:0] SMAX =  (1<<<(AW-1)) - 1;
 
       task check_u(input logic [AW-1:0] x, input logic [BW-1:0] y);
           ua = x; ub = y; #1;
           assert (uprod === (x * y))                  // full unsigned reference
               else begin $error("U %0d*%0d got %0d", x, y, uprod); errors++; end
       endtask
       task check_s(input logic signed [AW-1:0] x, input logic signed [BW-1:0] y);
           sa = x; sb = y; #1;
           assert (sprod === ($signed(x) * $signed(y)))  // full signed reference
               else begin $error("S %0d*%0d got %0d", x, y, sprod); errors++; end
       endtask
 
       initial begin
           // Unsigned: boundaries + random
           check_u(0,0); check_u(1,1); check_u('1,'1);   // 0, 1, max*max — needs all N+M bits
           for (int i = 0; i < 200; i++) check_u($urandom, $urandom);
           // Signed: the corners a naive test skips
           check_s(-1,-1);        // must be +1
           check_s(SMIN,-1);      // min * -1 — overflows a truncated product
           check_s(SMAX,SMIN);    // largest magnitude negative product
           check_s(0,SMAX); check_s(1,SMIN);
           for (int i = 0; i < 200; i++) check_s($urandom, $urandom);
           if (errors == 0) $display("PASS: combinational mult, full N+M product, signed+unsigned");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form is identical in intent; the differences are wire/reg typing, $random for stimulus, and if (product !== exp) $display("FAIL") as the self-check. Signedness is forced with signed ports plus $signed(...) on the multiply, exactly as in SystemVerilog.

mult_comb.v — combinational N+M multiply in Verilog (unsigned + signed)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mult_comb_u #(
       parameter AW = 16,
       parameter BW = 16
   )(
       input  wire [AW-1:0]    a,
       input  wire [BW-1:0]    b,
       output wire [AW+BW-1:0] product      // N+M wide — no truncation
   );
       assign product = a * b;              // unsigned partial-product array
   endmodule
 
   module mult_comb_s #(
       parameter AW = 16,
       parameter BW = 16
   )(
       input  wire signed [AW-1:0]    a,
       input  wire signed [BW-1:0]    b,
       output wire signed [AW+BW-1:0] product   // SIGNED N+M result
   );
       // $signed on BOTH operands forces the two's-complement array.
       assign product = $signed(a) * $signed(b);
   endmodule
mult_comb_tb.v — self-checking; full product vs reference, signed corners
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mult_comb_tb;
       parameter AW = 16, BW = 16;
       reg  [AW-1:0]    ua, ub;   wire [AW+BW-1:0]        uprod;
       reg  signed [AW-1:0] sa, sb;   wire signed [AW+BW-1:0] sprod;
       reg  [AW+BW-1:0] uexp;   reg signed [AW+BW-1:0] sexp;
       integer i, errors;
 
       mult_comb_u #(.AW(AW), .BW(BW)) dut_u (.a(ua), .b(ub), .product(uprod));
       mult_comb_s #(.AW(AW), .BW(BW)) dut_s (.a(sa), .b(sb), .product(sprod));
 
       initial begin
           errors = 0;
           // Unsigned boundaries: 0, 1, max*max (exercises the full N+M width)
           ua=0;      ub=0;      #1; uexp=ua*ub;              if (uprod!==uexp) begin $display("FAIL U 0"); errors=errors+1; end
           ua={AW{1'b1}}; ub={BW{1'b1}}; #1; uexp=ua*ub;      if (uprod!==uexp) begin $display("FAIL U max*max %h exp %h", uprod, uexp); errors=errors+1; end
           for (i=0;i<200;i=i+1) begin
               ua=$random; ub=$random; #1; uexp=ua*ub;
               if (uprod!==uexp) begin $display("FAIL U %0d*%0d=%0d exp %0d", ua, ub, uprod, uexp); errors=errors+1; end
           end
           // Signed corners: -1*-1=+1, min*-1 (overflows a truncated product)
           sa=-1;              sb=-1;              #1; sexp=$signed(sa)*$signed(sb); if (sprod!==sexp) begin $display("FAIL S -1*-1"); errors=errors+1; end
           sa=(1<<<(AW-1));    sb=-1;              #1; sexp=$signed(sa)*$signed(sb); if (sprod!==sexp) begin $display("FAIL S min*-1 %0d exp %0d", sprod, sexp); errors=errors+1; end
           for (i=0;i<200;i=i+1) begin
               sa=$random; sb=$random; #1; sexp=$signed(sa)*$signed(sb);
               if (sprod!==sexp) begin $display("FAIL S %0d*%0d=%0d exp %0d", sa, sb, sprod, sexp); errors=errors+1; end
           end
           if (errors==0) $display("PASS: combinational mult, full N+M product, signed+unsigned");
           else           $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

VHDL makes the two disciplines the most explicit of the three. numeric_std gives you unsigned and signed types whose * operator returns a result of the summed width (AW+BW), and you type the operands to pick the array; there is no accidental signedness. The result is resize-free because unsigned * unsigned already yields AW+BW bits.

mult_comb.vhd — combinational N+M multiply in VHDL (numeric_std signed/unsigned)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- UNSIGNED: the numeric_std "*" on two unsigned returns AW+BW bits.
   entity mult_comb_u is
       generic ( AW : positive := 16; BW : positive := 16 );
       port (
           a       : in  unsigned(AW-1 downto 0);
           b       : in  unsigned(BW-1 downto 0);
           product : out unsigned(AW+BW-1 downto 0)     -- N+M — full width
       );
   end entity;
   architecture rtl of mult_comb_u is
   begin
       product <= a * b;                                -- width is AW+BW by definition
   end architecture;
 
   -- SIGNED: same operator, but the operands are typed `signed` → two's-complement array.
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   entity mult_comb_s is
       generic ( AW : positive := 16; BW : positive := 16 );
       port (
           a       : in  signed(AW-1 downto 0);
           b       : in  signed(BW-1 downto 0);
           product : out signed(AW+BW-1 downto 0)       -- SIGNED N+M result
       );
   end entity;
   architecture rtl of mult_comb_s is
   begin
       product <= a * b;                                -- signed*signed → AW+BW signed
   end architecture;
mult_comb_tb.vhd — self-checking; full product vs reference (assert severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity mult_comb_tb is
   end entity;
 
   architecture sim of mult_comb_tb is
       constant AW : positive := 16;
       constant BW : positive := 16;
       signal ua, ub : unsigned(AW-1 downto 0);
       signal uprod  : unsigned(AW+BW-1 downto 0);
       signal sa, sb : signed(AW-1 downto 0);
       signal sprod  : signed(AW+BW-1 downto 0);
   begin
       dut_u : entity work.mult_comb_u generic map (AW=>AW, BW=>BW)
               port map (a=>ua, b=>ub, product=>uprod);
       dut_s : entity work.mult_comb_s generic map (AW=>AW, BW=>BW)
               port map (a=>sa, b=>sb, product=>sprod);
 
       stim : process
           -- pseudo-random via a simple LCG so the TB is self-contained
           variable seed : integer := 12345;
           impure function nxt(m : integer) return integer is
           begin
               seed := (1103515245*seed + 12345) mod 2147483647;
               return seed mod m;
           end function;
       begin
           -- Unsigned boundaries: 0, max*max (needs all N+M bits)
           ua <= (others=>'0'); ub <= (others=>'0'); wait for 1 ns;
           assert uprod = ua*ub report "U 0 mismatch" severity error;
           ua <= (others=>'1'); ub <= (others=>'1'); wait for 1 ns;
           assert uprod = ua*ub report "U max*max mismatch" severity error;
           for i in 0 to 199 loop
               ua <= to_unsigned(nxt(2**AW), AW);
               ub <= to_unsigned(nxt(2**BW), BW);
               wait for 1 ns;
               assert uprod = ua*ub report "U random mismatch" severity error;
           end loop;
           -- Signed corners: -1*-1=+1, min*-1 (overflows a truncated product)
           sa <= to_signed(-1, AW);          sb <= to_signed(-1, BW);          wait for 1 ns;
           assert sprod = sa*sb report "S -1*-1 mismatch" severity error;
           sa <= to_signed(-(2**(AW-1)), AW); sb <= to_signed(-1, BW);         wait for 1 ns;
           assert sprod = sa*sb report "S min*-1 mismatch" severity error;
           for i in 0 to 199 loop
               sa <= to_signed(nxt(2**AW) - 2**(AW-1), AW);
               sb <= to_signed(nxt(2**BW) - 2**(BW-1), BW);
               wait for 1 ns;
               assert sprod = sa*sb report "S random mismatch" severity error;
           end loop;
           report "mult_comb self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. Sequential shift-add multiplier — an FSMD (repeated addition)

When area matters more than throughput (a control-plane multiply, a low-rate channel), you do not spend a whole array — you reuse one adder N times. The shift-add multiplier is an FSM+datapath (Chapter 4.6): a small controller sequences BW cycles; the datapath holds a shifting multiplicand, a shifting multiplier, and an accumulator; each cycle, if the low multiplier bit is 1, it adds the multiplicand into the accumulator, then shifts. This is literally the adder of 5.1 done repeatedly — and the accumulator is still sized AW+BW so it never overflows. This example is unsigned for clarity (signed uses Booth or a sign-corrected variant, out of scope here).

mult_seq.sv — sequential shift-add multiplier (an FSMD, unsigned)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mult_seq #(
       parameter int AW = 16,
       parameter int BW = 16
   )(
       input  logic                clk,
       input  logic                rst_n,          // async active-low reset
       input  logic                start,          // pulse: latch a,b and begin
       input  logic [AW-1:0]       a,              // multiplicand
       input  logic [BW-1:0]       b,              // multiplier
       output logic [AW+BW-1:0]    product,        // N+M accumulator — no overflow
       output logic                done            // 1-cycle pulse when product valid
   );
       // Datapath registers, all sized so nothing truncates:
       logic [AW+BW-1:0] acc;                       // accumulator, N+M wide
       logic [AW+BW-1:0] mcand;                      // multiplicand, shifted LEFT each step
       logic [BW-1:0]    mplier;                     // multiplier, shifted RIGHT each step
       logic [$clog2(BW+1)-1:0] cnt;                 // counts BW iterations
       typedef enum logic [1:0] {IDLE, RUN, FIN} state_t;
       state_t state;
 
       always_ff @(posedge clk or negedge rst_n) begin
           if (!rst_n) begin
               state <= IDLE; acc <= '0; done <= 1'b0; product <= '0;
           end else begin
               done <= 1'b0;                          // default: done is a 1-cycle pulse
               case (state)
                   IDLE: if (start) begin             // latch operands, zero the accumulator
                       acc    <= '0;
                       mcand  <= {{BW{1'b0}}, a};      // a in the low bits of an N+M word
                       mplier <= b;
                       cnt    <= '0;
                       state  <= RUN;
                   end
                   RUN: begin                          // one add-and-shift per multiplier bit
                       if (mplier[0]) acc <= acc + mcand;   // conditional ADD (5.1's adder)
                       mcand  <= mcand  << 1;          // multiplicand weight climbs
                       mplier <= mplier >> 1;          // examine next bit next cycle
                       cnt    <= cnt + 1;
                       if (cnt == BW-1) state <= FIN;  // BW iterations done
                   end
                   FIN: begin
                       product <= acc; done <= 1'b1;   // publish product, pulse done
                       state   <= IDLE;
                   end
               endcase
           end
       end
   endmodule

The clocked testbench drives start, waits for done, and checks the accumulated product against the golden a*b — across random operands and the boundaries (0, 1, max). Because it is sequential, the self-check happens at done, not combinationally.

mult_seq_tb.sv — clocked self-check: start → wait done → assert product == a*b
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mult_seq_tb;
       localparam int AW = 16, BW = 16;
       logic clk = 0, rst_n, start;
       logic [AW-1:0] a; logic [BW-1:0] b;
       logic [AW+BW-1:0] product; logic done;
       int errors = 0;
 
       mult_seq #(.AW(AW), .BW(BW)) dut
           (.clk(clk), .rst_n(rst_n), .start(start), .a(a), .b(b), .product(product), .done(done));
 
       always #5 clk = ~clk;                          // 100 MHz
 
       task run_one(input logic [AW-1:0] x, input logic [BW-1:0] y);
           @(posedge clk); a <= x; b <= y; start <= 1'b1;
           @(posedge clk); start <= 1'b0;
           wait (done);                               // sequential: result valid at done
           assert (product === (x * y))               // full N+M reference
               else begin $error("%0d*%0d got %0d exp %0d", x, y, product, x*y); errors++; end
       endtask
 
       initial begin
           rst_n = 0; start = 0; a = 0; b = 0;
           repeat (2) @(posedge clk); rst_n = 1;
           run_one(0, 12345);                         // boundary: 0
           run_one(1, 65535);                         // boundary: 1 * max
           run_one('1, '1);                           // max * max — needs all N+M bits
           for (int i = 0; i < 50; i++) run_one($urandom, $urandom);
           if (errors == 0) $display("PASS: shift-add multiplier == a*b (full product) over %0d cases", 53);
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog shift-add multiplier is the same FSMD with reg state, a one-hot-free encoded state, and <= for all sequential updates. The testbench polls done and self-checks with if (product !== exp) $display("FAIL").

mult_seq.v — sequential shift-add multiplier in Verilog (FSMD)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mult_seq #(
       parameter AW = 16,
       parameter BW = 16
   )(
       input  wire                clk,
       input  wire                rst_n,
       input  wire                start,
       input  wire [AW-1:0]       a,
       input  wire [BW-1:0]       b,
       output reg  [AW+BW-1:0]    product,
       output reg                 done
   );
       localparam IDLE = 2'd0, RUN = 2'd1, FIN = 2'd2;
       reg [1:0]           state;
       reg [AW+BW-1:0]     acc, mcand;               // both N+M wide
       reg [BW-1:0]        mplier;
       integer             cnt;
 
       always @(posedge clk or negedge rst_n) begin
           if (!rst_n) begin
               state <= IDLE; acc <= 0; product <= 0; done <= 1'b0;
           end else begin
               done <= 1'b0;                          // done is a 1-cycle pulse
               case (state)
                   IDLE: if (start) begin
                       acc    <= 0;
                       mcand  <= {{BW{1'b0}}, a};      // a in the low half of N+M word
                       mplier <= b;
                       cnt    <= 0;
                       state  <= RUN;
                   end
                   RUN: begin
                       if (mplier[0]) acc <= acc + mcand;   // conditional add (repeated addition)
                       mcand  <= mcand  << 1;
                       mplier <= mplier >> 1;
                       cnt    <= cnt + 1;
                       if (cnt == BW-1) state <= FIN;
                   end
                   FIN: begin
                       product <= acc; done <= 1'b1;
                       state   <= IDLE;
                   end
               endcase
           end
       end
   endmodule
mult_seq_tb.v — clocked self-check; wait for done, compare full product
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mult_seq_tb;
       parameter AW = 16, BW = 16;
       reg clk, rst_n, start;
       reg [AW-1:0] a; reg [BW-1:0] b;
       wire [AW+BW-1:0] product; wire done;
       reg [AW+BW-1:0] exp;
       integer i, errors;
 
       mult_seq #(.AW(AW), .BW(BW)) dut
           (.clk(clk), .rst_n(rst_n), .start(start), .a(a), .b(b), .product(product), .done(done));
 
       initial clk = 0;
       always #5 clk = ~clk;
 
       task run_one;
           input [AW-1:0] x; input [BW-1:0] y;
           begin
               @(posedge clk); a <= x; b <= y; start <= 1'b1;
               @(posedge clk); start <= 1'b0;
               while (!done) @(posedge clk);          // wait for the sequential result
               exp = x * y;                           // full N+M reference
               if (product !== exp) begin
                   $display("FAIL: %0d*%0d=%0d exp %0d", x, y, product, exp);
                   errors = errors + 1;
               end
           end
       endtask
 
       initial begin
           errors = 0; rst_n = 0; start = 0; a = 0; b = 0;
           repeat (2) @(posedge clk); rst_n = 1;
           run_one(0, 12345);                         // 0
           run_one(1, 65535);                         // 1 * max
           run_one({AW{1'b1}}, {BW{1'b1}});           // max * max — full width
           for (i = 0; i < 50; i = i + 1) run_one($random, $random);
           if (errors == 0) $display("PASS: shift-add multiplier == a*b over all cases");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the FSMD is a single clocked process over an unsigned accumulator. numeric_std keeps the widths honest — the accumulator is AW+BW and the shifts use shift_left/shift_right on unsigned. Reset is async active-low to match the SV/Verilog versions.

mult_seq.vhd — sequential shift-add multiplier in VHDL (FSMD, unsigned)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity mult_seq is
       generic ( AW : positive := 16; BW : positive := 16 );
       port (
           clk     : in  std_logic;
           rst_n   : in  std_logic;                       -- async active-low
           start   : in  std_logic;
           a       : in  unsigned(AW-1 downto 0);
           b       : in  unsigned(BW-1 downto 0);
           product : out unsigned(AW+BW-1 downto 0);      -- N+M accumulator
           done    : out std_logic
       );
   end entity;
 
   architecture rtl of mult_seq is
       type state_t is (IDLE, RUN, FIN);
       signal state  : state_t;
       signal acc    : unsigned(AW+BW-1 downto 0);
       signal mcand  : unsigned(AW+BW-1 downto 0);        -- multiplicand, shifted left
       signal mplier : unsigned(BW-1 downto 0);           -- multiplier, shifted right
       signal cnt    : integer range 0 to BW;
   begin
       process (clk, rst_n)
       begin
           if rst_n = '0' then
               state <= IDLE; acc <= (others=>'0');
               product <= (others=>'0'); done <= '0';
           elsif rising_edge(clk) then
               done <= '0';                                -- done is a 1-cycle pulse
               case state is
                   when IDLE =>
                       if start = '1' then
                           acc    <= (others=>'0');
                           mcand  <= resize(a, AW+BW);      -- a in low bits of N+M word
                           mplier <= b;
                           cnt    <= 0;
                           state  <= RUN;
                       end if;
                   when RUN =>
                       if mplier(0) = '1' then
                           acc <= acc + mcand;              -- conditional add (repeated addition)
                       end if;
                       mcand  <= shift_left(mcand, 1);
                       mplier <= shift_right(mplier, 1);
                       cnt    <= cnt + 1;
                       if cnt = BW-1 then state <= FIN; end if;
                   when FIN =>
                       product <= acc; done <= '1';
                       state   <= IDLE;
               end case;
           end if;
       end process;
   end architecture;
mult_seq_tb.vhd — clocked self-check; wait for done, assert full product
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity mult_seq_tb is
   end entity;
 
   architecture sim of mult_seq_tb is
       constant AW : positive := 16;
       constant BW : positive := 16;
       signal clk     : std_logic := '0';
       signal rst_n   : std_logic := '0';
       signal start   : std_logic := '0';
       signal a       : unsigned(AW-1 downto 0) := (others=>'0');
       signal b       : unsigned(BW-1 downto 0) := (others=>'0');
       signal product : unsigned(AW+BW-1 downto 0);
       signal done    : std_logic;
   begin
       dut : entity work.mult_seq generic map (AW=>AW, BW=>BW)
             port map (clk=>clk, rst_n=>rst_n, start=>start, a=>a, b=>b,
                       product=>product, done=>done);
 
       clk <= not clk after 5 ns;                          -- 100 MHz
 
       stim : process
           variable seed : integer := 6789;
           impure function nxt(m : integer) return integer is
           begin
               seed := (1103515245*seed + 12345) mod 2147483647;
               return seed mod m;
           end function;
           procedure run_one(x : in unsigned(AW-1 downto 0);
                             y : in unsigned(BW-1 downto 0)) is
           begin
               wait until rising_edge(clk);
               a <= x; b <= y; start <= '1';
               wait until rising_edge(clk);
               start <= '0';
               wait until done = '1';                       -- sequential result valid here
               assert product = x * y                       -- full N+M reference
                   report "shift-add mismatch" severity error;
           end procedure;
       begin
           rst_n <= '0';
           wait until rising_edge(clk); wait until rising_edge(clk);
           rst_n <= '1';
           run_one(to_unsigned(0, AW), to_unsigned(12345, BW));   -- 0
           run_one(to_unsigned(1, AW), (others=>'1'));            -- 1 * max
           run_one((others=>'1'), (others=>'1'));                 -- max * max
           for i in 0 to 49 loop
               run_one(to_unsigned(nxt(2**AW), AW), to_unsigned(nxt(2**BW), BW));
           end loop;
           report "mult_seq self-check complete" severity note;
           wait;
       end process;
   end architecture;

4c. The product-width truncation bug — buggy vs fixed, in all three HDLs

Pattern (c) is the signature failure, shown as buggy vs fixed RTL in each language and dramatized narratively in §7. The bug is the same everywhere: the product is declared operand-width (or an intermediate truncates), so the tool computes the full N+M result and then discards the upper half. It passes small-number tests and fails at scale.

mult_bug.sv — BUGGY (operand-width product) vs FIXED (full N+M)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: product declared AW bits for an AW x BW multiply → the top BW bits are
   //        DROPPED. Correct only when the true product happens to fit in AW bits.
   module scale_bad #(parameter int AW = 16, parameter int BW = 16) (
       input  logic [AW-1:0] a,
       input  logic [BW-1:0] b,
       output logic [AW-1:0] product          // <-- TOO NARROW: silent truncation
   );
       assign product = a * b;                 // full product computed, high half lost
   endmodule
 
   // FIXED: product is AW+BW bits → the whole result is kept, no overflow ever.
   module scale_good #(parameter int AW = 16, parameter int BW = 16) (
       input  logic [AW-1:0]      a,
       input  logic [BW-1:0]      b,
       output logic [AW+BW-1:0]   product      // <-- N+M: cannot truncate
   );
       assign product = a * b;
   endmodule
mult_bug_tb.sv — small operands PASS both; large operands expose the truncation
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mult_bug_tb;
       localparam int AW = 16, BW = 16;
       logic [AW-1:0]    a, b;
       logic [AW-1:0]    prod_bad;
       logic [AW+BW-1:0] prod_good;
       int errors = 0;
 
       scale_bad  #(.AW(AW), .BW(BW)) dbad  (.a(a), .b(b), .product(prod_bad));
       scale_good #(.AW(AW), .BW(BW)) dgood (.a(a), .b(b), .product(prod_good));
 
       initial begin
           // SMALL operands: true product fits in AW bits → BOTH pass. This is the
           // trap: the bug is invisible until the operands get large.
           a = 3;  b = 5;  #1;
           assert (prod_good === 15);
           assert (prod_bad  === 15);                     // bug still hidden here
           // LARGE operands: true product needs > AW bits → the buggy one truncates.
           a = 16'd60000; b = 16'd50000; #1;              // true product = 3_000_000_000
           assert (prod_good === (a * b))                 // full N+M — correct
               else begin $error("good wrong: %0d", prod_good); errors++; end
           if (prod_bad !== (a * b)) $display("EXPECTED: buggy product truncated: %0d vs %0d", prod_bad, a*b);
           if (errors == 0) $display("PASS: full-width product correct; truncation demonstrated on large operands");
           $finish;
       end
   endmodule

The Verilog pair is the same story with wire/reg typing. The testbench shows both modules agreeing on small operands and diverging on large ones — the divergence is the bug.

mult_bug.v — BUGGY (operand-width) vs FIXED (full N+M) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: AW-wide product for an AW x BW multiply → high half discarded.
   module scale_bad #(parameter AW = 16, parameter BW = 16) (
       input  wire [AW-1:0] a,
       input  wire [BW-1:0] b,
       output wire [AW-1:0] product          // TOO NARROW
   );
       assign product = a * b;
   endmodule
 
   // FIXED: AW+BW product → whole result kept.
   module scale_good #(parameter AW = 16, parameter BW = 16) (
       input  wire [AW-1:0]    a,
       input  wire [BW-1:0]    b,
       output wire [AW+BW-1:0] product        // N+M
   );
       assign product = a * b;
   endmodule
mult_bug_tb.v — self-checking; small ops hide the bug, large ops expose it
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module mult_bug_tb;
       parameter AW = 16, BW = 16;
       reg  [AW-1:0]    a, b;
       wire [AW-1:0]    prod_bad;
       wire [AW+BW-1:0] prod_good;
       reg  [AW+BW-1:0] exp;
       integer errors;
 
       scale_bad  #(.AW(AW), .BW(BW)) dbad  (.a(a), .b(b), .product(prod_bad));
       scale_good #(.AW(AW), .BW(BW)) dgood (.a(a), .b(b), .product(prod_good));
 
       initial begin
           errors = 0;
           // Small operands: both correct — the bug hides.
           a = 3; b = 5; #1; exp = a * b;
           if (prod_good !== exp[AW+BW-1:0]) begin $display("FAIL good small"); errors=errors+1; end
           if (prod_bad  !== exp[AW-1:0])    begin $display("FAIL bad small");  errors=errors+1; end
           // Large operands: good is right, bad truncates.
           a = 16'd60000; b = 16'd50000; #1; exp = a * b;   // = 3_000_000_000
           if (prod_good !== exp) begin $display("FAIL good large %0d exp %0d", prod_good, exp); errors=errors+1; end
           if (prod_bad  !== exp) $display("EXPECTED: bad truncated to %0d (true %0d)", prod_bad, exp);
           if (errors == 0) $display("PASS: N+M product correct; truncation shown on large operands");
           else             $display("FAIL: %0d real mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the same bug appears when you resize a full-width product down to operand width, or type the product signal too narrow. numeric_std will happily truncate a resize to AW bits — the fix is to keep the product AW+BW wide and never resize it down.

mult_bug.vhd — BUGGY (resize to AW) vs FIXED (keep AW+BW)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: the product is resized DOWN to AW bits → high half discarded.
   entity scale_bad is
       generic ( AW : positive := 16; BW : positive := 16 );
       port ( a : in unsigned(AW-1 downto 0);
              b : in unsigned(BW-1 downto 0);
              product : out unsigned(AW-1 downto 0) );      -- TOO NARROW
   end entity;
   architecture rtl of scale_bad is
   begin
       product <= resize(a * b, AW);                        -- truncates the top BW bits
   end architecture;
 
   -- FIXED: product is AW+BW wide; a*b already returns AW+BW → no resize needed.
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   entity scale_good is
       generic ( AW : positive := 16; BW : positive := 16 );
       port ( a : in unsigned(AW-1 downto 0);
              b : in unsigned(BW-1 downto 0);
              product : out unsigned(AW+BW-1 downto 0) );    -- N+M
   end entity;
   architecture rtl of scale_good is
   begin
       product <= a * b;                                    -- full width, no truncation
   end architecture;
mult_bug_tb.vhd — self-checking; small ops agree, large ops expose truncation
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity mult_bug_tb is
   end entity;
 
   architecture sim of mult_bug_tb is
       constant AW : positive := 16;
       constant BW : positive := 16;
       signal a : unsigned(AW-1 downto 0);
       signal b : unsigned(BW-1 downto 0);
       signal prod_bad  : unsigned(AW-1 downto 0);
       signal prod_good : unsigned(AW+BW-1 downto 0);
   begin
       dbad  : entity work.scale_bad  generic map (AW=>AW, BW=>BW)
               port map (a=>a, b=>b, product=>prod_bad);
       dgood : entity work.scale_good generic map (AW=>AW, BW=>BW)
               port map (a=>a, b=>b, product=>prod_good);
 
       stim : process
       begin
           -- Small operands: the true product fits in AW bits → both correct.
           a <= to_unsigned(3, AW); b <= to_unsigned(5, BW); wait for 1 ns;
           assert prod_good = a * b report "good small mismatch" severity error;
           assert prod_bad  = resize(a * b, AW) report "bad small mismatch" severity error;
           -- Large operands: true product needs > AW bits → the buggy one truncates.
           a <= to_unsigned(60000, AW); b <= to_unsigned(50000, BW); wait for 1 ns;
           assert prod_good = a * b report "good large mismatch" severity error;
           assert prod_bad /= prod_good(AW+BW-1 downto 0)
               report "NOTE: buggy product truncated as expected" severity note;
           report "mult_bug self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: a multiply grows the word — declare the product AW+BW bits and match its signedness to the operands, or you ship silent overflow.

5. Verification Strategy

A combinational multiplier's correctness is an arithmetic identity, and the reference model is free — the language's own * on wide-enough variables is the golden model. The single invariant that captures a correct multiplier is:

The stored product, over its full N+M bits, equals the mathematical product of the operands — for the correct signedness — with no truncation and no sign error.

Everything below is that one invariant, made checkable, and it maps directly onto the testbenches in §4.

  • Self-check the FULL N+M product against a reference (not a truncated one). Compute expected = a * b into an AW+BW-wide reference variable, and assert product === expected over the entire width. The trap this catches is the §4c truncation: if you compare only the low AW bits, a truncated product passes — you must compare all AW+BW bits. The three mult_comb testbenches do exactly this: SV assert (uprod === (x*y)), Verilog if (product !== exp) $display("FAIL"), VHDL assert uprod = a*b … severity error.
  • Random plus boundary operands. Random operands find bulk bugs; boundaries find the edge ones. Always include 0 (product must be 0), 1 (product must equal the other operand — a width/pass-through check), and max (max × max exercises all N+M bits — it is the case a truncated product fails). Random-only testing can miss the max-width case entirely, which is precisely how the truncation bug ships.
  • The signed corners — the ones a naive test skips. For a signed multiplier, drive -1 × -1 (must be +1, not a huge number — the classic signedness sanity check), min × -1 (which overflows a naively-sized product and tests two's-complement handling), and max × min (the largest-magnitude negative product). These corners are where an unsigned-multiply-of-signed-data bug and a sign-extension bug show up; if -1 × -1 does not give +1, your operands multiplied as unsigned.
  • Signedness on both fronts — verify signed and unsigned DUTs. Run the signed testbench against the signed module and the unsigned testbench against the unsigned module, and make the reference model match: $signed(x) * $signed(y) for signed, plain x * y for unsigned. A single testbench that uses the wrong reference signedness will "pass" a wrong DUT — the reference must be as explicit about signedness as the design.
  • Sequential multiplier — check at done, and check latency. For the shift-add FSMD, the product is valid only when done pulses, so the self-check is @(done) assert product === a*b. Also assert the timing invariant: done arrives a bounded number of cycles after start (about BW iterations plus overhead) and the module returns to IDLE ready for the next start. A shift-add multiplier that produces the right value but never asserts done, or takes the wrong number of cycles, is still broken.
  • Parameter-generic verification (AW, BW = 8, 16, 32; AW ≠ BW). A parameterized multiplier must be verified generic. Re-run the sweep for several widths, and crucially for AW ≠ BW (say 18 × 25) — that is where an AW+BW vs 2*AW width mistake hides. A form that passes at 16 × 16 can still be wrong at 18 × 25 if someone sized the product 2*AW instead of AW+BW.
  • Expected waveform. For the combinational multiplier, product tracks a * b after the (long) propagation delay — never showing a stale or half-width value. For the shift-add FSMD, the correct picture is start pulsing, then BW add-and-shift cycles during which acc grows monotonically toward the product, then done pulsing for one cycle with product settled — and acc visibly using its full AW+BW width for large operands. A product that saturates or wraps at AW bits on a waveform is the visual signature of the §7 truncation bug.

6. Common Mistakes

Truncating the product (operand-width destination). The signature bug. Declaring product the same width as an operand for an AW × BW multiply (logic [AW-1:0] product = a * b) makes the tool compute the full AW+BW result and discard the top half. It lints clean, simulates clean on small numbers, and fails silently the moment the true product exceeds AW bits. The fix is one rule with no exceptions: the product of an N-bit and an M-bit number is N+M bits — size the destination AW+BW. (Full post-mortem in §7; buggy-vs-fixed RTL for all three HDLs in §4c.)

Signed/unsigned multiply mismatch. Multiplying signed data with an unsigned operator (or storing a signed product in an unsigned destination) treats -3 as the large unsigned value 2^N - 3, so the product is not slightly wrong — it is enormous and meaningless. Signed and unsigned multiply are different arrays, and the operands' signedness is load-bearing. State it explicitly: $signed(a) * $signed(b) (SV/Verilog) or signed/unsigned operands from numeric_std (VHDL). The -1 × -1 corner is the fast check: if it does not yield +1, you multiplied signed data as unsigned.

Assuming * is cheap or one-cycle at wide widths. A 32 × 32 multiply is a large adder array and often the longest combinational path in the block; at high clock it will not close timing in one cycle. * is not free — treat a wide multiply as an architectural element: pipeline it (register the tree ranks), or use a sequential shift-add if you can afford the latency. Dropping a bare 32 × 32 * into a fast pipeline stage and being surprised by a timing violation is a recurring mistake.

Forgetting DSP inference constraints (FPGA). On an FPGA you want the * to map to a hard DSP block — but the DSP has rules. It has a native operand width (e.g. 18 × 18 or 27 × 18); exceed it and the tool stitches DSPs or falls back to slow fabric. It has optional input/output pipeline registers that you must place (by adding register stages around the *) to hit the DSP's rated clock. Write a wide multiply with no pipeline registers and you get a DSP that cannot run fast, or a fabric multiplier that misses timing entirely. Match the multiply's width and register placement to the target DSP.

Mixing signed and unsigned operands without explicit handling. A signed × unsigned multiply is a real case (a signed sample times an unsigned gain), but it needs deliberate conversion — sign-extend the signed operand by one bit into a wider signed word and treat the unsigned one as a non-negative signed value, then multiply signed. Letting the tool infer the mix silently promotes everything to unsigned (or applies the wrong extension) and corrupts the sign. Convert on purpose; never leave a mixed-signedness multiply implicit.

Over-shifting or under-counting in a shift-add multiplier. The sequential form is BW iterations, shifting the multiplicand into an AW+BW word. Run BW-1 iterations (an off-by-one on the loop bound) and the top multiplier bit is never processed — large multipliers give a low result. Size the accumulator/multiplicand at operand width instead of AW+BW and the accumulation itself truncates. The FSMD must iterate exactly BW times over an AW+BW-wide datapath.

7. DebugLab

The filter that was fine on quiet passages — a product truncated to operand width

The engineering lesson: a multiply grows the word — the product of an N-bit and an M-bit number genuinely needs N+M bits, and any narrower destination silently discards the high half. The tell is a bug that scales with operand magnitude ("wrong on loud passages, fine on quiet ones"): amplitude-dependent corruption in an arithmetic result is the fingerprint of a truncated product, because only large operands overflow the too-narrow destination. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: size every product AW+BW bits and match its signedness to the operands, and verify the full N+M product against a reference with max × max (and the signed corners) in the vectors so the large-operand case can never slip through a friendly test.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "a multiply grows the word, and it isn't free" habit.

Exercise 1 — Size every product

For each multiply, give the exact minimum product width and state whether it can be negative: (a) unsigned 12-bit × 12-bit; (b) signed 16-bit × 16-bit; (c) unsigned 18-bit × 25-bit; (d) a signed 10-bit sample times an unsigned 6-bit gain. For (d), describe the explicit conversion you would apply before the multiply and the width of the result. (Hint: the rule is N+M for the magnitude; the sign of the destination follows the operands.)

Exercise 2 — Reproduce the truncation on paper

A 16 × 16 multiplier has its product declared 16 bits. For operands a = 3, b = 5 and then a = 60000, b = 50000, give the true product, the stored (truncated) value, and explain in one line why the first passes a unit test and the second ships a bug. Then name the single test vector you would add to the unit test to catch it, and say why random-only stimulus can miss it.

Exercise 3 — Choose the multiplier shape

For each scenario pick combinational, pipelined, or sequential shift-add, and justify in one line: (a) one 24 × 24 multiply per 1000-cycle control loop, area-critical; (b) a 16 × 16 multiply on every cycle of a 400 MHz streaming filter; (c) a 32 × 32 multiply whose result is needed the same cycle in a slow (50 MHz), area-rich block; (d) the same 32 × 32 at 400 MHz. (Hint: map each to the scarce resource — area, throughput, or latency.)

Exercise 4 — Break the signedness

Two engineers implement a 16 × 16 multiply of two-signed samples. Engineer A writes product = a * b with a, b declared as plain [15:0] (unsigned) nets. Engineer B declares them signed and writes product = $signed(a) * $signed(b). Both size the product 32 bits. (i) For a = -1, b = -1, give the result each produces and explain the difference. (ii) State the one-line test that distinguishes the two, and (iii) say why sizing the product correctly (32 bits) does not save Engineer A from the bug.

Continue in RTL Design Patterns (forward links unlock as they ship):

  • Adders & Subtractors — Chapter 5.1; the prerequisite — a shift-add multiplier is repeated addition, and the "the word grows / signedness is explicit" theme this page sharpens comes from there.
  • Barrel Shifters — Chapter 5.3; the shift that a shift-add multiplier does one place per cycle, done in one combinational step — the other half of the multiply's structure.
  • Saturation & Rounding — Chapter 5.4; how to bring a full N+M product back to a narrower datapath on purpose — the deliberate version of §7's accidental truncation.
  • ALU Construction — Chapter 5; where the multiplier becomes one operation among many in a shared arithmetic unit.

Backward / method / siblings:

  • FSM + Datapath — Chapter 4.6; the FSMD structure the sequential shift-add multiplier is built from (controller + shift/accumulate datapath).
  • The Register Pattern (D-FF) — Chapter 2.1; the accumulator and shift registers the sequential multiplier holds its state in.
  • Multiplexers — Chapter 1.1; the selection primitive behind the conditional add in the shift-add datapath and any multiply/no-multiply choice.
  • Comparators — Chapter 1; the terminal-count comparison the shift-add FSM uses to know when BW iterations are done.
  • Encoding & Conversions — Chapter 1; the signed/unsigned representation choices that decide which multiply array you build.
  • The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses — a combinational multiply is pure datapath; a shift-add multiply adds state and control.
  • What Is an RTL Design Pattern? — Chapter 0.1; why the multiplier earns "pattern" — a recurring need, reusable forms, a synthesis reality, and a signature failure.

Verilog v1 prerequisites (the grammar this pattern transcribes into):

11. Summary

  • A multiply grows the word to N+M — always size the product AW+BW. The product of an N-bit and an M-bit number needs up to N+M bits; declare the destination narrower and the tool computes the full result and silently truncates the high half — correct on small operands, wrong at scale. This is the signature trap (§7), and the fix is unconditional: product is AW+BW bits, never operand-width.
  • Signedness must match the operands — signed and unsigned are different arrays. Multiply signed data with an unsigned operator and -3 becomes a giant positive number and the product is meaningless. State signedness explicitly ($signed(...) / signed nets in SV/Verilog; signed/unsigned from numeric_std in VHDL); the -1 × -1 = +1 corner is the fast sanity check.
  • * is a partial-product array, not a free operator. It infers an AND-grid summed by an adder tree — area grows ~N×M, delay grows with width, and at 32 bits it is often the critical path. On an FPGA it maps to a hard DSP block if you match the DSP's native width and place its pipeline registers.
  • One array, three shapes — the area/throughput/latency trade-off. Combinational (one cycle, biggest area, longest path); pipelined (register the tree ranks for throughput, at the cost of latency and flops); sequential shift-add (an FSMD doing repeated addition over N cycles — tiny area, low throughput). The shift-add multiplier is literally 5.1's adder reused N times, with an AW+BW accumulator so the running sum never overflows.
  • Verify the FULL product against a reference, with the corners that expose the traps. Self-check all AW+BW bits against the language's own *, over random plus boundary operands — including max × max (the case a truncated product fails) and the signed corners -1 × -1 and min × -1. Check the sequential multiplier at done and verify its cycle count. Self-checking testbenches shown here in SystemVerilog, Verilog, and VHDL.