Attempting to understand PID in a practical way.

In a recent Thread I had been hoping to get some guidance about setting the k values for PID control of a small DC motor for a model train. Subsequently I came across a video by @zhomeslice (link at the bottom of this Post) showing how to tune PID for a balancing toy and later he posted code for controlling a DC motor.

I think it will be easier to discuss this if I start a new Thread. The other Threads have a lot of interesting stuff but I don’t think there is any need to refer to them to follow this discussion.

I was able to get my motor working well using that motor code and I have been doing some exploration to try to understand the system better.

I have found it very hard to get an clear understandung of the relationship between the k values, the input values and the output values. In @zhomeslice’s program the input is RPM and the output is a PWM value 0 - 255. But it is just as valid to use the number of µsecs per revolution as the input. And in another project the input might be angles or temperatures.

It seems to me that life would be very much simpler if the PIDcompute() function always worked with the same range of values for inputs and outputs and it was the responsibility of the user to scale his/her inputs and outputs to match. This should mean that the range of k values will always be the same, and independent of the real-world values (RPM, µsecs or oC) that the project uses.

In the code posted below I have put the PIDcompute() function directly into my program so that everything is in a single file. I find that makes it very much easier to explore the code. The actual code in PIDcompute() is very close to that in @zhomeslice’s motor program.

I have arbitrarily decided that PIDcompute should work with an input and output range of 4096. My program does not have negative input or output values but I see no reason why the range cannot be +/-4096. And it may be easier to use 0-8192 as a proxy for -4096 to +4096.

I have also arbitrarily decided the the k values should always be in the range 0 to 1. To achieve this it seemed necessary to add a scale factor for the derivative calcs. I think the fact that these calculations are based on the difference between subsequent input values has the effect of making the numbers too small to be useful without scaling up.

I wondered for a while what might be a practical way of dealing with the time factor and I concluded that the simplest way tp deal with this is probably for the user to input a minimum interval which is related to the expected shortest interval between measurements. For my tests I have used 1000µsecs which is about one fifth of the shortest duration for a revolution - 200rps = 12000rpm.

I had hoped that it would be possible to do the PID calculations using long rather than float variables to speed up the computation but my experiments suggested that it would actually be slower due to the need to use division so I have given up that line of investigation. Also note that on an Uno float and double are the same - 4 byte floats.

This program seems to work well with my small DC motor. It seems to hold the speed independent of the load on the motor so I can test the program with the unloaded motor and then install it in the loco without needing any code changes.

From a practical point of view it should be noted that the this simple code has no provision to recover from a stall - but that is not a PID issue. When the program is restarted code in setup() gives the motor a burst of power to get it going.

I am very interested to get feedback on any part of this. It would be especially interesting if other people could try the code for different problems - for example temperature control or balancing. Hopefully the end product will be a PID system and a description that makes it easy to understand and apply.

// python-build-start
// action, upload
// board, arduino:avr:uno
// port, /dev/ttyACM0
// ide, 1.6.3
// python-build-end


    //Variables for PID calcs
float PIDinMin = 0;
float PIDinMax = 4096;
float PIDrange = PIDinMax - PIDinMin;

float PIDoutMin = 0;
float PIDoutMax = 4096;

float Setpoint;

float kdScaleFactor = 10.0;


    //Variables for the project
#define PWMpin  5

float Target = 18000;
float Input;
float Output;
float inputMin = 4000.0;
float inputMax = 40000.0;
float inputRange = inputMax - inputMin;

float outMin = 0;
float outMax = 255;
float outRangeFactor = outMax / PIDoutMax;
float inRangeFactor = PIDrange / inputRange;

int dirn = -1;
float kp = 0.1;
float ki = 0.8;
float kd = 0.2;
float minInterval = 1000.0;

int PulsesPerRevolution = 1; // not used, should not be here
unsigned long sensorInterval;

unsigned long PIDstart, PIDend, PIDduration;

    // variables used by the ISR
volatile unsigned long ISRinterval;
volatile bool newISR = false;




void setup() {

    Serial.begin(115200);
    Serial.println(F("Source File /mnt/sdb1/SGT-Docs/ModelRailways/RadioControl/Dubl00BPRC/PIDThoughts/myTacho/myTacho4bPublish.ino"));
    Serial.println("aTest Tachometer");
    delay(1000);

        //Digital Pin 2 Set As An Interrupt for tacho.
    pinMode(2, INPUT_PULLUP);
    attachInterrupt(0, sensorInterrupt, RISING);

    Setpoint = (Target - inputMin) * inRangeFactor;

    pinMode(PWMpin, OUTPUT);
    analogWrite(PWMpin, 150);
    delay(200);
    PIDcompute();
    delay(100);
    PIDcompute();

}

//===========

void loop() {

    readRpm();

}

//===========

void sensorInterrupt() {

    unsigned long ISRmicros;
    static unsigned long prevISRmicros;
    ISRmicros = micros();
    ISRinterval = (micros() - prevISRmicros);
    prevISRmicros = ISRmicros;
    newISR = true;
}

//===========

void readRpm() {

    if(! newISR) return;

    cli ();
        sensorInterval = ISRinterval;
        newISR = false;
    sei ();

        // convert sensorInterval to correct range for PIDcompute()
    Input = ((float) sensorInterval - inputMin) * inRangeFactor;
    if (Input > PIDinMax) {
        Input = PIDinMax;
    }
    if (Input < PIDinMin) {
        Input = PIDinMin;
    }

    //~ PIDstart = micros();
    PIDcompute();
    //~ PIDend = micros();
    //~ PIDduration = PIDend - PIDstart;

        // convert result from PIDcompute() to required range (for PWM, in this case)
    Output = Output * outRangeFactor;
    analogWrite(PWMpin, Output);

}

//===========

void PIDcompute() {

    static unsigned long prevInput;
    static unsigned long prevPIDtime;
    static float ITerm;


    unsigned long PIDtime = micros();
    unsigned long PIDinterval = PIDtime - prevPIDtime;
    prevPIDtime = PIDtime;

    float DTerm = 0;
    float derivative = 0;

        // Calculate error
    float error = Setpoint - Input;

        // Proportional term
    float PTerm = kp * error * dirn;

        // Integral term
    float timeFraction =  minInterval / (float) PIDinterval;
    float ITadj = error * timeFraction * ki * dirn; // uses real delta T not a fixed delta T
    ITerm += ITadj;
    if((ITerm > PIDoutMax) || (ITerm < PIDoutMin) ) {
        ITerm -= ITadj; // prevents windup
    }
    //~ if(ITerm > PIDoutMax) {
        //~ ITerm = PIDoutMax;
    //~ }
    //~ if(ITerm < PIDoutMin) {
        //~ ITerm = PIDoutMin;
    //~ }

            // Derivative term using Input change
    derivative = (prevInput - Input)  * timeFraction; // uses real delta T not a fixed delta T
    prevInput = Input;
    DTerm = kd * derivative * dirn * kdScaleFactor;


        //Compute PID Output
    float output = PTerm + ITerm + DTerm ;

    if(output > PIDoutMax) {
        output = PIDoutMax;
    }
    else if (output < PIDoutMin)  {
        output = PIDoutMin;
    }

    Output = output;

        // Debugging
    static unsigned long QTimer = millis();
    if ( millis() - QTimer  >= 100 ) {  // one line Spam Delay at 100 miliseconds
        QTimer = millis();
        char S[10];

        Serial.print("PIDInterval: "); Serial.print(PIDinterval );
        Serial.print(F(" \tInput ")); Serial.print(dtostrf(Input,6,2,S));
        Serial.print(F("\tSetpt ")); Serial.print(dtostrf(Setpoint,6,2,S));
        Serial.print(F("\tKp ")); Serial.print(dtostrf(PTerm,6,2,S));
        //~ Serial.print(F("\tITadj ")); Serial.print(ITadj);
        Serial.print(F("\tKi ")); Serial.print(dtostrf(ITerm,6,2,S));
        Serial.print(F("\tKd ")); Serial.print(dtostrf(DTerm,9,0,S));
        Serial.print(F("\tOut ")); Serial.print((int)output);
        //~ Serial.print(F("\tPIDduration ")); Serial.print(PIDduration);
        Serial.println();
    }
}

…R

First Attempt :slight_smile:

_
PIDInterval: 256 Input 0.00 Setpt 1592.89 Kp -49.58 Ki 90.00 Kd -0outRangeFactor 0.06 Out 40.40
PIDInterval: 248 Input 0.00 Setpt 1592.89 Kp -49.58 Ki 90.00 Kd -0outRangeFactor 0.06 Out 40.40
more of the same

I set and tested with the following:
int PulsesPerRevolution = 400; // to match my encoder
also forced iTerm to 90 for test to get output to stay above zero to get the motor to run Kp Ki Kd are adjusted by the output factor so I can see how they match up to the actual 0-255 output
input is zero not sure why. I’ll have to look into it more

UPDATE<<<<<<<<<<<<<<<<<
I have determined that 400 steps per revolution is forcing the input to go to zero. I will need to review how to adjust the input to accommodate such a high pulses per revolution.

One of the fundamentals that you have overlooked is speed of response of the process.

here is a test. get some wood and start a fire in your fireplace. within seconds you whole house is warm ?

take a firecracker, light it. do you expect that it will take better than an hour for the sound to work it's way to the other side of the house ?

PID is designed to be able to handle control of both. each of the three terms was added to accomidate a different problem. D is utterly useless in heating and will actually make the control unstable. I/D (inverse derrivitive) is requied as your process approaches speeds close to the speed of sound.

every part of the loop adds error. your sensor has to react to the change and then output a value based on the change. some devices like humidity sensors need seconds or minutes to react. some react at near the speed of light.

you know the bit about speed of your logic.

then you have to output to a final control element. if you output to a laser or a gear motor, your speed of movement caused by the properties of the device have to come into account.

what happens when you move that final element ? if your driven wheel is 1 inch in diameter or 1 meter, the movement will not be the same.

how does your process react to the control ? air is compressable and moves through a damper over a completly different curve than water moves through a valve.

I would offer that you might find it interesting to make a project and get the PID tuned, then change any one part. the sensor, the distance of the sensor from the controller. the driver, the the final control device, motor gearing, etc, and the final control element.

much fun ahead !

dave-in-nj:
One of the fundamentals that you have overlooked is speed of response of the process.

I may indeed have done so but I can’t relate that comment to the code I posted - where is the deficiency?

I think I have taken account of speed of response by taking account of the time between measurements. You would not bother measuring the room temperature every 5 millisecs.

I have been dabbling with the k values for my DC motor. It is much easier to understand their impact if the k values are within a known range - in my case 0 to 1.

Thanks very much for the comments.

…R
PS I am not aware that anyone can control a firecracker once it starts :slight_smile:

zhomeslice:
First Attempt :)I set and tested with the following:
int PulsesPerRevolution = 400; // to match my encoder

Oops - my mistake. My code does not use pulses per revolution at all. It just works on the number of microseconds between pulses. That variable escaped my tidy-up process :slight_smile:

Of course there is no reason why you would not use PPR and use the RPM value as in input (scaled to the range 0-4096)

The PIDcompute() function takes about 200µsecs so if (with 400PPR) the interval between pulses is less than 600µsecs (my guess) it would probably be impossible to call the function for every pulse. But IMHO that problem should be solved outside the PIDcompute() function. Perhaps measure the time for 50 or 100 pulses.

Thanks for your continuing assistance.

…R

Thinking more about the basic concept, each stage of the PID is really a filter. just as you have low-pass, Butterworth, etc, so too, you have to have the correct values in each stage.

I thought of another idea. many of the DIN controllers you can buy have an auto-tune function,

if you take hot water, a valve with a motor and a temp sensor, you can connect the unit and then select auto-tune.

it does run the whole process over multiple wide deviations during the process, but in the end, it selects an appropriate value for each of the filters.

auto-tune may be an achievable goal for an Arduino.

Robin2: ...R PS I am not aware that anyone can control a firecracker once it starts :)

very true ! but the point is that controlling the flow of molasses to maintain portion control on your candy bars is an industrial process, just as flow control on an atomizer sprayer. one take ms, the other seconds. for temperature control, once every 5 minutes would usually suffice.

and, as you pointed out, primary elements dictate the electronics in the sensor and you can put all manner of circuits between the encoder wheel and the input of the PID controller. you can output pulses directly, or turn that into a DC signal. each step can alter the speed of the signal into the controller.

It is my firm belief that you will not be able to develop a fixed set starting point that will be worthwhile over a broad range. That said, even a very basic Ziegler-Nichols sketch to ascertain some starting points would create settings that would aid the project immensely. Do not be afraid to output 'parameter unable to be tuned' if the results fail.

dave-in-nj: It is my firm belief that you will not be able to develop a fixed set starting point that will be worthwhile over a broad range.

I wonder if we are at cross-purposes. I am not trying to constrain the real-world range. What I am proposing is to map the real-world range to the range 0-4096 for the purpose of the PID calcs

That said, even a very basic Ziegler-Nichols sketch to ascertain some starting points would create settings that would aid the project immensely.

That looks very complex. :)

Do not be afraid to output 'parameter unable to be tuned' if the results fail.

Maybe I misunderstand you, but I can't see why that message would be needed. I am not trying to create a system that tunes itself. I am assuming the user will figure out the k values. I just want to standardize the range of the k values so the user does not have to wonder if the correct value might be 40 or 0.0001. If the k values are always within a known range (say 0 to 1) it makes empirical tuning much easier.

Thanks again.

...R

dave-in-nj: Thinking more about the basic concept, each stage of the PID is really a filter. just as you have low-pass, Butterworth, etc, so too, you have to have the correct values in each stage.

You are not the first to have that thought, and you will not be the last. |500x423

I have also arbitrarily decided the the k values should always be in the range 0 to 1. To achieve this it seemed necessary to add a scale factor for the derivative calcs. I think the fact that these calculations are based on the difference between subsequent input values has the effect of making the numbers too small to be useful without scaling up.

Why? That's a silly restriction, and I'm pretty sure it's impossible to generally guarantee that the constants will be within a certain range. What's the point of a scaling factor anyway? All it does is change one constant (K) into two (K*C) that does the same job. I don't think there's any way to pick a C so that K is always within your normalized range, so it would need to be adjustable too. You've pointlessly increased the number of values that need to be considered for no benefit.

Why did you think it's necessary?

I have arbitrarily decided that PIDcompute should work with an input and output range of 4096. My program does not have negative input or output values but I see no reason why the range cannot be +/-4096. And it may be easier to use 0-8192 as a proxy for -4096 to +4096.

Robin2 The encoder adaptation to 400 ppm is going to be more difficult to achieve than the time I have. I like the concept of shifting the input to provide a high resolution value to then use in the PID loop thus providing a high resolution output to use for control. My task is to get my balancing bot working with your code.

My Input did use degres but radians will work better. My working value in radians is 0.4 to -0.4 (about 23° either side of zero) so to achieve your 4096 range I am multiplying it by 10,000 My setpoint must be Zero and I will be swinging to either side of zero My Output Must be zero when balanced I will ABS() the value for PWM usage but a Positive value will drive the motor in 1 direction and a negative value will drive the motor in the other.

How would I prep the my values to take advantage of your code?

Jiggy-Ninja: You've pointlessly increased the number of values that need to be considered for no benefit.

Why did you think it's necessary?

I don't see it like that at all.

When I first came to PID (see the first link in my Original Post here) I had no idea how to pick k values that would be anywhere near the correct ball park. By limiting the range to 0 to 1 my plan is to make life easier for someone who is new to the concept.

The scale factor for the kd part is not intended to be part of the fine tuning. It is only intended to make the range 0 to 1 usable. My expectation is that the scale factor can be adjusted very coarsely. It may even be the case that my value of 10.0 does not need to be changed.

I am aware that people who are already familiar with using PID will not see any particular value in my ideas. They are intended for newbies, not experienced people. However I very much appreciate comments from people who are more experienced than I am to ensure that I have no fundamental error - either in the concept or the code.

Perhaps it is helpful to think of the problem I am trying to adress as follows:- The people who are familiar with and use PID do so with an extensive quantity of background knowledge which the newcomer does not have. I am trying to figure out a way for the newcomer to make progress quickly. It's one thing to have trouble figuring out the exact set of k values that provide the best solution. But you can't even begin to do that until you are close enough to get some useful output from the system. @zhomeslice's code got me close enough. Now I am proposing a more generalized approach that would help others in my situation.

Thank you for your comments.

...R

dave-in-nj: D is utterly useless in heating and will actually make the control unstable.

Trying to control setpoint with D is futile D isn't a control routine it is a dampening routine it slows down the rate of cage by temperately negating the proportional input while landing setpoint, like shocks on a car. once setpoint is achieved trying to add enough derivative value into the equation to do something would cause uncontrolled oscillations, and would be useless, and make the control unstable. I used Derivative to control room temperature, supply air temperature and water temperature for thousands of VAV, RTU, AHU, Boiler and Chiller controls with 100% success. You are just using derivative wrong.

zhomeslice:
How would I prep the my values to take advantage of your code?

I am very much at the learning stage so this is all guesswork.

I would try mapping the radian range ( 0.4 to -0.4) to 0 to 4096. Implicitly then 0 radians would equate to 2048.

Am I right to think that your output needs to be -255 to +255 to take account of the direction of the motor? If so I would try mapping the output range of 0 to 4096 to -255 to +255.

I will be enormously interested to hear how you get on.

…R

The scale factor for the kd part is not intended to be part of the fine tuning. It is only intended to make the range 0 to 1 usable. My expectation is that the scale factor can be adjusted very coarsely. It may even be the case that my value of 10.0 does not need to be changed.

Coarse/fine adjustments make sense for physical knobs where you assign different value/radian amounts to them to deal with human’s limited dexterity. It makes no sense for a purely mathematical structure that takes all its inputs from code.

There’s a few different tuning algorithms you can use, even manually. I remember this one:

  1. Start with KP=KD=KI=0

  2. Increase KP until the system just begins to oscillate around the setpoint.

  3. Increase KD until the oscillation is gone.

  4. Increase KI until the steady-state error is gone.

I’m sure there’s more sophisticated processes, but this one’s easy to remember. There’s no need to just randomly stab around in the dark until you trip over the right values.

Robin2:
But it is just as valid to use the number of µsecs per revolution as the input.

It is somewhere between “desirable” and “critical” that Output and Process Variable have a linear relationship. Does your output have a linear relationship with µs/r?

Which brings up a point that, as far as I can tell, has not been mentioned. PID is based on the ideal world. The real world often differs from the ideal world. It is common to “precondition” the Process Variable to be closer to the ideal. It is common to “postcondition” the Output to be closer to the ideal. If the real and ideal differ by too much it can be impossible to get a PID controller to work as desired; conditioning can be a requirement.

It seems to me that life would be very much simpler if the PIDcompute() function always worked with the same range of values for inputs and outputs and it was the responsibility of the user to scale his/her inputs and outputs to match.

Percent with one decimal is often used (0.0% to 100.0%). Especially with Output.

It is somewhere between "desirable" and "critical" that Output and Process Variable have a linear relationship. Does your output have a linear relationship with µs/r?

I made that point in reply #9 of the above linked thread, but the point was rejected.

It would help to at least try to learn a bit of control theory. PID is one of its simplest applications.

jremington: I made that point in reply #9 of the above linked thread...

11?

...but the point was rejected.

This? (I assume "yes".)

Robin2: There can't be a linear relationship between the PWM duty cycle and the speed of an electric motor faced with a variable load.

Yup. And, as the load increases the current set of tuning parameters become less and less useful. In such situations it is common to use a different set of tuning parameters or even an entirely different control strategy.

If you want to understand PID, start with what it is: a controller for a first-order system. That means a linear relationship between Output and Process Variable. Once you have a reasonable grasp of how that works then move on to how PID has to be mutilated to be useful in the real world.

I think if I respond to this I will cover a few recent comments.

First (and without meaning to be awkward) I don’t know whether @Coding Badly accepts my comment in quotes or rejects it.

My (admitedly limited) experience with PID control of my DC motor suggests that my comment is correct. The motor holds its speed even when the load changes - which is what I want. I am also inclined to think that view is supported by @zhomeslice’s balancing toy.

Jiggy-Ninja:
There’s a few different tuning algorithms you can use, even manually. I remember this one:

  1. Start with KP=KD=KI=0
  2. Increase KP until the system just begins to oscillate around the setpoint.

I have come across this before and no doubt it is correct. But what it fails to do is give an indication of what might be a suitable starting value, or what size the steps might be. The approach would have to be very different starting from 0 if the correct value is 0.01 as compared to starting from 0 when the correct value is 40. What I am hoping to achieve is a system where the newcomer can be confident that the correct value will be somewhere between 0 and 1.

If you want to understand PID, start with what it is: a controller for a first-order system. That means a linear relationship between Output and Process Variable. Once you have a reasonable grasp of how that works then move on to how PID has to be mutilated to be useful in the real world.

I don’t know if this is intended to mean that my code is incorrect. If it is I would appreciate help to improve it.

I wonder if people suspect that I am trying to replace traditional PID with black-magic? I am not. i just have a belief that its application can be standardized and simplified by making the range of the k values independent of the units of measurement of the project.

Again, thank you everyone for your input.

…R

What I see we are trying to achieve is 1 We will need to convert our input into a range from 0 4096 for example my balancing bot needs +- .4 radians (MPU-6050 ideal output) so I would multiply it by 5,000 and then add 2048 to it. I would do the same for my setpoint which is Zero so the PID would get 2048 as the setpoint Now the PID has room to work with The Output would range again from 0-4096 Now I need to convert that to what I need for my motor drive My output for PWM needs to be 0-255 (This also has a dead band I eliminate between 0 and 30 where the motor does nothing). I would first shift the output back to zero by subtracting 2048 then I would check for a positive or negative value for drive direction. I would then map absolute value to a range from 30 to 255

Being able to map a higher resolution to your output would allowing you to have the resolution to be able to convert to non linear arrangement. I am looking into a non linear equation that would represent my balancing bot drive more accurately.

[u]Here is my comment[/u] in reply #9:

HOWEVER, your measurement (microseconds/revolution) is inversely proportional to PWM. 
It is best for PID if all variables scale linearly, so I suggest to convert microseconds/revolution to
RPMs or similar, which would be roughly proportional to PWM.

30,000 microseconds/rev = 33 revs/second
9,000 microseconds/rev = 111 revs/second

This has NOTHING TO DO WITH THE MOTOR LOAD, which was given as the reason for rejecting this suggestion:

There can't be a linear relationship between the PWM duty cycle and the speed of an electric motor faced with a variable load.

It has everything to do with how INPUT and OUTPUT are related, which is extremely important for proper function of PID regulation.