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 MSPM0
  • 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: 12 Oct 2026 7:50 AM Date Created
  • Views 240 views
  • Likes 8 likes
  • Comments 8 comments
  • MSPM0L1105
  • MSPM0
  • easyL1105
  • MSPM0L1306
  • pid
  • texas instruments
Related
Recommended

PID controller without floating point for MSPM0

Jan Cumps
Jan Cumps
12 Oct 2026
PID controller without floating point for MSPM0

PID controller implementation without using integer arithmetic. Uses TI IQMath lib to replace floating point instructions.

image

Some of the content is created with AI support. I've marked the parts where AI was used.
All narrative is written by me.

Math and PID

Traditionally, the PID algorithm uses floating point math. For an MSPM0 controller, those are expensive runtime calculations. And they have to be done in each execution of the PID control loop.

TI MSPM0 SDK comes with the 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.

The lib is used to:

  • declare floating point values to IQMath representation
  • multiply IQMath variables

It's not used to add values. You can just the C traditional + operation on IQMath variables.
I also didn't use the lib to convert ADC (the PID feedback signal) and PWM (the PWM output signal) to IQMath. Because of their nature, there was an optimised way (shift left / right) to convert those.

The lib allows you to select the number of bits reserved for the fractional part. I used the default Q(24) format, where 24 bits of an int32_t are used for fractional.

image

source: Google's Gemini AI

image

image: real time capture, regulating to desired output of 70% of 3.3V. PID loop executes every 2 ms.

PID algorithm

I created a C++ class that implements the PID algorithm, and can hold the controller state..

#include <ti/iqmath/include/IQmathLib.h>

constexpr  _iq out_max = _IQ(1.0);        // Maximum allowable output (e.g., _IQ(1.0) for 100% PWM)
constexpr  _iq out_min = _IQ(0.0);        // Minimum allowable output (e.g., _IQ(0.0) for 0% PWM)

class pid {
    public:
    pid(_iq kp, _iq ki, _iq kd) : kp(kp), ki(ki), kd(kd) {}
    
    void init(_iq kp, _iq ki, _iq kd, bool reset = true) {
        kp = kp;
        ki = ki;
        kd = kd;
        
        if (reset) {
            reset_state();
        }
    }
    
    _iq compute(_iq setpoint, _iq feedback) {
        _iq error;
        _iq p_term, i_term, d_term;
        _iq output;
        
        // 1. Calculate the current error (e = r - y)
        error = setpoint - feedback;
        
        // 2. Proportional Term: P = kp * error
        p_term = _IQmpy(kp, error);
        
        // 3. Integral Term with anti-windup: I = I_prev + (ki * error)
        integral = integral + _IQmpy(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 = _IQmpy(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:
    _iq kp;
    _iq ki;
    _iq kd;
    _iq integral = _IQ(0.0);
    _iq previous_error = _IQ(0.0);

    inline void reset_state() {
        integral = _IQ(0.0);
        previous_error = _IQ(0.0);
    }
};

I used Google Gemini AI to suggest algorithm transition from floating point to IQMath Q(24). I reviewed and adapted where needed, and turned it into a class.

image

Highlights of the class:

  • at initialisation, you set the PID Kp, Ki and Kd parameters. They are all in Q(24) format.
  • they can be tweaked at runtime with the init() method
  • the compute() method implements the algoritm using integer math only, no floating point.
  • runtime state is kept in two Q(24) data members: integral and previous_error.

Performance

With the clock at 32 MHz, and GCC 15.2.1, optimisation set to max, the PID takes 140 µs. That includes:

  • sample and convert value to IQMath
  • run PID algorithm
  • convert IQMath outcome to 16 bits unsigned and set PWM duty cycle

With the same toolchain, but optimisation off, for debugging, the loop takes 150 µs.

These values drive the minimum time for the hardware timer that drives the PID loop.
Other things to consider: there is no reason to loop faster than the output's PWM frequency, because setting duty cycle more than once per cycle has no effect.
With the settings of this project, where the PWM counter is 16 bits (65535), the maximum PWM is 488.2887006942855 Hz (when prescaler and divider are set to 1). So no use to set PID timer to less than 2.047 ms.

Google AI input:
Rule of thumb is to select a 10x to 20x lower PID loop frequency than the PWM.

Use Case

I reused my project  PID temperature controller for the EasyL1105 MSPM0 board - Pt. 1: framework . And replaced the PID algorithm with this class.
The project tries to set an output voltage to a given set level.. It uses PWM to generate that voltage. ADC is used to measure that voltage.
At runtime, the PID algorithm constantly validates if the voltage matches the set level. If not, ith adjusts the PWM duty cycle to drive the voltage either up or down.

image

PID controller object creation:

pid controller(_IQ(0.1), _IQ(0.01), _IQ(0.05)); // Example PID gains for voltage control

Set the desired output (value between 0 and 1):

_iq target_command = _IQ(0.75);

The control loop:

    while (1) {
        if (perform) {
            perform = false;

            perform_adc();
            
            perform_pid();

            perform_pwm();

        }
    }

perform_adc() retrieves the current output, and translates it to a Q(24):

void perform_adc() {
    DL_ADC12_startConversion(ADC12_0_INST);
    while (false == checkADC) {
        __WFE();
    }

    // Shift left by 12 bits (24 minus 12 equals 12)
    feedback = ((int32_t)DL_ADC12_getMemResult(ADC12_0_INST, DL_ADC12_MEM_IDX_0)) << 12; // Direct to 0.0 - 1.0 per-unit in Q24!
           
    checkADC = false;
    DL_ADC12_enableConversions(ADC12_0_INST);
}

You can spot that I use an optimised shift to translate the 12-bits ADC to a Q(24).
I used Google Gemini AI to suggest the most performant way to translate the 12 bits to Q(24)

performe_pid() calls the algorithm:

void perform_pid() {
    output_pid = controller.compute(target_command, feedback);
}

The PID takes the desired voltage, and the value the ADC reported. It then calculates the PMW duty cycle that 'll get us there.

perform_pwm() then alters the duty cycla, as suggested by the algorythm:

void perform_pwm() {
    if (output_pid == pid::out_max) { 
        // fix case: (_iq(1.0) >> 8) == 65536, and that turns the duty cycle off
        // Force a true 100% duty cycle by setting CC value to the LOAD value + 1
        DL_TimerG_setCaptureCompareValue(PWM_0_INST, DL_TimerG_getLoadValue(PWM_0_INST) + 1, DL_TIMER_CC_0_INDEX); 
    } else {
        // Convert Q24 per-unit (0.0 to 1.0 excluded) to raw timer counts (0 to 65535)
        DL_TimerG_setCaptureCompareValue(PWM_0_INST, ((uint32_t)output_pid) >> 8, DL_TIMER_CC_0_INDEX); // update ccr0 value
    }        
}

Again, a shift was used to translate the Q(24) to the desired duty cycle value for PWM register.
I used Google Gemini AI to suggest the most performant way to translate the Q(24) to DL_TIMER_CC_0_INDEX

The complete main file:

#include "ti_msp_dl_config.h"
#include <ti/iqmath/include/IQmathLib.h>

volatile bool perform = false;

volatile bool checkADC;
_iq feedback;
_iq target_command = _IQ(0.80); // Example target command in Q24 per-unit (0.0 to 1.0)
_iq output_pid = _IQ(0.0);

import pid;

// const _iq k_adc = _IQ(0.000244140625); // ADC conversion factor for 12-bit resolution (1/4096)

pid::pid controller(_IQ(0.1), _IQ(0.01), _IQ(0.05)); // Example PID gains for voltage control

void perform_adc() {
    DL_ADC12_startConversion(ADC12_0_INST);
    while (false == checkADC) {
        __WFE();
    }

    // Shift left by 12 bits (24 minus 12 equals 12)
    feedback = _iq(DL_ADC12_getMemResult(ADC12_0_INST, DL_ADC12_MEM_IDX_0)) << 12; // Direct to 0.0 - 1.0 per-unit in Q24!

    checkADC = false;
    DL_ADC12_enableConversions(ADC12_0_INST);
}

void perform_pwm() {
    if (output_pid == pid::out_max) { 
        // fix case: (_iq(1.0) >> 8) == 65536, and that turns the duty cycle off
        // Force a true 100% duty cycle by setting CC value to the LOAD value + 1
        DL_TimerG_setCaptureCompareValue(PWM_0_INST, DL_TimerG_getLoadValue(PWM_0_INST) + 1, DL_TIMER_CC_0_INDEX); 
    } else {
        // Convert Q24 per-unit (0.0 to 1.0 excluded) to raw timer counts (0 to 65535)
        DL_TimerG_setCaptureCompareValue(PWM_0_INST, output_pid >> 8, 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;
    }
}

}

Result

This "measure - assess - correct" loop is driven by a common timer, that makes it run every 200 ms.

image

You can influence speed and stability by tuning the 3 parameters. 

AI can support. You can ask it to suggest parameters to make it move faster to the set point, to react faster or slower on load changes, to improve stability, to avoid ringing or overshoot...
This is how Google Gemini AI explains the role of the parameters:
image

Convert ADC and PWM uint to IQMath format

In the perform_adc() function, you can spot that I used the bit shift operation to convert the unsigned integer ADC value to a Q(24) value.
The ADC returns a 12 bit value. You can safely turn that into a Q(24) by left-shifting it . It's typecast to an int32_t, because that's the underlying type of the Q(24) variable.

feedback = ((int32_t)DL_ADC12_getMemResult(ADC12_0_INST, DL_ADC12_MEM_IDX_0)) << 12;

Similar for the perform_pwm() function. The duty cycle compare register expects an uint32_t with a value between 0 and 65535. You can do that by right-shifting:

DL_TimerG_setCaptureCompareValue(PWM_0_INST, ((uint32_t)output_pid) >> 8, DL_TIMER_CC_0_INDEX); // update ccr0 value

Want to see if this works? Create a small program that runs these instructions. Look at the underlying representation with a debugger:

     // test conversion ADC unsigned 12 bit value to IQ24
     uint16_t adc_result = 4095; // max
     _iq iq_adc_result = ((int32_t)adc_result) << 12; // 1.0
     [[maybe_unused]] float float_adc_result = ((float)iq_adc_result) * 0.000000059604644775390625f; // for your convenience, show as float. Should be 1.0

     // test conversion IQ24 to 16 bit PWM duty cycle compare value
     _iq iq_duty_cycle = _IQ(1.0);
     [[maybe_unused]] uint32_t duty_cycle = ((uint32_t)iq_duty_cycle) >> 8; // should be 65536

 

Resources used:

  • ADC0, with input pin PA25. Its interrupt flags when conversion done
  • PWM timer TIMG0, with output pin PA5.
    I selected this timer / pin combination, because you can set its output drive to high.
    This helps when you want to apply some load to the DC output voltage (see last point in this list) and see how the PID algorithm corrects for it.
  • generic timer PWMG1, to make the control loop tick every 200 ms. It fires an Interrupt to signal the loop has to be executed.
  • GPIO pin PA27 to make a LED light up when the loop runs.
    The loop is so fast that you can't see it, but you can put an oscilloscope over it.
  • a simple RC low pass filter converts the PWM to a DC signal.

image

The attached project targets the MSPM0L1306 Launchpad. But you can adapt it to EasyL1105 with SysConfig.

references:

  • TI IQMath library. It's part of the MSPM0 SDK.
  • MSPM0 IQMath example.
  • PID controller on Wikipedia
  • EasyL1105 development board.
  • Expressif also uses that IQMath library.
  • comparable example that uses ARM's vendor-independent DPS++ fixed point math

Thank you for reading.

VS Code project: mspm0_pid_20261021.zip. In this project, both PWM and PID run at 488 Hz. Both can be adapted by modifying PWM_0 and TIMER_0 in SysConfig.
(check this post for the expected VS Code workspace settings: MSPM0 project with VS Code, CMake and GCC - part 6: empty "template" project 
)

part of the EasyL1105 series. all posts

  • Sign in to reply

Top Comments

  • Jan Cumps
    Jan Cumps 18 days ago +1
    This is a real-time render of the startup sequence of the controller in action. requested output: 0.7. (That's 3.3V * 0.7 = 2.31V) PWM frequency: 488 Hz PID loop executes every 2 ms P: 0.1 I: 0…
  • Jan Cumps
    Jan Cumps 15 days ago

    in this design, a passive low pass filter turns the PWM to a DC voltage:

    image

    Originally, I used TIMER TIMG1 for PWM. But it can only access low drive output pins.
    When you want to load the output (to experiment with the PID correction), those PWM pins are too weak, and don't drive to 3.3 V.

    TIMG0 can drive PA5. And that pin has the hardware for high drive. So I changed the design to use the TIMG0 / PA5 combination.
    In the original code, timer TIMG0 was used to control the PID control loop. That timer doesn't need to drive any output, so I switched it to TIMG1.

    All changes can be done in SysConfig. Code doesn't need to change. You 'll now find the high drive PWM on PA5 (LaunchPad headers pin 15).

    PWM:

    image

    PID Timer

    image

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • Jan Cumps
    Jan Cumps 16 days ago

    I added a new version of the project, with an updated PWM duty cycle set  function.

    There was bug in the original code, where if the output was set to max (1.0), the Duty Cycle counter resulted in value 56536. And that's one too high.
    It results in the PID thinking it's pushing to the max, but the PWM outputting nothing (0% duty cycle).

    The code below checks for that condition, and sets the the PWM to full on (3.3V)in that case (100% duty cycle).

    void perform_pwm() {
        if (output_pid == pid::out_max) { 
            // fix case: (_iq(1.0) >> 8) == 65536, and that turns the duty cycle off
            // Force a true 100% duty cycle by setting CC value to the LOAD value + 1
            DL_TimerG_setCaptureCompareValue(PWM_0_INST, DL_TimerG_getLoadValue(PWM_0_INST) + 1, DL_TIMER_CC_0_INDEX); 
        } else {
            // Convert Q24 per-unit (0.0 to 1.0 excluded) to raw timer counts (0 to 65535)
            DL_TimerG_setCaptureCompareValue(PWM_0_INST, output_pid >> 8, DL_TIMER_CC_0_INDEX); // update ccr0 value  
        }        
    }

    The blog code example is updated too.

    I first tried if I can just increment the timer period count and keep allowing the PWM code to go to that value. But that's just beyond the timer(s range. 65535 is the maximum.

    image

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • Jan Cumps
    Jan Cumps 17 days ago in reply to Jan Cumps

    ... and with optimisation set to -O3, 140 µs.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • Jan Cumps
    Jan Cumps 18 days ago

    This is a real-time render of the startup sequence of the controller in action.

    requested output: 0.7. (That's 3.3V * 0.7 = 2.31V)

    PWM frequency: 488 Hz

    PID loop executes every 2 ms

    P: 0.1
    I: 0.01
    D: 0.05

    image

    • Cancel
    • Vote Up +1 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • Jan Cumps
    Jan Cumps 18 days ago in reply to Jan Cumps

    note to @self: a single loop, from ADC sample to PID calculations and including output PWM set, takes 150 µs

    • 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