Understanding the basics of HDL design
All computer programming languages need variables. A CPU with a running program can access stored values in physical memory or registers. Hardware Description Languages (HDLs) are a little different than the usual programming languages in that you are building hardware. There are variable equivalents in terms of storage/sequential logic, which we’ll discuss in the next chapter, but we also need wires to move data around the hardware we’re building using the FPGA routing resources, even if they are never stored:
Figure 3.2: Program flow versus HDL flow
In a traditional flow, as shown on the left of Figure 3.2, you have a computer that has a processor and memory. The program flows linearly. Note that with modern machines, there are increasing levels of parallelism, but physical parallelism remains minimal compared to what can be possible in custom hardware such as an FPGA.
When you write HDL
code, you are using data types to create hardware that will store or move data around physically to and from Lookup Tables (LUTs), DSP elements, BRAMs, IO, or other device resources. If you need a large amount of memory, you’ll need to implement the external memory interface to communicate with it, which is something we will introduce in Chapter 9, Lots of Data? MIG and DDR2.
Introducing data types
SystemVerilog
has multiple built-in types, but the most interesting ones for design are the logic
and bit
types:
logic
: We used this type in the previous chapter. Thelogic
type can represent a 0, 1, x (undefined or treated as don’t care, as we’ll see shortly), or z (tri-state or also a don’t care).x
andz
are really sub bullets of logic,x
is a value that represents an undefined or don’t care value. When used as an undefined value in simulation, it will propagate through logic and can be used to find problems in your design. When used as a don’t care value, it can allow synthesis tools to optimize designs for better area/performance. We’ll see an example of optimization usage in Chapter 12, Using the PMOD Connectors – SPI and UART.z
is a value used for tri-state signals. This is used to allow multiple drivers to access a shared resource. We will see how this is used in the Handling multiple-driven nets section.z
can also be used to help logic optimization, like howx
is used, which we will examine in Chapter 10, A Better Way to Display – VGA.If you’ve ever used
verilog
, you will know of thereg
type. This was a confusing type to new HDL designers as they would seereg
and think it was short for register. In fact, thereg
type references any signal originating from analways
block, even thoughalways
blocks could be used to generate combinational logic, as we’ll see shortly. Althoughreg
can still be used for backward compatibility, you would be better off usinglogic
orbit
, which can be used in bothassign
statements andalways
blocks. Thelogic
type also allows for the propagation ofx
through a design. This can be helpful for debugging startup conditions.
bit
: Thebit
type uses less memory to store thanlogic
, but it can only store a 0 or 1. This allows lower memory usage and potentially faster simulation at the expense of tracking undefined values.
There are also four other lesser-used two-state types:
byte
: 8 bitsshortint
: 16 bitsint
: 32 bitslongint
: 64 bitsThese types are less often used because it’s usually easier to specify the exact size of signals using
logic
orbit
.The differences between
bit
andlogic
are purely related to how they behave in simulation. Both types will generate the same logic and storage elements in hardware. All the other types only differ in size or default sign representation.VHDL has a limited number of built-in data types, such as
bit
,boolean
, andinteger
. You’ll notice we have used a few packages in all our VHDL code so far. That is because the data types we need for logic design, such asstd_logic_vector
orsigned
andunsigned
, are not part of the core language. Let’s look at these packages:
std_logic
andstd_logic_vector
: Roughly equivalent tologic
, this type is the one that is generally used.std_logic
represents a single bit whilestd_logic_vector
is used for buses. There are a few additional logic levels, such as weak pull up or pull down, but they are only useful in modeling.Like
SystemVerilog
, VHDL provides a way of setting all bits in a vector to a value:my_sig <= (others => '0');
boolean
: VHDL, being strongly typed, has a separateboolean
type. InSystemVerilog
, a 0 would be false and a 1 true, but Booleans are required pre-VHDL-2008.VHDL 2008 has relaxed some of the type checking. For instance, there is now automatic type conversion in
if
statements. Pre-VHDL 2008, if you wanted to check a single bit, you would have to compare it to a value so it would become a Boolean operation, rather than just simply testing the value directly.
integer
: Integers are commonly also used in VHDL. The range of theinteger
data type is implementation-defined, but it is guaranteed to be at least a 32-bit signed integer. UnlikeSystemVerilog
, integers can have a defined range allowing optimization and error checking.The tip box refers to the integer bullet point.
Ex:
signal my_sig : integer range 0 to 15; -- Create a 4 bit wide value
signed
andunsigned
: VHDL has dedicated types for explicitly signed and unsigned operations, unlikeSystemVerilog
, wheresigned
andunsigned
are operators on a data type.
With that, we’ve looked at the basic types. But what if we need to deal with different sizes of data or more data than the types can handle?
Creating arrays
The reason that byte
, shortint
, int
, and longint
are not used as much in SystemVerilog is because, typically, you will size your signals as needed; for example:
bit [7:0] my_byte; // define an 8 bit value
Here, my_byte
is defined as a packed 8-bit value. It’s possible to also create an unpacked version:
bit my_byte[8]; // define an 8 bit value
Packed versions have the advantage of slicing into arrays in SystemVerilog, while unpacked versions have the advantage of inferring memories, as we’ll discuss in Chapter 6, FPGA Resources and How to Use Them.
Arrays can also have multiple dimensions:
bit [2:0][7:0] my_array[1024][768]; // define an 8 bit value
// 3 4 1 2 Array ordering
The ordering of the array is defined in the preceding code. The following are valid ways to access the array:
my_array[0][0] Returns a value of size [2:0][7:0]
my_array[1023][767][2] Returns an 8 bit value
Defining an array can be done using a range, such as [7:0]
, or the number of elements, such as [1024]
, which is zero-indexed and equivalent to [0:1023]
; bear this in mind since it will come up if you use this style to create arrays of instances.
VHDL also provides a mechanism for creating multidimensional arrays. We can create new types based on other types:
type array_3d is array (natural range <>, natural range <>) of std_logic_vector;
signal my_array : array_3d(31 downto 0, 31 downto 0)(31 downto 0);
my_array(0, 0) Returns a value of size (31 downto 0)
Querying arrays
SystemVerilog
provides system functions for accessing array information. As we’ll see in this project, this allows reusable code.
The dimension
parameter is optional and defaults to 1
.
This becomes even more important when we want to implement type parameters:
|
4. |
|
|
|
0 for all the dimensions. |
|
Largest value in the dimension’s range. |
|
Smallest value in the dimension’s range. |
|
|
|
Returns |
|
Returns the number of bits used by a particular variable or expression. This is useful for passing size information to instantiations. |
|
Returns the size of an array that can hold that number of items, not the value that was passed. For example, |
Table 3.1: SystemVerilog array querying
These system functions allow us to query an array to get its parameters. VHDL has a similar set of predefined attributes that can be used to create more reusable code:
|
Equivalent to |
|
Equivalent to |
|
Equivalent to |
|
Equivalent to |
|
Range of dimension (left to right). Used for looping. |
|
Range of dimension (right to left). Used for looping. |
|
Number of elements of array dimension. |
|
True if the range of array dimension is defined with |
Table 3.2: VHDL array querying
Assigning to arrays
When we want to assign a value to a signal defined as an array, we should size it properly to avoid warnings. In the original Verilog specification and tools, if we didn’t specify a size, then the size would default to 32 bits. Today, we simply want to avoid any warnings during simulation and synthesis as a standard build will already generate thousands of warnings and it becomes human nature to ignore them, which can lead to potentially missing important ones.
In SystemVerilog
, there are four ways we can assign without providing a size: '1
assigns all bits to 1
, '0
assigns all bits to 0
, 'z
assigns all bits to z
, and, less useful, 'x
assigns all bits to an unknown or don’t care value. If we have a single packed dimension, we can use n'b
to specify a binary value of n
bits, n'd
to specify a decimal value of n
bits, or n'h
to specify a hex value of n
bits:
SystemVerilog
logic [63:0] data;
assign data = '1; // same as data = 64'hFFFFFFFFFFFFFFFF;
assign data = '0; // same as data = 64'd0;
assign data = 'z; // same as data = 64'hzzzzzzzzzzzzzzzz;
assign data = 0; // data[31:0] = 0, data[63:32] untouched (Verilog-97).
It’s important to remember that n
in these cases is the number of bits represented, not the number of digits. For example, since the character F
in hex is represented by 4 bits, you would need 16 F
characters to represent all ones. If you used fewer than 16 F
s, the number would be set to a smaller value. For example, 64'hFF
would simply represent a value of 255 using 64 bits.
n'h
represents 4-bit hex characters utilizing a total of n bits. 0-F all represent 4 bits.
n'd
represents a decimal value utilizing a total of n
bits. 0-9 are valid digits, but the result is limited to n
bits.
n'o
represents 3-bit octal values where 0-8 are valid values.
n'b
represents 1-bit binary values where 0-1 represent each bit position.
VHDL
signal data : std_logic_vector(63 downto 0);
data <= (others => '1'); -- same as data <= 64b"11...11"
data <= (others => '0'); -- same as data <= 64b"00...00"
data <= (others => 'Z'); -- same as data <= 64b"ZZ...ZZ"
Now that we’ve seen types, arrays, array querying, and assignments, let’s take a quick look at multiple driven nets.
Handling multiple-driven nets
There is one other type in SystemVerilog
that deserves to be mentioned, although we will not be using it for a while. This is a wire
. The wire
type represents 120 different possible values, that is, the four basic values – 0
, 1
, x
, and z
– and drive strengths.
The wire
type has what is known as a resolution function. Wire types are the only signals that can be connected to multiple drivers. We will see this when we introduce the Serial Peripheral Interface (SPI) protocol and access the DDR2 memory on the Nexys A7 board:
Figure 3.3: Tri-state example
FPGAs, in general, do not have internal tri-state capabilities. The preceding example shows two devices each with tri-state Input/Output (I/O) buffers connected:
logic [1:0] in;
logic [1:0] out;
logic [1:0] enable;
tri1 R_in;
assign R_in = (enable[0]) ? out[0] : 'z;
assign R_in = (enable[1]) ? out[1] : 'z;
assign in[0] = R_in;
assign in[1] = R_in;
The preceding code demonstrates how the two tri-state buffers are constructed. tri1
is a testbench construct where a signal is declared as a tri-state with a weak pullup to 1
.
We have introduced a new operator, the conditional operator "out = a ? b : c"
. In this case, if a
is '1'
then out would be assigned the value of b
, otherwise out
will receive the value of c
. This can be viewed as a two-input multiplexor.
If you are using std_logic
and std_logic_vector
, the signals have resolution functions and can be used in the same way as wires in SystemVerilog
. The equivalent in VHDL would look like:
signal s_in : std_logic_vector(1 downto 0);
signal s_out : std_logic_vector(1 downto 0);
signal enable : std_logic_vector(1 downto 0);
signal r_in : std_logic;
r_in <= s_out(0) when enable(0) else 'Z';
r_in <= s_out(1) when enable(1) else 'Z';
s_in(0) <= r_in;
s_in(1) <= r_in;
Now let’s take a look at signed
and unsigned
numbers.
Handling signed and unsigned numbers
Verilog had just one signed signal type, integer
. SystemVerilog
allows us to define both unsigned
and signed
numbers explicitly for any built-in type:
bit signed [31:0] signed_vect; // Create a 32 bit signed value
bit unsigned [31:0] unsigned_vect; // create a 32 bit unsigned value
When performing signed arithmetic, it’s important to make sure the sizing is correct. Also, when computing with signed numbers, you should make sure all the signals involved are signed so that the correct result is obtained. The table below shows an example of unsigned
vs signed
representation.
Value |
2’s complement (signed) |
Signed |
|
0 |
0 |
|
1 |
1 |
|
2 |
-2 |
|
3 |
-1 |
Table 3.3: Signed versus unsigned example
In Table 3.3, we can see an example of a signed
and unsigned
two-bit number. The note below describes 2’s complement math.
Digital logic, such as computer processors or FPGA implementations, use 2’s complement to represent signed numbers. What this means is that to negate a number, you simply invert it and add 1. For example, to get -1
in 2’s complement, assuming there are 4 bits for representation, we would take 4'b0001
, invert it to get 4'b1110
, and add 1, resulting in 4'b1111
. Bit 3 is the sign bit, so if it’s 0, the number is positive, and it’s negative if it’s 1. This also means that the maximum number of signed values that we can represent by using 4 bits is 4'b0111
or +7 and 4'b1000
or -8.
VHDL allows us to create signed and unsigned vectors:
variable a_in : signed(BITS-1 downto 0);
variable b_in : unsigned(BITS-1 downto 0);
Because VHDL is strongly typed, the result size must be correct to prevent errors.
Adding bits to a signal by concatenating
SystemVerilog
provides a powerful concatenation function, {}
. We can use it to add bits or signals to create larger vectors, or for replication. When casting an unsigned
integer to a signed
integer, typically, you’ll want to use the concatenation operator, {}
, to prepend 1'b0
to the signed
bit so that the resulting signed
value remains positive because the signed
bit is forced to 0
. The concatenation operator can be used to merge multiple signals together, such as {1'b0, unsigned_vect}
. It can also be used to replicate signals. For example, {2{unsigned_vect}}
would be equivalent to {unsigned_vect, unsigned_vect}
.
VHDL also provides a concatenation function, &
:
"1" & unsigned_vect
unsigned_vect & unsigned_vect
The only equivalent to replication, i.e., {2{unsigned_vect}}
is "unsigned_vect" & "unsigned_vect"
.
Casting signed and unsigned numbers
You can cast an unsigned
number to a signed
number by using the signed'
keyword, and can cast a signed
number to an unsigned
number by using the unsigned'
keyword:
logic unsigned [15:0] unsigned_vect = 16'hFFFF;
logic unsigned [15:0] final_vect;
logic signed [16:0] signed_vect;
logic signed [15:0] signed_vect_small;
assign signed_vect = signed'({1'b0, unsigned_vect}); // +65535
assign signed_vect_small = signed'(unsigned_vect); // -1
assign unsigned_vect = unsigned'(signed_vect);
assign final_vect = unsigned'(signed_vect_small); // 65535
Here, you can see that an unsigned
16-bit number can go from 0 to 65535. A 16-bit signed
number can go from -32768 to 32767, so if we assign a number larger than 32767, it would have its signed
bit set in the same-sized signed number, causing it to become negative.
These are equivalent to the Verilog system functions; that is, $signed()
and $unsigned()
. However, it’s preferable to use the casting operators.
VHDL provides similar casting functions as well as a resize
function, which is important due to strict typing rules. For example, from the add_sub
:
a_in := resize(signed(SW(BITS-1 downto BITS/2)), BITS);
When casting signed
to unsigned
or unsigned
to signed
, pay attention to sizing. For example, to maintain the positive nature of unsigned
, typically, you’ll use the concatenation operator, {}
or &
, as in signed({1'b0, unsigned_vect})
, which means the resulting signal will be 1 bit larger. When going from signed
to unsigned
, care must be taken to ensure that the number is positive; otherwise, the resulting assignment will not be correct. You can see an example of mismatched assignments in the preceding code, where signed_vect_small
becomes -1 rather than 65535 and final_vect
becomes 65535, even though signed_vect_small
is -1.
Creating user-defined types
We have seen previously that to create arrays in VHDL, we could create a new type. SystemVerilog
provides a similar capability.
We can create our own types using typedef
. A common example that’s used in SystemVerilog
is to create a user-defined type for speeding up simulations. This can be done by using a define:
`ifdef FAST_SIM
typedef bit bit_t
`else
typedef logic bit_t
`endif
If FAST_SIM
is defined, then any time we use bit_t
, the simulator will use bit
; otherwise, it will use logic
. This will speed up simulations.
It is a good idea to adopt a naming convention when creating types – in this case, _t
. This helps you identify user-defined types and prevent confusion when using the type within your design.
Accessing signals using values with enumerated types
When it comes to readability, it’s often preferable to use variables with values that make more sense and are self-documenting. We can use enumerated types in SystemVerilog
to accomplish this, like so:
enum bit [1:0] {RED, GREEN, BLUE} color;
In VHDL, it would be like:
type color_t is (RED, GREEN, BLUE);
signal color : color_t;
In this case, we are creating a variable, color
, made up of the values RED
, GREEN
, and BLUE
. Simulators will display these values in their waveforms. We’ll discuss enumerated types in more detail in Chapter 4, Counting Button Presses.
Packaging up code using functions
Often, we’ll have code that we will be reusing within the same module or that’s common to a group of modules. We can package this code up in a function in SystemVerilog:
function [4:0] func_addr_decode(input [31:0] addr);
func_addr_decode = '0;
for (int i = 0; i < 32; i++) begin
if (addr[i]) begin
return(i);
end
end
endfunction
We can accomplish the same thing in VHDL:
function func_addr_decoder(addr : std_logic_vector(31 downto 0)) return integer is
begin
for i in addr'low to addr'high loop
if addr(i) then
return i;
end if;
end loop;
return 0;
end function func_addr_decoder;
Here, we created a function
called func_addr_decode
that returns a 5-bit value in SystemVerilog
($clog2
of 32) or an integer in VHDL. This address decoder function takes a 32-bit input called addr
(abbreviation for address). Functions in SystemVerilog
can have multiple outputs, but we will not be using this feature. To return the function’s value in either language, you can use return
. In SystemVerilog
, you can also assign the result to the function name.
SystemVerilog provides a convenient mechanism for getting the log base 2, or the number of bits required to index into a particular vector. This is the $clog2
function.
In VHDL, we can generate the equivalent using natural(ceil(log2(real(BITS))))
. These mechanisms are particularly useful for creating reusable code.
Creating combinational logic
The first way of creating logic is via assign
statements in SystemVerilog or an assignment in VHDL within the architecture, but outside of a process block (we’ll refer to this as an assign
for simplicity). assign
statements are convenient when creating purely combinational logic with only a few terms. This is not to say the resulting logic will necessarily be small. For instance, you could create a large multiply
accumulator using a single line of code, or large combinational structures by utilizing an assign
statement and calling a function:
assign mac = (a * b) + old_mac;
assign addr_decoder = func_addr_decode(current_address);
In VHDL:
--outside of a process block
mac <= (a * b) + old_mac;
addr_decoder <= func_addr_decode(current_address);
--process block example
example : process is
The second way of creating logic is an always
or process
block allows for more complex functionality to be defined in a single process. We looked at always
and process
blocks in the previous chapter. There, we were using a sensitivity list in the context of a testbench. Sensitivity lists allow an always
or process
block to only be triggered when a signal in the list changes. Let’s look back at the testbench that was provided in Chapter 2, FPGA Programming Languages and Tools:
always @(LED) begin
In this example, the always
block would only be triggered when LED
transitions from one state to another.
Sensitivity lists are not synthesizable and are only useful in testing. always_comb
in SystemVerilog
or process(all)
is recommended when describing synthesizable combinational logic in an always
block.
When we write synthesizable code using an always
block in SystemVerilog
, we use the always_comb
structure. This type of code is synthesizable and recommended for combinational logic. The reason is that always_comb
will create a warning or error if we inadvertently create a latch. In Verilog 2001 or earlier, always_comb
isn’t available. VHDL-2008 allows us to use process(all)
to trigger on any signal in the process block that changes but doesn’t enforce combinational only logic like always_comb
.
A note about latches: They are a type of storage element. They are level-sensitive, meaning that they are transparent when the gating signal is high, but when the gating signal transitions to low, the value is held. Latches do have their uses, particularly in the ASIC world, but they should be avoided at all costs in an FPGA as they almost always lead to timing problems and random failures. We will demonstrate how a latch works and why it can be bad as part of this chapter’s project.
There are a few different operations that can go within an always/process
block. Since we are generating combinational logic, we must make sure that all the possible paths through any of these commands are covered. We will discuss this later.
Handling assignment operators
There are two basic types of assignments in SystemVerilog
: blocking and non-blocking.
In VHDL, signals are non-blocking; however, local variables within a process
can only be assigned in a blocking way, like using =
in SystemVerilog
.
Blocking versus non-blocking is a very important concern when designing hardware. Because we are writing in an HDL, we need to be able to model the hardware we are creating. All the hardware you design will be effectively running in parallel inside the FPGA. This requires a different mindset than software, which predominantly runs serially.
Creating multiple assignments using non-blocking assignments
In hardware, whenever you create multiple always/process
blocks, they are all executing at the same time. Since this is effectively impossible on a normal computer running a program linearly or, at best, a few threads in parallel, we need a way to model this parallel behavior. Simulators accomplish this by using a scheduler that splits up simulation time into delta event cycles. This way, if multiple assignments are scheduled to happen, there is still a linear flow to them. This makes handling blocking and non-blocking assignments critical.
A non-blocking assignment is something that is scheduled to occur after delta events occur and when the simulator’s time advances. These signals are used in sequential logic. We will discuss non-blocking in more detail in Chapter 4, Counting Button Presses.
Using blocking assignments
Blocking assignments occur immediately. With rare exceptions, usually only with regard to testbenches, all assignments within an always_comb
block or a non-clocked process will be blocking.
In VHDL, only a variable assignment :=
is blocking.
There are several additional blocking assignments in SystemVerilog
:
Figure 3.4: Additional blocking assignments in SystemVerilog
In SystemVerilog
, there are also some shortcuts for incrementing or decrementing signals.
Incrementing signals
Here’s a list of the shortcuts for incrementing:
- Pre-increment,
++i
, increments the value ofi
before using it - Post-increment,
i++
, incrementsi
after using it - Pre-decrement,
--i
, increments the value ofi
before using it - Post-decrement,
i--
, incrementsi
after using it
Now that we’ve learned how to manipulate values, let’s learn how to use these variables to make decisions.
Making decisions – if-then-else
One of the basics of any programming language is to control the flow through any operation. In the case of a HDL, this is generating the actual logic that will be implemented in the FPGA fabric. We can view an if-then-else
statement as a multiplexer, the conditional expression of the if
statement is the select lines. Let’s take a look at it in its simplest form:
SystemVerilog
if (add == 1) sum = a + b;
else sum = a - b;
VHDL
if add then
sum := a + b;
else
sum := a - b;
end if;
This will select whether b
will be added or subtracted from a
based on whether the add
signal is high. A simplified view of what could be generated is shown in the following diagram:
Figure 3.5: An if-then-else representation
Probably, the logic will be implemented in a much less expensive way. It’s worth looking at the results of your designs as they are built to understand the kind of optimizations that occur.
Comparing values
SystemVerilog
supports normal equality operations such as ==
and !=
. These operators check if two sides of a comparison are equal or not equal, respectively. Since we are dealing with hardware and there is the possibility of us having undefined values, there is a disadvantage to these operators in that x
's can cause a match, even if it’s not intended, by falling through to the else
clause. This is usually more of an issue in testbenches. There are versions of these operators that are resistant to x
's in SystemVerilog
; that is, ===
and !==
. In a testbench, it is advised to use these operators to avoid unanticipated matches.
VHDL provides the equivalent of =
and /=
. Due to the strong typing of VHDL, it doesn’t suffer from the same issues as SystemVerilog
.
Comparing wildcard equality operators (SystemVerilog)
It is also possible to match against ranges of values. This is possible using the =?=
and !?=
operators. They allow us to use wildcards in the match condition. For example, say you had a 32-bit bus but needed to handle odd-aligned addressing:
if (address[3:0] =?= 4'b00zz) slot = 0;
else if (address[3:0] =?= 4'b01zz) slot = 1;
The wildcard operators allow you to do this. The preceding examples would ignore the lower two bits.
VHDL don’t care
VHDL allows “don’t care” values in comparison to VHDL-2008:
if address(3 downto 0) ?= "00--" then
slot := 0;
elsif address(3 downto 0) ?= "01--" then
slot := 1;
end if;
Qualifying if statements with unique or priority (SystemVerilog)
Normally, when thinking of an if
statement, you think of each if
evaluation as a separate comparison relying on the if
statements that came before it. This type of if
statement is a priority, meaning that the first if
that matches will evaluate to true. In the simple example shown previously, we can see that we are looking at the same address and masking out the lowest two bits. Often, during optimization, the tool will realize that the if
statements cannot overlap and will optimize the logic accordingly. However, if we know this to be the case, we can use the unique
keyword to tell Vivado that each if
doesn’t overlap with any that come before or after. This allows the tool to better optimize the resulting logic during the synthesis stage. Care must be taken, however. Let’s see what would happen if we tried to do the following:
unique if (address[3:0] =?= 4'b00zz) slot = 0;
else if (address[3:0] =?= 4'b01zz) slot = 1;
else if (address[3:0] =?= 4'b1000) slot = 2;
else if (address[3:0] =?= 4'b1zzz) slot = 3;
Here, we can see that the last two else if
statements overlap. If we specify unique
in this case, we are likely to get a mismatch between simulation and synthesis. If address[3:0]
was equal to 4'b1000
during the simulation, the simulator would issue a warning that the unique
condition had been violated. Synthesis would optimize incorrectly, and the logic wouldn’t work as intended. We’ll see this when we violate unique
on a case
statement, when we work on this chapter’s project.
This type of if
is actually a priority, and if we wanted to, we could direct the tool, like so:
priority if (address[3:0] =?= 4'b00zz) slot = 0;
Priority is not really required except to provide clarity of intent. This is because the tool will usually be able to figure out if an if
can be optimized as unique. If not, it will be treated as priority by default.
Introducing the case statement (SystemVerilog)
A case
statement is typically used for making a larger number of comparisons. There are three versions of the case
statement you might use: case
, casex
, and casez
. The case
statement is used when wildcards are not necessary. If you want to use wildcards, as we saw previously, casez
is recommended. There are two ways case
statements are usually used. The first is more traditional:
casez (address[3:0])
4'b00zz: slot = 0;
4'b01zz: slot = 1;
4'b1000: slot = 2;
4'b1zzz: slot = 3;
endcase
Just like in the case of the if
statement, unique
or priority
can be used to guide the tool. Also, we can have a default
fall-through case that can be defined. This must be defined if unique
is used.
unique
and priority
are powerful tools in that they can greatly reduce the final logic’s area and timing. However, care must be taken as incorrectly specifying them can cause logic errors. Simulation will check that the conditions are not violated, but it will only detect cases that occur during simulation. They are only available in SystemVerilog
.
In SystemVerilog
, there is another way of writing a case
statement that can be especially useful:
priority case (1'b1)
address[3]: slot = 0;
address[2]: slot = 1;
address[1]: slot = 2;
address[0]: slot = 3;
endcase
In this particular case, we have created a leading-ones detector (LOD). Since we may have multiple bits set, specifying a unique modifier could cause optimization problems. If the design had one-hot encoding on address
, then specifying unique
would create a more optimized solution. VHDL doesn’t provide a mechanism to create this kind of case
statement.
There are different ways to encode data. Binary encoding can set multiple bits at the same time and is typically an incrementing value. One-hot encoding has one bit set at a time. This makes decoding simpler. There is also something we’ll explore when we discuss First-In-First-Out (FIFO), called gray coding, which is a manner of encoding that is impervious to synchronization problems when properly constrained.
For more simple selections, SystemVerilog
supplies a simple way of handling this.
Using the conditional operator to select data
SystemVerilog
provides a shortcut for conditionally selecting a result in the following form:
Out = (sel) ? ina : inb;
When sel
is high, ina
will be assigned to out
; otherwise, inb
will be assigned to out
.
Writing sel ?
…
is a shortcut for sel == 1'b1 ?
…
.
In VHDL, we have a similar function to the ?
operator:
r_out <= ina when sel else inb;
with sel select r_out <=
ina when '1',
inb when '0';
VHDL-2008 also allows wildcards with select
:
with sel select? r_out <=
ina when "1-",
inb when "-1";
Both of these operators can contain multiple when
clauses.
Introducing the case statement (VHDL)
In VHDL, we have a case
statement to operate on defined values:
case sel is
when '0' => LED <= MULT_LED;
when '1' => LED(natural(ceil(log2(real(BITS))))-1 downto 0) <= LO_LED;
end case;
We also have case?
in VHDL-2008, which allows us to use wildcards:
case? sel is
when "1----" => LED <= MULT_LED;
when "01---" => LED(natural(ceil(log2(real(BITS)))) - 1 downto 0) <= LO_LED;
when "001--" => LED(natural(ceil(log2(real(BITS)))) - 1 downto 0) <= NO_LED;
when "0001-" => LED <= AD_LED;
when "00001" => LED <= SB_LED;
when others => LED <= (others => '0');
end case?;
Note the use of others
above, which is the equivalent of default
in SystemVerilog
.
In this section, we’ve looked at basic data types and arrays and how to use them. In the next section, we’ll learn how to use custom data types more tailored to our designs.
Using custom data types
Both SystemVerilog
and VHDL
provide us with a variety of ways to create user-defined types. User-defined types can also be stored in arrays.
Creating structures
In this section, we’ll look at how we can create and use structures, unions, and records.
SystemVerilog
Structures allow us to group signals that belong together. For example, if we wanted to create a 16-bit value composed of two 8-bit values, h
and l
, we could do something like this:
typedef struct packed {bit [7:0] hi; bit [7:0] lo;} reg_t;
reg_t cpu_reg;
assign cpu_reg.hi = 8'hFE;
Here’s what the keywords signify:
typedef
signifies we are creating a user-defined type.struct
means we are creating a structure.packed
signifies the structure is to be packed in memory.
Structures and unions can be packed or unpacked, but as packed tends to make more sense in the context of hardware, it’s what we’ll use here.
We access parts of a structure by using the created signal by appending the part of the structure – in this case, h
– separated by a period.
VHDL
VHDL provides the record
type, which is equivalent to the SystemVerilog
structure:
type reg_t is record
HI : std_logic_vector(7 downto 0);
LO : std_logic_vector(7 downto 0);
end record reg_t;
Creating unions (SystemVerilog)
A union
allows us to create a variable with multiple representations. This is useful if you need multiple methods for accessing the same data. For instance, as microprocessors advanced from 8 bits to 16 bits, there needed to be ways to access parts of the register for older operations:
union packed {bit [15:0] x; reg_t cr;} a_reg;
always_comb begin
a_reg.x = 16'hFFFF;
a_reg.cr.h = '0;
end
In the preceding example, we created a union of a 16-bit register and a structure composed of two 8-bit values. After the first blocking assignment, a_reg
sets all bits to 1. After the second assignment, the upper 8 bits were set to 0, meaning a_reg
is 16'h00FF
.
VHDL doesn’t have an equivalent to the union.
With the basics behind us, we can take a look at our project on combinational logic.