Efficient programming with the CPP SDK

This note provides practical guidelines and code snippets for programming imperix controllers with the CPP SDK. It follows up on the getting started guide and provides specific details and guidance about how to efficiently implement commonly-used functions, such as control strategies, state machines, background tasks, communication interfaces, etc. To this end, the article uses a three-phase PV inverter as an example, which advantageously regroups and illustrates all of these concepts.

When developing control software for imperix controllers, users should typically start by referring to the software documentation and/or the header files. However, to quickly bridge the gap between the template and a functioning system, it is also possible to draw inspiration from existing examples. With this mindset, this page dissects the user.cpp file from the AN006. The corresponding C++ project is provided below.

Control algorithms

To facilitate the development of control algorithms, the CPP SDK includes a dedicated API folder with pre-validated functions specific to power electronics, such as PI controllers, PLLs, and coordinate transformations. Developers are encouraged to use these standard functions, which they can also modify as needed.

The two next paragraphs showcase how to use a few of these functions.

The provided PI controller (pseudo-object) should first be declared as a global variable, in this case Ipv_reg:

float Vpv;                      // Solar panel voltage measurement
float Ipv = 0;                  // Solar panel current measurement
float Ipv_ref = 0;              // Solar panel current reference

//Global variables in a namespace will not show up in Cockpit
namespace{
    PIDController Ipv_reg;  // Controller for the PV current control
    float Eb;               // Boost switching voltage
};

// ...Code language: C++ (cpp)

It must then be initialized within the UserInit() routine using the appropriate parameters:

tUserSafe UserInit(void)
{
    // ...

    ConfigPIDController(&Ipv_reg, Kp_Ipv, Ki_Ipv, 0, 800, -800, SAMPLING_PERIOD, 10);

    // ...
}Code language: C++ (cpp)

Finally, the corresponding execution method can be executed within the main interrupt routine:

tUserSafe UserInterrupt(void)
{
    // ...

    // Execute the current controllers on the MPPT strings:
    Eb = Vpv - RunPIController(&Ipv_reg, Ipv_ref - Ipv);

    // ...
}Code language: C++ (cpp)

It is worth mentioning that the implemented PI controller, as shown above, already includes an anti-windup strategy, and its integrator is automatically reset when the controller is not operating, using the GetCoreState() method that returns the operating state of the controller.

Similar to the PI controller, the DQ PLL must first be declared as a global pseudo-object alongside the necessary state variables and voltage vectors:

DQPLLParameters DQPLL;         // DQ PLL for grid synchronization
float Theta;                   // Phase angle of the grid voltage
float w_grid;                  // Grid angular frequency ($\omega$)
float Kp_pll, Ki_pll;          // PLL PI gains

TimeDomain Vg_abc;             // Three-phase grid voltage measurements
SpaceVector Vg_dq0;            // Voltages in the dq0 reference frame								

// ...Code language: C++ (cpp)

The PLL must then be initialized within the UserInit() routine using the desired proportional/integral gains, the nominal grid frequency, and the sampling period:

tUserSafe UserInit(void){
    // ...

    // Initialize the DQ PLL
    ConfigDQPLL(&DQPLL, Kp_pll, Ki_pll, OMEGA, SAMPLING_PERIOD);

    // ...
}Code language: C++ (cpp)

Finally, the coordinate transformation and the PLL can be executed within the main interrupt. The three-phase measurements are directly transformed into the synchronous reference frame (dq0) and the PLL extracts the grid angle:

tUserSafe UserInterrupt(void){
// ...

    // 1. Apply the direct transformation (abc to dq0) using the previous angle
    abc2DQ0(&Vg_dq0, &Vg_abc, Theta);

    // 2. Execute the PLL to track the grid angle
    Theta = RunDQPLL(&DQPLL, &Vg_dq0);
    w_grid = (&DQPLL)->omega;

// ...
}Code language: C++ (cpp)

State machines

Power converter control often requires managing different operating states, such as standby, precharging, operating, discharging, fault, etc. State machines are powerful tools that allow managing transitions between these states safely. They can be implemented in many ways. The following snippet serves as a practical example, detailing the different operating states of the PV inverter.

tStateOperation State_operation = OP_INIT; // Current state of the operational state machine
tStateOperation Next_state_operation = OP_STANDBY; // Next state of the operational state machine
unsigned int	state_operation_uint; // Current state of the operation state machine for monitoring in Cockpit

void User_RunOperationFSM()
{
	switch (Next_state_operation){
	// Init
	case OP_INIT:
		State_operation = OP_INIT;
		Next_state_operation = OP_STANDBY;
		break;
		
        // The converter is in standby and is waiting to be turned on, everything is disabled
	case OP_STANDBY:
		if (State_operation != Next_state_operation)
					Log_SendMsg(5, NULL, 0);
		State_operation = Next_state_operation;

		activate_boost = 0;
		activate_inverter = 0;

		if (activate == 1 && core_state > 0)
		{
			Next_state_operation = OP_WAITING_ON_PRECHARGE;
		}

		break;

	// The converter is started and is waiting for the precharge procedure to complete
	case OP_WAITING_ON_PRECHARGE:
		if (State_operation != Next_state_operation)
		{
			Log_SendMsg(6, NULL, 0);
			operation_cnt = 0;
		}
		else
		{
			operation_cnt++;
		}
		State_operation = Next_state_operation;

		activate_boost = 0;
		activate_inverter = 0;

		if (core_state > 0 && Precharge_ready && operation_cnt > 1*SW_FREQ)
		{
			Next_state_operation = OP_READY_TO_OPERATE;
		}
		else if (activate == 0)
		{
			Next_state_operation = OP_STANDBY;
		}
		else if (Precharge_fault || core_state == 0 || operation_cnt > 30*SW_FREQ)
		{
			Next_state_operation = OP_FAULT;
		}

		break;

	// The converter is fully turned on, the PV relay is closed, the boost converter, the inverter and the PWM signals are enabled
	case OP_READY_TO_OPERATE:
		if (State_operation != Next_state_operation)
			Log_SendMsg(7, NULL, 0);
		State_operation = Next_state_operation;

		activate_boost = 1;
		activate_inverter = 1;

		if (activate == 0)
		{
			Next_state_operation = OP_STANDBY;
		}
		else if (Precharge_fault || core_state == 0)
		{
			Next_state_operation = OP_FAULT;
		}

		break;

	// A precharge or core fault was detected
	case OP_FAULT:
		if (State_operation != Next_state_operation)
			Log_SendMsg(8, NULL, 0);
		State_operation = Next_state_operation;

		activate_boost = 0;
		activate_inverter = 0;

		if (Precharge_fault == 0 && core_state > 0)
		{
			Next_state_operation = OP_STANDBY;
		}

		break;
	}

	state_operation_uint = (unsigned int) State_operation;
}
Code language: C++ (cpp)

Programmatic enabling/disabling of PWM outputs

Following the implementation of a state machine, developers may seek to automate the enabling/disabling of the PWM outputs. This relates to the different operating states of the so-called core state machine, which is further described in PN261.

While this action is usually performed manually via the dedicated Cockpit button (see the related documentation), it is also possible to do it programmatically using the CoreStart() and CoreStop() functions. When combined with state machine logic, these functions enable fully automated operation.

if(some_criteria) CoreStart();
else CoreStop();Code language: C++ (cpp)

System logging and diagnostics

User log messages are useful for tracking state machine transitions or reporting converter faults. These messages can be elaborated and displayed in Cockpit using the Log_AddMsg and Log_SendMsg functions. However, since simply placing Log_SendMsg within the main interrupt may continuously spam the Cockpit log messages, developers must implement trigger conditions instead. The following code demonstrates a possible approach.

tUserSafe UserInit(void)
{
    //...
    // Configure a warning message with a unique id of 0
    Log_AddMsg(0, 20, "Operating limits exceeded (V=%.3fV / I=%.3fA)");
    //...
    return SAFE;
}

tUserSafe UserInterrupt(void)
{
    //...

    static bool warning_message_sent = false;
    if(V_meas > 850 || I_meas > 40){
        float log_values[2];
        log_values[0] = V_meas;
        log_values[1] = I_meas;
        // Display the message in Cockpit
        if(!warning_message_sent) {
            Log_SendMsg(0, log_values, 2);
            warning_message_sent = true;
        }
    } else {
        warning_message_sent= false;
    }

    //...

    return SAFE;
}Code language: C++ (cpp)

Background tasks

Tasks such as MPPT, thermal monitoring, or background communication may not require execution at the deterministic, high-frequency rate of the main control interrupt. Instead, to manage non-critical tasks, the CPP SDK provides a background callback routine, typically implemented as UserBackground(), which executes during the CPU’s idle time. In this PV-related example, the background routine is used to implement an MPPT algorithm executed at 200Hz.

The example below shows how developers can implement sub-rate tasks by using a software timer inside the main interrupt to periodically raise a flag to trigger a task in the background.

#define SUB_TASK_PERIOD 0.005
float SubTaskTimer = 0.0;
bool SubTaskFlag = false;

// User background callback routine
tUserSafe UserBackground()
{
  if(SubTaskFlag){

    // Sub rate task to execute

    SubTaskFlag = false;
  }
  return SAFE;
}


tUserSafe UserInit(void)
{
  Clock_SetFrequency(CLOCK_0, SW_FREQ);
  ConfigureMainInterrupt(UserInterrupt, CLOCK_0, 0.5);

  // Register the routine to the background loop
  RegisterBackgroundCallback(UserBackground);

  // ...
}


tUserSafe UserInterrupt(void)
{
  // ...

  // Increment timer and trigger flag for background loop every SUB_TASK_PERIOD
  SubTaskTimer += 1/SW_FREQ;
  if(SubTaskTimer >= SUB_TASK_PERIOD ){
    SubTaskTimer = SubTaskTimer - SUB_TASK_PERIOD ;
    SubTaskFlag  = true;
  }

  // ...
}Code language: C++ (cpp)

Software interrupts

Starting with SDK 2026.2, the CPP SDK introduced native support for software interrupts, providing a more robust alternative to using the background callback routine for sub rate tasks execution.

For each task that must be executed at a lower rate, the user can assign a callback routine to a software interrupt ID (0 through 7) using ConfigureSoftwareInterrupt(). Then the software interrupt callback is immediately executed when TriggerSoftwareInterrupt() is called. This approach offers several advantages:

  • The software interrupts support preemption. Higher-priority tasks can preempt slower, lower-priority ones. (Lower ID have a higher priority. The main interrupt has a higher priority than the soft interrupts)
  • The software interrupt mechanism detects overruns. An error is raised if a software interrupts is triggered again before the task completed.

In the example below, similar to the UserBackground() example, the sub rate task is managed using a software timer inside the main control interrupt. However, instead of setting a global flag that is continuously polled during the CPU’s idle time, the software interrupt is dispatched explicitly by calling TriggerSoftwareInterrupt(). Once triggered, the assigned callback is executed right after the main interrupt finished its execution.

#define SUB_TASK_PERIOD 0.005
float SubTaskTimer = 0.0;

void UserSoftwareInterrupt() {
  // Task to execute
}

tUserSafe UserInit(void){
  Clock_SetFrequency(CLOCK_0, SW_FREQ);
  ConfigureMainInterrupt(UserInterrupt, CLOCK_0, 0.5);

  // Register the routine to software interrupt ID 0
  ConfigureSoftwareInterrupt(0, UserSoftwareInterrupt);

  // ...
}

tUserSafe UserInterrupt(void)
{
  // ...

  // Increment timer and trigger flag for background loop every SUB_TASK_PERIOD
  SubTaskTimer += 1/SW_FREQ;
  if(SubTaskTimer >= SUB_TASK_PERIOD ){
    SubTaskTimer = SubTaskTimer - SUB_TASK_PERIOD;
    TriggerSoftwareInterrupt(0);
  }

  // ...
}Code language: C++ (cpp)

User faults

Imperix controllers implement hardware-level protections that respond to three different fault types:

  • Hardware faults, typically occurring when an over.value is detected (e.g. over-current or over-voltage).
  • Software faults, typically indicating a loss of real time or a similar exception.
  • User faults, voluntarily thrown by the user from the user code.

The third type – user faults – is discussed below. The others are further documented in PN263.

To declare user fault, SetUserFault(const char* user_txt) may be called at any time. When triggered, this immediately disables all PWM outputs and displays the corresponding message in the Cockpit logs.

    if(V_meas > V_max){
        SetUserFault("Maximum voltage exceeded");
    }Code language: C++ (cpp)

As an alternative, it is also possible to trigger a user fault by returning the state UNSAFE state at the end of the user interrupt. Returning SAFE implies that no errors happened during execution. Note that returning UNSAFE will generate a generic fault. It is then recommended to use the SetUserFault(const char* user_txt) method to display a informative fault message.

Further readings