PID control of electromagnets

Hi all,

I am trying to PID control two electromagnets.
I would like to set the position between them and make them move to a specific location.

Before getting into PID I would first like to explain my test system:

  • Two electromagnets
  • One mounted static, one mounted on a sliding drawer-bearing
  • The magnets are driven by an L298N board
  • The position of the moving magnet is tracked by a linear encoder from an HP printer
  • The position from the linear encoder is read by an Arduino Micro
  • The magnets are controlled by an Arduino Mega that reads the position from the Micro using i2c : Master Reader/Slave Sender

Pictures of the setup:
(click for full size)

Now I have some very crude code to control the position.

The important part is this:

if(intPosition < 50){
    //If the distance is less than XX (=X.Xmm) the magnets should repell
    repell();
  }
  if(intPosition > 60){
    //If the distance is more than XX (=X.Xmm) the magnets should attract
    attract();
  }

The complete code is this

// includes
#include <Wire.h>

//SETUP PIN CONTROL ON L298N ================================
int pinPWM_M1 = 2;
int pinDIR_M1A = 3;
int pinDIR_M1B = 4;
int pinPWM_M2 = 5;
int pinDIR_M2A = 6;
int pinDIR_M2B = 7;

// VARIABLES TO GET CURRENT POSITION FROM ENCODER ==========
int intPosition;           // the current position reported by the encoder
byte a,b;                  // bytes to hold the data coming over i2c, two bytes needed for bigger and negative values: https://thewanderingengineer.com/2015/05/06/sending-16-bit-and-32-bit-numbers-with-arduino-i2c/

void setup() {

  //These pin control polarity and pwm for the magnets via the L298N board
  pinMode(pinPWM_M1,OUTPUT);
  pinMode(pinDIR_M1A,OUTPUT);
  pinMode(pinDIR_M1B,OUTPUT);
  pinMode(pinPWM_M2,OUTPUT);
  pinMode(pinDIR_M2A,OUTPUT);
  pinMode(pinDIR_M2B,OUTPUT);

  //Turn on magnets
  //With these values the magnets attract
  //Magnet 1
  analogWrite(pinPWM_M1,255);
  digitalWrite(pinDIR_M1A, LOW);
  digitalWrite(pinDIR_M1B, HIGH);
  //Magnet 2
  analogWrite(pinPWM_M2,255);
  digitalWrite(pinDIR_M2A, HIGH);
  digitalWrite(pinDIR_M2B, LOW);
  

  //SERIAL setup, for debugging
  Serial.begin(9600);

  //I2C WIRE
  Wire.begin();        // join i2c bus (address optional for master)
 
}  //end setup


void attract(){

  //Only one magnet must reverse polarity
  //Magnet 1
  digitalWrite(pinDIR_M1A, LOW);
  digitalWrite(pinDIR_M1B, HIGH);
    
}

void repell(){
  
  digitalWrite(pinDIR_M1A, HIGH);
  digitalWrite(pinDIR_M1B, LOW);
  
}


void loop() {

  //get the current position from the connected Arduino Micro over i2c
  Wire.requestFrom(1, 2);    // requests 2 bytes from slave device #1
  
  a = Wire.read();
  b = Wire.read();

  //Two bytes combined to give position
  intPosition = a;
  intPosition = (intPosition << 8) | b;

  Serial.println(intPosition);

  if(intPosition < 50){
    //If the distance is less than XX (=X.Xmm) the magnets should repell
    repell();
  }
  if(intPosition > 60){
    //If the distance is more than XX (=X.Xmm) the magnets should attract
    attract();
  }  
  
} //end loop

The result is this (video), I call it:
"Hammer Time !"

So my test system works, somewhat.

Now my question would be how to implement PID control to control the position decently.

Thanks for reading !

The P part of PID is easy. Change the drive strength based on the distance from the target. You can use analogWrite() on the PWM pins of your Arduino. Play with the "gain" which converts distance to PWM output until you get a stable response.

Ive never had to write a code to actually perform the PID control but from what i remember of the project i had, its really easy to "cheat" and just tune the values of the PID algorithm til you get the response that you want or use matlab functions to help you find those values for your code.

Well,

I decided to give the Arduino PID library a try.

So there is:

//Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &Setpoint,3,0,0, DIRECT);
  • Input (the current position)
  • Output (the calculated PWM value)
  • Setpoint (the desired position)

First the position:

  • when the manets touch the position is 0
  • the position increases when they move away from each other
  • the position is measured in tenths of a mm (intPosition = 139 means 13.9mm)

So what I try do to in the code below:

  • set the setpoint to 100 (10mm)
  • main loop gets current position

if(position > setpoint) : attract
if(position < setpoint) : repell

void attract(){

  • set the polarity of the magnets to attract
    As long as position > setpoint : let the PID adjust the pwm so the magnet moves towards the setpoint
    }

void repell(){

  • set the polarity of the magnets to repell
    As long as position < setpoint : let the PID adjust the pwm so the magnet moves towards the setpoint
    }

Now, the repelling works. The attract function does not :frowning:

What I think might be happening:

  • the magnets polarities are set to attract
  • the PID however "thinks" that the current position is greater than the setpoint, so the PWM output is set to 0

When, in the code below, I comment out the while loop in "void attract" the repell function works.

Does anyone have an idea how I can "tell" the PID to increase PWM in "void attract" ?

Thanks

Here's the code:

// includes

#include <PID_v1.h>
#include <Wire.h>

//SETUP PIN CONTROL ON L298N ================================
int pinPWM_M1 = 2;
int pinDIR_M1A = 3;
int pinDIR_M1B = 4;
int pinPWM_M2 = 5;
int pinDIR_M2A = 6;
int pinDIR_M2B = 7;

//SETUP LCD ===================================================
// initialize the display library with the numbers of the interface pins
LiquidCrystal lcd(35, 34, 33, 32, 31, 30);
int intPreviousposition;  //to determine when to refresh the lcd

//SETUP PID ==================================================
//Define Variables we'll be connecting to
double Setpoint, Input, Output;

// VARIABLES TO GET CURRENT POSITION FROM ENCODER
int intPosition;           // the current position reported by the encoder
byte a,b;                  // bytes to hold the data coming over i2c, two bytes needed for bigger and negative values: https://thewanderingengineer.com/2015/05/06/sending-16-bit-and-32-bit-numbers-with-arduino-i2c/

//Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &Setpoint,3,1,0, DIRECT);


void setup() {

  pinMode(pinPWM_M1,OUTPUT);
  pinMode(pinDIR_M1A,OUTPUT);
  pinMode(pinDIR_M1B,OUTPUT);
  pinMode(pinPWM_M2,OUTPUT);
  pinMode(pinDIR_M2A,OUTPUT);
  pinMode(pinDIR_M2B,OUTPUT);

  //Turn on magnets
  //With these values the magnets attract
  //Magnet 1
  analogWrite(pinPWM_M1,255);
  digitalWrite(pinDIR_M1A, LOW);
  digitalWrite(pinDIR_M1B, HIGH);
  //Magnet 2
  analogWrite(pinPWM_M2,255);
  digitalWrite(pinDIR_M2A, HIGH);
  digitalWrite(pinDIR_M2B, LOW);
  

  //SERIAL setup ==============================================================
  Serial.begin(9600);

  //I2C WIRE
  Wire.begin();        // join i2c bus (address optional for master)
  

  //PID Setup =================================================================
  Setpoint = 50;

  myPID.SetSampleTime(100); //sampletime in milliseconds

  //turn the PID on
  myPID.SetMode(AUTOMATIC);
  myPID.SetOutputLimits(0, 255);
  
}  //end setup


void attract(){

  //With these values the magnets attract
  //Magnet 1
  analogWrite(pinPWM_M1,220);
  digitalWrite(pinDIR_M1A, LOW);
  digitalWrite(pinDIR_M1B, HIGH);
  //Magnet 2
  analogWrite(pinPWM_M2,220);
  digitalWrite(pinDIR_M2A, HIGH);
  digitalWrite(pinDIR_M2B, LOW);

  while(intPosition > Setpoint){
  
    Wire.requestFrom(1, 2);    // requests 2 bytes from slave device #1
  
    a = Wire.read();
    b = Wire.read();
  
    intPosition = a;
    intPosition = (intPosition << 8) | b;
  
    //Feed the position to the PID
    Input = intPosition;
  
    //   
    myPID.Compute();
    analogWrite(pinPWM_M1,Output);
    analogWrite(pinPWM_M2,Output);

    Serial.println("ATTRACT");
    Serial.println(Output);
    
  } //end while
  
} //end void attract

void repell(){


  //With these values the magnets REPELL
  //Magnet 1
  digitalWrite(pinDIR_M1A, HIGH);
  digitalWrite(pinDIR_M1B, LOW);
  //Magnet 2
  digitalWrite(pinDIR_M2A, HIGH);
  digitalWrite(pinDIR_M2B, LOW);
  
  while(intPosition < Setpoint){
  
    Wire.requestFrom(1, 2);    // requests 2 bytes from slave device #1
  
    a = Wire.read();
    b = Wire.read();
  
    intPosition = a;
    intPosition = (intPosition << 8) | b;
  
    //Feed the position to the PID
    Input = intPosition;
  
    //   
    myPID.Compute();
    analogWrite(pinPWM_M1,Output);
    analogWrite(pinPWM_M2,Output);

    Serial.println("REPELL");
    Serial.println(Output);
  
  } //end while
  
} //end void repell


void loop(){

  Wire.requestFrom(1, 2);    // requests 2 bytes from slave device #1

  a = Wire.read();
  b = Wire.read();

  intPosition = a;
  intPosition = (intPosition << 8) | b;
  
    Serial.println(intPosition);

  if(intPosition < Setpoint){
    repell();
  }
  if(intPosition > Setpoint){
    attract();
  }

  
} //end loop

I've never used that library. It looks like you might be taking away some of the library's functions by writing your own if(intPosition < Setpoint) You will be messing with the library's assumptions very badly by doing that.

The thing with PID instead of just P, is that you may want to be repelling while you are on the 'attract' side of the position. Think about the magnets slamming together - you want to reverse the polarity and repel them before they reach the desired position.

So ditch all of your own logic related to position. You should call the attract/repel functions based on the PID output value, not the input position. So the main loop should update the PID and decide if the requested force is positive or negative, then call the appropriate attract() or repel(). Change those functions to accept an integer input (the PID output) and you may also need to flip the value to make it positive for repel().

Thanks MorganS,

I thought I came up with a solution but what you are saying sound more plausible.
I was thinking of using two seperate PID instances, one to control the attracting and one to control the repelling. But that more like playing volleyball with a wall in between instead of a net I guess.

So if I understand correctly I could try:

//Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &Setpoint,3,0,0, DIRECT);
  • current position as input
  • output range -255 to +255
  • setpoint is desired position
  • tune parameters

Now :

if (the output is negative/positive (when current position < or > setpoint) ){

  • reverse polarity as needed
  • set pwm(Output)

}

Positive/negative and attract/repell, I would have to see what combination works.

This would translate to the following code. I tested this and it seems to work :slight_smile:

// includes
#include <LiquidCrystal.h>
#include <PWM.h>
#include <math.h>
#include <PID_v1.h>
#include <Wire.h>

//SETUP PIN CONTROL ON L298N ================================
int pinPWM_M1 = 2;
int pinDIR_M1A = 3;
int pinDIR_M1B = 4;
int pinPWM_M2 = 5;
int pinDIR_M2A = 6;
int pinDIR_M2B = 7;


// VARIABLES TO GET CURRENT POSITION FROM ENCODER
int intPosition;           // the current position reported by the encoder
byte a,b;                  // bytes to hold the data coming over i2c, two bytes needed for bigger and negative values: https://thewanderingengineer.com/2015/05/06/sending-16-bit-and-32-bit-numbers-with-arduino-i2c/

//SETUP PID ==================================================
//Define Variables we'll be connecting to
double Setpoint, Input, Output;

float Kp = 2;
float Ki = 1;
float Kd = 0;

PID pidPosition(&Input, &Output, &Setpoint,Kp,Ki,Kd, DIRECT);

// String for output on serial
String strDebug; 


void setup() {

  // pinPWM_Mx is the pwm pin for magnet x, pinDIR_MxA and pinDIR_MxA determine the polarity of magnet x
  pinMode(pinPWM_M1,OUTPUT);
  pinMode(pinDIR_M1A,OUTPUT);
  pinMode(pinDIR_M1B,OUTPUT);
  pinMode(pinPWM_M2,OUTPUT);
  pinMode(pinDIR_M2A,OUTPUT);
  pinMode(pinDIR_M2B,OUTPUT);

  //Turn on magnets
  //With these values the magnets attract
  //Magnet 1
  analogWrite(pinPWM_M1,255);
  digitalWrite(pinDIR_M1A, LOW);
  digitalWrite(pinDIR_M1B, HIGH);
  //Magnet 2
  analogWrite(pinPWM_M2,255);
  digitalWrite(pinDIR_M2A, HIGH);
  digitalWrite(pinDIR_M2B, LOW);
  

  //SERIAL setup ==============================================================
  Serial.begin(9600);

  //I2C WIRE
  Wire.begin();        // join i2c bus (address optional for master)
  

  //PID setup =================================================================
  Setpoint = 50;

  pidPosition.SetSampleTime(10); //sampletime in milliseconds
  
  //turn the PID on
  pidPosition.SetMode(AUTOMATIC);
  pidPosition.SetOutputLimits(-255, 255);
  
  
}  //end void setup



void loop(){

  Wire.requestFrom(1, 2);    // requests 2 bytes from slave device #1

  a = Wire.read();
  b = Wire.read();

  intPosition = a;
  intPosition = (intPosition << 8) | b;

  //Serial.println(intPosition);

  //Feed the position to the PID
  Input = intPosition;
  
  pidPosition.Compute();

  Output = round(Output);

  strDebug = String(intPosition) + ";" + String(Output);

  Serial.println(strDebug);

  if(Output < 0){
    
    //attract
    //Only one of the magnets has to reverse polarity
    //That is magnet 1
    analogWrite(pinPWM_M1, Output);
    digitalWrite(pinDIR_M1A, LOW);
    digitalWrite(pinDIR_M1B, HIGH);
    //But both magnets wil be set to the same "strength" / PWM value
    //Magnet 2
    analogWrite(pinPWM_M2, Output);
  }

  if(Output > 0){
    //repell

    //Turn the negative number into positive
    Output = abs(Output);
    
    analogWrite(pinPWM_M1, Output);
    digitalWrite(pinDIR_M1A, HIGH);
    digitalWrite(pinDIR_M1B, LOW);
    analogWrite(pinPWM_M2, Output);
  }
 
} //end loop

Is this code sound ? It seems to work, so I'd say yeah for now.
Now it comes to tuning the PID parameters then.

At this moment It seems the response to change from the PID is way too slow.
My magnets only really move when the PWM is around 200 and they are not to far apart.
Also the mechanical (static) friction is pretty high relative to the strength and current in the magnets.

But MorganS, thanks for your Input :slight_smile:

Any suggestions as to were to go from here are welcome

Ok,

I took the approach suggested by MorganS. The PID now calculates a value from -255 to +255.

This seems to be working (see video below).
But I'm having trouble choosing the parameters. I'm just trying numbers without a decent plan.
The parameters used in the video are:
float Kp = 100;
float Ki = 50;
float Kd = 50;

Below is the video and my code.

Could anyone give me some advise on tuning ?

Thanks !

The video:
https://sendvid.com/8c0wwqre

My code:

// includes
#include <LiquidCrystal.h>
#include <PWM.h>
#include <math.h>
#include <PID_v1.h>
#include <Wire.h>

//SETUP PIN CONTROL ON L298N =================================
int pinPWM_M1 = 2;
int pinDIR_M1A = 3;
int pinDIR_M1B = 4;
int pinPWM_M2 = 5;
int pinDIR_M2A = 6;
int pinDIR_M2B = 7;


// VARIABLES TO GET CURRENT POSITION FROM ENCODER ============
int intPosition;           // the current position reported by the encoder
byte a,b;                  // bytes to hold the data coming over i2c, two bytes needed for bigger and negative values: https://thewanderingengineer.com/2015/05/06/sending-16-bit-and-32-bit-numbers-with-arduino-i2c/

//SETUP PID ==================================================
double Setpoint, Input, Output;

float Kp = 100;
float Ki = 50;
float Kd = 50;

PID pidPosition(&Input, &Output, &Setpoint,Kp,Ki,Kd, DIRECT);

// String for output on serial
String strDebug; 


void setup() {

  // pinPWM_Mx is the pwm pin for magnet x, pinDIR_MxA and pinDIR_MxA determine the polarity of magnet x
  pinMode(pinPWM_M1,OUTPUT);
  pinMode(pinDIR_M1A,OUTPUT);
  pinMode(pinDIR_M1B,OUTPUT);
  pinMode(pinPWM_M2,OUTPUT);
  pinMode(pinDIR_M2A,OUTPUT);
  pinMode(pinDIR_M2B,OUTPUT);

  //Turn on magnets
  //With these values the magnets attract
  //Magnet 1
  analogWrite(pinPWM_M1,255);
  digitalWrite(pinDIR_M1A, LOW);
  digitalWrite(pinDIR_M1B, HIGH);
  //Magnet 2
  analogWrite(pinPWM_M2,255);
  digitalWrite(pinDIR_M2A, HIGH);
  digitalWrite(pinDIR_M2B, LOW);
  

  //SERIAL setup ==============================================================
  Serial.begin(9600);

  //I2C WIRE
  Wire.begin();        // join i2c bus (address optional for master)
  

  //PID setup =================================================================
  Setpoint = 100;

  pidPosition.SetSampleTime(10); //sampletime in milliseconds
  
  //turn the PID on
  pidPosition.SetMode(AUTOMATIC);
  pidPosition.SetSampleTime(10); 
  pidPosition.SetOutputLimits(-255, 255);

  // PWM setup ================================================================
  //initialize all timers except for 0, to save time keeping functions
  InitTimersSafe(); 

  //sets the frequency for the specified pins
  SetPinFrequencySafe(pinPWM_M1, 10000);
  SetPinFrequencySafe(pinPWM_M2, 10000);
  
}  //end void setup



void loop(){

  Wire.requestFrom(1, 2);    // requests 2 bytes from slave device #1

  a = Wire.read();
  b = Wire.read();

  intPosition = a;
  intPosition = (intPosition << 8) | b;

  //Feed the position to the PID
  Input = intPosition;
  
  pidPosition.Compute();

  Output = round(Output);

  strDebug = "";

  strDebug = "position: " + String(intPosition) + " | PID output: " + String(Output);

  

  if(Output < 0){
    
    //attract
    strDebug = strDebug + " | Attracting";
    Serial.println(strDebug);
    
    //Only one of the magnets has to reverse polarity
    //That is magnet 1
    digitalWrite(pinDIR_M1A, LOW);
    digitalWrite(pinDIR_M1B, HIGH);
    //But both magnets wil be set to the same "strength" / PWM value
    //The PID has returned a negative number, this is made positive by abs()
    analogWrite(pinPWM_M1, abs(Output));
    analogWrite(pinPWM_M2, abs(Output));
    
  }

  if(Output >= 0){
    //repell
    strDebug = strDebug + " | Repelling";
    Serial.println(strDebug);
    //set polarity to repell
    digitalWrite(pinDIR_M1A, HIGH);
    digitalWrite(pinDIR_M1B, LOW);

    analogWrite(pinPWM_M1, Output);
    analogWrite(pinPWM_M2, Output);
    
  }

 
} //end loop

Sorry, I'm on a limited connection here and can't watch the video.

Start with zero for the I term. Higher values here always increase the instability. Also start with zero for the D term. As for the rest, there must be a hundred online guides (but stay away from Instructables; they are usually poor advice.)

One problem you will be facing is the physics is non-linear. As the magnets get closer, the magnetic attraction increase per milimeter will increase. So the 'too close' side of the line needs a higher gain compared to the 'too far' side. So you won't be able to tune a basic PID algorithm as tightly as an algorithm developed for this specific case.

Have you considered a linear motor as a solution?

regards

Allan

Hi,
What is the application that requires this sort of control with magnets?

Tom... :slight_smile:

mrExplore:
So my test system works, somewhat.

Now my question would be how to implement PID control to control the position decently.

This is interesting because when I read the title I though it would be impossible.

In my (uneducated) opinion you should not be seeking to "implement PID" in the sense that it is some magic box.

If you already have a partial solution you should study its physical (behavioural) short comings and think about how they should be addressed. Presumably there needs to be a strong reaction if the magnets get too close and keep getting closer.

The separate algorithms in the PID library are each very simple and their purpose is obvious or well explained by the Author on his Blog. But they may or may not be relevant for the physics of your system. My inclination would be to incrementally develop your existing system by adding such corrective algorithms as seem appropriate and, when tested, show an improvement.

The time between measurements and the time for a responsive action to take effect also have a huge impact on the system.

I'm saying all this on the back of my failure (so far) to apply PID to speed control for a small DC motor

...R