PID implementation in C and STM32 PID Motor Control Example

PID implementation in C and STM32 PID Motor Control Example


7 minute read

PID implementation in C

Proportional-Integral-Derivative (PID) control is one of embedded systems' most widely used control algorithms, particularly for motor control applications. Whether maintaining a set speed, achieving precise positioning, or stabilizing a balancing robot, PID offers a robust and flexible approach to system regulation. This article will explore the fundamentals of PID control and show PID implementation in C. In addition, we will see how PID can be used in STM32 microcontrollers for motor control. We’ll break down the impact of each gain—Proportional (P), Integral (I), and Derivative (D)—on system performance and discuss how to tune them effectively. You'll clearly understand how to apply PID control to motor systems by the end, ensuring smooth and accurate operation.

Open Loop vs. Closed Loop Control

In control systems, there are two fundamental approaches to regulating a process: open-loop control and closed-loop control. The choice between these methods depends on the application, accuracy requirements, and system dynamics.

An open-loop control system operates without feedback. It simply executes predefined commands without measuring the actual output. Since it does not correct deviations, the system's accuracy depends entirely on calibration and external conditions.

  • No feedback mechanism
  • Simple and cost-effective
  • Fast response but less accurate
  • Cannot compensate for disturbances or variations in the system

A closed-loop control system continuously monitors the output and adjusts its input based on feedback. This allows the system to compensate for disturbances and maintain accurate control over time.

  • Uses feedback to adjust performance
  • More accurate but may introduce complexity
  • Can automatically correct errors
  • Handles disturbances effectively

A DC motor with a PID controller is a classic example of a closed-loop system. A rotary encoder measures the actual speed, and the controller adjusts the input voltage accordingly to maintain the desired speed, even if external factors (like load changes) affect the motor. 

Closed Loop Control

Since PID control relies on feedback to adjust the system's response continuously, it is a closed-loop control method. The controller measures the difference between the desired and actual output (error) and dynamically adjusts the system's input using proportional, integral, and derivative actions. This allows the system to maintain stability, accuracy, and adaptability to disturbances—key advantages of closed-loop control.

PID implementation in C: Theory

The equation is the mathematical presentation of the PID Controller:

PID Controller equation

  • u(t) - output of the controller
  • e(t) - error, in other words, the difference between the desired and actual output
  • Kp, Ki, and Kd are proportional, integral, and Differential gains, respectively.

Our next step is to qualitatively assess each gain and intuitively understand how the variations affect the system's performance. We usually analyze the step response for that purpose when we abruptly set a new desired value.

Step reponse terminologies

  • Proportional (P) Gain: Reacts to the current error between the desired and actual values. Higher P gains mean the system will react aggressively to the error and try to minimize it as soon as possible. However, the system might overreact, leading to system oscillations. In general, increasing p-gain decreases rise time and the steady-state error
  • Integral (I) Gain: Although the P-gain effectively decreases the steady-state error, P-gain alone cannot eliminate it entirely. Hence, we use I-term, accumulating past errors to eliminate steady-state errors. However, excessive I gain can cause overshoot and instability due to integral windup.
  • Derivative (D) Gain: D-gain minimizes the rate of change of the error signal. It helps reduce overshoot and improve stability. However, the D-gain's major drawback is amplifying the system's high-frequency noise. For instance, if we take the derivative of 1000 Hz sine signal:

sin(2pi*1000t)' = 2000pi*cos(2pi*1000t)

As you can see, the amplitude will be multiplied by 2000pi! So, the tiny high-frequency noise can drive your system crazy because of the D-gain. Therefore, it is recommended that its value be kept low. 

We can fine-tune system performance by adjusting these gains appropriately to achieve a fast, stable, and accurate response. 

PID implementation in C Header and Source File

Finally, let's discuss how to implement the PID controller in C. Look at the code snippet below, which represents a pseudo-implementation of the PID Controller:

PID implementation in C

In this pseudo code, we implement the discrete version of the PID equation. Instead of integrating, we have a variable that tracks the error sum. We compute the difference between the current and past error for a derivative. In addition, we must consider how frequently we apply the PID. Hence, we have Ts, which holds the period at which we use the PID.

Finally, let me present you the source and header file of the PID Controller in C. You can find these files by following the links below:

We have a struct that contains all the essential elements of the PID Controller: gains, a variable for the integral part, and other elements. In addition, it is vital to define the maximum output of the PID Controller to avoid saturation and overdriving your system. 

#ifndef INC_PID_CONTROL_H_
#define INC_PID_CONTROL_H_ 

#include "main.h" 
typedef struct { 
   float p_gain; /* p gain */ 
   float i_gain; /* i gain */ 
   float d_gain; /* d gain */ 
   float last_error; /* last error */ 
   float error_integral; /* integral of an error */ 
   float output; /* output of the PID */ 
   uint16_t sam_rate; /* sampling rate */ 
   float integral_max; /* Maximum of the error integral */ 
   float pid_max; /* Maximum of the PID */ 
   }pid_instance; 

typedef enum { 
   pid_ok = 0, 
   pid_numerical 
}pid_typedef; 

pid_typedef apply_pid(pid_instance *pid, float input_error); 
void reset_pid(pid_instance *pid); 
void set_pid(pid_instance *pid, float p, float i, float d); 

#endif /* INC_PID_CONTROL_H_ */

Next, we have the source file of the PID controller. The complementary functions "set_pid" and "reset_pid" are self-explanatory. We integrate the error within "apply_pid" and ensure it does not exceed the maximum value. Then, we compute the output of the PID and update "last_error" for the derivative computation in the next iteration. Finally, we verify that the PID output is within the acceptable range. 

#include "pid_control.h"

void set_pid(pid_instance *pid, float p, float i, float d)
{
    pid ->error_integral = 0;
    pid ->p_gain = p;
    pid ->i_gain = i;
    pid ->d_gain = d;
}

void reset_pid(pid_instance *pid)
{
    pid -> error_integral = 0;
    pid -> last_error = 0;
}

pid_typedef apply_pid(pid_instance *pid, float input_error)
{
    pid ->error_integral += input_error;
    if(pid->error_integral > pid ->integral_max)
    {
        pid->error_integral = pid ->integral_max;
    }
    if(pid->error_integral < -pid ->integral_max)
    {
        pid->error_integral = -pid ->integral_max;
    }
    if ( pid ->sam_rate == 0)
    {
        return pid_numerical;
    }
    pid ->output = pid ->p_gain * input_error +
            pid ->i_gain * (pid->error_integral) / pid ->sam_rate +
            pid ->d_gain * pid ->sam_rate * (input_error - pid->last_error);


    if(pid->output >= pid ->pid_max)
    {
        pid->output = pid ->pid_max;
    }
    if(pid->output <= -pid ->pid_max)
    {
        pid->output = -pid ->pid_max;
    }
    pid->last_error = input_error;


    return pid_ok;
} 


Afterward, you include the header file, define the struct members, and apply PID easily. The code snippet below is the pseudo-code, and hopefully, you can adapt it to your project:

#include "pid_control.h"
static pid_instance mota_pid = {.d_gain = 0,
                                .error_integral = 0,
                                .i_gain = 25,
                                .integral_max = 5000,
                                .last_error = 0,
                                .output = 0,
                                .p_gain = 20,
                                .pid_max = 100,
                                .sam_rate = 100};   
int main(void)
{

   while(1){
   velocity = mesure_velocity(); // pseudo-code
   aplyy(apply_pid(&mota_pid, 5 - velocity) // 
   }
}


STM32 PID Motor Control Example

The video below demonstrates using the PID controller to control motor velocity using the STM32 MCU. I use an optical encoder to monitor the motor velocity. Using this feedback, I can estimate the error: the difference between the desired and actual velocity. Afterward, this error is inputted into the PID Controller. Its output sets the PWM signal's duty cycle: an input to the motor driver. Eventually, by adjusting the duty cycle of the PWM signal dynamically using the PID, we can keep track of the desired velocity.    

  

 Other Robotics and STM32 Programming Articles



« Back to Blog