Designing Circuits with VHDL
1. Introduction
VHDL is a hardware description language
that can be used to design digital logic circuits. VHDL specifications
can be automatically translated by circuit synthesizers
into digital circuits, in much the same way that Java or C++ programs
are translated by compilers into machine language. While VHDL code
bears a superficial resemblance to programs in conventional sequential
programming languages, the meaning of VHDL code differs in important
ways from sequential programs. Ultimately, the meaning of a VHDL
specification is a circuit, while the meaning of an ordinary program
is the sequential execution of the program statements. The underlying
notion of sequential execution is so pervasive in ordinary programming
that it is easy to take it for granted. In VHDL, on the other hand,
there is no built-in concept of sequential execution and this can be the
source of much misunderstanding when first learning the language. Understanding
the differences between circuit design in VHDL and conventional programming
is one of the key steps in learning to use the language. Don't worry.
You're not expected to understand these difference yet, but you should
be aware of them. As we go along, you'll recognize and start to appreciate
the differences and their implications.
2. Combinational Circuits
Signal Assignments in VHDL
Combinational circuits can be represented by circuit
schematics using logic gates. For example,
Equivalently, we can represent this circuit by
the Boolean equation A = BC + D´.
VHDL allows us to specify circuits with equations in much the same
way.
A <= (B and C) or (not D);
Here, A, B, C and D are names of VHDL signals;
<= is the concurrent
signal assignment operator and the keywords and, or and not are the familiar logical
operators. The parentheses are used to determine the order of
operations (in this case, they are not strictly necessary, but
do help make the meaning more clear) and the semicolon terminates the
assignment. A combinational circuit can have multiple outputs, as
in the full adder shown below.
We can represent this with a pair of Boolean equations
or the equivalent VHDL signal assignments.
S <= A xor B xor Ci;
Co <= (A and B) or ((A xor
B) and Ci);
In general, a logic circuit with n outputs
can be represented by n signal assignments. The signal assignments
are only part of a VHDL specification. Here is the complete specification
of the full adder.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity
fullAdder is
port( A,B: in std_logic;
-- input bits for this stage
Ci:
in std_logic; -- carry into this stage
S:
out std_logic; -- sum bit
Co:
out std_logic -- carry out of this stage
);
end fullAdder;
architecture
a1 of fullAdder is
begin
S <= A xor B xor
Ci;
Co <= (A and B) or ((A xor
B) and Ci);
end
a1;
The first four lines specify a library of standard
definitions. We'll just treat this as a required part of the
specification, without going into details. The next group of seven
lines is the entity declaration for our full adder circuit.
The entity declaration defines the name of the circuit (fullAdder), its inputs
and outputs and their types (std_logic). The inputs and
outputs are specified in a port list. Successive elements
of the port list are separated by semicolons (note that there is no
semicolon following the last element). A pair of dashes introduces
a comment, which continues to the end of the line. Comments
don't affect the meaning of the specification, but are essential for
making it readable by other people. It's a good idea to use comments
to identify the inputs and outputs of your circuits, document what your
circuits do and how they work. Get in the habit of documenting all your
code. The last five lines above, constitute the architecture specification,
which includes our two signal assignments. VHDL permits you to have multiple
architectures for the same entity, hence the architecture has its own
label, separate from the entity name.
It's important to understand the distinction between the entity declaration
and the architecture. The entity declaration defines the name of the circuit
and the architecture defines its implementation. In a block diagram or "abridged"
schematic, we often show a portion of a larger circuit as a block with
labeled inputs and outputs, as illustrated below for the fullAdder.
This corresponds directly to the entity declaration. When we supplement
such a diagram, by filling in the block with an appropriate schematic,
we are specifying an architecture.
In the fullAdder specification,
all the signals are either inputs or outputs. We can also have
internal signals, much as we have internal variables in ordinary programming
languages. For example, we could write the fullAdder architecture this
way.
architecture a1 of fullAdder is
signal X: std_logic;
begin
X <= A xor B;
S <= X
xor Ci;
Co <=
(A and B) or (X and Ci);
end a1;
The signal declaration is required and specifies
the type of X.
VHDL is a strongly typed language and requires that signals
have the proper types. Signal declarations appear before the begin keyword of the architecture.
The diagram for this architecture shown below, also includes the signal X explicitly and the use of
X in both of the output
signals. Note that the order of the signal assignments, in this
case, has no effect on the meaning of the specification. In particular,
if we put the assignment to X last, the resulting
circuit would be exactly the same. This may seem strange, but it illustrates
how VHDL is different from ordinary programming. The signal assignments
are Boolean equations specifying logic circuits. The order in which you
write the equations does not affect the meaning of the circuit any
more than the way you draw the corresponding schematic diagram affects
how it works.
VHDL also lets us define and use vectors of
signals.
architecture a1 of vecExample is
signal a, b: std_logic_vector(0
to 7);
signal c: std_logic_vector(7
downto 0);
begin
b <= "10101010";
-- double quotes used for arrays of bits
a <= b
and "00011111"; -- but only in VHDL 97 (so be sure to
c <= a(4
to 7) & b(0 to 3); -- set that in your preferences)
end a1;
The std_logic_vector
type can have indices that either increase or decrease in value as
you go from "left-to-right". The direction of the indices affects
how assignments involving vectors are interpreted, as we will see
shortly. For signals that are used to represent numerical values, it's
usual to have the indices decrease. A signal assignment involving logic
vectors can be thought of as just a short hand for a group of signal
assignments. For example, the first assignment above is equivalent
to
b(0) <= '1'; b(1) <= '0'; b(2) <= '1'; b(3) <= '0';
b(4) <= '1'; b(5) <= '0'; b(6) <= '1'; b(7) <= '0';
Here, the notation b(i) refers to the individual
elements of the vector. In the second assignment, the and operator is applied bit-wise,
so the meaning of the assignment is
a(0) <= b(0) and '0';
a(1) <= b(1) and '0';
a(2) <= b(2)
and '0'; a(3) <= b(3) and '1';
a(4) <= b(4)
and '1'; a(5) <= b(5) and '1';
a(6) <= b(6)
and '1'; a(7) <= b(7) and '1';
The last assignment refers to sub-vectors
of a and b and uses the concatenation
operator &.
Also note that the indices for c run in the opposite order
of the indices for a and b, so the meaning of
this assignment is
c(7) <= a(4);
c(6) <= a(5); c(5) <= a(6); c(4) <= a(7);
c(3) <= b(0);
c(2) <= b(1); c(1) <= b(2); c(0) <= b(3);
In principal, we can define any combinational logic
circuit using just simple signal assignments. In more complex
circuits it's convenient to have higher level constructs. The conditional
signal assignment allows us to make the value assigned to
a signal dependent on a series of conditions.
entity
conditionalExample is
port(a, b, c: in std_logic; d: out std_logic);
end conditionalExample;
architecture
a1 of conditionalExample is
begin
d <= '1' when a = b else
b when
b /= c else
a xor c;
end
a1;
The comparison operators '=' and '/=' in the when clauses
are true if the two operands are equal or not equal, respectively.
The conditional signal assignment is not strictly necessary, since
the following ordinary assignment is equivalent.
d <= ((a xnor b) and '1') or
(not (a xnor b) and (b xor c) and b) or
(not (a xnor b) and not (b xor c) and (a xor c));
While VHDL allows the comparison operators
in the condition part of the when clauses, they cannot be used in
ordinary assignments, but the xnor and xor operations can serve
the same purpose. Note the pattern of the transformation from the
conditional assignment to the ordinary assignment. Overall, the
new expression is the logical-or of several sub-expressions, one for
each part of the conditional assignment. Each sub-expression is
the logical-and of the condition in one of the when clauses, the complement
of the conditions in the previous when clauses and the value associated
with the 'current' clause. Of course, we could also simplify this
expression, giving
d <= not(a and (not b) and c);
but it's certainly easier to let the synthesizer
do the logic simplification so that we can concentrate on the
higher level meaning of the circuit. The conditional assignment
is one tool for allowing us to express the circuit behavior at a
somewhat higher level. The general form of the conditional assignment
is
x<= value1 when condition1 else
value2 when condition2 else
value3 when condition3 else
... else
valuen
Each of the values must have the same
type as the signal on the left side of the assignment. Each of
the conditions is a Boolean function on some set of variables. The
equivalent form of ordinary signal assignment is
x<= (condition1 and value1) or
(not condition1
and condition2
and value2) or
(not condition1
and not condition2
and condition3
and value3) or
...
(not condition1
and ... and not conditionn-1
and valuen)
We can also have conditional signal assignments involving logic
vectors. For example, if x, a, b and c all have type std_logic_vector(3 down to 0)
and s has
type std_logic_vector(1
downto 0), we can write
x <= a when s = "00" else
a and
b when s = "01" else
a xor
c when s = "10" else
b;
As with ordinary signal assignments, we can view
this as a short-hand for four separate signal assignments operating
on individual signals.
x(3) <= a(3) when
s(1) = '0' and s(0) = '0' else
a(3) and b(3) when
s(1) = '0' and s(0) = '1' else
a(3) xor
c(3) when s(1) = '1' and s(0) = '0' else
b(3);
x(2) <= a(2) when
s(1) = '0' and s(0) = '0' else
a(2) and b(2) when
s(1) = '0' and s(0) = '1' else
a(2) xor c(2)
when s(1) = '1' and s(0) = '0' else
b(2);
and so forth. Note that because the conditions in
this case just enumerate the different values of the signal s, the circuit specified
by the statement can be implemented with a 4:1 multiplexer with
a four bit wide data path.
Processes and Conditional Statements
VHDL provides a variety of higher level constructs
that make it easier to express more complex designs. In particular,
it includes an if-then-else
construct, similar to those in ordinary programming languages.
For example, we can write
if a = '0' then
x <= a; y <= b;
elsif
a = b then
x <= '0'; y <= '1';
else
x <= not b; y <= not b;
end
if;
VHDL requires that higher level constructs like the
if-then-else be used
only inside a process block. So the complete architecture for a module
with inputs a, b and outputs x, y that includes the above logic
would be
architecture a1 of ifThenExample is
begin
process(a,b) begin
if a = '0' then
x <= a; y <= b;
elsif a = b then
x <= '0'; y <= '1';
else
x <= not
b; y <= not b;
end if;
end process
end a1;
Following the process keyword is
a list of signal names, which is called the sensitivity list.
The sensitivity list should include all signals that can trigger changes
to signals that are assigned values within the process. If we're
using the process to define a combinational circuit, this means that
the sensitivity list should include all signals that appear in any
expression within the process. Note that we could have specified a
circuit with the same behavior using two conditional assignment statements,
one for x and one for
y. Or, we could have
written them using two ordinary signal assignments. For this simple
example, those alternatives might be preferable, but for more complex
circuits, the use of the if-then-else construct
can make it easier to express the logic you have in mind and easier
for others to read and understand your VHDL code.
There is an important rule that must be followed when
using a process to define a combinational circuit.
Every signal that is assigned a value inside a process must
be defined for all possible conditions.
The process above satisfies this condition, but the
following version does not.
architecture a1 of ifThenExample is
begin
process(a,b)
begin
if a = '0' then
x <= a ; y <= b;
elsif a = b then
x <= '0'; y <=
'1';
else
x <= not
b; -- y not defined when a=1, b=0
end if;
end process
end a1;
While this is a legitimate VHDL specification, it
does not correspond to any combinational circuit. The reason for
this is that VHDL defines the value of a signal to be unchanged by
a process if its value is not specified under some condition. So in
this case, the value of y
would not change when a=1
and b=0. To create a
circuit that has this behavior, the synthesizer must provide a storage
element (typically a latch) to retain the value of y in this case. The circuit
shown below has the specified behavior:
So if you are intending to implement a combinational
circuit, it's important to pay attention to this rule. If you don't,
the circuit synthesized for your specification will contain storage
elements that may cause it to behave differently than you intended.
It's easy to violate this rule and it can be hard to figure out what's
wrong when you do, so be aware of it and try to adopt coding practices
that will keep you from making such mistakes. One simple way to avoid
the problem is to always start your process with assignments of default
values for all signals that are assigned a value within the process.
For example, we could write
architecture a1 of ifThenExample is
begin
process(a,b)
begin
x <= '0'; y <= '0'; -- default values for x, y
if a = '0' then
x <= a; y <= b;
elsif a = b then
x <= '0'; y <= '1';
else
x <= not
a; -- y=0 when a=1, b=0
end if;
end process
end
a1;
Now, if you intended to write the original version
of this code, the above specification would still be incorrect,
but at least it specifies a combinational circuit and you'll have
an easier time figuring out what you did wrong than you would if the
circuit had a "hidden" storage element.
Note that for the above code to work as intended,
the assignments of the default values must come before the if-then-else. This is another
case where statement order matters. While the effect of statement
order is similar to that in ordinary programming languages, the underlying
reasons are a little different, since in VHDL, there is no built-in
concept of sequential execution. Whenever there are two assignments
to the same variable the later one takes precedence in all conditions
allowed by the context in which it appears. So for example, in this
architecture
architecture a1 of foo is
begin
process(a,b)
begin
c <= '0';
if a = b then
c <= '1';
end if;
end process
end
a1;
the first assignment to c applies no matter what
values a and
b have, while the second
applies only when a=b.
Because the second assignment comes after the first one, it overrides
the first, when a=b. If we wrote
architecture a1 of foo is
begin
process(a,b)
begin
if a = b then
c <= '1';
end if;
c <= '0';
end process
end
a1;
the resulting circuit would make c=0, not matter what value
a and b had. While the first version
is equivalent to
architecture a1 of foo is
begin
c <= a xnor
b;
end
a1;
the second is equivalent to
architecture a1 of foo is
begin
c <= '0';
end a1;
The implications of statement order within a VHDL process are
a little bit tricky. To explore this issue further, suppose we have
the following signal assignments in a VHDL specification within a process.
A <= '1'; -- '1' denotes the constant logic value 1
B <= A;
A <= '0';
-- '0' denotes the constant
logic value 0
If these were assignments in a sequential language,
the value assigned to B would be 1. However, in VHDL, B's value is 0. Why? The key to this
is understanding how VHDL interprets the two assignments to A. The first assignment says
that the signal A is wired to a constant
value of 1 in
the logic circuit defined by the code. The second says that A is wired a constant value
of 0.
They can't both be true, so how does VHDL interpret these two contradictory
statements? While it could just reject them as a coding error,
it does not. Instead, it simply ignores the first one (in general,
it ignores all but the last assignment to a given signal in situations
like this). So, the signal B
is wired to the signal A,
which is wired to the constant 0, meaning that B is also wired to 0. Note that if we reversed
the order of the two assignments to A, the meaning of the specification
(that is the circuit defined by the specification) would change.
So, while the order of signal assignments to A does matter, although,
the position of the assignment to B does not.
Note that we would not normally write the code fragment above in VHDL,
since it doesn't really make sense in the VHDL context. Also, it's important
to recognize that this code fragment is treated differently if it lies outside
a process block. In that case, the output A is treated as though it is
simultaneously wired to both a logic 1 and a logic 0. A simulator will
typically show the value of A as being undefined and a
synthesizer will typically reject the circuit as physically meaningless.
(Of course, if the synthesizer were to synthesize a real physical circuit
as specified, and you applied power to it, the result would be a short circuit
between power and ground, creating to a small puff of smoke, an unpleasant
smell and a useless lump of silicon.)
Case Statements
VHDL provides a case statement that is useful for
specifying different results based on the value of a single signal.
For example,
architecture a1 of foo is
begin
process(c,d,e)
begin
b <= '1'; -- provide default
value for b
case e is
when "00" => a <= c; b <= d;
when "01" => a <= d; b <= c;
when "10" => a <= c xor d;
when others => a <= '0';
end case;
end process;
end a1;
Anything you can do with a case statement, you can also
do with an if-then-else
construct, but often the case is more convenient.
Also, in those circumstances where it is appropriate, a circuit
synthesizer will generally produce a more efficient circuit for
a case than it will
for an if-then-else.
For Loops
VHDL provides a for-loop which is similar
to the looping constructs in sequential programming languages. We
can use it to define repetitive circuits, like the adder shown below.
In this example, we also introduce the use constants to define the size of
the words that the adder operates on. The constant is declared in the package
named commonConstants
and is referenced by a use
clause before the entity declaration. Packages are used to collect commonly
used declarations in one place so that they can be used in different parts
of the design. It's' a good practice to use named constants in this way,
to make the code easier to understand and to facilitate making changes.
package commonConstants is
constant wordSize: integer := 16;
end package commonConstants;
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
use work.commonConstants.all; -- makes package
visible to entity
entity
adder is
port(A, B: in
std_logic_vector(wordSize-1 downto 0);
Ci: in std_logic;
S: out std_logic_vector(wordSize-1 downto 0);
Co: out std_logic
);
end adder;
architecture a1 of adder is
signal C: std_logic_vector(wordSize
downto 0);
begin
process (A,B,C,Ci)
begin
C(0) <= Ci;
for i in 0 to wordSize-1 loop
S(i) <= A(i) xor B(i) xor C(i);
C(i+1) <= (A(i) and B(i)) or
((A(i) xor B(i)) and C(i));
end loop;
Co <= C(wordSize);
end process;
end a1;
The for-loop is equivalent to wordSize pairs of assignments
and while we could define the circuit that way, the loop is certainly
much more convenient and easier to understand. You might wonder
why we used a logic vector for the signal C. Wouldn't it be simpler
to write
architecture a1 of adder is
signal C: std_logic;
begin
process (A,B,C,Ci)
begin
C <= Ci;
for i in 0 to 15 loop
S(i) <= A(i) xor B(i) xor C;
C <= (A(i) and B(i)) or (A(i) xor B(i)) and
C);
end loop;
Co <= C(4);
end process;
end a1;
While this makes perfect sense in a sequential programming
languages it does not have the intended meaning in VHDL, since
there is no built-in concept of sequential execution. The signal
C can only be "wired"
one way. It cannot be defined by different expressions at different times.
Consequently, the circuit defined by this specification will not
behave in the intended way.
Structural VHDL
Larger circuits are generally implemented by combining
large building blocks together using what is known as structural
VHDL. In structural VHDL, we essentially "wire" together the
different components to form a larger circuit. We can illustrate this
by constructing a 4 bit adder using the full adder module defined earlier
as a building block.
entity adder4 is
port(A, B: in
std_logic_vector(3 downto 0);
Ci: in std_logic;
S: out std_logic_vector(3 downto 0);
Co: out std_logic
);
end adder4;
architecture a1 of adder4 is
--
local component declaration for fullAdder
component fullAdder port(
A, B, Ci:
in std_logic;
S, Co: out
std_logic
);
end component;
signal C: std_logic_vector(3 downto 1);
begin
b0: fullAdder port map(A(0),B(0),Ci,S(0),C(1));
b1: fullAdder port map(A(1),B(1),C(1),S(1),C(2));
b2: fullAdder port map(A(2),B(2),C(2),S(2),C(3));
b3: fullAdder port map(A(3),B(3),C(3),S(3),Co);
end a1;
The meaning of this specification is illustrated in the block diagram
shown below.
Note that structural VHDL does not use a process construct.
To use a component within a VHDL architecture, you must include
a local component declaration as part of the architecture. The component
declaration defines the interface to the architecture and is similar
to the entity declaration. The component declaration is required
even if the entity declaration for the component is in the same file.
This is because VHDL requires that each architecture be self contained.
The four component instantiation statements specify the
four full adders. Each statement has a label that is used to distinguish
the components from one another. The port map portion of the component
instantiation statement defines which signals of the adder4 module are associated
with which ports of the fullAdder component.
In this case, we are using positional association of the ports.
That is, the position of a signal in the port map list determines which
signal in the component declaration it is associated with. VHDL also
allows named association. For example, we could write
b0: fullAdder port map(A=>A(0),B=>B(0),S=>S(0),
Ci=>Ci,C0=>C(1));
Note that if we use named association, the order in
which the arguments appear does not matter. For larger circuit blocks
with many inputs and outputs, named association is preferred.
Structural VHDL also supports iterative definitions
so that we need not write a whole series of similar component instantiation
statements. This allows us to write the four bit adder as
architecture a1 of adder4 is
-- local
component declaration for fullAdder
component fullAdder port(
A, B, Ci:
in std_logic;
S, Co: out
std_logic
);
end
component;
signal C: std_logic_vector(4 downto 0);
begin
C(0) <= Ci;
bg: for i in
0 to 3 generate
b:
fulladder port map(A(i),B(i),C(i),S(i),C(i+1));
end generate;
Co <= C(4);
end a1;
Observe that in this version, we've declared C to be a five bit signal,
rather than a three bit signal and associated Ci with C(0) and Co with C(4). This avoids the need
for separate component instantiation statements for the first and last bits
of the adder. The for-generate
statement also allows us to define the adder using a named constant for
the word size, instead of explicit values. This makes the code more general
and easier to change, if we decide that we need a different word size. Note
that the labels on the for-generate
statement and on the component instantiation statement are both
required. Finally we should point out that while we can use structural
VHDL to define circuits like the adder, usually it is more convenient
to define circuits like this with for-loops, as discussed earlier.
Structural VHDL is most useful for putting together larger circuit
blocks.
3. Sequential Circuits
What distinguishes sequential circuits from combinational
circuits is the fact that they can store values and retain them for
later use. Clocked sequential circuits store values in flip flops,
most often, edge-triggered D flip flops. VHDL provides a
synchronization
condition for use in if-statements that allows
us to specify the storage of values in flip flops or registers of flip
flops. Here's an example.
if clk'event and clk = '1' then
x <= a xor b;
end if;
The expression in the if-statement is the
synchronization
condition. Here clk,
is a clock signal. The event attribute
is true whenever clk
is changing. The second part of the synchronization condition refers
to the value of clk
immediately after the transition. So this synchronization condition
says that the signal x
should be assigned the value a xor b on a rising clock
edge. To implement this behavior the circuit synthesizer associates
the signal x
with the output of a positive edge-triggered D flip flop. Note that
because x is the
output of a flip flop, it can only change when the flip flop changes.
This means that all assignments to x must be within the scope
of if-statements
with identical synchronization condition. This requirement makes it convenient
to arrange one's VHDL specification so that all assignments to signals
whose values are stored in flip flops lie within the scope of a single
if-statement containing
the synchronization condition. In fact, while the language doesn't require
this, many circuit synthesizers cannot handle specifications with more
than one synchronization condition in the same process. For this reason,
we adopt the common convention of using at most one synchronization condition
in the processes used to specify sequential circuits.
VHDL makes it easy to write a specification for a sequential
circuit directly from the state transition diagram for the circuit.
The state diagram shown below is for a sequential comparator with two
serial inputs, A and
B and two outputs G and L. There is also a reset input that disables the
circuit and causes it to go the 00 state when it is high. After reset drops,
the A and B inputs are interpreted as
numerical values, with successive bits presented on successive clock ticks,
starting with the most significant bits. The G and L outputs are low initially,
but as soon as a difference is detected between the two inputs, one
or the other of G
or L goes high.
Specifically, G
goes high if A>B and L goes high if A<B. Notice that G and L go high before the clock
tick that causes the transition to the 10 and 01 states.
Here is a VHDL module that implements the comparator.
entity serialCompare is
Port (clk, reset: in std_logic;
A, B : in std_logic; -- inputs to be compared
G, L: out std_logic -- G means A>B, L
means A<B
);
end serialCompare;
architecture a1 of serialCompare is
signal state: std_logic_vector(0 to 1);
begin
-- process that defines state
transition
process(clk) begin
if clk'event
and clk = '1' then
if reset = '1' then
state <= "00";
elsif state = "00" then
if A = '1' and B = '0' then
state <= "10";
elsif A = '0' and B = '1' then
state <= "01";
end if;
end if;
end
if;
end process;
-- process that defines the outputs
process(A, B, state) begin
G <= '0'; L <= '0';
if (state
= "00" and A = '1' and B = '0')
or state = "10" then
G <= '1';
end
if;
if (state
= "00" and A = '0' and B = '1')
or state = "01" then
L <= '1';
end
if;
end process;
end a1;
There are two processes in this specification. The first defines
the state transitions and starts with an if-statement containing
a synchronization condition. All assignments to the state signal occur within the
scope of this if-statement
causing them to be synchronized to the rising edge of the clk signal. We start the synchronized
code segment by checking the status of reset and putting the circuit
into state 00 if reset is high, The rest
of the process controls the transition to the 10 or 01 states, depending
on which of the two inputs is larger. Notice that there is no code
for the "self-loops" in the transition diagram, since these involve no
change to the state signal. The second process specifies the output signals
G and L. Although it's not essential
to define the outputs in a separate process, it's generally considered good
practice to do so. Notice that this second process has no synchronization
condition and specifies a purely combinational sub-circuit. The VHDL synthesizer
analyzes this specification and determines that the state signal must be stored
in a pair of flip flops. It also determines the logic equations needed
to generate the next state and output values and uses these to create
the required circuit. The diagram below shows a circuit that could be
generated by the synthesizer from this specification.
The figure below shows the output of a simulation run for
the serial comparator. Notice that the changes to the state variable
are synchronized to the rising clock edges but the low-to-high
transitions of G and
L are not.
VHDL allows us to define signals with enumerated types
allowing us to associate meaningful names to values of signals. This
is particularly useful for naming the states of state machines, as
illustrated below.
architecture
a1 of serialCompare is
type stateType is (unknown, bigger, smaller);
signal state: stateType;
begin
-- process that defines state transition
process(clk) begin
if clk'event
and clk = '1' then
if reset = '1' then
state <= unknown;
elsif state = unknown then
if A = '1' and B = '0' then
state <= bigger;
elsif A = '0' and B = '1' then
state <= smaller;
end if;
end if;
end if;
end process;
-- code that defines the outputs
G <= '1' when (state = unknown and A = '1' and B= '0')
or state = bigger else
'0';
L <= '1' when (state = unknown and A = '0' and B= '1')
or state = smaller else
'0';
end a1;
In this version, we have also defined the outputs with two conditional
signal assignments, instead of a process. In situations where the outputs
are fairly simple, this coding style is preferable.
Next, we look at an example of a more complex sequential circuit
that combines a small control state machine with a register to count
the number of "pulses" observed in a serial input bit stream, where
a pulse is defined as one or more clock ticks when the input is low,
followed by one or more clock ticks when it is high, followed by one
or more clock ticks when it is low. In addition to the data input A, the circuit has a reset input, which disables
and re-initializes the circuit. The primary output of the circuit is
the value of the four bit counter. There is also an error output which is high
if the input bit stream contains more than 15 pulses. If the number
of pulses observed exceeds 15, the counter "sticks" at 15. The simplified
state transition diagram shown below does not explicitly include the reset
logic, which clears the counter and puts the circuit in the allOnes state. Also, note that
the counter value us not shown explicitly, since this would require that
the diagram include separate between and inPulse states for each of
the distinct counter values. Instead, we simply show whether the counter
is incremented or not.
Here is a VHDL module that implements pulse counter. In this
example, we have introduced two constants, one for the word size and another
for the maximum number of pulses that we can count. Note that because the
second constant has type std_logic_vector,
the package declaration requires the IEEE library where the std_logic_vector type is defined.
Notice the correspondence between the transition diagram and the code.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
package commonConstants is
constant wordSize: integer := 4;
constant maxPulse: std_logic_vector
:= "1111";
end package commonConstants;
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
use work.commonConstants.all;
-- Count the number of pulses in the input bit stream.
-- A pulse is a 01...10 pattern.
entity countPulse is
Port (
clk, reset:
in std_logic;
A:
in std_logic; -- input bit stream
count: out std_logic_vector(wordSize-1
downto 0);
errFlag: out
std_logic -- high if more than maxPulse detected
);
end countPulse;
architecture a1 of countPulse is
type stateType is (allOnes, between, inPulse, errState);
signal state: stateType;
signal countReg: std_logic_vector(wordSize-1 downto
0);
begin
process(clk) begin
if clk'event
and clk = '1' then
if reset = '1' then
countReg <= (others => '0');
state <= allOnes;
else
case state is
when allOnes =>
if A = '0' then state <= between;
end if;
when between =>
if A = '1' then state <= inPulse;
end if;
when inPulse =>
if A = '0' and countReg /= maxPulse
then
countReg <=
countReg + "1";
state <= between;
elsif A = '0' and countReg = maxPulse
then
state <= errState;
end if;
when others =>
end case;
end if;
end if;
end process;
count <= countReg;
errFlag <= '1' when state =
errState else '0';
end a1;
Notice that we have defined a countReg signal separate from
the count output
signal because VHDL does not allow output signals to be used in expressions.
The standard way to get around this is to have an internal signal that
is manipulated within the module and then assign the value of this
internal signal to the output signal. Also notice the assignment to
countReg in the reset section.
countReg
<= (others => '0');
This means that all bits of the countReg are 0. We could have written
countReg
<= "0000";
but this makes the code dependent on the word size, which we would prefer
to avoid.
The countPulse
circuit illustrates a common characteristic of many sequential
circuits. While strictly speaking, the state of the circuit consists of
both the state signal
and the value of countReg,
the two serve somewhat different purposes. The state signal keeps tack of
the control state of the circuit while the countReg variable holds the
data state. We can generally simplify the state transition diagram
for a sequential circuit by representing only the control
state explicitly while indicating the modifications to the data state
as though they were outputs to the circuit. This leads directly to a
VHDL representation based directly on the transition diagram.
We finish this section with a larger sequential circuit
that implements a hardware priority queue. A priority queue maintains
a set of (key,value) pairs. Its primary output is called
smallValue and it
is equal to the value of the pair that has the smallest key. So for
example, if the priority queue contained the pairs (2,7), (1,5) and (4,2)
then the smallValue
output would be 5. There are two operations that can be performed on
the priority queue. An insert operation adds a new (key,value)
pair to the set of stored pairs. A delete operation removes the (key,value)
pair with the smallest key from the set. The circuit has the following
inputs.
clk the clock signal
reset initializes
the circuit, discarding any stored values that may be present
insert when high, it initiates an insert operation
delete when high, it initiates a delete operation;
however, if insert and delete are
high at the same time, then the delete signal is ignored
key the key part of a new pair being
inserted
value the value part of a new pair being inserted
The circuit has the following outputs, in addition to smallValue.
busy
is high when the circuit is in the middle of performing an
operation; while busy is high,
the insert and delete inputs are ignored; the outputs are not
required to have the
correct values when busy is high
empty
is high when there are no pairs stored in the priority queue; delete
operations are
ignored in this case
full
is high when there is no room for any additional
pairs to be stored; insert operations
are ignored in this case
The figure below shows a block diagram for one implementation
of a priority queue.
In this design, there is a set of blocks arranged in two rows.
Each block contains two registers, one storing a key, the other storing
a value. In addition, there is a flip flop called dp, which stands for data present.
This bit is set for every block that contains a valid (key,value)
pair. The circuit maintains the set of stored pairs so that three properties
are maintained.
- For adjacent pairs in the bottom row, the pair to the left
has a key that is less than or equal to that of the pair on the right.
- For pairs that are in the same column, the key of the pair
in the bottom row is less than or equal to that of the pair in the top
row.
- In both rows, the empty blocks (those with dp=0) are to the right and
either both rows have the same number of empty blocks or the top row
has one more than the bottom row.
When these properties hold, the pair with the smallest key is
in the leftmost block of the bottom row. Using this organization, it
is straightforward to implement the insert and delete operations. To do
an insert, the (key,value) pairs in the top row are all
shifted to the right one position, allowing the new pair to be inserted
in the leftmost block of the top row. Then, within each column, the keys
of the pairs in those columns are compared, and if necessary, the pairs
are swapped to maintain properties 2 and 3. Note that the entire operation
takes two steps. While it is in progress, the busy output is high. The delete
operation is similar. First, the pairs in the bottom row are all shifted to
the left, effectively deleting the pair with the smallest key. Then, for
each column, the key values are compared and if necessary, the pairs are
swapped to maintain properties 2 and 3. Given these properties, we can determine
if the priority queue is full by checking the rightmost dp bit in the top row and
we can determine if it is empty by checking the leftmost dp bit in the bottom row.
The complete state of this circuit includes all the values stored
in all the registers, but we can express the control state is much more
simply, as shown in the transition diagram below.
This is a somewhat conceptual state transition diagram, but it captures
the essential behavior we want. In particular, the labels on the arrows
indicate the condition that causes the given transition to take
place and any action that should be performed at the same time. The
variable top(rightmost).dp refers to the rightmost
data present flip flop in the top row and bot(leftmost).dp
refers to the leftmost data present flip flop in the bottom row. In the
ready state, the circuit is between operations and waiting for the
next operation. If it gets an insert request and it is not full, it goes
to the inserting state and shifts the new (key,value)
pair into the top row and shifts the whole row right. From there it makes
a transition back to the ready state while doing a "compare &
swap" between all vertical pairs. If the circuit gets a delete request
when it is in the ready state and is not empty, it goes to the deleting
state and shifts the bottom row to the left. From there, it immediately
returns to the ready state, while performing a compare &
swap.
A VHDL module implementing this design is shown below.
-- Priority Queue module implements a priority queue storing
-- up to 8 (key,value) pairs. The keys and
values are 4 bits
-- each. When the priority queue is not empty,
the output
-- smallValue is the value of a pair with the smallest
key.
-- The empty and full outputs report the status of the priority
-- queue. The busy output remains high while an insert or
-- delete operation is in progress. While it is high, new
-- operation requests are ignored
entity priQueue
is
Port
(clk, reset : in std_logic;
insert, delete : in std_logic;
key, value : in std_logic_vector(wordSize-1 downto 0);
smallValue : out std_logic_vector(wordSize-1 downto 0);
busy, empty, full : out std_logic
);
end priQueue;
architecture a1 of priQueue
is
constant rowSize: integer
:= 4; -- local constant declaration
type pqElement is record
dp: std_logic;
key:
std_logic_vector(wordSize-1 downto 0);
value:
std_logic_vector(wordSize-1 downto 0);
end record pqElement;
type rowTyp is array(0 to
rowSize-1) of pqElement;
signal top, bot: rowTyp;
type state_type is (ready,
inserting, deleting);
signal state: state_type;
begin
process(clk)
begin
if clk'event and clk = '1' then
if reset = '1' then
for i in 0 to rowSize-1 loop
top(i).dp <=
'0'; bot(i).dp <= '0';
end loop;
state <= ready;
elsif state = ready and insert = '1' then
if top(rowSize-1).dp /= '1' then
for i in 1 to rowSize-1
loop
top(i) <= top(i-1);
end loop;
top(0) <= ('1',key,value);
state <= inserting;
end if;
elsif state = ready and delete = '1' then
if bot(0).dp /= '0' then
for i in 0 to rowSize-2
loop
bot(i) <= bot(i+1);
end loop;
bot(rowSize-1).dp
<= '0';
state <= deleting;
end if;
elsif state = inserting or state = deleting then
for i in 0 to rowSize-1 loop
if top(i).dp =
'1' and
(top(i).key < bot(i).key
or bot(i).dp = '0') then
bot(i) <= top(i); top(i) <= bot(i);
end if;
end loop;
state <= ready;
end if;
end if;
end process;
smallValue
<= bot(0).value when bot(0).dp = '1' else
(others => '0');
empty
<= not bot(0).dp;
full
<= top(rowSize-1).dp;
busy
<= '1' when state /= ready else '0';
end a1;
This example illustrates the use of arrays of records to represent the
two rows in the priority queue. The code segment
type
pqElement is record
dp: std_logic;
key: std_logic_vector(wordSize-1
downto 0);
value: std_logic_vector(wordSize-1
downto 0);
end record pqElement;
defines the basic building block of the arrays. We can refer to
elements of the arrays or specific fields of specific elements using expressions
like
top(i)
bot(i).value
Note how the use of the use of these arrays allows us to express the design
of the circuit in a way that directly reflects the high level description.
Sequential circuits can be used to build systems of great sophistication
and complexity. The challenge, to the designer is to manage that complexity
so as not to be overwhelmed by it. VHDL is one important tool that can
help in meeting the challenge, but to use it effectively, you need to learn
the common patterns that experience has shown are most useful in expressing
the functionality of digital systems. This section has introduced some
of the more useful patterns. You should study the examples carefully to
make sure you understand how they work and to develop a familiarity with
the patterns they follow.
4. Functions and Procedures
Like conventional programming languages, VHDL provides a subroutine
mechanism to allow you to encapsulate circuit components that are used
repeatedly in different contexts. The example below shows how a function
can be used to represent a circuit that finds the first 1 in a logic vector.
entity firstOne is
Port (a: in std_logic_vector(0
to wordSize-1);
x: out
std_logic_vector (0 to wordSize-1)
);
end firstOne;
architecture a1 of firstOne is
function firstOne(x: std_logic_vector(0 to wordSize-1))
return std_logic_vector is
-- Returns a bit vector with a 1 in the position
where
-- the first one in the input bit string is
found
-- everywhere else, it is zero
variable allZero: std_logic_vector(0 to wordSize-1);
variable fOne: std_logic_vector(0 to wordSize-1);
begin
allZero(0) := not x(0);
fOne(0) := x(0);
for i in 1 to wordSize-1
loop
allZero(i)
:= (not x(i)) and allZero(i-1);
fOne(i)
:= x(i) and allZero(i-1);
end loop;
return fOne;
end function firstOne;
begin
x <= firstOne(a);
end a1;
Note that within the function definition, we can use the for-loop and other complex
statements. Also, note that within the function we use variables not
signals. When the function is invoked, the variables will be associated
with signals in the context from which the function is invoked, but within
the function definition, we use variables. When assigning a value to
a variable, we must use the variable assignment operator :=.
We can use procedures to specify common subcircuits that produce
more than one output. In the example shown below, the firstOne function has been
modified so that it returns the numerical index of the first 1 in the
argument bit string, instead of a bit vector that marks the position of
the first 1. It is implemented using a separate encode procedure which has
two output parameters, one for the index, and the other for an error
flag. The firstOne
function inserts the error flag into the high order bit of its return
value.
package commonConstants
is
constant
lgWordSize: integer := 4;
constant
wordSize: integer := 2**lgWordSize;
end package commonConstants;
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
use work.commonConstants.all;
entity firstOne is
Port (a:
in std_logic_vector(0 to wordSize-1);
x: out std_logic_vector (lgWordSize downto 0)
);
end firstOne;
architecture a1 of firstOne
is
procedure encode(x: in std_logic_vector(0
to wordSize-1);
indx: out std_logic_vector(lgWordSize-1 downto 0);
errFlag: out std_logic) is
-- Unary to binary encoder.
-- Input x is assumed to
have at most a single 1 bit.
-- Indx is equal to the index
of the bit that is set.
-- If no bits are set, errFlag
bit is made high.
-- This is conceptually simple.
--
--
indx(0) is OR of x(1),x(3),x(5), ...
--
indx(1) is OR of x(2),x(3), x(6),x(7), x(10),x(11), ...
--
indx(2) is OR of x(4),x(5),x(6),x(7), x(12),x(13),x(14(,x(15),...
--
-- but it's tricky to code
so it works for different word sizes.
type vec is array(0 to lgWordSize-1)
of std_logic_vector(0 to (wordSize/2)-1);
variable fOne: vec;
variable anyOne: std_logic_vector(0
to wordSize-1);
begin
-- fOne(0)(j)
is OR of first j bits in x1,x3,x5,...
-- fOne(1)(j)
is OR of first j bits in x2,x3, x6,x7, x10,x11,...
-- fOne(2)(j)
is OR of first j bits in x4,x5,x6,x7, x12,x13,x14,x15,...
for i
in 0 to lgWordSize-1 loop
for j in 0 to (wordSize/(2**(i+1)))-1 loop
for h in 0 to (2**i)-1 loop
if j = 0 and h = 0 then
fOne(i)(0) := x(2**i);
else
fOne(i)((2**i)*j+h)
:= fOne(i)((2**i)*j+h-1) or
x(((2**i)*(2*j+1))+h);
end if;
end loop;
end loop;
indx(i) := fOne(i)((wordSize/2)-1);
end loop;
anyOne(0)
:= x(0);
for i
in 1 to wordSize-1 loop
anyOne(i) := anyOne(i-1) or x(i);
end loop;
errFlag
:= not anyOne(wordSize-1);
end procedure encode;
function firstOne(x: std_logic_vector(0
to wordSize-1))
return std_logic_vector is
-- Returns the index of the
first 1 in bit string x.
-- If there are no 1's in
x, the value returned has a
-- 1 in the high order bit.
variable allZero: std_logic_vector(0
to wordSize-1);
variable fOne: std_logic_vector(0
to wordSize-1);
variable rslt: std_logic_vector(lgWordSize
downto 0);
begin
allZero(0)
:= not x(0);
fOne(0)
:= x(0);
for i
in 1 to wordSize-1 loop
allZero(i) := (not x(i)) and allZero(i-1);
fOne(i) := x(i) and allZero(i-1);
end loop;
encode(fOne,rslt(lgWordSize-1
downto 0),rslt(lgWordSize));
return
rslt;
end function firstOne;
begin
x <=
firstOne(a);
end a1;
Functions and procedures can be important components of a larger VHDL
circuit design. They eliminate much of the repetition that can occur in
larger designs, facilitate re-use of design elements developed by others
and can make large designs easier to understand and manage.
5. Closing Remarks
This tutorial is just an introduction to VHDL and makes no attempt
to be comprehensive. VHDL is a large, complex language with a great many
features that while useful in some contexts, are not essential to the
development of smaller digital systems. There are a great many books describing
VHDL that you'll find useful as you progress through later courses and apply
VHDL to more complex systems. As you learn more about the language, you
will discover that using VHDL to synthesize circuits is only one of its
uses. In fact, the original purpose of the language was to model digital
systems in software, to enable rapid simulation and evaluation of architectural
alternatives before proceeding with a specific design. The development
of circuit synthesizers that could convert VHDL specifications into actual
hardware descriptions came later, only after the language was in wide-spread
use for modeling and simulation. As a result of this history, it's possible
to write VHDL specifications that, while they can be simulated, cannot be
synthesized. In this tutorial, we have emphasized synthesizable VHDL specifications,
and indeed, all the examples in this tutorial can be (and have been) synthesized.
Prepared by Jonathan Turner: jst@cse.wustl.edu,
7/3/2003
Updated by David M. Zar:
dzar@wustl.edu,10/8/2003