Chapter 13

Test Benches

With most hardware description languages, the circuit description and the test waveforms are described in different ways, with the test waveforms described either using a waveform capture facility in the simulator or using a separate waveform language. Most VHDL simulators do not, however, have any form of waveform-capture facility because VHDL is itself sufficiently expressive to be used as a waveform language. The result is the test-bench, which is simply a naming convention for a VHDL model which generates waveforms with which to test a circuit model.

13.1 Test Benches

Test benches clearly only apply to the use of VHDL in simulation. They are not synthesised. Nevertheless, it is appropriate to have a chapter dedicated to the writing of test benches in a book on synthesis VHDL, since the writing of test benches is an important part of the design process and one where many designers get unnecessarily bogged down.

The necessity for test benches is clear. A synthesisable model should be extensively tested in simulation before synthesis to ensure correctness. A synthesiser works, as far as possible, on the principle that ‘what you simulate is what you get’ (WYSIWYG), so any errors in the design will be faithfully synthesised as errors in the final circuit. It is up to you as the designer to test carefully. Furthermore, this testing should be carried out on the RTL model prior to synthesis. This is where most errors can and should be found.

Diagnosing errors in synthesiser-generated netlists is almost impossible. Waiting until a gate-level model is obtained from synthesis is, in any case, far too late in the design cycle to start checking a design's integrity. The only checking that should be carried out at this late stage is go/no-go testing of the circuit behaviour and checks to ensure that timing problems have not arisen from the synthesiser's mapping to gates.

Because test benches are not synthesised, the full scope of the VHDL language is available for writing them. The restrictions described in the other chapters for writing synthesisable models do not apply when writing test benches, and a number of forms of VHDL will be used that have not been described before.

The methods used in writing test benches will be developed gradually over a number of examples, starting with a simple combinational circuit to introduce the main topics and then using gradually more complicated examples to introduce new concepts.

13.2 Combinational Test Bench

The first example will show the basic structure that is recommended for a test bench. The example is simple so that understanding of the example does not block the understanding of the test bench.

The task to be solved is to test a simple multiplexer that has the following interface:

library ieee;

use ieee.std_logic_1164.all;

entity mux is

  port (in0, in1, sel : in std_logic; z : out std_logic);

end;

There is no need to know the structure of the circuit under test. It is sufficient to say that input sel selects input in0 when sel = 0 and input in1 when sel = 1.

The first stage is to create an entity with no ports and an architecture with a component instance of the circuit under test contained within it. The test bench forms a wrapper that completely encloses the circuit under test, which is why there are no ports on the test bench entity. Also in the architecture will be a signal to connect to every port of the circuit under test. The convention is to use a signal with the same name and type as the component port so that the test bench is readable.

The recommended practice for naming the entity and architecture is to give the entity the same name as the circuit under test but with _test appended. The architecture should have a name that identifies it as a test bench and not a behavioural description – test_bench is a good choice of architecture name.

entity mux_test is

end;

library ieee;

use ieee.std_logic_1164.all;

architecture test_bench of mux_test is

  signal in0, in1, sel, z : std_logic;

begin

  CUT: entity work.mux port map (in0, in1, sel, z);

end;

This example is of course incomplete – so far it just consists of the circuit under test (CUT) as a component using direct binding and the signals connected to its inputs and outputs.

Note that an entity with no ports has no port specification at all, not just an empty port specification, which would be an error.

The next stage is to build up a test set and a process to generate test stimuli at the required times. Both of these use features of VHDL not previously used.

The recommended way of defining a test set that can be applied to most designs is by declaring a constant array of stimuli. Each stimulus is a record containing the values to be applied to each of the inputs for a single test. The record for the stimulus type contains one field for each input to the CUT. The convention is once again to name each field after the circuit port that it will be driving. The type declaration goes in the architecture declarative part (before the begin). The record type is:

type sample is record

  in0 : std_logic;

  in1 : std_logic;

  sel : std_logic;

end record;

Then there needs to be an array-type definition so that it is possible to create an array of this record type. The array-type declaration will be unconstrained so that an array containing a test set of any size can be created:

type sample_array is array (natural range <>) of sample;

Now that the type declarations are complete, it is possible to declare a constant array of samples containing the stimulus data for the test bench.

constant test_data : sample_array :=

  (

    ('0','0','0'),

    ('0','1','0'),

    ('1','0','0'),

    ('1','1','0'),

    ('0','0','1'),

    ('0','1','1'),

    ('1','0','1'),

    ('1','1','1')

  );

This constant declaration needs a bit of explaining. The constant is called test_data and is of type sample_array. No range has been given for test_data, even though the type is an unconstrained array, because in constant declarations the VHDL analyser can work out the range of an unconstrained array from the size of value given to it. It is good practice to allow the analyser to do this, since the test set will probably change often and it could become tedious remembering to re-count how many tests there are just to give the array the correct range when the analyser can do it for you.

The value of the constant has been given as an aggregate of aggregates. The outer level of aggregate corresponds to the array, which in this case contains eight elements. Each element is itself an aggregate because the element type is a record. The inner level of aggregate therefore corresponds to the sample record, which in turn contains three std_logic values, represented here as character literals.

The test set in this case carries out an exhaustive test of all the possible input permutations. This method of storing the test data in a constant array makes adding or removing tests extremely easy.

The final stage of building this test bench is to write a process that will apply this test set to the circuit under test. The test driver process for this example is:

process

begin

  for i in test_data'range loop

    in0 <= test_data(i).in0;

    in1 <= test_data(i).in1;

    sel <= test_data(i).sel;

    wait for 10 ns;

  end loop;

  wait;

end process;

The process contains a for loop that steps through the test_data array one element at a time. The use of the range attribute means that the loop will automatically adjust to the size of the test set if samples are added or removed. Within the loop, the signals that were connected to the ports of the circuit under test are given values from the current sample.

It is worth looking at one of the assignments a little more closely; consider the assignment to in0:

in0 <= test_data(i).in0;

Note that the constant array test_data is first indexed by the loop constant i to give one of the sample records, which is then accessed by element selection (.in0) to get at the individual std_logic value. The other assignments are similar.

Once the circuit inputs have been given their sample values, the process waits for a time delay of 10 ns. This wait statement is essential, because signals are not updated until the process pauses on a wait statement. When the process pauses at the wait statement, the signals are updated and so the sample values will be applied to the circuit under test. The CUT will then respond to the new stimuli, creating a new value on the output ports. This response will also be visible in the waveform display of the simulator. The delay time gives the CUT time to respond before the next sample is applied.

In principle, a combinational RTL model only needs delta time to respond, but it is good practice to use the specified time delay of the final circuit so that the waveform output has the correct timing for the final design. In this way it is easier to understand the waveform and check its correctness.

The final feature of the test process is the final unconditional wait statement after the loop has completed. If the wait statement was not there, the process would restart and carry out the test again (and again…). The wait statement stops the process completely once all the samples have been applied and therefore stops the simulation. A simulator will always stop when there are no more transactions to be processed.

It is good practice to write test benches to be self-stopping in this way so there is no need to specify a time limit to the simulator. Then, if new tests are added or redundant ones removed, it is not necessary to calculate the simulation time required to carry out all the tests. The test bench will effectively control the simulation time for you.

In summary, here's the test bench in its entirety so that the various parts, which have been presented disjointly, can be seen in context:

entity mux_test is

end;

library ieee;

use ieee.std_logic_1164.all;

architecture test_bench of mux_test is

  type sample is record

    in0 : std_logic;

    in1 : std_logic;

    sel : std_logic;

  end record;

  type sample_array is array (natural range <>) of sample;

  constant test_data : sample_array :=

    (

      ('0','0','0'),

      ('0','1','0'),

      ('1','0','0'),

      ('1','1','0'),

      ('0','0','1'),

      ('0','1','1'),

      ('1','0','1'),

      ('1','1','1')

    );

  signal in0, in1, sel, z : std_logic;

begin

  process

  begin

    for i in test_data'range loop

      in0 <= test_data(i).in0;

      in1 <= test_data(i).in1;

      sel <= test_data(i).sel;

      wait for 10 ns;

    end loop;

    wait;

  end process;

  CUT: entity work.mux port map (in0, in1, sel, z);

end;

This test bench is essentially complete, for a combinational circuit. However, there are a number of enhancements that must be made to cater for clock signals, asynchronous resets and so on. It is also possible to vary the time delay between samples by including the delay time in the sample array. The simplest enhancement though, is to check the responses of the circuit under test. This will be dealt with next.

13.3 Verifying Responses

A test bench that only generates stimuli is useful when designing a circuit and exploring its behaviour, since it is easier to examine the response on a simulator waveform display and confirm that it is right than to hand calculate the correct result. However, once a circuit is complete, it is a good idea to include the response data in the test bench too. This means that the circuit can be tested again at any stage in the future with just a simple go/no-go test. Such a test bench can then become part of a regression test suite. If the test bench finds no response errors, then the circuit is still working correctly. This is particularly useful if design changes have been made to the implementation and it is desired that no changes are made to the behaviour.

The response data is incorporated into the test set by extending the sample record. The record becomes:

type sample is record

  in0 : std_logic;

  in1 : std_logic;

  sel : std_logic;

  z : std_logic;

end record;

The output signal z has now got an entry in the sample record. Then, the response data must be added to the test set, which becomes:

constant test_data : sample_array :=

  (

    ('0','0','0', '0'),

    ('0','1','0', '0'),

    ('1','0','0', '1'),

    ('1','1','0', '1'),

    ('0','0','1', '0'),

    ('0','1','1', '1'),

    ('1','0','1', '0'),

    ('1','1','1', '1')

);

To make it clear, the stimulus and response fields have been separated by an extra space. This is just a convention for readability.

The final change is to include a response check in the test process. The response test is an assertion using the assert statement to check that the actual response is the same as the expected response. If the response does not match, then an assertion error will be reported.

process

begin

  for i in test_data'range loop

    in0 <= test_data(i).in0;

    in1 <= test_data(i).in1;

    sel <= test_data(i).sel;

    wait for 10 ns;

    assert z = test_data(i).z

      report "output z is wrong!"

      severity error;

  end loop;

  wait;

end process;

If no error reports are printed by the simulation, then all responses are known to be correct. There is therefore no need to examine the waveform display to check the circuit behaviour.

Note that the assertion is placed after the wait. The sequence of events for each sample is worth clarifying. First, the signals connected to the inputs of the circuit under test are given the values of a sample stimulus. These signals are updated with their new values when the process pauses at the wait statement. In this case the wait statement pauses for 10 ns to give the circuit time to respond. Then the response is checked against the expected response.

An assertion is a test for a true condition – so if the condition is true nothing happens. But if the condition is false, the report is printed. This is the opposite logic to an if statement used to check for an error. You sometimes see the assert statement used in its abbreviated report form combined with an if statement:

if z /= test_data(i).z then

  report "output z is wrong!" severity error;

end if;

Note that the logic of the test is inverted in this case.

This is a matter of personal taste. The assertion is saying “this condition should hold true” and some designers prefer to think that way, using positive logic. The if statement version is saying “this condition should not happen” and other designers prefer to think that way, using negative logic. Sometimes these can be combined where some part of the condition is negative and another part positive. For example:

if initialisation_complete then

  assert z = test_data(i).z

    report "output z is wrong!"

    severity error;

end if;

This disables the test while a circuit is being initialised, then enables the test once initialisation is complete. This logic is easier to follow than just an assertion:

assert (not initialisation_complete) and (z = test_data(i).z)

  report "output z is wrong!"

  severity error;

If the circuit under test is a behavioural model, then it will have a zero time delay, so the choice of wait time is completely arbitrary. However, if there is a known time requirement, it is good practice to run the circuit at actual speed. This means that the same test bench can be used to test the synthesised circuit.

13.4 Clocks and Resets

So far, the examples have been combinational circuits. For synchronous RTL designs a clock and optionally a reset control will be needed. So the test bench can be extended to generate these signals.

The best way to perform clock generation is within the test process. This has the advantage that the clock will stop when the tests stop. Remember that the test bench should be written so that the simulation is self-stopping, and it is in the generation of clocks that this guideline is usually broken. This is because the most common implementation of a clock generator is as a separate process that simply toggles the clock signal periodically for ever. Such a clock generator will cause the simulator to keep simulating for ever.

It is very easy to implement a single clock within the test process. Multiple clocks can also be accommodated without much extra difficulty. This example, however, will be restricted to a single clock.

The only change in the test bench to test clocked circuits is in the test process. There are no other changes. In particular, there are no changes to the test set.

Consider a simple clocked circuit under test. To keep the example as simple as possible, the example is nothing more than the multiplexer introduced in the previous example, but now with a registered output. The circuit is illustrated in Figure 13.1.

Figure 13.1 Registered multiplexer.

img

The register in the multiplexer is a rising edge sensitive register. This changed circuit obviously will require the addition of a clock signal to the entity:

library ieee;

use ieee.std_logic_1164.all;

entity dmux is

  port (in0, in1, sel, ck : in std_logic; z : out std_logic);

end;

There will also be corresponding changes to the component instance and the set of local signals declared in the test-bench architecture:

entity dmux_test is

end;

library ieee;

use ieee.std_logic_1164.all;

architecture test_bench of dmux_test is

  signal in0, in1, sel, ck, z : std_logic;

begin

  CUT: entity work.dmux port map (in0, in1, sel, ck, z);

end;

The test set will be declared exactly as in the previous example, so that part of the test bench will not be reproduced. The final stage in writing this test bench is the test driver process, which is similar to the previous version of the test process, but now with a clock generator:

process

begin

  for i in test_data'range loop

    in0 <= test_data(i).in0;

    in1 <= test_data(i).in1;

    sel <= test_data(i).sel;

    ck <= '0';

    wait for 5 ns;

    ck <= '1';

    wait for 5 ns;

    assert z = test_data(i).z

      report "output z is wrong!"

      severity error;

  end loop;

  wait;

end process;

Note that this process sets up the new stimuli on the falling (inactive) edge of the clock. Therefore, no change in the register output will happen at this time. The reason for doing this is to keep the changes in data inputs separate from the active clock edge so that there are no problems caused when data inputs change at exactly the same time as the register is clocked. This can be just as much a problem with behavioural designs as it is with gate-level designs. In behavioural designs, the clock signal will typically propagate through fewer intermediate signal assignments than the datapath, so can arrive at the register process one or more delta cycles before the data values. This means that the correct data values will not be loaded into the register.

The two assignments to the clock, with a wait statement after each one, means that a complete clock cycle is generated for each time round the test loop. The response is only checked 5 ns after the rising (active) edge of the clock. The exact relationships between the two clock edges and the sample time can be adjusted by varying the delay times in the wait statements.

If the circuit needs an asynchronous reset, this can be added before the test loop:

process

begin

  rst <= '1';

  wait for 5 ns;

  rst <= '0';

  wait for 5 ns;

  for i in test_data'range loop

This only works for an asynchronous reset since the clock is not running during the reset phase. For a synchronous reset, a combination of clock generator and reset generator can be used. For example, to set the reset signal high for 5 clock cycles:

process

begin

  rst <= '1';

  for i in 1 to 5 loop

    ck <= '0';

    wait for 5 ns;

    ck <= '1';

    wait for 5 ns;

  end loop;

  rst <= '0';

  for i in test_data'range loop

This implements a synchronous reset.

13.5 Other Standard Types

All the examples so far have been based on type std_logic. However, there is no restriction on the type of ports on the circuit under test. This test-bench writing technique can be used with any port type. The key is to simply match the types in the sample record to the types of the ports on the circuit under test. Even array ports can be handled in this way.

As an example, consider the following circuit under test, which is a combinational circuit that counts the number of bits set in the input bus.

library ieee;

use ieee.std_logic_1164.all;

use ieee.numeric_std.all;

entity count_ones is

  port (a : in std_logic_vector (15 downto 0);

        count : out unsigned(4 downto 0));

end;

The test bench for this circuit is much the same as for the multiplexer example – the count_ones circuit is combinational so there is no clock generation part to it. The main difference is that the test set and the internal signals of the test bench should be of types that match the port types of the circuit under test.

entity count_ones_test is

end;

library ieee;

use ieee.std_logic_1164.all;

use ieee.numeric_std.all;

architecture test_bench of count_ones_test is

  type sample is record

    a : std_logic_vector (15 downto 0);

    count : unsigned(4 downto 0);

  end record;

  type sample_array is array (natural range <>) of sample;

  constant test_data : sample_array :=

    (

      ("0000000000000000", "00000"),

      ("0000000000001111", "00100"),

      ("0000111100000000", "00100"),

      ("0000111111110000", "01000"),

      ("0001001000110100", "00101"),

      ("0111011001010100", "01000"),

      ("1111111111111111", "10000")

    );

  signal a : std_logic_vector (15 downto 0);

  signal count : unsigned(4 downto 0);

begin

  process begin

    for i in test_data'range loop

      a <= test_data(i).a;

      wait for 10 ns;

      assert count = test_data(i).count

        report "output count is wrong!"

        severity error;

    end loop;

    wait;

  end process;

  CUT: entity work.count_ones port map (a, count);

end;

Note that the sample data for the input has been written in binary using the string notation. It is also possible to write the samples in hexadecimal, using the bit-string notation, rather than writing out the std_logic_vector in its full width using the string notation. The hexadecimal form of the sample array would then be:

constant test_data : sample_array :=

  (

    (X"0000", "00000"),

    (X"000F", "00100"),

    (X"0F00", "00100"),

    (X"0FF0", "01000"),

    (X"1234", "00101"),

    (X"7654", "01000"),

    (X"FFFF", "10000")

  );

The values for the response are still expressed in binary, since the count output is five bits wide and hexadecimal bit strings can only be used for signals that are a multiple of four bits wide. However, the test bench could be written using integer response data, since the response is meant to represent an integer value, and then the type conversion to_unsigned can be used to convert the expected response into the same type as the output of the circuit under test. This approach makes the test bench more readable and therefore less prone to error. The resulting sample record and test data are shown below.

type sample is record

  a : std_logic_vector (15 downto 0);

  count : integer;

end record;

constant test_data : sample_array :=

  (

    ("0000000000000000", 0),

    ("0000000000001111", 4),

    ("0000111100000000", 4),

    ("0000111111110000", 8),

    ("0001001000110100", 5),

    ("0111011001010100", 8),

    ("1111111111111111", 16)

  );

The assertion that checks the expected response now needs to include the type conversion:

assert count = to_unsigned(test_data(i).count,count'length)

  report "output count is wrong!"

  severity error;

13.6 Don't Care Outputs

Many circuits do not generate valid data outputs on every clock cycle. For example, a pipeline will generate meaningless outputs for the first few clock cycles whilst the pipeline flushes. Similarly, circuits designed to calculate a result over multiple clock cycles will generate invalid outputs whilst calculating intermediate values. There are plenty of other examples where the output of a circuit is not valid for some reason. In these cases, the test bench should be designed to ignore the invalid outputs and only check the valid responses.

One way of achieving this is to add an extra field to the sample record that indicates whether a valid response is expected as a result of that sample. This valid flag is a boolean, which can be set to true when a correct response is expected and false otherwise.

Consider an example circuit that is simply a 1-bit register pipeline that delays its input by three clock cycles. This means that the pipeline will need to be flushed at the start of the simulation and so the first two responses will be invalid. The pipeline example has the following interface:

The test bench for this circuit is similar to the previous ones, so only the differences due to the introduction of the valid flag will be shown.

The first change is to the sample record:

type sample is record

  d : std_logic;

  q : std_logic;

  valid : boolean;

end record;

Then, for each sample in the test set, the flag should be set according to whether the response is valid for that sample or not:

constant test_data : sample_array :=

  (

    ('1', '0',false),

    ('0', '0',false),

    ('1', '1',true),

    ('1', '0',true),

    ('0', '1',true),

    ('1', '1',true));

The final change to the test bench is to switch off testing of the response data when the valid flag is false. The most readable way of doing this is by enclosing the assertion in an if statement:

if test_data(i).valid then

  assert test_data(i).q = q report "q is invalid" severity error;

end if;

An assertion error will be reported only if the condition on the if statement is true and the assertion condition evaluates to false. If the valid flag is false, then the assertion will not be tested. In other words, checking of the response is disabled. If, on the other hand, the valid flag is true, then the assertion will verify whether the q output matches the expected response.

An alternative approach can be used when the response is one of the synthesis types introduced in Chapter 6. These packages define a set of functions called std_match for each of these types that can be used to test the response against an expected value that contains '-' (don't care) values. This can be used instead of the valid flag. To switch off response checking, just make the expected response data contain '-' values. This can be used with some finesse – individual bits can be ignored by placing '-' values in the expected response fields, so only the remaining bits get tested.

The sample record now needs no valid flag:

type sample is record

  d : std_logic;

  q : std_logic;

end record;

Then, for each sample in the test set, the response should be set to an actual value or to '-' according to whether the response is valid for that sample or not:

constant test_data : sample_array :=

  (

    ('1', '-'),

    ('0', '-'),

    ('1', '1'),

    ('1', '0'),

    ('0', '1'),

    ('1', '1')

  );

The assert statement then uses the std_match function directly:

assert std_match(test_data(i).q, q)

  report "q is invalid"

  severity error;

To be more specific, the std_match functions return false if any of the bits in either of the values being compared contains any of the metalogical values 'U', 'X', 'Z' or 'W'. The array forms of the function also return false if the arguments are of different sizes. The functions returns true if the two arguments match element by element, with the value '-' treated as a wildcard that matches any of the remaining values. Note, however, that 'L' only matches 'L' and not '0'– there is no matching of weak values with their strong equivalents.

The biggest drawback of these functions is that the 'Z' value is considered a metalogical value that cannot match anything, so the test results in an error even if a 'Z' is in the expected result – in other words 'Z' does not match a 'Z'. This makes the functions difficult to use for testing tristate buses. However, this limitation aside, the functions are very useful for writing test benches.

13.7 Printing Response Values

So far, when an error has been detected, all the examples have simply printed the message "q is not valid" or some such message, which is not very helpful for diagnosing the problem. The simulator helps by also printing the simulation time of the error, but it can be difficult to translate this simulation time into a diagnosis of which test in the test set has failed. It is far more useful to print out the expected and actual values.

The key to improving the reporting is to recognise that the report part of the assertion can be any string expression. This means it is possible to build up a report by concatenating a series of strings together using the "&" operator.

In VHDL-2008, there is a complete set of overloaded functions for each type called to_string. This provides a way of converting any type to its string representation.

Note: if you are still using VHDL-1993, the to_string functions are to be found in the additions packages that can be downloaded and installed along with the fixed-point and floating-point packages as explained in Chapter 6. For example standard_additions contains the to_string functions for the types defined in package standard: integer, bit, real, boolean, character and time. Similarly, numeric_std_additions contains to_string functions for signed and unsigned that in VHDL-2008 are found in numeric_std. These additions packages can be compiled into ieee_proposed and made available to the test bench with a use clause.

The to_string function for std_ulogic (and therefore also for std_logic) is found in std_logic_1164 (or std_logic_1164_additions) and has the following interface:

function to_string (arg : std_ulogic) return string;

The assertion can then have a more sophisticated report part:

assert test_data(i).q = q

  report "q: expected = " & to_string(test_data(i).q) &

    ", actual = " & to_string(q)

  severity error;

The string to be printed in the report is built up out of sub-strings that are concatenated using the "&" operator.

A further convenience would be to have a way of displaying vector types such as signed and unsigned as integers. The best way to do this is to use type conversions from the array types to integer. Then, the to_string function for integer can be used to print it out. Furthermore, it could be useful sometimes to be able to display integer values as an array of bits and this can be achieved by using type conversions that work the other way round. The type conversions are explained in Section 6.7.

For example, a signed value can be converted to integer and then displayed using the to_string function for integer to give a decimal representation:

assert q = data(i).q

  report "q: expected = " & to_string(to_integer(q)) &

    ", actual = " & to_string(to_integer(data(i).q))

  severity error;

It is also possible to print values of the synthesisable array types (std_logic_vector, signed, unsigned, sfixed, ufixed and float) as hexadecimal or octal strings, using the to_hstring and to_ostring variants of these functions. For example, to print an unsigned value in hexadecimal:

assert q = data(i).q

  report "q: expected = " & to_hstring(q) &

    ", actual = " & to_hstring(data(i).q)

  severity error;

For consistency, there is a to_bstring that is identical to the to_string function.

Finally, there are long forms of these functions: to_binary_string, to_hex_string and to_octal_string for those that find long names add to the readability of the model.

13.8 Using TextIO to Read Data Files

All the examples of test benches so far have been completely self-contained. That is, all the stimulus and response data is built into the test bench itself. There are situations where it makes more sense to read the stimulus and response data from a data file. An example of such a situation is where the test data has been generated from an automated tool. Rather than go through the tedious task of editing this test data into the test bench, it is better to read it straight from the data file during simulation.

VHDL has an I/O system that allows this to be done. The built-in procedures for performing text I/O are contained in the appropriately named package textio, which is part of the VHDL standard and so is found in library std.

Package textio provides just enough functionality to read data files, so there isn't much room for ambiguity about how to use the package. However, it should be noted that the text I/O in VHDL is quite different from I/O in other (software) languages.

The textio package is listed in Appendix A.10, so that can be used as a reference. The basic subprograms that will be used in this example are:

procedure file_open (file f : text;

                     name : in string;

                     kind : in file_open_kind := read_mode);

procedure file_close (file f : text);

function endfile (file f : text) return boolean;

procedure readline (file f : text; l : inout line);

procedure read (l : inout line; value : out type);

There are two special types used in these subprograms: text and line. Type text is a type that represents a text file, whilst type line is a type that represents a line of text from that file.

The basic sequence of events in processing text files is, first to open the file, then as long as there is text to process, read a whole line of text from the file. Text can only be read a whole line at a time using the readline procedure. Once a text line has been read, then a set of read procedures can be used to disassemble the text line into its elements and convert the elements into VHDL types. In the simplified interface above, this is represented by the read procedure's last parameter where type can be replaced by any VHDL type supported by textio.

In order to illustrate the use of textio in practice, the original test bench from the beginning of the chapter, which was used to illustrate the basic test-bench structure, will be rewritten to use it. Initially, the example will be written for ports of type bit and later it will be rewritten to use type std_logic. This two-stage introduction is necessary because std_logic is not directly supported by textio so an example using bit, which is directly supported, is used to cover the basics.

Like all the other test benches, the first stage is to declare the component instance with signals connected to each port. This gives a skeleton test bench:

entity mux_test is

end;

use std.textio.all;

architecture test_bench of mux_test is

  signal in0, in1, sel, z : bit;

begin

  CUT: entity work.mux port map (in0, in1, sel, z);

end;

Note that the package textio has been made visible by a use clause so that it can be used in writing the test bench.

Unlike the previous examples, there will be no test set or data structures to store the test data. Therefore, the only thing left to do is to write the test process. The test process is:

process

  file data : text;

  variable sample : line;

  variable in0_var, in1_var, sel_var, z_var : bit;

begin

  file_open(data, "mux.dat", read_mode);

  while not endfile(data) loop

    readline (data, sample);

    read (sample, in0_var);

    read (sample, in1_var);

    read (sample, sel_var);

    read (sample, z_var);

    in0 <= in0_var;

    in1 <= in1_var;

    sel <= sel_var;

    wait for 10 ns;

    assert z = z_var report "z incorrect" severity error;

  end loop;

  file_close(data);

  wait;

end process;

This process needs some explanation.

First, the file declaration at the start of the process declares a file, which is then opened at the start of the process using the file_open procedure. It is opened with the identifier data of type text and kind read_mode (readable only), which is to be associated with a file called “mux.dat”.

The process contains a while loop. The while loop has not been covered before since it cannot be synthesised. A while loop is a loop that continues for as long as its condition remains true. In this case, the condition is: not endfile(data). This expression is first of all a function, endfile, testing whether the end of the file identified as data has been reached. The not inverts the sense of this so that the loop continues for as long as the end of the file has not been reached.

Within the loop, the first step is to read a line of data into the variable sample, which is of type line. This is done by the call to the procedure readline. Then, there follows a series of read operations, each of which reads a bit from the line. Note that a read operation reads from the line, not from the file itself. The read operations effectively consume the line, so each one starts reading where the previous read left off. Any attempt to read a value beyond the end of a line or to read a value of the wrong type, will result in an assertion error being raised.

The read operations need to be passed a variable to fill with the value read. This must be a variable and not a signal – the parameter is an out parameter of kind variable. This is why the example has separate read operations on intermediate variables that are then immediately assigned to signals.

The outputs of the circuit under test can be compared directly with the variable, as the assertion in the example shows.

Finally, at the end of the process, the file is closed using the file_close procedure.

The file itself will contain a series of lines of text, each one containing values of the correct type to match the values being read. In this case, the correct values are 0 or 1, since the type being read is a bit. Note that the values are the digits 0 and 1, without any quotes. Any whitespace (spaces or tabs) between the values will be ignored.

For example, a typical data file for this test bench would be:

000 0

010 0

100 1

110 1

001 0

011 1

101 0

111 1

In this example, for clarity, the stimuli have been grouped together and whitespace has been used to separate the response data.

Consider a test bench that reapplies the same test data twice. This is not necessary for this example, but it could be useful in a design where the same test data is to be applied to a circuit with a number of different modes. The same data file could be reused for each mode. However, in this example the same data set will simply be applied twice by enclosing the tests in an outer for loop.

process

  file data : text;

  ...

begin

  for i in 1 to 2 loop

    file_open(data, "mux.dat", read_mode);

    while not endfile (data) loop

      ...

    end loop;

    file_close(data);

  end loop;

  wait;

end process;

13.9 Reading Standard Types

Package textio is not limited to use with type bit. There are read operations defined for reading all of the synthesisable types from package standard – specifically:

bit

bit_vector

boolean

character

string

integer

There is also support for non-synthesisable types:

real

time

These can be useful within a test bench even though they cannot be used in a synthesisable design.

When reading the character types, bit and character, the read operation simply reads a single character from the line and then tries to match it to the type. For bit, any leading whitespace (spaces and tabs) is skipped first. For character, no skipping is done, since the whitespace characters are valid members of character.

When reading the string types, bit_vector and string, the read operation reads as many characters as necessary to fill the variable passed to the read procedure and tries to match them to the element type. In other words, the read operation adapts to the size of the variable. Once again, for bit_vector, leading whitespace is skipped before the first valid character (but not for subsequent characters in the string value), whereas for string it is not because the whitespace characters are valid members of a string.

To give an example, the following would allow the reading of an 8-bit bit_vector:

process

  variable byte : bit_vector (7 downto 0);

  ...

begin

  ...

  read(sample, byte);

When reading type integer, an optional sign followed by numeric digits are read until a non-numeric digit is found and then the result is converted to integer. Any leading whitespace will be skipped first. Generally, the best way to terminate an integer in a data file is to use whitespace.

The final type of use in writing test benches, which can be read but that is not synthesisable, is type time. This means that even the delays used in the wait statement that controls the testing can be read from the data file. The following is a template for reading a time and then using it in a wait statement:

process

  variable delay : time;

  ...

begin

  ...

  read(sample, delay);

  wait for delay;

The file format for times is the same as in VHDL – a number representing the time followed by a space and then a string representing the units. A number on its own is not a valid time and therefore will result in an error. Also, the space between the number and the unit is required. For example, here is a data file with the time value at the end of the line:

000 0 10 ns

010 0 10 ns

100 1 10 ns

110 1 10 ns

001 0 10 ns

011 1 10 ns

101 0 10 ns

111 1 10 ns

Note that the times are relative times – in other words, each delay is an additional 10 ns in this example. To use absolute times (measured from the start of simulation), simply subtract the time now from the delay in the wait statement:

process

  variable delay : time;

  ...

begin

  ...

  read (sample, delay);

  wait for delay - now;

The word now refers to a function with no parameters that returns the current simulation time. Note that if the delay is less than now, the result of the subtraction is negative, and this will raise an error because it is not possible to wait for a negative time. It is therefore essential that the samples in the file are sorted into time order:

000 0 10 ns

010 0 20 ns

100 1 30 ns

110 1 40 ns

001 0 50 ns

011 1 60 ns

101 0 70 ns

111 1 80 ns

13.10 TextIO Error Handling

There are in fact two read operations provided for each type:

procedure read(l : inout line; value : out type);

procedure read(l : inout line; value : out type;

  good : out boolean);

The basic read operation that was used in the previous example was the first of these procedures. If an error occurs when trying to read the type, an assertion of severity error will be raised.

The second version of the read procedure returns a boolean flag called good to signify whether the read was successful or not, instead of raising an error. This can be useful to allow the test bench to skip over, for example, blank lines or comments in the data file. To illustrate this, here's a simple modification that allows the test bench to skip over a line if the first read of a bit fails:

process

  file data : text;

  variable sample : line;

  variable in0_var, in1_var, sel_var, z_var : bit;

  variable OK : boolean;

begin

  file_open(data, "mux.dat", read_mode);

  while not endfile (data) loop

    readline (data, sample);

    read (sample, in0_var, OK);

    if OK then

      read (sample, in1_var);

      read (sample, sel_var);

      read (sample, z_var);

      in0 <= in0_var;

      in1 <= in1_var;

      sel <= sel_var;

      wait for 10 ns;

      assert z = z_var report "z incorrect" severity error;

    end if;

  end loop;

  file_close(data);

  wait;

end process;

This would then allow the data file to be annotated with comments and formatted for readability:

# test set for the exhaustive simulation of MUX

# in0 in1 sel z

  0 0 0 0

  0 1 0 0

  1 0 0 1

  1 1 0 1

  0 0 1 0

  0 1 1 1

  1 0 1 0

  1 1 1 1

It is also possible to append comments to each line, since the read operations only read up to the last value in the sample. You are not obliged to read the whole line before moving on to the next, so any comment strings at the end of the line can simply be ignored.

13.11 TextIO for Synthesis Types

The built-in read operations covered by textio only cover the standard built-in types of VHDL. They do not cover some of the other types in common use and, in particular, they don't cover std_ulogic or any of the synthesis types. This section describes standard extensions to textio for the synthesis types.

In the original standards for the synthesis packages std_logic_1164 and numeric_ std, no I/O operations were defined. I/O was provided for the std_logic_1164 types by many vendors providing a non-standard I/O package called std_logic_textio. However, being non-standard this package could not be guaranteed to be available. Package std_ logic_textio is now obsolete and its use is deprecated.

In VHDL-2008 all of the synthesis packages have I/O operations in the packages themselves.

In VHDL-1993, the compatibility versions of the new packages fixed_pkg and float_pkg have I/O operations built-in. For packages std_logic_1164 and numeric_std, additions packages have been provided in the same way and from the same source as the fixed-point and floating-point packages described in Chapter 6.

So, the following types have I/O procedures. The VHDL-2008 package and, where appropriate, the VHDL-1993 additions package providing the I/O operations for older systems is listed with the types covered by that package:

std_logic_1164 (VHDL-1993: std_logic_1164_additions)

  std_ulogic

  std_logic_vector

  std_ulogic_vector

numeric_std (VHDL-1993: numeric_std_additions)

  unsigned

  signed

fixed_pkg

ufixed

  sfixed

float_pkg

  float

These packages provide the following procedures:

procedure read (l : inout line; value : out type);

procedure read (l : inout line; value : out type;

                good : out boolean);

procedure write (l : inout line; value : in type;

                 justified : in side := right;

                 field : in width := 0);

Where type is one of the synthesis types, depending on the I/O package.

When reading the basic type, std_ulogic, the read operation simply reads a single character from the line and then tries to match it to the type. Any leading whitespace (spaces and tabs) is skipped first.

Note that the I/O routines for std_ulogic can also be used for the more commonly used std_logic, since std_logic is simply a subtype of std_ulogic.

Furthermore, the range of I/O operations has been extended to provide binary, octal and hexadecimal support for bit-array types. These packages also provide the following procedures for the array types only:

procedure oread (l : inout line; value : out type);

procedure oread (l : inout line; value : out type;

                 good : out boolean);

procedure hread (l : inout line; value : out type);

procedure hread (l : inout line; value : out type;

                 good : out boolean);

procedure owrite (l : inout line; value : in type;

                  justified : in side := right;

                  field : in width := 0);

procedure hwrite (l : inout line; value : in type;

                  justified : in side := right;

                  field : in width := 0);

When reading the array types the read operation reads as many characters as necessary to fill the variable passed to the read procedure and tries to match them to the element type. Once again, leading whitespace is skipped before the first valid character but not between characters. In octal (oread) and hexadecimal (hread) forms, the value read from the file is expanded into a bit-string and assigned to the variable. The variable must be a multiple of 3 bits long for octal and four bits for hexadecimal.

13.12 TextIO for User-Defined Types

The recommendation for logic synthesis is to use the synthesis types for most datapaths, so it is rare to need to write I/O procedures. However, sometimes user-defined types are needed and then I/O procedures can be provided for those types as well.

The most difficult read procedure to create is a read procedure for a named enumeration such as is used for writing FSMs (Section 12.2). A shortcut would be to read an integer that represents the position value and then convert the value to the target enumeration type using the val attribute. It has the disadvantage that the values of the enumeration type are stored in the data file as integer values, which is not very readable, but it is a quick solution.

However, it can be useful sometimes to allow the enumeration values to be written in the data file as string values, so in this case a means must be found of matching the enumeration values with the string values. Indeed, this is how type boolean is read. The file representation of boolean uses the strings "true" and "false" (without the quotes of course). To illustrate the method for reading enumeration values, here's a possible implementation for a read procedure for reading the following enumeration type:

type light_type is (red, amber, green);

The read procedure will be written to skip any preceding whitespace, then it will try to match the string representation of the enumeration values with the contents of the line.

procedure read (l : inout line; value : out light_type;

                good : out boolean) is

begin

  skipwhite(l);

  if (l.all'length >= 3 and

      l.all(l.all'left to l.all'left + 2) = "red")

  then

    value := red;

    skip (l, 3, good);

  elsif (l.all'length >= 5 and

         l.all(l.all'left to l.all'left + 4) = "amber")

  then

    value := amber;

    skip (l, 5, good);

  elsif (l.all'length >= 5 and

         l.all(l.all'left to l.all'left + 4) = "green")

  then

    value := green;

    skip (l, 5, good);

  else

    value := red;

    good := false;

  end if;

end;

This procedure uses the knowledge that the line type used in textio is an access type (a pointer in software terminology) to a string. Access types will not be covered in any detail by this book, but are being used here. It is sufficient to know that the .all selection is used to dereference the pointer and access the string itself. In this case, a slice of the line is being compared with string values for the text representing the values of the light_type type. Note that the uppercase representations could easily be incorporated into this procedure.

The read procedure above uses a procedure for skipping whitespace. This works by counting the number of whitespace characters that need skipping and then using uses the built-in read procedure for string to read a string of that length, effectively consuming the whitespace from the line in one go. This is more efficient (i.e. faster) than skipping one character at a time. The skipwhite procedure is:

procedure skip (l : inout line; length : in natural) is

  variable str : string (1 to length);

  variable good : boolean;

begin

  read (l, str, good);

end;

procedure skipwhite (l : inout line) is

  variable length : natural := 0;

begin

  for i in l.all'range loop

    exit when l.all(i) /= ' ' and l.all(i) /= HT;

    length := length + 1;

  end loop;

  if length > 0 then

    skip (l, length);

  end if;

end;

Note that the skipwhite procedure skips both spaces and tabs (a tab is indicated by the character literal HT, which stands for Horizontal Tab).

However, the read procedure is clearly clumsy since the same logic is used in every comparison. It would be better implemented as a loop:

procedure do_compare(l : inout line;

                     value : in light_type;

                     found : out boolean) is

  constant image : string := value'image;

begin

  if ((l.all'length >= image'length) and

      (l.all(l.all'left to l.all'left + image'length) = image))

  then

    skip (l, image'length);

    found := true;

  else

    found := false;

  end if;

end;

procedure read (l : inout line; value : out light_type;

                good : out boolean) is

  variable found : boolean := false;

begin

  good := true;

  skipwhite(l);

  for possible in light_type'range loop

    do_compare(l, possible, found);

    value := possible;

    exit when found;

  end loop;

  if not found then

    good := false;

  end if;

end;

The read procedure loops through all the possible values of the enumeration type, calling the do_compare procedure with each one. If a match is found, then the result is set to the value that caused the match and the loop exits. Otherwise, it keeps looping until all values have been tried. If a match is not found after all the values have been tried, the good flag is set false to indicate that the read failed.

The do_compare procedure works by first declaring a local constant string that is initialised with the image of the possible value of the type. This is done in this way because the size of the string varies from one call of the procedure to another, and this is the conventional way of capturing an unknown-length array in a local constant so that it can be referenced many times. Then, the comparison of the string value with the contents of the line of the file is performed by the if statement. If this matches, the requisite number of characters in the line is skipped and the found flag set.

13.13 Worked Example

13.13.1 Systolic Processor

This example develops a test bench to test the systolic processor developed in Section 10.7. The test data will run the processor through a single calculation and check the results as they shift out of the processor.

The test set will be:img

The original entity to be tested was:

library ieee;

use ieee.std_logic_1164.all;

use ieee.numeric_std.all;

entity systolic_multiplier is

  port (d : in signed (15 downto 0);

        ck, rst : in std_logic;

        q : out signed (15 downto 0));

end;

To make the test bench more readable, values will be represented by integer values in the test set. These integer values will then be type converted to type signed, which is the port type of the systolic processor. The conversion will be performed by using the type conversion to_unsigned from package numeric_std. A valid flag will be used since there are many times during the test when the data output is of no interest and there is no way of representing don't cares in the integer representation.

The test bench for this circuit is:

entity systolic_multiplier_test is

end;

library ieee;

use ieee.std_logic_1164.all;

use ieee.numeric_std.all;

architecture test_bench of systolic_multiplier_test is

  type sample is record

    d : integer;

    rst : std_logic;

    q : integer;

    valid : boolean;

  end record;

  type sample_array is array (natural range <>) of sample;

  constant test_data : sample_array :=

    (

      (0, '1', 0, false), -- reset

      (1, '0', 0, false), -- ld_a11

      (2, '0', 0, false), -- ld_a12

      (3, '0', 0, false), -- ld_a13

      (4, '0', 0, false), -- ld_a21

      (5, '0', 0, false), -- ld_a22

      (6, '0', 0, false), -- ld_a23

      (7, '0', 0, false), -- ld_a31

      (8, '0', 0, false), -- ld_a32

      (9, '0', 0, false), -- ld_a33

      (1, '0', 0, false), -- ld_b1

      (2, '0', 0, false), -- ld_b2

      (3, '0', 0, false), -- ld_b3

      (0, '0', 0, false), -- calc1

      (0, '0', 0, false), -- calc2

      (0, '0', 0, false), -- calc3

      (0, '0', 0, false), -- calc4

      (0, '0', 14, true), -- calc5

      (0, '0', 0, false), -- calc6

      (0, '0', 32, true), -- calc7

      (0, '0', 0, false), -- calc8

      (0, '0', 50, true) -- calc9

    );

  signal d, q : signed (15 downto 0);

  signal ck, rst : std_logic;

begin

    

  process

  begin

    for i in test_data'range loop

      d <= to_unsigned(test_data(i).d, d'length);

      rst <= test_data(i).rst;

      ck <= '0';

      wait for 5 ns;

      ck <= '1';

      wait for 5 ns;

      if test_data(i).valid then

        assert to_unsigned(test_data(i).q, q'length) = q

          report "q: expected = " &

                  to_string(to_unsigned(test_data(i).q)) &

                 ", actual = " & to_string(q);

          severity error;

      end if;

    end loop;

    wait;

  end process;

  CUT: entity work.systolic_multiplier

    port map (d, ck, rst, q);

end;

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.198.49