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.
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!