Digital Logic Simulation with the Intel® TBB Flow Graph, Part 3: Putting together a simulation

In Part 2 of this blog, I described a four-bit adder circuit built from components discussed in Part 1. In this last installment, I’ll continue using Intel®TBB’s flow graph to put together some signal input and output devices, and then use those to make a small simulation featuring the four-bit adder from Part 2.

Let’s look at two input devices here, the toggle and the pulse (or as I would have liked to have called them, the switch and the clock). A toggle sends a signal of high or low, toggling between the two states, every time it is “toggled” or flipped. A pulse continually alternates between the high and low states at a given duration. The toggle class is implemented as follows:

class toggle {
graph& my_graph;
signal_t state;
overwrite_node < signal_t > toggle_node;
public:
toggle(graph& g) : my_graph(g), state(undefined), toggle_node(g) {}
toggle(const toggle& src) : my_graph(src.my_graph), state(undefined), 
toggle_node(src.my_graph) {}
~toggle() {}
// Assignment ignored
 toggle& operator=(const toggle& src) { return *this; }
 sender < signal_t > & get_out() { return toggle_node; }
void flip() { 
 if (state==high) state = low; 
 else state = high;
 toggle_node.try_put(state); 
 }
void activate() { 
 state = low;
 toggle_node.try_put(state);
 }
 };

The toggle is represented internally by an overwrite_node, because it simply needs to keep track of one most-recent state. As an input device, it doesn’t receive output from any other items, so it has no explicit input ports, only actions (flip, activate) which can alter the output state. The output port can of course be acquired via get_out, so that the toggle can be used to send signals into a circuit.

The pulse class is a little more interesting:

class pulse {
 class clock_body {
 size_t& ms;
 int& reps;
 signal_t val;
 public:
 clock_body(size_t& _ms, int& _reps) : ms(_ms), reps(_reps), val(low) {}
 bool operator()(signal_t& out) {
 rt_sleep(ms); // our own portable sleep function
 if (reps>0) --reps;
 if (val==low) val = high;
 else val = low;
 out = val;
 return reps>0 || reps == -1;
 }
 };
 graph& my_graph;
 size_t ms, init_ms;
 int reps, init_reps;
 source_node < signal_t > clock_node;

 public:
 pulse(graph& g, size_t _ms=1000, int _reps=-1) : 
 my_graph(g), ms(_ms), init_ms(_ms), reps(_reps), init_reps(_reps),
 clock_node(g, clock_body(ms, reps), false)
 {}
 pulse(const pulse& src) : 
 my_graph(src.my_graph), ms(src.init_ms), init_ms(src.init_ms),
 reps(src.init_reps), init_reps(src.init_reps), 
 clock_node(src.my_graph, clock_body(ms, reps), false)
 {}
 ~pulse() {}
 pulse& operator=(const pulse& src) {
 ms = src.ms; init_ms = src.init_ms; 
 reps = src.reps; init_reps = src.init_reps;
 return *this; 
 }
 sender < signal_t > & get_out() { return clock_node; }
 void activate() { clock_node.activate(); }
 void reset() { reps = init_reps; }
 };

This class is based on the source_node. It generates a signal, alternating between low and high, every ms milliseconds. There is also an option to repeat the alternation a certain number of times and then stop, which is useful for designing simulations that use a clock but also terminate. The source_node body sleeps for a duration before flipping the signal and sending it. It doesn’t begin sending signals immediately, but requires activation. In the case of a non-infinite clock (reps is set), once the pulse object has run for the given number of repetitions, it can be reset and reactivated to use it again.

Next, we discuss two output devices, the LED and the digit. The LED is simply a tiny light that is on while the signal it is receiving is high, and off when the signal is low. For simple text display, the LED looks like this: (*) when it is on and ( ) when it is off. The digit device receives a four-bit input and displays a single hexadecimal digit. For simulations, both devices have the option of continuously displaying their state as it changes, or a silent mode, which displays only when a display method is called.

class led {
 class led_body {
 signal_t &state;
 string &label;
 bool report_changes;
 bool touched;
 public:
 led_body(signal_t &s, string &l, bool r) :
 state(s), label(l), report_changes®, touched(false)
 {}
 continue_msg operator()(signal_t b) {
 if (!touched || b!=state) {
 state = b;
 if (state != undefined && report_changes) {
 if (state) printf("%s: (*)n", label.c_str());
 else printf("%s: ( )n", label.c_str());
 }
 touched = false;
 }
 return continue_msg();
 }
 };
 graph& my_graph;
 string label;
 signal_t state;
 bool report_changes;
 function_node < signal_t, continue_msg > led_node;
 public:
 led(graph& g, string l, bool rc=false) : my_graph(g), label(l), state(undefined), 
 report_changes(rc), led_node(g, 1, led_body(state, label, report_changes))
 {}
 led(const led& src) : my_graph(src.my_graph), label(src.label), state(undefined), 
 report_changes(src.report_changes), 
 led_node(src.my_graph, 1, led_body(state, label, report_changes)) 
 {}
 ~led() {}
 led& operator=(const led& src) { 
 label = src.label; state = undefined; report_changes = src.report_changes; 
 return *this;
 }
 receiver < signal_t > & get_in() { return led_node; }
 void display() { 
 if (state == high) printf("%s: (*)n", label.c_str());
 else if (state == low) printf("%s: ( )n", label.c_str());
 else printf("%s: (u)n", label.c_str());
 }
 };

The led class contains a simple function_node that has no meaningful output (we use a continue_msg to indicate this) and thus no successors. Another way to implement this would be with an overwrite_node, but we would lose the report_changes functionality. Similarly, the digit class also cannot have successors, but we reused the gate base class to implement it, since it has multiple bits of input and needs to update its state whenever one of the inputs changes.

class digit : public gate < four_input > {
 using gate < four_input > ::my_graph;
 typedef gate < four_input > ::ports_type ports_type;
 typedef gate < four_input > ::input_port_t input_port_t;
 class digit_body {
 signal_t ports[4];
 unsigned int &state;
 string &label;
 bool& report_changes;
 public:
 digit_body(unsigned int &s, string &l, bool& r) : state(s), label(l), report_changes® {
 for (int i=0; i < N; ++i) ports[i] = undefined;
 }
 void operator()(const input_port_t::output_type& v, ports_type& p) {
 unsigned int new_state = 0;
 if (v.indx == 0) ports[0] = std::get < 0 > (v.result);
 else if (v.indx == 1) ports[1] = std::get < 1 > (v.result);
 else if (v.indx == 2) ports[2] = std::get < 2 > (v.result);
 else if (v.indx == 3) ports[3] = std::get < 3 > (v.result);
 if (ports[0] == high) ++new_state;
 if (ports[1] == high) new_state += 2;
 if (ports[2] == high) new_state += 4;
 if (ports[3] == high) new_state += 8;
 if (state != new_state) {
 state = new_state;
 if (report_changes) {
 printf("%s: %xn", label.c_str(), state);
 }
 }
 }
 };
 string label;
 unsigned int state;
 bool report_changes;
 public:
 digit(graph& g, string l, bool rc=false) : 
 gate < four_input > (g, digit_body(state, label, report_changes)), 
 label(l), state(0), report_changes(rc) {}
 digit(const digit& src) : 
 gate < four_input > (src.my_graph, digit_body(state, label, report_changes)), 
 label(src.label), state(0), report_changes(src.report_changes) {}
 ~digit() {}
 digit& operator=(const digit& src) { 
 label = src.label; state = 0; report_changes = src.report_changes; 
 return *this;
 }
 void display() { printf("%s: %xn", label.c_str(), state); }
 };

Because digit inherits from gate, it reuses gate’s get_in methods to connect to the ports of a digit object.

Here’s an example code to test out the four-bit adder. First, create a graph:

graph g;

Then, create the four-bit adder, some toggles with which to set the inputs to the adder, and a digit and an LED to display the output:

four_bit_adder four_adder(g);
 std::vector < toggle > A(4, toggle(g));
 std::vector < toggle > B(4, toggle(g));
 toggle CarryIN(g);
 digit Sum(g, "SUM");
 led CarryOUT(g, "CarryOUT");

Next, connect our toggles to the input ports of the adder, and connect the adder’s output ports to the display devices:

for (int i=0; i<4; ++i) {
 make_edge(A[i].get_out(), four_adder.get_A(i));
 make_edge(B[i].get_out(), four_adder.get_B(i));
 make_edge(four_adder.get_out(i), Sum.get_in(i));
 }
 make_edge(CarryIN.get_out(), four_adder.get_CI());
 make_edge(four_adder.get_CO(), CarryOUT.get_in());

Almost ready to go, activate all the switches at the low state so that everything starts at zero:

for (int i=0; i<4; ++i) {
 A[i].activate();
 B[i].activate();
 }
 CarryIN.activate();

Now I can start flipping toggles. I’ve set digit and led to display only when requested by default, because I don’t want to see all the changes before this circuit reaches a steady state. Let’s try 8+5:

A[3].flip();
 B[0].flip();
 B[2].flip();

Wait for the circuit to reach a steady state:

g.wait_for_all();

Now display the results:

Sum.display();
 CarryOUT.display();

And here they are:

SUM: d
 CarryOUT: ( )

And with that, I'll wrap up this blog by saying that the logic simulation example code is available as an example in Intel® TBB 4.0 Update 4, and that it has several other interesting features, like push button and constant signal input devices, NAND and NOR gates, and a D-latch circuit example. Please let us know of other interesting use cases for the or_node and any other feedback you’d be willing to give.

Para obter informações mais completas sobre otimizações do compilador, consulte nosso aviso de otimização.