Best practice, design and code

From Robin

Jump to: navigation, search


The rules noted here are guidelines that will help creating designs that are easy to verify, read and maintain

RTL code

Sequential logic

Separate registers from combinational logic

  • By separating registers from combinational logic you will reduce the risk of creating more registers than you planned to do. By keeping combinational logic in processes that are solely combinational, you will get the full benefit of warnings from the toolchain if you accidentally create latches or registers within the code.

Register reset should be stated where the registers are assigned

  • Reset is for clearing registers to have them in a predictable state. Although synchronous reset can be stated as combinational logic, the use of reset is setting registers, not interfering with the combinational code function. This may seem as a slight contradiction to the bullet above, but for the sake of modifiability and general readability, it makes sense to keep reset.

Sequential logic example:

REGISTERS: process(clk) is 
  begin 
    if rising_edge(clk) then 
      if reset then 
        r_signal <= '0';
        r_vector <= (others => '0');
      else
        r_signal <= next_signal;
        r_vector <= next_vector;
      end if;
    end if;
  end process;
  
COMBINATIONAL_LOGIC:
next_signal <= valid_a and valid_b;
next_vector <= std_logic_vector("0" & unsigned(a) + "0" & unsigned(b));

Variables and Signals

  • When use variables vs. signals?
  • Never use/set input and output signals directly

Processes

Unless necessary (eg. need to use if or case statements), don't use a process! They can clutter the code, and make it less clear as to what you want to do. This also ties in with the use of the appropriate conditional statement for a given task.

Conditional statements

In VHDL there are many ways of describing the wanted conditional behavior of your design. To help with deciding which syntax is best, look at this table overview and summary:

Conditional statement overview in VHDL
Statement Targets Conditions Process Prioritizes
if Multiple Multiple Required Yes
case Multiple Single Requires No
when ... else Single Multiple Optional Yes
with ... select Single Single Optional No
  • Target: How many signals/variables can be set if true?
  • Condition: Can the true condition be a statement?


  • When in doubt...
    • Try 'with...select'. This will force you to make visible choices.
  • Only use 'if'...
    • When you need to prioritize conditions...
    • and have multiple targets.
    • Typically used for clocked processes.
  • It is fine to use 'when...select' or 'when...else' inside if and case
    • Do you need if inside if?
    • Or case inside case?
    • Readability suffers when nesting several levels of if or case.

Whatever you choose, keep the following in mind:

  • define:
    • all output for
    • all conditions


Latches vs. Flip Flops

Often the issue with using the incorrect conditional statement for the task is the inferring of latches in the design. To understand why this is a problem, we must understand what a latch is, and how this can create a misbehaving and unpredictable design.

A latch is a digital component similar to a flip-flop in that it is used to store values. They differ from each other in when they can be read to or are "transparent". A latch is "open" during the positive duty cycle of the clock, while a flip-flop is edge-triggered. This means that if an input value changes during the positive duty cycle of the clock a flip-flop won't read this value, while a latch will. This may result in a functionally incorrect design if the wrong memory element is used.

The simulation tool infers a memory element when we tell it is told (explicitly or implicitly) to hold on to its old value. What differs in which memory element is chosen is when we set the new value. A latch is created when we check a signal and set another signal accordingly:

value <= '1' when enable else value;

In this example, we explicitly tell the signal to hold on to its old value. This is necessary, however: the simulation tool assumes the old value if nothing else is stated. This code gives the same result:

value <= '1' when enable; -- implicit else

When making a flip-flop, we check the rising (or falling) edge of a signal, instead of its active state:

value <= '1' when rising_edge(clk);

The issue arises when dealing with combinational logic, which shouldn't have any memory elements. It is very easy to infer a latch, especially in badly written and messy code. This results in incorrect and unpredictable behavior. All you need to forget is to not define all outputs for all inputs. In a simple design, this isn't that hard, but in more complex ones, it is very easy to forget.

To read more about the inferring of different memory elements in VHDL, click here.

if

  • Must be used in a process
  • Multiple conditions
  • Multiple targets
  • Prioritizes - The order of clauses matters!


ISPRIME: process(input) is
   begin
       if input = x"1" | input = x"2" | input = x"3" | input = x"5" | input = x"7" | input == x"b" | input == x"d" then
           isprime <= '1';
       else
           isprime <= '0';
       end if;
   end process;
  • First option has priority ("breaks" out of the if statement when the first true is found).
  • Can be used to infer both latches and flip-flops
    • A latch is infeered when a target is not sufficiently specified.
  • Can be nested using 'elsif'
    • Can replace any other conditional statement
      • Not recommended!
    • Avoid deep nesting
    • ~4 levels should be the maximum...


To illustrate some of the points given above, look at this deceptively simple process:

process(all) is
   begin
       if inp1 then
           if inp2 then
               a <= '1';
               b <= '1';
           else
               a <= '1';
               b <= '0';
           end if;
       else
           a <= '0';
       end if;
   end process;

Although this process may at first glance seem fine, it infers some latching behavior. From analyzing the process we get this state table:

inp1 inp2 a b
1 1 1 1
1 0 1 0
0 1 0 Latched
0 0 0 Latched

From the table, we can see that when "inp1" is 0, "b" is a latched signal. Reviewing the code with this information we sure enough see an assignment missing for "b" when "inp1" is zero. Furthermore, we also see that "a" only depends on "inp1", while "b" is dependent on both, making us able to simplify the code. Although this code simplification might seem to be a digression, it makes identifying the issue much easier:

process(all) is
   begin
       if inp1 then
           a <= '1';
           b <= inp2;
       else
           a <= '0';
           -- b ass. missing
       end if;
   end process;

From this simplified code we can much easier see that "b" is missing an assignment whereas "a" is fully covered. In addition to showing the importance of always specifying all output for all conditions of inputs it also shows the value of lowering the depth of if statements when possible.


Are default values recommended to be used with if statements, to avoid latches? If used for b in the last example, latching would be avoided.


case

  • Must be in a process
  • Single input vector
  • Multiple targets
  • Every alternative has the same priority
ISPRIME: process(input) is
   begin
       case input is
           when x"1" | x"2" | x"3" | x"5" | x"7" |x"b" | x"d" =>
               isprime <= '1';
           when others => isprime <= '0';
       end case;
   end process;
  • Every option for input must be declared
    • "when others" can be used...
    • but might also infer latches when not all outputs are defined for all inputs!
  • There exists a matching case statement "case?"
    • Allows for don't cares.


The typical use for a case statement is in state machines because you want to change behavior depending on the current state. Case statements are excellent when you want to set several output vectors depending on a single state vector.

As stated earlier, case statements can create latches, if not all outputs are specified in each input option. To remedy this, default values can be used. null should only be used in CL when using default values for all outputs:

process(input) is
   begin
       -- Default values
       isprime <= '0';
       isfour <= '0';
       case input is
           when x"1" | x"2" | x"3" | x"5" | x"7" | x"b" | x"d" =>
               isprime <= '1';
               -- No default = isfour latch inferred
           when x"4" =>
               -- No default = isprime latch inferred
               isfour <= '1';
           when others =>
               null
               -- No default = isfour and isprime latch inferred
       end case;
   end process;


when...else

  • Can be used concurrently (outside processes).
  • Multiple conditions
  • Single target
  • prioritizes
isprime <=
   '1' when input = x"1" else
   '1' when input = x"2" else
   '1' when input = x"3" else
   '1' when input = x"5" else
   '1' when input = x"7" else
   '1' when input = x"b" else
   '1' when input = x"d" else
   '0'; 
  • Can replace if statements for a single target
  • Compact - suitable when complexity is low.
  • Can infer FF's or latches
    • When not specifying "else" a latch is created.
q <= '0' when reset else 'd' when rising_edge(clk);    -- FF inferred because of lack of else and the use of rising_edge(clk)
a <= b when en;                                        -- Latch inferred because of the lack of else.


when...select

  • Can be used concurrently (outside processes).
  • Single input vector
  • Single target
    • Must have all input cases defined
with input select isprime <=
   '1' when x"1" | x"2" | x"3" | x"5" | x"7" | x"b" | x"d",
   '0' when others;
  • Can also infer latches
    • Least likely among the other conditional statements
  • Compact and readable


FSM (finite state machine)

General tips:

  • Make your own states as "enumerated" type.
    • Simplifies reading a lot.
  • Use three processes/statements (three-process fsm)
    1. One for assigning the state and updating registers = infering FFs.
      • Based on the clock (and reset when asynchronous reset)
    2. One for deciding the nest state (next_state_CL).
      • based on previous state and inputs
    3. One for setting output (output_CL)
      • based on present state (and inputs if Mealy type)
    • Sometimes points 2 and 3 can be combined
      • In simple cases where there are few output signals and state selection has very little decoding
      • Called a two-process fsm
      • Generally we do not recommend joining state- and output decoding
  • While points 2 and 3 state the same criteria (state tree), it makes reading the state machine behavior easier. In contrast, having next_state logic mixed with state output, debugging one will include debugging the other. When reading code, it does not take much logic before the smokescreen effect is notable.

Example following these principles:

 Note on ASM diagram: 
 all box entry should be from above
 "<-" for register assignment and 
 "<=" for assignment valid in this state only
 "next_signal" indicates a register input

File:Best_practices_fsm_demo.svg

entity fsm_demo is
   port(
       clk, reset, a: in std_logic;
       b: out std_logic
   );
end entity;

architecture rtl of fsm_demo is
   type states is (S_RESET, S_1, S_2);
   signal curr_state, next_state: states;
   signal count, next_count: unsigned(1 downto 0);

begin

   -- 1: Sequential state assignments and register resets
   REGISTERS: process(clk) is
       begin
           if rising_edge(clk) then
               if reset then
                   curr_state <= S_RESET;
                   count <= (others => '0'); 
               else
                   curr_state <= next_state;
                   count <= next_count;
               end if;
           end if;
       end process;

   -- 2: Combinatorial next_state logic
   next_state_CL: process(curr_state, next_count, a) is 
       begin
           -- default values
           next_state <= curr_state;
           case curr_state is
               when S_RESET =>
                   next_state <=   S_1;
               when S_1 =>
                   next_state <=   S_2 when (next_count = "11" and a = 1);
               when S_2 =>
                   next_state <=   S_RESET when (next_count = "11" and a = 0);
           end case;
   end process;

   -- 3: Combinatorial output decoded from state 
   output_CL: process( all ) is
       begin
           -- default values
           b <= '0';
           next_count <= count + 1;
           case curr_state is
               when S_RESET =>
                   next_count <= (others => '0'); 
               when S_1 =>
                   null;  -- use default values only 
               when S_2 =>
                   b <= '1' when (next_count = "11" and a = '0');
           end case;
   end process;

end architecture;

Pipeline

Naming conventions

When pipelining a design with N signals, we add N wires for each pipelining step. Without a proper naming scheme for all of the signals, the pipelining can become unreadable and error-prone. No matter which type of naming scheme you go for always be consistent!

Here is one naming scheme:

  • Depending on where a signal or variable is in the pipeline, add a _pn suffix, where n is the stage nr (0 is the first step)


Code structure

  • Limit the use of variables' if you are unsure of their functionality.
  • Overflow and underflow - Remember the effect of adding, subtracting, and multiplying in terms of overflow and underflow!
    • Padd accordingly at each pipelining step
  • Synchronize outputs
    • Do the outputs need to be synchronized?
Personal tools
Front page