Skip to content

Concurrent and sequential logic

The most important VHDL skill is predicting what hardware a coding pattern infers.

Combinational logic

Combinational outputs are functions only of current inputs. They remember nothing.

Direct concurrent form

y_o <= (a_i and b_i) or c_i;

Complete process form

process(all)
begin
  y_o <= '0'; -- default
  if enable_i = '1' then
    y_o <= a_i xor b_i;
  end if;
end process;

The default makes every path assign y_o. The later assignment has priority when enabled.

Accidental latch

process(all)
begin
  if enable_i = '1' then
    y_o <= data_i;
  end if;
end process;

When enable_i='0', the process says to retain the previous value. Retention requires storage, so synthesis infers a level-sensitive latch.

Warning

Latches are not forbidden, but they are rarely intended in beginner FPGA designs and complicate timing. If you need state, use an edge-triggered register.

Clocked logic

A register changes only on a clock edge:

process(clk_i)
begin
  if rising_edge(clk_i) then
    q_o <= d_i;
  end if;
end process;

Synchronous reset and enable

process(clk_i)
begin
  if rising_edge(clk_i) then
    if reset_i = '1' then
      count_q <= (others => '0');
    elsif enable_i = '1' then
      count_q <= count_q + 1;
    end if;
  end if;
end process;

Priority is reset, then enable, then implicit hold.

Asynchronous reset

process(clk_i, reset_i)
begin
  if reset_i = '1' then
    q_o <= '0';
  elsif rising_edge(clk_i) then
    q_o <= d_i;
  end if;
end process;

An asynchronous reset acts without waiting for a clock. Its deassertion near a clock edge can cause recovery/removal problems, so designs often assert asynchronously but release synchronously, or use a fully synchronous reset.

Clock enable, not fabric-generated clocks

Avoid:

slow_clock <= divider_q(25);
process(slow_clock)
begin
  if rising_edge(slow_clock) then
    -- ...
  end if;
end process;

This routes a logic-generated signal as a clock and creates a new clock domain.

Prefer:

process(clk_i)
begin
  if rising_edge(clk_i) then
    if tick_i = '1' then
      -- State advances once per tick while using the real clock.
    end if;
  end if;
end process;

Signal update semantics

Consider:

process(clk_i)
begin
  if rising_edge(clk_i) then
    first_q  <= data_i;
    second_q <= first_q;
  end if;
end process;

Both right-hand sides use values from before the edge. This correctly infers two pipeline registers. The assignments are sequential in source, but signal updates occur after the process suspends.

Variable calculation inside a clocked process

process(clk_i)
  variable next_count : unsigned(count_q'range);
begin
  if rising_edge(clk_i) then
    next_count := count_q;
    if enable_i = '1' then
      next_count := next_count + 1;
    end if;
    count_q <= next_count;
    zero_q  <= '1' when next_count = 0 else '0';
  end if;
end process;

The variable is a calculation aid. Whether it infers storage depends on how it is assigned and used, not on being called a variable.

Multiple drivers

A resolved std_logic signal can technically have multiple drivers, but opposing values resolve to X in simulation and are normally illegal or inappropriate for internal FPGA logic.

Recommended rule:

Each internal RTL signal has exactly one owning process or concurrent assignment.

For a shared choice, build an explicit mux rather than multiple drivers.

Delta cycles

An assignment with no time delay is scheduled for a later delta cycle at the same simulation time. Delta cycles let concurrent processes react without advancing physical time.

a <= '1';
wait for 0 ns; -- one or more delta-cycle advancement, tool/language semantics apply
assert y = expected;

In practical testbenches, using a small non-zero settling delay such as wait for 1 ns for combinational DUTs is clear. For synchronous DUTs, align checks to clock edges and account for the delta cycle after registered outputs update.

Combinational template

process(all)
  variable result_v : unsigned(result_o'range);
begin
  result_v := (others => '0');

  case operation_i is
    when OP_ADD  => result_v := a_i + b_i;
    when OP_XOR  => result_v := a_i xor b_i;
    when OP_PASS => result_v := a_i;
    when others  => null;
  end case;

  result_o <= result_v;
end process;

Sequential template

process(clk_i)
begin
  if rising_edge(clk_i) then
    if reset_i = '1' then
      state_q <= RESET_VALUE;
    else
      state_q <= state_d;
    end if;
  end if;
end process;

Review checklist

  • Can I name the intended gates/registers?
  • Does every combinational output receive a value on every path?
  • Does every stored value change in one clear clocked process?
  • Are there multiple drivers?
  • Is every clock an actual clock routed through clocking resources?
  • Have I used an enable instead of creating a slower fabric clock?
  • Is reset behavior specified and tested?