Pseudo random generation Tutorial

In this tutorial, we will design a VHDL block. We will start with a very simple version of the block and gradually add features to it. We will also simulate it and test its outputs using a testbench and Python. During the process we will see:

  • How to start with a simple block and gradually add features and improvements
  • How to add a test bench (simulation)
  • Saving the block data output to files (from simulation)
  • Exporting files to Python in order to:
    • Verify the results, and
    • Analyze the results (in this case, using FFT)

For this tutorial, it is assumed that you already have basic knowledge of the VHDL language and know how to use simulation tools (We will use Vivado, but you can easily adapt the tutorial to other tools you may be familiar with).

Chapter 1 – Simple implementation

We are going to generate random bitstreams by HW. One popular way of generating pseudo-random bits by HW is by means of LFSR. LFSR stands for Linear-Feedback Shift Register.

The input bit to the shift register is a linear function of its previous value. The linear function is implemented by XOR’ing several bits of the shift register and sending the output of the XOR gates to the input of the shift register. The mathematical basis used to generate these sequences is linear algebra, where the register is interpreted as a polynomial. Each ‘n’ bit in the register is the coefficient of order ‘n’ of such a polynomial.

A register of length n can generate a pseudo-random sequence of maximum length 2n-1. There are ‘recipes’ for the linear feedback function needed to generate maximum length sequences for any register length. For further reading, you can check this application note from Xilinx, or the LFSR entry from Wikipedia.

So let’s see our first version of a pseudo-random generator written in VHDL.

For this first example, the polynomial order is very low (4 bits), generating a sequence consisting of 15 values. After the generator register reaches the 15th value, the sequence repeats itself. It is important to notice that the LFSR is a one-bit random generator. If we want to generate n-bit random numbers, we have to either:

  • Generate the number using n-iterations of the LFSR, or
  • Use n LFSR registers working in parallel

Since the sequence repeats itself it is called pseudo-random. It has a certain variability, but on the other hand, it is repetitive, and even if it is not a trivial sequence, it will always be the same sequence. However, as mentioned on Xilinx app. note, that even a rather small 64-bit LFSR, working at 50MHz, would take several thousand years to repeat itself.

So, enough with the chit-chat, and let’s take a look at the first version of our LFSR.

library ieee;
  use ieee.std_logic_1164.all;
  use ieee.numeric_std.all;

entity lfsr_v1 is
  port (
    rstn   : in  std_logic;
    clk    : in  std_logic; 
    rand   : out std_logic    -- lfsr output
  );
end entity;

architecture rtl of lfsr_v1 is
  signal lfsr 	    : std_logic_vector (3 downto 0); -- for lfsr register
  signal feedback 	: std_logic;                     -- lfsr feedback

begin
  feedback <= not(lfsr(3) xor lfsr(2));		-- LFSR size 4

  -- lfsr generate
  lfsr_pr : process (clk) 
	begin
    if (rising_edge(clk)) then
      if (rstn = '0') then
        lfsr <= (others=>'0');
      else
        lfsr <= lfsr(2 downto 0) & feedback;
      end if;
    end if;
  end process lfsr_pr;
  rand <= lfsr(3);

end architecture;

The process lfsr_pr which starts at line 27 implements a shift register. The feedback input to the shift register is a linear combination of some of its own bits (see line 20).

You can find the sources for chapters 1 & 2 on Github



Chapter 2 – Adding a test bench

Now let’s go over the test bench for the LFSR.

library ieee;
  use ieee.std_logic_1164.all;
  use ieee.numeric_std.ALL;
    
entity tb_lfsr_v1 is
end entity;

architecture test of tb_lfsr_v1 is

  constant PERIOD  : time   := 10 ns;

  signal clk       : std_logic := '0';
  signal rstn      : std_logic := '0';
  signal rand      : std_logic;
  signal endSim	   : boolean   := false;

  component lfsr_v1 is
    port (
      rstn      : in  std_logic;                       
      clk       : in  std_logic;               
      rand      : out std_logic   
    );
  end component;
    

begin
  clk     <= not clk after PERIOD/2;
  rstn    <= '1' after  PERIOD*10;
  endSim  <= true after PERIOD*80;

 -- End the simulation
 process 
 begin
   if (endSim) then
     assert false 
       report "End of simulation." 
       severity failure; 
   end if;
   wait until (clk = '1');
 end process;	

 lfsr_inst : lfsr_v1
 port map (
   clk      => clk,
   rstn     => rstn,
   rand     => rand
 );

end architecture;

Usually, a test bench is a wrapper for the unit under test (UUT), and as such, it has no ports. The UUT is a component instantiated under the test bench.

The test bench has signals that are used to exercise the component under test. A 100MHz (10 nsec period) clock is defined on line 27. The rstn signal is asserted at the beginning (see line 13) and de-asserted after some time (line 28). This block is so simple that it is enough to provide a clock and de-assert rstn to get it going.

The process starting at line 32 stops the simulation after some time. In this way, the simulation can be run with the run -all command and will stop by itself.

Let’s see a screenshot of the simulation:

You can find the sources for chapters 1 & 2 on GitHub

It can be seen that the value of rand changes in a rather chaotic way. But even if the sequence is not an obvious, predictable sequence as a regular counter could produce, it repeats itself after 15 clocks (look at the markers and at the value of the internal shift register, that after 15 clocks also repeats itself.

If we make some statistics of the values generated at the rand output, we can see that:

Single 0 -> Two occurrences

Single 1 -> Two occurrences

Two consecutive zeroes, two consecutive ones -> One occurrence each

Three consecutive ones -> One occurrence

Four consecutive zeros -> One occurrence

This rather flat distribution of occurrences also points to a random distribution (up to a limit, we can see that there aren’t any appearances of five or more consecutive values. In a really random sequence, we must see any quantity of consecutive values appearing, although the higher the number of consecutive equal values, the lower the probability of this happening).

In further entries of the tutorial, we will talk about adding parameters to the VHDL block, saving the results from the testbench, and analyzing them with the help of Python routines.

Go to the second part of this tutorial

Leave a Reply

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