Help designing Embedded PI Controller
Started by 6 years ago●12 replies●latest reply 6 years ago●173 viewsint error=0; int target_RPM=0; int current_RPM = 0; int duty_cycle=0; float Kp = 0; float integral = 0; float Ki = 0; while { //Main program } //10 ms LOOP with TIM6 Interrupt //Get current RPM current_RPM = read_RPM(); //Get the error error = target_RPM - current_RPM; //Calculate the integral integral = integral + (Ki*error); if (integral > 800) // check for integral windup and correct for upper limit integral = 800; if (integral < 0)//check for integral windup and correct for lower limit integral = 0; //Calculate the Control Variable duty_cycle = (Kp*error) + integral; //Limit the Control Variable to within 0-800 if(duty_cycle>800){ duty_cycle = 800; } else if (duty_cycle<0){ duty_cycle = 0; } htim1.Instance->CCR1 = duty_cycle;
Your sense input is 6 pulses per rotation and at maximum 3000 RPM, which is (3000/60)*6 : 300 pulses per second. This translates to 3.33 milli seconds, You may have to use a timer with at least 100 micro seconds (less than 1/10 of lowest i.e. 3.33 ms) and lesser will be better.
From the input pulse timing, you can get the input pulse rate which is proportional to the RPM, in your case 1/3.33 ms -> 300 pulses per second, which is 3000 RPM. That is 1 pulse per second -> 10 RPM.
Assuming 100% duty cycle will give 3000 RPM, which is 800 count, and assuming the relations are linear, you will have 1 count proportional to 3000/800 = 3.75 RPM per count. This will be the proportional constant Kp in the feed back loop.
In your feed back system, you get the input pulse rate, translate it to RPM, compare with Target RPM and apply this constant to derive the output PWM duty cycle %.
int PWM_count; // 0=0%, 800=100% which is to be updated
int Pulse_rate; // 0=0 RPM, 300=3000 RPM can be measured.
int Target_RPM; // the target RPM to be achieved
int Pulse_Timing; // Timing measured between pulses, say it is in u Sec.
int Present_RPM;
int error;
// Control loop. note since preset is deducted from target, the error is
// positive and it has to be added to the output. Please confirm with a
// small prototype code.
Pulse_Timing = get_Time_elaspsed(); // a method from timers in the system.
Pulse_rate = 1000000/Pulse_Timing; // pulse rate is in pulses per second.
Present_RPM = 10*Pulse_rate.
error = Target_RPM - Present_RPM; // calculate the error.
PWM_count += 15*error/4; // 15/4 is same as 3.75, here I have
// used integers to simplify.
A note about the overall system loop timing. Since the time involved is in milliseconds the 10 m sec loop time is not sufficient at lower RPM, i.e at RPM below 1000 RPM you will get less than 100 pulses per second and it is lower than in loop timing and there will be no update. So the response may be sluggish.
Best Regards,
BV Ramesh.
Thank you. I did not mention that the speed is being calculated using Input Capture Mode interrupt. I mean, the interrupt callback gives me the wheel frequency and I have this function to calculate speed:
int read_RPM(){
RPM = (Wheel_Frequency*60)/6;
if (RPM>3000){
RPM = 0;
} else {
RPMT=RPM;
}
return RPMT;
}
I can't say yet that at 100% duty cycle, there's 3000 RPM. I will confirm that tomorrow.
About the 10 ms loop time, what do you suggest then? Increase it to what?
As BVRamesh has pointed out, you have to determine an appropriate low RPM limit for your application and factor this into your control loop update interval. You also need to think about the 0 RPM case, you won't be getting pulses from your sensor and your interrupt won't fire (I think this is what you're doing), so you'll need some way to detect an absence of pulses over some time period. Another consideration is noise in your actual velocity (RPM) calculation. At constant velocity, if your sensor pulses aren't perfectly spaced (and they probably won't be) you'll see an apparent variation (noise) in the calculated pulse-to-pulse velocity. You can smooth this out with some sort of averaging policy. Unfortunately this will introduce delay, a more heavily averaged value means more delay. This delay also has to be factored into your control loop update interval, it can produce stability problems if the loop updates too frequently. You might take a look at this article from Jason Sachs on velocity estimation:
How to Estimate Encoder Velocity Part 1
Jason talks about using pulses from a quadrature encoder, you are using a different type of sensor but the underlying velocity estimation technique based on event separation in time is essentially the same. He does a great job discussing the boundary cases and pitfalls inherent to this technique. Short story, you need a relatively low noise source of actual velocity feedback for your PI control loop, and you want stable 0 velocity detection such that you don't pop in and out of the 0 velocity state around the 0 velocity threshold. It is an easy problem to solve poorly, and a much more difficult one to solve well than is apparent at first glance! If this is for a class and it just has to work in some fashion, then perhaps you don't need a great solution and the path you're heading down is fine.
You're right. My pulses are not perfectly spaced. My lowest speed was 250 RPM, but the system is unstable here. Then I increased it to 300 RPM and now the system responds better. So my range will be from 300 RPM to 3000 RPM. My PI loop runs inside an interrupt routine every 20 ms. I tried to filter the speed using a running average exponential filter. Now I'm using the unfiltered speed for the control loop. I'm tuning the PI gains. With Kp = 1 and Ki = 0, the overshoot and oscillations are excessive. I working now around Kp = 0.25 and Ki = 0.020 and it has improved, although they're not the optimal gains. I think that a well tuned PI controller would deal with the speed noise. Also, the speed interrupt is always firing at each rising edge of the speed signal, but I'm actually sampling the speed inside the PI loop rate, i.e, every 20ms . I think this also would reduce speed noise (?).
Another thing, my initial speed as I said will be 300 RPM. At startup, the motor is stopped, so I think that giving 300 RPM setpoint, with 0 RPM current speed, gives a big error, and that's why I see a fast or abrupt change at the beginning. After that, the speed will be increasing with steps of 50, that is, 300, 350, 400, 450 etc. and it goes smoothly. The solution for this I think, is create a ramp for soft start/stop of the motor and let the PI controller acts fastly for changes in the load. It wouldn't be good idea to lower the PI gains to make the whole system respond slower.
Turning down Kp is certainly the main knob to control overshoot at startup, Kp is essentially your loop gain.
If you request a target RPM of 3000 starting from 0 with a Kp of 1, you'll immediately slam the PWM control to 800 and without a D term (PID) to compensate as actual velocity increases you will have the massive overshoot you're seeing.
Here's something you might want to consider. How fast do you want to ramp from 0 to 3000 RPM? Let's say it's 400 ms. You're running your PI loop every 20 ms, which means it will run 20 times in 400 ms. Your 100% PWM control is 800, so increasing it by 40 at each update will get you from 0 to 800 in 20 updates. Similar to the way you limit integral windup, you may want to limit the maximum rate of change of duty cycle control to +/- 40. This has the side benefit of softening instantaneous current demand which can be useful depending on your power source.
With that in mind you might consider something like:
float duty_cycle = 0.0; float duty_cycle_update = 0.0; // Calculate the control variable update duty_cycle_update = (Kp*error) + integral; if (duty_cycle_update > 40.0) { duty_cycle_update = 40.0; } else if (duty_cycle_update < -40.0) { duty_cycle_update = -40.0; } // Calculate the control variable duty_cycle += duty_cycle_update; if (duty_cycle > 800.0) { duty_cycle = 800.0; } else if (duty_cycle < 0.0) { duty_cycle = 0.0; } htim1.Instance->CCR1 = (int) duty_cycle;
We have a working copy of the duty cycle control, it is updated based on the PI computation as in BVRamesh's code. You don't want to use the per-step PI computation directly as the duty cycle, it is an update to the duty cycle!
One other thing, in your original post the integral computation should include a dt term. The idea is that the integral error at each step is weighted by the control loop update rate such that you contribute less to the integral at each step if you're running the loop more frequently. You may have already factored this into whatever constant you're using for Ki, I only mention it because it doesn't appear in your computation and I wanted to make sure you're not overlooking it.
Stopping and starting smoothly and reliably just doesn't come for free, I know I didn't see that coming the first time I went through one of these designs. Interesting stuff to be sure!
About the dt term, yes, I'm overlooking it for now because I'm still not sure how to include it.
What I'm looking for is this:
At startup (motor stopped), the setpoint will be set to 400 RPM, which is the lowest limit now, not 300 RPM as before. This is the startup of the motor, and I want it to be smooth. Every time the user decides to increase the speed, the setpoint will change in step of 50. The changes from 400 RPM on are smooth because I'm increasing the speed smoothly in step of 50 RPMs. Here the PI is apparently behaving okay.
But the issue is just with start/stop situations. Let's imagine we are on 2000 RPM and suddenly the user gives the command to stop the motor, that is, a setpoint of 0 RPM. I can't allow the motor to stop so fast. The same for the startup, from 0 to 400 RPM.
Lowering the Kp solves this, but then the whole system becomes slow, and I don't want that, since the load should be fastly compensated.
The dt term is usually the discrete PI controller update period in seconds. For a fixed Ki it will decrease the contribution of the integral term at each update when the update period is smaller (higher update rate).
See the Wikipedia PID controller page for a typical manual tuning algorithm, you of course don't have a D term but the P and I tuning applies just the same. It is important to determine a reasonable Kp for your system with Ki initially set to zero. If you can't arrive at a satisfactory tuning you can try changing the update period of your control loop. Running the discrete control loop faster increases the control loop gain, as does increasing Kp.
I understand what you want to do and the issues you're seeing. The math is such that the duty cycle adjustment is proportional to the magnitude of Kp and to the magnitude of the computed error. When you're near the set point and the error is small you can handle a larger Kp, but when the error is large the larger Kp will produce correspondingly larger changes in duty cycle at each control loop evaluation.
Assuming you have good velocity feedback, you should be able to find a satisfactory Kp and Ki tuning at some loop update rate. That said, limiting the maximum duty cycle update at each control loop iteration may help you. In particular, start up can be problematic as the motor will begin to move but you won't have any feedback until you get your second sensor data point. In the mean time your control loop thinks you're still at 0 RPM, applying duty cycle updates with maximum computed error, very conducive to overshoot.
OK. Thank you. I will see what I can do. So far, my controller is working good, except for the start/stop conditions. I will appreciate if you modify again my original code to point out the dt term. My gains for now are Kp = 0.2, Ki = 0.01. Still doing tuning.
The Wikipedia PID control page has a nice pseudo-code algorithm near the bottom, PID with Kd=0 collapses to PI. Their computation is slightly rearranged, they factor Ki out of the integral term and apply it in the final update assignment.
Here's a suggested dt inclusion based on your original code.
#define PI_UPDATE 20 # PI update rate in ms #define PI_DT (0.001*PI_UPDATE) # PI control dt value integral = integral + (Ki*error*PI_DT);
With a 20 ms update rate you'll have to multiply your current Ki value by 50 to get back to the same PI control integral contribution. The above code has the advantage of automatically adjusting the overall integral contribution if you change the PI loop update rate. Hopefully you already have a definition like PI_UPDATE used to program a timer interrupt or compare against an elapsed time value.
It occurs to me that your integral limit of 800 may be quite high. If actual and target RPMs are close it will take hundreds of PI updates to get 800 back down to 0, which doesn't really prevent windup and may be the source of your startup overshoot. A value of 8 or 16 feels like a better choice. The PI integral term is included just to prevent the system from operating in a stable state with a constant target/actual offset, that's all it needs to do.
The manual tuning step of determining an initial Kp with Ki=0 is very important: Set Ki=0, bring up Kp until the system begins to oscillate, divide Kp by two. If you try to tune Kp with Ki in the way you'll wind up chasing your tail.
Great work and good luck!
Many thanks. For the update time, I just set an appropiate prescaler and counter for the timer. For example, with a prescaler of 9 and counter of 63999, I get a 20 ms update event. I just have to modify those values to get the update time I want. That's why I got confused with the PI_UPDATE definition. I've set Ki to zero, and finding the Kp that makes the system oscillate as you suggest. Once I find it, I will start to increase Ki.
[suddenly the web page went off, so I am re-typing, may be the what I typed earlier got lost]
The loop time depends upon the response from the sensor input time. If you fix it as 10 m sec, then at 1000 RPM or below, the response will arrive at time later than your loop time.
So you have to fix the minimum RPM for your control, say 600 RPM. This translates to 60 pulses per second, which is ~ 16.7 m sec. SO in this situation if you set overall loop time as 18 msec, (16.7 m sec and some margin) there will be a good control of RPM down to 600.
Regards,
Howdy, I'm old school -- I do this in assembler, for the fastest execution I can get.
If you can get acceptable response in C code, good on.
If not, I'll work with you to "tighten" it up.
I don't know how to code into your compiler, but the core assembler code "should" be straight forward.
I'll see what you come up with... Good Hunting... <<<)))