FPGA logo large

Exploring Register Files

It is back to FPGA topics for this next article. This time we are going to be exploring register files. A register file is nothing more than an array, collection or “file” of registers. We have encountered registers before, first when I introduced sequential circuits and the Verilog reg keyword. A register is nothing more than a collection of one or more bits logically grouped together.

The important part attribute of a register is that it has memory. That is, a register “remembers” and maintains the last value set until it is either changed or reset. A memory bit is typically implemented in hardware as a D-Type Flip Flop (D-FF). Combining 8 of these D-FF’s together we can form an 8-bit register. A collection of 8-bit registers makes a register file.

Microprocessors use registers to maintain internal state. In many processors, the registers have specific names like A, B and X. However, in modern RISC processors there typically are a large number of registers (e.g. R0 – R31) organized as a register file.

Creating a register file in Verilog

We have created and used registers in many earlier projects. In Verilog a register is expressed as an array of memory bits, like the code below.

// declare an 8-bit register called my_register
reg [7 : 0] my_register;

But we want to create an array of registers for our register file. In Verilog this looks much like a two-dimensional array in other programming languages. Looking at the code below you can see that I’ve declared a component that I’ve called my_register_file.

// declare an array of 16 x 8-bit registers
reg [7 : 0] my_register_file [3 : 0];

Now if you are familiar with two-dimensional arrays in other programming languages you may note the specific syntax is a bit different than you might expect. The first array declaration reg [7 : 0] specifies the data type as an array of 8-bits. The second array declaration [3 : 0] specifies that there are 2 ^ 4 (i.e. 16) elements in the array.

Packaging up our register file

If we were incorporating a register file in another circuit, we might well decide to declare it as shown above. However, throughout these articles we are trying to build up a toolbox of re-usable components. And so, we want to package up a register file circuit that can be embedded in other designs. And to make these components more flexible we want to use module parameters which were introduced here.

The first thing that would be useful to parameterize on are the bit width of the registers. That way we can create an 8-bit wide set of registers as easily as a 32-bit wide set of registers. Let’s call this parameter REG_BITS. The second useful parameter would be the number of registers in our file. And let’s call this parameter ADDR_BITS.

In terms of inputs and outputs, as with all sequential circuits, we will have a clk signal. Also useful will be a signal to convey whether we are reading or writing data to the register file. Let’s call that signal wr_en. And since we have a number of registers, it may be convenient to be able to read and write to different parts of the register file simultaneously. Another way to express that is that we want to have separate read and write ports. For that we will define ports called r_data, r_addr, wr_data and wr_addr.

Remember that a port is simply a collection of inputs or outputs.

Putting this all together in Verilog is fairly straightforward. Other than two-dimensional arrays, there are no concepts that we have not seen before. Have a look at the code below.

module register_file
    #(
        parameter REG_BITS = 8,  // default to 8b wide registers
        ADDR_BITS = 3            // default to 8 registers total
    )
    (
        input clk,                                // clock signal
        input wr_en,                              // write enable
        input wire [ADDR_BITS - 1 : 0] wr_addr,   // write address
        input wire [ADDR_BITS - 1 : 0] r_addr,    // read address
        input wire [REG_BITS - 1 : 0] wr_data,    // input port
        output wire [REG_BITS - 1 : 0] r_data     // output port
    );
    
    // declare an array of registers
    reg [REG_BITS - 1 : 0] register_array [2 ** ADDR_BITS - 1 : 0];
    
    // handle synchronous write operation
    always @(posedge clk)
        if (wr_en)
            register_array[wr_addr] <= wr_data;

    // handle read operation
    assign r_data = register_array[r_addr];    

endmodule

Stepping through the code

The register file is declared on line 16 using the chosen module parameters REG_BITS and ADDR_BITS. Starting on line 19, on a positive clk edge, if the wr_en signal is raised, the value of the wr_data input port is written to the register array at the address defined by the wr_addr input port. Finally, on line 24 we use a continuous assignment to read the register value located at r_addr in the register file and put it on the output port r_data.

Testing the code

Putting together a simple test bench we can test the register file circuit. The test circuit instantiates an 8 x 8-bit register file. It then sequentially writes the value 255 to addresses 0 through 4 and the value 0 to addresses 5 through 7 over 8 clock cycles. And finally, it reads the values at addresses 3 and 7. The test bench code is shown below.

module register_test();
    localparam T = 20;
    localparam ADDR_BITS = 3;
    localparam REG_BITS = 8;
    
    reg test_clk;
    reg en;
    
    reg [ADDR_BITS - 1 : 0] waddr, raddr;
    reg [REG_BITS - 1 : 0] wdata;
    wire [REG_BITS - 1 : 0] rdata;

    integer i;
    
    // declare the unit under test
    register_file #(.ADDR_BITS(ADDR_BITS), .REG_BITS(REG_BITS)) regfile
    (
        .clk(test_clk), 
        .wr_en(en), 
        .wr_addr(waddr), 
        .r_addr(raddr), 
        .wr_data(wdata), 
        .r_data(rdata)
    );
    
    // generate the clock signal    
    always
    begin
        test_clk = 1'b1;
        #(T/2);

        test_clk = 1'b0;
        #(T/2);
    end

    // set initial conditions
    initial
    begin
        wdata = 0;
        en = 1'b0;
        waddr = 0;
        raddr = 0;   
    end
    
    initial
    begin
        @(negedge test_clk);

        wdata = 255;
        
        // do some writes of 255
        for(i = 0; i < 4; i = i + 1)
        begin
            @(negedge test_clk);
            waddr = i;
            en = 1'b1;
            @(negedge test_clk);
            en = 1'b0;
        end

        wdata = 0;
        
        // do some writes of 0
        for(i = 5; i < 8; i = i + 1)
        begin
            @(negedge test_clk);
            waddr = i;
            en = 1'b1;
            @(negedge test_clk);
            en = 1'b0;
        end                
  
        raddr = 3;
        
        #(T);        

        raddr = 7;

        #(T);        
        
        $stop;
    end
    
endmodule

Looking at the simulation waveform shows the results of the test. Note that the reads occur asynchronously as soon as the r_addr changes.

Register File Test Waveform

And that is it! We have a configurable register file that can be used in other circuit designs. The source code for this project is available on the github site for this blog.

Remember to please like and share this post. Have a comment, question or suggestion regarding this post? Please leave me a comment!

Discover more from FPGA Coding

Subscribe now to keep reading and get access to the full archive.

Continue reading