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 PASSEDThe log proves the accumulator tracks a golden sum-of-products across nine cycles: multiply-and-add, an
en=0hold, a mid-streamclrthat zeroes the running total, and the signed extremes-128*127and-128*-128handled correctly. Because the golden value is computed independently in the testbench, any sign-extension or timing bug would print a FAIL line instead ofALL 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
ovfflag 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 endmodulePASS: reset clears y and ovf PASS: dot product vector1 y=110 ovf=0 PASS: saturation vector2 y=32767 ovf=1 ALL TESTS PASSEDThe log checks two offline-computed dot products. Vector 1 (
12 - 10 + 100 + 8 = 110) accumulates without clamping and leavesovf=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
2nbits. SummingKsuch products can grow the magnitude by up to a furtherceil(log2 K)bits. So a safe accumulator width is roughly:ACCW >= 2*DW + ceil(log2(number_of_accumulations))The extra bits above
2*DWare 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
aandbaresigned [DW-1:0], the producta*bis signed and is automatically sign-extended to the width of the left-hand side on assignment.- Watch the asymmetric range: a signed byte spans
-128to+127. The case-128 * -128 = +16384is 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 whenclris 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_nbeatsclr, which beatsen. Getting this priority explicit and correct avoids ambiguous behavior when several controls assert at once.Pipelining the multiplier
The examples here compute
a*band 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 raisesfmax, 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/startandencontrols 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 * signedfollowed 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(orN/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 LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
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 Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres