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);
}