ASIC Verification: "e" syntax at a glance

Tuesday, March 4, 2008

"e" syntax at a glance

In this section, I am going to explain the syntax which are necessary to model the "e" verification environment with suitable examples. Since "e" language was developed based on other programming languages, most of the syntax is similar to other programming language.

Code Segments

All "e" code starts with a begin code marker (<') and ends with end code marker ('>).
"e" is a case-sensitive language.

Macros

The simplest way to define macros is with define statement. An 'e' macro can be defined with or without an initial ' character. You can also import the verilog define macros using the keyword verilog import. Say for example, you have macros.v file. You are going to import that verilog file into your 'e' code. See carefully the usage of verilog macro and 'e' macro in the 'e' file.

Importing 'e' files

'e' files are called modules. An 'e' file can import another 'e' file using the keyword import. The import statements must be before any other statements in the 'e' file.

Scalar types

Integer int is a numeric scalar data types. It represents a signed numeric data, both negative and non-negative integers. Default size is 32 bits.

Unsigned integer uint represents a unsigned numeric data. Non-negative integers only.
Boolean bool represents either TRUE (1) or FALSE (0).

Enumerated data types define the valid values for a variable of field as a list of symbolic constants.

List types hold ordered collection of data elements. Items in a list can be indexed with subscript operator [ ].

The string data type contains a series of ASCII characters enclosed within double quotes (" ").
We will see one example that explains all the details we saw in the previous section.
===========================================================================
<'
import other_e_file.e; // An e file that is being imported to this file
verilog import macros.v; // macros.v contains only one def. 'define WIDTH 7
'define WIDTH 5; // Definition of WIDTH macro
type opcode : [ADD, SUB, AND, OR];
// structs are more or less like classes
// or structures in C,
// we will see what is struct in the later section
struct macro {
first : uint; // This is an inline comment, same as in Verilog
second : uint; // This is an inline comment, same as in Verilog
length : uint; -- This is an inline comment, same as in VHDL
keep soft first == length + WIDTH; // Remember length and Length are not same
keep second == length + 'WIDTH; // for verilog ' is important (Don't forget)

type_of_opcode : opcode; // Define a field of type opcode. 4 possible values.

data : list of byte; //List of 8 bit values; data[0] - First element; data[1] - second element

method() is // we will see methods in later section
{
var message : string; // define a variable of string
message = " Hello, Welcome to Specman Tutorial!"; // string value
print message; // Print the string
};
'>
===========================================================================
Simulator Variables

There are two hierarchies in an 'e' based verification environment.
  1. The design hierarchy represented in HDL
  2. The verification hierarchy represented in 'e'
Any 'e' structure should be able to access the simulator variables for reading and writing during the simulation.

How to access the simulator variables?

One can access simulator variables by simply providing a hierarchical path to the variable within single quotes (' ').
===========================================================================
<'
struct driver
{
read_value : uint (bits:4); // Define a 4 bit field to read from DUT
read_from_dut () is // Define a method to read
{
read_value = '~/top/hub/hpie/reg_1'; // RHS is simulator var.
};

write_to_dut () is // Define a method to write
{
'~/top/hub/pie/reg_2' = 8; // LHS is simulator var; RHS is written value
};
'>
===========================================================================
Syntax Hierarchy

Unlike verilog, e forces a strict syntax hierarchy.

Statements : Top level constructs and are valid within <' and '>
Struct Members : Second level constructs; valid within struct definition
Actions : Third level constructs; valid when associated with struct members
Expressions: Lower level constructs, can be used only within another e construct.

We will see one example to better understand these hierarchies.
===========================================================================
<'
struct usb_pkt_xmission // struct is a statement
{
// Declare an event (struct member),
// This event is triggered when the simulator variable
// /top/rdy signal goes high.
event xmt_rdy is rise ('~/top/rdy");
// Declare fileds (SM)
sync : byte;
delay : uint;
// Declaring method is struct member
// calling method is action
on xmt_rdy
{
transmit ();
};
//Declare method
transmit () is
{
sync = 8'hFE;
delay = 10;
out("Sync is transmitted!"); // This is action
};
};
'>
===========================================================================
Structs

Struct definition are statements and are at the highest level in 'e' hierarchy. Struct can be extended and it add struct members to a previously defined struct. Moreover, it is possible to apply many extensions to a struct. These extensions can be specified in the same file or a different file. If it is in different file, then you need to import the file in which the struct is defined.

The advantage of using the extend statement is that the original definition of the struct doesn't need to be modified or edited. So it is very easy to create the basic verification environment and then incrementally we can enhance it.

The 'when' struct member creates a conditional subtype of the current struct type, if a particular field of the struct has a given value. This is called 'when inheritance' and is an important technique 'e' provides for implementing inheritance.

Let us see the struct definition for an USB packet.
===========================================================================
-- This module describes the type of packets

<'
type packet_id : [IN, OUT, SETUP]; -- The token type is either IN, OUT or SETUP

struct hub_packet
{
pid : packet_id; //pid 0 is IN, pid 1 is OUT, pid 2 is SETUP
pid_type : uint(bits:8);
// The percent sign indicates that this field is a real part of the
// packet, which will be sent (physical field).
// The “pid_type” field for example is a virtual field and will
// not be sent. After the packet is created, you can translate it into bits using
// a method of every struct called “pack()”
// “pack()” translates all the real, physical fields into bits and
// then concatenates them.

%syn : uint(bits:32); // SYNC pattern is 32'h8000_000;
keep soft syn == 32'h8000_0000;
%dev_add : uint(bits:7); -- Device address
keep soft dev_add in [1..63] ;

%ep_no : uint(bits:4); -- Endpoint Number
keep soft ep_no in [1..15];

%data : list of uint(bits:16); -- Packet length pid=8bits; dev_add=7bits; ep_nu=4bits;

keep pid == IN => pid_type == 8'h69;

keep pid == OUT => pid_type == 8'hE1;

keep pid == SETUP => pid_type == 8'h2D;

%crc_5 : uint(bits:5);
keep crc_5 = 5'b10101; // Keep the CRC fixed as of now

%eop : byte;
keep soft eop == 8'hFE;
};
'>
===========================================================================
In the above example, % indicates the physical field. Fields that represent data that are to be sent to DUT, need to be of physical field. ! means ungenerated filed. The value for this field is not assigned during the generator phase.

If a struct type named "packet_id" has a field named "pid" that can have a value of "IN", "OUT" and "SETUP", then three subtypes of "packet_id" are "IN packet_id", "OUT packet_id" and "SETUP packet_id"

Extending methods in when subtypes

When a method is declared in a base type, each extension of the method in a subtype must have the same parameters and return type as the original declaration.
===========================================================================
<'
struct method_in_when_subtype
{
opcode : [ADD, SUB]; // declare the type
field1, field2 : uint; // field declarations
method1 (field1:uint, field2:uint) : uint is // define a method; 2 arg.
{
result = field1+field2;
}; // End of method1
}; // End of struct

// extend the struct in the same or in a different file

extend method_in_when_subtype
{
when SUB'opcode method_in_when_subtype
{
field3 : uint; // declare additional field
method1(field1:uint, field2:uint) : uint is also // # of args. 2
{
result = result + 5;
}; // End of method1
}; // End of when
}; // End of extend
'>
===========================================================================
Units Overview

Unit is used to define the static verification object that doesn't move through the verification environment. Unlike structs, unit may be bound to a particular component in the DUT. Thus a unit instance can be moved to a different level of verification hierarchy by simply changing the HDL path of the unit instance.

The following program illustrates the use of units.
===========================================================================
File contains e code for the environment object
All simulator signal accesses are now done relative to hdl_path()
<'
import hub_packet;
import hub_driver;

struct hub_env // Instantiate hub_driver
{
hub_driver : hub_driver is instance;
keep hub_driver.hdl_path() == "~/pid_dec"; // Associate this instance with HDL design
};

// Create an instance of hub_env object in top level
extend sys
{
hub_env : hub_env is instance; // instantiate a hub_env
};
'>
===========================================================================
In the above program, an 'e' unit "hub_driver" is being bounded to a particular component in the DUT hierarchy, in this case, it is "pid_dec". It will allow you to reference signals within that DUT component using relative HDL path names.

Below is the program that I've written to check whether the USB device receives correct pid or not for the USB packet id decoder.
===========================================================================
<'
import hub_packet; // import the hub_packet here

unit hub_driver // define a hub_driver unit
{
!current_packet : hub_packet; // do not generate this packet initially
!delay : uint;
keep soft delay in [1..10];
no_of_pkts : uint;
keep no_of_pkts == 3;

event rise_clk is rise ('clock_in') @sim; // declare an event
event packet_started;
event packet_ended;

generate_and_drive () @ rise_clk is // generate the packets now
{
for i from 0 to no_of_packets-1
{
gen delay;
wait [delay];
gen current_packet;
drive_packet (current_packet); // drive the packet to DUT
}; // end of for loop

wait [500];

stop_run(); // End of simulation

}; // end of generate_and_drive method

// drive the packet to DUT 16 bits for single clock
// declare a variable packet_packed

drive_packet (packet : hub_packet) : list of uint(bits : 16) @rise_clk is
{
var packet_packed : list of uint(bits:16);
packet_packed=(pack(packing.low,%{syn[15:0]},%{syn[31:16]},%{ep[0],
dev_add[6:0],pid_type}, %{crc_5,ep[3:1],eop}));


emit packet_started;
// for each packet, data is being sent
for each (packet_16bit) in packet_packed
{
'data[15:0]' = packet_16bit;
// generate the signals to DUT
sync true (packet_16bit[7:0] == (pid == IN || pid == OUT || pid == SETUP)) @ rise_clk;

'rx_last_byte' = 0;
'rx_bs_err' = 0;
'rx_valid' = 1;

sync true (packet_16bit[15:8] == eop) @ rise_clk;
'rx_last_byte' = 1;

sync true (packet_16bit[7:0] == eop) @ rise_clk;
'rx_bs_err' = 1;
'rx_valid' = 0;

wait true (packet_16bit[15:8] == eop) @ rise_clk;
'rx_valid' = 0;

if(index == (packet_packed.size() - 1)) {
'rx_valid' = 0;
};

wait [25];

};
emit packet_ended;
}; // end drive_packet
run() is also
{ // call the method at the start of simulation
start gen_and_drive();
}; // End of run()
};
'>
===========================================================================
In this section we discussed the basic concepts of 'e' - code segments, comments, numbers and macros. We also discussed how to import the 'e' files and how to access simulator variables. Then we gone through the concepts of struct and unit in details, how to extend struct and the importance of inheritance.

No comments: