Best practice, design and code
From Robin
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
---
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 behaviour of your design. To help with deciding which syntax is best, look at this table overview and summary:
Statement | Targets | Conditions | Process |
---|---|---|---|
if | Multiple | Multiple | Required |
case | Multiple | Single | Requires |
when ... else | Single | Multiple | Optional |
with ... select | Single | Single | Optional |
- 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
Latching
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 but differs from a flip-flop in when it can be read to. A latch is "open" during the positive duty cycle of the clock, while a flip-flop is edge-triggered. This means that if a value changes during the positive duty cycle (maybe to prepare for the next clock cycle). A flip-flop won't read this value, but a latch will, which may result in an incorrect design.
In VHDL, a latch is inferred when not all conditions is specified for a signal.
if
- Must be used in a process
- Multiple conditions
- Multiple targets
- Prioritizes - 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...
- Can replace any other conditional statement
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
This example will base itself on this diagram: