Variables used as registers in VHDL

One question that I’ve debated many times over the years is whether it’s OK to use variables for registers in VHDL. It’s safe to say that newbies are more likely to do it than experienced VHDL designers. But is there any merit to that, or is it just a matter of preference?

In this blog post, I will try to shed some light on the issue so that you can make an informed decision about using this design practice.

First of all, let me explain what I mean by using a variable as a register.

If you read a variable in a VHDL process before you write to it, the synthesis tool will have to implement it using physical storage. That’s because its value has to be stored somewhere until the next time the process wakes up. In FPGAs, that means either registers (flip-flops) or memory (block RAM).

The code below shows an example process where my_var is not a register. The logic doesn’t rely on any previous value of the variable. We give it a default value of ‘0’ as soon as we enter the process.

process(clk)
  variable my_var : std_logic;
begin
  if rising_edge(clk) then
    
    my_var := '0';

    if some_condition then
      my_var := some_value;
    end if;

    my_signal <= my_var;

  end if;
end process;

If we comment out the default assignment to my_var, as shown in the example below, it becomes a register. That’s because if some_condition is false, my_signal gets whatever value my_var had the last time the process completed. You are telling the synthesis tool to remember the value of my_var over time, and the only way to do that is by using a register.

process(clk)
  variable my_var : std_logic;
begin
  if rising_edge(clk) then
    
    -- my_var := '0';

    if some_condition then
      my_var := some_value;
    end if;

    my_signal <= my_var;

  end if;
end process;

It’s perfectly legal to use variables like that in VHDL. Also, the FPGA tools won’t have any trouble implementing it most of the time. Still, it’s a design practice that’s frowned upon by many FPGA engineers. Some companies even prohibit such use of variables through their coding standards. Let’s have a look at an example and examine the pros and cons of using variables over signals.

Example using a variable to infer block RAM

In this example, I’ve created a VHDL process for inferring a dual-port RAM. The width and depth match a configuration of the Xilinx RAMB36E1 primitive, as shown on page 30 of the 7 Series FPGAs Memory Resources user guide. The code below shows the VHDL process. We store the values in the ram_v object, which is a regular variable.

DUAL_PORT_RAM : process(clk)
  type ram_type is array (0 to 2**10 - 1) of std_logic_vector(35 downto 0);
  variable ram_v : ram_type;
begin
  if rising_edge(clk) then

    data_out <= ram_v(addr_out);
    ram_v(addr_in) := data_in;
    
  end if;
end process;

When we synthesize the code in Xilinx Vivado, we see that it has indeed implemented ram_v in block RAM. Below is an excerpt from the synthesis log, showing the variable’s name mapped to a RAMB36 primitive.

Block RAM: Preliminary Mapping	Report (see note below)
+------------+------------+------------------------+---+---+------------------------+---+---+------------------+--------+--------+
|Module Name | RTL Object | PORT A (Depth x Width) | W | R | PORT B (Depth x Width) | W | R | Ports driving FF | RAMB18 | RAMB36 | 
+------------+------------+------------------------+---+---+------------------------+---+---+------------------+--------+--------+
|bram        | ram_v_reg  | 1 K x 36(READ_FIRST)   | W |   | 1 K x 36(WRITE_FIRST)  |   | R | Port A and B     | 0      | 1      | 
+------------+------------+------------------------+---+---+------------------------+---+---+------------------+--------+--------+

Variables used for limiting the scope

A possible advantage of using a variable is that its scope is limited to within the process. Along with the variable, we can also place the type declaration of the array inside of the process. Limiting the scope of data objects is generally considered to be a good coding practice. Keeping all the constructs that “belong” to the process within it helps to refine the process as a separate design unit.

There are ways of creating limited scopes for signals as well, for example, by using the VHDL block statement. But as you can see from the example below, it adds more code lines and another indentation level to your VHDL file. I have to give a small victory to variables when it comes to encapsulation.

RAM_BLOCK : block

  type ram_type is array (0 to 2**10 - 1) of std_logic_vector(35 downto 0);
  signal ram : ram_type;

begin

  DUAL_PORT_RAM : process(clk)
  begin
    if rising_edge(clk) then

      data_out <= ram(addr_out);
      ram(addr_in) <= data_in;
      
    end if;
  end process;

end block RAM_BLOCK;

Line ordering matters with variables

A thing to be aware of when using variables is that the ordering of the lines matter. If we swap lines 7 and 8 in the original DUAL_PORT_RAM process, it’s broken. That’s not the case if we swap lines 12 and 13 in the code above. With signals, only which enclosure they are within matters, while with variables, the correct ordering of code lines is crucial.

Refer to my earlier blog post to understand the general difference between signals and variables:
How a signal is different from a variable in VHDL

That’s one of the main objections many VHDL designers have against variables used for registers. Some engineers are accustomed to reading the code within an enclosure, like an if-statement, as parallel events. By using both variables and signals interchangeably, it becomes harder to follow the program flow. The code becomes less readable because your mind has to comprehend two constructs with different sets of rules, describing the same thing.

Of course, the arguments about readability is a subjective one. Furthermore, if you expect only signals to describe registers, you may be more inclined to overlook a variable that does the same. But if you use variables for that regularly, it may not be that big an issue for you.

Viewing variables in the ModelSim waveform

When using variables for data storage, you may want to track the value over time in the simulator. Most simulators treat variables different from signals in many ways. In ModelSim, they are not immediately accessible from the Objects window. It’s something to be aware of, but it’s a minor problem.

The video above shows how you can add a variable to the waveform in ModelSim. Bring up the Locals window by choosing View→Locals from the main menu. Select the process that contains the variable, and it will appear under Locals. Right-click the variable and add select Add Wave, just as you would with a signal.

Final thoughts

Before writing this article, I posted a question in my private Facebook group to see what other FPGA engineers had to say about using variables for storage. As I expected, most dislike using variables in this way in RTL modules. The consensus is that they are confusing to work with when they assume the same role as signals: to act as registers.

One user wrote that there’s a higher risk of creating latches with variables than signals. I’ve heard that statement before, and I tried to construct an example to demonstrate the problem, but I couldn’t. I wasn’t able to find any situations where a variable causes a latch while a signal doesn’t. Of course, that doesn’t mean that it can’t happen. But we will have to leave that statement unconfirmed at the moment.

Leave a comment if you have such an example!

Finally, a user whose first name is Ray sums up nicely what I think is the most significate argument against variables. He says:

My problem with this is that you are now relying on the synthesis tool to do “the right thing” rather than simply writing code that makes it do the right thing.

I would only use variables in processes that are implementing some combinatorial logic. Then I’d register that output in a separate clocked process with its own reset.

And I have to agree on that. It’s better to be explicit in VHDL. If you want a register, it’s safest to use a signal.

Similar Posts

8 Comments

  1. I acknowledge the positive effect of encapsulation provided by varuables, BUT:

    My coding style tends to favour short VHDL files. I,.e. I tend to prefer breaking up my design into small blocks. I seldom have files larger than 300 lines in total.

    When the files are small, the benefit of encapsulation is much less.

    1. I agree with you on that. It’s much better to break up a large architecture into a structural module with multiple submodules. The total number of files and lines increases, but so does the readability.

  2. >> If we swap lines 7 and 8 in the original DUAL_PORT_RAM process, it’s broken.

    How is it broken? It looks like write-before-read, dual port memory.

    Thank you for another interesting and thought-provoking article!

    1. Hello Andrew, that’s a good question! Let’s see what happens if we swap lines 7 and 8 in the DUAL_PORT_RAM process using variables. The listing below is from the Vivado synthesis log. It has mapped the ram_v variable to 192 RAM64M primitives, aka distributed RAM, also known as LUTRAM in Xilinx FPGAs.

      Distributed RAM: Preliminary Mapping	Report (see note below)
      +------------+------------+-----------+----------------------+---------------+
      |Module Name | RTL Object | Inference | Size (Depth x Width) | Primitives    | 
      +------------+------------+-----------+----------------------+---------------+
      |bram        | ram_v_reg  | Implied   | 1 K x 36             | RAM64M x 192	 | 
      +------------+------------+-----------+----------------------+---------------+
      

      Write-before-read in block RAM is only possible on the same port. By that, I mean using the same address and clock. In dual-port RAM, you can have two identical ports with separate clocks, addresses, and a write enable input. Take a look at the example code below. I have implemented one such port that has a wr_en signal that controls if we are writing to the RAM.

      DUAL_PORT_RAM : process(clk)
        type ram_type is array (0 to 2**10 - 1) of std_logic_vector(35 downto 0);
        variable ram_v : ram_type;
      begin
        if rising_edge(clk) then
      
          if wr_en = '1' then
            ram_v(addr) := data_in;
          end if;
      
          data_out <= ram_v(addr);
          
        end if;
      end process;
      

      The code example is write-first. In the listing below, we can see that Vivado correctly mapped it to a RAMB36 block RAM primitive. Also, take note that it says WRITE_FIRST in the PORT A column, while in the Preliminary Mapping Report in the blog post, it said READ_FIRST in the same column.

      Block RAM: Final Mapping	Report
      +------------+------------+------------------------+---+---+------------------------+---+---+------------------+--------+--------+
      |Module Name | RTL Object | PORT A (Depth x Width) | W | R | PORT B (Depth x Width) | W | R | Ports driving FF | RAMB18 | RAMB36 | 
      +------------+------------+------------------------+---+---+------------------------+---+---+------------------+--------+--------+
      |bram        | ram_v_reg  | 1 K x 36(WRITE_FIRST)  | W | R |                        |   |   | Port A           | 0      | 1      | 
      +------------+------------+------------------------+---+---+------------------------+---+---+------------------+--------+--------+
      

      I’m going to create a blog post about block RAM to clear up some misunderstandings, and also to educate myself. It’s easy to get confused with all the different configuration options that block RAM have.

      1. >> I’m going to create a blog post about block RAM to clear up some misunderstandings, and also
        >> to educate myself. It’s easy to get confused with all the different configuration options that
        >> block RAM have.

        Looking forward to that blog post Jonas!

      2. Vivado has some surprises. I extended your code with byte selectable write enable, which works fine for READ_FIRST

        architecture rtl of RAM_single_port is
        	subtype memory_cell_type is std_logic_vector(8 * width_in_bytes - 1 downto 0);
        	type ram_chip_type is array (0 to depth - 1) of memory_cell_type;
        begin	
        	portA: process is	
        	variable ram : ram_chip_type;	
        	begin
        		wait until rising_edge(clk);
        		-- read
        		douta <= ram(addra);					
        		-- write
        		for i in 0 to width_in_bytes - 1 loop
        			if wea(i) = '1' then
        				ram(addra)(8 * i + 7 downto 8 * i) := dina(8 * i + 7 downto 8 * i);		
        			end if;
        		end loop;	
        		
        	end process;		
        end architecture;
        

        but make it WRITE_FIRST

        		-- write
        		for i in 0 to width_in_bytes - 1 loop
        			if wea(i) = '1' then
        				ram(addra)(8 * i + 7 downto 8 * i) := dina(8 * i + 7 downto 8 * i);		
        			end if;
        		end loop;	
        		-- read
        		douta <= ram(addra);	
        

        and Vivado cannot infer the BRAM (even though this type is configurable from the IP tools). It reports

        [Synth 8-2914] Unsupported RAM template

Leave a Reply

Your email address will not be published. Required fields are marked *