Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
The FPGA Programming Handbook

You're reading from   The FPGA Programming Handbook An essential guide to FPGA design for transforming ideas into hardware using SystemVerilog and VHDL

Arrow left icon
Product type Paperback
Published in Apr 2024
Publisher Packt
ISBN-13 9781805125594
Length 550 pages
Edition 2nd Edition
Languages
Tools
Arrow right icon
Authors (2):
Arrow left icon
Guy Eschemann Guy Eschemann
Author Profile Icon Guy Eschemann
Guy Eschemann
Frank Bruno Frank Bruno
Author Profile Icon Frank Bruno
Frank Bruno
Arrow right icon
View More author details
Toc

Table of Contents (17) Chapters Close

Preface 1. Introduction to FPGA Architectures 2. FPGA Programming Languages and Tools FREE CHAPTER 3. Combinational Logic 4. Counting Button Presses 5. Let’s Build a Calculator 6. FPGA Resources and How to Use Them 7. Math, Parallelism, and Pipelined Design 8. Introduction to AXI 9. Lots of Data? MIG and DDR2 10. A Better Way to Display – VGA 11. Bringing It All Together 12. Using the PMOD Connectors – SPI and UART 13. Embedded Microcontrollers Using the Xilinx MicroBlaze 14. Advanced Topics 15. Other Books You May Enjoy
16. Index

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. The logic 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 and z 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 how x 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 the reg type. This was a confusing type to new HDL designers as they would see reg and think it was short for register. In fact, the reg type references any signal originating from an always block, even though always blocks could be used to generate combinational logic, as we’ll see shortly. Although reg can still be used for backward compatibility, you would be better off using logic or bit, which can be used in both assign statements and always blocks. The logic type also allows for the propagation of x through a design. This can be helpful for debugging startup conditions.

  • bit: The bit type uses less memory to store than logic, 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 bits
  • shortint: 16 bits
  • int: 32 bits
  • longint: 64 bits

    These types are less often used because it’s usually easier to specify the exact size of signals using logic or bit.

    The differences between bit and logic 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, and integer. 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 as std_logic_vector or signed and unsigned, are not part of the core language. Let’s look at these packages:

  • std_logic and std_logic_vector: Roughly equivalent to logic, this type is the one that is generally used. std_logic represents a single bit while std_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 separate boolean type. In SystemVerilog, 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 the integer data type is implementation-defined, but it is guaranteed to be at least a 32-bit signed integer. Unlike SystemVerilog, 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 and unsigned: VHDL has dedicated types for explicitly signed and unsigned operations, unlike SystemVerilog, where signed and unsigned 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:

$dimensions(my_array)

4.

$left(my_array, [dimensions])

[1] = 1023,[2]=767,[3]=2,[4]=7.

$right(my_array, [dimensions])

0 for all the dimensions.

$high(my_array, [dimensions])

Largest value in the dimension’s range.

$low(my_array, [dimensions])

Smallest value in the dimension’s range.

$size(my_array, [dimensions])

=$high(my_array, [dimensions]) - $low(my_array, [dimension]) + 1.

$increment(my_array, [dimensions])

Returns 1 if $left >= $right; otherwise, -1. Useful for for loops.

$bits()

Returns the number of bits used by a particular variable or expression. This is useful for passing size information to instantiations.

$clog2()

Returns the size of an array that can hold that number of items, not the value that was passed. For example, $clog(4) returns 2, which can store four values, 0 to 3.

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:

my_array'high[(dimension)]

Equivalent to $high.

my_array'low[(dimension)]

Equivalent to $low.

my_array'left[(dimension)]

Equivalent to $left.

my_array'right[(dimension)]

Equivalent to $right.

my_array'range[(dimension)]

Range of dimension (left to right). Used for looping.

my_array'reverse_range[(dimension)]

Range of dimension (right to left). Used for looping.

my_array'length[(dimension)]

Number of elements of array dimension.

my_array'ascending[(dimension)]

True if the range of array dimension is defined with to.

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 Fs, 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

2'b00

0

0

2'b01

1

1

2'b10

2

-2

2'b11

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 of i before using it
  • Post-increment, i++, increments i after using it
  • Pre-decrement, --i, increments the value of i before using it
  • Post-decrement, i--, increments i 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.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image