Date

The other day I was coding an FPGA design that needed a source of normally distributed random numbers. Uniformly distributed random numbers are fairly easy and cheap to generate with a linear feedback shift register, but generating normally distributed numbers is a harder problem. There are several ways to generate a normally distributed number using one or more uniformly distributed, some are more accurate than others. For my purposes the numbers don't need to exactly follow the normal distribution,, it's enough if it seems like for a normal human that the numbers are from the normal distribution.

So I went for the easiest and cheapest implementation and use the central limit theorem, which says that summing sufficiently many independent random numbers result will be approximately normally distributed. I went for the sum of four uniformly distributed random numbers generated by a linear feedback shift register. To avoid obvious period in the output I decided to have a rather large 31-bit registers. 31-bits because it's big enough and it needs only two taps when a 32-bit register would need four.

Code for the linear feedback shift registers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;


entity random_uniform is
    generic ( SEED : STD_LOGIC_VECTOR(30 downto 0):= (others => '0');
                 OUT_WIDTH : integer := 11);
    Port ( clk : in  STD_LOGIC;
           random : out  STD_LOGIC_VECTOR (OUT_WIDTH-1 downto 0);
           reset : in  STD_LOGIC);
end random_uniform;

architecture Behavioral of random_uniform is

signal rand : std_logic_vector(30 downto 0) := SEED;
signal feedback : std_logic;

begin

feedback <= not((rand(0) xor rand(3)));

process(clk,reset)
begin
if reset = '1' then
    rand <= SEED;
elsif rising_edge(clk) then
    rand <= feedback&rand(30 downto 1);
end if;
end process;

random <= rand(OUT_WIDTH-1 downto 0);

end Behavioral;

Implementation of the linear feedback shift registers is very straight forward. Seed value and output width can be set with generics making it also useful in many other places.

Summing four of these uniform random numbers should generate number that is almost normally distributed. Code for the normally distributed pseudorandom generator is below, it outputs 12-bit signed numbers every three clock cycles, but it should be easy to modify it to work in one clock cycle just by adding more adders.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity random_gaussian is
    Port ( clk : in  STD_LOGIC;
           reset : in  STD_LOGIC;
           random : out  STD_LOGIC_VECTOR (11 downto 0));
end random_gaussian;


architecture Behavioral of random_gaussian is

component random_uniform is 
generic ( SEED : STD_LOGIC_VECTOR(30 downto 0);
             OUT_WIDTH : integer);
port(
    clk : in  STD_LOGIC;
    random : out  STD_LOGIC_VECTOR (OUT_WIDTH-1 downto 0);
    reset : in  STD_LOGIC);
end component;

component adder_signed is
    Port ( clk : in  STD_LOGIC;
           a : in  STD_LOGIC_VECTOR (11 downto 0);
           b : in  STD_LOGIC_VECTOR (11 downto 0);
           r : out  STD_LOGIC_VECTOR (11 downto 0));      
end component;

signal uniform1 : std_logic_vector(9 downto 0);
signal uniform2 : std_logic_vector(9 downto 0);
signal uniform3 : std_logic_vector(9 downto 0);
signal uniform4 : std_logic_vector(9 downto 0);

signal adder_a : std_logic_vector(11 downto 0);
signal adder_b : std_logic_vector(11 downto 0);
signal adder_r : std_logic_vector(11 downto 0);

type statetype is (s0, s1, s2);

signal state, next_state: statetype := s0;

begin

unif1: random_uniform 
generic map (SEED => std_logic_vector(to_unsigned(697757461,31)),
                 OUT_WIDTH => 10)
port map(
    clk => clk,
    random => uniform1,
    reset => reset
);

unif2: random_uniform 
generic map (SEED => std_logic_vector(to_unsigned(1885540239,31)),
                 OUT_WIDTH => 10)
port map(
    clk => clk,
    random => uniform2,
    reset => reset
);

unif3: random_uniform 
generic map (SEED => std_logic_vector(to_unsigned(1505946904,31)),
                 OUT_WIDTH => 10)
port map(
    clk => clk,
    random => uniform3,
    reset => reset
);

unif4: random_uniform 
generic map (SEED => std_logic_vector(to_unsigned(2693445,31)),
                 OUT_WIDTH => 10)
port map(
    clk => clk,
    random => uniform4,
    reset => reset
);

adder1 : adder_signed
  PORT MAP (
    a => adder_a,
    b => adder_b,
    clk => clk,
    r => adder_r
);

process(clk, reset)
begin

if rising_edge(clk) then
    if reset = '1' then
        state <= s0;
        random <= (others => '0');
    else
        if state = s0 then
            random <= adder_r;
        end if;
        state <= next_state;
end if;

end if;
end process;


process(clk,state,uniform1,uniform2,uniform3,uniform4,adder_r)

begin

case state is

    when s0 =>
        -- Sign extend
        adder_a <=  uniform1(9)&uniform1(9)&uniform1(9)&uniform1(8 downto 0);
        adder_b <=  uniform2(9)&uniform2(9)&uniform2(9)&uniform2(8 downto 0);

        next_state <= s1;

    when s1 =>
        adder_a <= adder_r;
        adder_b <= uniform3(9)&uniform3(9)&uniform3(9)&uniform3(8 downto 0);

        next_state <= s2;

    when s2 =>
        adder_a <= adder_r;
        adder_b <= uniform4(9)&uniform4(9)&uniform4(9)&uniform4(8 downto 0);

        next_state <= s0;

end case;
end process;

end Behavioral;

And the code of the adder for adding two 12-bit signed numbers, it has a latency of one cycle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity adder_signed is
    Port ( clk : in  STD_LOGIC;
           a : in  STD_LOGIC_VECTOR (11 downto 0);
           b : in  STD_LOGIC_VECTOR (11 downto 0);
           r : out  STD_LOGIC_VECTOR (11 downto 0));      
end adder_signed;

architecture Behavioral of adder_signed is

signal r_next : std_logic_vector(11 downto 0) := (others => '0');

begin

process(clk)

begin

if rising_edge(clk) then
    r <= r_next;
    r_next <= std_logic_vector(signed(a)+signed(b));
end if;

end process;

end Behavioral;

I wrote a testbench to write output values to a file and a mathematica notebook to test how close the numbers are to the normal distribution. Histograms were generated with "Histogram[x, 200]" command and plots with "ListPlot[x, Joined->True]", where "x" is a list of values.

500 output values of the generator.

Same amount of samples from the normal distribution.

You can see that the approximation is pretty close to the normal distribution. It's maybe a little bit fatter, but it wouldn't be noticeable without comparing it to a true normal distribution.

Histogram of the generated numbers. Does it look like a normal distribution?

Histogram of the same amount of random numbers generated from the normal distribution. Notice the different values on the axes.

As you can see from the histograms the approximation is a little bit flatter, but it comes amazingly close. Just by looking at the histogram without a normal distribution to compare it to, I would say that it is a normal distribution and that's the only thing that was important.

Appendix 1.

VHDL test bench for writing the output values into a file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
use std.textio.all;
use ieee.std_logic_textio.all;
USE ieee.numeric_std.ALL;

ENTITY random_gaussian_test_bench IS
END random_gaussian_test_bench;

ARCHITECTURE behavior OF random_gaussian_test_bench IS

     FILE test_out_data: TEXT open WRITE_MODE is "output/gaussian.txt"; 
    -- Component Declaration for the Unit Under Test (UUT)

    COMPONENT random_gaussian
    PORT(
         clk : IN  std_logic;
         reset : IN  std_logic;
         random : OUT  std_logic_vector(11 downto 0)
        );
    END COMPONENT;


   --Inputs
   signal clk : std_logic := '0';
   signal reset : std_logic := '0';

    --Outputs
   signal random : std_logic_vector(11 downto 0);

   -- Clock period definitions
   constant clk_period : time := 10 ns;

BEGIN

    -- Instantiate the Unit Under Test (UUT)
   uut: random_gaussian PORT MAP (
          clk => clk,
          reset => reset,
          random => random
        );

   -- Clock process definitions
   clk_process :process
   begin
        clk <= '0';
        wait for clk_period/2;
        clk <= '1';
        wait for clk_period/2;
   end process;


   -- Stimulus process
   stim_proc: process
    variable L1 : LINE;
    variable I : integer;
   begin        
        reset <= '1';

      wait for clk_period*20;

        reset <= '0';
        wait for clk_period*30;

        for I in 0 to 1000000 loop
            wait for clk_period*3;

            write(L1, to_integer(signed(random)));
            writeline(test_out_data, L1);
        end loop;
      wait;
   end process;

END;