Trouble with ArduPID.h library

I've been using successfully the old PID Library of Brett, but it does not work anymore with the current IDE. If someone wants to fix it it would be great,

So Im trying the new library mentioned above.
I am having a bi-directional situation where the robot has to balance leaning fwd or back.
Feeding negative values to the PID was a mess, so I went absolute numbers.
The results are not right. The higher the input the lower the output , Opposite from what it should be....
Here is the code, please help,

#include "ArduPID.h"


const byte numChars = 32;
char receivedChars[numChars];   // an array to store the received data
double X1_Latest, X2_Latest;
boolean newData = false;

ArduPID myController;

unsigned long PrintTimer = 0;



double input;
double output;

// Arbitrary setpoint and gains - adjust these as fit for your project:
double setpoint = 0;
double p = 1;
double i = 0.3;
double d = 0.5;

// Motor Controls
#define LeanFwd 5 // Yellow Wire, PWM will lean WallE forward
#define LeanBck 3 // Orange Wire, PWM will lean WallE Back


void setup()
{
  Serial.begin(115200);
  Serial3.begin(115200);

  myController.begin(&input, &output, &setpoint, p, i, d);

 // myController.reverse() ;              // Uncomment if controller output is "reversed"
  // myController.setSampleTime(10);      // OPTIONAL - will ensure at least 10ms have past between successful compute() calls
  myController.setOutputLimits(0, 255);
  myController.setBias(255.0 / 2.0);
//  myController.setWindUpLimits(-10, 10); // Groth bounds for the integral term to prevent integral wind-up

  myController.start();
  // myController.reset();               // Used for resetting the I and D terms - only use this if you know what you're doing
  // myController.stop();                // Turn off the PID controller (compute() will not do anything until start() is called)
   Serial.println("BALANCE_PID_RXTX");
}




void loop()
{ recvWithEndMarker();
  showNewData();

  input = abs(X1_Latest - 1500) /4.00; // Replace with sensor feedback

  myController.compute();
  if (input > 0)
  { //analogWrite(LeanFwd, abs(output)); // Replace with plant control signal
  }
  else
  { //analogWrite(LeanBck, abs(output)); // Replace with plant control signal
  }




  if (millis() - PrintTimer >= 200)
  { PrintTimer = millis();

    //Serial.print("  X1 Latest: ");
    //Serial.print(X1_Latest);
    Serial.println("  input: "+ String(input) + " Out:" + output);
  }

} //end of loop
1 Like

The discontinuity that abs() makes at 0 can cause many problems.

For instance, if you have the discontinuity at zero and drive the direction with the sign of the input, and need to drive forward hard to approach balance, as soon as you hit balance with input==0, you'll immediately swap the output direction and drive hard back the way you came. If you have any Ki integrator accumulation, it will push in the wrong direction until it winds down.

It is better to make the PID inputs and outputs smooth across the normal operating point at zero and make the actuator contingent on the sign of the (also smooth) signed output:

  myController.compute();
  if (output > 0)
  { //analogWrite(LeanFwd, abs(output)); // Replace with plant control signal
//analogWrite(LeanBck, 0);
  }
  else
  { //analogWrite(LeanBck, abs(output)); // Replace with plant control signal
//analogWrite(LeanFwd, 0); 
  }

BTW, Brett's library seems to work in my the Arduino 2.2.1 IDE. ( I have a quibble with it's integrator windup, but it functions.) Did you install it from Github or the library manager in the IDE?

1 Like

Thanks for the advice. I re-installed Brett's PID and now it works. YEY

I will stick with it.
Since you get PID, what is your favorite library?

Brett's is pretty good. I modified my copy a little bit to expose a couple private features:

With read/write access to PID.outputSum one can do better diagnosis and interesting integral windup fixes as needed.

Dave,
I am using the Brett PID successfully, thanks of the improvement.
One issue is that I can't figure:
Setpoint is declared in setup.
I put it in the loop and tried to vary from a remote, nothing happens.
Any ideas?

Show your current code?

The PID object, input, output, and setpoint are normally declared globally, and then configured in setup. If you are declaring 'Setpoint' dynamically in setup(){} or loop(){}, weird things could happen when/after the function is finished.

I am making a wall-e robot. This algorithm will deal with the pitch angle of the body.

I kinda figured the problem. "Posture" is the value that will make it lean fwd in case of acceleration and back in case of decceleration.
I am adding "Posture" to the pitch angle and I am obtaining different attitudes.
The other way was to change the set point to = posture. Meaning to shoot for values other than 0.
Putting Setpoint in the loop didn't go well...

Any words of guidance are appreciated.

//Balancing system with emergency stop
// Harware for connecting to the Arduino Nano processor
int Pitch;
int Direction;
int Roll;
// stepper motor pins
int RPWM_LeanFwd = 5; // Yellow Wire, PWM will lean WallE forward
int LPWM_LeanBck = 3; // Orange Wire, PWM will lean WallE Back
int MarchReading; //This is the value of the remote control obtain By Serial1 port from Tracks Arduino
// switch limit pins and values (emergency stops the stepper at the phisical limit)

const int EmergencyPin = 13; //Will go high when the Pitch is over 300 in any direction OR when the roll is more than 1000
// Will connect to the tracks Processor and stop the motors.
const int StaticPin = 14; // Will recieve binary value from Tracks processor. When WallE is not moving is high and moving is LOW.
// Will vary the PID settings to low when not in motion and aggressive when in motion
unsigned long PrintTimer = 0;
unsigned long AvgTimer = 0;
#include <PID_v1.h>
#include "GY521.h"
#include <Average.h>
GY521 sensor(0x68);
double Setpoint, Input, Output;
double Kp, Ki, Kd;
PID TiltPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, REVERSE); //Kp Ki Kd
float x; // this is the angle of Pitch
float ThePitch; // this is the angle stabilized by avg
Average<float> ave_X(100);
float Posture =0;


/////Serial3Receive
const byte numChars = 32;
char receivedChars[numChars];   // an array to store the received data
int X1_Latest, X2_Latest;
boolean newData = false;

void setup() {

  Serial.println(__FILE__);

  pinMode(EmergencyPin, OUTPUT);
  pinMode(StaticPin, INPUT_PULLUP);

  Wire.begin();


  Serial.begin(115200);
  Serial3.begin(115200); // second serial that sends ThePitch to the Tracks arduino
  Wire.begin();
  delay(100);
  while (sensor.wakeup() == false)
  {
    Serial.print(millis());
    Serial.println("\tCould not connect to GY521");
    delay(1000);
  }
  sensor.setAccelSensitivity(.002);  //  8g
  sensor.setGyroSensitivity(.0010);   //  500 degrees/s

  sensor.setThrottle();
  Serial.println("start...");

  //  set calibration values from calibration sketch.
  sensor.axe = 0.574;
  sensor.aye = -0.002;
  sensor.aze = -1.043;
  sensor.gxe = 10.702;
  sensor.gye = -6.436;
  sensor.gze = -0.676;
  Setpoint = 0;
  //  TiltPID.SetMode(AUTOMATIC);
}



void loop() {

  
  recvWithEndMarker();
    showNewData();

  Posture = (1500- X1_Latest) / 20.00;   
  //Setpoint =Posture;
  int  BalanceBiasVal = (((analogRead(A0)) - 500) / 1.5);
  float KpMap = map(analogRead(A1), 0, 1024, 400, 10);
  Kp = (KpMap / 100);


  if (millis() - AvgTimer >= 5)
  { AvgTimer = millis();
    sensor.read();
    x = sensor.getAngleX();
    ave_X.push(x);
    ThePitch = ave_X.mean() + Posture;

    //float y = sensor.getAngleY();
    //float z = sensor.getAngleZ();
    /*
    Serial.print(30);
    Serial.print('\t');
    Serial.print(-30);
    Serial.print('\t');
    Serial.print(x, 1);
    Serial.print('\t');
    Serial.println(ThePitch);

    //  Serial.print(y, 1);
    //Serial.print('\t');
    //Serial.print(z, 1);
    Serial.println();
    */
  }
  if (millis() - PrintTimer >= 100)
  { PrintTimer = millis();
   Serial.println(String (ThePitch) +  "   Input:  "+String (Input) + "  Out: " + String(Output));
   Serial.println("POSTURE:  "+ String (Posture));
  }

  Input = abs (ThePitch*5 );//+ Posture;
int MAX_FWD=45; //maximum forward 
int MAX_BCK=-45; 

  if ( ThePitch <= MAX_BCK) // lean back too far
  { analogWrite(LPWM_LeanBck, 0);
    analogWrite(RPWM_LeanFwd, 0);
    Output = 0;
    delay (2000);
  }
  else if ( ThePitch >= MAX_FWD) // lean Fwd too far
  { analogWrite(LPWM_LeanBck, 0);
    analogWrite(RPWM_LeanFwd, 0);
    Output = 0;
    delay (2000);
    //  analogWrite(RPWM_LeanFwd, 0);
    //  analogWrite(LPWM_LeanBck, 200);
    //  delay (1000);
  }
  
  else if ( ThePitch > 2 && ThePitch < MAX_FWD) {

    TiltPID.SetTunings (Kp, 2, .5);
    TiltPID.SetMode(AUTOMATIC);
    TiltPID.Compute();
    analogWrite(RPWM_LeanFwd, 0);
    analogWrite(LPWM_LeanBck, Output);
  }

  else if (ThePitch <= -2 && ThePitch > MAX_BCK) {

    TiltPID.SetTunings (Kp, 2, .5);
    TiltPID.SetMode(AUTOMATIC);
    TiltPID.Compute();
    analogWrite(LPWM_LeanBck, 0);
    analogWrite(RPWM_LeanFwd, Output);
  }

  else  {
    Output = 0; /// do nothing zone
    TiltPID.SetTunings (0, 0, 0);
    TiltPID.SetMode(MANUAL);
    analogWrite(LPWM_LeanBck, 0);
    analogWrite(RPWM_LeanFwd, 0);
  }


  // */



Looks like the inputs are:

  • accelerometer
  • A0
  • A1

and the outputs:

  • RPWM_LeanFwd PWM
  • LPWM_LeanBck PWM

It is unclear what you intend the outputs to do based on the inputs.

if you were attempting to drive it manually, with a potentiometer on analogRead(A2), for example, what would you want RPWM_LeanFwd and LPWM_LeanBck to do?

Dave,
Sorry for the long absence, Ive been working on a new situation. A bidirectional PID based on a rotary angle encoder as an input. The drive is a DC motor controller but the PWM is used in reverse where 255 means on current and the max power is zero.

I am getting great results when I use it CW or the other way. The problem is that I need a dead zone where my device just sits. Instead of that the motor is shivering as if it is getting intermittent power.
Here is the code and a video;

PID L_ArmPID(&Input_L_Arm, &Output_L_Arm, &Setpoint_L_Arm, 1, .5, .1, REVERSE);[PID Dead zone trouble](https://machine-cycle.us/video-examples)

  Deg_L = convertRawAngleToDegrees(as5600_L.rawAngle());
  Deg_L = 360 - Deg_L; //This will make the degrees increase as I go CW. (The AS5600 reads it backward)
  Deg_L = constrain (Deg_L, 0, 350);

 Setpoint_L_Arm = analogRead(0) / 3.00;
 

  if ( Deg_L < Setpoint_L_Arm - 3 ) {
   Input_L_Arm = Deg_L;
 
    L_ArmPID.SetTunings (1, .5, .1);
    L_ArmPID.SetMode(AUTOMATIC);
    L_ArmPID.Compute();
    analogWrite (Left_PWM_ARM_UP,  Output_L_Arm); // to the brushed motor controller
    digitalWrite (Left_PWM_ARM_DN, HIGH);
    PID_MODE_INDEX=0;
    // Serial.println ("Running CW");
  }
  else if ( Deg_L > Setpoint_L_Arm + 3 ) {
Input_L_Arm = Deg_L-Setpoint_L_Arm;
 // Input_L_Arm = abs (Deg_L - Setpoint_L_Arm);
    L_ArmPID.SetTunings (1, .5, .1);
    L_ArmPID.SetMode(AUTOMATIC);
    L_ArmPID.Compute();
    analogWrite (Left_PWM_ARM_DN,  Output_L_Arm); // to the other direction of the controller
    digitalWrite (Left_PWM_ARM_UP, HIGH);
   PID_MODE_INDEX=1;
    //Serial.println ("Run CCW");
  }
  else//This is do nothing
  {

  Input_L_Arm = Setpoint_L_Arm ;
    L_ArmPID.SetTunings (0, 0, 0);
    L_ArmPID.SetMode(MANUAL);
   //L_ArmPID.Compute();
    Output_L_Arm = 0; /// do nothing zone
    analogWrite (Left_PWM_ARM_UP, 255);//This is do nothing
    analogWrite (Left_PWM_ARM_DN, 255);
    PID_MODE_INDEX=2;
    // Serial.println("Do Nothing");

  }

AS5600_position_M_04_WithPID.ino (3.5 KB)

My first impulse would be to not have a moving discontinuity at error = +/-3 but I'm confused about your inputs and outputs.

"on" means "no"?

What sets the A0? Is that something attached to the arm? Or is it a user input and completely disconnected from the arm?

Where is 0° on the Deg_L scale? Is it horizontal or vertical? up or down?

If you set Setpoint_L_Arm to correspond to 02:00 on a clock face, would the system need to produce a certain PWM to counteract the force of gravity on the arm and hold the arm in place?

If you had two potentiometers attached to A4 and A5 and tried to drive it manually with:

void loop(void){
   analogWrite(Left_PWM_ARM_UP,analogRead(A4)/4);
   analogWrite(Left_PWM_ARM_DN,analogRead(A5)/4);
}

... how would you physically need to move the two pots to do the right thing?

Dave
Thanks for bearing with me, this project is tough, not too many folks do PID. I could not find one successful example of a bi-directional one.

Now the answers.
The arm 0° is at 7 o'clock on a watch. As it moves up the degrees increase. 1 o'clock is 180 deg.
To move it in this direction I have to write PWM on pin Left_PWM_ARM_UP. If I write 0, it is full power , if I write 255 it is no power. That is how the controller hardware works.

If I just want to do the stuff above the PID is working wonderfully , it stops on time etc.
Gravity is no factor, there is lots of friction in the motor gears to keep the arm in any place.

About the Setpoint , I was trying to vary it with the pot, but is noisy . So I can send values from the keyboard so it is steady. The final goal is send different Setpoints and the arm to go there and stay put with a dead zone. (both directions)
explanation to the motor control
analogWrite(Left_PWM_ARM_UP,analogRead(A4)/4); Will move the arm CW, degrees are increasing. If I write 0 is full power. 125 half power, 255= no power. (The other pin must be written high)

analogWrite(Left_PWM_ARM_DN,analogRead(A5)/4); Will move the arm CCW, degrees are Decreasing. If I write 0 is full power. 125 half power, 255= no power. (The other pin must be written high)

Thanks for the answers. The last couple explain that you would probably have to do some complicated fiddling to turn the two knobs to manually get to the position.

Treating a bi-directional system as two separate PIDs with a discontinuity doesn't work because of the discontinuity. It is better to think of it as the bi-directional output is a continuous across zero. Quadcopter drones handle bi-directional +/- thrust with a single continuous PID, where it's +/- 100% thrust output is post-processed into the bidrectional motor signals.

For your application If run a PID to translate +/- position error into +/-100% CW_turning force, and then post-process the turning force into the CCW, CW and deadband cases.

In the manual knob-turning case, you could have one dial for +/- clockwise force and use that output to make the appropriate settings of the outputs:

float CWforce = (analogRead(A3)-512)/5.12;  // gives -100/+100

This would be easier to control manually, since you could let the computer calculate the appropriate tradeoffs and safe settings between the two motors:

// translate +/-100% PID output into actuator settings.
if (CWForce > 10 ){
   analogWrite(Left_PWM_ARM_DN,255);
   analogWrite(Left_PWM_ARM_UP,map(CWForce, 10,100,0,255));
} 
else if (CWForce < -10) {
   analogWrite(Left_PWM_ARM_DN,map(CWForce,-10,-100,0,255));
   analogWrite(Left_PWM_ARM_UP,255);
} else {
   analogWrite(Left_PWM_ARM_DN,255);
   analogWrite(Left_PWM_ARM_UP,255);
}

Then you replace the manual knob with the PID control the CWForce to +/-100 (using PID.SetOutputLimits(-100,100);) per

Here's a manual sketch & simulation:

image
https://wokwi.com/projects/384118296785140737

// for https://forum.arduino.cc/t/trouble-with-ardupid-h-library/1189957/11

const byte LHS = 11;
const byte RHS = 10;
const int KnobPin = A3;
int lastKnob ;
int thresholdL = -10;
int thresholdR = 10;

void setup() {
  pinMode(LHS, OUTPUT);
  pinMode(RHS, OUTPUT);
  lastKnob = -1; // make sure it adjusts // analogRead(KnobPin)-512;
  Serial.begin(115200);
}

void loop() {
  int knob = analogRead(KnobPin)-512;
  if(knob != lastKnob){ //change

    if(knob <= thresholdL){
      int offset = map(knob,-512,thresholdL,0,255);
      analogWrite(LHS, offset);
      analogWrite(RHS, 255);
   //   Serial.print(offset);
    } else if (knob >= thresholdR) {
      int offset = map(knob,thresholdR,511,255,0);
      analogWrite(LHS, 255);
      analogWrite(RHS, offset);
   //    Serial.print(offset);
    } else {
      analogWrite(LHS, 255);
      analogWrite(RHS, 255);
      Serial.print('.');
    }
    Serial.print(knob > lastKnob? "^": "v");
    lastKnob = knob;
  }
}
1 Like

Everything you say makes sense. I hated having 2 PID calculations and etc...

The problem I had so far is that I can not Feed negative numbers in the input of the PID>
The examples you gave me are great I getting to work right now.

If your system has negative numbers, you should be able to feed them in as negative numbers into the PID as either Setpoint or Input, but the PID library won't emit negative numbers unless you SetOutputLimits(?,?) to allow negative outputs.

Odd setups of a PID are much easier to wrap your head around if you put ranges and units on Input, Setpoint, Output, (and the actuators) If something gets confused in units or direction, or speed vs position, its easy to screw it up. The best way I've thought of to unscramble it is to think of yourself being the PID and adjusting the Output knob manually.

1 Like

This seems to work, Im imputing new set points by keyboard to avoid noise.

Still testing. Some Setpoints result in a continuous turn. 180 deg is reliable. But if I go close to the end of the circle, it passes through and keeps turning

  while (Serial.available() > 0) {

    // look for the next valid integer in the incoming serial stream:
    int NewNumber = Serial.parseInt();
 //Serial.println ("new Input  " + String(NewNumber));

    // look for the newline. That's the end of your sentence:
    if (Serial.read() == '\n') {
      // constrain the values to 0 - 255 and invert
      // if you're using a common-cathode LED, just use "constrain(color, 0, 255);"
      NewNumber = constrain(NewNumber, 0, 350);
      Serial.println ("Setpoint_L_Arm  " + String(NewNumber));
       Setpoint_L_Arm = NewNumber; 

    }
  }

  Deg_L = convertRawAngleToDegrees(as5600_L.rawAngle());
  //Deg_L = 360 - Deg_L; //This will make the degrees increase as I go CW. (The AS5600 reads it backward)
  // Deg_L = constrain (Deg_L, 0, 350);
  //  update every 100 ms
  //  should be enough up to ~200 RPM
 

  Input_L_Arm = Deg_L;
  L_ArmPID.SetTunings (3, 2, .4);
  L_ArmPID.Compute();
  L_ArmPID.SetMode(AUTOMATIC);
 
    if (Output_L_Arm < -10)
    { 
      analogWrite(Left_PWM_ARM_UP, abs (255+Output_L_Arm));
      analogWrite(Left_PWM_ARM_DN, 255);
    }
  
    else if (Output_L_Arm > 10)
    { 
      analogWrite(Left_PWM_ARM_DN, 255-Output_L_Arm);
      analogWrite(Left_PWM_ARM_UP, 255);
    }
    else
    {
      analogWrite(Left_PWM_ARM_DN, 255);
      analogWrite(Left_PWM_ARM_UP, 255);
    }

I replaced the keyboard input with analog read. Works much better.
The Kbd input was crashing the system.

Setpoint_L_Arm = analogRead(0) / 3.00;
Setpoint_L_Arm = constrain(Setpoint_L_Arm, 15,350);

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.