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¶
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¶
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:
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?