Esim is a simple language that allows the structural design of logic circuits. Because it is intended to be a teaching language, there's little (if any) support for actually implementing hardware. Instead, it's designed to allow students to design and simulate complex hardware such as simple microprocessors. Components can be defined from simple gates, and those components can be combined into more complex structures.
The language itself is very simple. Perhaps the best way to learn to use it is by example. The following is a definition for an 8-bit clocked latch which is loaded on a rising clock edge. The latch has two enables, both of which must be high on the rising edge in order to load a new value.
define latch8 (q[8], d[8], enb1, enb2, clk) // line 1 signal enabled; // line 2 signal qInternal[8]; // line 3 circuits // line 4 qInternal <= d when (enb1 & enb2) else qInternal; // line 5 q <= qInternal on rising clk; // line 6 end circuits; // line 7 end latch8;
Line 1 in the code fragment states that this is to be a definition for a component called latch8 and that the component has 5 signals going into it. Two of them (d &q) are 8-bit wide buses, and three (enb1, enb2, and clk) are 1 bit wide. Note that there is no indication of which are inputs and which are outputs; the simulator handles that automatically.
Lines 2 & 3 declare two signals internal to the latch. The first (enabled) is 1 bit wide, and the second (qInternal) is 8 bits wide. A signal statement may be followed by any number of signal declarations separated by commas, just as in C.
The circuit body follows the definitions. It is bracketed by the keywords circuits and end circuits;. Inside are circuit "assignments." In each case, the signal on the left is assigned (driven to) the value on the right. The expression on the right can consist of any combination of the following:
a & b a AND b a | b a OR b a ^ b a XOR b ~a NOT a a == b a equals b (result is 1 bit wide) a != b a doesn't equal b (result is 1 bit wide) a when cond else b a whenever cond (a 1 bit wide signal) is 1, b otherwise a . b a concatenated with b (result is as wide as a and b combined) a[x:y] bits x down to y of bus a #h0123 constant in hexadecimal (16 bits wide in this case) #b0101 constant in binary (4 bits wide in this case)As in C, expressions such as (a & b) | (c when d else e) are also permissible because they can be built from primitive expressions.
Thus, the first assignment (line 5) drives qInternal with one of two values. If (enb1 AND enb2) is 0, all signals in qInternal are driven to the value currently on the qInternal bus. Otherwise, qInternal is driven to the value on the dbus.
Assignments may be qualified with two kinds of qualiifiers: on and after. on specifies either rising or falling and a signal to "watch." When that signal rises or falls (as specified), the assignment is done. Otherwise, no assignment is made even when the other inputs to the expression change. The after keyword allows the designer to change the gate delay from the default. The delay must be an integral number of nanoseconds (ns) or microseconds (us).
The second assignment (line 6) thus means that q is driven to the value of qInternal every time clk rises. This circuit (latch8) implements an 8-bit D flipflop that may itself be used in other designs. Components may be used in other components with the use statement, as in this example using the 8-bit latch from above:
define latch16 (q[16], d[16], clk) circuits low use latch8 (q[7:0], d[7:0], #b1, #b1, clk); // always enabled high use latch8 (q[15:8], d[15:8], #b1, #b1, clk); // always enabled end circuits; end latch16;This says that the latch16 circuit uses two latch8 circuits hooked up as specified in the circuit definition. The names preceding the use keyword allow the user to identify the different instances of latch8 in the simulator. All names preceding a use keyword in a given component definition must be different. However, names may be reused in different components.
Note that both uses of latch8 show the use of "slicing" a bus. Instead of using the entire bus, slicing allows the user to specify part of a bus. For example, the statement:
q[5:1] <= d[8:5] . x;assigns d[8] to q[5], d[7] to q[4], d[6] to q[3], d[5] to q[2], and xto q[1]. The concatenate operator (.) connected the two buses on the right hand side, and the result was assigned to the slice of a bus specified on the left hand side.
Because it's common to select from a list of alternatives, the esim language provides a construct similar to the switch statement in C. This construct is used as follows:
signal value[4]; signal x[2]; signal foo[6]; circuits value <= with x select #b00: #h3; #b01: foo[4:2] . #b0; otherwise: #hf; end select; end circuits;This replaces the endless when...else clauses that would have to be used to do the same thing. Note that conditions must be constants, though they can be specified as either binary or hex. The compiler doesn't check to make sure that the constants are all different, however; it merely evaluates them in the order in which they appear in the file. The otherwise keyword is required even if there are no other options (in the above case, it would be necessary even if we specified #b00, #b01, #b10, and #b11). The above code is equivalent to:
value <= #h3 when (x == #b00) else (foo[4:2] . #b0 when (x == #b01) else #hf);The select clause is most valuable when building state machines, though it may also be useful for other situations with relatively many options (ALU functions, for example).
Once all of the components have been defined, they may be hooked together at the top level. This is done in a way similar to component definitions, except that there is no define line. Instead, the top level circuit consists just of signal statements followed by a single circuits ... end circuits block. Top level signals may be driven by instances of components, or they may be set from within the simulator. An entire valid program might then look like this:
// 8-bit latch clocked on the clk signal when enb1 and enb2 are both enabled define latch8 (q[8], d[8], enb1, enb2, clk) signal enabled; signal qInternal[8]; circuits qInternal <= d when (enb1 & enb2) else qInternal; q <= qInternal on rising clk; end circuits; end latch8; // Set up a 16 bit latch as 2 8-bit latches define latch16 (q[16], d[16], clk) circuits low use latch8 (q[7:0], d[7:0], #b1, #b1, clk); // always enabled high use latch8 (q[15:8], d[15:8], #b1, #b1, clk); // always enabled end circuits; end latch16; signal q[32]; signal d[32]; circuits low use latch16 (q[15:0], d[15:0], clk); high use latch16 (q[31:16], d[31:16], clk); end circuits;
This is the circuit for a half adder. It takes two inputs (a & b) and produces a result (result) and carry out (carryout).
define HalfAdder (a, b, result, carryout) circuits result <= a ^ b; carryout <= a & b; end circuits; end HalfAdder;
esim supports 4 logic states: 0, 1, Z, and X. Zis used to indicate that a signal is not driven to any value. X is used to indicate a signal whose value can't be determined, either because it is driven to multiple values or because the the simulator can't figure out the appropriate value for the signal (for example, the output of an AND gate whose inputs are 1and Z). When used as a literal (hex or binary digit) argument to a comparison, however, Xtakes on a different meaning - "don't care". Any value (including Xor Z) matches a "don't care" in an equality comparison.
In addition to signals, esim provides memories. The memory keyword allows a designer to efficiently include memories in their circuits. Memories may be read or written using special esim statements, allowing data to be transferred at an address specified in the statement. The memory statements are:
memory m[1024]; signal x[4], y[10], enb, clk; circuits m read x from y when enb; m write x to y[8:2] . #b00 when enb on rising clk; m write x[3:2] to y when enb; end circuits;The first statement reads from memory mwhenever enb is high (logic 1); when enb is low, xwill be set to all Z. Since the destination (x) is 4 bits wide, 4 bits of memory mare copied into xstarting at the address specified by y. Note that y need not have enough bits to specify all of the memory, as in the second statement (where the address has just 9 bits instead of the maximum 10). An address may even have more bits than necessary; addresses that are out of range for the memory will result in reading an Xinto the corresponding bit.
The second two statements write values into memory m. The first writes 4 bits starting at the location specified by bits 8 down to 2 of yconcatenated with #b00. This guarantees that the values stored in the memory can never overlap - 4 bits are being stored, and the addresses at which they're being stored are always divisible by 4 because the last two bits are 0. The store will only take place when enb is high and clk is rising. Otherwise, the memory will be unchanged. In the second write statement, only enb must be high; there is no timing involved.
Memory is very useful in many parts of the CPU design. It may be used to implement a register file as follows:
define regfile (rd[3], rs[3], d[16], s[16], wEnb, clk) memory reg[128]; circuits reg read s from rs . #b0000 when #b1; // always enabled reg write d to rd . #b0000 when wEnb on rising clk; end circuits;In this circuit, the memory must hold 128 bits (16 registers * 8 bits/register). The source operand is specified by rs, and the destination by rd. Since registers are 16 bits wide, though, the addresses are all multiplied by 16 (by shifting left 4 places and filling with zeroes) to insure that each register's 16 bits don't overlap. Similar structures can be used for larger register files and wider registers. The same kind of structure can also be used to define the "main memory" or the cache for a CPU.
Note that you may have multiple memories in your circuit. You may even have more than one memory in a single component if you like.
The compiler is available on the gl cluster as the binary /afs/umbc.edu/users/s/q/squire/pub/ecomp. You may want to make a soft link to it from your home directory or create an alias to it. The compiler can compile any number of files; compiling multiple files is equivalent to concatenating them and compiling the one large file that results. The compiler takes just one option: -o output file, which allows the user to specify a name for the simulator input file that is generated. If no name is specified, the simulator input file is written to default.net in the current directory. Only if all files compile properly will an output file be created.
The error messages are as verbose as possible, given the limitations of bison (the compiler compiler with which ecomp was written). In particular, the compiler may simply stop (or even get a segmentation fault) if it finds an error that it can't recover from.
There's a separate web page detailing the operation of esim, the simulator for this language.
Feedback on this page is welcome. Please send your comments to elm@umbc.edu.