Does a C block have knowledge of what step is being run?

I put that into the section indicated for implementation code. Here’s my full code for a counter I’m using:

// Automatically generated C++ file on Fri Nov 17 11:23:44 2023
//
// To build with Digital Mars C++ Compiler:
//
//    dmc -mn -WD counter.cpp kernel32.lib

#include <stdio.h>
#include <malloc.h>
#include <stdarg.h>
#include <time.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 display(const char *fmt, ...)
{ // for diagnostic print statements
   msleep(30);
   fflush(stdout);
   va_list args = { 0 };
   va_start(args, fmt);
   vprintf(fmt, args);
   va_end(args);
   fflush(stdout);
   msleep(30);
}

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 VCC

struct sCOUNTER
{
  // declare the structure here
  int  state;          // operation mode
  bool clk_prev_state; // used for edge detection
  int  count;          // holds current count value
};

extern "C" __declspec(dllexport) void counter(struct sCOUNTER **opaque, double t, union uData *data)
{
   double  CLK   = data[0].d; // input
   double  VCC   = data[1].d; // input
   int     COUNT = data[2].i; // input parameter
   double &OUT   = data[3].d; // output

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

// Implement module evaluation code here:
   // Trigger voltage for clock input:
   double Vt = 0.5*VCC;

   // IO signal variables
   bool b_CLK = false;
   bool b_OUT = false;

   // Convert voltage inputs to digital inputs for easier logic
   if( CLK >= Vt )
      b_CLK = true;

   // Set clock rising edge
   bool b_clk_re = false;
   if( !inst->clk_prev_state ){ // previously LOW
      if( b_CLK ) { // now HIGH
         b_clk_re = true; // rising edge detected!
      }
   }

   // Change states as necessary
   switch( inst->state ) {
      case 0: // Initialization state
         inst->count = 0;
         inst->state = 1;
         break;
      case 1: // Counting state
         if( b_clk_re ) {
            // If a rising edge is detected
            inst->count++; // add one to the counter
            if( inst->count == COUNT ) {
               // If the count hits the max value
               inst->state = 2; // Go to 'overflow' state
               inst->count = 0; // Reset the count
               b_OUT = true;
            }
            //display("RE detected! %d %d\n", inst->count, COUNT);
         }
         break;
      case 2: // Overflow state
         if( b_clk_re ) {
            inst->count++;   // add one to the count
            inst->state = 1; // return to counting state
         } else {
            b_OUT = true;
         }
         break;
      default:
         break;
   }

   // Set output state(s)
   if( b_OUT )
      OUT = VCC;
   else
      OUT = 0;

   // Set 'previous' state for next iteration
   inst->clk_prev_state = ( b_CLK );
}

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

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

      // Save output vector
      const double _OUT   = OUT  ;

      struct sCOUNTER tmp = *inst;
      counter(&(&tmp), t, data);
   // if(tmp != *inst) // 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 sCOUNTER *inst)
{
   free(inst);
}

@Emrys ,

Thank you for your suggestion. However, in the case of stepping, this will not work.

You recommendation uses the fact that when this C-block is first called, *opaque == 0. Then the instance memory is allocated and then zeroed out giving the inst->state parameter a 0 setting.

Your recommendation works for non-stepped sims because *opaque = 0 happens only once.

In stepped sims, *opaque = 0 happens at the beginning of each step executed. Therefore, I still cannot detect the first step of the sim. From what I can tell, there is no variable that is or can be made persistent across steps.

Len

Hey Len,
Thanks for letting me know!

I wonder if we could put together a full API somewhere for the QSpice code system. Would be nice to have a good reference for more complex issues like this.

@Emrys ,

I’m interested in contributing public IP. I think there are others who are contributing.

In general, I find a PSpice tool very useful but can be limited to basic building components (mostly because it started as an analog simulator). Most sophisticated models can be created using the basic building components but can be very complex to create, modify or reverse-engineer to find unpredicted issues.

C-blocks allow for easy and elaborate creation of extremely sophisticated models. It also allow for easy access to computer system resources where .subcir models don’t provide that easily or at all. I’ve created C-blocks to access the filesystem and perform string parsing.

Suggestion:
Propose a common version controlled repository (such as GitHub) that others can access with some rules for submission.

Len

1 Like

Len, you might want to start a new thread with appropriate topic so that others who aren’t following/interested this thread topic notice.

–robert

That’s a good idea and I’d be happy to set something like that up. I expect I’ll have to find some free time to learn quite a bit before I get it started, but when I do, I’ll post a new thread here. I’m definitely not a “software guy.”

To all,

Mike addressed this issue in Build Jan 1, 2024.
12/31/2023 .DLL global variables are now preserved from .step to .step

Thank you Mike.

Len

With the new build update, shall the global counter put in Destroy()? Or how this step counter should implement from your expertise in C++?

In my opinion, you have options to put the global_count in Destroy() or in if(!opaque){}. Both of them are only called once throughout the whole transient simulation.

Destroy() at the end of sim

if(!opaque){} at the t = 0;

1 Like

An direct way to let the .DLL to know the step number it to tell it with a parameter:
image

image

The problem with using global data is that it is shared between all instances that call that .DLL from the simulation. That’s why the template generator offers to declare a structure for you to store per-instance data.

–Mike

1 Like

Well, this isn’t exactly true. A generic DLL component should anticipate multiple schematic component instances. Try putting two instances of the C-Block on a schematic and both get called twice.

To solve that, you’ll need to implement reference counting in the instance structure and only increment the global step counter when allocating the first instance. And for that, you’ll need to allocate/delete the structure instances using new/delete (not malloc/free).

I’ll try to post some project code to demonstrate this later today.

–robert

OK, I’ve posted code for a reliable way for a C-Block to determine the current simulation step (even with multiple schematic instances) on the dev branch of my GitHub repo here.

Let me know if you have questions or problems.

–robert

1 Like

Mike is correct that a dll global variable persists across steps. He is also correct that the global variable occurs only once in the same component. Therefore, if you have multiple instances of the same component in the sim, this global var is shared across instances.

I believe I solved the latest issue in my particular situation.

Here are code frags for that ‘fix’:

bool started = false; // sim started var

/**************************************************************/
extern “C” __declspec(dllexport) void compname(struct sLOGGER **opaque, double t, union uData *data)
{
… uData assignments …

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

// Implement module evaluation code here:
if(t == 0)
{ // Make sure we’re at the beginning of the sim/step.
if(started == false)
{ // Has not started yet
… Code to initialize at the first step
}
}
else
{ // t != 0
started = true; // signal the sim has started. This prevents reinitialization
}
… More code …
}

This method allows all instances of the same component to go through the initialization phase at t=0.
Once t no longer = 0 then the started var is set to true.

This method appears to work in my testing.

Len

Hi, Len.

Depending upon what you’re doing, there could be an issue with the code. Unless something changed in recent releases, each component instance is called multiple times with t==0 before being called with t>0. I believe the above will execute “Code to initialize at the first step” something like six times for each instance.

If you want some initialization to occur exactly once for each instance at the beginning of the sim/step, put that code in the instance initialization (right after the bzero() call).

Of course, it all depends on your use case. Just something to be aware of.

–robert

Robert,

You are correct. Once a sim step is started, t=0 occurs 5 to 6 times

In the code frag I listed above, the fopen(filename,“w”) occurs every time t=0 and when started = false. Therefore the file gets opened in write mode multiple times. Redundant. True.

Let me try your suggestion of doing file initialization in the instance initialization.

Len

Update: It does appear to work. Potentially less fopen()s.

Len, if you’re comfortable with C++ code, you might look at the code I posted yesterday on the dev branch of my GitHub repo.

FWIW, I’m in the process of writing a tutorial that might also help but I don’t expect to complete it for a couple of days. However, it will also assume an understanding of C++ so it may or may not help depending on your skill set.

–robert

Robert,

I’ll be very glad to look at your code example. Although I may wait for your tutorial first.
I’ve coded in embedded C for ‘eons’ (30+ years in computer-relative terms) but have little direct experience in C++ (which supports the C-subset). Most embedded systems compilers haven’t advanced to C++.

I’ve gotten my C-based solution to work well but it’s always good to learn new things any possibly find better solutions.

Len

Len:

Yes, it’s finally dawning on me that most of the QSpice users aren’t C++ coders. Thanks for pointing this out.

I’ve rewritten the sample code for both C and C++ and plan to rework the tutorial to explain things using the C version before publishing it.

–robert

2 Likes

I notice that too. Most people that complain about Qspice seems to have allergy with programming.

Though admittedly, I found a few issues with some timing accuracy as well

I have a lot of control firmware design work in the past. Thus, programming in C is generally not an issue for me.

Hi, Len.

I’ve developed both C & C++ code to reliably handle multiple component instances, multiple simulation steps, and shared resources (such as a data logging file). The C code version is heavily commented and, I hope, is clear.

What the code doesn’t explain is exactly why QSpice requires this approach. I’ve been working on a tutorial-style PDF but, well, code is easy, coherent explanations are hard. Maybe I’ll get that finished tomorrow.

Anyway, see the C-Block Basics #2 code on the dev channel of my GitHub repo. Let me know what you think.

–robert

3 Likes