PID controller speed control for a DC motor with encoder

Hi,
I am a beginner in Arduino programming. I want to control a constant speed of a DC motor with PID controller and encoder

my idea is:
Arduino controls the motor driver of the DC motor (target speed).
Encoder reports speed to Arduino (actual speed).
PID controller regulates the speed constantly.
I have this code below:

#include <Encoder.h>   //encoder library
#include <PID_v1.h>    // PID library

// DC24V40RPM DC motor  and MD30C R2 30A DC Motor Driver
#define DIR 10            // DIR motor Driver
#define PWM_OUTPUT 12     // PWM motor Driver

// PIR 
double Setpoint, Input, Output;
double   Kp=2, Ki=5, Kd=1;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
 const int PPR = 100; //define number of pulses in one round of encoder

// Rotary encoder Type: HQK38H6-100N-G5-24
#define EN_A 2
#define EN_B 3
Encoder Enc(EN_A, EN_B);

//Time variables
long previousMillis = 0;
long currentMillis = 0;

//Encoder variables
volatile long currentEncoder;
volatile long previousEncoder = 0;
volatile long oldPosition = 0;
volatile long newPosition;

float old_rot_speed=0;
float rot_speed;    

void setup() 
{
  //Input = analogRead(PIN_INPUT);
  Setpoint = 100;  
  Serial.begin(9600);
 // pinMode(DIR, OUTPUT);
  pinMode(EN_A, INPUT_PULLUP);
  pinMode(EN_B,  INPUT_PULLUP); 
  Serial.println("TwoKnobs Encoder Test:");
   
  //turn the PID on
   myPID.SetMode(AUTOMATIC);
   myPID.SetOutputLimits(0,255);
}
float read_speed(void)
{
    const int PPR = 100; //define number of pulses in one round of encoder
    currentEncoder = Enc.read();
    const int interval = 1000; //choose interval is 1 second (1000 milliseconds)
    currentMillis = millis();

    if ((currentMillis - previousMillis) > interval){
    
        previousMillis = currentMillis;
        rot_speed = ((currentEncoder - previousEncoder)*60)/PPR;     //rotating speed in rpm
        previousEncoder = currentEncoder;
        return rot_speed;
    }
}

void loop() {
   //Input = analogRead(PIN_INPUT);
   Input = read_speed();

  myPID.Compute();
  analogWrite(PWM_OUTPUT, Output);

  Serial.print("Input = ");
  Serial.print(Input);
  Serial.println();
  Serial.print("Output = ");
  Serial.print(Output);
  Serial.println();
}

Unfortunately the motor doesn't turn properly. sometimes spins at 255 and sometimes at 150... and sometimes doesn't spin. but ı cann't find whats wrong
Could you help my in this issue ?

Please show your output. We don't have your motor (they vary wildly in size, speed, and inertia) or wiring here, so it's hard to tell exactly what is wrong.

You might try reducing these parameters:

I'd try "Kp = 1, Ki =0, Kd =0; " to eliminate integral windup as a possibility.

The Kp is the proportional constant, and should do most of the ramping-up work to dial up toward whatever is the right PWM for 100.

Posting schemaics showing all the powering would be good.

1 Like

Thank you for your response
output for these parameters:
double Kp=2, Ki=5, Kd=1;
is

 -> Output = 255.00
 -> Input = 3.00
 -> Output = 255.00
 -> Input = 138.00
 -> Output = 255.00
 -> Input = 138.00
 -> Output = 255.00
 -> Input = 138.00
 -> Output = 0.00
 -> Input = 138.00
 -> Output = 0.00
 -> Input = 138.00
 -> Output = 0.00
 -> Input = 138.00
 -> Output = 0.00
 -> Input = 138.00
 -> Output = 141.00
 -> Input = 138.00
 -> Output = 141.00
 -> Input = 138.00
 -> Output = 141.00
 -> Input = 138.00
 -> Output = 122.00
 -> Input = 138.00
 -> Output = 122.00
 -> Input = 138.00
 -> Output = 122.00
 -> Input = 138.00
 -> Output = 103.00
 -> Input = 138.00
 -> Output = 103.00
 -> Input = 138.00
 -> Output = 103.00
 -> Input = 138.00
 -> Output = 84.00
 -> Input = 138.00
 -> Output = 84.00
 -> Input = 138.00
 -> Output = 84.00
 -> Input = 138.00
 -> Output = 65.00
 -> Input = 138.00
 -> Output = 65.00
 -> Input = 138.00
 -> Output = 65.00
 -> Input = 138.00
 -> Output = 46.00
 -> Input = 138.00
 -> Output = 46.00
 -> Input = 138.00
 -> Output = 46.00
 -> Input = 138.00
 -> Output = 46.00
 -> Input = 138.00
 -> Output = 27.00
 -> Input = 138.00
 -> Output = 27.00
 -> Input = 138.00
 -> Output = 27.00
 -> Input = 138.00
 -> Output = 27.00
 -> Input = 138.00
 -> Output = 8.00
 -> Input = 25.00
 -> Output = 8.00
 -> Input = 25.00
 -> Output = 8.00
 -> Input = 25.00
 -> Output = 8.00
 -> Input = 25.00
 -> Output = 255.00
 -> Input = 25.00
 -> Output = 255.00
 -> Input = 25.00
 -> Output = 255.00

and these Parameters
Kp = 1, Ki =0, Kd =0;
motor doesn't turn

 -> Output = 100.00
 -> Input = 0.00
 -> Output = 100.00
 -> Input = 0.00
 -> Output = 100.00
 -> Input = 0.00
 -> Output = 100.00
 -> Input = 0.00
 -> Output = 100.00
 -> Input = 0.00
 -> Output = 100.00
 -> Input = 0.00
 -> Output = 100.00
 -> Input = 0.00
 -> Output = 100.00
 -> Input = 0.00
 -> Output = 100.00
 -> Input = 0.00
 -> Output = 100.00
 -> Input = 0.00

Cool. 100/255 power isn't strong enough to start your motor, 255/255 gets you to a top speed of 138 in 50ms.

Two things: If the system comes to full speed in 50ms as shown, the PID's default deltaTime should be much smaller than its default of 100ms.

Try try shortening the sampling time to 1/10 of the time constant of the system

myPID.SetSampleTime(2); // 2ms from 50ms/3/10 

Try a larger proportional constant. With an input of 0 and a setpoint of 100, a Kp= 2.5 would give you a full power 255 output at the beginning.

Also, are you getting good speeds out of your readSpeed() function? What does it return if interval hasn't passed?

with Kp= 2.5 and
myPID.SetSampleTime(2); // 2ms from 50ms/3/10

Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = ovf
Output = 0.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 0.00
Output = 250.00
Input = 144.00
Output = 0.00
Input = 144.00
Output = 0.00
Input = 144.00
Output = 0.00
Input = 144.00
Output = 0.00
Input = 144.00
Output = 0.00

so it spins and stops all the time

I believe that this read_speed() function is responsible for the problem of the motor spinning and then constantly stopping For example if I increase interval = 2000 -->motor spins 2 second then stops 2 second steadily. do you know how to optimize this function read_speed()?

Yes, if you don't have a reliable speed measurement, you can't use it for control.

Looking deeper, when you got speed results, you get 138RPM, which would mean 138*100*60/60 = 13800 pulses per second\\\minute. At target speed, that's 10000\\\167 pulses per second. You could sample much faster than once per second. I'd include the sampling interval in the calculation, and shorten the interval significantly. (And do a smoothing.)

I'd separate the reporting from the updating and maintain a state variable for motor speed.

float read_speed(void)
{
    static float currentSpeed = 0;
    const float alpha = 0.2 ; // EWMA smoothing constant;
    const int PPR = 100; //define number of pulses in one round of encoder
    const int interval = 100; //choose interval is 1 second (1000 milliseconds)
    currentMillis = millis();

    if ((currentMillis - previousMillis) > interval){
    
        previousMillis = currentMillis;
        currentEncoder = Enc.read();
        rot_speed = ((currentEncoder - previousEncoder)*60*1000 )/PPR/interval;     //rotating speed in rpm
        previousEncoder = currentEncoder;
        //return rot_speed;
        currentSpeed += alpha * (rot_speed - currentSpeed); // EWMA smoothing
     }
    return currentSpeed;
}

I'd also wrap the reporting in a millis() conditional, and speed up loop:

void report(void){
  static uint32_t last=0;
  uint32_t now = millis();
  const uint32_t interval = 500;
  if(now - last >= interval ){
     last += interval;
  Serial.print(now); 
  Serial.print(" Input = ");
  Serial.print(Input);
  //Serial.println();
  Serial.print("  Output = ");
  Serial.print(Output);
  Serial.println();
  }
}

void loop() {
   //Input = analogRead(PIN_INPUT);
   Input = read_speed();

  if (myPID.Compute()) {
     analogWrite(PWM_OUTPUT, Output);
  }
  report();
}

(This code is completely untested. -- It may well have syntax errors, and I'd tune the read_speed interval and maybe the EWMA alpha smoothing parameter.)
(Edited to correct pulse-per-second calculation per PID controller speed control for a DC motor with encoder - #14 by DaveX )

Thank you, yes is a big problem with the speed measurement
I don't have a reliable encoder speed measurement yet.
with new update is output:

motor rotates in 100 milliseconds
then stops in 100 milliseconds like this constantly

5Input = 55.82  Output = 110.44
6000 Input = 66.57  Output = 83.57
500 Input = 41.13  Output = 147.18
1000 Input = 54.62  Output = 113.45
1500 Input = 67.35  Output = 81.63
2000 Input = 43.47  Output = 141.32
2500 Input = 54.89  Output = 112.78
3000 Input = 67.44  Output = 81.41
3500 Input = 45.57  Output = 136.08
4000 Input = 57.18  Output = 107.05
4500 Input = 66.40  Output = 84.00
5000 Input = 45.31  Output = 136.72
5500 Input = 56.70  Output = 108.24
6000 Input = 62.98  Output = 92.55
6500 Input = 43.42  Output = 141.44
7000 Input = 54.02  Output = 114.96
7500 Input = 62.50  Output = 93.74
8000 Input = 42.31  Output = 144.23
8500 Input = 55.10  Output = 112.24
9000 Input = 67.69  Output = 80.78

when i comment if statement in read_speed function

float read_speed(void)
{
     static float currentSpeed = 0;
    const float alpha = 0.2 ; // EWMA smoothing constant;
    const int PPR = 100; //define number of pulses in one round of encoder
    const int interval = 100; //choose interval is 1 second (1000 milliseconds)
    currentMillis = millis();

    //if ((currentMillis - previousMillis) > interval){
    
       // previousMillis = currentMillis;
        currentEncoder = Enc.read();
        rot_speed = ((currentEncoder - previousEncoder)*60*1000 )/PPR/interval;     //rotating speed in rpm
        previousMillis = currentMillis;
        previousEncoder = currentEncoder;
        //return rot_speed;
        currentSpeed += alpha * (rot_speed - currentSpeed); // EWMA smoothing
    // }
    return currentSpeed;
}

--> motor rotates without stopping only input is wrong

1 = 0.00  Output = 250.00
17000 Input = 0.00  Output = 250.00
17500 Input = 0.00  Output = 250.00
500 Input = 0.00  Output = 250.00
1000 Input = 0.00  Output = 250.00
1500 Input = 0.01  Output = 250.00
2000 Input = 0.00  Output = 250.00
2500 Input = 0.00  Output = 250.00
3000 Input = 0.31  Output = 250.00
3500 Input = 0.00  Output = 250.00
4000 Input = 0.00  Output = 250.00
4500 Input = 0.96  Output = 247.60
5000 Input = 0.00  Output = 250.00
5500 Input = 0.00  Output = 250.00
6000 Input = 0.00  Output = 250.00
6500 Input = 0.00  Output = 250.00

What's happening here with millis() going backwards? How are things wired up? (Per @Railroader's Q in PID controller speed control for a DC motor with encoder - #3 by Railroader )

To increase responsiveness of the read_speed, instead of commenting out the if statement completely out, bump the interval down to 10 or 5 or 1ms. And maybe increase the EWMA parameter up to 0.5 or 0.8 or all the way to 1.0.

If the motor is cycling on and off at 100ms, speed up the reporting interval to 50ms (you may need to up the baud rate like Serial.begin(115200);)

Another thing to try before going too far with PID, is to try controlling it manually with a potentiometer (or with hardcoded constants if you don't have a potentiometer) to see how your motor runs:

void loop() {
   //Input = analogRead(PIN_INPUT);
   Input = read_speed();

  if (myPID.Compute()) {
     analogWrite(PWM_OUTPUT, Output);
  }

  //override with constant:
  Output = 140; // Chosen from https://forum.arduino.cc/t/pid-controller-speed-control-for-a-dc-motor-with-encoder/985306/8
  // override with potentiometer:
  // const byte PIN_INPUT= A0;
  // Output = analogRead(PIN_INPUT)/4;
  analogWrite(PWM_OUTPUT,Output);
  report();
}

If the system is too touchy, it could be hard to tune a PID to control it properly. With a manual potentiometer, you should be able to find at what PWM level the motor starts turning, the motor's top speed , and maybe the value that the PID should output for steady state at 100RPM.

Oh, these aren't the problem, but they should be type "unsigned long" for storing millis() results

(Edited code to uncomment overriding analogWrite(PWM_OUTPUT,Output); )

Which motor, which driver?

1 Like

I mean the target value Setpoint = 100
there is only one dc motor and one drive
DC24V40RPM DC motor and MD30C R2 30A DC Motor Driver

Schemaics is :

1 Like

mills() going not backwards i deleted a line

1 = 0.00  Output = 250.00
17000 Input = 0.00  Output = 250.00
17500 Input = 0.00  Output = 250.00
TwoKnobs Encoder Test:
500 Input = 0.00  Output = 250.00
1000 Input = 0.00  Output = 250.00
1500 Input = 0.01  Output = 250.00

Schemaics is her PID controller speed control for a DC motor with encoder - #12

When you take the if out, the loop() can run much faster than the encoder pulses happen, averaging in lots of zeros.

At 138RPM with 100 pulses per rev, that's 13800 pulses/min or 230 pulses/sec or an interval 4ms/pulse (I'll edit my error above).

You'd probably want to use a sample time of at least 40ms in order to catch ~10 pulses at top speed to have 10% resolution.

Here is my working version of a similar sketch to the one you wrote

// This version of Tacometer auto adjusts for difference in pulses and has Stall Recovery
#include <PID_v3.h> // uses my new version of PID_v1

// Adjust these variables:
int PulsesPerRevolution = 400;// From Tachometer How Many Pulses Pur Revolution
int SampleDuration = 30; // in Milliseconds -- How often do you want to trigger the PID Lower the number the more detailed control but the more the arduino processor time is used up
double consKp = 1, consKi = 0.01, consKd = 1;
double Setpoint = 3000;// RPM 
#define PWMpin  3
#define interruptPin 2
#define  HBridgeEnable pinMode(4, OUTPUT);digitalWrite(4, HIGH); // enables my H-Bridge Remove line 35 also 
//Define Variables we'll be connecting to

double Input, Output;

//Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &Setpoint, consKp, consKi, consKd, DIRECT);

// Varables used for Calculations 
volatile unsigned long timeX = 1;
unsigned long StallTimer = SampleDuration *3;
volatile int Counts = 1;
double PulsesPerMinute;
volatile unsigned long LastTime;
volatile int PulseCtrX;
int PulseCtr;
unsigned long Counter;
unsigned long Time;

void setup() {
  Serial.begin(115200);
  Serial.println("Test Tachometer");
  PulsesPerMinute = (60 * 1000000) / (PulsesPerRevolution / Counts);// initialize
  pinMode(interruptPin, INPUT_PULLUP);
  pinMode(PWMpin, OUTPUT);
  HBridgeEnable // Remove this if you don't need it
  attachInterrupt(digitalPinToInterrupt(interruptPin), sensorInterrupt, FALLING);
  myPID.SetSampleTime(1);
  myPID.SetOutputLimits(0, 255); // Standard PWM Range
  myPID.SetMode(AUTOMATIC);
}

void loop() {
  readRpm();

  static unsigned long SpamTimer;
  if ((unsigned long)(millis() - SpamTimer) >= 15000) {
    SpamTimer = millis();
    Setpoint = (Setpoint < 5000) ? 7000 : 3000;
  }

}

// New version of sensorInterrupt
void sensorInterrupt()
{
  static int Ctr;
  unsigned long Time;
  Ctr++;
  if (Ctr >= Counts) { // so we are taking an average of "Counts" readings to use in our calculations
    Time = micros();
    timeX += (Time - LastTime); // this time is accumulative ovrer those "Counts" readings
    LastTime = Time;
    PulseCtrX ++; // will usually be 1 unless something else delays the sample
    Ctr = 0;
  }
}

void readRpm()
{
  static unsigned long STime;
  if (!PulseCtrX) {
    if ((unsigned long)(millis() - STime) >= StallTimer) {
      Time = micros();
      timeX += (Time - LastTime); // this time is accumulative ovrer those "Counts" readings
      LastTime = Time;
      PulseCtrX ++; // will usually be 1 unless something else delays the sample
      Serial.print(" Stall \t");
      //Setpoint +=100;
    } else return; // << Added lets not stop interrupts unless we know we are ready (keep other code happy).
  }
  cli ();         // clear interrupts flag
  Time = timeX;   // Make a copy so if an interrupt occurs timeX can be altered and not affect the results.
  timeX = 0;
  PulseCtr = PulseCtrX ;
  PulseCtrX = 0;
  sei ();         // set interrupts flag
  if (PulseCtr > 0) {
    Input =  (double) (PulsesPerMinute /  (double)(( (unsigned long)Time ) *  (unsigned long)PulseCtr)); // double has more percision
    //   PulseCtr = 0; // set pulse Ctr to zero
    AverageCapture(Input);
    debug();
    myPID.Compute();
    analogWrite(PWMpin, Output);
    long DSDur = ((long) Time *  (long)PulseCtr) - ((long) SampleDuration * 1000L);
    Counts -= DSDur * .015;
    Counts = max(Counts, max(1, PulsesPerRevolution * .25));
    Time = 0; // set time to zero to wait for the next rpm trigger.
    Counter += PulseCtr;
    PulseCtr = 0; // set pulse Ctr to zero
    PulsesPerMinute = (60.0 * 1000000.0) / (double)((double)PulsesPerRevolution / (double)Counts);
    STime = millis();
  }
}
float AvgArray[100];
int Readings = 0;
void AverageCapture(float in) {
  static int Position = 0;
  AvgArray[Position] = in;
  Position++;
  Readings++;
  Readings = min (100, Readings); // 100 readings 0-99;
  if (Position >= 100)Position = 0;//99 spots
}
float AverageValue() {
  float Total = 0;
  float Average;
  if (!Readings)return (0.0);
  for (int Position = 0; Position < Readings; Position++) {
    Total += AvgArray[Position];
  }
  Average = Total / Readings;
  return (Average);
}

void debug(){
  char S[20];
  for (static long QTimer = millis(); (long)( millis() - QTimer ) >= 100; QTimer = millis() ) {  // one line Spam Delay at 100 miliseconds
    Serial.print("Counts: "); Serial.print(Counts );
    //    Serial.print(" Target RPM: ");Serial.print(RPM );
    //    Serial.print(" Counts: "); Serial.print(Counts );
    //    Serial.print(" time: "); Serial.print(Time );
    //    Serial.print(" DeltaT: "); Serial.print(Time *  PulseCtr);
    //    Serial.print(" PulseCtr: "); Serial.print(PulseCtr );
    //    Serial.print(" PulsesPerMinute: "); Serial.print(dtostrf(PulsesPerMinute, 6, 1, S));
    Serial.print("\t Average: "); Serial.print(dtostrf(AverageValue(), 6, 1, S));
    //    Serial.print(" Setpoint: "); Serial.print(dtostrf(Setpoint, 6, 1, S) );
    //    Serial.print(" Calculated RPM: "); Serial.print(dtostrf(Input, 6, 1, S));
    //    Serial.print(" Output: "); Serial.print(dtostrf(Output, 6, 2, S));
    Serial.print("\t "); 
    //    Serial.println();

  }
}

the library PID_v3

has no timing issues
This drove a small motor attached directly to a 400 pulse per revolution encoder
the motor controller is just an H-bridge

Hope this helps give you ideas
ZHomeSlice

1 Like

Thanks very much. I have a question.
if i change Setpoint value, the speed doesn't change.
in your sketch Setpoint is equal to 3000. is not speed from 0 to 255?
how can I determine the PID parameters double consKp = 1, consKi = 0.01, consKd = 1;?

The pid parameters all convert input error units to output units, with the Ki and Kd terms including the time. Setting them is “tuning” and depends on the dynamic response of your system.

If a PWM output of 147 counts gets you a speed measure of 100RPM, then Kp is in units of counts/rpm an can specify where you start tapering down from full power. If you want full power up to 50 rpm and then to taper down to 100RPM then kp =255counts/50rpm=5.1count/rpm.

Ki converts integrated error to output units, and at steady state at target, is the sole term that is providing the output signal.

To reach an output of 147 counts with a
Ki=0.1counts/(rpm seconds) would mean it needed to integrate 1470 rpm*sec worth of error to reach steady state. If you go too aggressive with ki, it will oscillate around the target.

Kd is in units of counts/(rpm/sec) and tries to look forward and counteract rapid changes. Most of the time you can ignore it

Since the terms interact and are dependent on sampling time, it’s very important to know how your system reacts and how you want it to respond.

If your system hits full speed in 50ms, that’s important.

I start with getting my delta-t and kp close, then fiddle with ki.

What to do next:
You will want to set the pulses per revolution to 100 to match your encoder
next, if you have a fixed setpoint remove the setpoint changing routine I placed in the loop()

  static unsigned long SpamTimer;
  if ((unsigned long)(millis() - SpamTimer) >= 15000) {
    SpamTimer = millis();
    Setpoint = (Setpoint < 5000) ? 7000 : 3000;
  }

I found it easier to tune with the motor speed changing every 15 seconds so I could see how the integral response and the derivative reacts to the change.

Start your PID tuning with Kp = 0 and Kd = 0 Ki= 0.1 and see how long it takes to reach the setpoint. Ki of 0.1 may be too much if it oscillates start at 0.01.
note that input is the actual RPM and the PID magically converts it into a number between 0 and 255 for the analogWrite() myPID.SetOutputLimits(0, 255); // Standard PWM Range
The PID:
Proportional is instantaneous and no time factor is required to adjust this value.
if your error from the setpoint is 0 (you are on setpoint) Proportional output is ZERO. so Proportional is not good at controlling the setpoint of a motor that needs something other than zero to keep the speed at setpoint.
Derivative requires change between samples.
My Sample duration is:
int SampleDuration = 30; // in Milliseconds --
If there is no change between samples the output is ZERO so Derivative can't be our primary control method.
Integral on the other hand is perfect to control our motor to achieve setpoint. each 30 mS cycle a portion of the error is added to the output. If the error is negative a positive value is added to the output (This could cause windup if setpoint can't be achieved)
P + I + D values = Output
The Ki and Kd values are converted to seconds even though we are sampling at 30ms so in other words, if you have an error of -1 and your Ki is equal to 1, the output would change by +1 every second, and with an input error of -10 with a Ki of 1, the out would change by +10 every second.

the derivative "kicker I call it" is weird and you should add it to help lock setpoint quickly after everything else is adjusted too much derivative influence the output becomes jittery, so add it a little at a time at the very end of tuning to just make things lockdown faster.

Start with integral and increase it till the motor starts to oscillate at setpoint and then back it off till it stops.
now add some proportional control to speed up the landing and help it stay on the setpoint.
once you are happy add some derivative and when loads change the extra kick of derivative will improve response times to the change.
ZHomeSlice

A little note: The goal of this was to stress test at the max RPM and Pulse per minute the Arduino UNO could handle. I was able to achieve setpoint and maintain it between 3000RPM and 7000RPM with an encoder that has 400 pulses per revolution! At 7000RPM the pulses were arriving at 46,666 pulses per second! And the Arduino UNO handled it!

1 Like

This is a nice way to approach tuning--it's sort of thinking about it in the velocity form. I'd always thought of the Proportional term doing most of the work of adjusting to the proper control value, but as one approaches the setpoint, the source for the entire output signal must depend on the integral term. Tuning the Ki parameter first makes excellent sense.

1 Like

@zhomeslice @DaveX
many thanks for your help. I still have questions:
even though motor is not running yet, does the serial monitor show that Input is ~164?

02:36:37.136 ->  Stall 	 Input 166.46 Setpoint 150.00 DeltaTserm   0.00 DTerm   0.00 Output   0.00 0.00	 
02:36:37.136 ->  Stall 	 Input 164.59 Setpoint 150.00 DeltaTS 0.001000 PTerm  -0.00 I  -0.00 ITerm   0.00 DTerm   0.00 Output   0.00Test Tachometer
02:36:37.183 ->  Stall 	 Stall 	 Input 166.46 Setpoint 150.00 DeltaTS 0.001000 PTerm  -0.00 I  -0.00 ITerm   0.01 DTerm   0.00 Output   0.01 Stall 	Counts: 25 PulseCtr: 1 Calculated RPM:  159.2 Output:   0.01	 
02:36:37.464 ->  Stall 	 Input 164.60 Setpoint 150.00 DeltaTS 0.001000 PTerm  -0.00 I  -0.00 ITerm   0.01 DTerm   0.00 Output   0.01 Stall 	Counts: 25 PulseCtr: 1 Calculated RPM:  159.2 Output:   0.01	 
02:36:37.651 ->  Stall 	 Input 164.59 Setpoint 150.00 DeltaTS 0.001000 PTerm  -0.00 I  -0.00 ITerm   0.01 DTerm   0.00 Output   0.01 Stall 	Counts: 25 PulseCtr: 1 Calculated RPM:  159.2 Output:   0.01	 
02:36:37.839 ->  Stall 	 Input 164.58 Setpoint 150.00 DeltaTS 0.001000 PTerm  -0.00 I  -0.00 ITerm   0.01 DTerm   0.00 Output   0.01 Stall 	Counts: 25 PulseCtr: 1 Calculated RPM:  159.2 Output:   0.01	 
02:36:38.026 ->  Stall 	 Input 164.59 Setpoint 150.00 DeltaTS 0.001000 PTerm  -0.00 I  -0.00 ITerm   0.00 DTerm   0.00 Output   0.00 Stall 	Counts: 25 PulseCtr: 1 Calculated RPM:  159.2 Output:   0.00	 

what should I change in the sketch so that I have a Setpoint between 0 and 255 (how can I convert Setpoint?