What is the best way to control a DC motor with an Absolute Encoder, PID?

Hello All,

I'm trying to tackle a problem where I need to control a two axis antenna positioner.

I would like to use an Arduino Mega with a Pololu dual VNH5019 motor driver (https://www.pololu.com/product/2507) to drive the two DC brushed motors. On both, the elevation and azimuth axis will be a Hengstler 16 bit Absolute Optical Encoder to measure the angular position of the shafts.

So far I can issue commands to drive the motors at fixed speeds etc, and I can reliably read angular data from the encoders.

My next problem is to determine what is the best method of controlling the motors so that I can send commands for various sweeps or sector scans so that either axis can sweep back and forth between specified angles, or to send a command to make the positioner face a specific angle. Is this something the servo or PID library can do?

Up to now I have written functions to obtain the azimuth and elevation angles and to store that value as a variable to be used by a control algorithm. I just don't know what kind of algorithm to use that the Arduino can handle.

I have searched the forum and Google and find mostly info regarding hobby servos with pots, not much regarding using angle as the feedback value.

I would very much appreciate it if anyone has experience with this or can point me in the right direction. Thank you.

Paul

1 Like

PID is intended for this purpose, so try the Arduino PID libary.

Hint, start with just the P term, that doesn't even need the PID library, and get the
sign and rough magntide of the P term right, expect some overshoot, but not prolonged
oscillation.

Then move over to PID library and use just the P term and get the same behaviour (baby
steps, always).

Then add a D term and tune this to stop over-shoot, perhaps increase P and retune D for a quicker
overshoot-free response a couple of times. Ultimately too much D means noise (jitter in
the output) as it amplifies system noise.

Finally add a little I term. It you are mainly doing step transitions you will need to
deal with integral wind-up in some fashion, or you won't get sensible behaviour. The easiest
way is to limit the absolute value of the integral value. The I-term is only of use when
close to the target value anyway, which won't be the case for big step transitions.

The other way is to slew-rate limit the command input to the PID so that integral windup
isn't allowed to build up (the loop is always locked on).

Thank you for the tips. I'll give it a try.

I am trying to use the PID library with the angular feedback from the absolute encoders which I can now read data from.

I am using a Pololu Dual VNH5019 motor controller shield which I have used without PID before. I have the code to read from two optical encoders but have implemented only one PID to begin with.

Here is the problem, using some arbitary angle as the setpoint, when the program is started it just makes the motor run at the maximum speed in the reverse direction. The Pololu shield takes a speed value from -400 to 400. Negative numbers are reverse direction.

As I understand it, the PID outputs 0 to 255, is that correct? In that case the motor can only run forward? I'm not certain this is the case.

Would anyone more experienced be able to shed light on this? I'd like the PID to output between -400 and 400, if this is possible.

Here is the code

/*  
    Code to read data from two Hengstler AC58 16 bit SSI Encoders
    and to provide basic motor control with the Dual VNH5019 Shield. 
   *** Only one PID set up so far!***
    Written in Arduino 1.6.5 for Mega
    12/08/2015 
*/
#include "DualVNH5019MotorShieldMega.h"
#include <PID_v1.h>

DualVNH5019MotorShield md;

double AZspeed=0;
double angleAZsetpoint=0;
double kpAZ=10, kiAZ=0, kdAZ=0;

const int CLOCK_PIN_AZ = 50;     // Azimuth Encoder Clock Pin 
const int DATA_PIN_AZ = 51;      // Azimuth Encoder Data Pin
const int CLOCK_PIN_EL = 52;     // Elevation Encoder Clock Pin 
const int DATA_PIN_EL = 53;      // Elevation` Encoder Data Pin

const int BIT_COUNT = 16;       //for 16 bit encoder

unsigned int readingAZ = 0;     // Initialising variables to store reading and angles 
double angleAZ = 0;
float previousreadingAZ=0;
float previousreadingEL=0;
unsigned int readingEL = 0;
double angleEL = 0;
boolean directionAZ = false;
boolean directionEL = false;

PID AZPID(&angleAZ, &AZspeed, &angleAZsetpoint, kpAZ, kiAZ, kdAZ, DIRECT);

void setup() {
 
 pinMode(DATA_PIN_AZ, INPUT);
 pinMode(CLOCK_PIN_AZ, OUTPUT);
 pinMode(DATA_PIN_EL, INPUT);
 pinMode(CLOCK_PIN_EL, OUTPUT);
 
 digitalWrite(CLOCK_PIN_AZ, HIGH);  //Initialising both encoder clock pins to High
 digitalWrite(CLOCK_PIN_EL, HIGH);

md.init();                       //Initialise Motor Driver
AZPID.SetMode(AUTOMATIC);
AZPID.SetSampleTime(200);
AZPID.SetOutputLimits(-400,400);
 angleAZsetpoint=50;

 Serial.begin(19200);             // Start serial comms.  This will be replaced with Ethernet later

}

void loop() //Main loop to print angle to serial port every XXXms.
{

readingAZ = readPositionAZ();                      //Get value from encoder
angleAZ = ((float)readingAZ / 65536.0) * 360.0;    //Make that value in range of 0-360 degrees
AZPID.Compute();                                   //Perform PID calculation 
md.setM1Speed(AZspeed);                            //Set motor speed according to PID calculation
Serial.print(AZspeed);                             //Check to see updated motor speed and angle
Serial.print("   ");
Serial.println(angleAZ);

}


//read the current angular position from Azimuth Encoder
unsigned long readPositionAZ() {
  unsigned long sample1 = shiftInAZ(DATA_PIN_AZ, CLOCK_PIN_AZ, BIT_COUNT);
  delayMicroseconds(25);  // Clock must be high for 25 microseconds before a new sample can be taken
  return sample1;
  }
  
//read the current angular position from Elevation Encoder
unsigned long readPositionEL() {
  unsigned long sample1 = shiftInEL(DATA_PIN_EL, CLOCK_PIN_EL, BIT_COUNT);
  delayMicroseconds(25);  // Clock must be high for 25 microseconds before a new sample can be taken
  return sample1;
  }  

//read in a byte of data from the digital input of the Azimuth Encoder.
unsigned long shiftInAZ(const int DATA_PIN_AZ, const int CLOCK_PIN_AZ, const int bit_count) {
  unsigned long data = 0;

  noInterrupts();  //Switch off interrupts so that clocking in of data is not interuupted
  
  for (int i=0; i<bit_count; i++) { //reads first bits
    data <<= 1;
    digitalWrite(CLOCK_PIN_AZ,LOW);
    delayMicroseconds(1);
    digitalWrite(CLOCK_PIN_AZ,HIGH);
    data |= digitalRead(DATA_PIN_AZ);
  }
  
  interrupts();
  
  digitalWrite(CLOCK_PIN_AZ,LOW); //clocks space bit
  delayMicroseconds(1);
  digitalWrite(CLOCK_PIN_AZ,HIGH);
  delayMicroseconds(1);      

  return data;   
}

//read in a byte of data from the digital input of the Elevation Encoder.
unsigned long shiftInEL(const int DATA_PIN_EL, const int CLOCK_PIN_EL, const int bit_count) {
  unsigned long data = 0;

  noInterrupts();  //Switch off interrupts so that clocking in of data is not interuupted
  
  for (int i=0; i<bit_count; i++) { //reads first bits
    data <<= 1;
    digitalWrite(CLOCK_PIN_EL,LOW);
    delayMicroseconds(1);
    digitalWrite(CLOCK_PIN_EL,HIGH);
    data |= digitalRead(DATA_PIN_EL);
  }
  
  interrupts();
  
  digitalWrite(CLOCK_PIN_EL,LOW); //clocks space bit
  delayMicroseconds(1);
  digitalWrite(CLOCK_PIN_EL,HIGH);
  delayMicroseconds(1);      

  return data;   
}

When the program has started, the motor turns at -400 (max reverse speed), but the angle is not changing which implies that the loop is not looping.

Hope someone can help, thank you.

I found out what the problem was. The wrong absolute encoder was being read, no wonder it didnt work.

The next problem (I'm sure there will be many more) is that when I set an angle to go to, and using just a little bit of proportional gain, the system gets confused as the angle crosses over from 359.99 degrees to 0.01 degrees. And oscillates around this point instead of going to the setpoint of say, 100 degrees. Being an absolute encoder, each position increment has a specific value in the range 0.0 to 359.99.

Is there a known way to avoid this crossover problem?

Is there a known way to avoid this crossover problem?

There are many. Here are a couple of them.

In a PID for a motor the output must be signed, and would normally be float.

If you have a single-turn absolute encoder you'll need to condition its output to be
multi-turn correct before showing it to the PID of course.

Thank you for the link.

Mark, the PID output is a double and it seems to work. I got the behavior I expect by changing DIRECT to REVERSE in the PID settings. I thought this was odd, but it now it does go to the setpoint angle. I still have the problem of the crossover. If I make the setpoint 358 degrees it will run past is and suddenly find itself having to go right around. I notice the motor slowing down but then keeps overshooting.

I will check the link to see if it works for me. As for the PID tuning on my test rig, it seems to be tuned quite well now.

I was thinking, If I change the motor speed to slow down within the last 5 or 10 degrees from the setpoint, then perhaps the overshoot may not happen.

I was thinking, If I change the motor speed to slow down within the last 5 or 10 degrees from the setpoint, then perhaps the overshoot may not happen.

That is what the PID algorithm does. When properly tuned, the PID method is about the best you can do.

Terrorhertz:
Thank you for the link.

Mark, the PID output is a double and it seems to work. I got the behavior I expect by changing DIRECT to REVERSE in the PID settings. I thought this was odd, but it now it does go to the setpoint angle. I still have the problem of the crossover. If I make the setpoint 358 degrees it will run past is and suddenly find itself having to go right around. I notice the motor slowing down but then keeps overshooting.

I will check the link to see if it works for me. As for the PID tuning on my test rig, it seems to be tuned quite well now.

The wrap-around has two approaches, one is to synthesize a value that doesn't wrap, in other words
it is a multi-turn angle (you notice when the actual encoder value wraps and add/sub 360 degrees as
necessary).

Alternatively you make your control loop aware of the position error as a signed value in the range
+/- 180 degrees. subtract the angles then normalise to the range -180 to +180, only then feed
in as an error.

The former method will recover from errors or steps larger than 1/2 a turn, the latter method
won't. Which is best depends on what you are trying to achieve.

(by 360, 180 etc I don't mean to imply you actually work in degrees, you'd normally work in
encoder units directly).

Thank you Mark. I thought about working in actual angle but as you say, I should work in encoder units as it will be faster.