The C Based Hardware
  Description Language

 

Overview

In developing fast DSP technologies we needed a simulation environment and modeling language with the following characteristics:

Now, Verilog is an excellent language. However commercial versions, with good optimizing compilers and analog support, cost more than $100K and still suffer performance disadvantages compared to compiled C.

Hence, we choose to develop something pragmatic in C++ based on the concepts of our existing L2I logic simulator. It would be based on these ideas:

Code Development Environment.

CHDL uses Microsoft Visual C++ for user code development platform or any other good C++ code development system.

This represents a massive re-use of development effort.

   

 

Trivial Logic Example File

This shows a logic simulation of a 2 to 4 decoder using some pre defined logic elements. Per L2I this is just a C routine with some pre declared functions available as a library. However, the overall effect is very similar to that of a conventional HDL language. Extensive use of operator overloading in the libraries allows for some nicer embedded constructs such as s0 = 1; rather than a function call.
This example connects four nand gates and two inverters, then simulates all four input states.

void example1(void) {printf("Example 1: 2 to 4 line decoder\n");

 netStart();
   sig s0("s0"), s1("s1"), ns0("ns0"), ns1("ns1"),
       y0("y0"), y1("y1"), y2("y2"),   y3("y3"); 
   defIn("s1 s0");              // Define IOs
   defOut("y3 y2 y1 y0");
     new inv2("i1 s0 ns0");     // Instantiate library elements
     new inv2("i2 s1 ns1");
     new nand2("i3,ns0,ns1 y0"); 
     new nand2("i4 s0 ns1 y1");
     new nand2("i5 ns0 s1 y2");
     new nand2("i6 s0 s1 y3");
 netEnd(); 


 xref();                        // Print cross-reference of signals and instances
 sim.trace();
 sim.list.addIO();              // List all global I/O signals in table

  sim();                        // Run till no activity with no inputs set
  s1 = 0; s0 = 0; sim(1 uS);    // Try all four input combinations
  s1 = 0; s0 = 1; sim(1 uS);
  s1 = 1; s0 = 0; sim(1 uS);
  s1 = 1; s0 = 1; sim(1 uS);
}
When run the output from this is:
             s s  y y y y
             1 0  3 2 1 0

     0.0     u u  u u u u
  1004.5     0 0  1 1 1 0
  2004.5     0 1  1 1 0 1
  3004.5     1 0  1 0 1 1
  4004.5     1 1  0 1 1 1

The Variable "t"

Time is represented as a pre declared variable t which is a long double. This means that models, netlists and test benches can use the value for comparisons and, in the case of models, force changes in time. Generally however users do not need to be aware of t, except to avoid changing it. It is the simulators event queue which is responsible for managing time t.
Note however that since t is a global variable the user may not declare it for personal use anywhere is the source.

Constant Time Values

Time may be expressed using standard C constant double formats. E.g. 1nS can be expressed as 1e-9. However it is more convenient to use engineering names such as pS, nS etc. C doesn't support that directly but with a small syntactic compromise macros can be assigned which can (for example) define "nS" as "*e-9". Hence 1nS can the written as "1 nS". Note the space which is required. pS, nS, uS, and mS, are defined. For frequency MHz and GHz are also defined. These macros are defined as part of the language and can be used as follows:

    if(t > 1.7 nS)                       // Is t greater than 1.7nS?
    20 mS                                // 20 milliseconds
    202 pS                               // 202 picoseconds
    20mS                                 // ERROR! Space is required.
    25 MHz				 // 25MHz

Logic Signal Assignment Examples

    sig a,b;                             // Define logic signals a & b
    a = 0;                               // Set a to logic low
    a = '0';                             // Set a to logic low (alt method)
    b=a;                                 // Set b to the value of a    
    b = a.not();                         // Hopefully obvious
    b = not(a);                          // Hopefully obvious
    b = !a;				 // Hopefully obvious

    for(int i=0;i<20;i++){               // Ten clocks, 40nS period
      sim(20 nS); clk=!clk;  }

    a.que(5 nS,1);                       // Queue a to be 1 in 5nS

 

Netlist Key-Functions

 

sig a
sig a("a"); 
sig a("a",type); 
sig a("a",ub,lb); 
This class is used to define default logic signals. For example sig a; defines 'a' as a signal which may be assigned, queued, and used in logic functions. 

However, due to the limitations of the embedded coding a signal thus declared is not associated with a text name which is required for may operations such as tracing and plotting. Hence normally we need to associate a text name with this declaration. This is done with an alternative declaration. 
For example: sig mysig("mysig");

For non simple logic types an expanded declaration is needed. Current types are:

  • SIG_REAL or SR. A real signal who's data part is represented as a C float type. Typically this is used to represent voltage. sig a("a",SR); can be assigned to floating point constants, variables, and other real signals. E.g.:

  •  a = 1.75;                 // set signal a to 1.75 volts at current time.
     a->que(1.2, 1 nS);  // set signal a to 1.2 volts 1nS from now.
     
  • SIG_LOGIC or SL. A logic signal. Actually the same as the default declaration. This type can have values '1', '0', 'x', or 'u'. 
         
    the following are partially implemented:
        
  • SIG_INT or SI. A signal who's data part is represented as a 32 bit C integer type. Designed for use in simple fixed type DSP simulations. Values can be printed in the trace output in hex representation.
               
  • SIG_BUS or SB. This is an array of SIG_LOGIC signals which actually is implicitly declared when the form sig a("a",ub,lb); is used. For example:
    sig abus("abus",31,0); declares a 32 bit logic signal bus with indices from 31 down to 0. 

 

insig a("a",typ);
outsig a("a",typ);
iosig a("a",typ); 

 

 

An alternative to the text declarations defIn etc. Used directly after netstart or defMacro declares IO signals for the netlist.

The typ parameter is optional. If not used the signal defaults to type SIG_LOGIC (ie SL).

Unlike defIn etc, these declarations create a C++ reference to the signal which may be used (for example) in C assignments: a = 1;

netstart()  A call to this library routine is required before the formal logical top level netlist begins. It's function is to initialize the data structures for the netlist. This routine takes a void parameter. 
netend()  This routine formally closes the top level netlist. An error is given if a top level netlist has not been opened with netstart(); 
macroStart()  Start and name a macro netlist to be called later within the top level netlist. This function takes one parameter, a string, which uniquely names the macro and can be used instantiate it later in other macros or the top level netlist 

Example use: macroStart("dff");

 

macroEnd(). Ends the definition of a netlist macro started with macroStart. 

 

macro(pars) Instantiate a netlist macro defined by macroStart(). The parameters (pars) to the  macro key-function are order significant and generally is the form of a constant string of names separated by spaces and or commas.. The order is:
  1. Macro name. The text name used in the associated macroStart.
  2. Instance name. A name unique to the current netlist or macro definition.
  3. Signal List. An ordered list of signals to match the signal order in the macro definition.

Example: macro("dff m1 nq0 ck ns nr q0 nq0");

Hierarchical signal nomenclature. An instantiation of a macro creates hierarchy in the design. In order to be able to display than force signals inside the hierarchy a nomenclature is required to describe them unambiguously. In CHDL the familiar "dot" form is used. 

Hence hierarchical signals are in the form: instName[.instName].sigName

For example the signal "s1" in the above example macro "dff" (instance name "m1") is referred to as "m1.s1".

 

defIn(slist).
defOut(slist).
defIo(slist).
defIn(slist,type).
defOut(slist,type).
defIo(slist,type).
Define signals which are the inputs, outputs, or bi-directionals to the top level netlist (or current macro). 

By convention defIn, defOut and defIo declarations should appear after netstart or macroStart. slist is usually a constant string of signal names separated by non name characters such as " " or ",". Some examples are:

defIn("a b c");  defOut("x, y, z");

The order of the slist is significant. In the case of the top level netlist this order will be used in default printing of simulation results. Also, multiple statements are additive in effect which allows the declaration of long lists and the intermixing of the order of signal types.

In the case of macro definition the slist order defines the signal ordering when the macro is instantiated. 

 

 $("sig_name") This function returns a signal reference. It is used when a signal has been implicitly declared as a string to gain direct access to the signal class. 

For example, if the signal "a" has been defined through the statement: 

defIn("ck a b c");

it can be set using the ss statement: ss("ck","0");

However, this is a relatively slow in operation. A alternative is to make a signal pointer and this can then be used without internal text searches:

sig *ck = $("ck"); 
*ck = 0;

 

   
   

 

Simulation Key-Functions

xref() Builds a cross reference listing of the netlist by signal name and by instance. Signals are listed in hierarchical format.

Must appear after the netEnd key-function.

 

sim(time)
sim()
Run the simulation for a specified period of time. This is the primary means of triggering simulation activity from the test bench description. The time parameter is usually in the form of a constant e.g.: sim(1 uS); but may also be a float variable.

The form "sim()" may also be used. This has no time parameter and hence runs the simulation until all event activity stops.

 

Changing Signals: Logic and real signals made be assigned anytime after netEnd. 

To change the value of a signal at the current time simply assign it to a value or a constant of the appropriate type:

sig re("re",SR), logicSig("logicSig");
re = 0.4; logicSig = 1; sim(10 nS);
logicSig = '0'; 

See Logic Signal Assignment Examples 

 

Plotting Signals:

Scope()
launchPlot()

A library element called scope is provided to easily make signal plots. This is wired into the netlist like an oscilloscope (hence the name). It uses a public domain program called ptplot at it's heart but is launched through the scope and launchPlot key-functions.

A scope instance in a netlist is in the form: 
     new scope(controlString, sigRefList, strobeSigRef);

     where: 

  • controlString is a const char string which includes ptplot commands.
  • sigRefList is a list of references to string types which will usually be of the form: &signalName,[&signalName,].
  • strobeSigRef is a reference to a logic signal used as a positive edge sample clock for the plot.

Example 1: new scope("-t plotABC", &a, &b, &c, &ck1g);

This plots signals a, b, and c on a single panel with auto scale. In this case the ptplot control string names the plot "plotABC".

Up to four scopes may be used in the netlist. Each will plot in a different place on the screen and can have up to five signals. 

Currently both real and logic signals may be plotted. However, logic signals are represented as zero (logic low) and 0.05 (logic high). Further improvements are in progress.

At the end of the simulation the "scopes" can be plotted on the screen using the launchPlot() key function. This function take no parameters and launches all the scopes in the order they were declared.

Example: launchPlot();

   
   
   

Introduction to Writing CHDL Behavioral Models

A simple single pole low pass filter behavioral model implementation is shown below. Note that this is defined as a C++ class. For this example just two class methods are required:

  1. A C++ constructor (in this case LPF::LPF) in the configuration shown. This uses the getpars key function to bring in the signal names from the parameter string and connect them to signal pointers, type them, and declare their directional and event triggering characteristics.
  2. A method called func() which is called whenever a signal declared with an upper case 'I' parameter changes. This routine is the functional part of the model. Within this model the writer can look at the value of io signals and queue changes to output signals.

A number of methods are available in the signal class to smooth the modeling code and give it more of a feel of HDL code.

Note that behavioral models must be prototyped as a derived class using model_base as the base class thus:

This will usually appear in a corresponding .h header file.

 

Behavioral Modeling Key Functions

getpars() getpars is a public member of model_base and hence is available to every behavioral model. It's main function is to hide the complexity of connecting and typing signals from the model writer.getpars has the form:

getpars(sigParamString,modelNameString [,sigRef,sigType,sigDir]);

where:

  • sigParamString is a null terminated string which holds a white space and/or comma seperated list of ordered IO signals. Usually this character array is simply passed down from the constructor of the model class which is usually on the line above.
    The order of the signals is significant. It represents the call order that must be used when instantiating the model in a netlist or macro.
  • modelNameString is a const null terminated character array which give a text name for this behavioral model.By convention (and to prevent confusion) this must have the same name as to model class. (LPF in the example above).
  • [,sigRef, sigType, sigDir]. For each signal which appears in the sigParamString a set of three parameters must be defined:

    sigRef is a C reference to sig pointer type. From the above example the sig pointer is declared with the statement sig *ck; and is connected to the ordered named signal by means of the reference &ck.

    sigType is the basic type of the signal. Currently available types are SIG_LOGIC (or SL), SIG_REAL (or SR), SIG_INT (or SI), and SIG_BUS. See above. Currently only SIG_LOGIC and SIG_REAL are fully implemented.

    sigDir is a constant character which represents the directional and triggering characteristics of the signal. Currently supported values include:

    • 'I' : (upper case I) an input signal. The upper case indicates that this is an input signal which, when it changes externally to the model will trigger a call to func().
    • 'O' : an output signal.func() is not called when this signal is changed.
    • 'i' : (lower case i) an input signal not triggering a call to func() when it changes.
    • 'B' : (upper case B) is a bidirectional signal which does trigger a call to func() when it changes. Note in the case when the model drives (queues) this signal a call back to the func() will result from func() changing this signal. Hence the bidirectional signal can be used to model an oscillator.

 

changed()
changedTo(const)
These are methods of the signal class used frequently in models to check whether a signal has changed at the current time (t) and has therefore been responsible for calling the local func() routine.

In the case of changedTo() we can also check if the signal has changed and if the value of the signal is now the same as a specified constant.

These functions return an integer and our effectively boolean return type.

These methods are embedded into the signal class. In most cases the signals are external to the model (in the netlist) and hence we normally work with signal pointers in the behavioral model. In this situation we use the "->" form to invoke the method. Some examples:

if(ck->changedTo('1')) { ... }

if(ck->changed()) ck->que(!ck, 5 nS);  // Invert the ck

 

que(value,dt) This is a method of the signal class used to queue future signal value changes. This method is appropriately overloaded so as to work with all signal and value types.

Value must be:

  • An int or char variable or const type for SIG_LOGIC typed signals.
  • An int variable or const for SIG_INT signal types.
  • A float variable or constant for SIG_REAL typed signals.

The parameter dt is the relative time from the current value of t when the signal should be changed. 

Here are some examples of use of the queue method in a model:

if(ck->changed()) ck->que(!ck, 5 nS);  // Invert the ck

nandOut->que('1', 0.8 nS); // Make nandOut 1 in 800pS

filterOut->que(2.5, 1 pS); // Set voltage to 2.5V

 

 

Sig Values:
b, r, i
In order to do behavioural modeling we need to be able to read the value of signal at the current time t. There are public data members in the signal type to allow this to happen. Namely:

b : A char type which represents a logic signal value. Currently values '0', '1', 'x', and 'u' are defined.

r : A float type (ie real) which represents the value of a real signal.

i : An int type which represents the value of an integer signal.

Note that these signal values are only defined for signals of the appropriate type. Hence, for example, the value of r is undefined in a logic signal.

Also note access to these signals is provided mainly for reading the current values of signals. Setting these values can be done but is not recommended because this avoids proper event queuing of the new signal value.

Examples:

if(inSig->r < 0.5) printf("inSig less than 0.5 Volts\n");

 

 

   
   

Debug Functions

sigStatus(char*) Print information in the signals class. E.g. values, type, next signal etc.

Eamples:

sigStatus("x");

 

   
   
   


 updated 12/09/06