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)

Multi-rate control

This section presents the multitasking options available for multi-rate control. Multitasking allows portions of the control algorithm to execute at a slower rate without violating real-time constraints on the main interrupt task. Two methods are available:

  • Background loop: Best suited for slower, non-critical tasks, such as thermal monitoring, or background communication.
  • Software interrupts: Ideal for periodic tasks running at a lower rate than the main control loop. By leveraging preemption, higher-priority sub-rate tasks can interrupt lower-priority ones, enabling the system to support multiple task rates concurrently.

The general principles of multi-rate control on imperix controllers are presented in PN265.

Background tasks

The CPP SDK provides a background callback routine, typically implemented as UserBackground(), which executes during the CPU’s idle time. A typical use case for this approach is the implementation of background CAN communication, which involves processing incoming and outgoing CAN frames using the Can_Receive() and Can_Transmit() functions (documented in the CAN in and CAN out pages, respectively).

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

// User background callback routine
tUserSafe UserBackground()
{
  tUserCanFrame canFrame;

  if (Can_Receive(canFrame)) {

    // Process the received CAN frame

  }

  return SAFE;
}


tUserSafe UserInit(void)
{
  // ...

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

  // ...
}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