element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products & Partners
    Products & Partners
    • Arduino
    • Avnet & Tria Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • About Us
    About the element14 Community
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Japan
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      •  Vietnam
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
Embedded and Microcontrollers
  • Technologies
  • More
Embedded and Microcontrollers
Blog PID controller without floating point for ARM controllers using DSP C++ fixed point API
  • Blog
  • Forum
  • Documents
  • Quiz
  • Polls
  • Files
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Join Embedded and Microcontrollers to participate - click to join for free!
  • Share
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: Jan Cumps
  • Date Created: 21 Oct 2026 12:47 PM Date Created
  • Views 134 views
  • Likes 7 likes
  • Comments 4 comments
  • MSPM0
  • arm cortex
  • c++26
  • easyL1105
  • dsp
Related
Recommended

PID controller without floating point for ARM controllers using DSP C++ fixed point API

Jan Cumps
Jan Cumps
21 Oct 2026

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.

image

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.

image

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 namespace arm_cmsis_dsp;

using fp_t = Q15;
constexpr Q15 operator""_fp(long double val) {
        return operator""_q15(val);
}

fp_t feedback;
constinit fp_t target_command = 0.66_fp; // Example target command in fixed point per-unit (0.0 to 1.0)
constinit fp_t output_pid = 0.0_fp; // Initialize output_pid to 0.0 in fixed point per-unit

C++ "" operator: User-Defined Literals

fp_t target_command = 0.66_fp;

I type 0.66_fp to assign 0.66 to an fp_t variable. The _fp part is an "" operator. The effect is that the build will convert the 0.66 float to type fp_t. 

The conversion does not incure a runtime cost. The compiler does it when you build the firmware. You can test this by using constinit during initialisation. This will throw a compile error, if the right side of the initialisation requires runtime calculations:

constinit fp_t target_command = 0.66_fp; // Example target command in fixed point per-unit (0.0 to 1.0)
constinit fp_t output_pid = 0.0_fp; // Initialize output_pid to 0.0 in fixed point per-unit

Check my main file below for the definition of the operator.

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.

import pid;

using pid_controller_t = pid::pid<fp_t>;


pid_controller_t controller(0.1_fp, 0.01_fp, 0.05_fp); // Example PID gains for voltage control

C++ templates:

I use a template, to let the developer decide what data type the PID controller should use.

The PID class knows how to perform the PID algorithm. But it doesn't know yet what data type the developer wants to use. On a controller with FPU, they may want to use a float or long double. On an ARM Cortex without FPU, they would most likely want to avoid using floating point calc in a tight loop. Here, they can use a fixed floating point data type, like ARMs DSP++ fixed point Q15. 

The template will take care that at compile time, the correct math operations will be applied, based on the type you pass to the template.

If you pass it a double, the compiler will use floating point math:

using pid_controller_t = pid_fixedpoint::pid<float>;

If you pass it a DSP++ Q15 fixed point type, it will use that library's integer operations to do the math.

using pid_controller_t = pid_fixedpoint::pid<Q15>;

The compiler will also adapt the constructor, so that the data types of the P, I and D parameters are matching the data type. The same with the private data members inside the class.

So the value of this template is that I can write a generic PID algorithm, and that the firmware designer has the freedom (and duty) to select the best data type for their design. There are no runtime costs. This is all resolved at compile time.

This is the full class definition:

module;

#include <limits>
#include <concepts>
#include <type_traits>

export module pid;


// to avoid that PID saturates the output, it's neccessary to know the max allowed value
// different approach is needed for base types and classes / structs, 
// because for base types we can use std::numeric_limits<T>::max() 
// but for classes / structs we need to extract the internal type and 
// then use std::numeric_limits<InternalType>::max() to get the max value of the internal type.

// Primary template: Matches primitives (when std::is_class is false)
template <typename T, typename = void>
struct automated_traits {
    static constexpr T max() { 
        return std::numeric_limits<T>::max(); 
    }
};

// Specialization: Automatically matches ANY class or struct
template <typename T>
struct automated_traits<T, typename std::enable_if<std::is_class<T>::value>::type> {
    static constexpr T max() {
        return T(std::numeric_limits<typename T::value_type>::max());
    }
};

template <typename T>
concept SupportsSelfMultiplication = requires(T a, T b) {
    { a * b } -> std::same_as<T>;
};

export namespace pid {  

template<typename T> class pid {
   public:
    pid(T kp, T ki, T kd) : kp(kp), ki(ki), kd(kd) {
        // Provide a clear message to display if compilation fails
        static_assert(SupportsSelfMultiplication<T>,
            "Error: This class doesn't support template types that can't multiply themselves");        
    }

    // static constexpr  T out_max {std::numeric_limits<typename T::value_type>::max()};
    static constexpr  T out_max {automated_traits<T>::max()}; // Use dsp_traits to determine the maximum value for type T
    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

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;

using namespace arm_cmsis_dsp;

using fp_t = Q15;
constexpr Q15 operator""_fp(long double val) {
        return operator""_q15(val);
}

using pid_controller_t = pid::pid<fp_t>;


volatile bool perform = false;

volatile bool checkADC;
fp_t feedback;
constinit fp_t target_command = 0.66_fp; // Example target command in fixed point per-unit (0.0 to 1.0)
constinit fp_t output_pid = 0.0_fp; // Initialize output_pid to 0.0 in fixed point per-unit

pid_controller_t controller(0.1_fp, 0.01_fp, 0.05_fp); // 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

  • Sign in to reply

Top Comments

  • shabaz
    shabaz 1 day ago +1
    Hi Jan, This is interesting stuff. Very neat that the library can use any type! I was watching a youtube video the other day (I don't have the link) where they mentioned how even double precision causes…
Parents
  • Jan Cumps
    Jan Cumps 1 day ago

    The PID class requires that you use it with data types that can perform * + -  = > <.. 
    If you try to use it with a data type that doesn't support one of those operations, it will not compile. That's good. But it shows a cryptic compile error message.

    C++26 supports checks that validate if the type you pass to the template supports all necessary operations. And gives a clear error message at compilation. Without any runtime overhead.

    I used it here to validate if the passed type supports multiply

    #include <concepts>
    
    template <typename T>
    concept SupportsSelfMultiplication = requires(T a, T b) {
        { a * b } -> std::same_as<T>;
    };
    
    template<typename T> class pid {
       public:
        pid(T kp, T ki, T kd) : kp(kp), ki(ki), kd(kd) {
            // Provide a clear message to display if compilation fails
            static_assert(SupportsSelfMultiplication<T>,
                "Error: This class doesn't support template types that can't multiply themselves");        
        }
    
    // ...

    If you use it with a type that supports the * operator, all is good. Example, with a float, or with Q15.

    If you use a data type that doesn't support multiplication (likeQ<1, 14, true>), it will tell during compilation why it fails:

    image

    You can add checks for the other required operations, to give developers a clear reason of why they use an incompatible data type

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
Comment
  • Jan Cumps
    Jan Cumps 1 day ago

    The PID class requires that you use it with data types that can perform * + -  = > <.. 
    If you try to use it with a data type that doesn't support one of those operations, it will not compile. That's good. But it shows a cryptic compile error message.

    C++26 supports checks that validate if the type you pass to the template supports all necessary operations. And gives a clear error message at compilation. Without any runtime overhead.

    I used it here to validate if the passed type supports multiply

    #include <concepts>
    
    template <typename T>
    concept SupportsSelfMultiplication = requires(T a, T b) {
        { a * b } -> std::same_as<T>;
    };
    
    template<typename T> class pid {
       public:
        pid(T kp, T ki, T kd) : kp(kp), ki(ki), kd(kd) {
            // Provide a clear message to display if compilation fails
            static_assert(SupportsSelfMultiplication<T>,
                "Error: This class doesn't support template types that can't multiply themselves");        
        }
    
    // ...

    If you use it with a type that supports the * operator, all is good. Example, with a float, or with Q15.

    If you use a data type that doesn't support multiplication (likeQ<1, 14, true>), it will tell during compilation why it fails:

    image

    You can add checks for the other required operations, to give developers a clear reason of why they use an incompatible data type

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
Children
  • Jan Cumps
    Jan Cumps 1 day ago in reply to Jan Cumps

    Even though, in theory, you could use this class with raw integers or unsigned integers, don't do that. 
    You have to realise that they miss the precision needed by the PID algorithm math. The regulation will be bad.

    Use the ARM DSP++ fixed floating point library, or if you have an FPU, go ahead and use floats / doubles. In all circumstances, the DSP++ lib is expected to run cheaper than the other options. While offering the needed precision.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2026 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube