Chapter 21. Design for Synthesis

In this book we have discussed many aspects of VHDL and looked at examples of its use. One very strong motivation for using VHDL is hardware synthesis. The idea behind synthesis is to allow us to think of our design in abstract terms. We need not be so concerned about how best to implement the design in hardware logic—that is the job of the synthesis tool. It converts our abstract description into a structural description at a lower level of abstraction.

This chapter offers a brief introduction to synthesis, based on the IEEE standards that cover synthesis of VHDL models. A full coverage of the topic warrants a complete book in its own right. We refer the interested reader to the large number of books on hardware synthesis (for example, [1]).

Synthesizable Subsets

There are several synthesis tools available from different design automation tool vendors. While many of them perform the same general process, they differ in their command sets and the way in which we specify synthesis constraints. Hence we discuss synthesis tools only in very general terms. More important, synthesis tools differ in the subsets of VHDL that they accept as input. The majority only accept designs described at the register-transfer level and synthesize to circuits composed of gates, flipflops, registers, and other basic components. A small number of behavioral synthesis tools accept designs described at a higher level of abstraction. However, developing behavioral synthesis technology that is usable in practice has proven to be very difficult, so the techniques are not widely adopted. They are most successful in certain specialized application areas, such as digital signal processing.

The disparity between synthesis tools motivated the development of IEEE Standard 1076.6, Standard for VHDL Register Transfer Level Synthesis. The first version of this standard, published in 1999, specified a “level-1” lowest common denominator subset of VHDL that was acceptable to most synthesis tools. The intention was to assist designers in writing models that were portable between synthesis tools and to ensure that the behavior of the synthesized designs matched simulation results.

In 2004, a revision of the standard was published, specifying a level-2 synthesis subset of VHDL. The intention of this subset, as described in the Introduction to the standard, is “to include a maximum subset of VHDL that could be used to describe synthesizable RTL logic.” It provides considerably more flexibility in the way models can be written. It is much closer to the subsets now implemented by synthesis tools. Nonetheless, there remains variation among tools, so we need to consult the documentation for any particular tool that we might use to find out what forms of input it accepts.

There are two aspects of synthesis subsets of VHDL. The first is the collection of language features that are included in the subset. This comprises the types that can be used to represent data and the declarations, specifications, and statements that can be included in models. The second aspect is the way in which we write code to represent different hardware elements. The task of a synthesis tool is to analyze a VHDL description and infer what hardware elements are represented and how they are connected. A tool cannot infer hardware from any arbitrarily written VHDL model. Instead, we need to write models in a synthesis style that is recognized by the tool. Style guidelines include templates for process statements from which various kinds of hardware elements can be inferred, and restrictions on the way in which we write and combine statements. We will look at both of these aspects, as specified by the IEEE 1076.6-2004 standard.

One further point to note about synthesis subsets is that they have historically lagged behind revisions of the VHDL standard. As an illustration, the 1999 version of the IEEE 1076.6 standard specified that models use the VHDL-87 version of the language, despite VHDL-93 having been published six years earlier. In 1999, many synthesis tools still only supported VHDL-87; now, VHDL-93 is widely supported. The changes between VHDL-93 and VHDL-2002 were relatively minor, apart from the addition of protected types. Since these are not supported for synthesis, synthesis tools effectively support VHDL-2002. That is the version of the language referenced in IEEE 1076.6-2004. The changes between VHDL-2002 and VHDL-2008 are much more significant. If past experience is an indicator, it may be some time before synthesis vendors implement the changes in their tools. Again, we should consult the documentation for any particular tool to see whether it supports VHDL-2008 features. The notes throughout this book describing differences between earlier versions of VHDL and VHDL-2008 will also be helpful as we write synthesizable models.

Use of Data Types

The synthesis standard allows us to use the following data types:

  • enumeration types, including the predefined types boolean, bit and character

  • integer types, including the predefined type integer and the subtypes natural and positive

  • arrays of scalar elements, including the predefined types bit_vector and string

  • std_ulogic, std_ulogic_vector, std_logic and std_logic_vector, defined in package std_logic_1164

  • unsigned and signed, defined in package numeric_bit

  • unsigned and signed, defined in package numeric_std

The synthesis standard allows us to use these types for constants, signals, and variables. When we declare a constant, we must include an initial value expression to give the constant a value. We cannot declare a deferred constant in a package. The synthesis standard specifies that any initial value expression in a signal or variable declaration is ignored. This makes sense in some circuits, such as ASICs, where the initial value of a storage location is indeterminate. In FPGAs, however, the storage can be initialized to specified values. Tools for synthesizing to FPGAs may allow an initial value expression in a signal or variable declaration for this purpose.

Scalar Types

Models conforming with the synthesis standard may define and use enumeration types, with some restrictions. The predefined types boolean and bit and the standard logic types std_ulogic and std_logic are implemented in hardware as individual bits. Most of the time, we use std_ulogic and std_logic, since that allows us to represent high-impedance and unknown states, as well as low and high logic levels. User-defined enumeration types may be implemented by tool-dependent encoding. Alternatively, we may specify the encoding by decorating the type with a string attribute, enum_encoding, described in Section 21.7.

Models conforming with the synthesis standard may also define and use integer types. Values of these types are implemented in the synthesized design as vectors of bits. If an integer type includes only non-negative values, the synthesized vector uses unsigned binary encoding. If the type includes negative values, two’s-complement signed encoding is used. The number of bits in the encoding is determined by the range of values in the type. For example, given the following declarations in a model:

   type sample is range -64 to 63;
   subtype table_index is natural range 0 to 1023;

values of type sample should be implemented using 7-bit two’s-complement encoding, and values of subtype table_index should be implemented using 10-bit unsigned encoding. Types that don’t include 0 are encoded as though 0 were allowed. For example, the type

   type index_type is range 4 to 15;

would be represented using 4-bit unsigned encoding. Synthesis tools conforming with the standard should support integers within the range -231 to +231 - 1, mapping to 32-bit two’s-complement encoding.

The synthesis standard also allows use of other predefined enumeration types, including character, but they may not be supported by tools. The remaining classes of scalar types, namely, physical and floating-point types, are not supported by the synthesis standard. Definition and use of such types in a model are either ignored or treated as an error.

Composite and Other Types

Models conforming with the synthesis standard may define and use array and record types, but there are some significant restrictions on the use of array types. They must be indexed by an integer range, and the index bounds must be static, so that the synthesis tool can determine how much storage or how many bits of data are required in the hardware. The element type can only be an allowed scalar type, as described above, or a one-dimensional vector of an enumeration type representing individual bits. Thus, for example, the following array types are permissible:

   type coeffs is array (3 downto 0) of integer;
   type channel_states is array (0 to 7) of state;
    -- state is an enumeration type
   subtype word is bit_vector(31 downto 0);
   type reg_file is array (0 to 15) of word;

whereas the following are not:

   type color is (red, green, blue);
   type plane_status is array (color) of boolean;
    -- non-integer index type
   type matrix is array (1 to 3, 1 to 3) of real;
    -- 2D, and floating-point elements
   type reg_file_set is array (0 to 3) of reg_file;
    -- elements are vectors of non-bits

In addition, some tools limit arrays to be one-dimensional. Such tools would not allow the matrix type shown above.

The types unsigned and signed defined in numeric_bit and numeric_std are array types that meet the requirements for synthesizability, since they are one-dimensional arrays of elements that represent bits. The synthesis standard requires that we use these types if we need to represent unsigned or signed numbers at the bit level. We cannot use array types that we define in our model.

Historically, many designers have used the non-standard packages std_logic_arith, std_logic_signed, and std_logic_unsigned. These packages provide types and operations similar to those now provided by numeric_std and numeric_std_unsigned. With VHDL-2008, the standard packages incorporate all the operations provided by the non-standard packages. Nonetheless, synthesis tools still support use of the non-standard packages for representing numeric data at the bit level.

The synthesis standard does not support use of access types, file types or incomplete type declarations. Synthesis tools should ignore their declarations and are not required to accept models that use access-type values or file operations. In particular, dynamic allocation of objects using the new allocator and deallocation of objects using the deallocate procedure are not supported. The synthesis standard does support declaration of subtypes, but ignores user-defined resolution functions within subtype indications.

Interpretation of Standard Logic Values

If we use the standard logic types std_ulogic or std_logic in our models, we need to consider how a synthesis tool interprets values of different driving strength and unknown values. The synthesized hardware deals only with logic 0 and 1 values. We use standard logic values other than 0 and 1 to simulate the effects of weak driving strength and indeterminate logic values. We use the term metalogical to refer to the values ‘U’, ‘X’, ‘W’, and ‘–’ that do not represent logic levels.

When our model assigns to a signal a value calculated by an expression from other signal values, the synthesis tool generates a hardware circuit that implements the logic of the expression. However, when our model uses a literal standard logic value, the synthesis tool must represent the value as either a logic 0 or a logic 1. The synthesis standard specifies that the standard logic values ‘0’ and ‘L’, like the bit value ‘0’ and the Boolean value false, are represented as a logic 0. Similarly, the standard logic values ‘1’ and ‘H’, like the bit value ‘1’ and the Boolean value true, are represented as a logic 1. Thus the synthesis tool does not attempt to interpret the strength information associated with the standard logic value.

When our model assigns the standard logic value ‘Z’ to a signal, the synthesis tool generates a tristate buffer for the signal. Usually such an assignment is nested within a conditional statement. In that case, hardware generated for the condition is used to enable or disable the tristate buffer. For example, the if statement

   if request_enable = '1' then
    request <= ready;
   else
    request <= 'Z';
   end if;

would result in synthesis of a tristate buffer driving request. The input to the buffer would be connected to ready, and the control signal to enable the buffer would be connected to request_enable. When our model uses ‘Z’ in contexts other than a signal assignment (for example, in a comparison expression), the synthesis tool treats it in the same way as a metalogical value.

Use of metalogical values in a model is either ignored or not accepted by the synthesis tool, depending on the context. When values are tested for equality with metalogical values, the result is deemed to be false. Similarly, a test for inequality with metalogical values is deemed to be true. The rationale is that real hardware values are known to be either logic 0 or logic 1, so it does not make sense to synthesize hardware to compare with any other values. Thus any statements controlled by an equality comparison with a metalogical value, such as statements nested in an if statement or a case statement, can be ignored by the synthesis tool. They exist in the model purely for simulation purposes. In the cases of metalogical values appearing as operands of other relational operators and of arithmetic, logical and shift operators, the synthesis tool should not accept the model.

The std_match function defined in the numeric_std package can be used to compare standard logic values and vectors. It has the advantage of producing the same results in simulation and synthesis, unlike comparison using the “=” operator. Synthesis tools represent the use of std_match by an equivalence test. Simulation tools perform the comparison ignoring the driving strength of the parameters. If both values represent the same logic level, the comparison returns true. If either value is a metalogical value other than ‘–’, the comparison returns false. The value ‘–’ is interpreted as “don’t care”, so comparison with it returns true. Synthesis of a comparison using std_match with a literal vector containing “don’t care” elements results in comparison hardware that excludes the “don’t care” bits from the comparison. VHDL-2008 defines the matching relational operators, including “?=”, which has the same behavior for std_ulogic and std_ulogic_vector values as std_match. As synthesis tools evolve to implement VHDL-2008 features, we should expect to see them treat the matching relational operators in a similar way to std_match.

Modeling Combinational Logic

Combinational circuits are those in which the outputs are determined solely by the current values of inputs; the circuit does not maintain any internal state. The simplest way to model combinational logic in synthesizable VHDL is using concurrent signal assignment statements. For example, we can model a Boolean function of inputs as follows:

   status <= '1' when ready and sample < limit else '0';

A synthesis tool would generate hardware composed of a comparator for the input signals sample and limit, and a gate to combine the comparison output with the signal ready. It may optimize the hardware to meet timing and area constraints, but the result would perform the same function. Note that we have written the statement in this way to be compatible with VHDL-2002. As tools include VHDL-2008 features, we can rewrite the statement as

   status <= ready and sample ?< limit;

Example 21.1. Modeling arithmetic circuits

We can model a combinational arithmetic circuit using a concurrent assignment with an arithmetic expression on the right-hand side. For example, the following assignment in an architecture represents an adder for unsigned operands a and b, producing an unsigned result, sum, of the same size:

   sum <= a + b;

If we also need to include a carry input to an addition, we can write the following in VHDL-2008:

   sum <= a + b + carry_in;

In earlier versions of the numeric_bit and numeric_std packages, there was no overloaded addition operator with a scalar operand. We would have to write this as

   sum <= a + b + unsigned'(0 => carry_in);

to create a one-element vector from the carry_in scalar. Alternatively, if we are using a synthesis tool that supports the std_logic_arith package, we could use the vector/scalar operator defined there.

If we want the carry result of the addition, we need to extend the operands by one bit, so that the result is one bit longer than the operands. We can then use the extra result bit as the carry:

   tmp_sum <= ('0' & a) + ('0' & b);
   sum <= tmp_sum(7 downto 0);
   carry <= tmp_sum(8);

In VHDL-2008, we can write this as:

   (carry, sum) <= ('0' & a) + ('0' & b);

Example 21.2. Modeling a multiplexer

We can use a selected signal assignment statement to describe a multiplexer, for example:

   with addr(1 downto 0) select
    request <=  request_a when "00",
                request_b when "01",
                request_c when "10",
                request_d when "11";

This assumes bit_vector signals. If we are using std_ulogic_vector signals, the choices do not cover all possible values. We would have to include a further alternative as follows:

   with addr(1 downto 0) select
    request <=  request_a when "00",
                request_b when "01",
                request_c when "10",
                request_d when "11",
                'X' when others;

A synthesis tool would interpret the choices covering valid logic levels as implying hardware, and the others choice as representing metalogical values for simulation purposes, to be ignored.

We could also have expressed this behavior using a conditional signal assignment statement, as follows:

   request <= request_a when addr(1 downto 0) = "00" else
             request_b when addr(1 downto 0) = "01" else
             request_c when addr(1 downto 0) = "10" else
             request_d when addr(1 downto 0) = "11" else
             'X';

However, in a conditional signal assignment, the conditions need not be mutually exclusive, so the synthesis tool would infer a priority-encoded chain of multiplexers to conform with the language semantics. This structure would be slower than a simple multiplexer. The tool may be able to optimize the hardware, but it is safer to use a selected signal assignment to imply a multiplexer function if that is our design intent.

One situation in which a conditional signal assignment statement is appropriate is combinational logic with a tristate buffered output, for example:

   data_bus <= resize(sample_byte, 16)
                when std_match(sample_enable, '1') else
              "ZZZZZZZZZZZZZZZZ";

We can also use a process statement to describe combinational logic. The process must be sensitive to all of the inputs, and the combinational outputs must be assigned values in all possible executions of the process. This form of process is most useful when there are multiple outputs.

Example 21.3. A combinational process for multiple outputs

Suppose we need to model a block of logic that has multiple outputs with tristate drivers all controlled by the same condition. We could use separate conditional signal assignment statements, but that would require us to repeat the condition in each one. Instead, we use a process to represent the logic block, as follows:

   read_sample : process ( read_enable,
                          sample, limit_exceeded, ready )
   begin
    if std_match(read_enable, '1') then
      data <= sample;
      parity <= calc_parity(sample);
      status <= ready and not limit_exceeded;
    else
      data <= "ZZZZZZZZ";
      parity <= 'Z';
      status <= 'Z';
    end if;
   end process read_sample;

In this process, any change in any of the inputs results in new values being determined for all of the outputs. Thus the design is purely combinational. A synthesis tool would infer combinational network with tristate drivers on the outputs.

When we write a process that declares and uses a variable, a synthesis tool may infer a need for storage in the synthesized hardware. However, if all possible executions of the model in response to input changes involve the variable being assigned a value before being read, no storage is needed.

Example 21.4. Intermediate variables in combinational processes

Consider the following process containing a variable assignment:

   adder : process ( sel, a, b, c )
    variable operand : integer;
   begin
    if sel = '1' then
      operand := a;
    else
      operand := b;
    end if;
    sum <= operand + c;
   end process adder;

The process is sensitive to all of the inputs. There are two possible execution paths when an input changes. If sel is ‘1’, the value of a is assigned to operand and subsequently added with c to determine the output sum. Alternatively, if sel is ‘0’, the value of b is assigned to operand and added with c. Thus we can think of operand as representing the intermediate node in a combinational network consisting of a multiplexer and an adder. The value assigned to operand need not be stored.

Note the importance of including in the sensitivity list all inputs that are read by a combinational process. A synthesis tool will typically issue a warning if an input is read in the process but not mentioned in the sensitivity list. The difficulty in maintaining consistency between the sensitivity list and the set of signals read is the main motivation for allowing the reserved word all in sensitivity lists in VHDL-2008. Since this is a relatively minor extension for synthesis vendors to implement, we would hope to see it introduced quickly.

Modeling Sequential Logic

Sequential circuits are those that maintain an internal state. The outputs they produce in response to given inputs depend on the history of inputs received previously. Most sequential circuits we design are synchronous, or clocked. They use a rising or falling edge of a clock, or a level of an enable signal, to control advance of state or storage of data. The synthesis standard supports descriptions of these kinds of circuits. Most current design methodologies prefer edge-triggered sequential design, since achieving correct timing is more straightforward. Occasionally we might design an asynchronous circuit: a sequential circuit without a clock or enable input. Such circuits store state using combinational feedback loops. The synthesis standard and most synthesis tools do not support synthesis of these kinds of circuits. If we must include them, we must describe them using structural models and instantiate them as components within a synthesizable model.

Unlike signals used for data, which can be of a fairly wide variety of types, clock signals are restricted to be of type bit, std_ulogic or a subtype such as std_logic. A clock signal need not necessarily be a single scalar signal; it may be a scalar element of an array of bit or std_ulogic values.

Modeling Edge-Triggered Logic

We model edge-triggered sequential logic using processes. The synthesis standard allows considerable flexibility in modeling edge-triggered sequential logic, though not all synthesis tools implement the full generality. The premise is that a signal or variable assignment executed under control of a clock-edge condition implies edge-triggered storage. Clock-edge conditions are expressions of the following forms, for rising clock-edges:

  • rising_edge( clock_signal_name )

  • clock_signal_name’event and clock_signal_name= ‘1’

  • clock_signal_name= ‘1’ andclock_signal_name’event

  • not clock_signal_name’stable and clock_signal_name= ‘1’

  • clock_signal_name= ‘1’ and notclock_signal_name’stable

and for falling clock-edges:

  • falling_edge( clock_signal_name )

  • clock_signal_name’event and clock_signal_name= ‘0’

  • clock_signal_name= ‘0’ and clock_signal_name’event

  • not clock_signal_name’stable and clock_signal_name= ‘0’

  • clock_signal_name= ‘0’ and not clock_signal_name’stable

We can write an expression of one of these forms in the condition of an if statement within a process. The process must also have the clock signal name in its sensitivity list. Any signal or variable assignments within the if statement are then said to be synchronous assignments, controlled by the clock-edge condition. We can also include assignments in if statements controlled by other conditions involving asynchronous control signals. We must also include the control signals in the sensitivity list of the process. The assignments within such if statements are called asynchronous assignments, as they are not controlled by a clock-edge condition. The operators in expressions on the right-hand sides of assignments, whether synchronous or asynchronous, imply combinational logic connected to register inputs. As well as assignments, we can include other sequential statements within the process. These statements govern the flow of control leading to assignments. Thus, they imply combination logic, such as multiplexers, that feed the inputs of registers implied by the assignments. There are some restrictions, however. For example, we cannot arbitrarily include wait statements, nor can we refer to clock-edge expressions in assignments.

Example 21.5. Edge-triggered register

One of the simplest forms of process represents an edge-triggered register:

   simple_reg : process ( clk ) is
   begin
    if clk'event and clk = '1' then
      reg_out <= data_in;
    end if;
   end process simple_reg;

In this process, the assignment to reg_out is a synchronous assignment, controlled by the rising clock-edge condition. The process represents a register with clk as the clock signal, data_in as the input, and reg_out as the output.

Example 21.6. Edge-triggered register with synchronous control inputs

We can include more involved statements within the controlling if statement to model registers with synchronous control signals. For example, the following process models a register with synchronous reset and enable controlling storage for two output signals:

   dual_reg : process ( clk ) is
   begin
    if rising_edge(clk) then
      if reset = '1' then
        q1 <= X"00";
        q2 <= X"0000";
      elsif en = '1' then
        q1 <= d1;
        q2 <= d2;
      end if;
    end if;
   end process dual_reg;

In this case, all of the assignments are synchronous, governed by the rising clock-edge condition. The nested if statement chooses between resetting the outputs, updating them, or leaving them unchanged. A synthesis tool could infer a register, updated on every clock-edge, with a multiplexer at the input selecting between the hardwired 0 values, the data inputs, and the fed-back outputs. Alternatively, if the target technology supports registers with separate reset and enable control signals, the tool may infer use of them. We will see in Section 21.7 how we can use attributes to select the implementation.

Example 21.7. Counters as registers combined with arithmetic

We can combine computational logic and storage in the one process. The computational logic is represented by the expressions in assignments. Counters are a good illustration of this approach. The following process represents an up/down counter wtih synchronous reset, load and count enable:

   signal d_in, count : unsigned(11 downto 0);
   ...
   up_down_counter : process (clk) is
   begin
    if rising_edge(clk) then
      if reset = '1' then
        count <= X"000";
      elsif load_en = '1' then
        count <= d_in;
      elsif count_en = '1' then
        if dir = '1' then
          count <= count + 1;
        else
          count <= count - 1;
        end if;
      end if;
    end if;
   end process up_down_counter;

We could augment this with a concurrent assignment statement to derive a terminal count signal:

   tc <= '1' when std_match(count, X"111") else '0';

Example 21.8. Register wtih asynchronous control signals

If our implementation technology provides registers with asynchronous control signals, we can represent them with processes containing asynchronous assignments. For example, the following process represents a register with asynchronous reset:

   reg : process (clk, reset) is
   begin
    if reset = '1' then
      q <= "0000";
    elsif rising_edge(clk) then
      q <= d;
    end if;
   end process reg;

In this process, the first assignment is not controlled by a clock-edge condition; hence, it is asynchronous, and the reset control signal must be included in the sensitivity list of the process. The second assignment is synchronous and models the edge-triggered behavior of the register.

Example 21.9. Shift register with asynchronous and synchronous control

We can combine asynchronous and synchronous control in a single process. For example, the we could model a shift register with asyncrhnous reset and synchronous parallel load as follows:

   shift_reg : process (clk, reset) is
   begin
    if reset = '1' then
      q <= "00000000";
    elsif rising_edge(clk) then
      if load_en = '1' then
        q <= d_in
      else
        q <= q(6 downto 0) & d_s;
      end if;
    end if;
   end process reg;

Note that we do not include the load_en signal in the sensitivity list, as it is a synchronous control signal.

The synthesis standard allows more involved structures than those demonstrated by the preceding examples. For example, it lists the following as a legal synthesizable process:

   RegProc5 : process( clk, reset )
   begin
    if (en = '1' and rising_edge(clk)) or reset = '1' then
      if reset = '1' then
        Q <= '0'; -- async assignment
      elsif en = '1' and rising_edge(clk) then -- sync condition
        Q <= D; -- sync assignment
      end if;
    end if ;
   end process ;

While it is, in principle, possible to analyze the control flow in such a process and determine whether each of the assignments is synchronous or asynchronous, synthesis tools are generally more restrictive in what they will accept. It is often clearer to write processes in the forms illustrated by the preceding examples than in more convoluted forms.

Recall that a process with signals listed in the sensitivity list is equivalent to a process containing a wait statement that is sensitive to the signals. Synthesis tools allow us to express edge-sensitive behavior using explicit wait statements in a process, with some restrictions. The synthesis standard specifies quite complicated rules for the structure of such wait statements, covering the signals that can be listed in the on clause and the form of condition that can be written in the until clause. (A for clause is not allowed, as that would imply specific timing.) Current synthesis tools are more restrictive, since inferring control logic for the general cases allowed by the standard could be arbitrarily complicated.

The simplest case allowed by synthesis tools is a wait statement as the first statement in the process, with a clock-edge condition in the until clause. There must not be any other wait statements or references to clock-edges in the process, nor in any procedures called from the process.

Example 21.10. Edge-triggered register with explicit wait statement

We can represent and edge-triggered register with synchronous reset as follows:

   reg : process is
   begin
    wait until rising_edge(clk);
    if reset = '1' then
      q <= X"00";
    elsif en = '1' then
      q <= d;
    end if;
   end process dual_reg;

The assignments to q only occur after a rising edge occurs on the clk signal. Hence, both reset and en are synchronous control signals.

Example 21.11. Explicit wait and asynchronous control

The synthesis standard allows us to write the following process to express asynchronous control using an explicit wait statement.

   reg : process is
   begin
    wait until reset = '1' or rising_edge(clk);
    if reset = '1' then
      q <= X"00";
    elsif rising_edge(clk) then
      if en = '1' then
        q <= d;
      end if;
   
    end if;
   end process dual_reg;

The asynchronous condition is included in the wait statement. The wait statement is followed immediately by an if statement that tests both the asynchronous condition and the clock-edge condition. Thus, in this example, reset is an asynchronous control signal and en is a synchronous control signal. While this is acceptable according to the synthesis standard, not all tools accept it, instead limiting conditions in wait statements to just clock-edge conditions.

The synthesis standard allows a process to include multiple explicit wait statements, though some tools do not support it. In general, the hardware inferred for such a process includes some form of state machine, since the hardware must keep track of progress through the statements from one cycle to the next. Compare this with a process containing only one wait statement (explicit or implied), which always performs one complete pass through the process statement body for each clock-edge.

The rules for processes with multiple wait statements require that each statement must wait for the same condition, and in particular, must wait for the same edge of a single clock signal. This makes sense, as the wait statements correspond to transitions in the controlling state machine in the inferred hardware. If the wait statements include asynchronous conditions, as in Example 21.11, then each wait statement must be followed by identical tests for those conditions. Again, this corresponds to the control hardware implementation. The asynchronous behavior is that of the state machine.

Example 21.12. Sequential multiplier

We can describe a multiplier that takes multiple clock cycles to compute its result using a shift-and-add method. The following process, based on an example in the IEEE 1076.6 standard, describes the behavior:

   MultProc : process is
   begin
    wait until rising_edge(clk);
    if start = '1' then
      done <= '0';
      P <= (others => '0'),
      for i in A'range loop
        wait until rising_edge(clk);
        if A(i) = '1' then
          P <= (P(6 downto 0) & '0') + B;
        else
          P <= P(6 downto 0) & '0';
        end if;
      end loop;
      done <= '1';
   
    end if;
   end process;

This process implies an edge-triggered state machine that controls registers for the done and P output signals. The state machine tests the start signal on each clock-edge. When it is ‘0’, it leaves P unchanged and sets done. Otherwise, it resets done and P, then sequences through a number of cycles (determined by A’range) to update P with successive partial products. On completion of the sequence, it sets done again.

Where wait statements appear in a loop that implements sequential behavior, we can use an exit or next statement after each wait statement to describe reset behavior. For asynchronous reset, this is a case of each wait statement being followed by identical tests for asynchronous conditions.

Example 21.13. Reset in a loop

The following process from the IEEE 1076.6 synthesis standard models a UART serializer for data transmission:

   UartTxFunction : process is
   begin
    TopLoop : loop
      if nReset = '0' then
        SerialDataOut <= '1';
        TxRdyReg <= '1';
      end if;
      wait until nReset = '0' or
                 (rising_edge(UartTxClk) and DataRdy = '1'),
      next TopLoop when nReset = '0';
      SerialDataOut <= '0';
      TxRdyReg <= '0';
      -- Send 8 Data Bits
      for i in 0 to 7 loop
        wait until nReset = '0' or rising_edge(UartTxClk);
        next TopLoop when nReset = '0';
        SerialDataOut <= DataReg(i);
        TxRdyReg <= '0';
      end loop;
      -- Send Parity Bit
      wait until nReset = '0' or rising_edge(UartTxClk);
      next TopLoop when nReset = '0';
      SerialDataOut <=
        DataReg(0) xor DataReg(1) xor DataReg(2) xor
        DataReg(3) xor DataReg(4) xor DataReg(5) xor
   
        DataReg(6) xor DataReg(7);
      TxRdyReg <= '0';
      -- Send Stop Bit
      wait until nReset = '0' or rising_edge(UartTxClk);
      next TopLoop when nReset = '0';
      SerialDataOut <= '1';
      TxRdyReg <= '1';
    end loop;
   end process;

Each wait statement in the process includes a test for a rising edge of the UartTxClk signal, as well as for the the asynchronous nReset signal. The identical next statements all restart the outer loop when the asynchronous reset condition is true.

We can rewrite this process to describe synchronous reset, as follows:

   UartTxFunction : process is
   begin
    TopLoop : loop
      wait until rising_edge(UartTxClk);
      if nReset = '0' then
        SerialDataOut <= '1';
        TxRdyReg <= '1';
      elsif DataRdy = '1' then
        SerialDataOut <= '0';
        TxRdyReg <= '0';
        -- Send 8 Data Bits
        for i in 0 to 7 loop
          wait rising_edge(UartTxClk);
          exit TopLoop when nReset = '0';
          SerialDataOut <= DataReg(i);
          TxRdyReg <= '0';
        end loop;
        -- Send Parity Bit
        wait rising_edge(UartTxClk);
        exit TopLoop when nReset = '0';
        SerialDataOut <=
          DataReg(0) xor DataReg(1) xor DataReg(2) xor
          DataReg(3) xor DataReg(4) xor DataReg(5) xor
          DataReg(6) xor DataReg(7);
        TxRdyReg <= '0';
        -- Send Stop Bit
        wait until rising_edge(UartTxClk);
        exit TopLoop when nReset = '0';
        SerialDataOut <= '1';
        TxRdyReg <= '1';
   
      end if;
    end loop;
   end process;

In this case we have used exit statements instead of next statements, though the latter would work just as well.

The until clause in a wait statement can also be an expression of the following forms, in addition to those listed on page 642:

  • clock_signal_name= ‘1’

  • clock_signal_name = ‘0’

Since the wait statement waits until the signal changes value, a change to ‘1’ must represent a rising edge, and a change to ‘0’ must represent a falling edge. Thus, we could write the wait statement in Example 21.10 as:

   wait until clk = '1';

While this is allowed, the form using rising_edge is a preferred style, since it is more descriptive and deals correctly with weak driving strengths (‘L’ and ‘H’) during simulation.

Level-Sensitive Logic and Inferring Storage

Level-sensitive sequential logic maintains state, but does not respond to clock-edges. Instead, state is usually updated under control of an enable signal. While the enable signal is asserted, the state can change according to data inputs. While the enable signal is negated, changes on the data inputs are ignored and the circuit maintains its current state.

Example 21.14. A transparent latch

Consider the following model for a transparent latch:

   latch : process ( enable, d )
   begin
    if enable = '1' then
      q <= d;
    end if;
   end process latch;

This process is sensitive to changes on the enable and d inputs. If enable is ‘1’ when either input changes, the data value is used to update the output q. If enable is ‘0’, the current value is maintained on q. This behavior is implied by the semantics of signals and signal assignment in VHDL. When the synthesis tool implements the model as a hardware circuit, it must provide some storage to maintain the value for the output. The tool must infer the need for storage from the model semantics.

In general, a synthesis tool must infer storage if there are possible executions of the process that do not involve assignment to a given signal or variable. If the process does not include clock-edge conditions, then level-sensitive storage is inferred. The process must then include in its sensitivity list all signals that the process reads. The latch example illustrates storage inference due to existence of paths on which a signal is not updated. If the process executes when enable is ‘0’, the assignment to q is bypassed, so storage is inferred for q.

Example 21.15. Transparent latch with reset

The following process is another example of a latch, in this case involving storage inference for a variable:

   latch_with_reset : process ( enable, reset, d )
    variable stored_value : bit;
   begin
    if reset = '1' then
      stored_value := '0';
    elsif enable = '1' then
      stored_value := d;
    end if;
    q <= stored_value;
   end process latch_with_reset;

The output signal q is assigned on every execution of the process, so no storage is inferred for it. However, the variable stored_value is not assigned when reset and enable are both ‘0’; hence storage is inferred for the variable.

In principle, storage is also inferred for a process that does not include a reference to a clock-edge expression if there are possible executions in which a signal or variable is read before being assigned. However, such a process represents an asynchronous sequential circuit and is not supported by synthesis tools. Consider the following erroneous process intended to describe a counter with reset:

   counter : process ( count_en )
    variable count : natural range 0 to 15;
   begin
    q <= (q + 1) mod 16;
   end process counter;

The process is sensitive to changes of count_en. Whenever that signal changes, the old value of q is read and incremented to determine the new value. The value must be maintained until the next change of count_en, implying the need for storage for q, even though it is assigned in all possible executions of the process.

The synthesis standard recommends against writing level-sensitive processes in which signals or variables are read before being assigned. Usually such processes are not what we intend, and the inference of storage is inadvertent. On the other hand, a process in which a variable is first assigned and then read in all possible execution paths is legal and useful. Such a process simply models combinational logic, as we discussed in Section 21.4, with the variable denoting an intermediate node in the combinational network.

Modeling State Machines

Many designs expressed at the register-transfer level consist of combinational data paths controlled by finite-state machines. Hence it is important to be able to describe a finite-state machine in such a way that it can be synthesized. The preferred style is to separate the implementation into two processes, one describing the combinational logic that calculates the next state and output values, and the other being a register that stores the state. This style is accepted by all synthesis tools, whereas finite-state machines implied by multiple wait statements in a process, as described in Section 21.5.1, are not uniformly supported.

Example 21.16. Finite-state machine with Mealy and Moore outputs

The following architecture body represents a finite state machine described using two processes:

   architecture rtl of state_machine is
    type state is (ready, ack, err);
    signal current_state, next_state : state;
   begin
    next_state_and_output : process ( current_state, in1, in2 )
    begin
      case current_state is
        when ready =>
          out1 <= '0';
          if in1 = '1' then
            out2 <= '1';
            next_state <= ack;
          else
            out2 <= '0';
            next_state <= ready;
          end if;
        when ack =>
          out1 <= '0';
          if in2 = '1' then
            out2 <= '0';
            next_state <= ready;
          else
            out2 <= '0';
            next_state <= err;
          end if;
   
        when err =>
          out1 <= '1';
          out2 <= '0';
          next_state <= err;
      end case;
    end process next_state_and_output;
    state_reg : process ( clk, reset )
    begin
      if reset = '1' then
        current_state <= ready;
      elsif rising_edge(clk) then
        current_state <= next_state;
      end if;
    end process state_reg;
   end rtl;

The architecture body defines an enumeration type for the state values. We can either rely on the synthesis tool to determine the encoding for the state or use the enum_encoding attribute to define our own encoding, as described in Section 21.7. The signal current_state represents the output of the state register, and the signal next_state is the state to be assumed by the state machine on the next clock-edge. The process next_state_and_output describes the combinational logic. It uses a case statement to determine the next state and output values, depending on the current state value. Note that the output out1 is uniquely determined by the current state. It is a Moore machine output. The output out2, on the other hand, depends on both the current state and the current inputs to the machine. It is a Mealy machine output. The process state_reg describes the state register. It has an asynchronous reset control input that forces the machine into the ready state. When reset is inactive, the process updates the current state on each rising clock-edge.

In the process representing the combinational logic, we have included assignments for both output signals in all states. An alternative way of writing the process is:

    next_state_and_output : process ( current_state, in1, in2 )
    begin
      out1 <= '0'; out2 <= '0';
      case current_state is
        when ready =>
          if in1 = '1' then
            out2 <= '1';
            next_state <= ack;
          else
            next_state <= ready;
          end if;
        when ack =>
          if in2 = '1' then
            next_state <= ready;
   
          else
            next_state <= err;
          end if;
        when err =>
          out1 <= '1';
          next_state <= err;
      end case;
    end process next_state_and_output;

In this version, we include “default” assignments to the outputs before the case statement. Then, in the case statement, we only assign to each output when the value differs from the default. This is a much more succinct form, especially when there are many outputs. Moreover, it helps us avoid missing an assignment and inadvertently implying level-sensitive storage for an output. For these reasons, we recommend this style.

Modeling Memories

Many designs include memories, and many implementation technologies, such as FPGAs, include memory resources that can be used as RAMs and ROMs. Synthesis tools can infer memory hardware for processes written in certain ways. The IEEE 1076.6 synthesis standard specifies that a RAM be modeled in much the same way as a register, but with the storage represented by an array of bits, vectors, or integers. An address vector, converted to integer, is used to index the array for reading or writing.

A RAM model typically includes declaration of an array type and a signal of that type, for example:

   type mem_array is array (0 to 2**depth - 1) of
                      std_ulogic_vector(width - 1 downto 0);
   signal RAM : mem_array;

The way we write the process modeling the memory determines what kind of RAM inferred.

Example 21.17. Asynchronous RAM

An asynchronous RAM is essentially a level-sensitive storage device. Thus, we can model it in a similar way to a latch. Assuming the type and signal declaration given above, the behavior is modeled as follows:

   asynch_RAM : process (addr, d_in, we) is
   begin
    if we = '1' then
      RAM(to_integer(addr)) <= d_in;
    end if;
   end process asynch_RAM;
   
   d_out <= RAM(to_integer(addr));

The process represents the writing part of the behavior; it updates the RAM signal while the we input is ‘1’. The assignment represents the reading part of the behavior. Note that not all implementation technologies provide asynchronous memories, since they do not interface well with clocked synchronous designs.

Example 21.18. Synchronous RAM with asynchronous read

Most implementation technologies provide RAMs that perform write operations synchronously. They have embedded registers that store the address and data for a write. In some technologies, the read operation is done asynchronously. A model for such a RAM is

   synch_RAM : process (clk) is
   begin
    if rising_edge(clk) then
      if we = '1' then
        RAM(to_integer(addr)) <= d_in;
      end if;
    end if;
   end process synch_RAM;
   d_out <= RAM(to_integer(addr));

Example 21.19. Synchronous RAM with synchronous read

If a RAM has embedded registers for the read control signals, read operations are also performed synchronously. RAMs differ in the data they read when a write is also performed in the same cycle. One form of RAM reads the old content of the memory location before updating it with the new data. We can model this as follows:

   synch_RAM : process (clk) is
   begin
    if rising_edge(clk) then
      d_out <= RAM(to_integer(addr));
      if we = '1' then
        RAM(to_integer(addr)) <= d_in;
      end if;
    end if;
   end process synch_RAM;

We could interchange the assignment to d_out with the inner if statement without affecting the behavior, since the assignments do not affect the RAM content until after the process suspends. A synthesis tool would infer the same behavior with the statements in either order.

Another form provides the newly written data, modeled as follows:

   synch_RAM : process (clk) is
   begin
    if rising_edge(clk) then
      if we = '1' then
        RAM(to_integer(addr)) <= d_in;  d_out <= d_in;
      else
        d_out <= RAM(to_integer(addr));
      end if;
    end if;
   end process synch_RAM;

In both cases, we can add an enable input controlling reading and writing at a new address. For example, the first version above, augmented with an enable input, is

   synch_RAM : process (clk) is
   begin
    if rising_edge(clk) then
      if en = '1' then
        d_out <= RAM(to_integer(addr));
        if we = '1' then
          RAM(to_integer(addr)) <= d_in;
        end if;
      end if;
    end if;
   end process synch_RAM;

Example 21.20. Pipelined synchronous RAM

The RAMs with synchronous read in the preceding examples start a read access on a clock-edge and provide the data after a read-access delay. In some designs, the data may arrive too late in a clock cycle to be used for further computation. We can add a storage register on the RAM output so that the data can be used in the subsequent clock cycle. This amounts to pipelining the RAM access. We can combine the RAM and pipeline registers into a single process representing a pipelined RAM:

   pipelined_RAM : process (clk) is
    variable pipelined_en : std_ulogic;
    variable pipelined_d_out :
               std_ulogic_vector(width - 1 downto 0);
   begin
    if rising_edge(clk) then
      if pipelined_en = '1' then
        d_out <= pipelined_d_out;
      end if;
      pipelined_en := en;
   
      if en = '1' then
        pipelined_d_out := RAM(to_integer(addr));
        if we = '1' then
          RAM(to_integer(addr)) <= d_in;
        end if;
      end if;
    end if;
   end process pipelined_RAM;

If we are synthesizing to an implementation technology, such as an FPGA, in which a RAM can be loaded with initial contents on system reset, we may be able to specify the initial content as part of the synthesizable model. Some tools allow us to specify initial contents in an initial value aggregate in the signal declaration, for example:

   signal RAM : RAM_array := (X"0020", X"FC01", X"101E", X"C000",
                             ...
                             others => X"0000");

Some tools also allow us to write a function that loads data from a file and returns an array of values to assign as the initial value for the signal. We took this approach in the case study in Chapter 17. The IEEE 1076.6 synthesis standard, however, does not specify either of these approaches. We would need to consult our tool vendor’s documentation to see how a memory can be initialized, and we should recognize that the approach used may not be portable among different tools.

We can model ROMs using similar techniques to those used for RAMs, but omitting the code representing the write operations. Since ROM content does not change, we can use a constant instead of a signal to model the storage. We specify the ROM content in the form of an array aggregate, for example:

   constant ROM : mem_array := (X"0020", X"FC01", X"101E", X"C000",
                               ...
                               others => X"0000");

Reading the ROM asynchronously can then be modeled using a concurrent assignment:

   d_out <= ROM(to_integer(addr));

For a small ROM, a synthesis tool could optimize this as combinational logic.

Another way to model a small ROM is using a case statement. We use the address as the selector expression and assign different literal values to the output signal based on the address.

Example 21.21. A small ROM modeled using a case statement

We can model a lookup ROM giving 7-segment display codes for BCD digits as follows:

   
   decoder : process ( bcd ) is
   begin
    case bcd is
      when X"0" =>   seg <= "0111111";
      when X"1" =>   seg <= "0000110";
      when X"2" =>   seg <= "1011011";
      when X"3" =>   seg <= "1001111";
      when X"4" =>   seg <= "1100110";
      when X"5" =>   seg <= "1101101";
      when X"6" =>   seg <= "1111101";
      when X"7" =>   seg <= "0000111";
      when X"8" =>   seg <= "1111111";
      when X"9" =>   seg <= "1101111";
      when others => seg <= "1000000";
    end case;
   end process decoder;

Alternatively, we could use a selected assignment:

with bcd select

    seg <= "0111111" when X"0", "0000110" when X"1",
           "1011011" when X"2", "1001111" when X"3",
           "1100110" when X"4", "1101101" when X"5",
           "1111101" when X"6", "0000111" when X"7",
           "1111111" when X"8", "1101111" when X"9",
           "1000000" when others

If we want to use a block RAM resource as a ROM, we can use the same form of process as for a RAM, but omit the statements that update the array. For example, we can adapt the first process in Example 21.19 to model a ROM as follows:

   block_ROM : process (clk) is
   begin
    if rising_edge(clk) then
      d_out <= ROM(to_integer(addr));
    end if;
   end process block_ROM;

Synthesis Attributes

When we use a synthesis tool to infer a hardware implementation for a design, we can direct it to optimize either the speed or area of the generated circuit. In some applications, that general directive may be sufficient, resulting in an implementation that meets our constraints. Often, however, we need to take finer control of the synthesis process. One way in which we can do so is by including attribute specifications (see Chapter 20) attribute specifications in our models to direct a synthesis tool to infer hardware in particular ways.

Different synthesis tools support different attributes to specify different aspects of hardware inference and different aspects of target technologies. This is possibly an aspect in which tools most widely diverge, since the attributes a given tool supports reflect the particular capabilities and synthesis algorithms implemented by the tool. We need to refer to a tool’s documentation to discover what attributes are supported and how to use them.

In an effort to create at least a small amount of harmony, the IEEE 1076.6 synthesis standard defines a minimal set of synthesis attributes. We describe them here, as they are indicative of the kinds of attributes supported by tools. The standard specifies a package of attribute declaration to be analyzed into the ieee library. While we could declare the attributes ourselves in each design, using the standard package is more convenient. Synthesis tools usually include similar packages for their implementation-defined attributes. The standard package is

   package RTL_ATTRIBUTES is
    attribute KEEP : boolean;
    attribute CREATE_HIERARCHY : boolean;
    attribute DISSOLVE_HIERARCHY : boolean;
    attribute SYNC_SET_RESET : boolean;
    attribute ASYNC_SET_RESET : boolean;
    attribute ONE_HOT : boolean;
    attribute ONE_COLD : boolean;
    attribute FSM_STATE : string;
    attribute FSM_COMPLETE : boolean;
    attribute BUFFERED : string;
    attribute INFER_MUX : boolean;
    attribute IMPLEMENTATION : string;
    attribute RETURN_PORT_NAME : string;
    attribute ENUM_ENCODING : string;
    attribute ROM_BLOCK : string;
    attribute RAM_BLOCK : string;
    attribute LOGIC_BLOCK : string;
    attribute GATED_CLOCK : boolean;
    attribute COMBINATIONAL : boolean;
   end package RTL_ATTRIBUTES;

If we need to use any of these attributes, we can include a use clause in our model to make them directly visible. Several of these attributes are boolean. Decorating an item with the value true for one of these attributes directs a tool to synthesize in a particular way. Decorating with the value false is the same as not decorating the item. Attributes of type string allow us to specify further information for use by the synthesis tool. The meaning of each attribute is described below.

  • KEEP : boolean

    Decorates: entity, component declaration, component instantiation, signal, variable

    This attribute directs the tool to preserve the hardware represented by the decorated item in the inferred hardware. The item should not be deleted or replicated during optimization of the design. We can use this attribute for parts of a design that we have previously synthesized and are re-using.

  • CREATE_HIERARCHY : boolean

    Decorates: entity, block, subprogram, process

    This attribute directs the tool to maintain the decorated item as a distinct hierarchical construct in the inferred hardware. It should not be subsumed into an enclosing construct during optimization.

  • DISSOLVE_HIERARCHY : boolean

    Decorates: entity, component declaration, component instantiation

    This attribute directs the tool to merge the construct into the hardware in which the item is instantiated. The tool can then globally optimize the construct in the context of its instantiation.

  • SYNC_SET_RESET : boolean

    Decorates: signal, process, block, entity

    This attribute is used to identify edge-sensitive storage devices that have separate synchronous set/reset inputs in the target technology. Using those inputs is more efficient than multiplexing the data inputs. We use the attribute to decorate a set/reset signal connected to storage devices or the construct that represents a storage device. For example, in the following:

       attribute SYNC_SET_RESET of reset : signal is true;
       ...
       reg : process ( clk ) is
       begin
        if rising_edge(clk) then
          if reset = '1' then
            q <= (others => '0'),
          else
            q <= d;
          end if;
        end if;
       end process reg;

    the tool infers hardware with reset connected to the separate synchronous reset input of the register. Without the attribute, the tool could infer a register whose input comes from a multiplexer with reset as the select input, a zero vector as one data input, and d as the other data input.

  • ASYNC_SET_RESET : boolean

    Decorates: signal, process, block, entity

    This attribute is used to identify level-sensitive storage devices that have separate asynchronous set/reset inputs in the target technology. It is used in a similar way to SYNC_SET_RESET.

  • ONE_HOT : boolean

    Decorates: signal

    This attribute specifies that, in a collection of signals, each of which is decorated with true for this attribute, at most one scalar value is ‘1’ at any time. The directive allows a synthesis tool to avoid inferring priority logic based on the signals in the collection. For example, if we write

       ff : process ( clk, reset, set ) is
       begin
        if reset = '1' then
          q <= '0';
        elsif set = '1' then
          q <= '1';
        elsif rising_edge(clk) then
          q <= d;
        end if;
       end process ff;

    we are specifying that reset has priority over set. If the target technology includes flipflops with set and reset, but requires that only one be active at a time, the tool would infer a connection to the set input driven with set and not reset. If we add the following attribute specification:

       attribute ONE_HOT of set, reset : signal is true;

    we are telling the tool that only one of set and reset is active at a time, so it can make direct connections to the set and reset input of the flipflop. Of course, we must ensure separately that our statement is valid. We might include an assertion to that effect in the model.

  • ONE_COLD : boolean

    Decorates: signal

    This attribute specifies that, in a collection of signals, each of which is decorated with true for this attribute, at most one scalar value is ‘0’ at any time. It is used in a similar way to ONE_HOT.

  • FSM_STATE : string

    Decorates: type, subtype, signal, variable

    This attribute directs the tool to encode the state vector of a finite-state machine in a specified way. The allowed attribute values are:

    • “BINARY”: unsigned binary encoding with the minimal number of bits

    • “GRAY”: Gray coding, in which exactly one bit value changes on each tansition

    • “ONE_HOT”: an encoding in which each code value has exactly one ‘1’ bit

    • “ONE_COLD”: an encoding in which each code value has exactly one ‘0’ bit

    • “AUTO”or empty string: the tool selects an encoding

    • A string of the same form as the ENUM_ENCODING attribute, described on page 664

    If both the FSM_STATE and ENUM_ENCODING attributes are specified for a given state machine, the FSM_STATE attribute takes precedence.

  • FSM_COMPLETE : boolean

    Decorates: type, subtype, signal, variable

    This attribute directs the tool to include default transitions in the finite-state machine that uses the decorated item for its state vector. We typically describe the default transitions with an others clause in a case statement, dealing with state encodings not explicitly described. For example, given the following declarations and attribute specifications:

       type state_type is (idle, state1, state2);
       signal state, next_state : state_type;
       attribute FSM_STATE of state, next_state : signal is
                  "ONE_HOT";
       attribute FSM_COMPLETE of state, next_state : signal is
                  true;
       ...

    we can write a process for the state machine’s combinational logic as follows:

       fsm_logic : process (state, ...) is
       begin
        case state is
          when idle   =>  next_state <= ...;
          when state1 =>  next_state <= ...;
          when state2 =>  next_state <= ...;
          when others =>  next_state <= idle;
        end case;
       end process fsm_logic;

    According to the rules of VHDL, all alternatives for the state value are covered by the first three choices in the case statement. However, there are several synthesized encoding values that are not covered, since one-hot encoding is specified. Given the FSM_COMPLETE attribute, the synthesis tool includes additional transitions based on the others clause, as this would be the alternative selected for an illegal state value.

  • BUFFERED : string

    Decorates: signal

    This attribute directs the tool to use a particular library cell to buffer the driver for the decorated signal. The attribute value specifies the name of the library cell to use, for example:

       attribute BUFFERED of clk : signal is "BUFG";

    Alternatively, the attribute value can be one of the following special strings:

    • “HIGH_DRIVE“: use a high-drive buffer from the synthesis library

    • “CLOCK_BUF“: use a clock buffer from the synthesis library

    • “RESET_BUF“: use a reset buffer from the synthesis library

  • INFER_MUX : boolean

    Decorates: case statement label, selected assignment statement label

    This attribute directs the tool to infer a multiplexer implementation for the statement instead of random logic or ROM implementation.

  • IMPLEMENTATION : string

    Decorates: procedure, function, signal or variable assignment label

    This attribute directs the tool to use a specified synthesis library cell to implement calls to a subprogram or an assigment to a signal or variable, rather than inferring hardware from the subprogram body or assignment expression. The attribute value specifies the library cell name. We use this attribute when we have a VHDL implementation that we wish to simulate, but want to synthesize using the library cell. For example:

       procedure multiplier
                  ( signal clk, start : in std_ulogic;
                    signal a, b : in unsigned(15 downto 0);
                    signal done : out std_ulogic;
                    signal p : out unsigned(31 downto 0) is
       begin
        ...
       end procedure multiplier;
       attribute IMPLEMENTATION of multiplier : procedure is
                 "MULTSEQ_16X16;
       ...
       multiplier(clk, start, data1, data2, done, product);

    When we simulate this model, the body of the procedure is called. When we synthesize, the procedure body is ignored, and the MULTSEQ_16X16 library cell is included in the inferred hardware.

  • RETURN_PORT_NAME : string

    Decorates: function

    This attribute is used in conjunction with the IMPLEMENTATION attribute for a function. The RETURN_PORT_NAME attribute value specifies the name of the port on the library cell that corresponds to the return value of the function. For example:

       function and_or_invert (a, b, c, d : in std_ulogic)
                             return std_ulogic is
       begin
        return not ( (a and b) or (c and d) );
       end function and_or_invert;
       attribute IMPLEMENTATION of and_or_invert : function is
                  "AOI";
       attribute RETURN_PORT_NAME of and_or_invert : function is
                  "O";
       ...
       q <= and_or_invert(w, x, y, z);

    The IMPLEMENTATION attribute specifies that the AOI library cell be used for the function. The RETURN_PORT_NAME attribute specifies that the O port of that cell is the output port.

  • ENUM_ENCODING : string

    Decorates: type, subtype

    This attribute directs the tool to use a specified binary encoding for values of the enumeration type or subtype decorated by the attribute. The value of the attribute is a sequence of bit-vector literals, representing the encoding for the enumeration values in the type. For example, given an enumeration type for states in a finite-state machine:

       type state is (idle, preamble, data, crc, ok, error);

    we can define the state encoding as follows:

       attribute enum_encoding of state : type is
                  "000 001 010 011 100 111";

    The bit vectors correspond in order to the enumeration values in the type definition. All of the literals must contain the same number of bits, and underscore characters may be included to enhance readability. Note that we should ensure that the enumeration values are listed in ascending order of their encodings. Otherwise synthesized relational operations may not produce the same results as relational operators evaluated during simulation.

  • ROM_BLOCK : string

    Decorates: constant, variable, signal

    This attribute directs the tool to implement the decorated item as a ROM. The attribute value specifies the library cell to use for the ROM. For example:

       constant ROM : mem_array := (X"0020", X"FC01",
                                   ...
                                   others => X"0000");
       attribute ROM_BLOCK of ROM : constant is "ROM_SYNCH";
  • RAM_BLOCK : string

    Decorates: variable, signal

    This attribute directs the tool to implement the decorated item as a RAM. The attribute value specifies the library cell to use for the RAM. For example:

       signal RAM : mem_array;
       attribute RAM_BLOCK of RAM : signal is "RAM_PIPELINED";
  • LOGIC_BLOCK : string

    Decorates: constant, variable, signal

    This attribute directs the tool to implement the decorated item as a combinational logic block (instead of a ROM) or discrete sequential logic (for example, using flipflops and registers, instead of a RAM).

  • GATED_CLOCK : boolean

    Decorates: signal, process

    This attribute directs the tool to use clock gating rather than separate enable inputs for edge-triggered storage devices. Use of clock gating can reduce power consumption considerably. However, we must ensure that the enable signal used to gate the clock is glitch free and has appropriate timing.

    We can use this attribute to decorate a gated clock signal, in which case all storage devices connected to the clock use clock gating. For example:

       attribute GATED_CLOCK of gclk : signal is true;
       ...
       gclk <= clk and enable;
       reg : process (gclk) is
       begin
        if rising_edge(gclk) then
          q <= d;
        end if;
       end process;

    Alternatively, we can use the attribute to decorate specific processes that are to use clock gating. For example:

       attribute GATED_CLOCK of reg : label is true;
       reg : process(clk) is
       begin
        if rising_edge(clk) then
       
          if enable = '1' then
            q <= d;
          end if;
        end if;
       end process;

    Without gated clocking, the register inferred for this process would be active on each clock-edge. The internal logic transitions would involve dynamic power consumption. By decorating the process with the attribute, we direct the tool to use clock gating instead of using the enable signal to select the input to be stored. The register is only active on those clock-edges where enable is ‘1’.

  • COMBINATIONAL : boolean

    Decorates: process, signal assignment label

    This attribute directs the tool to infer purely combinational logic for the decorated item. It is an error if the decorated item represents sequential logic. We can use this attribute to force the synthesis tool to issue an error if we inadvertently imply level sensitive storage by omitting an assignment to an object on some path through a process.

Metacomments

Normally, comments in a model are not interpreted by a tool. We include comments as documentation for the human reader. However, the synthesis standard defines two metacomments, that is, comments that are to be interpreted by a synthesis tool. They are

   -- rtl_synthesis off

and

   -- rtl_synthesis on

The metacomments are not case sensitive; they can be in lowercase, uppercase or a combination of the two. Any VHDL code following an rtl_synthesis off metacomment and before a subsequent rtl_synthesis on metacomment is ignored by the synthesis tool. It is as though the code were also comments. Thus the model, excluding the ignored parts, must still be a valid VHDL model. Other tools, such as simulators, do not ignore the code. We would normally use the metacomments to exclude from synthesis parts of the model that are only intended for simulation. Examples are processes that check timing or that use file input/output for instrumentation.

Example 21.22. Omitting monitoring code from synthesis

Suppose we include code in an architecture body to monitor operation of the design during simulation. We can exclude the code from analysis by the synthesis tool by surrounding it in metacomments, as follows:

   
   architecture rtl of subsystem is
     ...
   begin
    ... -- synthesizable processes and assignments
    -- rtl_synthesis off
    monitor : process is
      use std.textio.all;
      file monitor_file : text open write_mode is "monitor.txt";
      variable L : line;
      ...
    begin
      ...
    end process monitor;
    -- rtl_synthesis on
   end architecture rtl;

Use of files and file operations is not allowed in synthesizable code, so the synthesis tool would produce errors if it tried to interpret the monitor process.

We must take care when excluding part of a model from synthesis to ensure that we don’t inadvertently omit part of the model that should be synthesized to hardware. Otherwise we can get a mismatch between simulation results and operation of the synthesized hardware.

Exercises

1.

[ Exercises 21.2] Which of the following types does the IEEE 1076.6 standard allow as the type of a variable?

   type temp is range -60 to 150;
   type temp_vec is array (natural range <>) of temp;
   type location is (inside, outside, buried);
   type local_temp_vec is array (location) of temp;
   type location_vec is array (natural range <>) of location;
   type word_vec is array (natural range <>) of
           std_ulogic_vector(31 downto 0);

2.

[ Exercises 21.3] What hardware, if any, would be inferred for the following statement?

   if sel = '0' or sel = 'L' then
    z <= in0;
   elsif sel = '1' or sel = 'H' then
    z <= in1;
   elsif sel = 'U' then
    z <= 'U';
   
   else
    z <= 'X';
   end if;

3.

[ Exercises 21.4] What hardware would be inferred for the following assignment?

   with fn select
    z <= a + b       when "00",
         a - b       when "01",
         a and b     when "10",
         a and not b when "11";

4.

[ Exercises 21.4] Write a synthesizable process that represents a logic block with tristate outputs. If enable_n is ‘0’, an 8-bit output dat_o is driven with the value of either reg1 (if adr is ‘0’) or reg2 (if adr is ‘1’), and a single-bit output ack_o is driven with ‘1’. If enable_n is ‘1’, both outputs are ‘Z’.

5.

[ Exercises 21.4/ 21.5] Why would a synthesis tool infer storage for the following process?

   mux_adder : process ( sel, x, y, z, carry_in )
     variable operand : unsigned(15 downto 0);
   begin
    case sel is
      when "00" => operand := x;
      when "01" => operand := y;
      when "10" => operand := z;
      when others =>
        report "Illegal value for sel." severity error;
    end case;
    sum <= operand + carry_in;
   end process mux_adder;

6.

[ Exercises 21.5] Write a process representing an edge-triggered register with no reset but with synchronous enable.

7.

[ Exercises 21.6] Write declarations for the storage of an 8K ∞ 16-bit RAM storing values of type signed, with all locations initialized to zero.

8.

[ Exercises 21.6] Rewrite the 7-segment decoder of Example 21.21 using a constant to represent the ROM storage.

9.

[ Exercises 21.7] Write an attribute specification for inclusion in the state machine model of Example 21.16 specifying an encoding of “00” for ready, “01” for ack, and “11” for err.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.144.139