Learning SystemC: #005 Signal Channels
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.
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:
However, in reality these signals would look more something like this:
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); }; }; |
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; } }; |
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; ... }; |
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(); } }; |
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; ... }; |
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()); } }; |
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:
- const T& read() – function for reading a signal
- operator const T& () – operator overloaded to facilitate reading
Reading a signal will return the current value of the signal.
There are also two options for writing a signal:
- void write(const T&) – function for writing a signal
- operator = (const T& a) – operator overloaded to facilitate writing
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:
- m_negedge_event_p – negative edge event
- m_posedge_event_p – positive edge event
To access these events there are a couple of functions:
- sc_event& posedge_event() – get a reference to the m_posedge_event_p event
- sc_event& negedge_event() – get a reference to the m_negedge_event_p event
- bool posedge() – returns true when m_posedge_event_p is triggered
- bool negedge() – returns true when m_negedge_event_p is triggered
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); }; }; |
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; } } |
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