Jump to content

How to implement a proper clock mux in SystemC ?


DavidC
 Share

Recommended Posts

By "proper" clock mux I mean a run-time selectable MUX (very much like what you would get in a hardware implementation), that preserves the attributes of a SystemC clock object (e.g. the clock period, so that downstream of the MUX you can use this trick).

A naïve implementation might have the interface below:

SC_MODULE(clk_mux) {
  sc_in_clk     clk1;
  sc_in_clk     clk2;
  sc_in< bool > clk_sel;
  sc_out_clk    clk;
 };

In practise, I'm not sure sc_out_clk can be used. What might have to be used instead is sc_export< sc_signal_in_if<bool> >. Anyway, the above is for illustration purposes only.

The way I've worked around it so far is by using what I would call an 'elaboration-time' selectable MUX instead. Ports are bound to the right clock (clk1 or clk2) at elaboration, but once the simulation has started it's obviously not possible to switch clocks. While it is this a step in the right direction, it's not as flexible as I'd like.

 

Bonus question: assuming a run-time selectable MUX can be implemented in SystemC, is it possible for any blocks downstream to automatically detect that the clock they receive has changed ? (so the behaviour of these blocks could be adjusted, for example when the clock period changes during the course of a simulation) 

 

 

Link to comment
Share on other sites

2 hours ago, DavidC said:

By "proper" clock mux I mean a run-time selectable MUX (very much like what you would get in a hardware implementation), that preserves the attributes of a SystemC clock object (e.g. the clock period, so that downstream of the MUX you can use this trick).

A naïve implementation might have the interface below:

SC_MODULE(clk_mux) {
  sc_in_clk     clk1;
  sc_in_clk     clk2;
  sc_in< bool > clk_sel;
  sc_out_clk    clk;
 };

In practise, I'm not sure sc_out_clk can be used. What might have to be used instead is sc_export< sc_signal_in_if<bool> >. Anyway, the above is for illustration purposes only.

The way I've worked around it so far is by using what I would call an 'elaboration-time' selectable MUX instead. Ports are bound to the right clock (clk1 or clk2) at elaboration, but once the simulation has started it's obviously not possible to switch clocks. While is this a step in the right direction, it's not as flexible as I'd like.

 

Bonus question: assuming a run-time selectable MUX can be implemented in SystemC, is it possible for any blocks downstream to automatically detect that the clock they receive has changed ? (so the behaviour of these blocks could be adjusted, for example when the clock period changes during the course of a simulation) 

 

 

This looks suspiciously like a professor’s homework assignment or exam. 
 

the answer is yes and there are many ways to do it. 

Link to comment
Share on other sites

1 hour ago, David Black said:

This looks suspiciously like a professor’s homework assignment or exam. 

Well.... it might look like it, but it's not..... I have 20+ years experience with advanced digital signal processing & hardware implementation 😉

I started learning SystemC a couple of years ago and I'm just not an expert. I can make most things work. It doesn't mean I always know how make them right, and that's why I'm trying to learn best practices over the years (that's what you call 'experience' isn't it ?)

Of course you can do the obvious thing:

  SC_CTOR(clk_mux)
  {
    SC_METHOD(gen_mux);
    sensitive << clk_sel;
  }

  inline void gen_mux() { clk = clk_sel.read() ? clk2 : clk1; };

but I'm facing difficulties somewhere else. 

On a given 'clk_mux' instance, the 'sc_out_clk' port can be bound to sc_signal< bool >. Due to my lack of detailed understanding of the SystemC concepts, I could very well be wrong, but I believe this is where the attributes of my SystemC clock object get lost. Essentially 'clocks' are entering my mux, but what's coming out is just a wire, that has no 'period' attribute

 

1 hour ago, David Black said:

there are many ways to do it.

Would you mind sharing some ideas at least, if not a full implementation ? (shouldn't take more than 20 lines of code I believe, or it sounds like a pretty complicated way to implement a mux)

Link to comment
Share on other sites

So, I thought I had cracked it...  but in fact it turns out not.

Here is what I did - All in a single file (main.cpp) for your convenience if you want to compile, run and experiment.

#include <systemc.h>


// Derive sc_export - Overload the binding method so the export doesn't complain about being already bound
template<class IF>
class my_sc_export : public sc_export<IF>
{
 public:
    void bind( IF& interface_ )
    {
      this->sc_export<IF>::m_interface_p = &interface_;
    }
};


SC_MODULE(clk_mux) {

  sc_in_clk                              CLK1_IN;
  sc_in_clk                              CLK2_IN;
  sc_in< bool>                           CLK_SEL_IN;
  my_sc_export< sc_signal_in_if<bool> >  CLK_OUT;

  SC_CTOR(clk_mux)
  {
    SC_METHOD(gen_mux);
    sensitive << CLK_SEL_IN;

    // Binding ports can be delayed till before_end_of_elaboration()
    // But unlike ports, it seems that exports need to be bound at construction time (or else, expect "sc_export instance has no interface"). Why ?
    CLK_OUT(clk3);   // Need to bind to a properly defined interface - Binding to CLK1_IN.get_interface() is not good enough because it's NULL at this point
  }

  inline void gen_mux() { CLK_OUT(*dynamic_cast<sc_clock *>((CLK_SEL_IN.read() ? CLK2_IN : CLK1_IN).get_interface())); };

private:
  sc_clock clk3{"clk3", 3, SC_NS, 0.5};
};


SC_MODULE(target) {

  sc_in_clk CLK_IN;

  SC_CTOR(target)
  {
    SC_METHOD(detect_clock_change);
    sensitive << CLK_IN;
  }

  void end_of_elaboration()
  {
    clk_p = dynamic_cast<sc_clock *>(CLK_IN.get_interface());
  }

  void detect_clock_change()
  {
    sc_clock *current_clk_p = dynamic_cast<sc_clock *>(CLK_IN.get_interface());

    if (clk_p != current_clk_p)
    {
      cout << "Clock change detected - New clock period = " << clk_p->period() << endl;
      clk_p = current_clk_p;
    }
  }

private:
  sc_clock *clk_p;  
};


SC_MODULE(tb) {

  sc_clock          clk1      {"clk1", 1, SC_NS, 0.5};
  sc_clock          clk2      {"clk2", 2, SC_NS, 0.5};
  sc_signal< bool > clk_sel   {"clk_sel"};
  clk_mux           u_clk_mux {"u_clk_mux"};
  target            u_target  {"u_target"};

  SC_CTOR(tb)
  {
    u_clk_mux.CLK1_IN   (clk1);
    u_clk_mux.CLK2_IN   (clk2);
    u_clk_mux.CLK_SEL_IN(clk_sel);

    u_target.CLK_IN     (u_clk_mux.CLK_OUT);

    SC_THREAD(scenario);
  }

  void scenario(void)
  {
    clk_sel = false;
    wait(10, SC_NS);

    clk_sel = true;
    wait(10, SC_NS);

    sc_stop();
  }
};


int sc_main (int argc, char* argv[])
{
  tb u_tb("u_tb");
  sc_start();

  return 0;
}

 

In practise, that doesn't work because even if the MUX output (rather 'export') correctly points to varying interfaces over time (clk1 or clk2), the target doesn't see that at all because its input port is bound to a fixed interface (clk3) at construction time....

I guess there aren't really any ways to work around that, are there ?

On 1/28/2022 at 2:34 PM, David Black said:

the answer is yes and there are many ways to do it. 

Ah well I would still love to see an implementation proposal (not just vague concepts) that actually works 😉

Link to comment
Share on other sites

3 hours ago, DavidC said:

In practise, that doesn't work because even if the MUX output (rather 'export') correctly points to varying interfaces over time (clk1 or clk2), the target doesn't see that at all because its input port is bound to a fixed interface (clk3) at construction time....

Correct. The port-to-export binding chain can be arbitrarily long.. like port0->port1->...->export_n->export_n-1->..->export0->channel. Fetching the final downstream channel pointer through this full path every time a function is called on port0 is not optimal. Since port binding should be complete by elaboration stage (as per SystemC semantics), port0 can safely store the pointer to the channel at end of elaboration.

One way is to let the downstream mux set a pointer in the upstream clock receiver. It could be as "dumb" as the following (caveat emptor: this is only a rough sketch and will likely not compile)

SC_MODULE(clk_mux)
{
   public:
     sc_vector<sc_in<bool>> in;
     sc_in<int>  clk_sel; // Which input port to select

     register_downstream_ptr(sc_in<bool> **ptr) { m_ptr = ptr; }   // to be called from top module, register receiver's port pointer

     // method sensitive to clk_sel changes
     void clk_sel_changed() {
       sc_interace *intfs = in[clk_sel].get_interface();
       (*m_ptr) = &(intfs);
     }
};

SC_MODULE(clk_receiver)
{
   public:
     sc_in<bool> *ptr; // Is set outside the module
};

This can be made better - by e.g., using an observer pattern.

The other way (cleaner) is to implement your own channel, say my_clock. Like sc_clock, this will provide a period() function, provide events for pos/neg edge and sc_signal_in_if<bool> interface. In addition, it can have input ports and a clock select port. Deriving from sc_clock would have been an option..but sc_clock methods (like period(), duty_cycle() etc) are not virtual and sc_clock is a primitive channel (so the derived my_clock can't have additional ports).

Let me know if this helps. I'll try to post a sketch of the implementation at a later date.

 

Link to comment
Share on other sites

Thanks for the feedback, for the details and proposals !

11 hours ago, karthickg said:

One way is to let the downstream mux set a pointer in the upstream clock receiver.

I guess that was a typo, what you really meant is "let the downstream clock receiver set a pointer in the upstream mux".

There is little doubt that the implementation you provided is going to work (I have not tried, but am quite optimistic). Having said that it doesn't seem like an extremely flexible solution (perhaps that's what you alluded to by "This can be made better")

One of the main restrictions I can see is that when a large number of "receivers" get their clock from the mux, the entire list need to be exhaustively registered inside the mux from the top-level. Implicitly, it also means that the full hierarchical names of all receiver instances has to be specified (to register their *ptr). This is not very maintainable when the hierarchy changes, etc....

Imagine a situation where you start from an existing system without any clock mux, a system that is fully functional with lots of modules that communicate with each other. Now, somewhere on the path between an existing (plain vanilla) sc_clock and a (possibly long) list of scattered receivers you want to introduce a "clock mux". Possibly I'm biased by my hardware centric vision of the world, but ideally this should be (almost) totally transparent and quite painless. That would be my goal.

11 hours ago, karthickg said:

The other way (cleaner) is to implement your own channel, say my_clock. (...) Deriving from sc_clock would have been an option..but sc_clock methods (like period(), duty_cycle() etc) are not virtual and sc_clock is a primitive channel (so the derived my_clock can't have additional ports).

Even before you made that suggestion, I started working on a variant of my initial implementation, that follows the same idea (deriving sc_clock). This is not an attempt at merging the clock and the mux (like you seem to suggest above) but rather an attempt at cloning a remote clock (one that drives the selected mux input) locally:

// Derive sc_clock - Overload the = operator
class my_sc_clock : public sc_clock
{
 public:
    my_sc_clock& operator = ( const sc_clock& rhs )
    {
      // The below is certainly necessary but isn't sufficient.
      // What else is required to properly clone a remote clock locally?  
      this->m_period = static_cast<const my_sc_clock *>(&rhs)->sc_clock::m_period; // Using the approach from https://stackoverflow.com/a/53726397/8543838
      
      return(*this);
    }
};


SC_MODULE(clk_mux) {

  sc_in_clk                           CLK1_IN;
  sc_in_clk                           CLK2_IN;
  sc_in< bool>                        CLK_SEL_IN;
  sc_export< sc_signal_in_if<bool> >  CLK_OUT;

  SC_CTOR(clk_mux)
  {
    SC_METHOD(gen_mux);
    sensitive << CLK_SEL_IN;

    CLK_OUT(clk3);
  }

  inline void gen_mux() { clk3 = *dynamic_cast<sc_clock *>((CLK_SEL_IN.read() ? CLK2_IN : CLK1_IN).get_interface()); };

private:
  my_sc_clock clk3;
};

At this point I guess you have to be deeply familiar with the inner details of the SystemC library, to understand how sc_clock is implemented. Any expert feedback ?

 

Link to comment
Share on other sites

Reading the answer to this question, I had very high hopes that no_clock (essentially a custom implementation of sc_clock) would effectively solve the challenge mentioned in the last message (the challenge being changing the clock period at run-time).

In fact I could even imagine an even more elegant implementation of a clock mux that's implemented with sc_out_clk instead of sc_export< sc_signal_in_if<bool> > (perhaps my imagination is wrong though, and that's just wishful thinking).

Alas, it seems the current implementation of no_clock is broken:

  • no_clock.hpp includes "no_clock_if.h" which doesn't exist (should be "no_clock_if.hpp" instead)
  • four methods (wait_posedge, wait_negedge, wait_anyedge, wait_sample, wait_setedge) are incorrectly calling wait(t) but, I believe, should be calling sc_core::wait(t) instead
  • no_clock.cpp doesn't compile because no default implementation is provided for three methods that are pure virtual (clock_name, cycles, wait)... At this point I'm kind of giving up.... Would be nice if the author could fix it 😉

EDIT: I've addressed all compilation issues in no_clock - But it turns out it's not as useful as I thought, because it doesn't implement sc_signal_in_if<bool>. Perhaps it can be added.... but it sounds a bit beyond my skills (and it might just defeat the initial purpose, even if I'm happy to admit I don't quite understand what it is).

Link to comment
Share on other sites

I created a basic implementation. You can see it here: https://github.com/Vayavya-Labs/sysc-clock-mux

This is the test file:

int sc_main(int argc, char *argv[])
{
   sc_clock clock0("clock0", sc_time(2, SC_NS));
   sc_clock clock1("clock1", sc_time(3, SC_NS));
   sc_clock clock2("clock2", sc_time(5, SC_NS));
   sc_signal<int> clock_select;
   clock_mux mux("mux", 3);
   clock_receiver rx("rx");

   mux.clock_in[0](clock0);
   mux.clock_in[1](clock1);
   mux.clock_in[2](clock2);
   mux.clock_select(clock_select);
   rx.clock_in(mux);

   clock_select.write(0);
   sc_start(10, SC_NS);
   clock_select.write(1);
   sc_start(20, SC_NS);
   clock_select.write(2);
   sc_start(30, SC_NS);

   return 0;
}

 

And this is the output:

Initial clock period: 2 ns
@0 s: Triggered clock process
@2 ns: Triggered clock process
@4 ns: Triggered clock process
@6 ns: Triggered clock process
@8 ns: Triggered clock process
@10 ns: Clock period changed to:3 ns
@10 ns: Triggered clock process
@12 ns: Triggered clock process
@15 ns: Triggered clock process
@18 ns: Triggered clock process
@21 ns: Triggered clock process
@24 ns: Triggered clock process
@27 ns: Triggered clock process
@30 ns: Clock period changed to:5 ns
@30 ns: Triggered clock process
@35 ns: Triggered clock process
@40 ns: Triggered clock process
@45 ns: Triggered clock process
@50 ns: Triggered clock process
@55 ns: Triggered clock process

Note how the trigger happened at 12 ns - because the clock with period 3 ns has a pos-edge at that time. Note also how the clock process got triggered at 10 ns. This is due a race condition between clock select change and a pos-edge on the current clock.

Link to comment
Share on other sites

8 hours ago, karthickg said:

I created a basic implementation

Thank you so much for that !! 

To be perfectly honest, I am going to have to look at the details to work out how you did that - It seems to be a little bit beyond my average SystemC skills 😉

What I find very elegant is that the receiver can interchangeably and transparently be hooked straight to a sc_clock, or to a clock_mux. In the latter case, what is also great is that the receiver gets events when the upstream clock select changes. 

Having said that I can reproduce all your results on Windows but not on Linux. On Linux, I get segmentation faults unless I comment out the following lines in the clock_receiver:

std::cout << "Initial clock period: " << m_mux->period() << std::endl;

SC_METHOD(clock_changed);
sensitive << m_mux->clock_changed_event();
dont_initialize();

Just to clarify: with these lines commented out, the clock mux works perfectly fine, and the receiver clock process triggers as expected, every 2, 3, or 5 ns. Something seems to be wrong with m_mux but I couldn't really figure out what it is.... Any ideas ?

Bonus question: it seems your implementation mandates that the code is compiled with SC_INCLUDE_DYNAMIC_PROCESSES defined (which is not the default behaviour). Is there any downside to doing that ?

Link to comment
Share on other sites

6 hours ago, DavidC said:

I can reproduce all your results on Windows but not on Linux. On Linux, I get segmentation faults

I've tried quite a few things actually:

  • using pre-generated SystemC libraries that ship with commercial tools (Cadence, Synopsys, etc...)
  • building my own SystemC libraries from source
    • using QuickThreads (default)
    • using posix threads (./configure --enable-pthreads)

and I invariably get the same result.... (crash). Can anyone reproduce ?

Link to comment
Share on other sites

Good to hear that it was helpful and thanks for the feedback!

9 hours ago, DavidC said:

Having said that I can reproduce all your results on Windows but not on Linux. On Linux, I get segmentation faults unless I comment out the following lines in the clock_receiver:

I'm not sure what is the problem, I'm running this on a Ubuntu VM and it works correctly. I'll cross-check. Can you compile with "-g -O0" and run in gdb and send a back-trace?

 

9 hours ago, DavidC said:

Bonus question: it seems your implementation mandates that the code is compiled with SC_INCLUDE_DYNAMIC_PROCESSES defined (which is not the default behaviour). Is there any downside to doing that ?

Yes - that is because the implementation uses `sc_spawn` (need to define that macro in Accellera SystemC simulator if sc_spawn is used). There is no down-side as such.. note that the implementation spawns all required processes at end of elaboration itself (so it is still considered "static process" as per SystemC).

Link to comment
Share on other sites

On 2/4/2022 at 7:55 PM, karthickg said:

I'm running this on a Ubuntu VM and it works correctly.

I've just carried out a few more experiments on a Linux box.

  • I built my own SystemC 2.3.3 library from source using gcc 6.3.0 (that's a constant in these experiments)
  • Then compile and run your example using many different compilers:
    • It always crashes with gcc 6.3.0 (I have quite a few versions available - some that ship with commercial tools, even one that I built from source myself)
    • It works nicely with gcc 7.1.0, 7.3.0, 8.2.0, 10.2.0, 11.2.0

Can anyone reproduce issues with gcc 6.3.0 ?

Are there known bugs in gcc 6.3.0 and/or SystemC 2.3.3 or some incompatibility between the two ?

Link to comment
Share on other sites

12 hours ago, DavidC said:

Are there known bugs in gcc 6.3.0 and/or SystemC 2.3.3 or some incompatibility between the two ?

You can try running "make check" (or "make test", don't remember exactly) after installing SystemC - to run the included tests. I don't currently have a setup with an older GCC version and can't easily check this.

Link to comment
Share on other sites

On 2/12/2022 at 4:37 AM, karthickg said:

You can try running "make check"

All tests are passing very nicely with gcc 6.3.0, so it's something quite subtle... Without in-depth investigation, it's hard to say if that's a compiler issue or a SystemC issue (the fact it works with more recent compilers suggests it's related to the compiler, but it's not an absolute proof).

Link to comment
Share on other sites

On 2/14/2022 at 10:41 PM, DavidC said:

All tests are passing very nicely with gcc 6.3.0, so it's something quite subtle... Without in-depth investigation, it's hard to say if that's a compiler issue or a SystemC issue (the fact it works with more recent compilers suggests it's related to the compiler, but it's not an absolute proof).

I checked on a CentOS system with gcc 4.8, and it works fine (with one change - I need to specify "-std=c++11" for that compiler version).

 

$ gcc --version
gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-44)
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ make
g++ -Wall -Werror -DSC_INCLUDE_DYNAMIC_PROCESSES -g -O0 -std=c++11 -I /usr/local/systemc233/include -I ./src -I ./test -Wl,-rpath=/usr/local/systemc233/lib-linux64 src/clock_mux.cpp test/clock_mux_test.cpp -L /usr/local/systemc233/lib-linux64 -lsystemc -lm -o sim.x

$ ./sim.x 

        SystemC 2.3.3-Accellera --- Feb  3 2022 13:03:45
        Copyright (c) 1996-2018 by all Contributors,
        ALL RIGHTS RESERVED
Initial clock period: 2 ns
@0 s: Triggered clock process
@2 ns: Triggered clock process
@4 ns: Triggered clock process
@6 ns: Triggered clock process
@8 ns: Triggered clock process
@10 ns: Clock period changed to:3 ns
@10 ns: Triggered clock process
@12 ns: Triggered clock process
@15 ns: Triggered clock process
@18 ns: Triggered clock process
@21 ns: Triggered clock process
@24 ns: Triggered clock process
@27 ns: Triggered clock process
@30 ns: Clock period changed to:5 ns
@30 ns: Triggered clock process
@35 ns: Triggered clock process
@40 ns: Triggered clock process
@45 ns: Triggered clock process
@50 ns: Triggered clock process
@55 ns: Triggered clock process

 

I'm facing errors if I try to compile gcc 6.3.0 from sources - it will take sometime to get that to work. I'll give it a try again in few days.

Link to comment
Share on other sites

  • 2 weeks later...
On 2/15/2022 at 10:24 PM, karthickg said:

with gcc 4.8, and it works fine (with one change - I need to specify "-std=c++11" for that compiler version).

Interesting...

So I've made a few more experiments and here is what I find:

1. If I build my own SystemC 2.3.3 library from source using gcc 4.8.3:

./configure CPPFLAGS="-std=c++11" CXXFLAGS="-DSC_CPLUSPLUS=201103L"

Then if I compile and run your example using many different compilers:

  • it works with gcc 4.8.3 and 4.9.3 (similar to your own observations)
  • but crashes with all other versions available to me: 5.2.0, 5.3.0, 5.4.0, 6.2.0, 6.3.0, 7.1.0, 7.2.0, 7.3.0, 8.2.0, 9.3.0, 10.2.0, 11.2.0

2. If I build my own SystemC 2.3.3 library from source using gcc 6.3.0:

./configure CXXFLAGS="-DSC_CPLUSPLUS=201103L"

Then if I compile and run your example using many different compilers (that understand CXX11_ABI=1):

  • it crashes with 5.2.0, 5.3.0, 5.4.0, 6.2.0, 6.3.0,
  • but works with 7.1.0, 7.2.0, 7.3.0, 8.2.0, 9.3.0, 10.2.0, 11.2.0

3. If I build my own SystemC 2.3.3 library from source using gcc 10.2.0, but preserve the CXX11_ABI=0 to be backwards compatible with older gcc's:

./configure CXXFLAGS="-DSC_CPLUSPLUS=201103L -D_GLIBCXX_USE_CXX11_ABI=0"

Then if I compile and run your example using many different compilers

  • I can't link with 4.8.3 (something silly I'm sure but couldn't figure it out)
  • it works with 4.9.3
  • it crashes with all other versions available to me: 5.2.0, 5.3.0, 5.4.0, 6.2.0, 6.3.0, 7.1.0, 7.2.0, 7.3.0, 8.2.0, 9.3.0, 10.2.0, 11.2.0
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...