This documentation is archived and is not being maintained.

Delegates in Action with J#

Visual Studio .NET 2003
 

Pratap Lakshman
Microsoft Corporation

January 2005

Applies to:
   Microsoft Visual J# .NET
   Visual Studio .NET

Summary: Consider designing a system for performing digital logic simulation. Specifically consider designing a system to simulate a half-adder circuit. This is the second part of a three-part series discussing problem solving with J#. (12 printed pages)

Note   The other two articles in the series are part one, An Introduction to Design Concepts Using J#, and part three, Solving Constraints with J#.

Contents

Introduction
Problem Statement
Solution Domain
Testing
Design Exercises

Introduction

Simulation, as a technique for achieving an understanding of a system and predicting its behavior, predates the digital computer. It is typical of many design problems that the system consists of components whose fundamental laws of behavior are well known—the difficulty of the design problem resides in predicting how an assemblage of such components will behave. The model basin and the wind tunnel are means of the studying the behavior of large systems by modeling them in the small. Digital circuit simulation is no different. Such circuits are constructed by interconnecting simple components whose laws of behavior are well understood, and yet a circuit composed of such components could have complex behavior [1].

Problem Statement

Consider designing a system for performing digital logic simulation. Specifically consider designing a system to simulate a half-adder circuit [2].

Problem Domain

The half-adder circuit shown below consists of an or-gate, two and-gates, and an inverter. It takes two input signals, A and B, and has two output signals, S (sum) and C (carry). S will become 1 whenever either A or B is 1, and C will become 1 whenever A and B are both 1.

Figure 1. A half-adder circuit

Consider the artifacts from which this system is constructed. There are wires, which carry signals. A signal may at any moment have only one of two possible values, 0 and 1. There are also various kinds of components that connect wires carrying input signals to other output wires. Such components produce output signals computed from their input signals. For example, an inverter is a component that drives its output signal to the logical not of its input signal; that is, if the input signal to an inverter changes to 0, the inverter will drive its output signal to 1; if the input signal changes to 1, the inverter will drive its output signal to 0. An and-gate is a component with two inputs and one output. It drives its output signal to a value that is the logical and of the inputs; that is, the output will become 1 only if both of its input signals are 1; otherwise the output will become 0. An or-gate is a similar two-input component that drives its output signal to a value that is the logical or of the inputs; that is, the output will become 1 if at least one of the input signals is 1; otherwise the output will become 0.

Solution Domain

The system as described above consists of components whose fundamental laws of behavior are known. The challenge of the design problem resides in modeling how an assemblage of such components will behave.

Our design goals, then, are:

  • The design must show appropriate functional decomposition mapping to the components, and their assemblage.
  • The design must be extensible—it must be possible to compose more complex circuits using our basic design as a building block.
  • The resulting implementation must be testable.

This system typifies a kind of program called an event-driven simulation, in which actions ("events") trigger further actions, which in turn trigger further actions, and so forth.

The basic elements of our simulation will be the objects wire, andgate, orgate, and inverter. Given that we can construct these objects, we can wire up the half-adder as follows.

    orgate(a, b, d);
    andgate(a, b, c);
    inverter(c, e);
    andgate(d, e, s);

where a, b, d, e, s, and c, are wires.

A wire "carries" a signal and act as an event source. When a signal is set on a wire it "raises" an event. Raising an event causes an event handler to be invoked. The gates implement the "logic" by which a change in the signal on one wire influences the signals on other wires. The gates provide the event handlers—these are methods that enforce the correct relationships among the signals. The event handlers are dynamically registered with the event source to be notified when an event is raised.

In other words, the wires are shared among the various gates that have been connected to them. A change made by an interaction with one gate will affect all the other gates attached to the wire. The wire communicates the change to its neighbors by calling the event handler methods provided to it when the connections were established.

A wire cannot know a-priori the component to which it may be connected. Indeed, it may be connected to more than one component—one can imagine a user visually composing the circuit through a GUI. The coupling between the wire as an event source and the gates as an event sink must be loose; we must be able to vary the coupling at run time.

We begin by declaring a delegate [3] that will act as an event dispatcher for the class that raises the event by maintaining a list of registered event handlers for the event. We implement wire to be a source for the SignalChangeEvent event. To build the gates, we would use the following methods on a wire:

  • set_Signal(boolean val)

    set the value of the signal on the wire

  • boolean get_Signal()

    return the value of the signal on the wire

  • add_ SignalChangeEvent(EventHandler eh)

    register a designated event handler to be invoked whenever the value of the signal on the wire changes. Event handlers are the means by which changes in the value of the signal on a wire are propagated.

Here is our implementation of the wire.

// file: wire.jsl
/** @delegate */
public delegate void EventHandler();

public class wire
{
  private boolean signal;

  public wire()
  {
    signal = false;
  }

  private EventHandler el;

  /** @event */
  public void add_SignalChangeEvent(EventHandler eh)
  {
    if (el != null)
    {
      el = (EventHandler) System.Delegate.Combine(el, eh);
    }
    else
    {
      el = eh;
    }
  }

  /** @event */
  public void remove_SignalChangeEvent(EventHandler eh)
  {
    el = (EventHandler) System.Delegate.Remove(el, eh);
  }

  public void set_signal(boolean val)
  {
    signal = val;
    if (el != null)
    {
      el.Invoke();
    }
  }

  public boolean get_signal()
  {
    return signal;
  }
}

Notice that the wire declares itself to be the source of the SignalChangeEvent event and that the field el, which is a reference to a delegate of type Eventhandler, serves as the event dispatcher.

Using the methods available on a wire we can now define the logic gates. To connect an input to an output through an inverter, we associate with the input wire an event handler method that will be invoked whenever the signal on the input wire changes value. The method computes the logical not of the input signal and sets the output signal to be this new value. Here is our implementation of the inverter.

// file: inverter.jsl

public class inverter
{
  private wire ip;
  private wire op;

  public inverter(wire i, wire o)
  {
    ip = i;
    op = o;

    EventHandler eh = new EventHandler(this.action);
    // if the signal changes on the ip wire,
    // notify me at action()
    ip.add_SignalChangeEvent(eh);
  }

  public void action()
  {
    boolean b = ip.get_signal();
    op.set_signal(!b);
  }
}

In the case of an and-gate the event handler method must be invoked if either of the inputs to the gate changes. It computes the logical and of the values of the signals on the input wires and sets the output signal to be this new value. Here is our implementation of the and-gate.

// file: andgate.jsl

public class andgate
{
  private wire ip1;
  private wire ip2;
  private wire op;

  public andgate(wire i1, wire i2, wire o)
  {
    ip1 = i1;
    ip2 = i2;
    op  = o;

    EventHandler eh = new EventHandler(this.action);
    // if the signal on either of the input wires
    // change notify me at action
    ip1.add_SignalChangeEvent(eh);
    ip2.add_SignalChangeEvent(eh);
  }

  public void action()
  {
    boolean b1 = ip1.get_signal();
    boolean b2 = ip2.get_signal();

    op.set_signal(b1 && b2);
  }
}

An or-gate is implemented similarly—the output signal is set to the logical or of the input signals. Here is our implementation of the or-gate.

// file: orgate.jsl

public class orgate
{
  private wire ip1;
  private wire ip2;
  private wire op;

  public orgate(wire i1, wire i2, wire o)
  {
    ip1 = i1;
    ip2 = i2;
    op  = o;

    EventHandler eh = new EventHandler(this.action);
    // if the signal on either of the input wires
    // change notify me at action
    ip1.add_SignalChangeEvent(eh);
    ip2.add_SignalChangeEvent(eh);
  }

  public void action()
  {
    boolean b1 = ip1.get_signal();
    boolean b2 = ip2.get_signal();

    op.set_signal(b1 || b2);
  }
}

Given the wires a, b, s, and c, we can now wire up the half-adder as follows.

// file: halfadder.jsl

public class halfadder
{
  public halfadder(wire a, wire b, wire s, wire c)
  {
    wire d = new wire();
    wire e = new wire();

    orgate og   = new orgate(a, b, d);
    andgate ag  = new andgate(a, b, c);
    inverter i  = new inverter(c, e);
    andgate ag2 = new andgate(d, e, s);
  }
}

And run a sample simulation of the digital circuit as follows.

// file: dc.jsl

public class dc
{
  public static void main(String [] args)
  {
    showhalfadder();
  }

  public static void showhalfadder()
  {
    wire a = new wire();
    wire b = new wire();
    wire s = new wire();
    wire c = new wire();

    halfadder ha = new halfadder(a, b, s, c);

    a.set_signal(true);
    b.set_signal(true);

    System.out.println("s: " + s.get_signal());
    System.out.println("c: " + c.get_signal());
  }
}

Build the application and run it. Notice that because both input wires to the half-adder carry a signal with the value true, the half-adder outputs a sum signal of false, and a carry signal of true.

Testing

In order to test the simulator one may want to inspect the signal that is carried over the wires. Applying a tap on the wire enables us to inspect the signal and "see" the simulator in action. A tap may be conveniently thought of as another component that connects to a wire. It associates with the wire an event handler method that will be invoked whenever the signal on the input wire changes value. The event handler method merely prints out the value of the signal along with the name of the tap itself. Here is our implementation of the tap.

// file: tap.jsl

public class tap
{
  private wire w;       // wire with which this tap is associated
  private String name;  // a name for this tap

  public tap(wire w1, String s)
  {
    w = w1;
    name = s;

    EventHandler eh = new EventHandler(this.action);
    w1.add_SignalChangeEvent(eh);
  }

  public void action()
  {
    boolean b1 = w.get_signal();
    System.out.println("tap: " + name + "  signal: " + b1);
  }
}

We can now apply a tap to all the wires in our half-adder. We update the file halfadder.jsl as follows.

// file: halfadder.jsl

public class halfadder
{
  public halfadder(wire a, wire b, wire s, wire c)
  {
    wire d = new wire();
    wire e = new wire();

    orgate og = new orgate(a, b, d);
    andgate ag = new andgate(a, b, c);
    inverter i = new inverter(c, e);
    andgate ag2 = new andgate(d, e, s);

    tap ta = new tap(a, "a");
    tap tb = new tap(b, "b");
    tap td = new tap(d, "d");
    tap te = new tap(e, "e");
    tap ts = new tap(s, "s");
    tap tc = new tap(c, "c");
  }
}

Notice that we did not change the implementation of the wire in order to add a tap to it. Our choice of using a delegate-based event dispatcher served us well—we were able to add functionality to a wire (that is, the capability to report its signal) non-invasively.

Rebuild and run the application. In dc.jsl when the signal on the wire 'a' was set to true, the simulator springs to life. Observe the taps reporting the various signals. Since the signal on the wire 'b' is set to false by default, the half-adder produces a sum-signal of true, and a carry-signal of false as expected. The exact flow of the signals within the circuit is reported by the taps. Now, when the signal on the wire 'b' is also set to true, the sum signal is driven to false and the carry signal is driven to true.

QED.

Design Exercises

  • Alas, technology is not ideal! Every component has a characteristic "delay." Output signals are driven to their final value only after this delay. We have deliberately avoided this issue of latency. How would you add latency to this design?
  • How would you compose a full-adder based on this design?

References

[1] Simon, Herbert A., The Sciences of the Artificial 3e, MIT Press, 1996

[2] Mano, Morris M., Digital Logic and Computer Design, Prentice Hall, 1979

[3] Lakshman, Pratap, The Delegate Type in J#, MSDN, 2004

Show: