Designing Digital Circuits Using VHDL

From CSE260

(Redirected from Introduction to VHDL)
Jump to: navigation, search

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 differences yet, but you should be aware of them. As we go along, you'll recognize and start to appreciate the differences and their implications.

Contents

Simple Signal Assignments

Signal assignments are the most common element of a VHDL circuit specification. Here's an example.

  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 are not strictly necessary, but do help make the meaning more clear) and the semicolon terminates the assignment. This assignment can be implemented by the combinational circuit shown below.

Image:Simple.gif

Any logic circuit made up of AND gates, OR gates and inverters in which there are no feedback paths (a feedback path is a circuit path that leads from a gate output to an input of the same gate) is a combinational circuit. Every VHDL assignment corresponds to a combinational circuit, and any combinational circuit can be implemented using one or more VHDL assignments.

The specific circuit shown above is only one possible implementation of the given signal assignment. Any logically equivalent circuit is also an acceptable implementation (when we say logically equivalent circuit, we just mean any circuit that produces the same output values as the above circuit for all combinations of input values). The meaning of the given assignment is any circuit that is logically equivalent to the one shown above.

The following pair of signal assignments specifies one bit position of an n bit adder.

   S <= A xor B xor Ci;
   Co <= (A and B) or ((A xor B) and Ci);

Here, A and B represent corresponding bits of the two binary numbers being added together and Ci represents the carry into this bit position. S is the sum for this bit and Co is the carry out of this bit position. The xor keyword represents the exclusive-or operator. For any expressions X and Y, X xor Y is equivalent to (X and (not Y)) or ((not X) and Y). We note that no parentheses are required in the assignment to S since the exclusive-or operator is associative. This pair of assignments could be implemented by two separate circuits that happen to share the same inputs, but the circuit shown below provides a more efficient implementation, since it uses the first exclusive-or gate to produce both of the signals.

Image:FullAdder.gif

We can also use signal assignments to define internal signals that are used in other assignments, but are not outputs of the circuit we are specifying. For example, we might specify the full adder using the following assignments.

   generate <= A and B;
   propagate <= A xor B;  
   S <= propagate xor Ci;
   Co <= generate or (propagate and Ci);

This specifies a circuit is logically equivalent to the previous one. By creating names for these intermediate signals we can use them in other assignments and this can help a VHDL language system find the most efficient implementation of our circuit.

This example provides a good illustration of the difference between VHDL and conventional programming languages. Suppose that we wrote the assignments shown below.

   S <= propagate xor Ci;
   Co <= generate or (propagate and Ci);
   generate <= A and B;
   propagate <= A xor B;

If these were assignments in a conventional programming language this version would not mean the same thing as the original version. However, in VHDL these two code fragments have exactly the same meaning, because they both specify the same circuit. The order in which the statements appear makes no difference.

VHDL also supports composite signals or signal vectors which allows several simple signals to be treated as a unit. For example, if A and B are both signal vectors that represent the component signals A0,A1,A2 and B0,B1,B2 the assignment

   A <= B;

is equivalent to the three simple assignments

   A(0) <= B(0); A(1) <= B(1); A(2) <= B(2);

If C is a similar signal vector, the assignment

   A <= B and C;

is equivalent to

   A(0) <= B(0) and C(0);
   A(1) <= B(1) and C(1);
   A(2) <= B(2) and C(2);

We can also refer to parts of signal vectors. So, the assignment

   A(0 to 2) <= B(3 downto 2) & C(2);

is equivalent to

   A(0) <= B(3); A(1) <= B(2); A(2) <= C(2);

Here the ampersand (&) is a signal concatenation operator that is used to combine signals or signal vectors to form longer signal vectors. The direction indicators, to and downto, determine which end of a range of signals is considered the left end and which is the right end. Signal assignments use this notion of left-to-right ordering to determine which signals of the right-hand side vector are paired with signals on the left-hand side.

The right-hand side of a signal assignment can also include constant values, and there are several ways to specify constants. Single bit values are enclosed in single quotes ('0' or '1'). Multi-bit signals are written as strings enclosed by double quotes ("001" or "11001"). For multibit signals with more than a few signals, it's convenient to use hexadecimal constants. The constant x"c4" specifies the same value as "11000100". VHDL allows constants to be specified in more general ways as well. For example, if A is an 8 bit signal vector, then the assignment

   A <= (7 | 6 => '1', 5 downto 3 => '0', others => '1');

is equivalent to

   A <= "11000111";

The special case

   A <= (others => '0');

provides a convenient way to specify that all bits of A are '0'. This works no matter how many bits A actually has.

Entities and Architectures

More Signal Assignments

In principle, we can define any combinational logic circuit using just simple signal assignments. However, it's convenient to have higher level constructs that concisely represent more complex circuits. The conditional signal assignment allows us to make the value assigned to a signal dependent on a series of conditions.

   c <=  x when a /= b else
         y  when a = '1'  else
         z;

The comparison operators '=' and '/=' in the when clauses are true if the two operands are equal or not equal, respectively. This assignment can be implemented by the following circuit.

Image:condSigAssign.gif

Here, the trapezoidal circuit elements are a 2:1 multiplexors. A multiplexor can be implemented using gates, but it arises so often that it's convenient to show it in logic diagrams using this special symbol. The multiplexor has two data inputs on the left, and a control input at the bottom. When the control input is high, the output (on the right) is equal to the data input labeled 1. When the control input is low, the output is equal to the data input labeled 0. So, we can thing of the multiplexor as selecting one of two alternatives, just what we need to implement the alternatives implied by a when-else clause in a conditional signal assignment. The conditional signal assignment can have any number of when-else clauses, and each successive clause can be implemented using another multiplexor.

We can also use this conditional signal assignments with signal vectors. If A, B and C are all 8 bit signal vectors, then the assignment

   A <= B when A > B else
        B+C when A < B else
        C;

is equivalent to the circuit

Image:condSigAssignVec.gif

Here the labeled blocks designate combinational sub-circuits that implement an inequality comparison function and an addition function. We don't have to specify how these functions are implemented, as the VHDL language system provides standard implementations of the common arithmetic functions.

There is one more type of signal assignment in VHDL called the selected signal assignment. For example, if x is a signal vector with 2 bits, then

   with x select
       a <= '0' when "00" ,
            '1' when "01" | "10" , 
             b  when others;

specifies the circuit

Image:selectSigAssign.gif

This diagram includes a 4:1 multiplexor, which has 4 data inputs and a 2 bit control input. It's output is equal to the data input whose index is specified by the control value. That is, if the value of the control input is 10, the output is equal to the value on input 2.

Entities, Architectures and Declarations

The signal assignments are only part of a VHDL specification. To define a circuit, we must also specify its inputs and outputs and any internal signals that it uses. As an example, here is a complete specification of a full adder.

   --
   -- Full adder circuit implements a 1 bit segment of a
   -- multi-bit addition circuit. The carry input and outputs
   -- are used to connect successive stages together.
   --
   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
   signal generate, propagate: std_logic;
   begin
       generate <= A and B;
       propagate <= A xor B;
       S <= propagate xor Ci;
       Co <= generate or (propagate and Ci);
   end a1;

The first few lines are a comment describing the circuit. In general, 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 next few lines specify a library of standard definitions. We'll just treat this as a required part of the specification for now, 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). The last five lines above, constitute the architecture specification, which includes 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.

Image:FullAdderEntity.gif

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.

Image:FullAdderArch.gif

The fullAdder specification includes two internal signals generate and propagate, in addition to the inputs or outputs. All such internal signals must be declared using a signal declaration that comes before the begin keyword in the architecture specification. Signal declarations for signal vectors define the number of signals they contain and the indices used to identify them. So for example, the declarations

   signal x: std_logic_vector(3 downto 0);
   signal y: std_logic_vector(2 to 5);

define x to be a signal vector consisting of the components x(0), x(1), x(2) and x(3) and y to be a signal vector consisting of the components y(2), y(3), y(4) and y(5). The declarations also define a default direction for each of the signals. This defines the direction to be used in an assignment, when no specific order has been specified. So for example,

   x <= y;

is equivalent to

   x(3 downto 0) <= y(2 to 5);

or

   x(3) <= y(2); x(2) <= y(3); x(1) <= y(4); x(0) <= y(5);

One can over-ride the default order, by writing

   x(0 to 3) <= y;

in order to reverse the order of signals in the previous assignment.

One can also declare constants in VHDL.

   constant wordSize: integer := 16;

Given such a constant declaration, we can write

   signal dataReg: std_logic_vector(wordSize-1 downto 0);

By associating a name with a constant value, we can make it easier for someone else reading our VHDL code to understand our intent. Moreover, if we use wordSize consistently in our code, we can easily modify our circuit specification to accommodate a different value for wordSize at some point in the future. Instead of changing possibly dozens of individual numerical constants, we can just change a single constant declaration.

VHDL also allows us to declare new signal types. For example, we can define a register type using the declaration

   type regType is std_logic_vector(wordSize-1 downto 0);

and then use it to define signals of this type.

   signal regA, regB: regType;

VHDL also allows us to define more complex signal types. For example, the declarations

   type regFileType is array(0 to 15) of regType;
   signal reg: regFileType;

defines regFileType to be an array of items, each of type regType, and declares reg to be a signal of this type. Given these declarations, we can write

   reg(2) <= x"3fe5";
   reg(3) <= reg(5) + reg(7);
   reg(8 to 15) <= (9 => x"abcd", 11 => x"ffff", others => x"0000");

A signal of type regFileType may be implemented in one of several different ways, depending on how it is used. The most general implementation is a collection of separate registers, each of which is implemented using flip flops.

Type declarations can also be used to declare composite signals that combine components of different types. For example

   type pqElement is record 
       dp: std_logic; 
       key, value: regType;
   end record pqElement;
   signal pqe1, pqe2, pqe3: pqElement;

declares signals of type pqElement to contain a single bit signal called dp, and two signals, key and value, of type regType. Given these declarations we can write

   pqe1 <= (dp => '1', key => x"0000", value => x"ffff");
   pqe2.value <= pqe1.key(15 downto 8) & pqe1.value(0 to 7);
   pqe3 <= pqe1;

Processes and If-Then-Else

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 (as we are here), this means that the sensitivity list should include all signals that appear in any expression within the process. The sensitivity list is primarily a mechanism for enabling more efficient functional simulation of circuits. If a signal is accidentally omitted from a sensitivity list, functional simulations are likely to behave in ways that are unintended and may be perplexing. So, if you find that a circuit you are simulating displays unexpected behavior, a good rule of thumb is to double-check the sensitivity list to make sure that all signals used by the process have been included.

Note that we could have specified the circuit described above 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.

Here's another example of an if-then-else that is preceded by two other signal assignments.

   x <= x"0000";
   y <= x"abcd";
   if a > b then
       x <= y; z <= b;
   elsif a = b then
       y <= x; z <= a;
   else
       z <= x + y;
   end if;

The initial assignments to x and y provide default values. These are used to define the values of x and y under those conditions where the if-then-else does not specify values. The precise meaning of a process can be difficult to grasp at first. One way to clarify the meaning to re-write the process with separate code segments for each signal that is assigned a value in the process. So for example, we might re-write the above code segment as

   -- code defining x
   x <= x"0000";
   if a > b then
       x <= y;
   end if;
   -- code defining y
   y <= x"abcd";
   if a = b then
       y <= x;
   end if;
   -- code defining z
   if a > b then
       z <= b;
   elsif a = b then
       z <= a;
   else
       z <= x + y;
   end if;

Note that the default assignment must come before the if-then-else in order to have the intended effect. If we were to write

   if a > b then
       x <= y;
   end if;
   x <= x"0000";

then x would always be assigned the value x"0000" and the if-then-else would have no effect. So in this case, the relative order of the statements does matter. In general, when a signal value is defined within a process, assignments that come earlier in the process specification provide values that can be overridden by assignments that come later. Typically, these later assignments are conditional.

The implications of statement order within a VHDL process can be 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 understanding this is to remember that VHDL specifies a circuit, not sequential execution. Since there is no condition that limits the application of the second assignment to A, it takes precedence over the first assignment. That is, the first assignment could have been omitted altogether and the specified circuit would remain the same. The assignment to B just means that in the specified circuit, B is wired to A, and since A is 0, B is also. 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, 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 a small puff of smoke, an unpleasant smell and a useless lump of silicon.)

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.

Our examples so far have satisfied this condition, but the following one does not.

   architecture a1 of foo 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:

Image:inferLatch.gif

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.

Other Complex Statement Types

Case Statement

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. The case statement can be implemented using a decoder, the outputs of which enable the different statement lists. This often results in a faster circuit that would be produced for an equivalent 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 of 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 wordSize-1 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;
       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.

Image:Adder4.gif

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.

Specifying Clocked Sequential Circuits

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 signals whose values are to be stored in flip flops or registers of flip flops. Here's an example.

   if rising_edge(clk) then
       x <= a xor b;
   end if;

The condition in the if-statement is the synchronization condition. The scope of the synchronization condition is the body of the if-statement. What this means is that assignments to signals within the body of the if statement are to occur only when the signal clk makes a transition from low to high. We obtain this behavior in a digital circuit by associating the signal x with a positive edge-triggered D flip flop, with clk connected to the clock input and the signal and an xor gate with inputs a and b connected to the D input.

Note that since x is associated with a D-flip flop controlled by the rising edge of clk, it does not make sense to have other assignments to x with incompatible synchronization conditions or no synchronization condition at all. It's usually most 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.

Serial Comparator

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 rising_edge(clk) 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 synthesizer will generate the appropriate logic to handle the "no-change" conditions, but we need not write any explicit code for them. Also note that the sensitivity list in the first process contains only the clock signal. This is sufficient because signal changes only occur when clk changes. So unlike a process specifying a purely combinational circuit, there is no need to include the other signals that are used in the process.

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 the low-to-high transitions of G and L are not.

[[Image:SimulatorOutput.jpg|center]

VHDL allows us to define signals with enumerated types so that we can 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 rising_edge(clk) 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.

Counting Pulses

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 is 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;
   --
   -- Count the number of pulses in the input bit stream.
   -- A pulse is a 01...10 pattern.
   --
   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 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 rising_edge(clk) 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.

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.


Priority Queue

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.

  1. 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.
  2. 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.
  3. 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, whild performaning 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;

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.

Variables in VHDL

In addition to signals, VHDL supports a similar but different construct called a variable, which has an associated variable assignment statement, which uses a distinct assignment operation symbol (:=). We have already seen a special case of variables, in the loop indexes used in for-loops. But we can also declare variables and use them in ways that are similar to the way signals are used.

Unlike signals, variables do not correspond to wires or any other physical element of a synthesized circuit. The best way to think of a variable is as a short-hand notation representing the expression that was most recently assigned to the variable. So for example, the VHDL code fragment shown below with assignments to the variable y

   a <= x"3a";
   y := a + x"01";
   b <= y;        
   y := y + x"10";
   c <= y;          

is exactly equivalent to the fragment

   a <= x"3a";
   b <= a + x"01"; 
   c <= (a+x"01") + x"10";

Such uses of variables are not necessary, since we can always eliminate the variables by replacing each occurrence of a variable with the variable-free expression it represents. However, judicious use of variables can make code easier to both write and to understand. For example, the code fragment shown below implements an adder circuit, using a logic vector to represent the carries joining stages together.

   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;
   

When we introduced this example earlier, we pointed out that it was important to define C to be a logic vector, rather than a simple signal. However, we can re-write it using a variable for C, making the code a little simpler.

   architecture a1 of adder is  
   begin
       process (A,B,Ci)
       variable C: std_logic; 
       begin
           C :=Ci;
           for i in 0 to wordSize-1 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;
       end process;
   end a1;

To understand why this works, remember that the meaning of the for-loop can be understood by unrolling the loop and substituting the appropriate values for the loop index i. Thus, each variable assignment to C will correspond to a different value of the loop index and when the signal assignment for each value of S(i) is processed, it will substitute the appropriate expression in place of the variable name C. The behavior of variable assignments in VHDL is much like the behavior of variable assignments in conventional sequential programming languages and distinctly different from the behavior of signal assignments. But the underlying mechanisms that lead to this behavior are distinctly different.

The use of both signals and variables in VHDL can be confusing, especially at first. Indeed, while variables can be helpful, the fact that they behave differently from signals can make code harder to understand, rather than easier. You may prefer to limit your use of variables to contexts where they can't be avoided (like loop indexes), and this is certainly a reasonable choice to make, since most uses of variables are not really necessary. To reiterate, it's generally best to think of a variable as just a short-hand notation for the expression most recently assigned to the variable name. Variables correspond to nothing physical in the synthesized circuit, but they can simplify the specification of a circuit.

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 frstOne 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.

Personal tools
Design Problems
Source Files
Extra Material
Tutorials