• 0

Feedback on a register file design?


Go to solution Solved by Piasa,

Question

Hey everyone,

I've done the initial design of a register file (16x 32-bit registers, two write ports, four read ports) in VHDL as part of a larger project, but seeing as I am a relative newcomer to HDLs, I was hoping to get some feedback on my design, any errors I may have made, or any improvements I might want to make.

Here is the VHDL:

-- Register file

-- Two write ports, four read ports.
-- For performance reasons, this register file does not check for the same 
-- register being written on both write ports in the same cycle. CPU control
-- circuitry is responsible for preventing this condition from happening.

library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;

use work.cpu1_globals_1.all;
use work.func_pkg.all;

entity registerFile is
    port
    (
        clk : in std_logic;
        rst : in std_logic;
        writeEnableA : in std_logic;
        writeEnableB : in std_logic;
        readSelA, readSelB, readSelC, readSelD, writeSelA, writeSelB : in std_logic_vector(3 downto 0);
        data_inA, data_inB : in std_logic_vector(DATA_WIDTH - 1 downto 0);
        data_outA, data_outB, data_outC, data_outD : out std_logic_vector(DATA_WIDTH - 1 downto 0)
    );
end registerFile;

architecture behavioral of registerFile is

    type regArray is array (0 to 15) of std_logic_vector(DATA_WIDTH - 1 downto 0);
    signal registers : regArray := (others => (others => '0'));

begin

    data_outA <= registers(to_integer(unsigned(readSelA)));
    data_outB <= registers(to_integer(unsigned(readSelB)));
    data_outC <= registers(to_integer(unsigned(readSelC)));
    data_outD <= registers(to_integer(unsigned(readSelD)));

    registerFile_main : process(clk)
    
    begin
    
        if(rising_edge(clk)) then
        
            if(rst = '1') then
        	
                registers <= (others => (others => '0'));
                
            else
                
                if(writeEnableA = '1') then
                    registers(to_integer(unsigned(writeSelA))) <= data_inA;
                end if;
            	
                if(writeEnableB = '1') then
                    registers(to_integer(unsigned(writeSelB))) <= data_inB;
                end if;
                
            end if;
            
        end if;
        
    end process;
    
end behavioral;

This design is intended for use on FPGAs, hence the use of default values for the registers.

I appreciate any feedback you might have!

Thanks,

 - Curt

Link to post
Share on other sites

Recommended Posts

  • 0
16 hours ago, zygot said:

Tell me that I'm an idiot if you want (I really don't mind) but.... I predict a lot less cursing and frustration if you develop the FPGA craft skills before pouring hours into the implementation stage where the initial product is supposed to rival current state of the art processors. The last 20 years has given us hardware optimizations like out-of-order execution, speculative branching and the like and it's just been recently that we've been served the bill ( from a security perspective ). I get the passion. I like it. I don't get masochism. I don't get wanting an end product without wanting to understand the process to achieve it. So I'll channel your moms... "well dear, as long as it makes you happy.." 

I'm going to heed the advice of pretty much everyone on this thread and tackle some more simple, self-contained projects to better learn the craft, and the quirks. It is important to understand the basic platform you're working on before developing something highly advanced on it.

As for the advanced features and optimizations you mentioned, you're probably right to say that implementing them will be difficult, time-consuming and possibly not even necessary. But those very aspects are essentially the reason I'm doing this. The genesis of my fascination with CPU design comes from when I was much younger and found myself pouring over Intel architectural manuals and pretty much anything I could absorb about machine design. So these things like speculative and out-of-order execution, branch prediction, ILP, caching policies, and so forth, are core to my interest in this. I'd honestly rather try to implement them and fail, than sacrifice them and succeed. Though that doesn't preclude me from designing a much simpler design -first- and then getting more advanced from there.

Thanks again to everyone in this thread for the conversation and advice!

Link to post
Share on other sites
  • 0

A comment on your latest code.

You chose to select the ieee.numeric library. Usually when I do this it's because I know that I'm going to deal with signed and unsigned types. This helps keep my focus on that pesky sign bit. So I generally assign signals to either signed or unsigned types when using ieee.numeric. In general, I use ieee.std_logic_unsigned or ieee.std_logic_signed libraries. The details involved in signals that carry signed or unsigned values can trip you up if you aren't careful. This is especially true if you use multipliers or fractional arithmetic ( a whole fascinating subject in it's own right ). Here's where a good textbook or coding guidelines come into play. A std_logic_vector can always hold signed values but you have to do the bookkeeping. The takeaway is that nothing is chosen randomly, you need to know what consequences of your choices are before you have a large piece of code with hundreds or thousands of lines. The prep work is more important to success (or at least how long it takes to achieve success) than the implementation in my experience. Here is where Dan might point out why he prefers Verilog which has a lineage more like C than VHDL which ADA-like. A lot of nitty-gritty details that your friendly ( or perhaps not so friendly... ) C compiler will take care of for you becomes your responsibility in logic design.

PS. so I've avoided directly commenting you your original question until now leaving that up to others.

Oh, and one little problem with using good code as a guide is that it probably doesn't have the commentary that points out why the code was written the way that it was... just a fair warning.

Edited by zygot
Link to post
Share on other sites
  • 0

Actually, since I just now started reading your code carefully ( you're probably thinking "and you're just telling me this now??" ), there are a number of things that I'd do differently. I suggest that you create a small complete project that you can simulate, and create a bitstream from and test. Look at the post-route timing as you play with the structure. There's a lot of ways to achieve a behavioural result but not many that also scale well with clock frequencies and total design complexity. This is where experience becomes important.

Link to post
Share on other sites
  • 0

Ummm. A quick reality check: So that one asks later, why did no one tell me.

There is no "business case" for high-end CPUs on FPGAs.
Period.

Meaning, it will always be dramatically slower, more expensive, less energy-efficient than a dedicated chip.
Maybe pick a Raspberry Pi as reference.

Yes, as a learning experience it's a great idea - probably the best that has shown up in amateur electronics in the last decade or so - and I don't even want to rain on anybody's parade. But, a general purpose CPU on a general-purpose FPGA just won't fly, it can never be competitive against an ASIC. It's like mining bitcoins on a PC, the basic facts are against you.

So just keep that in mind before heading out into a dead end. CPUs on FPGA, the journey is the destination.

Edited by xc6lx45
Link to post
Share on other sites
  • 0
1 hour ago, zygot said:

A comment on your latest code.

You chose to select the ieee.numeric library. Usually when I do this it's because I know that I'm going to deal with signed and unsigned types. This helps keep my focus on that pesky sign bit. So I generally assign signals to either signed or unsigned types when using ieee.numeric. In general, I use ieee.std_logic_unsigned or ieee.std_logic_signed libraries. The details involved in signals that carry signed or unsigned values can trip you up if you aren't careful. This is especially true if you use multipliers or fractional arithmetic ( a whole fascinating subject in it's own right ). Here's where a good textbook or coding guidelines come into play. A std_logic_vector can always hold signed values but you have to do the bookkeeping. The takeaway is that nothing is chosen randomly, you need to know what consequences of your choices are before you have a large piece of code with hundreds or thousands of lines. The prep work is more important to success (or at least how long it takes to achieve success) than the implementation in my experience. Here is where Dan might point out why he prefers Verilog which has a lineage more like C than VHDL which ADA-like. A lot of nitty-gritty details that your friendly ( or perhaps not so friendly... ) C compiler will take care of for you becomes your responsibility in logic design.

PS. so I've avoided directly commenting you your original question until now leaving that up to others.

Oh, and one little problem with using good code as a guide is that it probably doesn't have the commentary that points out why the code was written the way that it was... just a fair warning.

I view signed and unsigned as just different ways to look at a bucket of bits. My current practice is to favor the use of std_logic_vector for moving and storing data, and converting to unsigned for arithmetic and logical operations. I try to minimize the use of casting or conversion to types that don't preserve 9-level logic, as this can hide problems in simulation that might pop up in implementation. So if I need to perform arithmetic on a set of std_logic_vectors, I convert them to unsigned, perform the operation, and then store the results as std_logic_vectors.

Wherever possible, I try to design modules that output correct results without respect to sign, leaving it up to the user or higher-level functions to interpret the values as signed or unsigned. However I'm aware that some operations -must- be sign-aware (i.e. multiplication requiring sign extension).

For example, the key line of code for the "add" pathway in my ALU is designed to perform sign-agnostic addition and subtraction based on a set of parameters:

tempResult := unsigned("0" & operandA) + unsigned("0" & opB_adjusted) + unsigned'("" & (invert_opB xor tempCarry));

Where:
tempResult -- self-explanatory
operandA -- self-explanatory
opB_adjusted -- operand B, which is either pre-inverted (logical NOT), or passed through unchanged based on the value of:
invert_opB -- a std_logic parameter where '1' specifies to invert operand B and '0' specifies to pass it through
tempCarry -- a std_logic value representing the carry-in ANDed with the carry_enable parameter

Given that:
 - The the carry-in bit is represented as '1' = there was a carry or a borrow, and '0' = there was no carry or borrow, and
 - Two's complement negation is effectively one's complement negation + 1, and
 - A - B = A + -B

The line of code above provides results with or without carry that will be correct regardless of interpretation as signed or unsigned. By XOR-ing invert_opB with tempCarry, that means if carry/borrow are enabled, the carry bit (if set) will effectively be added during addition with carry, or subtracted during subtraction with borrow (by essentially withholding the "+ 1" normally used in two's complement negation). If carry/borrow isn't enabled, tempCarry is guaranteed to be '0' by the earlier AND operation, and invert_opB will be '1' only if subtraction is being performed (completing the two's-complement negation).

I'm sure this has been implemented in a better way by someone else, but it's the solution I've chosen for now at least.

Edited by CurtP
Link to post
Share on other sites
  • 0
44 minutes ago, CurtP said:

I view signed and unsigned as just different ways to look at a bucket of bits.

I might go with the notion that STD_LOGIC_VECTOR is a bucket of ULOGIC bits. I've done a lot of signed and unsigned VHDL projects and I can't say that I can't support the quote above. In C abstraction is a concept to be embraced. In logic design you are responsible for any abstraction you want to imply. VHDL, like ADA is a strongly typed language and will give you lots of error messages. You can still get into trouble, even with VHDL, especially with carries, overflow, comparison etc.

Now that you've brought it up

44 minutes ago, CurtP said:

tempResult := unsigned("0" & operandA) + unsigned("0" & opB_adjusted) + unsigned'("" & (invert_opB xor tempCarry));

The ':=' implies instantaneous value assignment. It is not the same as the "<=" gets assignment. The two are not interchangeable. Confusing the two will result in "you := are_in_trouble" in a hurry. I rarely use ":=" except for simulation and special circumstances.

You should understand that VHDL was not designed for synthesis. IT is a simulation language. Most, but not all of its statements can be synthesized into logic or are supported by vendors synthesis tools. Both Altera and Xilinx will tell you what statements are supported.

Edited by zygot
Link to post
Share on other sites
  • 0
26 minutes ago, CurtP said:

I view signed and unsigned as just different ways to look at a bucket of bits. My current practice is to favor the use of std_logic_vector for moving and storing data, and converting to unsigned for arithmetic and logical operations.

You can also import just "+" and "-" from std_logic_signed as well as the conversion functions from std_logic_arith.  This way you still are required to specify signed/unsigned for "<", "*", etc...  Importing "-" from std_logic_signed will give you the unary "-", which can be used for logical induction in expressions like (-x) and (x).

Link to post
Share on other sites
  • 0
1 hour ago, xc6lx45 said:

There is no "business case" for high-end CPUs on FPGAs.
Period.

@xc6lx45,

You must like experimenting with the reaction of Africanized bee colonies to jack-hammers. I'm in a feisty mood today so what the heck...

There is absolutely a place for hard CPU core based FPGA devices like the Zynq. I don't even feel the needs to support that statement. For almost all low power applications the FPGA can't compete with a commercial uC or DSP. I tend to be more sympathetic with you on soft CPU cores using FPGA resources. The exception is when you are pursuing a project that is a labour of love. Implementing a full-stack Ethernet interface in HDL makes no sense to me. There are times when post configuration programmability might push me toward a soft processor. But then I'd use an Atmel clone that someone else's software toolchain. If someone ( I can think of someone ) makes a great soft processor that is compatible with the gcc toolchain I might be interested. By and large HDLs get almost everything done that needs to be done.

BTW there's thread in another section in the Digilent forum dedicated to just this topic... which would be a better place to post your argument.

Edited by zygot
Link to post
Share on other sites
  • 0
42 minutes ago, zygot said:

I might go with the notion that STD_LOGIC_VECTOR is a bucket of ULOGIC bits. I've done a lot of signed and unsigned VHDL projects and I can't say that I can't support the quote above. In C abstraction is a concept to be embraced. In logic design you are responsible for any abstraction you want to imply. VHDL, like ADA is a strongly typed language and will give you lots of error messages. You can still get into trouble, even with VHDL, especially with carries, overflow, comparison etc.

Now that you've brought it up

The ':=' implies instantaneous value assignment. It is not the same as the "<=" gets assignment. The two are not interchangeable. Confusing the two will result in "you := are_in_trouble" in a hurry. I rarely use ":=" except for simulation and special circumstances.

You should understand that VHDL was not designed for synthesis. IT is a simulation language. Most, but not all of its statements can be synthesized into logic or are supported by vendors synthesis tools. Both Altera and Xilinx will tell you what statements are supported.

tempResult is a 33-bit variable, used for convenience within the process. Its use is appropriately expanded upon elaboration and synthesis. It isn't used to register a value between cycles. The result signal becomes tempResult(31 downto 0) and the carry flag bit on the flag signal output becomes tempResult(32). Using a simple container module for clocking and I/O, I have synthesized and verified its operation on a Spartan7 board.

Link to post
Share on other sites
  • 0
49 minutes ago, Piasa said:

You can also import just "+" and "-" from std_logic_signed as well as the conversion functions from std_logic_arith.  This way you still are required to specify signed/unsigned for "<", "*", etc...  Importing "-" from std_logic_signed will give you the unary "-", which can be used for logical induction in expressions like (-x) and (x).

Very good to know! I feel like there are a million little things I don't know about VHDL, and the worst part is that I don't know what I don't know, haha.

Link to post
Share on other sites
  • 0
6 minutes ago, CurtP said:

tempResult is a 33-bit variable, used for convenience within the process. Its use is appropriately expanded upon elaboration and synthesis. It isn't used to register a value between cycles. The result signal becomes tempResult(31 downto 0) and the carry flag bit on the flag signal output becomes tempResult(32). Using a simple container module for clocking and I/O, I have synthesized and verified its operation on a Spartan7 board.

There are a lot of things that you can do and not have problems for any given entity... if you fully understand what you are doing. There are hours or days of debugging ahead for those who don't when all of a sudden what seemed to be good practice for one project doesn't work out so well on another. Especially when entities become components in large hierarchical designs.

More advice that you should feel free to ignore. I spent the first couple of years restricting my HDL to basic constructs and still had plenty of surprises to learn about. As time wore on I became more adventurous. Not everyone need operate this way... but probably more than do. You will, no doubt , find a coding style that suits the way that you work. Using individual operators from libraries would probably not work out for me as it does Piasa.

Link to post
Share on other sites
  • 0
17 minutes ago, zygot said:

@xc6lx45,

You must like experimenting with the reaction of Africanized bee colonies to jack-hammers. I'm in a feisty mood today so what the heck...

Laughing out loud. I thought, gate crashing the local motorcycle club with a "Harley s*cks" T-shirt.

Labor of love, that's the point. Sometimes it's just healthy if someone states the obvious.

And Zynq, agree fully. But that's essentially an ASIC part on the die.

 

Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now