Trunc() bug - simulator occasionally ignores trunc() timestep

@datkin and all.

I worked on solving this same issue a few months ago – simulating discrete motor control (SVPWM) in C-block with the motor control bridge circuit (and even the mechanical motor model) simulated in the schematic.

I also needed to ensure a strict sample clock that only depended on inst->tNext. The solution I came up with is similar but much simpler than what you wound up with last, above. You may already have progressed to simplify your solution to match mine, but for what it’s worth this is all you need, works for me very accurately (to better than a femto-second, my code checks for this tolerance, but it’s probably better than that, spot on)…

See how simple my MaxExtStepSize call is. Rather than compute tNext every time MaxExtStepSize is called, tNext is already pre-calculated once in the main routine. This will save you some simulation time.

extern "C" __declspec(dllexport) void svpwm_motorcontroller(struct sSVPWM_MOTORCONTROLLER **opaque, double t, union uData *data)
{
    // All the initialization code...

    // Implementation code
    if (t >= inst->tNext) {
        // Warn if t is not close enough to tNext
        if ((t-inst->tNext)>1e-15)  Display("Warning: t=%.16lf is greater than tNext=%.16lf\n", t, inst->tNext);

        // All your sample processing code here...

        // Setup tNext.
        // inst->tNext += inst->dT;    // To avoid accumulation of binary float/double error, might be best to set it based on an incrementing multiple of dT.
        inst->tNext = inst->dT * (++inst->ClkCount);
    }
}

extern "C" __declspec(dllexport) double MaxExtStepSize(struct sSVPWM_MOTORCONTROLLER *inst, double t)
{
   return inst->tNext-t; // implement a good choice of max timestep size that depends on struct sSVPWM_MOTORCONTROLLER
}

1 Like

Hi Ean365,

That’s a nice simple solution for the case where you have a fixed clock.

In my case I am running with a verilated power supply controller model that has a variable timestep. So my tNext comes from calling vinst->nextTimeSlot() on the verilated model. The variable timestep means that you have to put a bit more effort into keeping track of t.

Thanks

Dale

Hmmm. I didn’t see that in your test code – it just looked like you were testing a fixed clock – and I’m not as familiar with Verilator or what you mean exactly by “verilated time clock”.

But, assuming that you mean that the clock cycle time – the period to the next clock edge that you need to attend to – changes after each call to your main routine, this is actually what I am doing as well. In SVPWM motor control, the output PWM timing – the time to the next edge – changes every PWM cycle in accordance with the sampled feedback from the motor. However, it is all predicted at the beginning of the cycle, and nothing will happen mid-cycle to change it. Then all I need to do each time my main function returns is to make sure that tNext is some (clock edge) time in the future – then just wait. This allows the sim to run at fastest speed while still precisely hitting each clock-edge (even if I vary the time to next edge). (Your power supply is probably using PWM as well.)

Otherwise, I would either need to increase my sample time, or implement something more asynchronous, which…
I’m not sure, but believe that if there was something mid-cycle, like an asynchronous analog comparator, that needed to be captured accurately AND would cause an immediate change in the output from my function/component, then it might involve use of the Trunc() function in QSPICE.

Maybe we are doing (and describing) the same thing… Would it not work if in your main routine you set inst->tNext=nextTimeSlot()? And, then just use the same MaxExtStepSize() as mine? I was only remarking on your trunc_bug_x1.cpp “test” code that you linked in your Mar 24 post, where you were performing a somewhat complex calculation every time MaxExtStepSize() is called (which is very often in Qspice).

Again, not sure if this is relevant to your task, but still seemed like it would work. Anyway, offered for what it’s worth, as it sounds like you got yours running for your purposes. So, just offering this in case it makes a difference.

(I am not a QSPICE let alone Qspice C-block “expert” beyond the very few things I have worked though with it so far – but I do thank and think well of Mike E. (and Qorvo) every time I use it, both for his generous and invaluable prior work and for the gift of mixed analog/digital SPICE simulation capability.)

Yes , agreed. It is amazing what you can achieve with Verilator and Qspice given the cost.

From my understanding I don’t think your code is quite right.

The value that you return from MaxExtStepSize needs to be your suggested maximum step from the last evaluation to the next evaluation. The t coming into MaxExtStepSize is the suggested next evaluation time and NOT the last evaluation time. So your suggestion of (t_next-t) is incorrect.
This is why I am storing t_last in the first function and then returning (t_next - t_last) in the second function.

OK, that “t” parameter in MaxExtStepSize() was new to me. Not sure when Mike added it.

Per Mike, it is the “completed time,” not some hypothetical next step time (as in Trunc()). Have you actually seen it be a hypothetical?

Edit: I just gave this a minimal test (save “t” in eval function and compare to “t” in MaxExtStepSize()). They were always the same. Just FYI.

–robert

The t passed into MaxExtStepSize() seems to me to be sim time and is updated with every call to MaxExtStepSize. Below is the simple code I used to test it, and a few other things. It just simulates a simple discrete low-pass filter. I made a very simple schematic block for it with a step input and an output pin. (I also ran the step input to an RC low-pass filter (1K and 159nF) with the same values in the schematic to see how well it matches the digital filter.)

My understanding from this is that MaxExtStepSize() is for when you have prior knowledge about how soon something in your model is going to change or needs to update and you are letting Qspice know what your timeframe is; whereas Trunc() is for when you are concerned that you will “miss” an external change that would cause a change in your model and so need to “go back in time” a little so as to “creep up on it” to capture it within a certain time tolerance, ttol.

Anyway, watch the debug output from below. It seems at least to suggest that the way I am using it works properly (and the other work I am doing seems to also work properly) as far as hitting the sample times exactly while otherwise allowing the sim to run quickly.

I would certainly welcome Mike’s input on this, especially if this is not proper (or in any case)! Also, since mixed mode simulation with “digital” (typically “sampled”) logic is what the C-block (and Veri-block) is primarily meant for, it would be great to establish the best/proper/quickest method to do this.

Edit: One thing I forgot to mention is that below I simply add dT to get tNext, which can accumulate floating point errors over time, and so have since added a sample counter to my code which I now multiply by dT to calculate tNext. (In my case, may actually do both, for instance when I have “edges” in between PWM cycles, I will add a certain dT moving an “edge” within a PWM period, but then use the sample-count times dT when calculating the next whole PWM cycle “edge”, so as to ensure that any floating point errors don’t accumulate.)
Also, I have since eliminated the Trunc() function in cases where I don’t have any concerns, as outlined above, for external (asynchronous) changes. Which gives back a few processor/sim cycles, hopefully allowing it to run just a little bit faster.

// Automatically generated C++ file on Sat Apr  5 15:55:05 2025
//
// To build with Digital Mars C++ Compiler:
//
//    dmc -mn -WD dll_t_test.cpp kernel32.lib

#include <stdio.h>
#include <malloc.h>
#include <stdint.h>
#include <float.h>
#include <math.h>

extern "C" __declspec(dllexport) int (*Display)(const char *format, ...) = 0; // works like printf()
extern "C" __declspec(dllexport) const double *DegreesC                  = 0; // pointer to current circuit temperature
extern "C" __declspec(dllexport) const int *StepNumber                   = 0; // pointer to current step number
extern "C" __declspec(dllexport) const int *NumberSteps                  = 0; // pointer to estimated number of steps
extern "C" __declspec(dllexport) const char* const *InstanceName         = 0; // pointer to address of instance name
extern "C" __declspec(dllexport) const char *QUX                         = 0; // path to QUX.exe
extern "C" __declspec(dllexport) const bool *ForKeeps                    = 0; // pointer to whether being evaluated non-hypothetically
extern "C" __declspec(dllexport) const bool *HoldICs                     = 0; // pointer to whether instance initial conditions are being held
extern "C" __declspec(dllexport) int (*DFFT)(struct sComplex *u, bool inv, unsigned int N, double scale) = 0; // discrete Fast Fourier function

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 In
#undef Out

const double Fs=20000;  // Sample frequency in Hz
const double dT=1.0/Fs; // Sample period in sec

struct sDLL_T_TEST
{
   double Klpf;   // LPF gain = (a/2)/(a/2+1) where a=2*pi*Fc/Fs
   double In;     // Last input
   double Out;    // Last output
   double tNext;  // Last t
};

extern "C" __declspec(dllexport) void dll_t_test(struct sDLL_T_TEST **opaque, double t, union uData *data)
{
   double  In  = data[0].d; // input
   double &Out = data[1].d; // output

   struct sDLL_T_TEST *inst = *opaque;

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

// Module instantiation initialization code:
      inst->Klpf=(M_PI*1000)/(M_PI*1000+Fs);
      inst->In=0;
      inst->Out=0;
      inst->tNext=0;

      Display("I: t=%.9lf, Keeps=%i, Step=%i/%i\n", t, *ForKeeps, *StepNumber, *NumberSteps);
   }

// Implement module evaluation code here:
   if (t >= inst->tNext) {
      Display("R: t=%.9lf, dT=%.9f, In=%lf, Keeps=%i, Step=%i/%i\n", t, t-inst->tNext, In, *ForKeeps, *StepNumber, *NumberSteps);
      Out = inst->Out += (In + inst->In - inst->Out*2.0) * inst->Klpf; // Filter
      inst->In = In; // Remember last input
      inst->tNext+=dT;
   }
   else Display("r: t=%.9lf, dT=%.9f, In=%lf, Keeps=%i, Step=%i/%i\n", t, t-inst->tNext, In, *ForKeeps, *StepNumber, *NumberSteps);
}

extern "C" __declspec(dllexport) double MaxExtStepSize(struct sDLL_T_TEST *inst, double t)
{
   Display("S: t=%.9lf, tNext=%.9lf, MaxExtStepSize()=%.9lf, Keeps=%i, Step=%i/%i\n", t, inst->tNext, inst->tNext-t, *ForKeeps, *StepNumber, *NumberSteps);
   return inst->tNext-t; // implement a good choice of max timestep size that depends on struct sDLL_T_TEST
}

extern "C" __declspec(dllexport) void Trunc(struct sDLL_T_TEST *inst, double t, union uData *data, double *timestep)
{ // limit the timestep to a tolerance if the circuit causes a change in struct sDLL_T_TEST
   const double ttol = 1e-9; // 1ns default tolerance
   if(*timestep > ttol)
   {
      //struct sDLL_T_TEST tmp = *inst;
      //dll_t_test(&(&tmp), t, data);
      if(t >= inst->tNext) // implement a meaningful way to detect if the state has changed
         *timestep = ttol;
   }
   Display("T: t=%.9lf, Keeps=%i, Step=%i/%i\n", t, *ForKeeps, *StepNumber, *NumberSteps);
}

extern "C" __declspec(dllexport) void Destroy(struct sDLL_T_TEST *inst)
{
   Display("D: t=%lf, Keeps=%i, Step=%i/%i\n", -1.0, *ForKeeps, *StepNumber, *NumberSteps);
   free(inst);
}