Learning SystemC: #005 Signal Channels

Learning SystemC: #005 Signal Channels

Posted by

In this post I will talk about SystemC signal channels.
These channels help us establish an easier data synchronization.

Here is a list of content if you want to jump to a particular subject:

1. What Are Signal Channels?
   1.1. Parallelism In A Digital Circuit
   1.2. Parallelism In SystemC
      1.2.1 Example Of A Shift Register Using Only Threads
      1.2.2 Example Of A Shift Register With Current-Next Pair Values
      1.2.3 Example Of A Shift Register With Signal Channel
2. sc_signal
   2.1. Reading And Writing A Signal
   2.2. Events
3. sc_signal<bool> and sc_signal<sc_logic>
4. sc_buffer
   4.1. Example



1. What Are Signal Channels?

The short answer to this question is this one:

Signal channels are the equivalent of non-blocking assignments of a reg in Verilog or a signal in VHDL.

If you have a background in a HDL then everything should be clear for you. If you are not familiar with these concepts I’ll try to explain what it means.

Next in this chapter I’ll show you what it takes to implement correctly a simple shift register without signal channels. Once you understand this functionality we will take a look at how a shift register is implemented with signal channels so you can better understand what a signal channel is doing for you in the background.

1.1. Parallelism In A Digital Circuit

Let’s say that we have to model in our code a 4 bit shift register like in the image bellow.

4 bit shift register

4 bit shift register (Source: Wikipedia)


In a purely C++ program, in order to obtain the shift effect you just allocate a new value for each flip-flop in a strict order:

1
2
3
4
5
6
void on_posedge_clock() {
   Q4 = Q3;
   Q3 = Q2;
   Q2 = Q1;
   Q1 = DataIn
};

In a real electronic circuit all these shifts are happening in parallel: on the rising edge of the clock each flip-flop takes the value of its input (D) and puts it on its output (Q).

But how can this shifting work if all the flip-flops see the rising edge of the clock in the same time? Shouldn’t Data In  value go straight through all the flip-flops?

If we were to simulate this shift register then the inputs and outputs of the first two flip-flops would look like this:

4 bit shift register

Inputs and outputs of first two flip-flops (ideal)

However, in reality these signals would look more something like this:

4 bit shift register

Inputs and outputs of first two flip-flops (reality)


In reality, each flip-flop has a very narrow time window in which it evaluates its input (see the red stripes in the image above). This window is determined by the rising edge of the clock signal.
Because this evaluation window is very narrow you can consider that all the inputs of all the flip-flops are stable.

Once the flip-flops evaluated their inputs they start updating their output values. The output values become stable at the end of the update phase (see the blue stripes in the image above).

This parallelism is possible because no evaluate phase is overlapping with any update phase.

DISCLAIMER: please keep in mind that the above explanation is a very simplified version on how the flip-flops are working. If you want to find out more I recommend studying the physical implementation of flip-flops.

1.2. Parallelism In SystemC

In SystemC parallelism is obtained via processes like threads and methods. But we will see that parallelism is not enough if we want to obtain the same effect as in a real electronic circuit.

1.2.1 Example Of A Shift Register Using Only Threads

Let’s try to implement a 4 bit shift register using only threads.
The first thing we have to do is to create a SystemC module header file like the one below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
SC_MODULE(shift_register) {
     
  //event triggered on the rising edge of the clock
  sc_event clock_rise_ev;
     
  sc_logic data_in;
     
  //model of the flip-flops
  sc_logic q1, q2, q3, q4;
     
  //thread which will update Q1 flip-flop
  void update_q1();
     
  //thread which will update Q2 flip-flop
  void update_q2();
     
  //thread which will update Q3 flip-flop
  void update_q3();
     
  //thread which will update Q4 flip-flop
  void update_q4();
     
   SC_CTOR(shift_register) : data_in(0), q1(0), q2(0), q3(0), q4(0) {
     SC_THREAD(update_q1);
     SC_THREAD(update_q2);
     SC_THREAD(update_q3);
     SC_THREAD(update_q4);
   };
};

shift_register.cpp

The actual implementation of each thread is quite easy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void shift_register::update_q1() {
  for(;;) {
    wait(clock_rise_ev);
    q1 = data_in;
  }
};

void shift_register::update_q2() {
  for(;;) {
    wait(clock_rise_ev);
    q2 = q1;
  }
};

void shift_register::update_q3() {
  for(;;) {
    wait(clock_rise_ev);
    q3 = q2;
  }
};

void shift_register::update_q4() {
  for(;;) {
    wait(clock_rise_ev);
    q4 = q3;
  }
};

shift_register.cpp

At this point we have 4 parallel threads, just like in a real electronic circuit, in which each flip-flop is copying, on the rising edge of the clock, the value of its neighbour. The exception is for the first flip-flop which copies the value of the data input.

To try this code, let’s shift value ‘1’ into our shift register.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void example::generate_clock_thread() {
  //drive '1' on the input of the shift register
  shift_register_inst.data_in = 1;
 
  for(int i = 0; i < 4; i++) {    
    //this wait dictates the clock period
    wait(10, SC_NS);

    shift_register_inst.clock_rise_ev.notify();
   
    wait(SC_ZERO_TIME);
   
    cout << "[" << sc_time_stamp() << "] "
           << "q1: " << shift_register_inst.q1
         << ", q2: " << shift_register_inst.q2
         << ", q3: " << shift_register_inst.q3
         << ", q4: " << shift_register_inst.q4 << endl;
  };
};

If you think that this shift register is working then you’d be wrong. The output I’ve got was this:

1
2
3
4
[10 ns] q1: 1, q2: 1, q3: 0, q4: 0
[20 ns] q1: 1, q2: 1, q3: 1, q4: 1
[30 ns] q1: 1, q2: 1, q3: 1, q4: 1
[40 ns] q1: 1, q2: 1, q3: 1, q4: 1

The explanation is very simple. Each thread is managed in the background by SystemC and the order is more or less random.
In our case, at time 10 ns, first run update_q1() which changed the value of q1 then run update_q2() which used the new value of q1.
Same thing happen at 20 ns between update_q3() and update_q4().

The problem is that the input values of each flip-flop are not stable when they are used in the actual shifting operation. Based on how SystemC is starting the threads one flip-flop might used the old value of its input or the new value.
So we need to make the value of the inputs for each flip-flop stable, just like in a real electronic circuit.

You can run this example on your own on EDA Playground.

1.2.2 Example Of A Shift Register With Current-Next Pair Values

To make the input values stable we need to make something similar with what the real electronic circuit is doing – first we calculate a next value. After some time passed and we are sure nobody is using the current value we will update the current value with the next value.
The changes to our initial code are this:

1
2
3
4
5
6
7
8
9
10
11
SC_MODULE(shift_register) {
  ...
  //transform the single values of each flip-flop into a current-next pair
 
  //model of the current values of the flip-flops
  sc_logic q1_current, q2_current, q3_current, q4_current;

  //model of the next values of the flip-flops
  sc_logic q1_next, q2_next, q3_next, q4_next;
  ...
};

shift_register.h

In the header file we need to change the single value flip-flops into a pair of current-next values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void shift_register::update_q1() {
  for(;;) {
    wait(clock_rise_ev);
    q1_next = data_in.value();
    wait(SC_ZERO_TIME);
    q1_current = q1_next.value();
  }
};

void shift_register::update_q2() {
  for(;;) {
    wait(clock_rise_ev);
    q2_next = q1_current.value();
    wait(SC_ZERO_TIME);
    q2_current = q2_next.value();
  }
};

void shift_register::update_q3() {
  for(;;) {
    wait(clock_rise_ev);
    q3_next = q2_current.value();
    wait(SC_ZERO_TIME);
    q3_current = q3_next.value();
  }
 
};

void shift_register::update_q4() {
  for(;;) {
    wait(clock_rise_ev);
    q4_next = q3_current.value();
    wait(SC_ZERO_TIME);
    q4_current = q4_next.value();
  }
};

shift_register.cpp

In the implementation file each thread first calculates the next value and after a simulation tick, when nobody is interested in the current value, it updates the current value.

In this way, the inputs are stable and we obtain an output like this:

1
2
3
4
5
[10 ns] q1: 0, q2: 0, q3: 0, q4: 0
[20 ns] q1: 1, q2: 0, q3: 0, q4: 0
[30 ns] q1: 1, q2: 1, q3: 0, q4: 0
[40 ns] q1: 1, q2: 1, q3: 1, q4: 0
[50 ns] q1: 1, q2: 1, q3: 1, q4: 1

As you can see this shift-register is working properly.

You can run this example on your own on EDA Playground.

1.2.3 Example Of A Shift Register With Signal Channel

You might be thinking that the implementation of a simple shift register just became quite complicated for such a trivial behavior, and you’d be right. Luckily SystemC provides a signal channel, via class sc_signal, which does for us all that work that we did in the previous chapter.
Here is how our shift register simplifies using the signal channel:

1
2
3
4
5
6
SC_MODULE(shift_register) {
  ...
  //model of the the flip-flops
  sc_signal<sc_logic> q1, q2, q3, q4;
  ...
};

shift_register.h

In the header file we get rid of those current-next pair values as this will be done automatically for us by the signal channel and we model our four flip-flops with sc_signal class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void shift_register::update_q1() {
  for(;;) {
    wait(clock_rise_ev);
    q1.write(data_in.value());
  }
};

void shift_register::update_q2() {
  for(;;) {
    wait(clock_rise_ev);
    q2.write(q1.read());
  }
};

void shift_register::update_q3() {
  for(;;) {
    wait(clock_rise_ev);
    q3.write(q2.read());
  }
 
};

void shift_register::update_q4() {
  for(;;) {
    wait(clock_rise_ev);
    q4.write(q3.read());
  }
};

shift_register.cpp

Each thread becomes much more simple, as signal channel class will take care of updating the current value in the next simulation tick.
The output we obtain by running this code is this

1
2
3
4
5
[10 ns] q1: X, q2: X, q3: X, q4: X
[20 ns] q1: 1, q2: X, q3: X, q4: X
[30 ns] q1: 1, q2: 1, q3: X, q4: X
[40 ns] q1: 1, q2: 1, q3: 1, q4: X
[50 ns] q1: 1, q2: 1, q3: 1, q4: 1

As you can see this shift-register is working properly. We get the ‘X’ because we did not initialized the flip-flops.

You can run this example on your own on EDA Playground.

So in a nutshell, a signal channel will delay the assignment of the new value with a simulation tick to ensure a stable current value when it is used. This is basically a copy of the flip-flop behaviour from a real electronic circuit. In the next chapter we will take a closer look on how it actually does this and what API we can access.

2. sc_signal

The definition of the sc_signal class from Language Reference Manual is this:

Class sc_signal is a predefined primitive channel intended to model the behavior of a single piece of wire carrying a digital electronic signal.

I hope that the behavior of a single piece of wire carrying a digital electronic signal was clearly explained in the previous chapter. If not then please leave me a comment below and I’ll try to provide more explanations.
The sc_signal class is a template class and the argument passed to the template must obey several rules. For details on these rules I recommend looking at chapter “6.4.3 Template parameter T” in Language Reference Manual.

2.1. Reading And Writing A Signal

There are two options for reading the value of a signal:

Reading a signal will return the current value of the signal.

There are also two options for writing a signal:

Writing a signal will update only the the new (or next) value of the signal. You should keep in mind that when doing multiple writes in the same time only the last executed one will matter as that is the one which updates the new value.
If you need this new value you have available function get_new_value().
One important rule that you have to be aware or is that it is illegal to write a signal from more than one process. If you do this you will get an error. However, if you need to have a signal written by multiple processes you must use sc_signal_resolved class.

2.2. Events

sc_signal class provides several event related functions which you might find useful.
Internally there is an event called m_change_event_p which is triggered when the current value of the signal changes. You can get a reference to this event via two functions:

Another special function is event() which returns true if and only if the value of the signal changed in the update phase of the immediately preceding delta cycle.

3. sc_signal<bool> and sc_signal<sc_logic>

In order to come closer to other HDL languages like Verilog or VHDL, SystemC provides aditional functionality to two-value signals. This new functionality focuses on determining the positive and negative edges of a signal and it is implemented in two classes: sc_signal<bool> and sc_signal<sc_logic>.

A positive edge of a signal is when the value changes from 0 to 1, for sc_signal<bool>, or when the signal changes to LOGIC_1 from any value, for sc_signal<sc_logic>.

A negative edge of a signal is when the value changes from 1 to 0, for sc_signal<bool>, or when the signal changes to LOGIC_0 from any value, for sc_signal<sc_logic>.

The additional events in these classes are:

To access these events there are a couple of functions:

4. sc_buffer

sc_buffer class is a child of sc_signal and has a very small difference in comparison with its parent: its default event is triggered whenever the buffer is written not when it is changed. Remember that for sc_signal its event is triggered whenever its value is changed.

Let’s see this behavior in a simple example.

4.1. Example

We have a buffer and a signal initialized to 0.
We will do two identical writes to these variables – first with value 1 and then also with value 1.
The purpose of this exercise is to observe when m_change_event_p event is triggered for the buffer and for the signal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SC_MODULE(example) {

  sc_buffer<bool> sc_buffer_inst;

  sc_signal<bool> sc_signal_inst;

  SC_CTOR(example) :
    sc_buffer_inst("sc_buffer_inst"),
    sc_signal_inst("sc_signal_inst") {
   
    //initialize the buffer and the signal with zero
    sc_buffer_inst.write(0);
    sc_signal_inst.write(0);
  };
};

example.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void  example::update_values() {    
  wait(1, SC_NS);
  cout << "[" << sc_time_stamp() << "] "<< "old value: 0, new value: 1" << endl;
  sc_buffer_inst.write(1);
  sc_signal_inst.write(1);
   
  wait(1, SC_NS);
  cout << "[" << sc_time_stamp() << "] "<< "old value: 1, new value: 1" << endl;
  sc_buffer_inst.write(1);
  sc_signal_inst.write(1);
}
     
void  example::on_buffer_event_thread() {
  const sc_event &default_event = sc_buffer_inst.default_event();
 
  for(;;) {
    wait(default_event);
    cout << "[" << sc_time_stamp() << "] "<<
       "sc_buffer_inst: value: " << sc_buffer_inst.read() << endl;
  }
}
     
void  example::on_signal_event_thread() {
  const sc_event &default_event = sc_signal_inst.default_event();
 
  for(;;) {
    wait(default_event);
    cout << "[" << sc_time_stamp() << "] "<<
       "sc_signal_inst: value: " << sc_signal_inst.read() << endl;
  }
}

example.cpp

The output for this example is this:

1
2
3
4
5
[1 ns] old value: 0, new value: 1
[1 ns] sc_buffer_inst: value: 1
[1 ns] sc_signal_inst: value: 1
[2 ns] old value: 1, new value: 1
[2 ns] sc_buffer_inst: value: 1

 
You can see that for sc_buffer the event m_change_event_p is triggered at every write, regardless if the write changes the value of the buffer.
For sc_signal the event m_change_event_p is only triggered when the value is changed.
You can run this example on your own on EDA Playground.

That’s it for this lesson, hope you find it useful 🙂

Next Lesson: Learning SystemC: #006 Module Hierarchy And Connectivity
Previous Lesson: Learning SystemC: #004 Primitive Channels



Cristian Slav

Leave a Reply

Your email address will not be published.