PID Control Object (with questions)

Posted on
Page
of 2
/ 2
Next
  • A standard way to implement a feedback control system is the Proportional, Integral, Derivative (PID) control algorithm.
    Think of things to control and how different the control action can be.
    Driving a car. Use a compass module as the input and a motor to steer the wheels. As a driver you turn and hold the steering wheel until the new heading is achieved. Does a boat work the same way?
    When piloting an airplane you turn the yoke to tilt the plane over about 15 degrees and then neutralize the yoke. The plane will circle until you turn the yoke to level the plane. (I’m ignoring rudder action)
    When riding a bicycle or motorcycle, once in motion, the controls act opposite to that of a car. Turn right and the bike tilts to the left and you the bike turns left.
    So how to implement PID control?
    We need:

    1. An input sensor such as a compass module, a thermistor, a thermocouple, an accelerometer, a piezo-gyro etc.
    2. An output device that can be controlled using either a 0 to 3 Volt Voltage or a pulse width (servo motor).
    3. The PID algorithm implemented in Espruino.
      Using Google we find:
      https://en.wikipedia.org/wiki/PID_contro­ller
      The pseudocode from the link:

    previous_error = 0
    integral = 0
    start:
    error = setpoint - measured_value
    integral = integral + error*dt
    derivative = (error - previous_error)/dt
    output = Kp*error + Ki*integral + Kd*derivative
    previous_error = error
    wait(dt)
    goto start

    Suppose the setpoint is 0.5 and the measured value is 1.0. Then errpr= 0.5 – 1.0 = -0.5. Let dt = 1 second, then integral = 0.0 – 0.5 * 1 = -0.5 and derivative= (-0.5 -0.0 )/1 = -0.5.

    I think we have a problem since in an Espruino world analog inputs and outputs work between 0.0 and 1.0. I tried coding this and ran into this problem.
    Another link from a University course just seems to confuse the issue.
    http://www.cds.caltech.edu/~murray/cours­es/cds101/fa02/caltech/astrom-ch6.pdf
    I recall from a course taken many years ago that the PID algorithm can be expressed in a differential form so that the change in output is expressed as a function of the PID terms.
    doutput = P*sumof(error) + I *error + D*derivative,
    Where:
    P=Kp= the proportional gain,
    I=Ki= the integral gain, and
    D=Kd= the derivative gain.
    Maybe a math GURU can properly insert the dt values into the equation.

    First let’s setup a PID object:

    //The PID is an object that could be supplied as a stringified file or client
    //request using http. For development it is coded here.
    var PID={
      Dir:1,//direction +1 or -1
      P:0.1,//proportional gain
      I:0.1,//integral gain
      D:0.1,//derivative gain
      Fname:"PID.CSV" //a file to record results in CSV format on the SD card
    };
    
    

    The Dir field is used to reverse the control action.
    The Fname field is used to record a CSV file.
    Now implement an object to use the PIS information and extend it.

    //The constructor for PIDobj
    function PIDobj(pid,setpoint,input,output,interva­l){
      this.Interval=interval; //How often to apply algorithm
      this.Input=input; // a function that returns the process value
      this.Output=output; //a function that applies the output
      this.PID=pid; // a pointer to the PID object
      this.error=0;
      this.Setpoint=setpoint;
      this.integral=0;
      this.derivative=0;
      this.previous_error=0;
      this.id=0;
      this.out=0;
      this.N=0; // a counter for the CSV file
    }//end TimeObj
    
    //exports =PIDobj;
    
    

    Adding a method to setup and start the timer

    PIDobj.prototype.start = function() {
      this.N=0;
      var Q = E.openFile(this.PID.Fname, "w");
      Q.write("N,Error,Input,Output,Setpoint\r­");
      Q.close();
      if (this.id) return;
      this.id = setInterval( this.update.bind(this), this.Interval );
    };//end start
    
    

    Add a method to stop the timer

    PIDobj.prototype.stop=function(){
     if (this.id) {
      clearInterval(this.id);
      this.id = null;
      console.log(this.PID.Fname, "Finished");
      console.log(process.memory().usage);
     }//endif this.id
    };//end stop
    
    
  • Continued:
    Add a method to do one iteration of PID control

    PIDobj.prototype.update=function(){
      this.N++;
      var A=this.PID;
      var inp=this.Input();
      this.error=this.Setpoint-inp;
      this.integral+=this.error*this.Interval/­1000;
      if(this.integral<0)this.integral=0;
      if(this.integral>1)this.integral=1;
      this.derivative=(this.error-this.previou­s_error)*1000/this.Interval;
    var b=this.out+A.Dir*(A.P*this.integral+A.I*­this.error+A.D*this.derivative);
      if(b<0)b=0;if(b>1)b=1;
    
     this.Output(b);
     this.out=b;
     console.log(this.N,',',this.error.toFixe­d(4),',',inp.toFixed(4),",",b.toFixed(4)­,",",this.Setpoint.toFixed(4));
     var Q = E.openFile(this.PID.Fname, "a");
     Q.write(this.N,this.error.toFixed(4),inp­.toFixed(4),b.toFixed(4),this.Setpoint.t­oFixed(4)+"\r");
     Q.close();
      this.previous_error=this.error;
    };//end update
    
    

    And finally some code to test it all.

    function input1(){
    //  console.log("in ",analogRead("A1"));
      return analogRead("A1");
    }// end input;
    
    
    function output1(a){
    //  console.log("Out ",a);
      analogWrite('A4',a);
    }//end output
    
    function Testit(){
    //A1.mode('analog');
    //A4.mode('analog');
    output1(0.5); //1.0
      var PID1=new PIDobj(PID,0.5,input1,output1,1000);
      input1();
      PID1.start();
    }//end Testit
    
    Testit();
    
    

    For a simple test on an Espruino board I connected pin A4 to Pin A1.
    Some console output:

    >echo(0);
    =undefined
    1 , -0.0005 , 0.5005 , 0.0000 , 0.5000
    2 , 0.4802 , 0.0198 , 0.1441 , 0.5000
    3 , 0.3562 , 0.1438 , 0.2509 , 0.5000
    4 , 0.2493 , 0.2507 , 0.3652 , 0.5000
    5 , 0.1348 , 0.3652 , 0.4672 , 0.5000
    6 , 0.0322 , 0.4678 , 0.5602 , 0.5000
    7 , -0.0618 , 0.5618 , 0.6384 , 0.5000
    8 , -0.1399 , 0.6399 , 0.6965 , 0.5000
    9 , -0.1978 , 0.6978 , 0.7309 , 0.5000
    10 , -0.2329 , 0.7329 , 0.7409 , 0.5000
    11 , -0.2427 , 0.7427 , 0.7282 , 0.5000
    12 , -0.2297 , 0.7297 , 0.7065 , 0.5000
    13 , -0.2080 , 0.7080 , 0.6879 , 0.5000
    14 , -0.1899 , 0.6899 , 0.6707 , 0.5000
    15 , -0.1719 , 0.6719 , 0.6553 , 0.5000
    16 , -0.1563 , 0.6563 , 0.6412 , 0.5000
    17 , -0.1423 , 0.6423 , 0.6284 , 0.5000
    18 , -0.1291 , 0.6291 , 0.6168 , 0.5000
    19 , -0.1182 , 0.6182 , 0.6061 , 0.5000
    20 , -0.1067 , 0.6067 , 0.5965 , 0.5000
    21 , -0.0986 , 0.5986 , 0.5875 , 0.5000
    22 , -0.0884 , 0.5884 , 0.5797 , 0.5000
    23 , -0.0811 , 0.5811 , 0.5723 , 0.5000
    24 , -0.0739 , 0.5739 , 0.5656 , 0.5000
    25 , -0.0669 , 0.5669 , 0.5596 , 0.5000
    26 , -0.0605 , 0.5605 , 0.5542 , 0.5000
    27 , -0.0552 , 0.5552 , 0.5492 , 0.5000
    28 , -0.0498 , 0.5498 , 0.5448 , 0.5000
    29 , -0.0459 , 0.5459 , 0.5406 , 0.5000
    30 , -0.0415 , 0.5415 , 0.5369 , 0.5000
    
    

    Tuning the P, I, and D values is an art. Please consult Google.
    Things to try:

    1. Use an RC low-pass circuit between the output pin and the input pin. One or many stages of RC filter.
    2. Try setting the analogWrite() to a 50 Hz frequency and the pulse width of 1ms for a zero value and 2ms for a 1 value. Connect this output to a servo and use an accelerometer as the input. Mount the accelerometer on the servo arm and have some fun tilting it back and forth. (my parts are on order).
    3. Setup a temperature sensor and a solid state switch supplying current to a heating element to control an oven temperature.
      How to make input, output and setpoint appear to the user in Engineering units?



    2 Attachments

  • If you're coding PID up from scratch, I highly recommend reading http://brettbeauregard.com/blog/2011/04/­improving-the-beginners-pid-introduction­/ :-)

  • Please find a attached a port of the Arduino pid library to the Espruino. Function names have been shortened, to make it more suitable for this environment. If there is interest, I'll comment the code and submit it as a module.

    In the short term, this can be saved as a local module for testing.

    var PID = require('pid-controller');
    var model = {};
    model.temp = [0, 0];
    model.setpoint = 66;
    model.duty = 0;
    
    var config = {
        mash : {
            "Kc" : 10,
            "Ti" : 2,
            "Td" : 2,
            "setPoint" : model.setpoint,
            cycleTime : 2000
        }
    };
    var ctr = new PID(model.temp[0], config.mash.setPoint, config.mash.Kc, config.mash.Ti, config.mash.Td, PID.fwd);
    ctr.mode(PID.auto);
    ctr.setLimits(0, 100);
    setInterval(function () {
        ctr.compute();
        model.duty = ctr.getOutput();
    }, 200);
    

    1 Attachment

  • Can't seem to attach two attachments


    1 Attachment

  • regarding attaching/uploading of multiple files: I did not figure out how to do it at once, but one after the other work:

    1. enter/edit the post
    2. upload a file
    3. save
    4. more files, go to 1.

    Also with deleting and adding again I had trouble... therefore, after every file upload or delete, save the post and edit it again with a delete or update. (I recall vaguely that deleting more than one file in one edit/save session worked.)

  • Yeah, I'm not sure what's up with the forum there :(

    Tuning the P, I, and D values is an art. Please consult Google.

    :) Yeah... What I'd love is some 'magic' library that could do some tests and guess values - for example in heater-based systems, by turning the output full-on for 1 sec and then off, and then looking at the rise and fall of values on the sensor you should be able to pick out at least some of the values that are needed.

  • The arduino pid library has a self learning module. I've not implemented that yet.depending on what you are heating, it can take some time to raise the temp by a degree. And then if it overshoots it takes quite a bit of time for that degree to drop back to the first level.... In my case I'm heating 18L of water, and with a 3w element it can crank out about 1 deg/min. I think the current P value I have 14.

  • In Windows, when I attach files, I first put them in one directory and then select multiple files at once then send them. Trying to do one at a time requires editing the submitted post.

    Both of the files Wilberforce seem to be the minimized version.

    How does one go about minimizing a module?

    Magic PID tuners exist in the world, there is some variance in PID implementations so one has to use an auto-tune with the matching PID implementation.
    There also so rules of thumb methods that cut down on the iterations of a slow process that are required to achieve acceptable results. Google "PID tuning rules of thumb".
    The most common heater control is the On/Off method. Some diffusion furnaces I worked with in the past have had SCR based control that varies the current to the heater.

    tve thanks for the link. The method I used is based on differentiation the basic equation PID so
    it calculates the change in output.

  • This link lays out three forms of PID control, including the differential form that calculates the change in output. Looking at it my first stab at it from memory needs to be corrected.

    http://bestune.50megs.com/typeABC.htm

  • About minimizing... since my module was simple and small, I selected Closure (online) Simple Minification , placed module into sandbox modules folder, made a one-liner in edit pane that requires the module, uploaded that one-liner to th3 board, 'pullled it back' in the console pane using arrow up.

    What you can see is the string that the Web IDE sent to the Espruino, something like this: Modules.addCached("MyModule","var F=function(x){return x*x+\" square\";}" see Modules.addChached(...); I copied the string, trimmed it, and replaced the \" with plain "...

    I'm sure there are smarter and more elegant ways to do that... but it did what it had to do without figuring out a single command.

    It would be nice the Web IDE had a setting that would write the minimized code back.... (I would not mind an extra user prompt for the times it writes back. With leaving a timestamp with thee minified file, an auto-detect for change... (if the Web IDE can read the file without user interaction).

  • On Windows I use notepad++. It has a js plugin, that has a js-format (pretty print) and a js-minifier.

    I'll attach the js-formatted version tomorrow.

  • Thanks for the information.
    I've reworked my code and been doing some reference work on the various flavors of PID control.
    Seems there are two broad categories dependent and independent, which effect how the PID gains are defined. Then there are position and velocity versions of the basic equation.
    So far the best link to explain this is:
    http://literature.rockwellautomation.com­/idc/groups/literature/documents/wp/logi­x-wp008_-en-p.pdf
    For testing so far I've used two different RC low pass filters R=100k, C=10uF and the other one R=100k C=50uF.
    The tuning method depends on the flavor of PID being used. Some head scratching and Google searching can figure or turn up conversions of the PID values between the flavors. You see lots of examples in various blogs

    I'm working on an independent velocity equation based PID algorithm. I'm going to check using the Arduino link that Wilberforce provided to determine the flavor of that PID.

  • The attachment here has been updated to the human readable form:

    #4 http://forum.espruino.com/comments/12972­573/

    The module is a port of the reference that @tve mentioned above. This an adaption of the node version here: https://github.com/wilberforce/pid-contr­oller made suitable for the Espurino.

  • Thanks that helps a lot.
    I've been working on the velocity version of PID and here are some results so far.

    Deriving the velocity PID equations from the position PID equations.
    I used Maxima (download Maxima and open PID.wxm)
    Download Maxima from:
    http://maxima.sourceforge.net/
    Starting at the top line, use the mouse to select the bracket on the left and press control-enter. This will make the line evaluate. Use the down arrow to go to the next line and repeat.
    The file PIDobj3.js is the code to do the velocity PID.
    Test were conducted using and R-C filter between the analog output and the analog input. R=100k, C=10uF
    Input, output and setpoint are scaled to be in the range 0.0 to 1.0.
    To make the velocity form work there has to be integral gain in order to get a setpoint change into the math.
    The initial setpoint is 0.5 and is changed to 0.6 at some point to test the response.
    The ZZZn.csv files contain several combinations of PID tunings.
    Adding proportional gain improves the response but introduces “droop” error. It doesn’t hit the setpoint. Tradition is to add integral gain to remove the droop.
    Puzzling over this I noticed that if the setpoint is zero the “droop” error disappears.
    My first attempt was to modify the PID internals to operate between -0.5 an 0.5. This showed some improvement. Then it occurred to me to shift the PID internals so that the internal setpoint is always zero so that “droop” is eliminated.
    Caution beyond this point there be dragons!
    PIDobj6.js contains the code.
    YYY3.csv contains the output for P=1.0, I=0.1 D=0.0 and
    YYY4.csv for P=1.0, I=0.0, D=0.0.


    8 Attachments

  • Trying out the code Wilberforce posted using the low pass filter as a test process.
    CSV files are runs with different tuning parameters.

    var PID = require('pid-controller');
    
    var setpoint = 0.5;
    var duty = 0;
    var N=0;
    var Kc= 1.5;
    var Ti= 2.0;
    var Td= 0.0;
    var setPoint=0.5;
    var cycleTime= 1000;
    
    var ctr = new PID(0, setPoint, Kc,Ti, Td, PID.fwd);
    ctr.mode(PID.auto);
    ctr.setLimits(-0.5, 0.5);
    ctr.out=0.0;
    analogWrite("A4",ctr.sp);
    console.log("N,error,input,output,setpoi­nt");
    var I=setInterval(function () {
      N++;
      if(N>20)ctr.setpoint(0.6);
      if(N>50)clearInterval(I);
    ctr.inp=analogRead("A1");  
        ctr.compute();
    analogWrite("A4",ctr.out+0.5);
    console.log(""+N+","+(ctr.sp-ctr.inp).to­Fixed(4)+","+
                ctr.inp.toFixed(4)+","+
                (ctr.out+0.5).toFixed(4)+","+ctr.sp.toFi­xed(4));
    }, 1000);
    
    

    5 Attachments

  • Great to see you testing the code.

    You really should not be setting internal parts of the library - ctr.out=0.0;

    At this point - you could call ctr.compute(); if you have set a value for input. There is a ctr.setOutput(x) - however this is intended to be used if the controller in manual mode.

    Same goes for getting the output ctr.getOutput(). If the internal implementation of the library was changed and internal vars renamed (i.e. perhaps shorted to save space), your code could break...

  • @ClearMemory041063, do you have actually something concrete running where you apply these PID control object? If so, you can use the Web IDE built in testing to visualize in a graph what is going on. It's a pretty neat feature of the Web IDE. I would not call it necessarily testing, but something more like monitoring.

    It allows the sampling with custom sampling rate of practically any value from connected Espruino and includes also optional recording.

  • @allObjects - that's a great idea - I had actually opened the cvs in excel and plotted there... Much easier to do within the web ide in realtime!

  • You still can do the cvs in excel / google spreadsheet/docs 'thing' because it has more options, but it is 'fun' to watch it real-time. The recording option gives you the best of both worlds.

  • @allObjects the testing part of WebIDE is a cool thing to use. I gave it a try with @Wilberforce's code and after fiddling with the testing buttons managed to get it to plot a graph of the process.

    Seems in my haste to try @Wilberforce's module, I violated the rule of object access. Use methods to read/write object properties. Not a big thing if you are just fooling around trying to understand a piece of code, but a big deal if you build with it because a rewrite of the module can break your code. I'm wondering if there is a method to enforce that when using the keyword "export"? C++ classes have private and public keywords that can be applied to class properties.

    My testing so far has been using an analog output connected by a 100k resistor to a 10 uF capacitor to ground, and the resistor-capacitor junction connected to an analog input. The other option would be to dig out some chemE textbooks and write some code to simulate a process. My head still hurts thinking about simulating the control of a 20 plate fractioning column with three different control loops on a 286 computer. So slow I had to buy a math coprocessor and even then it was slow. Such things are a lot faster today.

  • There are ways in Javascript to have private members - state and behavior | values and functions/methods - but I'm not seeing it done often. Some minimal discipline in following a few rules is good enough to mitigate the (worst) issues. No langue - no matter how strongly or inferred or even static ...typed and compiled compensates for ingnorance in the problem space... After acquiring a decent understanding what is going on and sorting the things out in the mind, a few formal rules consistently applied foster an equally decent implementation. These are a few rules I stick with... but as you know: Rules can only exist when Exceptions to the Rule(s) exist... ;-)...

    1. CamelCase and camelCase notation for all things (except for constants).
    2. Constants all UPPER_CASE and 'mimicking camelCasing' with UNDER_SCORES.
    3. Class names (which in Javascript are constructor functions are always starting with an UpperCase.
    4. Members - state or behavior - start with lowerCase, and - if you want to pay attention to public and private, prefix it them with single underscore (_).
    5. Above code uses private members - state (_inquiries) and behavior (_getXyz() ) to show what is all accessible from the outside and what should not be used for various reasons (hide implementation, allow change under the hood),...
    6. Since there is only a single name space - the global name space - available, keeping as little as possible globals is very helpful. Therefore, not even functions should freely float around: they belong to a function library, which is like a singleton, sometimes named xyzUtils or just utils. It is though a good thing to be somewhat specific what the utility library is about.

    var Person = function(first,last,birthDate) { // constructor expects String, String, Date
        this.first = first;
        this.last = last;
        this.birthDate = birthDate;
        this._inquiries = { ageInYears: 0, inquiries: 0 };
    }
    Person.prototype.getFullName = function() {
        return this.first + " " + this.last;
    }
    Person.prototype.getAgeInYears = function() {
        this._updateInquiries("ageInYears");
        return this._getAgeInYears();
    }
    Person.prototype.getNumberOfAgeInquiries­ = function() {
        this._updateInquiries("inquiries");
        return this._inquiries.ageInYears;
    }
    Person.prototype.getSumOfAllInquiries = function() {
        this._updateInquiries("inquiries");
        return this._getSumOfInquiries();
    }
    Person prototype.getYearlyAverageOfInquiries = function() {
        return this.getSumOfAllInquiries() / this._getAgeInYears();
    }
    Person.prototype._updateInquiries = function(fact) {
        this._inquiries[fact]++;
    }
    Person.prototype._getAgeInYears = function() {
        return new Date().getFullYear() - this.birthDate().getFullYear();
    }
    Person.prototype._getSumOfInquiries = function() {
        this._updateInquiries("inquiries");
        var sum = 0;
        for (var fact in this._inquiries) { sum += this._inquiries[fact]; }
        return sum;
    }
    
    var personUtils =
    { utilityFnctA: function(person,someOtherThing,andAThird­Thing) {
            // do what it has to do
        }
    , utilityFnctB: function(person,someXThing,someYThing) {
            // do what it has to do
        }
    };
    

    Since JavaScript by nature has closures, various frameworks have popped-up to overcome name space issues in order to mix and match components from various - non-synchronized - providers. One very useful library is node.js' require("module(Path)Name") and requirejs' require(["modulePathName1","modulePathNa­me2",...],function(module1,module2,...){­ ... };. The first one is more server-side oriented, and latter more client - Browser - side oriented. Both keep a cache to load a thing only once. The second goes though farther in functionality: first is usually synchronous, where the second one is usually asynchronous and handles the load of all modules like a promise and enters the anonymous function only after all modules have been loaded. In a 'small' world like an mc, the first one works just good enough.

    .

  • Hello @ClearMemory041063, @Wilberforce

    I am using pid-controller and mqtt modules for our project, now.
    We use Wemos D1 Mini.
    I am trying the example simulation of the github @Wilberforce, it is working, normally.
    However, if we combine the pid-controller and mqtt modules, when we
    try to connect to mqtt broker, there is an error.

    Execution Interrupted during event processing.
    New interpreter error: LOW_MEMORY,MEMORY
    

    Is there any issue for compatibility of pid-controller and mqtt module ?

    Regards,
    Maman

  • It looks like you are using an esp8266 - and this has very limited memory - you could try the save on send option in the ide, and this might out put the code into flash, saving more ram space.

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

PID Control Object (with questions)

Posted by Avatar for ClearMemory041063 @ClearMemory041063

Actions