
|
The C Based Hardware
Description Language |
Overview
In developing fast DSP technologies we needed a simulation
environment and modeling language with the following
characteristics:
- Analog signal modeling
- Very high performance simulator
- Low cost
- High degree of flexibility
- First class code development tools
- Portability
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:
- Embed the language interface into C++. In other words no
language development as such. This saves a lot of work.
It means that all the parsing and compilation is achieved
in C++. The HDL compilation and simulation would be
achieved through a user friendly API which would have the
look and feel of a regular HDL language. In particular
heavy use of C++ operator overloading would be used to
make signal operations appear to be part of the language.
- Since the source is written in C++ a standard code
development tool can be used such as Microsoft Visual C++
or equivalent tools in the UNIX World.
- Time would be modeled as a long double. This had enough
dynamic range to model analog signals in pS steps while
running simulations which could represent many seconds.
- Special pre defined signal classes would make it easy to
build models and netlists which would support modeling in
logic, real, integer, and logic bus signals.
- Mixed analog & digital signal displays would be
available.
- Hierarchy would of course be supported. Both parameters
and signals could be parameterized.
 |
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:
- Macro name. The text name used in the
associated macroStart.
- Instance name. A name unique to the
current netlist or macro definition.
- 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:
- 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.
- 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