Multiply-Accumulate (MAC) in Verilog with Code

A multiply-accumulate (MAC) unit computes acc = acc + a*b on every enabled clock cycle. It is the single most common datapath element in digital signal processing and AI accelerators: a FIR filter, a matrix multiply, and a convolution are all just long chains of multiply-accumulate operations. This page gives you clean, runnable multiply accumulate verilog code with self-checking testbenches you can simulate directly.

Both designs below are written in portable Verilog-2005 and were simulated with Icarus Verilog. The first is a parameterized signed MAC with a wide accumulator and a synchronous clear; the second is a burst dot-product MAC that saturates its output and flags overflow.

This parameterized signed MAC samples a and b on each rising clock edge, adds the product into a wide accumulator when en is high, and clears synchronously on clr or an active-low reset. The testbench streams several operand pairs (including negatives and the signed extremes) and checks the running accumulator against a golden sum-of-products every cycle, with a mid-stream clear.

// Parameterized signed multiply-accumulate (MAC) unit.
module mac #(
    parameter DW   = 8,   // operand width (signed)
    parameter ACCW = 32   // accumulator width (signed, wide to avoid overflow)
) (
    input                   clk,
    input                   rst_n,  // active-low synchronous reset
    input                   clr,    // synchronous clear of accumulator
    input                   en,     // accumulate enable
    input  signed [DW-1:0]  a,
    input  signed [DW-1:0]  b,
    output reg signed [ACCW-1:0] acc
);
    always @(posedge clk) begin
        if (!rst_n)      acc <= {ACCW{1'b0}};
        else if (clr)    acc <= {ACCW{1'b0}};
        else if (en)     acc <= acc + a * b;   // product sign-extended into ACCW
    end
endmodule

module tb;
    localparam DW = 8, ACCW = 32;
    reg clk, rst_n, clr, en;
    reg  signed [DW-1:0] a, b;
    wire signed [ACCW-1:0] acc;
    integer gold, errors;

    mac #(.DW(DW), .ACCW(ACCW)) dut (.clk(clk), .rst_n(rst_n), .clr(clr), .en(en), .a(a), .b(b), .acc(acc));

    initial clk = 1'b0;
    always #5 clk = ~clk;

    // Apply one stimulus vector, mirror the DUT update in a golden model, compare.
    task step;
        input t_clr; input t_en;
        input signed [DW-1:0] t_a; input signed [DW-1:0] t_b;
        begin
            @(negedge clk);
            clr = t_clr; en = t_en; a = t_a; b = t_b;
            @(posedge clk);
            if (t_clr)      gold = 0;
            else if (t_en)  gold = gold + t_a * t_b;
            #1;
            if (acc !== gold) begin
                $display("FAIL: acc=%0d expected=%0d (clr=%0d en=%0d a=%0d b=%0d)", acc, gold, t_clr, t_en, t_a, t_b);
                errors = errors + 1;
            end else
                $display("PASS: acc=%0d (a=%0d b=%0d en=%0d clr=%0d)", acc, t_a, t_b, t_en, t_clr);
        end
    endtask

    initial begin
        errors = 0; gold = 0; clr = 0; en = 0; a = 0; b = 0;
        rst_n = 0;
        @(negedge clk); @(posedge clk); #1;
        rst_n = 1;
        if (acc !== 0) begin $display("FAIL: reset did not clear (acc=%0d)", acc); errors=errors+1; end
        else $display("PASS: reset cleared accumulator");

        step(0,1,  3,  4);   // acc = 12
        step(0,1, -2,  5);   // acc = 12 - 10 = 2
        step(0,0,  7,  7);   // en=0: acc unchanged = 2
        step(0,1,-10,-10);   // acc = 2 + 100 = 102
        step(0,1, 50,  2);   // acc = 102 + 100 = 202
        step(1,0,  0,  0);   // clr: acc = 0
        step(0,1,  6,  6);   // acc = 36
        step(0,1,-128,127);  // acc = 36 + (-16256) = -16220
        step(0,1,-128,-128); // acc = -16220 + 16384 = 164

        if (errors == 0) $display("ALL TESTS PASSED");
        else $display("TESTS FAILED: %0d error(s)", errors);
        $finish;
    end
endmodule
PASS: reset cleared accumulator
PASS: acc=12 (a=3 b=4 en=1 clr=0)
PASS: acc=2 (a=-2 b=5 en=1 clr=0)
PASS: acc=2 (a=7 b=7 en=0 clr=0)
PASS: acc=102 (a=-10 b=-10 en=1 clr=0)
PASS: acc=202 (a=50 b=2 en=1 clr=0)
PASS: acc=0 (a=0 b=0 en=0 clr=1)
PASS: acc=36 (a=6 b=6 en=1 clr=0)
PASS: acc=-16220 (a=-128 b=127 en=1 clr=0)
PASS: acc=164 (a=-128 b=-128 en=1 clr=0)
ALL TESTS PASSED

The log proves the accumulator tracks a golden sum-of-products across nine cycles: multiply-and-add, an en=0 hold, a mid-stream clr that zeroes the running total, and the signed extremes -128*127 and -128*-128 handled correctly. Because the golden value is computed independently in the testbench, any sign-extension or timing bug would print a FAIL line instead of ALL TESTS PASSED.

This variant accumulates a dot product over a vector of N element pairs in one burst, then saturates the result into a narrower signed output and raises a sticky ovf flag if clamping occurred. The testbench drives two known-answer vectors: one that fits (result 110) and one engineered to overflow (127*127*4 = 64516), which must clamp to 32767.

// Burst dot-product MAC: accumulate N element pairs, saturate the output.
module dot_mac #(
    parameter DW  = 8,    // signed element width
    parameter N   = 4,    // products in the dot product
    parameter OW  = 16    // saturated signed output width
) (
    input                   clk,
    input                   rst_n,  // active-low synchronous reset
    input                   start,  // first element: reset internal sum
    input                   en,     // accumulate this cycle's pair
    input  signed [DW-1:0]  a,
    input  signed [DW-1:0]  b,
    output reg signed [OW-1:0] y,    // saturated dot product
    output reg                 ovf   // sticky: saturation occurred
);
    // Full-precision accumulator with guard bits (2*DW product + headroom for N adds).
    localparam INW = 2*DW + 8;
    reg signed [INW-1:0] sum;
    localparam signed [INW-1:0] YMAX = (1 <<< (OW-1)) - 1;   //  2^(OW-1)-1
    localparam signed [INW-1:0] YMIN = -(1 <<< (OW-1));      // -2^(OW-1)

    reg signed [INW-1:0] nxt;
    always @(posedge clk) begin
        if (!rst_n) begin
            sum <= {INW{1'b0}};
            y   <= {OW{1'b0}};
            ovf <= 1'b0;
        end else begin
            if (start)      nxt = (en ? a * b : {INW{1'b0}});
            else if (en)    nxt = sum + a * b;
            else            nxt = sum;
            sum <= nxt;
            // saturate the full-precision value onto the output each cycle
            if (nxt > YMAX) begin
                y <= YMAX[OW-1:0]; ovf <= 1'b1;
            end else if (nxt < YMIN) begin
                y <= YMIN[OW-1:0]; ovf <= 1'b1;
            end else begin
                y <= nxt[OW-1:0];
            end
        end
    end
endmodule

module tb;
    localparam DW = 8, N = 4, OW = 16;
    reg clk, rst_n, start, en;
    reg  signed [DW-1:0] a, b;
    wire signed [OW-1:0] y;
    wire ovf;
    integer errors, i;

    dot_mac #(.DW(DW), .N(N), .OW(OW)) dut (.clk(clk), .rst_n(rst_n), .start(start), .en(en), .a(a), .b(b), .y(y), .ovf(ovf));

    initial clk = 1'b0;
    always #5 clk = ~clk;

    reg signed [DW-1:0] a1 [0:N-1];
    reg signed [DW-1:0] b1 [0:N-1];
    reg signed [DW-1:0] a2 [0:N-1];
    reg signed [DW-1:0] b2 [0:N-1];

    task drive_elem;
        input t_start;
        input signed [DW-1:0] t_a; input signed [DW-1:0] t_b;
        begin
            @(negedge clk);
            start = t_start; en = 1'b1; a = t_a; b = t_b;
            @(posedge clk); #1;
        end
    endtask

    initial begin
        errors = 0; start = 0; en = 0; a = 0; b = 0;
        // Vector 1: 3*4 + (-2)*5 + 10*10 + 4*2 = 12 - 10 + 100 + 8 = 110 (no saturation)
        a1[0]=3; a1[1]=-2; a1[2]=10; a1[3]=4;
        b1[0]=4; b1[1]=5;  b1[2]=10; b1[3]=2;
        // Vector 2: 127*127*4 = 64516 > 32767 -> clamp to 32767, ovf=1
        a2[0]=127;a2[1]=127;a2[2]=127;a2[3]=127;
        b2[0]=127;b2[1]=127;b2[2]=127;b2[3]=127;

        rst_n = 0;
        @(negedge clk); @(posedge clk); #1;
        rst_n = 1;
        if (y !== 0 || ovf !== 0) begin $display("FAIL: reset state y=%0d ovf=%0d", y, ovf); errors=errors+1; end
        else $display("PASS: reset clears y and ovf");

        for (i = 0; i < N; i = i + 1) drive_elem((i==0), a1[i], b1[i]);
        @(negedge clk); en = 0; @(posedge clk); #1;
        if (y === 110 && ovf === 1'b0) $display("PASS: dot product vector1 y=%0d ovf=%0d", y, ovf);
        else begin $display("FAIL: vector1 y=%0d (exp 110) ovf=%0d (exp 0)", y, ovf); errors=errors+1; end

        for (i = 0; i < N; i = i + 1) drive_elem((i==0), a2[i], b2[i]);
        @(negedge clk); en = 0; @(posedge clk); #1;
        if (y === 16'sd32767 && ovf === 1'b1) $display("PASS: saturation vector2 y=%0d ovf=%0d", y, ovf);
        else begin $display("FAIL: vector2 y=%0d (exp 32767) ovf=%0d (exp 1)", y, ovf); errors=errors+1; end

        if (errors == 0) $display("ALL TESTS PASSED");
        else $display("TESTS FAILED: %0d error(s)", errors);
        $finish;
    end
endmodule
PASS: reset clears y and ovf
PASS: dot product vector1 y=110 ovf=0
PASS: saturation vector2 y=32767 ovf=1
ALL TESTS PASSED

The log checks two offline-computed dot products. Vector 1 (12 - 10 + 100 + 8 = 110) accumulates without clamping and leaves ovf=0. Vector 2 (127*127*4 = 64516) exceeds the signed 16-bit range, so the output clamps to the maximum 32767 and the sticky overflow flag asserts. Both the clamped value and the flag are asserted against constants, so a broken saturation comparison would print FAIL.

Why MAC is everywhere

The multiply-accumulate is the atom of linear algebra in hardware. A dot product is a sum of products; a matrix multiply is a grid of dot products; a convolution slides a dot product across data. That is why DSP blocks in FPGAs are literally called "DSP slices" built around a hardened multiplier and adder, and why AI accelerators are measured in MACs/second (often quoted as TOPS, two operations - a multiply and an add - per MAC).

  • FIR filters: y[n] = sum(h[k] * x[n-k]) is a MAC chain over the tap coefficients.
  • Matrix multiply / fully-connected layers: each output element is one dot product of a row and a column.
  • Convolution: each output pixel is a MAC over the kernel window.

Because these loops run millions of times, the MAC is almost always the critical, most-replicated, and most power-hungry cell in the datapath - which is why its width, timing, and overflow behavior matter so much.

Accumulator width and guard bits

The product of two n-bit signed numbers needs 2n bits. Summing K such products can grow the magnitude by up to a further ceil(log2 K) bits. So a safe accumulator width is roughly:

ACCW >= 2*DW + ceil(log2(number_of_accumulations))

The extra bits above 2*DW are called guard bits. In Example 1 the 32-bit accumulator over 8-bit operands leaves 16 guard bits - enough for 2^16 accumulations before any risk of wraparound. If you under-size the accumulator, a long FIR or a large matrix row will silently wrap around (two's-complement overflow), producing a plausible-looking but wrong result. When you cannot afford a full-width accumulator, you must either guarantee the input range or saturate, as Example 2 does.

Signed handling

Signed arithmetic in Verilog is easy to get wrong. Keep these rules in mind:

  • Declare operands and the accumulator as signed. If any operand in an expression is unsigned, Verilog evaluates the whole expression as unsigned, and a negative product becomes a huge positive number.
  • When a and b are signed [DW-1:0], the product a*b is signed and is automatically sign-extended to the width of the left-hand side on assignment.
  • Watch the asymmetric range: a signed byte spans -128 to +127. The case -128 * -128 = +16384 is a classic corner the testbench in Example 1 checks explicitly.
  • For saturation constants, use arithmetic shifts (<<<) on signed localparams so the min bound is genuinely negative.

Clear and load semantics

A real MAC needs a clean way to start a new accumulation. Two common schemes appear above:

  • Synchronous clear (clr): Example 1 zeroes the accumulator on the next clock edge when clr is high. This is cheap and predictable and is preferred over an asynchronous clear in synchronous designs.
  • Load-on-first-element (start): Example 2 overwrites the accumulator with the first product instead of adding to stale data, so each burst begins fresh without wasting a cycle on a separate clear. This is the typical pattern for streaming dot products.

Note the priority ordering in Example 1: rst_n beats clr, which beats en. Getting this priority explicit and correct avoids ambiguous behavior when several controls assert at once.

Pipelining the multiplier

The examples here compute a*b and the add in a single cycle, which keeps them readable but limits the clock frequency - the multiplier plus the adder sit on one combinational path. In high-throughput designs you pipeline the MAC: register the product in one stage and perform the accumulate add in the next. This shortens the critical path and raises fmax, at the cost of one cycle of latency and a bookkeeping change - the accumulator result for a given input now appears a cycle later.

When pipelining, remember that the clr/start and en controls must be delayed to line up with the data they govern, or the first product of a burst will be added to the previous burst's tail. On FPGAs, the DSP slice already contains pipeline registers around its multiplier and accumulator; inferring them (by registering the product) lets the tool map the whole MAC into one hardened slice at full speed.

FAQ

Why is the accumulator wider than the product?

Because you add many products together. A single 8x8 product fits in 16 bits, but summing thousands of them needs guard bits on top, or the sum wraps around. Size the accumulator for the worst-case number of accumulations.

What is the difference between truncation, rounding, and saturation on the output?

Truncation just drops low bits (fast, biased). Rounding adds half an LSB before truncating (less biased). Saturation, shown in Example 2, clamps out-of-range values to the max/min instead of wrapping - essential in DSP, where a wrapped sample sounds or looks far worse than a clamped one.

Does this synthesize to an FPGA DSP block?

Yes. A signed * signed followed by an accumulate maps naturally onto a DSP slice (e.g. Xilinx DSP48, Intel DSP block). Registering the product helps the tool absorb the whole MAC into one slice at high clock speed.

Can I change the operand width?

Both modules are parameterized. Override DW, ACCW (or N/OW) at instantiation - no edits to the body are needed.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD