The DSP48 Primitive - Small Multiplications - Two For the Price of One

The DSP48E2 primitive contains a signed 27x18 multiplier, any signed multiplier up to this size can be implemented with just one such primitive. Unsigned multiplications are of course possible if you add a zero MSB bit to the operands and then treat them as signed but the largest unsigned multiplication that can be done that way with one DSP48E2 is 26x17.

When the operands are much smaller it becomes possible to implement two signed multiplications with a single DSP48E2. The largest signed operand sizes for this is possible is 9x9 and a further restriction is that one operand must be shared between the two multipliers. So we can do OA=IA*IC and OB=IB*IC at the same time with one DSP48E2, where IA, IB and IC are signed, 9-bit numbers.

The basic idea is to concatenate IB and IA, separated by 9 zero bits as the DSP48 27-bit A input and sign extend IC as the 18-bit DSP48E2 B input. The two OA and OB 18-bit signed results can then be extracted from the DSP48E2 48-bit output. If the signed inputs IA and IC are positive, or if we interpret the 9-bit IA, IB and IC operands as unsigned this actually works really well. However, when IA or IC are negative then OB and even OA will produce incorrect results. This can be however corrected using the DSP48E2 C input port - the situations that generate incorrect results are detected and a non-zero correction value is applied to the C port, which added to the A*B product then produces the expected output.

As was the case with earlier examples, a USE_LUTs generic is used to select between a behavioral, fabric based implementation or the DSP48E2 based one:

library IEEE;
use IEEE.STD_LOGIC_1164.all;
use
IEEE.NUMERIC_STD.all;

use
work.types_pkg.all;

entity
DUAL_MUL9x9 is

generic(USE_LUTs:BOOLEAN:=FALSE);

port(CLK:in STD_LOGIC;
IA,IB,IC:
in SFIXED(8 downto 0);
OA,OB:
out SFIXED(17 downto 0));
end DUAL_MUL9x9;

architecture TEST of DUAL_MUL9x9 is
begin
i0:if USE_LUTs generate

signal A1:SFIXED(IA'range):=(others=>'0');

signal B1:SFIXED(IB'range):=(others=>'0');

signal C1:SFIXED(IC'range):=(others=>'0');

signal MA:SFIXED(IA'high+IC'high+1 downto IA'low+IC'low):=(others=>'0');

signal MB:SFIXED(IB'high+IC'high+1 downto IB'low+IC'low):=(others=>'0');

begin

process(CLK)

begin

if rising_edge(CLK) then
A1<=IA;
B1<=IB;
C1<=IC;
MA<=A1*C1;
MB<=B1*C1;
OA<=RESIZE(MA,OA);
OB<=RESIZE(MB,OB);

end if;

end process;

end;

else i1: generate

signal A:SFIXED(26 downto 0);

signal B:SFIXED(17 downto 0);

signal C:SFIXED(47 downto 0):=(others=>'0');

signal D:SFIXED(26 downto 0);

signal P:SFIXED(47 downto 0);

begin
A<=IB&"000000000"&IA;
B<=RESIZE(IC,B);
D<=TO_SFIXED(
0.0,D);
-- correction term applied to the DSP48E2 C input port

process(CLK)

begin

if rising_edge(CLK) then

if IA(IA'high)='1' then
C<=RESIZE(-SHIFT_LEFT(IC,9),C);
C(
47 downto 18)<=30x"000000000";

elsif IC(IC'high)='1' then

if IA=TO_SFIXED(0.0,IA) then
C<=48x"000000000000";

else
C<=48x"000000040000";

end if;

else
C<=48x"000000000000";

end if;

end if;

end process;

dx:
entity work.DSP48E2GW port map(CLK=>CLK,
INMODE=>
"10001",     -- use A1 and B1
OPMODE=>"000110101", -- P<=C+A*B
A=>A,
B=>B,
C=>C,
D=>D,
P=>P);
OA<=P(
17 downto 0);
OB<=P(
35 downto 18);

end;

end generate;
end TEST;

At the cost of 8 LUTs and 10 FFs we were able to implement two 9x9 signed multiplications with a shared input with a single DSP48E2. The fabric based version uses 198 LUTs and 141 FFs and they are functionally equivalent. Both versions can run at the maximum clock speed permitted by the data sheet for a particular FPGA family and speed grade. If the 9-bit operands are unsigned then no C correction factor is necessary.

One final point to make here - the design is not trivial, it is not obvious what exact correction values are required for various signed operand values and it is easy to misconfigure the DSP48E2 primitive. The most insidious problems are rare corner cases, when the design seems to work well but still fails for some very particular input value combinations. To make sure the design is correct I created a tesbench that does an exhaustive comparison with a golden model for all the 2**27 possible input values. This testbench is a good example on how to achieve such type of a testing, so here it is:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

library STD;
use STD.textio.all;
use STD.env.all;

use work.types_pkg.all;

entity TB_DUAL_MUL9x9 is

generic(USE_LUTs:BOOLEAN:=FALSE);
end TB_DUAL_MUL9x9;

architecture
TB of TB_DUAL_MUL9x9 is

signal CLK:STD_LOGIC:='1';

signal IA,IB,IC:SFIXED(8 downto 0):=(others=>'0');

signal OA,OB:SFIXED(17 downto 0);

signal OA1,OB1,OA2,OB2,OA3,OB3:SFIXED(17 downto 0):=(others=>'0');
begin
CLK<=not CLK after 1.0 ns;
-- input stimulus

process

begin

wait until rising_edge(CLK);
for AI in -256 to 255 loop
IA<=TO_SFIXED(REAL(AI),IA);

for BI in -256 to 255 loop
IB<=TO_SFIXED(REAL(BI),IB);

for CI in -256 to 255 loop
IC<=TO_SFIXED(REAL(CI),IC);

wait until rising_edge(CLK);

end loop;

end loop;

end loop;

for K in 1 to 3 loop

wait until rising_edge(CLK);

end loop;
stop(
0);

end process;
-- unit under test
uut:entity work.DUAL_MUL9x9 generic map(USE_LUTs=>USE_LUTs)

port map(CLK=>CLK,
IA=>IA,
IB=>IB,
IC=>IC,
OA=>OA,
OB=>OB);
-- golden reference model with the same latency as the DSP48E2, 3 clocks

process(CLK)

begin

if rising_edge(CLK) then
OA1<=IA*IC;
OB1<=IB*IC;
OA2<=OA1;
OB2<=OB1;
OA3<=OA2;
OB3<=OB2;

end if;

end process;
-- compare the two outputs and report any mismatches

process

begin

wait until rising_edge(CLK);

assert OA=OA3 and OB=OB3 report "Data Mismatch!" severity error;

end process;
end TB;

Exhaustive testing like this is not always possible but in this case it works due to the smaller size of the operands and ensures that the design functionality is correct.

What about multiplications larger than 27x18? In the next post I will look at how to implement multiple precision multipliers with more than one DSP48E2.

Back to the top: The Art of FPGA Design