Beyond STD_LOGIC
In the last post we just started to scratch the surface of VHDL types. This time we will try to go deeper - which types should we use, what are their properties and limitations, what are the main pitfalls a beginner would encounter and how to solve them.
We have seen that while the basic type for software programming is INTEGER, of various but fixed sizes, signed or unsigned, the VHDL fundamental type is the 1-bit STD_LOGIC. It has 9 possible values, some of which would make sense for simulations but for actual synthesis we only care about two of them, '0' and '1'. By the way, if you haven't discovered that yet, STD_LOGIC values are actually CHARACTER, that's why we have to use the single quotes. In the end, hardware synthesis uses Boolean logic operators acting on STD_LOGIC signals that can take values of '0' and '1'.
The next step up climbing the complexity ladder is STD_LOGIC_VECTOR, an array of arbitrary size with the base type STD_LOGIC. Many VHDL designers simply stop here and never use any other types in their designs. While there is nothing fundamentally wrong with this, it's like driving a race car in first gear only.
INTEGER is not a good choice for hardware design
Another early trap that people coming from a programming background can quickly fall into is the INTEGER type. It looks familiar and people like to keep to the beaten path but using INTEGER signals in VHDL is actually a bad design choice and should be avoided, even if that looks painful initially. The reason is that INTEGER is a fixed, 32-bit size type. The actual signal you may need could be a 10-bit counter but the synthesis tool has no way of knowing that and you may end up using three times more resources than you actually need. While in software land you probably do not care about such waste, in hardware land that can quickly turn into a very costly proposition. The hardware that will run your software program, CPU and memory, is already paid for and probably sits idle on your desktop anyway so who cares that you use a 32-bit CPU and 4 bytes of RAM to implement a 10-bit counter. However, when you do hardware design you (or even worse, your customers) will pay for every FF and logic gate so it makes a lot of sense to produce very efficient designs.
On the other hand, if you need a 48-bit counter, describing that with 32-bit INTEGERs becomes rather difficult and error prone. So for these two reasons the accepted wisdom in the VHDL community is that signals of INTEGER type should avoided in general, which is why people use STD_LOGIC_VECTORs to represent multi-bit values of any arbitrary size.
STD_LOGIC_VECTOR is not a good choice for integer arithmetic
However, integer arithmetic, one of the most important things people use hardware designs for is difficult to do with STD_LOGIC_VECTORs and fixed point arithmetic is virtually impossible for the reasons mentioned in the previous post. So whenever you need to do integer arithmetic of the signed or unsigned kind you should use the IEEE.numeric_std.all package which will give you access to SIGNED and UNSIGNED types as well as a lot of very useful operators and conversion functions. It is really worth taking a peek at the source code for this package, which can be found doing a file search for numeric_std.vhd in the Vivado install folder (any tool that either simulates or synthesizes VHDL will contain the source code for this package and many others). You can see exactly what operators and conversion functions are available for these types and more importantly you can see how these are defined and get ideas about how you can then create your own useful types, operators and functions. Many beginner questions are of the type "how do I convert this type into this other type because I get errors when I try to add STD_LOGIC_VECTOR to INTEGER" and the answer is "you should not add STD_LOGIC_VECTOR and INTEGER in the first place but if you really need to do that use the right type conversion functions or just create your own".
If as a beginner you learn one thing from this blog post it should be this - never do integer arithmetic using STD_LOGIC_VECTOR type operands. The SIGNED and UNSIGNED types in the numeric_std package are the ones you should use instead.
If you look at the source file for the "+" operator for example in numeric_std.vhd you can discover that you can add two SIGNED or UNSIGNED operands of different sizes, the shorter one is resized to have the same range as the longer one and the result also has that same length, with any overflows ignored, the result just wraps around. This behavior is ideal for implementing binary counters for example, not so much for doing actual integer additions. If you do not want overflow errors you have to resize first the largest operand by sign extending it with one MSB and then you will get the expected result all the time.
Conversion blues and why verbosity is good for you
Even if you use SIGNED/UNSIGNED instead of STD_LOGIC_VECTOR and maybe BOOLEAN instead of STD_LOGIC when it makes sense to do so the fact remains that there will be a lot of STD_LOGIC and STD_LOGIC_VECTOR signals in any VHDL design and you will need to convert between these types a lot. VHDL is a strong typed language where type incompatibilities are enforced to protect you from your own insidious mistakes which are very often hard to find and fix.This makes VHDL rather verbose, which is argument #1 that people cite for disliking VHDL. VHDL designers spend their time typing code (or using templates and a lot of cut and paste if they are smart), Verilog designers spend their time chasing the bugs they inadvertently created by typing faster than they can think. This book is not cheap but hey, your time is not cheap either so if you fall into the second camp you might be interested in:
I did search for a similar book on VHDL coding traps but I was not able to find one, which probably says something.
So, verbosely or not, in VHDL you can easily convert anything into anything else using either language built in features like typecasting, existing package functions or if push comes to shove your own user created functions.
While STD_LOGIC_VECTOR, SIGNED and UNSIGNED are clearly related, all three are arrays of STD_LOGIC under the hood, they are incompatible with each other and you cannot directly assign between them. Similarly, INTEGER and REAL are not directly compatible with each other and cannot be mixed in expressions. But being closely related means that you can use type casting so things like this are OK:
signal SLV:STD_LOGIC_VECTOR(7 downto 0);
signal S:SIGNED(7 downto 0);
signal U:UNSIGNED(7 downto 0);
signal I:INTEGER; signal R:REAL;
begin
SLV<=STD_LOGIC_VECTOR(S); – SIGNED or UNSIGNED to STD_LOGIC_VECTOR typecast
S<=SIGNED(SLV); – STD_LOGIC_VECTOR to SIGNED typecast
U<=UNSIGNED(SLV); – STD_LOGIC_VECTOR to UNSIGNED typecast
S<=SIGNED(U); – UNSIGNED to SIGNED typecast
U<=UNSIGNED(S); – SIGNED to UNSIGNED typecast
R<=REAL(I); – INTEGER to REAL typecast
I<=INTEGER(R); – REAL to INTEGER typecast
Conversion between unrelated types is trickier but still possible. Here are some examples:
signal B:BOOLEAN;
signal SL:STD_LOGIC;
signal S:SIGNED(7 downto 0);
signal U:UNSIGNED(7 downto 0);
signal I:INTEGER;
begin
B<=SL='1'; – STD_LOGIC to BOOLEAN
SL<='1' when B else '0'; – BOOLEAN to STD_LOGIC
I<=TO_INTEGER(S); – SIGNED or UNSIGNED to INTEGER conversion function
S<=TO_SIGNED(I,S'length); – INTEGER to SIGNED conversion function
U<=TO_UNSIGNED(I,U'length); – INTEGER to UNSIGNED conversion function
Of course, the left hand and the right hand sides of an assignment must be the same size, did I mention that VHDL is strong typed? So after you take care of the type conversion, you have to make sure the sizes match. Here is a very common situation that occurs when doing full precision integer addition, when the result must be one bit larger than the operands:
signal A,B:SIGNED(7 downto 0);
signal S:SIGNED(8 downto 0); – S has to be one bit larger than A and B
signal UA,UB:UNSIGNED(7 downto 0);
signal US:UNSIGNED(8 downto 0); – US has to be one bit larger than A and B
begin
S<=(A(A'high)&A)+(B(B'high)&B); – Integer addition without overflow by sign extending A and B
US<=("0"&A)+("0"&B); – Sign extension for SIGNED and UNSIGNED types is different
Note that sign extension is different for SIGNED and UNSIGNED types. If you need to sign extend by more than one bit this approach becomes cumbersome. However, there is a more elegant solution which uses the numeric_std RESIZE function and an auxiliary Max function. This code works without any changes, no matter what the actual sizes of the two operands are and it is the same for both SIGNED and UNSIGNED operands:
signal A:SIGNED(7 downto 0);
signal B:SIGNED(3 downto 0);
signal S:SIGNED(Max(A'length,B'length) downto 0); – S has to be one bit larger than Max(A'length,B'length)
signal UA:SIGNED(7 downto 0); signal UB:SIGNED(3 downto 0);
signal US:SIGNED(Max(UA'length,UB'length) downto 0); – US has to be one bit larger than Max(UA'length,UB'length)
begin
S<=RESIZE(A,S'length)+RESIZE(B,S'length); – SIGNED Integer addition without overflow no matter what the sizes of A and B are
US<=RESIZE(UA,US'length)+RESIZE(UB,US'length); – UNSIGNED Integer addition without overflow no matter what the sizes of UA and UB are
B<=RESIZE(A,B'length); – the 4 MSBs of A are dropped before assigning A to B
A<=RESIZE(B,A'length); – B is sign extended by 4 bits before assigning B to A
RESIZE works on both SIGNED and UNSIGNED operands, returning a result of the same type. The second argument of the RESIZE function call tells the result size, if the argument must be shortened MSBs are dropped, if it must be lengthened it is sign extended, which is different for the SIGNED and UNSIGNED cases - in the first case the MSB is replicated, in the second case '0' MSBs are added.
So another important lesson to learn is that unlike Verilog, VHDL is a strong typed language, when you do an assignment both left and right hand sides must have the same type and the same size..When doing integer arithmetic you will find yourself doing a lot of type conversions through typecasts and type conversion functions as well as using RESIZE a lot. This will lead to code verbosity, which is the price you have to pay for not wasting your time later on debugging your own mistakes, typos and bugs.
The numeric_std SIGNED and UNSIGNED types take care of integer arithmetic, with both ways of treating bit growth overflows - either ignore the overflows through wraparound or address them by increasing the operand sizes using the RESIZE function. In the next post I will take a look at another important case, arithmetic with fixed point numbers.
Back to the top: The Art of FPGA Design