This example deals with one of the most fundamental aspects of the VHDL language: the simulation semantics. It is intended for VHDL beginners and presents a simplified view where many details have been omitted (postponed processes, VHDL Procedural Interface, shared variables...) Readers interested in the real complete semantics shall refer to the Language Reference Manual (LRM).
Most classical imperative programming languages use variables. They are value containers. An assignment operator is used to store a value in a variable:
a = 15;
and the value currently stored in a variable can be read and used in other statements:
if(a == 15) { print "Fifteen" }
VHDL also uses variables and they have exactly the same role as in most imperative languages. But VHDL also offers another kind of value container: the signal. Signals also store values, can also be assigned and read. The type of values that can be stored in signals is (almost) the same as in variables.
So, why having two kinds of value containers? The answer to this question is essential and at the heart of the language. Understanding the difference between variables and signals is the very first thing to do before trying to program anything in VHDL.
Let us illustrate this difference on a concrete example: the swapping.
Note: all the following code snippets are parts of processes. We will see later what processes are.
tmp := a;
a := b;
b := tmp;
swaps variables a
and b
. After executing these 3 instructions, the new content of a
is the old content of b
and conversely. Like in most programming languages, a third temporary variable (tmp
) is needed. If, instead of variables, we wanted to swap signals, we would write:
r <= s;
s <= r;
or:
s <= r;
r <= s;
with the same result and without the need of a third temporary signal!
Note: the VHDL signal assignment operator
<=
is different from the variable assignment operator:=
.
Let us look at a second example in which we assume that the print
subprogram prints the decimal representation of its parameter. If a
is an integer variable and its current value is 15, executing:
a := 2 * a;
a := a - 5;
a := a / 5;
print(a);
will print:
5
If we execute this step by step in a debugger we can see the value of a
changing from the initial 15 to 30, 25 and finally 5.
But if s
is an integer signal and its current value is 15, executing:
s <= 2 * s;
s <= s - 5;
s <= s / 5;
print(s);
wait on s;
print(s);
will print:
15
3
If we execute this step by step in a debugger we will not see any value change of s
until after the wait
instruction. Moreover, the final value of s
will not be 15, 30, 25 or 5 but 3!
This apparently strange behavior is due the fundamentally parallel nature of digital hardware, as we will see in the following sections.
VHDL being a Hardware Description Language (HDL), it is parallel by nature. A VHDL program is a collection of sequential programs that run in parallel. These sequential programs are called processes:
P1: process
begin
instruction1;
instruction2;
...
instructionN;
end process P1;
P2: process
begin
...
end process P2;
The processes, just like the hardware they are modelling, never end: they are infinite loops. After executing the last instruction, the execution continues with the first.
As with any programming language that supports one form or another of parallelism, a scheduler is responsible for deciding which process to execute (and when) during a VHDL simulation. Moreover, the language offers specific constructs for inter-process communication and synchronization.
The scheduler maintains a list of all processes and, for each of them, records its current state which can be running
, run-able
or suspended
. There is at most one process in running
state: the one that is currently executed. As long as the currently running process does not execute a wait
instruction, it continues running and prevents any other process from being executed. The VHDL scheduler is not preemptive: it is each process responsibility to suspend itself and let other processes run. This is one of the problems that VHDL beginners frequently encounter: the free running process.
P3: process
variable a: integer;
begin
a := s;
a := 2 * a;
r <= a;
end process P3;
Note: variable
a
is declared locally while signalss
andr
are declared elsewhere, at a higher level. VHDL variables are local to the process that declares them and cannot be seen by other processes. Another process could also declare a variable nameda
, it would not be the same variable as the one of processP3
.
As soon as the scheduler will resume the P3
process, the simulation will get stuck, the simulation current time will not progress anymore and the only way to stop this will be to kill or interrupt the simulation. The reason is that P3
has not wait
statement and will thus stay in running
state forever, looping over its 3 instructions. No other process will ever be given a chance to run, even if it is run-able
.
Even processes containing a wait
statement can cause the same problem:
P4: process
variable a: integer;
begin
a := s;
a := 2 * a;
if a = 16 then
wait on s;
end if;
r <= a;
end process P4;
Note: the VHDL equality operator is
=
.
If process P4
is resumed while the value of signal s
is 3, it will run forever because the a = 16
condition will never be true.
Let us assume that our VHDL program does not contain such pathological processes. When the running process executes a wait
instruction, it is immediately suspended and the scheduler puts it in the suspended
state. The wait
instruction also carries the condition for the process to become run-able
again. Example:
wait on s;
means suspend me until the value of signal s
changes. This condition is recorded by the scheduler. The scheduler then selects another process among the run-able
, puts it in running
state and executes it. And the same repeats until all run-able
processes have been executed and suspended.
Important note: when several processes are
run-able
, the VHDL standard does not specify how the scheduler shall select which one to run. A consequence is that, depending on the simulator, the simulator's version, the operating system, or anything else, two simulations of the same VHDL model could, at one point, make different choices and select a different process to execute. If this choice had an impact on the simulation results, we could say that VHDL is non-deterministic. As non-determinism is usually undesirable, it would be the responsibility of the programmers to avoid non-deterministic situations. Fortunately, VHDL takes care of this and this is where signals enter the picture.
VHDL avoids non determinism using two specific characteristics:
signal r, s: integer; -- Common to all processes
...
P5: process
variable a: integer; -- Different from variable a of process P6
begin
a := s + 1;
r <= a;
a := r + 1;
wait on s;
end process P5;
P6: process
variable a: integer; -- Different from variable a of process P5
begin
a := r + 1;
s <= a;
wait on r;
end process P6;
Note: VHDL comments extend from
--
to the end of the line.
Every time a signal is assigned, the assigned value is recorded by the scheduler but the current value of the signal remains unchanged. This is another major difference with variables that take their new value immediately after being assigned.
Let us look at an execution of process P5
above and assume that a=5
, s=1
and r=0
when it is resumed by the scheduler. After executing instruction a := s + 1;
, the value of variable a
changes and becomes 2 (1+1). When executing the next instruction r <= a;
it is the new value of a
(2) that is assigned to r
. But r
being a signal, the current value of r
is still 0. So, when executing a := r + 1;
, variable a
takes (immediately) value 1 (0+1), not 3 (2+1) as the intuition would say.
When will signal r
really take its new value? When the scheduler will have executed all run-able processes and they will all be suspended. This is also referred to as: after one delta cycle. It is only then that the scheduler will look at all the values that have been assigned to signals and actually update the values of the signals. A VHDL simulation is an alternation of execution phases and signal update phases. During execution phases, the value of the signals is frozen. Symbolically, we say that between an execution phase and the following signal update phase a delta of time elapsed. This is not real time. A delta cycle has no physical duration.
Thanks to this delayed signal update mechanism, VHDL is deterministic. Processes can communicate only with signals and signals do not change during the execution of the processes. So, the order of execution of the processes does not matter: their external environment (the signals) does not change during the execution. Let us show this on the previous example with processes P5
and P6
, where the initial state is P5.a=5
, P6.a=10
, s=17
, r=0
and where the scheduler decides to run P5
first and P6
next. The following table shows the value of the two variables, the current and next values of the signals after executing each instruction of each process:
process / instruction | P5.a | P6.a | s.current | s.next | r.current | r.next |
---|---|---|---|---|---|---|
Initial state | 5 | 10 | 17 | 0 | ||
P5 / a := s + 1 | 18 | 10 | 17 | 0 | ||
P5 / r <= a | 18 | 10 | 17 | 0 | 18 | |
P5 / a := r + 1 | 1 | 10 | 17 | 0 | 18 | |
P5 / wait on s | 1 | 10 | 17 | 0 | 18 | |
P6 / a := r + 1 | 1 | 1 | 17 | 0 | 18 | |
P6 / s <= a | 1 | 1 | 17 | 1 | 0 | 18 |
P6 / wait on r | 1 | 1 | 17 | 1 | 0 | 18 |
After signal update | 1 | 1 | 1 | 18 |
With the same initial conditions, if the scheduler decides to run P6
first and P5
next:
process / instruction | P5.a | P6.a | s.current | s.next | r.current | r.next |
---|---|---|---|---|---|---|
Initial state | 5 | 10 | 17 | 0 | ||
P6 / a := r + 1 | 5 | 1 | 17 | 0 | ||
P6 / s <= a | 5 | 1 | 17 | 1 | 0 | |
P6 / wait on r | 5 | 1 | 17 | 1 | 0 | |
P5 / a := s + 1 | 18 | 1 | 17 | 1 | 0 | |
P5 / r <= a | 18 | 1 | 17 | 1 | 0 | 18 |
P5 / a := r + 1 | 1 | 1 | 17 | 1 | 0 | 18 |
P5 / wait on s | 1 | 1 | 17 | 1 | 0 | 18 |
After signal update | 1 | 1 | 1 | 18 |
As we can see, after the execution of our two processes, the result is the same whatever the order of execution.
This counter-intuitive signal assignment semantics is the reason of a second type of problems that VHDL beginners frequently encounter: the assignment that apparently does not work because it is delayed by one delta cycle. When running process P5
step-by-step in a debugger, after r
has been assigned 18 and a
has been assigned r + 1
, one could expect that the value of a
is 19 but the debugger obstinately says that r=0
and a=1
...
Note: the same signal can be assigned several times during the same execution phase. In this case, it is the last assignment that decides the next value of the signal. The other assignments have no effect at all, just like if they never had been executed.
It is time to check our understanding: please go back to our very first swapping example and try to understand why:
process
begin
---
s <= r;
r <= s;
---
end process;
actually swaps signals r
and s
without the need of a third temporary signal and why:
process
begin
---
r <= s;
s <= r;
---
end process;
would be strictly equivalent. Try to understand also why, if s
is an integer signal and its current value is 15, and we execute:
process
begin
---
s <= 2 * s;
s <= s - 5;
s <= s / 5;
print(s);
wait on s;
print(s);
---
end process;
the two first assignments of signal s
have no effect, why s
is finally assigned 3 and why the two printed values are 15 and 3.
In order to model hardware it is very useful to be able to model the physical time taken by some operation. Here is an example of how this can be done in VHDL. The example models a synchronous counter and it is a full, self-contained, VHDL code that could be compiled and simulated:
-- File counter.vhd
entity counter is
end entity counter;
architecture arc of counter is
signal clk: bit; -- Type bit has two values: '0' and '1'
signal c, nc: natural; -- Natural (non-negative) integers
begin
P1: process
begin
clk <= '0';
wait for 10 ns; -- Ten nano-seconds delay
clk <= '1';
wait for 10 ns; -- Ten nano-seconds delay
end process P1;
P2: process
begin
if clk = '1' and clk'event then
c <= nc;
end if;
wait on clk;
end process P2;
P3: process
begin
nc <= c + 1 after 5 ns; -- Five nano-seconds delay
wait on c;
end process P3;
end architecture arc;
In process P1
the wait
instruction is not used to wait until the value of a signal changes, like we saw up to now, but to wait for a given duration. This process models a clock generator. Signal clk
is the clock of our system, it is periodic with period 20 ns (50 MHz) and has duty cycle.
Process P2
models a register that, if a rising edge of clk
just occurred, assigns the value of its input nc
to its output c
and then waits for the next value change of clk
.
Process P3
models an incrementer that assigns the value of its input c
, incremented by one, to its output nc
... with a physical delay of 5 ns. It then waits until the value of its input c
changes. This is also new. Up to now we always assigned signals with:
s <= value;
which, for the reasons explained in the previous sections, we can implicitly translate into:
s <= value; -- after delta
This small digital hardware system could be represented by the following figure:
With the introduction of the physical time, and knowing that we also have a symbolic time measured in delta, we now have a two dimensional time that we will denote T+D
where T
is a physical time measured in nano-seconds and D
a number of deltas (with no physical duration).
There is one important aspect of the VHDL simulation that we did not discuss yet: after an execution phase all processes are in suspended
state. We informally stated that the scheduler then updates the values of the signals that have been assigned. But, in our example of a synchronous counter, shall it update signals clk
, c
and nc
at the same time? What about the physical delays? And what happens next with all processes in suspended
state and none in run-able
state?
The complete (but simplified) simulation algorithm is the following:
Tc
to 0+0 (0 ns, 0 delta-cycle)wait
statement.
Tn
as the earliest of:
wait for <delay>
.Tc=Tn
.run-able
state all processes that were waiting for a value change of one of the signals that has been updated.run-able
state all processes that were suspended by a wait for <delay>
statement and for which the resume time is Tc
.Tn
as the earliest of:
wait for <delay>
.Tn
is infinity, stop simulation. Else, start a new simulation cycle.To conclude, let us now manually exercise the simplified simulation algorithm on the synchronous counter presented above. We arbitrary decide that, when several processes are run-able, the order will be P3
>P2
>P1
. The following tables represent the evolution of the state of the system during the initialization and the first simulation cycles. Each signal has its own column in which the current value is indicated. When a signal assignment is executed, the scheduled value is appended to the current value, e.g. a/b@T+D
if the current value is a
and the next value will be b
at time T+D
(physical time plus delta cycles). The 3 last columns indicate the condition to resume the suspended processes (name of signals that must change or time at which the process shall resume).
Operations | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Set current time | 0+0 | |||||||
Initialize all signals | 0+0 | '0' | 0 | 0 | ||||
P3/nc<=c+1 after 5 ns | 0+0 | '0' | 0 | 0/1@5+0 | ||||
P3/wait on c | 0+0 | '0' | 0 | 0/1@5+0 | c | |||
P2/if clk='1'... | 0+0 | '0' | 0 | 0/1@5+0 | c | |||
P2/end if | 0+0 | '0' | 0 | 0/1@5+0 | c | |||
P2/wait on clk | 0+0 | '0' | 0 | 0/1@5+0 | clk | c | ||
P1/clk<='0' | 0+0 | '0'/'0'@0+1 | 0 | 0/1@5+0 | clk | c | ||
P1/wait for 10 ns | 0+0 | '0'/'0'@0+1 | 0 | 0/1@5+0 | 10+0 | clk | c | |
Compute next time | 0+0 | 0+1 | '0'/'0'@0+1 | 0 | 0/1@5+0 | 10+0 | clk | c |
Operations | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Set current time | 0+1 | '0'/'0'@0+1 | 0 | 0/1@5+0 | 10+0 | clk | c | |
Update signals | 0+1 | '0' | 0 | 0/1@5+0 | 10+0 | clk | c | |
Compute next time | 0+1 | 5+0 | '0' | 0 | 0/1@5+0 | 10+0 | clk | c |
Note: during the first simulation cycle there is no execution phase because none of our 3 processes has its resume condition satisfied.
P2
is waiting for a value change ofclk
and there has been a transaction onclk
, but as the old and new values are the same, this is not a value change.
Operations | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Set current time | 5+0 | '0' | 0 | 0/1@5+0 | 10+0 | clk | c | |
Update signals | 5+0 | '0' | 0 | 1 | 10+0 | clk | c | |
Compute next time | 5+0 | 10+0 | '0' | 0 | 1 | 10+0 | clk | c |
Note: again, there is no execution phase.
nc
changed but no process is waiting onnc
.
Operations | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Set current time | 10+0 | '0' | 0 | 1 | 10+0 | clk | c | |
Update signals | 10+0 | '0' | 0 | 1 | 10+0 | clk | c | |
P1/clk<='1' | 10+0 | '0'/'1'@10+1 | 0 | 1 | clk | c | ||
P1/wait for 10 ns | 10+0 | '0'/'1'@10+1 | 0 | 1 | 20+0 | clk | c | |
Compute next time | 10+0 | 10+1 | '0'/'1'@10+1 | 0 | 1 | 20+0 | clk | c |
Operations | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Set current time | 10+1 | '0'/'1'@10+1 | 0 | 1 | 20+0 | clk | c | |
Update signals | 10+1 | '1' | 0 | 1 | 20+0 | clk | c | |
P2/if clk='1'... | 10+1 | '1' | 0 | 1 | 20+0 | c | ||
P2/c<=nc | 10+1 | '1' | 0/1@10+2 | 1 | 20+0 | c | ||
P2/end if | 10+1 | '1' | 0/1@10+2 | 1 | 20+0 | c | ||
P2/wait on clk | 10+1 | '1' | 0/1@10+2 | 1 | 20+0 | clk | c | |
Compute next time | 10+1 | 10+2 | '1' | 0/1@10+2 | 1 | 20+0 | clk | c |
Operations | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Set current time | 10+2 | '1' | 0/1@10+2 | 1 | 20+0 | clk | c | |
Update signals | 10+2 | '1' | 1 | 1 | 20+0 | clk | c | |
P3/nc<=c+1 after 5 ns | 10+2 | '1' | 1 | 1/2@15+0 | 20+0 | clk | ||
P3/wait on c | 10+2 | '1' | 1 | 1/2@15+0 | 20+0 | clk | c | |
Compute next time | 10+2 | 15+0 | '1' | 1 | 1/2@15+0 | 20+0 | clk | c |
Note: one could think that the
nc
update would be scheduled at15+2
, while we scheduled it at15+0
. When adding a non-zero physical delay (here5 ns
) to a current time (10+2
), the delta cycles vanish. Indeed, delta cycles are useful only to distinguish different simulation timesT+0
,T+1
... with the same physical timeT
. As soon as the physical time changes, the delta cycles can be reset.
Operations | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Set current time | 15+0 | '1' | 1 | 1/2@15+0 | 20+0 | clk | c | |
Update signals | 15+0 | '1' | 1 | 2 | 20+0 | clk | c | |
Compute next time | 15+0 | 20+0 | '1' | 1 | 2 | 20+0 | clk | c |
Note: again, there is no execution phase.
nc
changed but no process is waiting onnc
.
Operations | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Set current time | 20+0 | '1' | 1 | 2 | 20+0 | clk | c | |
Update signals | 20+0 | '1' | 1 | 2 | 20+0 | clk | c | |
P1/clk<='0' | 20+0 | '1'/'0'@20+1 | 1 | 2 | clk | c | ||
P1/wait for 10 ns | 20+0 | '1'/'0'@20+1 | 1 | 2 | 30+0 | clk | c | |
Compute next time | 20+0 | 20+1 | '1'/'0'@20+1 | 1 | 2 | 30+0 | clk | c |
Operations | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Set current time | 20+1 | '1'/'0'@20+1 | 1 | 2 | 30+0 | clk | c | |
Update signals | 20+1 | '0' | 1 | 2 | 30+0 | clk | c | |
P2/if clk='1'... | 20+1 | '0' | 1 | 2 | 30+0 | c | ||
P2/end if | 20+1 | '0' | 1 | 2 | 30+0 | c | ||
P2/wait on clk | 20+1 | '0' | 1 | 2 | 30+0 | clk | c | |
Compute next time | 20+1 | 30+0 | '0' | 1 | 2 | 30+0 | clk | c |
Operations | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Set current time | 30+0 | '0' | 1 | 2 | 30+0 | clk | c | |
Update signals | 30+0 | '0' | 1 | 2 | 30+0 | clk | c | |
P1/clk<='1' | 30+0 | '0'/'1'@30+1 | 1 | 2 | clk | c | ||
P1/wait for 10 ns | 30+0 | '0'/'1'@30+1 | 1 | 2 | 40+0 | clk | c | |
Compute next time | 30+0 | 30+1 | '0'/'1'@30+1 | 1 | 2 | 40+0 | clk | c |
Operations | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Set current time | 30+1 | '0'/'1'@30+1 | 1 | 2 | 40+0 | clk | c | |
Update signals | 30+1 | '1' | 1 | 2 | 40+0 | clk | c | |
P2/if clk='1'... | 30+1 | '1' | 1 | 2 | 40+0 | c | ||
P2/c<=nc | 30+1 | '1' | 1/2@30+2 | 2 | 40+0 | c | ||
P2/end if | 30+1 | '1' | 1/2@30+2 | 2 | 40+0 | c | ||
P2/wait on clk | 30+1 | '1' | 1/2@30+2 | 2 | 40+0 | clk | c | |
Compute next time | 30+1 | 30+2 | '1' | 1/2@30+2 | 2 | 40+0 | clk | c |
Operations | Tc | Tn | clk | c | nc | P1 | P2 | P3 |
---|---|---|---|---|---|---|---|---|
Set current time | 30+2 | '1' | 1/2@30+2 | 2 | 40+0 | clk | c | |
Update signals | 30+2 | '1' | 2 | 2 | 40+0 | clk | c | |
P3/nc<=c+1 after 5 ns | 30+2 | '1' | 2 | 2/3@35+0 | 40+0 | clk | ||
P3/wait on c | 30+2 | '1' | 2 | 2/3@35+0 | 40+0 | clk | c | |
Compute next time | 30+2 | 35+0 | '1' | 2 | 2/3@35+0 | 40+0 | clk | c |