VHDL · Chapter 7.9 · Sequential Logic Design
Signals vs Variables in Clocked Processes
A subtlety runs through every clocked process: signals and variables can both become registers, but they behave differently in time. A signal assigned under the clock edge is scheduled, so it always reads its old value within the cycle and updates next cycle, making it a register with one cycle of latency. A variable updates immediately, so it is a same-cycle intermediate when written before it is read, and becomes a register only when it is read before it is written and carries state across cycles. Getting this right is the difference between a computation that completes this cycle and one that takes an extra cycle, and it is central to writing correct pipelines. This closing lesson makes the distinction precise in the clocked context and works the accumulator example step by step.
Foundation14 min readVHDLSignalsVariablesRegistersPipelineClocked
1. Engineering intuition — same edge, different timing
Inside a clocked process, both <= (signal) and := (variable) can end up as flip-flops, so it is easy
to assume they are interchangeable. They are not. A signal is scheduled: it keeps its old value for
the rest of this cycle and updates at the edge, so reading it back always gives last cycle's value — it is
inherently a one-cycle-latency register. A variable is immediate: assign it and the next line sees
the new value, so it is a within-cycle intermediate — unless you read it before assigning it, in which
case it must remember last cycle's value and becomes a register. The practical effect is whether a
multi-step computation finishes this cycle or slips to the next.
2. Formal explanation — three cases
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
process (clk)
variable v : unsigned(7 downto 0);
begin
if rising_edge(clk) then
-- (1) SIGNAL: scheduled → register, value next cycle.
s2 <= s1; -- s2 gets the OLD s1 → one cycle of latency (a flip-flop)
-- (2) VARIABLE written-before-read: same-cycle intermediate (combinational inside the reg).
v := unsigned(a) + unsigned(b);
y <= std_logic_vector(v); -- uses THIS cycle's v → no extra latency
-- (3) VARIABLE read-before-write: carries state across cycles → a register.
v := v + 1; -- reads last cycle's v → v is itself a register
end if;
end process;In a clocked process: a signal <= is always a register (scheduled, next-cycle value); a variable
:= written before read is a same-cycle intermediate (combinational logic feeding the registered
outputs); a variable := read before written is a register (it must hold across cycles). The same
syntax, three different timings — decided by scheduled-vs-immediate and access order (lessons 5.3, 5.7).
3. Production RTL — variable vs signal accumulator
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
-- VARIABLE intermediate: compute and register the result in ONE cycle.
var_acc : process (clk)
variable t : unsigned(15 downto 0);
begin
if rising_edge(clk) then
t := unsigned(acc) + unsigned(x); -- immediate
acc <= std_logic_vector(t); -- result available next cycle, computed from THIS x
end if;
end process;
-- SIGNAL intermediate: an extra register stage → one more cycle of latency.
sig_acc : process (clk)
begin
if rising_edge(clk) then
t_s <= unsigned(acc2) + unsigned(x); -- scheduled (registered intermediate)
acc2 <= std_logic_vector(t_s); -- uses the OLD t_s → acc2 lags by an extra cycle
end if;
end process;What hardware does this become? The variable version is an adder feeding one register (acc) — the
intermediate t is combinational. The signal version inserts an extra register (t_s) before acc2,
adding a pipeline stage and one more cycle of latency. Same arithmetic; the signal-vs-variable choice
decides whether you get one register or two.
4. Hardware interpretation — intermediate wire vs extra register
5. Simulation interpretation — the one-cycle difference
Variable accumulator (same cycle) vs signal-intermediate accumulator (extra cycle)
8 cycles6. Debugging example — the accidental extra pipeline stage
Expected: a clocked computation completes with the intended latency. Observed: the result is one cycle later than expected (or a value is a cycle stale). Root cause: an intermediate was held in a signal and read back in the same clocked process, so it read the old value and added an unintended register stage; the designer expected variable-like same-cycle behaviour. Fix: use a variable for same-cycle intermediates inside a clocked process, reserving signal intermediates for when you genuinely want a pipeline register. Engineering takeaway: in a clocked process, each signal intermediate you read back is an extra register (a pipeline stage); use variables for same-cycle math and count your signal stages deliberately.
-- BUG: signal intermediate read back → extra register, +1 cycle latency.
-- t_s <= a + b; y <= t_s + 1; -- reads OLD t_s
-- FIX: variable intermediate → same-cycle, one register.
process (clk)
variable t : unsigned(7 downto 0);
begin
if rising_edge(clk) then
t := unsigned(a) + unsigned(b); -- immediate
y <= std_logic_vector(t + 1); -- uses NEW t → no extra stage
end if;
end process;7. Common mistakes & what to watch for
- Reading a signal intermediate back in a clocked process. It is the old value and adds a register stage; use a variable for same-cycle intermediates.
- Assuming a variable is never a register. Read-before-write across cycles makes it a register; that is fine when intended (an accumulator), a surprise when not.
- Miscounting pipeline depth. Each signal stage is one cycle; chains of signal assignments add latency per stage.
- Using variables for cross-process state. Variables are process-local; observable state shared across processes must be a signal.
- Mixing same-cycle and pipelined intent unclearly. Decide deliberately where you want registers (signals) versus combinational intermediates (variables).
8. Engineering insight & continuity
In a clocked process the signal-vs-variable choice is really a latency choice: signals are scheduled and thus registers (one stage each), variables are immediate and thus same-cycle intermediates unless read before written. Use variables for the math you want to finish this cycle and signals (or explicit extra stages) where you want pipeline registers — and always count your registered signal stages. With this, Module 7 is complete: edges, flip-flops, registers, enables, counters, shift registers, the gated-clock hazard, and now register timing. All of this state needs a defined starting point, which is the subject of the next module — Reset Design — beginning with why reset matters and the synchronous-versus- asynchronous choice.