0
\$\begingroup\$

Taking as an example the following simple multiplication:

library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_arith.all;
use ieee.std_logic_unsigned.all;

entity multiplier is
  port(
    clk : in std_logic;
    a   : in std_logic_vector(15 downto 0);
    b   : in std_logic_vector(15 downto 0);
    p   : out std_logic_vector(31 downto 0)
    -- ready : out std_logic;
  );
end multiplier;

architecture IMP of multiplier is

begin
  process (clk)
  begin
    if clk'event and clk = '1' then
      p <= a * b;
    -- ready <= '1';
    -- else
    -- ready <= '0';
    end if;
  end process;
end IMP;

At the raising edge of input clock, 'a' and 'b' are assumed to be valid, and the product 'p' is processed. For more complex and slower processes, what are the options in VHDL to have an 'operation finished' output?

I've put the ready symbol just as a way to try to exemplify what I'm trying to achieve. I'm assuming that the process could be complicated enough to last more than a clock period.

\$\endgroup\$
3
\$\begingroup\$

Short answer: you can't.

First off, remove the std_logic_unsigned and std_logic_arith packages. They're non-standard (credit: https://electronics.stackexchange.com/a/188677/148777). Use numeric_std instead.

I'm not even sure multiplication (*) is defined for std_logic_vector. If it is, it shouldn't be. It should be defined only for unsigned or signed types, or similar.

In the end, VHDL has no idea how long a * b will take. Maybe the synthesis tool knows your hardware has a multiplication unit and uses that. Maybe it's implemented as a big messy combinational logic block.

In a synchronous design (like your clocked process here), you don't care exactly how long an operation takes, as long as it's finished in time for the next register stage to read it. That is, the propagation delay in the source register, plus the propagation delay in the combinational logic (the multiplication), plus the wire routing delay, must be less than one clock cycle.

You ensure this constraint is met by running static timing analysis (STA). You tell the tool that your clock is (e.g.) 8ns long, and it checks that your multiplication implementation (plus delays, as mentioned) is shorter than that. If it passes, good! If not, you have issues.

You can specify that you know your multiplication will take "n" clock cycles. But if you don't know how it's synthesized, you can't know that. You could try synthesizing, see how long it takes, and count at least that many clock cycles before trying to check the output.

Or, you can write the multiplication logic yourself. When doing that, you can add pipeline stages to ensure each sub-operation is done in one clock cycle, and you know that it will take one clock cycle times the number of pipeline stages to complete. As a bonus, if you design the logic yourself you can add an "output valid" signal. (Hint: this is a good approach.)

Or, you can brute-force the approach and slow down your clock.

In summary there's no simple solution. If you rely on the synthesis tool's implementation of a multiplication operation, you can't know how long it takes until the synthesis is done. If you don't perform STA and you don't inform the STA tool that you're relying on the operation to take "n" clock cycles, you won't be able to notice if a subsequent synthesis run gives you different results.

\$\endgroup\$
  • \$\begingroup\$ Thanks a lot to you and Marcus. That was exactly my point: how can I be sure that when I synthesize it for a different platform it would still be reliable. Too bad I can't tell the synthesis tool, using VHDL: "whatever you do to implement this 'process', let me know now long it takes and enable this bit when it's done". \$\endgroup\$ – vangelo Apr 19 at 14:55
1
\$\begingroup\$

For more complex and slower processes, what are the options in VHDL to have an 'operation finished' output?

For slower processes you can use a two signal handshake (here start and ready):

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- or use ieee.numeric_std_unsigned.all; without type conversions

entity multiplier is
    port (
        clk:    in  std_logic;
        a:      in  std_logic_vector(15 downto 0);
        b:      in  std_logic_vector(15 downto 0);
        start:  in  std_logic;  -- added for two wire handshake 
        p:      out std_logic_vector(31 downto 0);
        ready:  out std_logic
    );                       
end entity multiplier;

architecture slow of multiplier is
    constant NUM_OF_CLKS:   positive := 2;   -- could be generic constant
    signal product:         unsigned(p'range);
    signal rcount:          natural := NUM_OF_CLKS - 1; -- in lieu of reset
    signal rdy:             std_logic := '1';           -- in lieu of reset
begin

DELAYED:
    process (clk) 
    begin
        if rising_edge(clk) then
            if rcount = NUM_OF_CLKS - 1 then
                p <= std_logic_vector(product);
                rdy <= '1';
            elsif start = '1' then
                rdy <= '0';
            end if;
        end if;
    end process;

    product <= unsigned(a) * unsigned(b) after 17 ns;  
               -- after ... ignored by synthesis

    ready <= rdy;  -- pre -2008, ready used internally

READY_DELAY: 
    process (clk)
    begin
        if rising_edge(clk) then
            if  rdy = '1' and start = '0' then
                rcount <= 0;
            elsif rcount /= NUM_OF_CLKS - 1 then
                rcount <= rcount + 1;
            end if;
        end if;
    end process;
end architecture;

In this case a slower multiply takes more than 1 1/2 clock periods from an update on either 'a' or 'b' signaled by start = '1'.

Besides the start and `ready signals a counter is used to determine an integral number of clocks that the multiply takes.

This operation can be shown with a testbench:

library ieee;
use ieee.std_logic_1164.all;

entity multiplier_tb is
end entity;

architecture foo of multiplier_tb is
   component multiplier is
        port (
            clk:    in  std_logic;
            a:      in  std_logic_vector(15 downto 0);
            b:      in  std_logic_vector(15 downto 0);
            start:  in  std_logic;
            p:      out std_logic_vector(31 downto 0);
            ready:  out std_logic := '1' 
        ); 
    end component;
    signal clk:     std_logic := '0';
    signal a:       std_logic_vector (15 downto 0);
    signal b:       std_logic_vector (15 downto 0);
    signal start:   std_logic := '0';
    signal p:       std_logic_vector (31 downto 0);
    signal ready:   std_logic;
begin
DUT:
    multiplier
        port map (
            clk => clk,
            a   => a,
            b   => b,
            start  => start,
            p   => p,
            ready => ready
        );

CLOCK:
    process
    begin
        wait for 5 ns;  -- one half clock period
        clk <= not clk;
        if now > 60 ns then
            wait;
        end if;
    end process;
STIMULI:
    process
    begin
        wait until rising_edge(clk);
        wait until ready = '1' and rising_edge(clk);
        a <= x"0002";
        b <= x"0015";
        start <= '1';
        wait until rising_edge (clk);
        start <= '0';  -- a single clock long
        wait;
    end process;
end architecture;

That produces:

multiplier_tb.jpg

The process taking longer than a clock here shown on signal product requires more than one clock from the inputs a and b being updated. The desired update on register p occurs on detail D.

The combinatorial value of rdy = '1' and start = '0' is used to synchronously reset a counter rcount used to determine the number of clocks (minus one) that a slower multiply takes. This is shown at detail A.

When start = '1' occurs (for one clock) the synchronous reset condition isn't met andrcount` increments (if the multiply takes more than one clock) as shown at detail C.

rcount can increment up to NUM_OF_CLKS - 1 and sticks. The reached value of NUM_OF_CYCLES - 1 is also used to issue ready which as a design goal is a necessary requirement for issuing the next start. When ready = '1' and start = '0' the rcount counter is reset again.

This should work with values from 1 to N where N is POSITIVE'HIGH. For a NUM_OF_CLKS value of 2 as shown rcount is reduced to one flip flop by optimization in synthesis.

The same basic mechanism can be used for complex operations while it may be possible to use events occuring during operation instead of a counter to determin when done (and ready for the next operation). The example here reflects the case illustrated in the question.

In support of Kevin's point of view the information on how many clocks an operation might take could depend on a target silicon device. To deal with that eventuality you could use a generic to provide (here) NUM_OF_CLKS.

\$\endgroup\$
0
\$\begingroup\$

How should VHDL know what "operation finished" means?

It's up to your logic to implement some state machine that outputs some form of "data is valid now" flag; you'll find such logic in many buses, for example AXI4 stream (tvalid).

If that logic is just "it takes N cycles", then, well, implement an N-counter.

\$\endgroup\$
  • \$\begingroup\$ "How should VHDL know": I'm assuming it does not, hence my question. I'd like to explicitly code that once 'p' is valid after the 'p <= a * b' operation, a 'ready' signal should be enabled. If I don't know how exactly the operation will be synthesized, I'd like to avoid fixed delays. \$\endgroup\$ – vangelo Apr 19 at 14:07
  • \$\begingroup\$ well, if you just write a * b, then your synthesizer implements that as combinatorical logic – and thus, it takes at most one cycle, but the maximum cycle frequency, i.e. the minimum cycle duration, will be increased to make that work. So, you don't need that signal – your operation is guaranteed to happen within one cycle, but that cycle might need to become loooong. If you build something pipelined to avoid that, then the logic becomes more complicated, and it's again, still, up to your understanding of your specific problem to define a state machine that emits a "done" signal. \$\endgroup\$ – Marcus Müller Apr 19 at 14:33
  • \$\begingroup\$ so, there's still no "VHDL option" other than "if you write something complex that you yourself divide into multiple cycles, then it's up to you to know how many cycles these are". \$\endgroup\$ – Marcus Müller Apr 19 at 14:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.