Keywords

8.1. Design Assumptions

8.1.1. Synchronous Designs

Our working assumption is that the system we are creating is a synchronous digital system where all signals change in lockstep to the system clock. This is why you should always use the rising_edge() function in your VHDL designs when you create a process in VHDL. You should get into the habit of always writing if rising_edge(clk) right after begin in a process(clk) statement as shown in Listing 8.1 (unless you have a very good reason to do otherwise).

Listing 8.1 Synchronous VHDL process using the rising_edge() function. You should always write if rising_edge(clk) right after begin in a process(clk) statement

8.1.2. Hierarchical Designs

We are assuming a hierarchical design process where larger components are comprised of smaller ones. If you waited till the very end and your complicated system did not work, where would you find the error? It would be hard to find. Systems using SoC FPGAs are even more complex and harder to debug than your typical computer system because you are developing both the underlying hardware and the software that uses your hardware. If the system breaks, the problem could exist in your hardware or in the software using your hardware. Thus to reduce the time you spend debugging, it is important to verify that your building blocks are correctly created before you use them.

A systematic design approach is to design small components and make sure they are correct before moving on. It is tempting to jump in and starting using them right away, but this approach will in the end cause you to waste a lot of time debugging the system that could have been avoided if you tested and verified right after creating a building block. (Yes, I have been guilty of this myself.) If you verify the building blocks as you create them, then when you create a larger component, and it does not work, you know the error is unlikely to be coming from the smaller components, but rather in how you are using them in the larger component.

8.1.3. VHDL Code Formatting

When you write code, it is good to follow a style guide. This can help reduce coding mistakes. Here we present how to use Python to format your VHDL code using the VSG package.

There are many Python Integrated Development Environments (IDEs) (List of Python IDEs), so if you already have your own Python environment, then use that one. If not, then we give PyCharm as a suggested Windows environment and give the steps to set it up to format VHDL code. The steps are:

  1. Step1:

    Install the PyCharm Community Edition. (Download Link)

  2. Step2:

    In PyCharm, install the VHDL Style Guide (VSG) package. You can do this in PyCharm by opening a terminal window and entering the command pip install vsg.

  3. Step3:

    Create a project in PyCharm (suggested project name = vsg). Having a project ready to open and run on a file will make it convenient to use.

  4. Step4:

    Add the VHDL style guide adsd_vhdl_style.yaml to the project folder. Change any style rules in the file if so desired. It will work fine as is.

  5. Step5:

    Add the Python script format_vhdl_file.py to the project folder and make the following edits in the file:

    1. Edit1:

      Add the name of your VHDL file (line 18) that you want to format or check.

    2. Edit2:

      Add the path to your VHDL file (line 19).

    3. Edit3:

      Add the path to the .yaml file you downloaded in step 4. (It will be the path to the vsg project directory.)

  6. Step6:

    Run the python script format_vhdl_file.py, and fix any errors and warnings in your VHDL file. Repeat until there are no errors or warnings. If you disagree on what a warning or error should be, modify the file adsd_vhdl_style.yaml accordingly and rename it something like my_style.yaml.

8.2. Verification

8.2.1. Why Verify?

Computer systems are arguably one of the most complicated systems to develop, and it is easy to introduce errors in the design process. A design goal is to minimize errors, while you are developing a system, and it is best (and cheaper) to find potential errors right away, rather than having your customers find them for you. This is why you should test and verify your code and digital logic building blocks as you develop them.

8.2.2. Verification Process

Here we assume that you have just finished creating a VHDL component that you wish to verify. The process is to send values, known as test vectors through the component, and check if the results are correct. But how do you know what the correct result should be? Well, in our case, we will use Matlab and create a Matlab function that should produce the same output as the VHDL component. Then if both the VHDL and Matlab code agree with each other, we are pretty certain that the component is correct. Theoretically, it is possible to make the same mistake in two different languages where the same mistake manifests itself over all the test cases. However, the probability of this actually happening is pretty close to zero. The verification process shown in the following examples assumes that you have both ModelSim and Matlab installed on your system.

Two examples are provided that illustrate the verification process. Example 1 performs a simple VHDL component verification using the file input.txt for the input test vectors, and the results are written to the file output.txt. Matlab is used to generate the test vectors contained in the input file and then to verify that the output is correct.

Example 2 builds upon Example 1 in two ways. The first extension is that the number of I/O ports in the VHDL component has increased. There are two input ports to create test vectors for and two output ports to verify. The second extension is that there is an IP component created with the Quartus IP Wizard (a ROM memory). We will create this ROM, generate the memory initialization file, and setup ModelSim so that it can simulate this ROM IP component.

8.3. Verification Example 1: File I/O

Verification Example 1 performs verification of a simple VHDL component where the file input.txt contains that test vectors and the results are written to output.txt. Matlab is used to generate the test vectors and then to verify that the output is correct.

The starting point for verification is the VHDL design file that we wish to verify, which in this example is my_component1.vhd. The verification flow is illustrated in Fig. 8.1, which shows how all the files relate to each other. These files are listed in Table 8.1.

Fig. 8.1
figure 1

Verification Flow Diagram. ModelSim verifies the VHDL component my_component1.vhd using the associated testbench my_component1_tb.vhd by reading the test vectors from file input.txt and writing the results to output.txt. The Matlab script my_test_vectors1.m is used to create the test vectors in the file input.txt. Verification is performed by the Matlab scrip my_verification1.m that reads input.txt and computes what the results should be using my_component1.m. It then compares this output to what is in output.txt

Table 8.1 Files used to verify my_component1.vhd

8.3.1. VHDL File to Verify: my_component1.vhd

The file my_component1.vhd (click here for the source file) will be the VHDL code that we wish to verify. The entity can be seen in Listing 8.2, which has a generic MY_WIDTH that defines the signal widths of the signals my_input and my_output.

Listing 8.2 Entity of my_component1.vhd

The computation that my_component performs is to simply add 1 to the input, i.e., my_output = my_input + 1. This is done by the synchronous process my_add1_process that adds 1 to my_input on the rising edge of the clock. This process can be seen in Listing 8.3.

Listing 8.3 my_add1_process

To simulate a component that has multiple clock cycles of latency, we arbitrarily delay the signal my_result three clock cycles before we send it out of the component. The my_delay_process is shown in Listing 8.4, and this coding style is only suitable for a couple of delays. We also use this style to illustrate the behavior of VHDL processes. Although processes allow sequential programming, they are still different from the usual serial programming languages. If we follow a value through this process, on rising edge 1 of the clock, the contents of my_result are assigned to my_delay_signal_1. On rising edge 2, my_delay_signal_1 is assigned to my_delay_signal_2, and on rising edge 3, my_delay_signal_2 is assigned to my_output. This ordering may look like it will not work if you are looking at it from the perspective of a typical serial programming language, where it looks like the value in my_result overwrites all the signals with the same value in successive order. In a VHDL process however, the assignments do not occur till you are at the very end of the process where they all occur “instantaneously” and in parallel (actually the simulator has delta cycles with zero time for these assignments). Thus the behavior of the process is that all these assignments occur simultaneously at the rising edge of the clock. This is why in a VHDL process a signal could have several different assignments occurring at different locations in the sequential code, but it is only the last assignment made in the process that actually occurs when you get to the end of the process. Since my_clock is in the sensitivity list, the process runs again when any change in my_clock occurs, and we condition our code to run on the rising edge of the clock to make our designs synchronous with this rising clock edge.

Listing 8.4 my_delay_process

8.3.2. VHDL Testbench File: my_component1_tb.vhd

The file my_component1_tb.vhd (click here for the source file) is the VHDL testbench file that verifies my_component1.vhd, which is the Device Under Test (DUT). The signal width that is controlled by the generic MY_WIDTH is set by assigning the constant W_WIDTH to the desired value. In this example, we will be creating signals that are 16 bits wide.

Listing 8.5 Defining signal widths

Since our components are synchronous, we need to create a clock for our simulation. We do not really care what the frequency of the clock is, just that we have rising edges. We set the half period of the clock and create the clock signal as shown below.

Listing 8.6 Clock definitions

Note that we need to initialize the clock to zero since we will be toggling the clock every half period.

Listing 8.7 Clock creation

The main process in this testbench is reading the test vectors from the file input.txt and writing the results to output.txt. We start the process by first creating the local variables for this process as seen in Listing 8.8. The variables read_file_pointer and write_file_pointer are declared as file objects that will point to files of text (characters) where text is a VHDL type defined in the package textio. This is why we need to include this package at the beginning of the testbench (use std.textio.all).

Next, we create two variables line_in and line_out that are declared as line types, and a line type is a pointer to a string. This allows us to hold the address of a string (i.e., keep track of where the string is in memory), but it is not the string itself. To create a place to hold the string, we create a variable input_string, and we must tell it how many characters the string will contain. We also create the variable input_vector of std_logic_vector type because this is the type that we want to convert the input vectors to.

Listing 8.8 Variables for file I/O

Using the file_open() function, we open the file input.txt in read mode, and read_file_pointer points to this opened file. In a similar fashion, we open the file output.txt, but in write mode, it is pointed to by our variable write_file_pointer.

Listing 8.9 Opening files

We want to read all the test vectors in the file input.txt so we create a while loop to keep reading until there are no more input test vectors. When that happens, the endfile() function returns true and the loop stops.

Listing 8.10 While loop

The function readline() moves a line of text from the file input.txt into an internal buffer, and it gives us the address of this buffer in the pointer line_in. We then use the read() function where we give it the line_in pointer that points to the string in the buffer, and it copies it into our string variable input_string. We want this string to be of type std_logic_vector so we use the conversion function to_std_logic_vector() to perform the conversion and place it in our input_vector variable. Now we can make the signal assignment to input_signals since they are the same type and same vector width. Thus a line of text in input.txt has been converted into a std_logic_vector type and placed into the input of the component under test. The line that starts with the keyword report is used to display the values as they are being read in. If you have a lot of test vectors, you will want to delete or comment this line out.

Listing 8.11 Reading test vectors

In parallel with a test vector being read in, we have a result vector output_signal that we want to save to output.txt. The function write() takes our std_logic_vector output_signal and writes it to the buffer pointed to by line_out. It converts it into a string in the buffer, and we have to tell it how many characters to write and if it should be right or left justified. We then write the buffer to the file with the writeline() function.

Listing 8.12 Writing results

Notice that this file I/O process does not have a sensitivity list. Instead we use wait statements within the process. All the statements in the process do not happen until a wait occurs, and since we want a line to be read in and converted into our input vector and a result to be written every clock cycle, we wait until we have a rising edge of the clock signal. If this wait was omitted, the process would run, looping and reading in all the test vectors until it saw the next wait statement. Then the last assignments would occur, and you would only see the last test vector in the input file being assigned and fed into the test component.

Listing 8.13 Synchronizing file I/O with clock

When done, the files are closed, and a final wait statement is written so that the file I/O process is not restarted again during simulation.

Listing 8.14 Closing files

8.3.3. Creating Test Vectors with Matlab Script my_test_vectors1.m

In our testbench, we want to verify a component that has an input that is 16 bits wide and where these 16-bit vectors will be read from the file input.txt. We will create this input.txt file using the Matlab script called my_test_vectors1.m (click here for the source file)

The first parameter is Nvectors that will be the number of test vectors created. Here it is set to 10 so the simulation will be short. Normally, you want as many test vectors as possible while still being practical given the simulation time. Ideally you would generate all possible input bit patterns as the test vectors, which is known as complete coverage. However, if you had an input vector that was 128 bits wide, complete coverage would require 2128 test vectors. If your simulator could process 232 or 4.3 billion test vectors per second, it would take 2.5 × 1019 centuries to complete the simulation, clearly not practical. Rather you would need to sample the input range to reduce the number of test vectors to a more manageable simulation time.

The second parameter is Component_latency that should be set to the latency of the component being tested. This causes zeros to be written to input.txt to make sure that the last non-zero test vector gets completely through the component before the simulation stops.

The next parameter W controls the number of bits or word size in the test vectors. Even though we will be using fixed-point values, we only need to know the test vector’s width to create the input bit patterns. We do not concern ourselves with what these bit patterns mean at this point, and so we set the number of fractional bits to zero and make the values unsigned so we can treat the vector as an unsigned integer when creating the test vector bit patterns.

Listing 8.15 Parameter settings

In Matlab, we use the fopen() function to open the file input.txt in write mode (W) where we will be writing the test vectors. The function returns the file identifier that we store in the variable fid (short for file ID).

Listing 8.16 Opening a file

Since the number of test vectors being generated is likely much smaller than the number of possible test vectors, we sample a small subset from the much larger set of all input possibilities. We treat the possible input set as a uniform distribution and draw from this uniform distribution. We use the rng() function in Matlab to set up the random number generator. The shuffle argument seeds the generator with the current time, so that when each time Matlab is started up, the random numbers that are generated will be different. The twister argument specifies to use the Mersenne Twister algorithm. This algorithm is good enough for our purposes, and we will not concern ourselves with finding the “best” random number generator since this is a rabbit hole we could go down into and not come back from. We create a vector of random integers using the randi() function. The range of the uniform distribution of integers is specified by the interval [0 2W−1]. This is the range of unsigned integers that gives us all possible bit patters for a word length of W. The last two parameters 1,Nvectors in randi() tell it to create a matrix of random values that has 1 row and Nvectors columns, which is returned in the variable r. The vector r contains the values that we will use for the test vectors.

Listing 8.17 Random coverage

We then go through all the Nvectors random values in the variable r by using a for loop. We make use of Matlab’s Fixed-Point Designer toolbox by creating a fixed-point object using the fi() function. The first argument in this function is the value that will be used for the fixed-point number, which we get by indexing into our r vector. The second argument is the sign bit S, which we set to zero for unsigned data types. The third argument is the word length in bits, which we set to W. The last argument is the number of fraction bits in the word, which we set to zero since we are assuming unsigned integers. The fixed-point object is returned as the variable f.

A convenient radix conversion that is associated with these fixed-point objects is the binary string conversion that is accessed by using the suffix .bin (.hex for hexadecimal and .oct for octal). We can then use this string directly in our fprintf() function to write the binary string representation directly to our test vector file input.txt. An example of this test vector input file can be seen in input.txt (click here for the source file) and is also shown in Listing 8.20.

Listing 8.18 Writing binary strings

When we are done writing the test vectors to input.txt, we close the file.

Listing 8.19 Closing a file

Listing 8.20 File contents : input.txt

8.3.4. Computing the Results with Matlab Function my_component1.m

When the ModelSim simulation is run, the testbench my_component1_tb.vhd produces the results in file output.txt (click here for the text file), which can also be seen in Listing 8.21. You can see the latency of my_component1.vhd as the lines comprised of the character “U,” which means Uninitialized. This means that ModelSim is putting out values from internal signals that have not been initialized and do not know what these values should be. When the first input finally gets through the pipeline, we see a result showing up on line 6. If you compare output.txt with input.txt, you can see that one has been added to all the input binary values.

Listing 8.21 File contents: output.txt

In order to verify that these results are correct, we need a Matlab function that produces the same results. This function is called my_component1.m (click here for the source file). If this Matlab function agrees with the ModelSim output produced by my_component1.vhd, we can be confident that both the VHDL and Matlab codes are correct.

The Matlab function declaration of my_component1.m is shown below where we expect a fixed-point variable to be passed into the function.

Listing 8.22 Function declaration

We want our fixed-point Matlab operations to reflect what our VHDL code is doing. This means that we need to change the default behavior of Matlab’s Fixed-Point Toolbox. The normal behavior is to automatically extend the word length of the result, which is not always what we do in our VHDL code. In our VHLD code, when we add 1 to a std_logic_vector signal, the signal size stays the same (yes, overflow is possible). To make our fixed-point variable have the same behavior, we need to modify the fixed-point math settings for this variable. We can do this by using the fimath() function to create a fimath object with our desired settings and then apply this to our fixed-point variable. We can create multiple fimath objects, each with their own settings, and apply these to different variables. In this example, we just use a single fimath setting and apply it to the fixed-point variable x.

Listing 8.23 Setting fimath properties

Even though we really only care about the SumWordLength property in this example, the fimath settings shown can be used in many situations where you are adding and multiplying in VHDL, and you want to reflect this in your Matlab verification code. We will discuss only a few of the fimath options. A complete list can be seen in Matlab’s fimath reference page.

The first property that is being set is RoundingMethod, which is set to Floor. Floor causes rounding to round toward negative infinity. This is useful if you are truncating a fixed-point value where you are taking a VHDL signal slice that is eliminating some of the least significant fractional bits.

The next property that is being set is OverflowAction, where it can be set to either Saturate or Wrap. Both cases can occur in your VHDL code. Wrap is more common where you have a counter in VHDL using a std_logic_vector signal and you add 1 to the largest value (all ones in the vector), which causes it to become zero (wraps to zero). Since this is desired behavior in many cases, you want your Matlab code to reflect this so you would use the Wrap option in this case.

Where wrapping behavior is not desirable is when you are operating on audio signals. A wrap will cause a large positive value to immediately turn negative (i.e., two’s complement overflow), and this will sound like a “pop” in your audio. If the signal value is too large to fit into your data type, what you want to do is to clip it to the maximum value that can be represented by the data type (fixed-point vector). This clipping will cause harmonic distortions to occur, but this will sound much better than a loud pop. Thus in audio applications, you would use the Saturate option for overflow behavior of your audio signals.

The next property that is being set is ProductMode, which is set to SpecifyPrecision. The default behavior is FullPrecision where Matlab will automatically grow the data type size in order to keep all of the bits after a multiplication. Here we want to control both the word length and fraction length of the result. To control the word length, we set ProductWordLength to W, which means we keep the same word length as x, which we get from the fixed-point object field x.WordLength. To control the fraction length, we set ProductFractionLength to F, which means we keep the same fraction length as x, which we get from x.FractionLength.

You will find in audio hardware that the audio signal is kept as a fractional data type, i.e., between [−1 1]. The reason is that after a multiplication occurs in hardware, you can easily resize the vector just by throwing away the least significant bits that do not fit into the vector. For example, if you had a 24-bit fractional signal and multiply it by another 24-bit fractional signal, you could keep it as a 24-bit fractional signal by throwing away the least significant 24 fractional bits. It is unlikely that you could hear these bits anyway so there would be no point in keeping them around.

The next property that is being set is SumMode, which is set to SpecifyPrecision. The default behavior is FullPrecision where Matlab will automatically grow the data type size in order to keep all of the bits after an addition. Here we want to control both the word length and fraction length of the result. To control the word length, we set SumWordLength to W, which means we keep the same word length as x where we can get it from x.WordLength. To control the fraction length, we set SumFractionLength to F, which means we keep the same fraction length as x where we can get it from x.FractionLength. This is how we keep the output the same length as the input since this is the behavior of the add 1 operation in my_component1.vhd and why the vectors in output.txt are the same length as the vectors in input.txt.

Once a fimath object has been created with the properties that we want, which are now in the fimath variable Fm, we assign it to the x.fimath field that controls what happens when x is used.

Finally, we add one to x where we have now specified that x should keep the same length after addition. This results in the variable y with the same bit width as variable x, which reflects what our VHDL code is doing.

Listing 8.24 Adding one

8.3.5. Performing Verification with the Matlab Script my_verification1.m

When the ModelSim simulation is run, the testbench my_component1_tb.vhd produces the results in file output.txt, which can be seen in Listing 8.21. You can see the latency of my_component1.vhd as the lines comprised of the character “U,” which means that ModelSim is putting out values from internal signals that have not been initialized and do not know what these values should be. When the first input finally gets through the pipeline, we see a result showing up on line 6. If you compare output.txt with input.txt, you can see that one has been added to all the input values.

We want to compare what is in output.txt with what my_component1.m produces, and we automate the comparison process using the Matlab script my_verification1.m (click here for the source file).

The one parameter setting in my_verification1.m that needs to match the parameter settings in my_test_vectors1.m is the test vector bit width W. The test vectors were created with no concern as to the interpretation of these binary patterns. Now we do care what these binary patterns mean. Thus the parameters F and S need to be set with how we really will be interpreting these input numbers. It just so happens that in this case we will interpret the numbers again as unsigned integers, keeping these parameters the same, but typically this is not the case when using fixed-point numbers.

Listing 8.25 Verification parameter settings

The test vectors are read in from the file input.txt as shown in the code in Listing 8.26. The file is opened in read mode by Matlab’s fopen() function, and the returned value assigned to the file handle fid1, which is the pointer to the input file. The Matlab function fgetl() then reads the first line in the file and puts this in the variable line_in.

Since we want to interpret what the binary strings in input.txt mean in terms of numerical values, we create a fixed-point object where variable S has been set to zero, which means the bit strings will be interpreted as unsigned numbers. W has been set to 16, which must match the number of bits on each line as seen in Listing 8.20. The parameter F is set to zero since our numbers are unsigned integers and have no fractional bits. At this point, in the code, we do not care what the value of the fixed-point object is and we set it to zero (first argument). We will use this fixed-point object in a bit. We are going to create an array of fixed-point objects for each line in input.txt so we set the starting index of this array to one (index = 1;).

We create a while loop and test line_in with the function ischar(). Function fgetl() returns the lines of text until it reaches the end of the file. When this occurs, a value of −1 is returned, which is not a character, and this causes ischar() to return false, stopping the while loop.

We assign the binary bit string that was read in to the fixed-point variable a. By assigning the binary string to the .bin field of a, Matlab updates the value of a to be consistent with the binary string and the parameters (S,W,F) that we used when we first created this fixed-point variable.

We then assigned a to our test_vector array with the current index and display what this value is. We increment the index for the next round and read in a new line from the input file and the while loop starts again. When an end-of-file condition is encountered, the loop ends and we close the input file.

When we are done reading in the test vectors from input.txt, we end up with the Matlab array test_vectors that contains all of the test vectors where the interpretation of the numbers given the binary strings have been controlled by the parameters (S,W,F).

Listing 8.26 Reading in the test vectors

When ModelSim writes the test vectors to output.txt, it can write any of the characters associated with the VHDL std_logic type as seen in Listing 8.27. Since we only want to deal with numeric values, we are going to ignore any output that has characters other than “0” or “1.” We create a list of these nonbinary characters in the string stdchar. We will check for these characters and ignore them, when reading in the result vectors from output.txt.

Listing 8.27 std_logic characters

The result vectors that ModelSim produced are read in from the file output.txt as shown in the code seen in Listing 8.28. This code is similar to reading in the test vectors in Listing 8.26 so we will only describe the differences here. In the while loop, we want to see if line_in contains any of the nonbinary std_logic characters. We start by setting the variable s to zero. Then, in a for loop, we add to s the output of the contains() function that will return 1 if line_in contains one of these nonbinary characters as we index through all of them. If there are not any, s will remain zero, and we assign the string to a.bin, which in turn gets saved into the array vhdl_vectors. If the string does contain nonbinary characters, we ignore this line and print out message that we are doing so.

Listing 8.28 Reading in the result vectors

We have now read in the test vectors from input.txt, which have been saved in the array test_vectors, and the result vectors from output.txt, which have been saved in the array vhdl_vectors. We next run the test_vectors values through our Matlab function my_component1.m, producing the vector matlab_vectors as seen in Listing 8.29. If our VHDL and Matlab codes are correct, matlab_vectors should contain the same values as vhdl_vectors.

Listing 8.29 Computing the Matlab version of my_component1

The comparison of matlab_vectors with vhdl_vectors is shown in Listing 8.30. We need to take care of one wrinkle before we can fully automate the comparison of these vector values. We need to know where the results start showing up in vhdl_vectors; otherwise, we will always be comparing wrong values. This is because of two things. One, we have the latency through the VHDL component. Second, we may or may not be ignoring initial result vectors depending on if there are initialized results (i.e., “U” values) or if these are zero. This can be seen by inspection by comparing the starting values of the vectors and seeing what the offset is between these two vectors. This offset is then set in the Matlab verification code (line 105) so the verification can then be automated for all the values in the vectors. A for loop is used to go through all the matlab_vectors, comparing them to their associated vhdl_vectors values. A comparison of the bit patterns is made since verification needs to be bit true, and if the bit strings do not match, a verification error is produced for this case with the values being printed out. If no errors are seen, the variable error_flag will remain zero, and this will result in the final message Verification Succeeded!.

Listing 8.30 Comparing the VHDL and Matlab results

8.3.6. Running the Example 1 Verification

Here are the steps to take to run this verification example:

  1. Step1:

    Create a \Ex1 directory in Windows and download all the Example 1 verification files from GitHub (click here for the files), with the exception of the input.txt and output.txt files (you will be creating these), and put them in this new directory.

  2. Step2:

    Open up my_test_vectors1.m in Matlab and run the script. It will create a new file input.txt, which will be different from the one found on GitHub because the test vectors are randomly generated.

  3. Step3:

    Open ModelSim:

    1. a.

      Create a Project by selecting FileNewProject.

      1. i.

        Name the project Ex1.

      2. ii.

        Under Project Location, browse to the \Ex1 directory you just created.

      3. iii.

        Keep the Default Library Name as “work.”

      4. iv.

        Keep the “Copy Settings From” as is (keep default setting).

      5. v.

        Click OK.

    2. b.

      When it asks to Add items to the Project, click on Add Existing File, browse to \Ex1, and add the three VHDL files. Click OK and close the Add items to the Project window.

    3. c.

      Modify the compilation order if it needs to be modified by going to CompileCompile Order… and rearranging the compile order (select file and use up/down arrows to change the file order). The compile order should be:

      1. i.

        text_util.vhd (Order 0 in Project tab)

      2. ii.

        my_component1.vhd (Order 1 in Project tab)

      3. iii.

        my_component1_tb.vhd (Order 2 in Project tab)

    4. d.

      Compile the files (CompileCompile All).

      Note: If the compile fails (a red x will show up in the Status column), you can see the errors that have been logged in the compile summary (CompileCompile Summary).

    5. e.

      To run the simulation:

      1. i.

        Select the Library window in ModelSim (click Library tab above Transcript window).

      2. ii.

        Expand the work folder.

      3. iii.

        Double click on my_component_tb (this will open the Wave window).

      4. iv.

        To see the signals you want to see, select the component hierarchical level in the sim–Default window, click on a signal name in the Objects window, and either right click and select Add Wave or drag the signal into the first column of the Wave window. Add the following signals (from the my_component_tb instance):

        1. A.

          clk

        2. B.

          input_signal

        3. C.

          output_signal

      5. v.

        Set the simulation time to 500 ns. (This needs to be long enough to run all the stimulus vectors through the simulation, so if you change the number stimulus vectors, you will need to change this value.)

      6. vi.

        Run the Simulation (button to the right of the simulation time window). The file output.txt should be created that contains the result vectors.

  4. Step4:

    Open up my_verification1.m in Matlab and run the script. You should see the message Verification Succeeded! in the Matlab Command Window. Note: To see an error message, open the output.txt, change a bit in one of the result vectors, and re-run my_verification1.m.

8.4. Verification Example 2: Using a Quartus ROM IP Component

Example 2 builds upon Example 1 in two ways. The first extension is that the number of I/O ports in the VHDL component has increased. There are two input ports to create test vectors for and two output ports to verify. The second extension is that there is an IP component created with the Quartus IP Wizard (a ROM memory). The steps for creating the Quartus ROM IP can be found in Sect. 6.4.1 Creating a ROM IP Component (page 78). We will use this ROM IP and its memory initialization file when verifying my_component2.vhd. The files used in this Example 2 verification are listed in Table 8.2.

Table 8.2 Files used to verify my_component2.vhd

8.4.1. VHDL File to Verify: my_component2.vhd

The file my_component2.vhd (click here for the source file) will be the VHDL code that we wish to verify. The entity can be seen in Listing 8.31, which has generics that specify the signal widths for the ROM being used, the I/O signal widths for the component, and the number of clock cycles to delay the output.

Listing 8.31 Entity of my_component2.vhd

The computation that my_component2 performs is to multiply the input signal by a ROM value, i.e., my_output = my_inputxROM_signal. The input fixed-point signal is MY_WORD_W bits wide and has MY_WORD_F fractional bits, and the ROM memory word is MY_ROM_Q_W bits wide and has MY_ROM_Q_F fractional bits. The ROM signal is specified by the input signal my_rom_address, which is MY_ROM_A_W bits wide and can address \(2^{MY\_ROM\_A\_W}\) memory locations. The ROM takes two clock cycles for the output to appear after the ROM address is presented. Note that the ROM generic values need to be consistent with the generated ROM IP and ROM memory initialization file. The input signal my_input must be delayed by two clock cycles so that it time aligns with the specified ROM signal as seen in Listing 8.32. The resulting product my_product is a signal with MY _WORD_W + MY _ROM_Q_W bits and is trimmed so that it has the same fixed-point data type (same W and F) as the input signal my_input.

Listing 8.32 Process that delays and multiplies

The product signal my_product_trimmed is then delayed by MY_DELAY clock cycles. We use VHDL generate statements to include the appropriate VHDL code depending on the delay value. If the delay is zero, we send the product out with no delay by connecting it directly to my_output as seen in Listing 8.33.

Listing 8.33 Generated code when MY_DELAY is ero

If MY_DELAY is set to one, we generate a process where the output is assigned on the rising edge of the clock as seen in Listing 8.34.

Listing 8.34 Generated code when MY_DELAY is one

If MY_DELAY is set to two, we generate a process where the additional delay is made by assignment to an internal signal, which is declared before the generate begin statement as seen in Listing 8.35. Note that the generate begin statement is not needed if there are no signals to declare as seen in the previous generate statements.

Listing 8.35 Generated code when MY_DELAY is two

If MY_DELAY is greater than two, we generate a process where the additional delays are made by assignments to an array of signals, which can be seen in Listing 8.36. We do this by creating our own array type called my_delay_array and then specifying the size of this array when we create the signal delay_vector. We can then make the appropriate assignments between the delay_vectors inside the for loop. Note: There is a limit to the amount of delay that can be done by creating registers out of the logic elements in the FPGA fabric. If the delay needs to be large, then it is better to use dedicated memory in the fabric (e.g., FIFOs or circular buffers created out of the on-chip RAM blocks).

Listing 8.36 Generated code when MY_DELAY is greater than two

8.4.2. VHDL Testbench File: my_component2_tb.vhd

The file my_component2_tb.vhd (click here for the source file) is the VHDL testbench file that verifies my_component2.vhd, which is the Device Under Test (DUT). This testbench is similar to my_component1_tb.vhd, which is described in Sect. 8.3.2 VHDL Testbench File: my_component1_tb.vhd (page 6). The difference is that two input files are read and two output files created. The input file input1.txt is fed to the input signal my_input, and the input file input2.txt is fed to the input signal my_rom_address. The output file output1.txt is created from the output signal my_output, and the output file output2.txt is created from the output signal my_rom_value.

8.4.3. Creating Test Vectors with Matlab Script my_test_vectors2.m

The file my_test_vectors2.m (click here for the source file) is the Matlab script that creates the two input test vector files. This script is similar to my_test_vectors1.m, which is described in Sect. 8.3.2 VHDL Testbench File: my_component1_tb.vhd (page 9). It creates the input file input1.txt with test vectors for the input signal my_input that are 16 bits wide as shown in Listing 8.37. It also creates the input file input2.txt with test vectors for the input signal my_rom_address that are 8-bit addresses as shown in Listing 8.38.

Listing 8.37 File contents : input1.txt

Listing 8.38 File contents : input2.txt

8.4.4. Computing the Results with Matlab Function my_component2.m

When the ModelSim simulation is run, the testbench my_component2_tb.vhd produces two output files output1.txt, shown in Listing 8.39, and output2.txt, shown in Listing 8.40. Output1.txt (click here for the source file) contains the result of multiplying the fixed-point input signal (W=16, F=8) with the ROM word (W=12, F=11). You can see the latency of my_component2.vhd as the lines comprised of the “U” and “X” characters. The character “U” means Uninitialized and “X” means Forcing Unknown. This means that ModelSim is putting out values from internal signals that have not been initialized or do not know what these values should be yet. When the first result finally gets through the pipeline, we see it showing up on line 9.

The output file Output2.txt (click here for the source file) shows the output from the ROM component (Listing 8.40), which takes two clock cycles to appear. The latency is two clock cycles because it takes the first rising edge of the clock to capture the address and then a second rising edge to put out the value associated with this address.

Listing 8.39 File contents: output1.txt

Listing 8.40 File contents: output2.txt

The Matlab function declaration of my_component2.m (click here for the source file) is shown below where three parameters are passed to the function. The parameter x is the input port that takes in the my_input values from the file input1.txt. The word length and the number of fractional bits of x are extracted in lines 39–40. The parameter addresses is the input port that takes in the my_rom_address values (W=8) from the file input2.txt. The parameter rom is the ROM variable array that was saved in ROM.mat when the ROM memory initialization file was created. This allows Matlab to easily access the ROM memory values just by loading in a .mat file rather than having to parse a .mif file. Note that the word length and number of fractional bits for the ROM memory need to be explicitly set in lines 43–44.

Listing 8.41 Function declaration

The address values are converted into the ROM memory words as seen in Listing 8.42. A value of one is added to the address values since the ROM hardware has a zero index offset and Matlab indexes into arrays with an index offset of one.

Listing 8.42 ROM lookup

In order to account for the two clock cycle latency of the ROM, the array of ROM values is then shifted by two in the array as seen in Listing 8.43 where zeros of the same fixed-point data type are inserted as place holders. In a similar fashion, the input values are also delayed by two to match the VHDL component behavior (line 65).

Listing 8.43 ROM latency

The input values are then multiplied by their respective ROM value as seen in Listing 8.44. We allow Matlab to expand the fixed-point product to the full range, which is its default behavior and why we do not set any fimath properties as we did in my_component1.m. We extract the resulting binary string of the full product from the fixed-point object result. We then trim the binary string in a similar way to how the VHDL std_logic_vector was trimmed to get the same result. This string is then assigned to a fixed-point object so that we can interpret the results and save it in the z array. The verbose setting is to allow one to see the string results, which is useful during development to debug the indexing, but then is turned off once the indexing for the trimming is correct.

Listing 8.44 Computation of my_component2

8.4.5. Performing Verification with the Matlab Script my_verification2.m

When the ModelSim simulation is run, the testbench my_component2_tb.vhd produces the results found in the files output1.txt and output2.txt, which can be seen in Listings 8.39 and 8.40. We want to compare what is in these files with what my_component2.m produces, and we automate the comparison process using the Matlab script my_verification2.m (click here for the source file). This is similar to the process described in Sect. 8.3.5 Performing Verification with the Matlab Script my_verification1.m (page 14). The primary difference is reading in the additional input and output files and performing two separate verifications.

The first verification (lines 154–189) checks that the ROM values in output2.txt are consistent with the memory initialization file contents that were saved as a .mat file.

The second verification (lines 202–230) checks that the Matlab function my_component2.m, using the same inputs, input1.txt (test_vectors) and input2.txt (address_vectors), produces the same results as the VHDL component did in output1 (vhdl_vectors).

When the Matlab code agrees with the VHDL ModelSim simulation over the entire input coverage, one is pretty certain that both the Matlab and VHDL codes are correct.

8.4.6. Running the Example 2 Verification

Here are the steps to take to run this verification example:

  1. Step1:

    Create a \Ex2 directory in Windows and download all the Example 2 verification files from GitHub (click here for the files), with the exception of the input1.txt, input2.txt, output1.txt, and output2.txt files (you will be creating these), and put them in this new directory.

  2. Step2:

    Download the files ROM.vhd, ROM.mif, and ROM.mat from GitHub (click here for the files) and place them in the \Ex2 directory.

  3. Step3:

    Open up my_test_vectors2.m in Matlab and run the script. It will create two new files input1.txt and input2.txt, which will be different from the ones found on GitHub because the test vectors in these files are randomly generated each time.

  4. Step4:

    Compile the Quartus altera_mf Simulation Library:

    1. a.

      Create a folder for the library: \Ex2\simlib.

    2. b.

      Open Quartus and open the EDA Simulation Library Compiler (Tools → Launch Simulation Library Compiler).

    3. c.

      Under Tool Name, select ModelSim.

    4. d.

      Under Executable Location: browse to where you installed ModelSim, e.g., C:\Modeltech_pe_edu_10.4a\win32pe_edu.

    5. e.

      Under Library families, add the Cyclone V device (using > ). Note: The actual device family is not really important since we will just be simulating with ModelSim.

    6. f.

      Under Library Language, select VHDL.

    7. g.

      Under Output Directory, browse to the folder you created (i.e., \Ex2\ simlib). Note: You will need this path information when compiling in ModelSim so save this path information. You also might want to put this library folder in a more general place since you will be using it for other simulations (e.g., in Lab 1).

    8. h.

      Click the Start Compilation button.

  5. Step5:

    Open ModelSim:

    1. a.

      Create a Project by selecting FileNewProject.

      1. i.

        Name the project Ex2.

      2. ii.

        Under Project Location, browse to the \Ex2 directory you just created.

      3. iii.

        Keep the Default Library Name as “work.”

      4. iv.

        Keep the “Copy Settings From” as is (keep default setting).

      5. v.

        Click OK.

    2. b.

      When it asks to Add items to the Project, click on Add Existing File, browse to \Ex2, and add the four VHDL files. Click OK and close the Add items to the Project window.

    3. c.

      Modify the compilation order if it needs to be modified by going to CompileCompile Order… and rearranging the compile order (select file and use up/down arrows to change the file order). The compile order should be:

      1. i.

        text_util.vhd (Order 0 in Project tab)

      2. ii.

        ROM.vhd (Order 1 in Project tab)

      3. iii.

        my_component2.vhd (Order 2 in Project tab)

      4. iv.

        my_component2_tb.vhd (Order 3 in Project tab)

    4. d.

      Tell ModelSim where the altera_mf library is. In ModelSim and in the Transcript (command) window, issue the following vmap command that defines a mapping between a logical library name (altera_mf) and the associated folder that contains the library.

      Listing 8.45 ModelSim Tcl command: vmap altera_mf

      1. i.

        For the vmap command, the directories need to be separated by forward slashes (“/”) rather than back slashes (“∖”), since it follows a Linux path specification rather than Windows. If you cut and paste the path from Windows, you will need to change this; otherwise, ModelSim will complain.

      2. ii.

        Replace /..../ with the appropriate path on your computer system to the location of the altera_mf simulation library.

      3. iii.

        Create a ModelSim Do file to automate commands such as this vmap command. In ModelSim, go to File → New → Source → Do, and type the vmap command into the DO file. Save the file as setup.do To reissue this command, when you reopen ModelSim, you just type do setup.do in the command window. You can also add commands to add signals to the WAVE window. Notice that when you add a signal, that command was echoed to the command window. You can then just cut and paste this command into your .do file to automate any setup for the next ModelSim session.

      Listing 8.46 ModelSim Tcl command: do setup.do

    5. e.

      Compile the files (CompileCompile All).

      Note: If the compile fails (a red x will show up in the Status column), you can see the errors that have been logged in the compile summary (CompileCompile Summary).

    6. f.

      To run the simulation,

      1. i.

        Select the Library window in ModelSim (click Library tab above Transcript window).

      2. ii.

        Expand the work folder.

      3. iii.

        Double click on my_component2_tb (this will open the Wave window).

      4. iv.

        To see the signals you want to see, select the component hierarchical level in the sim–Default window, then click on a signal name in the Objects window, and either right click and select Add Wave or drag the signal into the first column of the Wave window. Add the following signals from the my_component_tb instance. You can also add these to the setup.do file by copying the commands in the Transcript window after you add the signals:

        • clk

        • input1_signal

        • input2_signal

        • output1_signal

        • output2_signal

        Add the following signals from the my_component_tb/DUT instance so that you can see the results and the delay of the output:

        • my_product

        • my_product_trimmed

        • my_output

      5. v.

        Set the simulation time to 500 ns. (This needs to be long enough to run all the stimulus vectors through the simulation, so if you change the number stimulus vectors, you will need to change this value.)

      6. vi.

        Run the simulation (button to the right of the simulation time window). The files output1.txt and output2.txt should be created that contain the result vectors.

  6. Step6:

    Open up my_verification2.m in Matlab and run the script. You should see the message Verification Succeeded! in the Matlab Command Window.

8.5. Homework Problems

Problem 8.1

Perform the Example 1 verification as described in Sect. 8.3.6 but with all the modifications listed below where you operate on signed fixed-point numbers rather than on unsigned integers. The verification should result in no errors signifying that both your VHDL and Matlab codes are correct. Note: Specific values for (Vadd, W, F, S) may be individually assigned to you by the instructor.

  1. Modification1:

    Change the vector bit width W from 16 bits to 24 bits.

  2. Modification2:

    Change the number of fractional bits F from zero to 12 bits.

  3. Modification3:

    Change the signedness from unsigned to signed, i.e., change S from zero to one.

  4. Modification4:

    Instead of adding 1 to the input signal in my_component1.vhd, add the value Vadd = 4.125 to the input signal. This will require creating a constant VHDL signal that is initialized to 4.125 and where the binary point is aligned with the input signal in order to add properly. You will also need to modify my_component1.m to reflect this change in the Matlab function.

Problem 8.2

Perform the Example 2 verification as described in Sect. 8.4.6 but with all the modifications listed below. This will involve creating a new ROM IP component as described in Sect. 6.4.1 Creating a ROM IP Component (page 78). The verification should result in no errors signifying that both your VHDL and Matlab codes are correct. Note: Specific values for (ROM_A_W, ROM_Q_W, ROM_Q_F, ROM_Q_S, WORD_W, WORD_F, DELAY) may be individually assigned to you by the instructor:

  1. Modification1:

    Change the ROM address size ROM_A_W from 8 bits to 10 bits.

  2. Modification2:

    Change the ROM word size ROM_Q_W from 12 bits to 16 bits.

  3. Modification3:

    Change the number of fractional bits in the ROM word ROM_Q_F from 11 bits to 10 bits.

  4. Modification4:

    Change the signedness of the ROM word from unsigned to signed, i.e., ROM_Q_S from zero to one.

  5. Modification5:

    Create a new memory initialization file (.mif and associated .mat files) for the ROM that contains random values with the (ROM_Q_W, ROM_Q_F, ROM_Q_S) specifications given above.

  6. Modification6:

    Change the input word size WORD_W from 16 bits to 24 bits.

  7. Modification7:

    Change the number of fraction bits in the input word WORD_F from 8 bits to 16 bits.

  8. Modification8:

    Change the output DELAY from 4 clock cycles to 3 clock cycles.