HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

PID Control Implementation

03-23-2009, 01:33 AM#1
Ammorth
I'm wondering if I'm doing this implementation of PID correctly. Specifically the integral and derivative parts. I would think the integral would over-flow in one direction or the other, depending on which way the value changes (think of a car driving clockwise around a circle, its error is always to the right (positive). Therefore, are you supposed to reset the integral once it reaches it's goal? And as the derivative, since I can't take the instantaneous derivative of the error function, I just have a sampling, is this fine?

I've read through the wiki page and they don't talk much about implementing to code, so I did this by guesswork.

Collapse JASS:
library PID initializer init requires LinkedList

globals
    real Pgain = 0.00
    real Igain = 0.00
    real Dgain = 0.00
    
    real timeStep = 0.1
    
endglobals

globals
    real sumErrors = 0.
    real derivTime = 0.
    real derivError = 0.
    List errors = 0
    
    integer derivSample = 10
endglobals

struct Error
    real value
endstruct

private function init takes nothing returns nothing
    local integer i = 0
    set errors = List.create()
    loop
        exitwhen i > derivSample
        call Link.create(errors, 0) // populate the error list
        set i = i + 1
    endloop
    set derivTime = timeStep * derivSample
endfunction

function ReadValue takes real SP, real PV returns nothing
    local Error e = Error.create() // create a new error
    local Error e2 = errors.last.data
    set e.value = SP - PV // set the error value
    set sumErrors = sumErrors + e.value // add the value to the running total
    set derivError = derivError - e2.value // remove the old error from the deriv
    if e2 != 0 then
        call e2.destroy() // destroy the old error
    endif
    if errors.last != 0 then
        call errors.last.destroy() // destroy the link
    endif
    call Link.create(errors, e) // add the new error to the list
endfunction

function Proportional takes nothing returns real
    local Error e = errors.first.data
    return Pgain * e.value
endfunction

function Integral takes nothing returns real
    return Igain * sumErrors
endfunction

function Derivative takes nothing returns real
    if derivTime == 0. then
        return 0
    endif
    return Dgain * (derivError/derivTime)
endfunction

function PIDloop takes real SP, real PV returns real
    call ReadValue(SP, PV)
    return Proportional() + Integral() + Derivative()
endfunction
    
endlibrary

This is just for my own experiments as I'm getting into MCUs/Robotics and this is a big element of the topic.

edit: for the integral... I'm just thinking. If the integral term continues to push the PID value upwards, the proportional and derivative term would keep it semi stable. Therefore the derivative would most likely 0 out?

edit2: The integral does 0 out after some time so that is not an issue.

I'm having troubles with the derivative term. For some reason, a Dgain of larger than 0.1 causes the PID value to oscillate and grow in size. I'm not sure if this is contributed to the fact that the derivative is not for the instantaneous error, but the slope of an average error over some defined dt (dt > 0).
03-23-2009, 02:49 AM#2
Vexorian
So, there's a wikipedia disambiguation page for PID, but I can't make any of the stuff it provides fit in this thread. Nice?

Are there forums closer to the topic of this PID thing? Maybe you could try asking them? Jass should be readable to anyone that has some familiarity with programming languages, so they can help you I guess.
03-23-2009, 03:50 AM#3
PipeDream
It looks like you're integrating the error (e.value -> sumError), but maybe that has something to do with the plant?

From memory the loop looks like this:
Code:
input = desired_plant_output()
error1 = measured_plant_output() - input
p = pgain * error
i = i + igain * dt * error
i = clamp(i,imin,imax)
d = dgain * (error1 - error0)/dt
plant_control = p + i + d
update_plant_control(plant_control)
error0 = error1
sleep(dt)

If you've implemented that correctly, then there are just places in parameter space where the loop is unstable. It's OK that your dt is finite, and you can analyze it as if it's infinitesimal as long as it's faster than the dynamics of your plant.

In practice you lowpass in front of the PID filter, because all the output from your system at high frequencies is noise. It prevents the derivative from going bonkers as you increase the sampling rate.

I encourage you to learn how to analyze filters/controllers/linear systems, it's a nice journey down complex analysis. I can find references if you want. Depending on how much calculus you know, I might be able to run through it on IRC.
03-23-2009, 04:20 AM#4
Vexorian
Could you at least tell me what does PID stand for?
03-23-2009, 04:34 AM#5
moyack
Proportional Integral Derivative control.
03-23-2009, 05:01 AM#6
Ammorth
I changed the derivative term to match the code you posted, and it is a lot more stable. I am confused about it though, since it is hardly the derivative of anything.

wikipedia defines the derivative term to be:



but the version in your code appears to just be an altered proportional term, in relation to the timestep. The derivative term is also supposed to slow down the other terms by countering them, while nearing the setpoint, to reduce overshoot. I can't see it doing it like this.

Collapse JASS:
library PID initializer init requires LinkedList

globals
    real Pgain = 1.00
    real Igain = 0.50
    real Dgain = 1.00
    
    real timeStep = 0.1
    
endglobals

globals
    real sumErrors = 0.
    real derivTime = 0.
    real derivError = 0.
    List errors = 0
    
    integer derivSample = 1
endglobals

struct Error
    real value
endstruct

private function init takes nothing returns nothing
    local integer i = 0
    set errors = List.create()
    loop
        exitwhen i > derivSample
        call Link.create(errors, 0) // populate the error list
        set i = i + 1
    endloop
    set derivTime = timeStep * derivSample
endfunction

function ReadValue takes real SP, real PV returns nothing
    local Error e = Error.create() // create a new error
    local Error e2 = errors.last.data
    set e.value = SP - PV // set the error value
    set sumErrors = sumErrors + e.value // add the value to the running total
    set derivError = derivError - e2.value // remove the old error from the deriv
    if e2 != 0 then
        call e2.destroy() // destroy the old error
    endif
    if errors.last != 0 then
        call errors.last.destroy() // destroy the link
    endif
    call Link.create(errors, e) // add the new error to the list
endfunction

function Proportional takes nothing returns real
    local Error e = errors.first.data
    return Pgain * e.value
endfunction

function Integral takes nothing returns real
    return Igain * sumErrors
endfunction

function Derivative takes nothing returns real
    local Error e = errors.first.data
    return Dgain * e.value * timeStep
    //return Dgain * (derivError/derivTime) 
    //if derivTime == 0. then
        //return 0
    //endif
endfunction

function PIDloop takes real SP, real PV returns real
    call ReadValue(SP, PV)
    return Proportional() + Integral() + Derivative()
endfunction
    
endlibrary

I'm taking a mcu programming course currently and we are building a line-following robot. I was looking into different methods when I ran across the PID approach. It intrigued me and it appears it is all over the place, so I decided to look at it in more detail. I know the concepts around both derivatives and integrals, but am not the best at integrating difficult functions.
03-23-2009, 06:36 AM#7
PipeDream
The derivative term in my code is the "finite difference" approximation to the derivative

Code:
de/dt ~ (e(t+dt) - e(t))/dt
If the error is low passed, it has finite bandwidth and is smooth enough to be taylor expanded
Code:
 e(t+dt) = e(t) + dt de/dt + dt^2/2 d^2e/dt^2 + ... 
Substituting
Code:
 (e(t+dt) - e(t)) = (e(t) + dt de/dt + O(dt^2) - e(t))/dt = de/dt + O(dt) 
As you reduce dt, and if the error signal is band limited, the finite difference approximation to the error derivative will converge to the actual error derivative.

It turns out you can also analyze the controller directly in the finite approximation. The above taylor expansion shows us how to construct a shift operator z. Applying z to a function shifts it forward in time by dt. Since we can implement shifts in time more easily than we can do continuous derivatives it's a more natural representation.
Code:
 e(t+dt) = exp(dt d/dt) e(t) = z e(t)
Note this is totally dippy, since d/dt is an operator not a number. Well, there are ways to formalize it and make it correct, but they don't really matter if you just want to analyze a system.

Now we want to represent all of your operators (proportional, integral, derivative) in terms of z and e(t). Set dt to 1 (absorb it into other coefficients) for convenience.

Code:
 pin = p e(t) 
Code:
 din = d(e(t) - e(t-1)) = d(1 - z^-1) e(t) = d e(t) (z-1)/z 
Code:
 iin = i e(t) + e(t-1) + e(t-2) + ... = i e(t) (1 + z^-1 + z^-2 + z^-3 ...) = i e(t) 1 / (1 - z^-1) = i e(t) z/(z-1) 

Code:
 out(t) = pin + din + iin = (p + d (z-1)/z + i z / (z-1)) e 

By writing in this notation we've extracted all the information of the system's behavior into a time independent transfer function
Code:
 H(z) = out/e = p + d (z-1)/z + i z / (z-1) 

Now we can analyze the properties of the transfer function independently of the input and output signals. This is possible because the controller is linear, even though it mixes signal from different time.

Code:
 H(z) = ((p + i + d) z^2 - (p + 2 d) z + i)/(z (z-1)) 
Just as H(z) tells us the response at one time given all past inputs, it tells us the portion of the response at all future times of the current input. This is because z^n shifts e(t) forward by n steps.
Code:
 out(t+n) = {z^n coefficient of H(z)} * e(t)

Now, we want to know if H(z) represents a stable system. Since the system is linear, one thing we could do is provide an impulse to the system at time 0 (z^0 e(t)) and check the response dies away. We'd like

Code:
 sum n=0 to inf |out(t+n) from a single e(t) is equal to 1| < infinity 
Code:
 sum n=0 to inf |z^n coefficient of H(z)| ~not exactly~ |H(1)| 
By starting the sum from n = 0, I'm assuming that the system is causal. Inputs at later times have no effect on outputs of earlier times. Obviously any real system will satisfy this condition.

So, we need the taylor series for |H(1)| to converge. I don't know if this can be proved with out complex analysis, but it turns out that the radius of convergence of a taylor series stops when you reach the first pole of the function in the complex plane. For example,

Code:
 1/(1+x^2) 
Even though this function is totally well behaved and smooth on the real line, the taylor series converges only for |x| < 1 because it has poles at x = +- i.

The poles of our H(z) is given by the roots of the denominator
Code:
 H(z) = ... / (z (z-1)) 

This has poles at z=0 and z = 1. Since 0 is inside the unit circle, the taylor series has a zero radius of convergence, and our system is unstable! Of course this was obvious. If you give an impulse response to an integrator, it will rise up a bit and then sit there for eternity. The PID controller alone isn't an interesting system - you need to include the plant.

So, using these tools, write down the transfer function for your whole system, PID controller + plant, locate the poles of the transfer function, and find what values of p, i and d will bring the poles inside the unit circle. That's the region where your system will be unstable.

Writing down the transfer function of a closed loop requires a little cleverness, but you can figure it out or find the trick on wikipedia.
03-26-2009, 06:31 AM#8
Blackroot
Wouldn't it be easier and faster to use a table of aproximations for a finite sytem such as this? Obviously the values would still need to be calculated; but PID should have a finite amount of possible inputs shouldn't it?
03-26-2009, 12:05 PM#9
PipeDream
The clamp is essentially a 2 entry look up table, but the rest of PID is a trivial linear map. Maybe you're looking at the analysis.
03-26-2009, 07:46 PM#10
ShadowWolf
Quote:
Originally Posted by Vexorian
Could you at least tell me what does PID stand for?
Yeah, I thought PID stood for process identification number, and I was wondering what the hell JASS has to do with that. :/