Interesting case of C-code with Trunc()

Hi All,

@RDunn @KSKelvin @Engelhardt
As with my journey to study the C-code implementation in Qspice, I am now learning the Trunc() and what will be the advantage of using this Trunc() and how it may be useful to improve the accuracy of the simulation.

// Automatically generated C++ file on Thu Oct 19 16:11:47 2023
//
// To build with Digital Mars C++ Compiler:
//
//    dmc -mn -WD study_pcmc.cpp kernel32.lib

union uData
{
   bool b;
   char c;
   unsigned char uc;
   short s;
   unsigned short us;
   int i;
   unsigned int ui;
   float f;
   double d;
   long long int i64;
   unsigned long long int ui64;
   char *str;
   unsigned char *bytes;
};

// int DllMain() must exist and return 1 for a process to load the .DLL
// See https://docs.microsoft.com/en-us/windows/win32/dlls/dllmain for more information.
int __stdcall DllMain(void *module, unsigned int reason, void *reserved) { return 1; }

// #undef pin names lest they collide with names in any header file(s) you might include.
#undef CLK
#undef OUT
#undef IL

double pwm;
double clk_prev;

extern "C" __declspec(dllexport) void study_pcmc(void **opaque, double t, union uData *data)
{
   double  CLK = data[0].d; // input
   double  IL  = data[1].d; // input
   double &OUT = data[2].d; // output

// Implement module evaluation code here:
   if((clk_prev<=0.5)&&(CLK>=0.5))
   {
      pwm = 5;
   }
   if(IL>1)
   {
      pwm = 0;
   }
   clk_prev = CLK;
   OUT = pwm;
}

study_pcmc.cpp (1.2 KB)

// Automatically generated C++ file on Thu Oct 19 16:07:55 2023
//
// To build with Digital Mars C++ Compiler:
//
//    dmc -mn -WD study_pcmc_trunc.cpp kernel32.lib

#include <malloc.h>

union uData
{
   bool b;
   char c;
   unsigned char uc;
   short s;
   unsigned short us;
   int i;
   unsigned int ui;
   float f;
   double d;
   long long int i64;
   unsigned long long int ui64;
   char *str;
   unsigned char *bytes;
};

// int DllMain() must exist and return 1 for a process to load the .DLL
// See https://docs.microsoft.com/en-us/windows/win32/dlls/dllmain for more information.
int __stdcall DllMain(void *module, unsigned int reason, void *reserved) { return 1; }

void bzero(void *ptr, unsigned int count)
{
   unsigned char *first = (unsigned char *) ptr;
   unsigned char *last  = first + count;
   while(first < last)
      *first++ = '\0';
}

// #undef pin names lest they collide with names in any header file(s) you might include.
#undef CLK
#undef OUT
#undef IL

struct sSTUDY_PCMC_TRUNC
{
  // declare the structure here
  double pwm;
  double clk_prev;
};

extern "C" __declspec(dllexport) void study_pcmc_trunc(struct sSTUDY_PCMC_TRUNC **opaque, double t, union uData *data)
{
   double  CLK = data[0].d; // input
   double  IL  = data[1].d; // input
   double &OUT = data[2].d; // output

   if(!*opaque)
   {
      *opaque = (struct sSTUDY_PCMC_TRUNC *) malloc(sizeof(struct sSTUDY_PCMC_TRUNC));
      bzero(*opaque, sizeof(struct sSTUDY_PCMC_TRUNC));
   }
   struct sSTUDY_PCMC_TRUNC *inst = *opaque;

// Implement module evaluation code here:
   if((inst->clk_prev<=0.5)&&(CLK>=0.5))
   {
      inst->pwm = 5;
   }
   if(IL>1)
   {
      inst->pwm = 0;
   }
   inst->clk_prev = CLK;
   OUT = inst->pwm;
}

extern "C" __declspec(dllexport) double MaxExtStepSize(struct sSTUDY_PCMC_TRUNC *inst)
{
   return 1e308; // implement a good choice of max timestep size that depends on struct sSTUDY_PCMC_TRUNC
}

extern "C" __declspec(dllexport) void Trunc(struct sSTUDY_PCMC_TRUNC *inst, double t, union uData *data, double *timestep)
{ // limit the timestep to a tolerance if the circuit causes a change in struct sSTUDY_PCMC_TRUNC
   const double ttol = 1e-9;
   if(*timestep > ttol)
   {
      double &OUT = data[2].d; // output

      // Save output vector
      const double _OUT = OUT;

      struct sSTUDY_PCMC_TRUNC tmp = *inst;
      study_pcmc_trunc(&(&tmp), t, data);
      if(tmp.pwm != inst->pwm) // implement a meaningful way to detect if the state has changed
         *timestep = ttol;

      // Restore output vector
      OUT = _OUT;
   }
}

extern "C" __declspec(dllexport) void Destroy(struct sSTUDY_PCMC_TRUNC *inst)
{
   free(inst);
}

study_pcmc_trunc.cpp (2.6 KB)

Please refer to the schematic above. Here we can see, that the code that is implemented with Trunc() can force the solver to reduce the timestep to the timestep tolerance when switching event is detected.

While, the comparator+SR latch block and simple C-block implementation completely failed to detect the accurate switching event.

I hope somebody can give a better insight why is it the case?
how does the C-code and the analog circuit are solved for each timestep?

I have a guess that the Analog Circuit solver first followed by C-code solver following the time step as suggested by the Spice solver.
However, with Trunc(), it can force the the Spice solver to revert to the previous step then use a very small time step when a change in any of the any of the data Instance is detected.

Looking for suggestions… Thanks,
Arief,

2 Likes

Sorry,

The simulation waveform results are here (*i dont know why I cant modify the post, 403 forbidden)


@physicboy I have very limited knowledge in C++, this is the reason when I asked @RDunn help in C++ PID project which we posted in this forum.

For logic gate (SR-flop), you can add attribute for instance param TTOL (Temporal tolerance). My guess is that, this may be same concept what Trunc() is doing. In Qspice help, it mentioned : TTOL allows one to determine how accurately the switch time should be found.

For ā€œ403 forbiddenā€, whatever you have image in your post, you cannot modify that post and we believe it is a bug in this forum. Hope Qorvo IT will work to fix that someday.

My understand and guess is that, Qspice is dynamic time step which simulate very fast. My past experience to deal with switching circuit (logic, SMPS), is to limit maximum time step. TTOL is available for major switching device likes voltage/current controlled switch and logic (Ā„-Device, Ā£-Device and €-Device). In Qspice, my experience is that I generally don’t need to limit maximum time step but can improving switching accuracy by reduce TTOL. Therefore, total simulation is still fast, but more step is inserted at switching action. That why I think Trunc() in Ƙ-Device is closely related to this concept (in Trunc(), it also define a constant called ttol).
I believe I will learn from this post when you guys digging more into this topic.

1 Like

I’m hoping that we all do. :hand_with_index_finger_and_thumb_crossed:

1 Like

@physicboy, @KSKelvin

I think that I’ve cracked the Trunc() nut. Here’s my theory based on some testing:

  1. Trunc() is always called between ā€œevaluation functionā€ (ā€œEvalFuncā€) calls (well, after a bunch of initial calls at simulation startup).
  2. The t parameter of Trunc() is the proposed next simulation point. I presume that the input port(s) (in uData) reflect the external state at that proposed simulation point. (If this isn’t true, well, my theory is garbage.)
  3. When Trunc() is called, the DLL must decide if it would change it’s output states at this new timepoint given the uData and per-instance data. If so, it can return a shorter timestep.
  4. If Trunc() returns a shorter timestep, Trunc() gets called again before the next EvalFunc call. Go back to step 3.

Now, that still leaves the ā€œwhat value to return in the *timestepā€ bit.

What we don’t want is to tell QSpice to use some ridiculously small timestep for each Trunc() call.

One logical approach (and the way that I was testing) was to decide if the state would change at the new simulation timepoint. If not, do nothing. If so, cut the *timestep interval since the last EvalFunc call (saved the simulation time in the per-instance data) in half. Trunc() gets called again with the new proposed next simulation timepoint. If the state would still change, cut the interval in half again. Rinse, repeat.

That’s one approach. But it assumes that the last timepoint is saved between Trunc() calls. Mike doesn’t do this in his PracticalSMPS demo IIRC. He simply returns 1ns (1us?) if the current Trunc() call changes the proposed output state. It’s not clear to me that this is efficient but I’ve not tested his code for Trunc() calls. And, in his example, this level of time precision is presumably ā€œgood enoughā€ for simulation purposes.

Sorry for the long read. As usual, I might be totally wrong. I’ll try to post some test code on my GitHub dev channel in the next day or two.

Thoughts?

–robert

Hi @RDunn @KSKelvin

Actually I think to fully understand how it all behaves, we also need to know the call sequence between the analog circuit solver and C-code.

In my believe, C-code is called after analog solver (with its exponentially increasing variable step solver). The basic goal of implementing Trunc() is to have the most accurate timestep for EvalFunc() call by ensuring that EvalFunc() only executed with time step equals to the minimum.
The call logic is:

  1. Run analog solver with spice solver tstep suggestion or forced tstep from prev Trunc() call
  2. Trunc() is called to check:
    A. tstep > ttol
    B. run EvalFunc() and see if data struct is changed with the latest analog solver data.
    if A&&B true,
    then forced the analog solver to discard the latest analog data->return to the previous simulation time->use tstep = ttol
    else
    call EvalFunc() and apply the output.

For your speed optimization:
you can choose
a. use *timestep = ttol and use different constant for ttol or
b. use *timestep = *timestep * alpha;//alpha = constant <1

I hope it makes sense to you… let me know your thoughts

Arief,

I’m pretty sure that my explanations of how the QSpice C-Block Trunc() function works have been correct. Of course, I could be wrong…

I’ve posted a ā€œreverse engineeringā€ test suite on my GitHub repository in the dev branch CBlock_Doc folder as TruncTest.zip.

It is ugly code and definitely not a tutorial – just a test jig for investigations.

Please let me know if you try it and what you learn.

–robert