Implementing PID control using Timerone library

Hello
I've been trying to find a way to implement pid speed control of a dc motor using timerone library. so the interrupt should happen every 0.25s for my case and then count the number of encoder counts withing that interval and use it to calculate current rpm and send it to computePID() for error correction.

Up till now the program successfully computes,returns and displays the currentrpm, however, when I send that rpm to computePID() the output is not as desired, the motor starts to jack while rotating(its unable to roatate properly, here's the code.

#include <TimerOne.h>
const int encoderPinA = 2;
const int pwmPin = 4;   // Motor PWM 
const int dirPin = 53;  // Motor DIR 

float counter=0; // Counter variable for counting pulses
int RPM, currentRPM;

unsigned long currentTime, previousTime;
double elapsedTime;

// PID declaration
double kp = 0.06;   //proportional gain
double ki = 0.1;    //integral gain
double kd = 0.082;  //derivative gain

double error;
double lastError;
double input, output;
int setPoint;
double cumError, rateError;

void setup() {
  Serial.begin(115200);
  pinMode(encoderPinA, INPUT_PULLUP);
  pinMode(pwmPin, OUTPUT);
  pinMode(dirPin, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(encoderPinA), encoder, RISING);
  Timer1.initialize(250000);             // takes microseconds-Set timer interrupt interval to 0.25s
  Timer1.attachInterrupt(computerpm);  // Attach timer interrupt service routine

  digitalWrite(dirPin, LOW);  //set direction
}

void loop() {
  setPoint = 80;
  currentRPM = RPM;
  Serial.println(currentRPM); // Print RPM value

  output = computePID();
  analogWrite(pwmPin, output); //write new pid output to pin
  
}

void encoder() {
  counter++;
  //Serial.println(counter);
}

double computerpm() {
  RPM = (counter / 96)*240;  // Calculate RPM from counter value
  counter = 0; 
 
  return RPM;
}

double computePID() {
  currentTime = millis();                              //get current time
  elapsedTime = (double)(currentTime - previousTime);  //compute time elapsed from previous computation

  error = setPoint - RPM;                  // determine error
  cumError += error * elapsedTime;                // compute integral
  rateError = (error - lastError) / elapsedTime;  // compute derivative

  double out = kp * error + ki * cumError + kd * rateError;  //PID output

  lastError = error;           //remember current error
  previousTime = currentTime;  //remember current time
  //Serial.println(error);

  return out;  //return PID output - PWM
}
//end of program

pid values are correct as i'm able to acheive the speed control with using pulsin() but I no longer wana use that, here's my previous code that works with pid:

void loop() {

  setPoint = 121;
  currentRPM = computerpm();
  output = computePID();
  analogWrite(pwmPin, output);
}

void encoder() {
  interrupted = true;
}

double computerpm() {
  noInterrupts();
  rps_Ton = pulseIn(encoderPinA, HIGH);  // calculate ON time of RPus pulse input
  rps_Toff = pulseIn(encoderPinA, LOW);  // calculate OFF time of RPus pulse input
  rps_time = rps_Ton + rps_Toff;         // calculate total time(microseconds)
  rps = rps_time / 1000;                 // calculate frequency that is RPms
  RPM = 60000 * (1 / (96 * rps));        // Update current RPM
  //Serial.print("currentRPM; ");
  Serial.println(rps);

  interrupted = false;
  interrupts();
  return RPM;
}

But as said, I no longer wana use interrupts() and pulse in like that because there's more i wana do and the interupt part is causing trouble.

So can anyone point out what's wrong in my current code using timerone?

Thanks

Please post your complete code. Posting snippets just wastes everyone's time.

From How to get the best out of this forum

We can only find problems in code we can see. Please supply your complete code in code tags <CODE/> More about posting code and using code tags. If you are using Arduino Create then you can post a link to your project (but keep in mind that not everyone uses Arduino Create).

Posting a snippet of code is generally useless. The problem is usually in another part of the program.

Before posting code, please use Tools / Auto Format at the top of the IDE. It makes the code much easier to read and you will probably find it very helpful yourself. Once you've done that, use Edit / Copy for Forum.

Always use code tags (<CODE/>) when posting code or compiler error messages.
Using code tags makes the code easier to read and easy to copy. Not using code tags means some of the code gets displayed with smiley faces and other stuff that should not be there. Be aware that there is a limit of 120000 characters for a post. If your code takes you above this limit then you can add your .ino file as an attachment. However, it will be much easier to help you if you can write a short program that illustrates the problem you are having and post that.

Please don't post an image of code instead of the code itself, or an image of error messages instead of the error messages themselves.

It is very important to be clear about what is expected from code and what happens instead. Code ALWAYS works; that is the nature of code. Whether it does what you expect is a different thing altogether. We need to know what you expected the code to do and what happened instead.
More about posting code and common code problems.

Only the variable declaration part was missing which i have editing now

No, that's still not your complete sketch. How do I know? There's no #include <TimerOne.h> for one thing. Who knows what else you left out.

Who knows what else you left out.

Correct, the code does not compile even with the library included.

counter is undeclared, and it needs to be volatile int counter;

Your are misusing the Timer1.attachInterrupt()function.

The call back function needs to be void without a return type.

int RPM is a global variable and since it is declared as an int, there will be truncation in the callback. RPM will only see values in multiples of 240.

I think that RPM is best declared globally as a volatile float, and the call back should then be

computerpm() {
  RPM = (counter / 96.0)*240.0;  // Calculate RPM from counter value
  counter = 0; 
}

you might be interested in simulating the code, or at least just dumping values for each computation

simulation output

#   n    rpm  setPt      out    error cumError rateError
    0   0.00     50   130.00     50.0      5.0    500.0
    1  13.00     50    96.20     37.0      8.7   -130.0
    2  22.00     50    72.80     28.0     11.5    -90.0
    3  29.00     50    54.60     21.0     13.6    -70.0
    4  35.00     50    39.00     15.0     15.1    -60.0
    5  39.00     50    28.60     11.0     16.2    -40.0
    6  42.00     50    20.80      8.0     17.0    -30.0
    7  44.00     50    15.60      6.0     17.6    -20.0
    8  45.00     50    13.00      5.0     18.1    -10.0
    9  47.00     50     7.80      3.0     18.4    -20.0
   10  47.00     50     7.80      3.0     18.7      0.0
   11  48.00     50     5.20      2.0     18.9    -10.0
   12  49.00     50     2.60      1.0     19.0    -10.0
   13  49.00     50     2.60      1.0     19.1      0.0
   14  49.00     50     2.60      1.0     19.2      0.0
   15  49.00     50     2.60      1.0     19.3      0.0
   16  50.00     50     0.00      0.0     19.3    -10.0
   17  50.00     50     0.00      0.0     19.3      0.0
   18  50.00     50     0.00      0.0     19.3      0.0
   19  50.00     50     0.00      0.0     19.3      0.0
   20  50.00     50     0.00      0.0     19.3      0.0
   21  50.00     50     0.00      0.0     19.3      0.0
   22  50.00     50     0.00      0.0     19.3      0.0
   23  50.00     50     0.00      0.0     19.3      0.0
   24  50.00     50     0.00      0.0     19.3      0.0
   25  50.00     50     0.00      0.0     19.3      0.0
   26  50.00     50     0.00      0.0     19.3      0.0
   27  50.00     50     0.00      0.0     19.3      0.0
   28  50.00     50     0.00      0.0     19.3      0.0
   29  50.00     50     0.00      0.0     19.3      0.0

// -----------------------------------------------------------------------------
// PID declaration
#if 0
double kp = 0.06;  //proportional gain
double ki = 0.1;  //integral gain
double kd = 0.082;  //derivative gain

#else
double kp = 2.6;   // proportional gain
double ki = 0.0;    // 0.1;  //integral gain
double kd = 0.00;  //derivative gain
#endif

double elapsedTime;
double error;
double lastError;
double input, output;
int    setPoint  = 50;
double cumError, rateError;


double
computePID (
    int             RPM,
    unsigned long   dTmsec )
{
    elapsedTime = dTmsec / 1000.0;

    error     =  setPoint - RPM;                    // P
    cumError +=  error * elapsedTime;               // I
    rateError = (error - lastError) / elapsedTime;  // D

    double out = kp * error + ki * cumError + kd * rateError;

    lastError = error;

    return out;
}

// -----------------------------------------------------------------------------
int
model (
    double  f,
    unsigned long   dTmsec )
{
    const  double mass = 1;
    static double acc;
    static double spd;

    acc   = f / mass;
    spd  += acc * dTmsec / 1000.0;

    return spd;
}

// -----------------------------------------------------------------------------
# include <stdio.h>

int
main ()
{
    double out;
    float  rpm;

        printf ("#%4s",  "n");
        printf (" %6s",  "rpm");
        printf (" %6s",  "setPt");
        printf (" %8s", "out");
        printf (" %8s", "error");
        printf (" %8s", "cumError");
        printf (" %8s", "rateError");
        printf ("\n");

    unsigned long dTmsec = 100;

    for (int n = 0; n < 30; n++) {
        double out = computePID (rpm, dTmsec);
        printf (" %4d", n);
        printf (" %6.2f", rpm);
        printf (" %6d",   setPoint);
        printf (" %8.2f", out);
        printf (" %8.1f", error);
        printf (" %8.1f", cumError);
        printf (" %8.1f", rateError);
        printf ("\n");

        rpm  = model(out, dTmsec);
    }

    return 0;
}
1 Like

Sorry about that, I pasted entire code again

Hey I updated the code, you can try to compile it again. I'm able to compile the code here.
Also I'm able to print correct counter values using as type float and also resets properly based on timer, so what's your view on this? But I know it will be a safe declare as volatile int because it will be changed anytime in the timer interrupt part based on timer. I will make that change. Please try to compile and see if there's any other programming mistake.

The problem remains same I'm able to calculate and display correct rpm values for the setpoint while the computerPID() is commented. But when I uncomment it and try to control the motor it doesn't rotate at any constant rpm....it jacks while rotating so rpm varies everytime.

I'll try to implement this and see.

But for this, I'm able to display the RPM in the loop, so it means the call back function is correctly returning the RPM as output. So what do you think about this? or is there certain cases where it doesn't work?

What do you mean by this?

The motor rotates but the speed is not constant...I displayed the rpm error and the error was always changing from -2, -13, -50....its always changing so the rotation is not constant

#include <TimerOne.h>

const int encoderPinA = 2;
const int pwmPin = 4;
const int dirPin = 53;

float counter = 0;
int RPM, currentRPM;

unsigned long currentTime, previousTime;
double elapsedTime;

// PID declaration
double kp = 0.06;
double ki = 0.1;
double kd = 0.082;

double error;
double lastError;
double input, output;
int setPoint;
double cumError, rateError;

void setup() {
  Serial.begin(115200);
  pinMode(encoderPinA, INPUT_PULLUP);
  pinMode(pwmPin, OUTPUT);
  pinMode(dirPin, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(encoderPinA), encoder, RISING);
  Timer1.initialize(250000);
  Timer1.attachInterrupt(computerpm);

  digitalWrite(dirPin, LOW);
  previousTime = millis();  // Initialize previousTime here
}

void loop() {
  setPoint = 80;
  currentRPM = RPM;
  Serial.println(currentRPM);

  // Calculate the time elapsed since the previous loop iteration
  currentTime = millis();
  elapsedTime = (double)(currentTime - previousTime) / 1000.0;  // Convert to seconds
  previousTime = currentTime;

  output = computePID(elapsedTime);
  analogWrite(pwmPin, output);
}

void encoder() {
  counter++;
}

double computerpm() {
  RPM = (counter / 96) * 240;
  counter = 0;
  return RPM;
}

double computePID(double elapsedTime) {
  error = setPoint - RPM;
  cumError += error * elapsedTime;
  rateError = (error - lastError) / elapsedTime;

  double out = kp * error + ki * cumError + kd * rateError;
  lastError = error;
  
  return out;
}

The fix here involves using millis() to track time in the loop() function and passing the elapsedTime as an argument to the computePID() function. Here's the explanation:

  1. We initialize previousTime in the setup() function, which is used to keep track of the time elapsed between loop iterations.

  2. In the loop() function, we calculate the time elapsed (elapsedTime) since the previous loop iteration using millis(). This ensures that the time-based calculations in the computePID() function are based on the TimerOne interrupt interval (0.25 seconds).

  3. We pass elapsedTime as an argument to the computePID() function to make sure that the PID calculations use the correct time interval.

By making these changes, you ensure that your PID controller uses a more accurate timing mechanism that aligns with the TimerOne interrupt interval, which should help you achieve better motor control.

I don't think you are understanding about the global variable taking on the value without the return.

int RPM;

Also I'm able to print correct counter values using as type float and also resets properly based on timer, so what's your view on this?

This is wrong.

rpm should be a volatile float.
counter should be a volatile int

Please post the latest code in a new posting. Constantly revising the original post makes if very difficult to follow the thread. Each time you modify the code, make a new posting of it.

Here's the necessary changes that you pointed out, however now the RPM is always displayed as 0.00.

#include <TimerOne.h>

const int encoderPinA = 2;
const int pwmPin = 4;
const int dirPin = 53;

volatile int counter = 0;
volatile float RPM, currentRPM;

unsigned long currentTime, previousTime;
double elapsedTime;

// PID declaration
double kp = 0.06;
double ki = 0.1;
double kd = 0.082;

double error;
double lastError;
double input, output;
int setPoint;
double cumError, rateError;

void setup() {
  Serial.begin(115200);
  pinMode(encoderPinA, INPUT_PULLUP);
  pinMode(pwmPin, OUTPUT);
  pinMode(dirPin, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(encoderPinA), encoder, RISING);
  Timer1.initialize(250000);
  Timer1.attachInterrupt(computerpm);
  setPoint = 80;

  digitalWrite(dirPin, LOW);
  analogWrite(pwmPin, 50);
  previousTime = millis();  // Initialize previousTime here
}

void loop() {
  
  Serial.println(RPM);
  
  // Calculate the time elapsed since the previous loop iteration
  currentTime = millis();
  elapsedTime = (double)(currentTime - previousTime) / 1000.0;  // Convert to seconds
  previousTime = currentTime;
  

  //output = computePID(elapsedTime);
  //analogWrite(pwmPin, output);
}

void encoder() {
  counter++;
  //Serial.println(counter);

}

void computerpm() {
  RPM = (counter / 96) * 240;
  counter = 0;
  //return RPM;
}

double computePID(double elapsedTime) {
  error = setPoint - RPM;
  cumError += error * elapsedTime;
  rateError = (error - lastError) / elapsedTime;

  double out = kp * error + ki * cumError + kd * rateError;
  lastError = error;
  
  return out;
}

Try this revision of the call back. It makes clear to the compiler that it needs to treat the values as float and not integers.

void computerpm() {
  RPM = ( float counter / 96.0) * 240.0;
  counter = 0;
  //return RPM;
}

@codingapacalyspe whatever you said makes total sense and I think that should have been a fix but unfortunately, the motor doesn't rotate at all using your changes. I then tried to display the elapsedtime in the loop and then only motor starts to rotate(but still rotation is not smooth as it should be) but when i comment the display part the motor stops.

oid loop() {
  setPoint = 80;
  currentRPM = RPM;
  //Serial.println(currentRPM);

  // Calculate the time elapsed since the previous loop iteration
  currentTime = millis();
  //Serial.println(currentTime);
  elapsedTime = (double)(currentTime - previousTime) / 1000.0;  // Convert to seconds
  Serial.println( elapsedTime); //--- if I uncomment this line then motor doesn't rotate
  previousTime = currentTime;

  output = computePID(elapsedTime);
  analogWrite(pwmPin, output);
}

shouldn't computeRpm() and computePid() be performed at the same time. (happens if one if performed more often than the other) ??

why not use a timer in loop() to perform an update periodically (~100msec)

  • capture counter (posisbly using noInterrupts() and reset the counter
  • compute the rpm
  • compute the correction applied to the motor
  • update the motor drive voltage

print values each iteration to make sure things are working as expected

Yes initially i used interrupts but I'm trying to achieve wireless control so when i try to control the speed using joystick then that interrupt part hinders the program, so I was trying to replace interrupt with Timerone library and use different approach for calculating rpm

the input to PID is simply a target speed (or position). while that target could change more frequently than the PID execution cycle, the PID algorithm would run on the device controlling the motors independently of the device where the target value comes from

I've redid everything again so hopefully this helps.

#include <TimerOne.h>

const int encoderPinA = 2;
const int pwmPin = 4;   // Motor PWM 
const int dirPin = 53;  // Motor DIR 

float counter = 0; // Counter variable for counting pulses
int RPM, currentRPM;

unsigned long currentTime, previousTime;
double elapsedTime;

// PID declaration
double kp = 0.06;   // Proportional gain
double ki = 0.1;    // Integral gain
double kd = 0.082;  // Derivative gain

double error;
double lastError = 0; // Initialize lastError to 0
double input, output;
int setPoint;
double cumError = 0;  // Initialize cumError to 0
double rateError;

void setup() {
  Serial.begin(115200);
  pinMode(encoderPinA, INPUT_PULLUP);
  pinMode(pwmPin, OUTPUT);
  pinMode(dirPin, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(encoderPinA), encoder, RISING);
  Timer1.initialize(250000);             // Set timer interrupt interval to 0.25s
  Timer1.attachInterrupt(computerpm);  // Attach timer interrupt service routine

  digitalWrite(dirPin, LOW);  // Set direction
}

void loop() {
  setPoint = 80;
  currentRPM = RPM;
  Serial.println(currentRPM); // Print RPM value

  cumError = 0; // Reset cumError at the beginning of each control loop
  output = computePID();
  analogWrite(pwmPin, output); // Write new PID output to the pin
}

void encoder() {
  counter++;
}

double computerpm() {
  RPM = (counter / 96) * 240;  // Calculate RPM from the counter value
  counter = 0;
  previousTime = millis();  // Update previousTime here

  return RPM;
}

double computePID() {
  currentTime = millis();  // Get current time
  elapsedTime = (double)(currentTime - previousTime);  // Compute time elapsed from previous computation
  previousTime = currentTime;  // Update previousTime

  error = setPoint - RPM;  // Determine error
  cumError += error * elapsedTime;  // Compute integral
  rateError = (error - lastError) / elapsedTime;  // Compute derivative

  double out = kp * error + ki * cumError + kd * rateError;  // PID output

  lastError = error;  // Remember the current error

  return out;  // Return PID output (PWM)
}

Explanation of Fixes:

  1. previousTime in computePID:

    • In the computePID function, I added an update to previousTime right after calculating the RPM. This ensures that the elapsed time for PID calculations is based on the correct interval.
  2. Initialize cumError and lastError:

    • I initialized cumError and lastError to 0 at the beginning of the program. This prevents issues caused by using uninitialized variables and resets the integral and derivative terms for each control loop.
  3. Reset cumError in loop:

    • I added cumError = 0; at the beginning of the loop function to reset the cumulative error at the start of each control loop. This prevents the cumulative error from growing excessively over time.

These changes should help you achieve more stable and predictable PID control for your DC motor without the need for pulseIn and noInterrupts/interrupts. Remember to fine-tune your PID constants to optimize the performance of your motor control.

Resetting cumError=0 on every loop essentially eliminates the I term from the PID. Or rather turns it into an additional portion of the P term.

There doesn't look like there is anything limiting the rate of re-calculating the PID, so it could happen that elapsedTime == 0 and then rateError = (error - lastError) / elapsedTime; // Compute derivative could cause a divide-by-zero error.

Also, since you are measuring the sample time in milliseconds, the units of cumError are RPM*millisecond and the units of error/elapsed time are RPM/millisecond. This scales up the integral term fast (*1000) and scales down the derivative term to be very slow (/1000). Without scaling within the PID routine, ki and kd with that math should perhaps be ki/1000 = 0.0001 and kd should be kd = kd*1000 = 82.

I'd rate-limit the PID adjustment, scale the PID parameters or code to the adjustment time, and instead of zeroing the cumulative error every loop, add some output limits and stop accumulating if the PID reached the limits (also known as conditional integration). Something like this where I simulated the code in Wokwi:

#include <TimerOne.h>

const int encoderPinA = 2;
const int pwmPin = 4;   // Motor PWM
const int dirPin = 53;  // Motor DIR

float counter = 0; // Counter variable for counting pulses
int RPM, currentRPM;

unsigned long currentTime, previousTime;
double elapsedTime;

// PID declaration
double kp = 0.06;   // Proportional gain
double ki = 0.1;    // Integral gain
double kd = 0.082;  // Derivative gain

double error;
double lastError = 0; // Initialize lastError to 0
double input, output;
int setPoint;
double cumError = 0;  // Initialize cumError to 0
double rateError;

void setup() {
  Serial.begin(115200);
  pinMode(encoderPinA, INPUT_PULLUP);
  pinMode(pwmPin, OUTPUT);
  pinMode(dirPin, OUTPUT);
  attachInterrupt(digitalPinToInterrupt(encoderPinA), encoder, RISING);
  Timer1.initialize(250000);             // Set timer interrupt interval to 0.25s
  Timer1.attachInterrupt(computerpm);  // Attach timer interrupt service routine

  digitalWrite(dirPin, LOW);  // Set direction
}

void loop() {
  setPoint = 80;
  currentRPM = RPM;
  //Serial.println(currentRPM); // Print RPM value

  //cumError = 0; // Reset cumError at the beginning of each control loop
  output = computePID();
  analogWrite(pwmPin, output); // Write new PID output to the pin
  simulate();
  report();
}

void encoder() {
  counter++;
}

double computerpm() {
  RPM = (counter / 96) * 240;  // Calculate RPM from the counter value
  counter = 0;
  previousTime = millis();  // Update previousTime here

  return RPM;
}

double computePID() {
  currentTime = millis();  // Get current time
  elapsedTime = (double)(currentTime - previousTime);  // Compute time elapsed from previous computation
  if (elapsedTime < 100) return;
  previousTime = currentTime;  // Update previousTime

  error = setPoint - RPM;  // Determine error
  cumError += error * elapsedTime/1000.0;  // Compute integral
  rateError = (error - lastError) / elapsedTime *1000.0;  // Compute derivative

  double out = kp * error + ki * cumError + kd * rateError;  // PID output
  
  // Undo integral if out of limits
  if (out > 255) {cumError -= error * elapsedTime/1000.0; out = 255;}
  if (out < 0) {cumError -= error * elapsedTime/1000.0; out = 0;}

  lastError = error;  // Remember the current error

  return out;  // Return PID output (PWM)
}

// extra bits for reporting and simulating in WokWi with a slide-pot attached to A0

void simulate(void) {
  static uint32_t last = 0;
  const uint32_t interval = 5;
  uint32_t now = millis();
  if (now - last < interval) return;
  last = now;
  //encoder();
  //if(analogRead(A0) >= random(1023)) encoder();
  counter = analogRead(A0) / 8;
}


void report(void) {
  static uint32_t last = 0;
  const uint32_t interval = 250;
  uint32_t now = millis();
  if (now - last < interval) return;
  last = now;
  Serial.print("RPM: ");
  Serial.print(currentRPM); // Print RPM value
  Serial.print(" Output: ");
  Serial.print(output);
  Serial.print(" cumError: ");
  Serial.print(cumError);
  Serial.print("rpm*secs");
  Serial.println();
  //Serial.println(currentRPM); // Print RPM value
}