8
\$\begingroup\$

I have implemented a PID function using the formula,

correction = Kp * error + Kd * (error - prevError) + kI * (sum of errors)

What should I do to keep my output between a certain range? say 0-255 If I disregard any value not between 0 to 255 it produces a jiggly behavior?

\$\endgroup\$
6
\$\begingroup\$

I usually just limit the integral term (sum of errors) and if you cannot handle ringing you need to drop the gain to make the system over damped. Also make sure your variables for error, prevError and (sum of error ) are larger variables that do not clip or overflow.

When you just clip the correction and then feed that back into the next error term it will cause a non-linearity and the control loop will be getting a step response into it each time you clip which will cause your jiggly behavior.

\$\endgroup\$
6
\$\begingroup\$

You need to handle two issues:

  1. arithmetic overflow
  2. integrator windup

The arithmetic overflow is fairly straightforward -- whenever you're doing integer math, make sure that you use larger-width intermediate values: for example, if a and b are 16-bit, and you add/subtract them, use a 32-bit intermediate value, and limit it to the range of a 16-bit value (0 to 65535 for unsigned, -32768 to 32767 for signed) before casting back down to 16 bits. If you are absolutely sure that you can never get overflow, because you are absolutely sure of the ranges of the input variables, then you can skip this step, but beware.

The integrator windup issue is more subtle. If you have a large error for an extended period of time, so that you reach the saturation limit of your controller output, but the error is still nonzero, then the integrator will keep on accumulating error, possibly getting much larger than it should to achieve steady-state. Once the controller comes out of saturation, the integrator has to come back down, causing unnecessary delay and possibly instability in your controller response.


On another note:

I would strongly recommend (yeah, I know this question is 18 months old, so you're probably done with your task, but for the benefit of readers let's pretend it isn't) that you calculate the integral term differently: Instead of Ki * (integrated error), calculate integral of (Ki*error).

There are several reasons for doing so; you can read them in a blog post I wrote about how to implement PI controllers correctly.

\$\endgroup\$
4
\$\begingroup\$

A couple of refinements you might want to consider:

  • generate proper I and D terms using suitable filters rather than just using sums and differences (otherwise you will be very prone to noise, accuracy problems and various other errors). NB: make sure your I term has sufficient resolution.

  • define a prop band outside which D and I terms are disabled (i.e. proportional-only control outside prop band, PID control inside prop band)

\$\endgroup\$
2
\$\begingroup\$

Well, as Jason S said, this question is old :). But below is my approach. I've implemented this on a PIC16F616 running at 8MHz internal oscillator, using XC8 compiler. The code should explain itself in the comments, if not, ask me. Also, I can share the whole project, as I will do in my website later on.

/*
 * applyEncoder Task:
 * -----------------
 * Calculates the PID (proportional-integral-derivative) to set the motor
 * speed.
 *
 * PID_error = setMotorSpeed - currentMotorSpeed
 * PID_sum = PID_Kp * (PID_error) + PID_Ki * ∫(PID_error) + PID_Kd * (ΔPID_error)
 *
 * or if the motor is speedier than it is set;
 *
 * PID_error = currentMotorSpeed - setMotorSpeed
 * PID_sum = - PID_Kp * (PID_error) - PID_Ki * ∫(PID_error) - PID_Kd * (ΔPID_error)
 *
 * Maximum value of PID_sum will be about:
 * 127*255 + 63*Iul + 63*255 = 65500
 *
 * Where Iul is Integral upper limit and is about 250.
 * 
 * If we divide by 256, we scale that down to about 0 to 255, that is the scale
 * of the PWM value.
 *
 * This task takes about 750us. Real figure is at the debug pin.
 * 
 * This task will fire when the startPID bit is set. This happens when a
 * sample is taken, about every 50 ms. When the startPID bit is not set,
 * the task yields the control of the CPU for other tasks' use.
 */
void applyPID(void)
{
    static unsigned int PID_sum = 0; // Sum of all PID terms.
    static unsigned int PID_integral = 0; // Integral for the integral term.
    static unsigned char PID_derivative = 0; // PID derivative term.
    static unsigned char PID_error; // Error term.
    static unsigned char PID_lastError = 0; // Record of the previous error term.
    static unsigned int tmp1; // Temporary register for holding miscellaneous stuff.
    static unsigned int tmp2; // Temporary register for holding miscellaneous stuff.
    OS_initializeTask(); // Initialize the task. Needed by RTOS. See RTOS header file for the details.
    while (1)
    {
        while (!startPID) // Wait for startPID bit to be 1.
        {
            OS_yield(); // If startPID is not 1, yield the CPU to other tasks in the mean-time.
        }
        DebugPin = 1; // We will measure how much time it takes to implement a PID controller.


        if (currentMotorSpeed > setMotorSpeed) // If the motor is speedier than it is set,
        {
            // PID error is the difference between set value and current value.
            PID_error = (unsigned char) (currentMotorSpeed - setMotorSpeed);

            // Integrate errors by subtracting them from the PID_integral variable.
            if (PID_error < PID_integral) // If the subtraction will not underflow,
                PID_integral -= PID_error; // Subtract the error from the current error integration.
            else
                PID_integral = 0; // If the subtraction will underflow, then set it to zero.
            // Integral term is: Ki * ∫error
            tmp1 = PID_Ki * PID_integral;
            // Check if PID_sum will overflow in the addition of integral term.
            tmp2 = 0xFFFF - tmp1;
            if (PID_sum < tmp2)
                PID_sum += tmp1; // If it will not overflow, then add it.
            else
                PID_sum = 0xFFFF; // If it will, then saturate it.

            if (PID_error >= PID_lastError) // If current error is bigger than last error,
                PID_derivative = (unsigned char) (PID_error - PID_lastError);
                // then calculate the derivative by subtracting them.
            else
                PID_derivative = (unsigned char) (PID_lastError - PID_error);
            // Derivative term is : Kd * d(Δerror)
            tmp1 = PID_Kd * PID_derivative;
            // Check if PID_sum will overflow in the addition of derivative term.
            if (tmp1 < PID_sum) // Check if subtraction will underflow PID_sum
                PID_sum -= tmp1;
            else PID_sum = 0; // If the subtraction will underflow, then set it to zero.

            // Proportional term is: Kp * error
            tmp1 = PID_Kp * PID_error; // Calculate the proportional term.
            if (tmp1 < PID_sum) // Check if subtraction will underflow PID_sum
                PID_sum -= tmp1;
            else PID_sum = 0; // If the subtraction will underflow, then set it to zero.
        }
        else // If the motor is slower than it is set,
        {
            PID_error = (unsigned char) (setMotorSpeed - currentMotorSpeed);
            // Proportional term is: Kp * error
            PID_sum = PID_Kp * PID_error;

            PID_integral += PID_error; // Add the error to the integral term.
            if (PID_integral > PID_integralUpperLimit) // If we have reached the upper limit of the integral,
                PID_integral = PID_integralUpperLimit; // then limit it there.
            // Integral term is: Ki * ∫error
            tmp1 = PID_Ki * PID_integral;
            // Check if PID_sum will overflow in the addition of integral term.
            tmp2 = 0xFFFF - tmp1;
            if (PID_sum < tmp2)
                PID_sum += tmp1; // If it will not overflow, then add it.
            else
                PID_sum = 0xFFFF; // If it will, then saturate it.

            if (PID_error >= PID_lastError) // If current error is bigger than last error,
                PID_derivative = (unsigned char) (PID_error - PID_lastError);
                // then calculate the derivative by subtracting them.
            else
                PID_derivative = (unsigned char) (PID_lastError - PID_error);
            // Derivative term is : Kd * d(Δerror)
            tmp1 = PID_Kd * PID_derivative;
            // Check if PID_sum will overflow in the addition of derivative term.
            tmp2 = 0xFFFF - tmp1;
            if (PID_sum < tmp2)
                PID_sum += tmp1; // If it will not overflow, then add it.
            else
                PID_sum = 0xFFFF; // If it will, then saturate it.
        }

        // Scale the sum to 0 - 255 from 0 - 65535 , dividing by 256, or right shifting 8.
        PID_sum >>= 8;

        // Set the duty cycle to the calculated and scaled PID_sum.
        PWM_dutyCycle = (unsigned char) PID_sum;
        PID_lastError = PID_error; // Make the current error the last error, since it is old now.

        startPID = 0; // Clear the flag. That will let this task wait for the flag.
        DebugPin = 0; // We are finished with the PID control block.
    }
}
\$\endgroup\$
  • \$\begingroup\$ use the typedefs in <stdint.h> for uint8_t and uint16_t, rather than unsigned int and unsigned char. \$\endgroup\$ – Jason S Nov 10 '15 at 20:17
  • \$\begingroup\$ ...but why the heck are you using unsigned variables for a PI controller? This adds a lot of complexity to your code; the separate if/else cases are unnecessary (unless you use different gains depending on the error sign) You are also using the absolute value of the derivative, which is incorrect. \$\endgroup\$ – Jason S Nov 10 '15 at 20:21
  • \$\begingroup\$ @JasonS I don't remember at the moment, but I guess at that time +- 127 was not enough for me. Also, I don't get how I use the absolute value of the derivative, which part of the code do you mean? \$\endgroup\$ – abdullah kahraman Nov 11 '15 at 7:55
  • \$\begingroup\$ look at your lines containing PID_derivative assignment; you get the same value if you switch PID_error and PID_lastError. And for that matter you've already lost PID_error's sign: if last time setMotorSpeed =8 and currentMotorSpeed = 15, and this time setMotorSpeed = 15 and currentMotorSpeed = 8, then you'll get a PID_derivative value of 0, which is wrong. \$\endgroup\$ – Jason S Nov 12 '15 at 0:47
  • \$\begingroup\$ Also your code for computing products is wrong if unsigned charis an 8-bit type and unsigned int is a 16-bit type: if PID_kd = 8 and PID_derivative = 32, then their product will be (unsigned char)256 == 0, because in C, the product of two integers of the same type T is also of that same type T. If you want to do an 8x8 -> 16 multiply, you need to cast one of the terms to an unsigned 16-bit number before the multiply, or use a compiler intrinsic (MCHP calls them "builtins") designed to give you an 8x8 -> 16 multiply. \$\endgroup\$ – Jason S Nov 12 '15 at 0:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.