warning: C++ heavy post
|
A few days ago, I created a PID controller implementation, for a TI MSPM0 device. Without using floating point arithmetic. It used the TI IQMath lib to perform PID algorithm. All calculations are done using ARM integer instructions. It works good, but it's a TI specific implementation. ARM has released a vendor-independent C++ fixed floating point lib, as part of its CMSIS DSP C++ (DSP++) extensions. It should work for the majority of controllers that are supported by ARM CMSIS-DSP. In this post, I adapt my PID class, so that it works with the DSP++ types.
|
None of the content is created with AI support, but some code is developed with Google AI support.
All narrative is written by me.
Math and PID - what happened before
Traditionally, the PID algorithm uses floating point math.
For smaller ARM cortex controllers, those are expensive runtime calculations. And they have to be done in each execution of the PID control loop
I created a TI MSPM0 PID controller class that uses the SDK IQMath library. A library that lets you represent floating point numbers in an integer variable. And lets you calculate with them.
I implemented the PID algorithm using IQMath variables and algorithms.
IQMath is a TI specific implementation - although it's published under an open source license and ESP has made its own port. Probably the best choice for the TI controllers that it supports, like the MSPM0.
For this post, it's relevant to know that the IQMath library defines a type _iq, that represents a number with fixed point decimal. It's implemented using an int32_t base type. One bit is the sign. A fixed set of the bits are the integer value, the remainder are the fraction. The library uses integer operations only, to perform math with these variables. On a controller with no native FPU, this is way more performant than doing floating point using C software libraries. With acceptable precision.

ARM CMSIS DSP++ fixed point extension
The DSP++ fixed point API gives similar functionality. And it's templated. You can create your own custom precision types.
example:
using fp_t = arm_cmsis_dsp::Q<7, 24, true>;In my project, I used one of the predefined types: Q15
using fp_t = arm_cmsis_dsp::Q15;One of the functionalities of C++ is that you can override operators.
In TI's traditional-C IQMath API, you use C functions, E.g.: multiply with _IQmpy().
p_term = _IQmpy(kp, error);DSP++ uses operator overrides, so you can use * for multiplication. The DSP++ overridden function implementation handles the multiplication using integer math.
p_term = kp * error;This is the begin of the code, where I include the DSP++ lib, and declare variables:
#include "fixed_point.hpp"
using fp_t = arm_cmsis_dsp::Q15;
fp_t feedback;
fp_t target_command = fp_t::f(0.50f); // Example target command (0.0 to 1.0)
fp_t output_pid {0}; // Initialize output_pid to 0.0
Getting the DSP++ library
The CMake file does that. ARM makes the sources available on GitHub. I use CMake's FetchContent to get the sources, and tell CMake where the required include file is.
include(FetchContent)
# CMSIS DSP fixed point c++ library dsppp
FetchContent_Declare(dsp_fixed_point
GIT_REPOSITORY "https://github.com/ARM-software/CMSIS-DSP.git"
GIT_TAG HEAD
GIT_SHALLOW OFF # needed to find the commit HEAD
# prevent makefile execution (if any)
SOURCE_SUBDIR =
)
FetchContent_MakeAvailable(dsp_fixed_point)
# ...
include_directories( # this project uses CMSIS DSP fixed point library, so include the path to the CMSIS DSP headers
${dsp_fixed_point_SOURCE_DIR}/dsppp/Include/dsppp
)
data type independent PID class
Little disclosure: this C++ PID implementation isn't depending on DSP++ fixed point. It works for any data type that can calculate = , *, +, -, < and >. And that supports getting its max value with std::numeric_limits<typename T::value_type>::max(). So it 'll also work with a float or double. Useful on controllers that have an FPU, like Raspberry Pico.
I created a template class for the PID controller. You pass it the number type that you want to use in your firmware at compile time. It will store all variables in that format, and will use the appropriate implementations for all math operations. Here is a summary of its signature:
template<typename T> class pid {
public:
pid(T kp, T ki, T kd);
T compute(T setpoint, T feedback);
};
In your code, this is how you create an object (called controller) of that class. fp_t was that Q15 datatype that we decided to use earlier.
using pid_controller_t = pid<fp_t>; pid_controller_t controller(fp_t::f(0.1f), fp_t::f(0.01f), fp_t::f(0.05f)); // Example PID gains for voltage control
This is the full class definition:
module;
#include <limits>
export module pid_fixedpoint;
export namespace pid_fixedpoint {
template<typename T> class pid {
public:
pid(T kp, T ki, T kd) : kp(kp), ki(ki), kd(kd) {}
static constexpr T out_max {std::numeric_limits<typename T::value_type>::max()};
static constexpr T out_min {0}; // Minimum allowable output
void init(T kp, T ki, T kd, bool reset = true) {
kp = kp;
ki = ki;
kd = kd;
if (reset) {
reset_state();
}
}
T compute(T setpoint, T feedback) {
T error;
T p_term, i_term, d_term;
T output;
// 1. Calculate the current error (e = r - y)
error = setpoint - feedback;
// 2. Proportional Term: P = kp * error
p_term = kp * error;
// 3. Integral Term with anti-windup: I = I_prev + (ki * error)
integral = integral + ki * error;
// Anti-windup: Clamp the integral accumulator to prevent runway saturation
if (integral > out_max) {
integral = out_max;
} else if (integral < out_min) {
integral = out_min;
}
i_term = integral;
// 4. Derivative Term: D = kd * (error - prev_error)
d_term = kd * (error - previous_error);
// 5. Total Output: Control = P + I + D
output = p_term + i_term + d_term;
// 6. Save current error for the next execution cycle's derivative calculation
previous_error = error;
// 7. Final Output Saturation: Clamp the overall control signal
if (output > out_max) {
output = out_max;
} else if (output < out_min) {
output = out_min;
}
return output;
}
private:
T kp;
T ki;
T kd;
T integral {0}; // Initialize integral term to zero
T previous_error {0}; // Initialize previous error to zero
inline void reset_state() {
integral.v = 0;
previous_error.v = 0;
}
};
} // namespace pid_fixedpoint
|
C++ disclaimer: This class is as efficient as a classic C API style design. Nothing in this OO code uses any clock tick, flash space or RAM. Not a bit. Not a tick. |
main: Use the PID class with the DSP++ fixed point format:
Here, everything comes together:
- the floating point format is defined. I use DSP++ Q15 and its integer math support.
- a PID controller object is created, that knows it is working with Q15 format.
- PID loop is executed at regular times. Look at its code to see how UINT ADC and PWM values are converted to DSP++ Q15.
#include "ti_msp_dl_config.h"
#include "fixed_point.hpp"
import pid_fixedpoint;
using fp_t = arm_cmsis_dsp::Q15;
using pid_controller_t = pid_fixedpoint::pid<fp_t>;
volatile bool perform = false;
volatile bool checkADC;
fp_t feedback;
fp_t target_command = fp_t::f(0.50f); // Example target command in Q15 per-unit (0.0 to 1.0)
fp_t output_pid {0}; // Initialize output_pid to 0.0 in Q15 per-unit
import pid_fixedpoint;
pid_controller_t controller(fp_t::f(0.1f), fp_t::f(0.01f), fp_t::f(0.05f)); // Example PID gains for voltage control
void perform_adc() {
DL_ADC12_startConversion(ADC12_0_INST);
while (false == checkADC) {
__WFE();
}
// Shift left by 3 bits
feedback.v = static_cast<int16_t>((DL_ADC12_getMemResult(ADC12_0_INST, DL_ADC12_MEM_IDX_0)) << 3); // Direct to 0.0 - 1.0 per-unit in Q15!
checkADC = false;
DL_ADC12_enableConversions(ADC12_0_INST);
}
void perform_pwm() {
// Convert Q15 per-unit (0.0 to 1.0 excluded) to raw timer counts (0 to 65535)
DL_TimerG_setCaptureCompareValue(PWM_0_INST, (static_cast<uint32_t>(output_pid.v) << 1), DL_TIMER_CC_0_INDEX); // update ccr0 value
}
void perform_pid() {
output_pid = controller.compute(target_command, feedback);
}
int main() {
SYSCFG_DL_init();
/* timer 5 interrupt ticks per second */
/* Enable Timer0 NVIC */
NVIC_EnableIRQ(TIMER_0_INST_INT_IRQN);
NVIC_EnableIRQ(ADC12_0_INST_INT_IRQN);
checkADC = false;
/* Start PWM */
DL_TimerG_startCounter(PWM_0_INST);
/* Start Timer counting */
DL_TimerG_startCounter(TIMER_0_INST);
while (1) {
if (perform) {
perform = false;
DL_GPIO_setPins(GPIO_GRP_LEDS_PORT,
GPIO_GRP_LEDS_PIN_LED_GREEN_PIN);
perform_adc();
perform_pid();
perform_pwm();
DL_GPIO_clearPins(GPIO_GRP_LEDS_PORT,
GPIO_GRP_LEDS_PIN_LED_GREEN_PIN);
}
}
}
extern "C" {
void TIMER_0_INST_IRQHandler(void) {
switch (DL_TimerG_getPendingInterrupt(TIMER_0_INST)) {
case DL_TIMER_IIDX_ZERO:
perform = true;
break;
default:
break;
}
}
void ADC12_0_INST_IRQHandler(void) {
switch (DL_ADC12_getPendingInterrupt(ADC12_0_INST)) {
case DL_ADC12_IIDX_MEM0_RESULT_LOADED:
checkADC = true;
break;
default:
break;
}
}
}
references:
The code is available on GitHub.
ARM CMSIS DSP C++ extension (DSP++) fixed point datatypes documentation
source on GitHub
All DSP++ extensions documentation
PID controller without floating point for MSPM0 , using TI IQMath library (my original vendor specific PID design)
Thank you for reading.
VS Code project: mspm0_pid_20261021_1.zip.
part of the EasyL1105 series. all posts

Top Comments