How to use netlist-passed parameter in C++ code

I have implemented a digital mono-directional delay in a DLL using C++.
I took inspiration form Mike’s ACMESEMI switcher dll and the code is the following:

// Automatically generated C++ file on Wed Aug 16 10:01:52 2023
//
// To build with Digital Mars C++ Compiler:
//
//    dmc -mn -WD testdll_x1.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; }

// #undef pin names lest they collide with names in any header file(s) you might include.
#undef A
#undef B
#undef out
#define TDELAY 1e-7

struct SSM 
{
   int state; // -2 means latched off, -1 means B on, 1 mean T on, 0 means all off
   double Tstart;
};

extern "C" __declspec(dllexport) void testdll_x1(struct SSM **opaque, double t, union uData *data)
{
   bool  A   = data[0].b; // input
   bool  B   = data[1].b; // input
   bool &out = data[2].b; // output




// Implement module evaluation code here:
   if(!*opaque)
      *opaque = (struct SSM *) calloc(sizeof(struct SSM), 1);
   struct SSM *instance = *opaque;

   switch(instance->state)
         {
            case  0:
              {
              if(A)
               {
               instance->Tstart=t;
               instance->state=1;
               }
              out=0;
              }
              break;
            case 1:
               if(A && (t-instance->Tstart)>TDELAY)
               {
                instance->state=2;
                out=1;
               }
               if(!A)
               {
               instance->state=0;
               out=0;
               }
              break;
             case 2:
              if(!A)
              {
              instance->state=0;
              out=0;
              }
              break;



            default:
            instance->state=0;
            out=0;
            break;
        }
}

extern "C" __declspec(dllexport) void Trunc(struct SSM *instance, double t, union uData *data, double *timestep)
{
   const double ttol = 1e-9;
   if(*timestep > ttol)
   {
      bool &out       = data[2].b; // output
      const bool _out   = out;
      struct SSM tmp   = *instance;
      struct SSM *inst = &tmp;
      testdll_x1(&inst, t, data);
      if(tmp.state != instance->state)
         *timestep = ttol;
      out                 = _out;
   }
}

Please ignore input B, it’s simply not used.

The device works as expected.

In this code, the delay time is predefined but I want to make a more flexible device by passing the delay time as parameter through the symbol:

which will translate in the spice netlist command:

؆X1 «N01´b N02´b» «N03´b» «»  testDLL_X1 Tdelay=0.1u VHIGH=5 REF=2.5

when I execute, without editing the C++ code, I get this warning in the Output Window:

Warning: Ignoring unknown instance parameter "TDELAY" of device ؆X1.

A couple of questions:

  • The † symbol is autogenerated, did not edit this netlist outside Qspice: is it expected ?
  • How to use the Tdelay parameter in my C++ code (instead of the #define statement) ?

As far as I understand, the names of the device instance parameters are predefined and using any other name will get you this error. Also, you cannot even read these ‘normal’ instance parameters (it’s certainly not documented).

It would be great if Mike could add user instance parameters, readable from the module. Maybe passed as char* (or rather two: one for the name, one for the value) in the uData *data pointer, after the port values.

I am trying similary add schematic parameters to the block. Jope has right. The warning will disappear if you put a type before the user defined attribute (e.g int TDELAY=1). But I could not figure out how can access to these parameters from the C++ module.

You are right guys, I forgot the type!

I tried writing the type before the parameter and the block does not work anymore: simulation ends with no errors, but output signal is always 0V. Also the simulation time is much shorter, as if the dll was not executed at all.

I think, when adding a parameter (the correct way, that is with the type “in front”) this is passed to the dll and, if not handled properly in the c++ code, the ddl will “crash”.

The second question remains: how to use it in the C++ code ?

Egrana, you were right. You can use user-defined data.

And I think I’ve figured out how to read it: It is part of the ‘union uData *data’ that is passed to the DLL function.

Example: If you have two input ports, two output ports and two user data values, it works like this:
data[0]: first input port
data[1]: second input port
data[2]: first user data value
data[3]: second user data value
data[4]: first output port
data[5]: second output port

@Jope , thanks for the solution, it works!

However, the parameter should be always in between inputs and outputs within the data[] array. This seems to be the way the spice “variables” are passed to the dll.

So, in case of automatic code generation this is a no-brainer: parameters defined in the symbol will be added to the code and at the right place in the data[] array.

But if one has the epiphany to add parameters after writing the whole code, it is a little inconvenient since re-generating the c++ template will overwrite all the work done. One has to shift the position of the outputs and place the parameters in between inputs and outputs.

   bool  A   = data[0].b; // input
   bool  B   = data[1].b; // input
   bool &out = data[3].b; // output
   double TDELAY = data[2].d; // input parameter

Maybe @Engelhardt could change the data[] array generation so that the parameters could be added at the end of it? Not sure, if that’s possible.

So propably this is why your modul stopped working. We added the paramter after the automatic code generation, so the port indexes were shifted.

It feels a little dangerous. What will happen when you define a parameter before automatic code generation but the user deletes the parameter from the attributes? Second, the parameters in the netlist are named, but they are passed by order. If the user swaps two parameters’ order in the module’s netlist the modul will not work properly?

That’s all correct.

Some code development IDEs try to rewrite the code you have already written in situations like this, but there usually ends up situations where it doesn’t work.

When you want to edit the number of ports or their types or data type and/or add user defined parameters, keep a copy of the code, regenerate the template and add back what is still appropriate for the new module definition.

–Mike

I don’t see this as a problem. STM32CubeIDE does it just fine in my experience. They don’t try to rewrite my code but have clearly marked user code blocks where I can but my code. Like the // Implement module evaluation code here: you already have. Everything else can be overwritten. Copying code back and forth is error-prone and since the debugging capabilities are limited, not something I want to do often.

I would be fine even without code regeneration for late added parameters. I could simply remember that they need to be a new data[] element and manually write the line of code. However, if they could be “appended” at the end of data[], there will be no need to shift the outputs’ positions.

Something like this:

Addition to Egrana’s suggestion: maybe pass to the function the number of elements in the uData array too as extra parameter on the function (or it is simpler if the first element of uData array can contain the number of elements) so it can prevent of overindexing uData.

But what if I want to add an input or another output, maybe only for debugging? All manual solutions are not ideal.
(On a side note: keep in mind that adding parameters is probably just a shortcut for adding inputs with voltage sources, so it comes with a performance penalty compared to hard coded numbers)