Variables vs signals in VHDL
VHDL gives you two kinds of object you can assign inside a process, and the
difference between them is the difference between two circuits. Signals
(<=) don’t take their new value until the process suspends; every read
during the process sees the old value. Variables (:=) update
immediately, like a line of software; the next statement sees the new value.
If you know Verilog, this is the
blocking vs nonblocking story
told with types instead of operators: signal assignment behaves like <=,
variable assignment like =. The trap just moves: in Verilog you pick the
wrong operator, in VHDL you pick the wrong kind of object.
The demonstration: a flip-flop disappears
With signals, this clocked process is a two-stage shift register. The
second line reads stage1’s old value, because the new one hasn’t landed
yet:
process (clk) is
begin
if rising_edge(clk) then
stage1 <= d; -- signals: stage2 reads the OLD stage1
stage2 <= stage1; -- two flip-flops, a real shift register
end if;
end process;
Live, and editable. Count the registers: two.
Interactive schematic by RapidRTL
Now the same shape written with a variable in the middle. The variable
updates immediately, so stage2 reads the new value, which is just d.
The middle stage isn’t storage any more, and synthesis agrees. Count the
registers: one.
Interactive schematic by RapidRTL
A flip-flop vanished because of one token. That’s the entire subject, made visible: a variable read after it’s written in the same clock edge is a wire, not a register. Whether that’s a bug or exactly what you wanted depends on which circuit you meant to describe.
When variables are the right tool
The example above makes variables look like a hazard, but the immediate update is genuinely useful wherever you’re computing, not storing:
Intermediate values inside one edge. A sum built in steps, a parity, a byte-swap: things that exist only as scaffolding between the inputs and the one register that stores the result.
process (clk) is
variable parity : std_logic;
begin
if rising_edge(clk) then
parity := '0';
for i in data'range loop
parity := parity xor data(i); -- accumulates across the loop
end loop;
parity_reg <= parity; -- one register, at the end
end if;
end process;
That loop needs immediate update: each iteration must see the previous one’s result. Written with a signal, every iteration would read the same old value, the loop would collapse to a single XOR, and simulation would disagree with your intent. When you accumulate, you want variable semantics; when you store across clock edges, you want a signal.
Keeping scope tight. A variable exists only inside its process, which is the honest scope for scaffolding. (VHDL does have shared variables across processes; treat them as a specialist tool you’ll know when you need, which is approximately never in synthesizable code.)
The rules
- Storage that survives between clock edges: a signal, assigned with
<=in a clocked process. One signal, one process driving it. - Scaffolding consumed in the same edge it’s computed: a variable. If you never read it before writing it, it didn’t need to be a variable.
- Communication between processes: signals, always. Variables can’t leave the process, and that’s a feature.
- A variable that’s read before it’s written in some path carries its value over from the previous execution: that’s storage you didn’t label as storage, VHDL’s quietest way to infer a latch or an accidental register. Initialise variables at the top of the process unless you specifically mean to keep state in one.
Why simulation makes it feel fine
Both versions of the shift register simulate cleanly; nothing warns you that a register evaporated. The mismatch only becomes visible when you look at the netlist, which is exactly what the two schematics above are. It’s the same moral as the latch guide: simulation checks what the code does, synthesis reveals what the code is, and the habit worth building is looking at both.