12 · DSP multipliers vs logic
The MAX 10 has hardware multipliers. This example runs the same multiply-accumulate engine twice, once in them and once in ordinary logic, checks both against Python, and measures what each costs.
Verified on the board 2026-10-24
Quartus Prime Lite 17.1
No shield needed
Skill: use-dsp-multipliers
The repo keeps the full project (.qar, plus the sources here) but no prebuilt bitstream: one build (a few minutes) makes the .sof on your computer.
The result
| One 18 × 18 signed multiplier | Embedded multiplier (DSP) | Logic elements |
|---|---|---|
| Resources | 2 multiplier elements, 0 logic elements | 368 logic elements |
| Fastest clock for the engine (Fmax, slow corner) | 175 to 187 MHz | 81 to 93 MHz |
| Runs here at | 100 MHz | 75 MHz (at 100 MHz it failed timing by 0.78 ns) |
| Result of 1,000,000 multiply-adds | 01E17200A928 | 01E17200A928 |
The Python reference (sim/mac_reference.py) gives 01E17200A928 too. The 10M50 has 288 multiplier elements
(144 of 18 × 18), so a design can have up to 144 multiplies running every clock for free. Fmax moves a few MHz between builds;
the ranges are from the builds of this example.

What you will see
| Where | What |
|---|---|
| HEX3..HEX0 | PASS when both engines match the expected result, FAIL if not, RUN while running |
| LEDR0, LEDR1, LEDR9 | DSP done, logic done, pass |
| KEY0 | Run again (each run is 10 ms at 100 MHz) |
| Your PC | DSP acc=01E17200A928 cycles=000F4241 and the same for LOGIC (0xF4241 = 1,000,001 clocks) |
Block diagram
How to choose where a multiply goes
(* multstyle = "dsp" *) // or "logic"
module mult18_dsp (input signed [17:0] a, input signed [17:0] b, output signed [35:0] p);
assign p = a * b;
endmodule
- By default Quartus puts every
*it can into the embedded multipliers. Most of the time that is what you want. - Put the
multstyleattribute on a module. On a wire inside agenerateblock Quartus 17.1 ignored it and both engines ended up in DSP blocks: check the fitter's per-entity report ("DSP Elements" column) to see where a multiply really went. - Each embedded multiplier is 18 × 18 (or two 9 × 9). Wider products use several, plus adders in logic.
- Register the product before adding it (the engine does:
p <= a * b; acc <= acc + p;): one multiply-add per clock and an easy timing path.
Things learned building it
- A result that crosses clocks after a "done" handshake still shows up as a timing failure (here −0.24 ns from 75 MHz into 50 MHz).
It is safe because the value is not changing when it is read; say so in the
.sdc:set_false_path -from [get_registers {*mac:*|acc[*] *mac:*|cycles[*]}] -to [get_clocks clk50](src/timing.sdc). - Checking against a reference model (Python) catches everything: off-by-one in the pipeline, wrong signs, wrong widths.
Run it
python scripts/de10.py build examples/12_dsp_multipliers
python scripts/de10.py program examples/12_dsp_multipliers
python scripts/de10.py log 2
Build it yourself
python scripts/new_project.py examples/12_dsp_multipliers mult_top --lib pll print seg7
python scripts/de10.py build examples/12_dsp_multipliers
Test without the board
python examples/12_dsp_multipliers/sim/mac_reference.py 1000 → acc=FFDBBCBDE1C7
python scripts/sim.py examples/12_dsp_multipliers/sim/tb_mac.sv → both styles FFDBBCBDE1C7 PASS
Files
| File | What |
|---|---|
src/mult_top.sv | Top: PLL, two engines, compare, display, report |
src/mac.sv | The engine, plus mult18_dsp and mult18_logic |
src/timing.sdc | Clocks and the false path for the results |
sim/mac_reference.py, sim/tb_mac.sv | Python reference and unit test |