Jump to content

User:Dock3230/sandbox

From Wikipedia, the free encyclopedia

Maia
ParadigmImperative (procedural), declarative
Designed byEvan Lavelle
Stable release
2019.11 / November 2019; 4 years ago (2019-11)
Typing disciplineStatic and dynamic, configurable level
OSCross-platform
Filename extensions.tv
Websitewww.maia-eda.net
Influenced by
VHDL, Verilog, ABEL, C, Java

Maia is a compiled Hardware verification language, and is designed specifically to verify designs written in the Verilog and VHDL Hardware description languages.

The language provides facilities to communicate with a DUT, and to automatically drive DUT inputs, sample and test DUT outputs, and advance time. Overall control is provided by a set of procedural imperative constructs, which are designed to be familiar (and in many cases identical) to those provided by C and related languages.

The compiler (or 'transpiler') produces Verilog output which is, in most cases, a stand-alone self-checking testbench. This must be run on an HDL simulator either under the simulator GUI or, more commonly, as a batch-mode command-line simulation.

Maia is generally used for running large sets of automated tests (unit, regression, or acceptance tests, for example). A Tcl test environment is provided with the compiler for automating test runs.

The language was created in the early 2000s by Evan Lavelle, as a means to eliminate the low-level and detailed VHDL and Verilog coding which was required to verify HDL modules, and to automate the creation of reusable self-checking testbenches.

Overview[edit]

HDL designs are normally verified directly in the language they are written in (Verilog or VHDL), but this requires a great deal of low-level detailed coding. This code is not directly related to the functionality of the DUT, and instead handles the language-specific details which are required to run a simulation, including:

  • Writing clock and reset generators
  • Defining and driving device inputs at the correct times
  • Recording device outputs at the correct times and comparing them to expected (static or dynamic) values
  • Pipeline setup and flush
  • Forcing internal signals
  • Avoiding race conditions
  • File I/O to read test stimulus, or expected output values

This coding becomes significantly more complex if the DUT includes timing information (as a back-annotated netlist, for example) and the testbench must be written to exercise and verify the worst-case DUT timing.

This complexity means that verifying a hardware module can be significantly more difficult than actually writing it. The skills required to write a Verilog or VHDL testbench also differ from those required to code the hardware to be tested, since they are essentially Software engineering, rather than Electronic engineering. It is common for Electronic Engineers to be proficient only in the 'synthesizable subset' of their preferred HDL, and this subset does not include the language features required to write a testbench. This means that, in many cases (particularly in FPGA design), hardware is poorly verified, and verification is replaced with lab testing, which can greatly increase development times.

Maia addresses these issues by generating self-checking testbenches, with no requirement for low-level coding, primarily through the use of drive statements. Information about the DUT must be provided in a 'DUT section', which includes a declaration of the module and any internal signals to be tested, along with the required clocks and, if required, signal timing information.

The language is intended to be used by RTL engineers to verify their own designs or, for example, by technical authors, who do not have the detailed VHDL or Verilog knowledge required to exercise a DUT.

Drive statements[edit]

Drive statements provide a mechanism to drive a defined subset of the DUT inputs, and to test the resulting outputs. In the simplest case, static values are applied to the inputs, and the output is tested against another static value at some later time. This code, for example, is a complete test for a single 2-input AND gate:

// the DUT section:
DUT {
   module and2(input A, B, output C) // the DUT declaration
   [A, B] -> [C]                     // declarations of any drive statements to be used
}

// the drive statements:
[0,0] -> [0]
[0,1] -> [0]
[1,0] -> [0]
[1,1] -> [1]

If this code is saved in file and2.tv, and file and2.vhd contains an implementation of an AND gate, then that implementation can be tested as follows:

$ rtv and2.tv and2.vhd

If the gate implementation is correct, the simulation output is as follows:

(Log) (40 ns) 4 vectors executed (4 passes, 0 fails)

The compiler produces Verilog output, which is run in a batch-mode simulation by the rtv driver. rtv is not an HDL simulator: it simply runs the simulation on a pre-installed Verilog simulator (which must support IEEE 1364-2005, and which must be a dual-language simulator if the test module is coded in VHDL).

Drive statements can contain directives as well as values to drive or test against. The .C directive, for example, instructs the testbench to drive a clock waveform on a given DUT input, or to respond to a clock signal on a DUT output. The clock must be declared in the DUT section with a create_clock statement, which allows the compiler to identify the relevant DUT pin, and the clock waveform.

A program which is entered in this form is known as a test vector program, and can contain only a DUT section, and a list of test vectors. These vectors can contain only constant values for input driving and output testing (the signals on the left and right of the drive statement are generally referred to as DUT 'inputs' and 'outputs', respectively, but the only requirement is that the relevant signal can be driven or read, and a bidirectional signal may appear on both sides of the statement). This simple test vector form was influenced by ABEL test vectors. ABEL is now obsolete, but was historically used for the definition and test of PAL devices.

The test vector form is generally of limited value, and tests are normally written in an alternative procedural form. In this second form, the program must contain at least one function, which is named main. Drive statements must now appear inside a function, and can contain arbitrary expressions, rather than just constant values. The use of control flow constructs in a function allows tests to be 'reactive' (in other words, to respond to the current state of the DUT), while the use of expressions in the drive statement allows DUT inputs and expected outputs to be derived algorithmically.

Device timing parameters may be entered in the DUT section as input setup and hold times, and output hold and delay times, for both program forms. If timing parameters are present, the drive statement drives the relevant inputs with worst-case timing, and tests the relevant outputs to confirm that they are valid (and do not glitch) throughout the 'stability window' defined by the output hold and delay parameters.

If timing parameters are not present, the simulation is carried out as a unit-delay simulation. In this context, 'unit delay' means that, for synchronous logic (in other words, for a drive statement for which a relevant create_clock declaration can be found), the inputs are driven 'just before' the clock edge, and the outputs are tested 'just after' the clock edge. For combinatorial logic, the compiler defines a pseudo-'cycle time', and chooses a point in the cycle at which to drive the inputs, and tests outputs 'just after' this time.

Logic system[edit]

Maia uses a two-value (0 and 1) logic system for bit objects, and a 4-value logic system (0, 1, X, and Z) for var objects. The 4-value system is the same as Verilog's 4-value system; X denotes an unknown value, while Z denotes tri-state.

The AND gate example above can be extended to confirm X-propagation through the gate by adding these vectors:

[.X,0] -> [0]
[.X,1] -> [.X]
[0,.X] -> [0]
[1,.X] -> [.X]

Procedural language[edit]

There are a number of language features which are specifically related to accessing the DUT, concurrency, and advancing time. The remaining constructs form a simple imperative procedural language which is intended to be familiar to users of C and related languages. This is both statically and dynamically typed, and lexically scoped.

A program consists of a sequence of external declarations, which are either declarations or function definitions. The program has a main entry point. The language has a full set of control flow primitives: if-else, for, while, do-while, switch, and for all.

The expression syntax is almost identical to C, although Maia adds a number of operators. Scoping is, for all practical purposes, identical to C scoping (within a given source file).

The main changes and simplifications are as follows:

  1. An entire program must be compiled at the same time, as a single compilation unit (in the same way as Verilog or Pascal, for example); there is no provision to link separately-compiled units
  2. There are no pointers. However, parameters can be passed by reference to functions (in the same way as C++)
  3. There is no standard library, although a small number of functions (rand, exit, assert, and so on) are built into the language
  4. The primary difference between Maia and C is in the handling of 'data' types. Maia has three basic data types:
    1. int is a 2-state signed 'small' integer with a fixed width (which defaults to 32 bits). This type is of little value in hardware description or verification, but is useful for general software 'housekeeping': loop indexing, counting, and so on
    2. bitn is a 2-state data type; the n denotes the number of bits in the type (a bit192, for example, is a 192-bit 2-state type)
    3. varn is a 4-state data type; the n denotes the number of bits in the type. The 4 states are the same as the Verilog states: 0, 1, X, and Z
  5. In addition to the data types, there is a boolean type (bool), a structure type (struct), a stream type (stream), and an array type
  6. Arrays are first-class objects, and are declared using a Java-style syntax (which is a superset of C syntax). Both int foo[4][3] and int[4][3] foo are supported, for example, although the latter is preferred since it can be used to declare functions which return arrays. Multi-dimensional arrays can be declared with either Algol-style or C-style syntax (int[4][3] foo is equivalent to int[4,3] foo). The latter style is more familiar to VHDL programmers, and more convenient when accessing arrays with several dimensions (these include Karnaugh maps, for example, which can be declared and manipulated directly in Maia)
  7. Concurrency is built into the language. The exec statement creates a new thread, and initiates execution of a function in that thread. This is particularly useful for DUTs which have multiple asynchronous clocks, or which have an interface which produces data at times which cannot easily be predicted

The data type specialisations include ubit and uvar, which are unconstrained bit and var types. These have no predefined size, and take their size from the context. This program, for example, declares 8 variables with sizes ranging from 1 to 8 bits, and checks the parity of each variable:

main() {
   bit1 a = 1'b1;          assert( odd_parity(a));
   bit2 b = 2'b01;         assert( odd_parity(b));
   bit3 c = 3'b101;        assert(!odd_parity(c));
   bit4 d = 4'b0101;       assert(!odd_parity(d));
   bit5 e = 5'b10101;      assert( odd_parity(e));
   bit6 f = 6'b010101;     assert( odd_parity(f));
   bit7 g = 7'b1010101;    assert(!odd_parity(g));
   bit8 h = 8'b01010101;   assert(!odd_parity(h));

   report("_assertCount: %d; _errorCount: %d\n", _assertCount, _errorCount);
}

bool odd_parity(ubit a) {
   for(int i=0; i<a'size; i++)
      result ^= a.(i);
}

When run, this (complete) program produces the output _assertCount: 8; _errorCount: 0, confirming that 8 assertions were executed, with no failures. This program uses a number of other language features:

  • result is a predefined variable, and the current value of result is returned in the absence of an explicit return. In this case, result is of type bool, and so automatically initialises to false. However, it is common to explicitly initialise variables, for clarity
  • a.(i) uses the bitslice operator to select a single bit in a. The selection is dynamic in this case, and so cannot be checked for correctness during compilation. The index is instead checked at runtime, and a runtime error is raised if it is out of range
  • ' is the attribute operator; a'size returns the number of bits in a
  • The level of type checking is set by a pragma, and defaults to approximately C-like. In this case, this means that each bit of a can be XORed with a boolean value
  • The language has a number of predefined variables which have a leading _ in their name. These are normally used for reporting test results
  • It is not necessary to declare odd_parity before its use in this program. A compiler pre-pass finds external objects and allows them to be forward-referenced
  • There is no requirement to include any 'standard header files'

Operators[edit]

A hardware register is likely to contain a data pattern which can be interpreted in many different ways at different times. It might, for example, contain a 2's-complement integer, or a block floating-point value, or a fixed-point value. The normal software approach to modelling this complexity is to create a specific type which can be used to represent the current data interpretation. In this simple case, we require three types: one for signed integers, one for block floating-point, and one for fixed-point. We can then define an operator, such as a multiplication operator, for objects of these types. A single * operator, for example, might be defined to carry out all of signed integer multiplication, block floating-point multiplication, and fixed-point multiplication. This is the essence of Object-oriented programming. In OOP, the object is central. Objects have a specific type, and the type has multiple properties which define what can be done with objects of that type, and how.

While this is a proven paradigm for general-purpose high-level programming, it is a poor fit for the description and verification of electronic hardware. A register is simply a register, and holds a binary data value. At different times, the register may be connected to function units which carry out two's complement multiplication, block floating-point multiplication, or fixed-point multiplication. In this paradigm, complexity is provided by operators (the function units), and not by objects (the registers or memory locations).

Maia takes this second approach, and models hardware in much the same way that it would appear on a schematic. Data objects have no properties apart from their size, and whether they represent 2-state or 4-state data. These objects are therefore not 'signed', or 'unsigned', or 'floating point': they are simply data. Complexity is provided by operators, rather than types.

The operators in Maia are generally identical to those in C, except that Maia adds bitslice and rotate operators, and some attribute operators. None of the C operators related to pointers are supported. Where the operators are identical, Maia and C have identical precedence and associativity. The basic operator symbol is followed by, in order:

  1. An optional # character, which represents a 'signed' operator
  2. An optional $n, where n represents the operator size

A signed operator is identical to a default 'unsigned' operator, except that:

  1. It sign-extends the inputs (which are expected to be 2's complement) if necessary
  2. It may have a different behaviour from its unsigned counterpart (signed and unsigned comparisons have different behaviour, for example)

Some examples of operators are:

+$10       10-bit integer addition
/#$20      Signed 20-bit integer division
<=#        Signed integer less-than-or-equal
=#         Sign-extending assignment (assignment is an operator)
e1.(e2:e3) Extract the inclusive descending bit range [e2,e3] from e1 (a 'bitslice')
.R<<       Rotate left

Operators generally precisely specify the required operation, rather than relying on the compiler to deduce the programmer's intent from the surrounding context. However, in some circumstances it would be tedious to continuously write +$96, for example, where a datapath is known to be exclusively 96-bit. Maia does therefore deduce the required operator size when a base unsized integer or logic operator is used. The procedure used is the normal one: for binary operators, for example, an unsized operator is assumed to be the same size as the larger of the two operands. On assignment, the result is truncated if necessary. If the result has to be extended on assignment, then it is zero-extended for a plain assignment (=), and sign-extended for a signed assignment (=#).

Example programs[edit]

Hello World[edit]

Maia's character set is UTF-8. However, the underlying simulator may support only the printable ASCII character set, in which case this program will display incorrectly (although UTF-8 may be used in variable names and comments without problems). Note that there is no DUT section in this program: this is only required when it is necessary to instantiate and access a DUT.

main() {
  report("Καλημέρα κόσμε\n");
}

MAC module test[edit]

This program verifies a DUT which implements a configurable MAC. The DUT has two 4-bit inputs, and a 10-bit output; it has a configurable pipeline depth, which is set by the stages parameter. The DUT is tested by cycling through all combinations of the two 4-bit inputs, and checking the resulting 10-bit output at each step (after the appropriate pipeline delay). The module declaration in the DUT section is translated into a module instantiation in the compiled output, and the stages parameter is set to the defined value of NSTAGES, thus setting the pipeline depth:

#define NSTAGES 3                   // set a pipeline depth of 3: the preprocessor is cpp-compatible

DUT {
   module MAC1                      // declare the module interface, by pasting in the Verilog module declaration,
      @(.stages(NSTAGES))           // replacing '#' with '@'. for a VHDL module, you will need to write a Verilog-
       (input  RST, CLK,            // equivalent declaration
        input  [3:0] A, B,
        output [9:0] Q);
   
   [RST, CLK, A, B] -> [Q];         // declare any drive statements to be used
   create_clock CLK;                // declare the module clock, with default waveform and timing (50%, 10ns)
}

main() {
   bit4  a, b;                      // 4-bit, 2-state. 'bit' objects initialise to 0
   bit10 sum = 4;                   // 10-bit, 2-state, initialise to 4

   [1, .C, -, -] ->NSTAGES [0];     // reset: check that Q is 0 after NSTAGES cycles
   [0, .C, 2, 2] ->NSTAGES [sum];   // sanity check: confirm that 2*2=4

   for(int i=0; i<16; i++) {
      for(int j=0; j<16; j++) {
         // derive the expected result as 'sum'. attach 'a' and 'b' to an 8-bit multiplier,
         // zero-extend the result and add it to the 10-bit 'sum', drive the inputs, and
         // confirm that the DUT output is the same as 'sum' NSTAGES cycles later
         sum += (a *$8 b);
         [0, .C, a, b++] ->NSTAGES [sum];
      }
      a++;
   }
}

If the HDL module is correctly coded, the testbench reports:

(Log) (2600 ns) 258 vectors executed (258 passes, 0 fails)

Note that the testbench runs for 260 cycles (at the default period of 10ns), rather than the 258 cycles which might be expected for the 258 test operations. The additional 2 cycles are required to flush the pipeline, and extract the final two values for testing. The pipeline load and flush are handled automatically by the compiler.

The for all construct can be used to simplify loops over the range of a 'small' variable. The loop above can be replaced with:

   for all a {
      for all b {
         sum += (a *$8 b);
         [0, .C, a, b] ->NSTAGES [sum];
      }
   }

Transpiler limitations[edit]

The reference compiler currently produces 'vanilla' Verilog (which conforms to IEEE 1364-2005[1]). 1364-2005 was the last release of the original Verilog language. This ensures maximum compatibility with free simulators, and may also allow the use of cheaper versions of commercial SystemVerilog simulators. However, Verilog has significant limitations, in areas including memory management, the implementation of fully automatic functions, and access to the underlying hardware. These limitations are reflected in a number of restrictions and lack of functionality in the current compiler output, which are:

  1. Recursive functions are not supported; the compiler raises an error when a function attempts to call itself
  2. There is no string data type, and string manipulation is not supported
  3. There are no managed list objects (associative arrays, and so on)
  4. Floating-point arithmetic is restricted to double precision, with no ability to set up the hardware FPU
  5. Maia's report output uses C printf syntax. Verilog has equivalent functionality, but it is poorly defined, and there are implementation differences between different simulators. report output is therefore not guaranteed to display identically when running with different simulators.

Resources[edit]

A free ('gratis') compiler can be downloaded from the Official website, together with the Language Reference Manual.

External links[edit]

Official website

  1. ^ 1364-2005 - IEEE Standard for Verilog Hardware Description Language. 2006. doi:10.1109/IEEESTD.2006.99495. ISBN 978-0-7381-4851-9.