0

I'm working on a pid-controlled levitating magnet device. It uses a hall-sensor to turn on and off an electro magnet in ordet to achieve levitation.

When I hold a magnet close to the hallsensor, I can sense a pulsation from the electro-magnet, however, I if let go of the magnet, it always sticks to the electro-magnet every time, as though the pid is not properly calibrated, the code i'm using came with a pre built possibility to calibrate the PID, you can change set point, Proportion, integral and derivative I've tried messing with it, but it doesn't seem to make any difference.

/*
   Magentic Levitation

   1) connect the digital pin PIN_COIL to the MOSFET gate for electromagnet activation.
   2) connect the analog input pin PIN_HALL_SENSOR to the hall sensor signal pin (the other
      two pins shall be wired to gnd and +5V).
   3) (optional) connect the digital pin PIN_CTRL_INDICATOR to the LED that indicates when the
      controller loop is running.
   4) run Arduino, on the serial port the following commands are available:
      - press 's' or 'S' to decrease or increase the setpoint,
      - press 'p' or 'P' to decrease or increase the P gain,
      - press 'd' or 'D' to decrease or increase the D gain,
      - press 'i' or 'I' to decrease or increase the I gain,
      - press 'x' or 'X' to show the current settings
   5) the system enters in IDLE mode, with a minimum PWM command that allows the orientation of the
      permanent magnet, when in range the system goes in CONTROL mode. When the magnet is too close to
      the coil, the system switches in OFF mode by turning off the PWM command.

   version 1.0: 23/12/2011 -  E.Assenza
*/

const int PIN_COIL = 11; // Pins 3 and 11 are connected to Timer 2.
const int PIN_HALL_SENSOR = 0;
const int PIN_CTRL_INDICATOR = 13;
// Coil resistance 3 ohm, max 1 A = 3 Volt max.
const int COIL_MAX_PWM_VALUE = 160;
// Minimum magnetic field when in idle mode (needed to properly orientate the magnet).
const int COIL_IDLE_VALUE = 50;
// Proximity thresholds, when the magnetic field is in this range enters in control mode.
const int MAG_PROX_THRESHOLD_MIN = 585;
const int MAG_PROX_THRESHOLD_MAX = 615;
// Magnetic field limits, when outside this range enters in idle mode.
const int MAG_LIMIT_MIN = 560;
const int MAG_LIMIT_MAX = 700;
// Control loop frequency.
const int CTRL_LOOP_PERIOD_MS = 1;
// Control loop bias value (PWM midpoint)
const int CONTROL_BIAS = 65;
// Set point corresponding to the control bias
const int CONTROL_SETPOINT = 597;
const int MAX_ERR_INTEGRAL = 10000;
// PID controller parameters
const float PGain = 2.1;
const float DGain = 23.6;
const float IGain = 0.001;

const float P_INCR = 0.1;
const float D_INCR = 0.1;
const float I_INCR = 0.001;

// State machine constants.
const byte MODE_OFF = 0;
const byte MODE_IDLE = 1;
const byte MODE_CONTROL = 2;

const char     *gAppName = "Arduino Magnetic Levitation";
byte            gMode;
int             gSetPoint;
int             gSensorReadout;
int             gPrevSensorReadout;
int             gPwmCommand;
int             gIntegral;
float           gBias;
float           gP;
float           gD;
float           gI;
unsigned long   gMillisCounter;

void setupCoilPWM()
{
   // Setup the timer 2 as Phase Correct PWM, 3921 Hz.
   pinMode(3, OUTPUT);
   pinMode(11, OUTPUT);
   // Timer 2 register: WGM20 sets PWM phase correct mode, COM2x1 sets the PWM out to channels A and B.
   TCCR2A = 0;
   TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM20);
   // Set the prescaler to 8, the PWM freq is 16MHz/255/2/<prescaler>
   TCCR2B = 0;
   TCCR2B = _BV(CS21);
}

void writeCoilPWM(uint8_t pin, int val)
{
   if( pin == 3 )
   {
      OCR2B = val;
   }
   else if( pin == 11 )
   {
      OCR2A = val;
   }
   else
   {
      OCR2A = 0;
      OCR2B = 0;
   }
}

void processCommand( int cmd )
{
   char tmp[64];

   switch(cmd)
   {
      case 'p':
         gP -= P_INCR;
         if( gP < 0 ) gP = 0;
         break;
      case 'P':
         gP += P_INCR;
         break;
      case 'd':
         gD -= D_INCR;
         if( gD < 0 ) gD = 0;
         break;
      case 'D':
         gD += D_INCR;
         break;
      case 'i':
         gI -= I_INCR;
         if( gI < 0 ) gI = 0;
         break;
      case 'I':
         gI += I_INCR;
         break;
      case 's':
         gSetPoint --;
         break;
      case 'S':
         gSetPoint ++;
         break;
      case 'x':
      case 'X':
         sprintf(tmp, "s(%d) c(%d) h(%d) P(%u) D(%u) I(%u,%d)\n", gSetPoint, gPwmCommand, gSensorReadout, (uint16_t)(gP*1000.0), (uint16_t)(gD*1000.0), (uint16_t)(gI*1000.0), gIntegral );
         Serial.print(tmp);
         break;
      default:
         break;
   }
}

byte executeOffMode()
{
   digitalWrite(PIN_CTRL_INDICATOR, LOW);

   // turn off magnetic field
   gPwmCommand = 0;
   writeCoilPWM(PIN_COIL, gPwmCommand);

   gSensorReadout = analogRead(PIN_HALL_SENSOR);

   if( gSensorReadout <= MAG_LIMIT_MIN )
   {
      // Switch to idle mode.
      return MODE_IDLE;
   }
   else
   {
      return MODE_OFF;
   }
}

byte executeIdleMode()
{
   digitalWrite(PIN_CTRL_INDICATOR, LOW);

   // Apply minimum magnetic field.
   gPwmCommand = COIL_IDLE_VALUE;
   writeCoilPWM(PIN_COIL, gPwmCommand);

   gSensorReadout = analogRead(PIN_HALL_SENSOR);

   if( (gSensorReadout >= MAG_PROX_THRESHOLD_MIN) && (gSensorReadout <= MAG_PROX_THRESHOLD_MAX) )
   {
      // Switch to control mode.
      gIntegral = 0;
      return MODE_CONTROL;
   }
   else if( gSensorReadout > MAG_LIMIT_MAX )
   {
      return MODE_OFF;
   }
   else
   {
      return MODE_IDLE;
   }
}

byte checkOutOfLimits(int val)
{
   if( val <= MAG_LIMIT_MIN )
   {
      return MODE_IDLE;
   }
   else if( val >= MAG_LIMIT_MAX )
   {
      return MODE_OFF;
   }

   return MODE_CONTROL;
}

byte executeControlMode()
{
   int err;
   int der;

   byte mode = MODE_CONTROL;

   // Execute control loop at frequency = 1/CTRL_LOOP_PERIOD_MS.
   if( (signed long)( millis() - gMillisCounter ) >= 0)
   {
      gMillisCounter = millis() + CTRL_LOOP_PERIOD_MS;

      digitalWrite(PIN_CTRL_INDICATOR, HIGH);

      gPrevSensorReadout = gSensorReadout;

      // Read hall sensor.
      gSensorReadout = analogRead(PIN_HALL_SENSOR);

      // Check if out of limits (the magnet is out of coil range).
      if( (mode = checkOutOfLimits(gSensorReadout)) != MODE_CONTROL )
      {
         return mode;
      }

      err = (gSetPoint - gSensorReadout);
      der = (gPrevSensorReadout - gSensorReadout);
      gIntegral += err;
      gIntegral = constrain( gIntegral, -MAX_ERR_INTEGRAL, MAX_ERR_INTEGRAL);

      gPwmCommand = gBias + (int)(gP*err) + (int)(gD*der) + (int)(gI*gIntegral);

      gPwmCommand = constrain( gPwmCommand, 0, COIL_MAX_PWM_VALUE);

      // Apply output.
      writeCoilPWM(PIN_COIL, gPwmCommand);

      // Write data to serial port.
/*      Serial.write( (uint8_t)0xAF );
      Serial.write( (uint8_t)gSetPoint );
      Serial.write( (uint8_t)(gSetPoint >> 8) );
      Serial.write( (uint8_t)gSensorReadout );
      Serial.write( (uint8_t)(gSensorReadout >> 8) );
      Serial.write( gPwmCommand );
   }

   return mode;
}

void setup()
{
   setupCoilPWM();

   pinMode(PIN_CTRL_INDICATOR, OUTPUT);

   Serial.begin(115200);

   gMillisCounter = 0;
   gMode = MODE_IDLE;
   gSetPoint = CONTROL_SETPOINT;
   gBias = CONTROL_BIAS;
   gSensorReadout = 0;
   gIntegral = 0;
   gP = PGain;
   gD = DGain;
   gI = IGain;
}

void loop()
{
   // User commands.
   if( Serial.available() )
   {
      processCommand( Serial.read() );
   }

   // State machine.
   switch( gMode )
   {
      case MODE_OFF:
         gMode = executeOffMode();
         break;

      case MODE_IDLE:
         gMode = executeIdleMode();
         break;

      case MODE_CONTROL:
         gMode = executeControlMode();
         break;

      default:
         break;
   }
}
  • Robotics Stackexchange is more suitable for questions like this. – James Waldby - jwpat7 Nov 16 '16 at 18:52
  • PIDs are reasonably on-topic here. Is the question specifically about robotics? – Nick Gammon Nov 16 '16 at 21:41
  • How about telling us the value of gPwmCommand while running your code? There also a possibility that the error is not because of your code, but your electronics. – dhimaspw Nov 17 '16 at 1:48
  • @NickGammon, I'm inclined to disagree that tuning a PID is on-topic here (but nevertheless won't vote to close as off-topic). Also, I think PID expertise is stronger in R.S.E than here. – James Waldby - jwpat7 Nov 17 '16 at 3:34
  • If the OP requests it, I will migrate to Robotics. – Nick Gammon Nov 17 '16 at 4:07
3

Gidday! First response as a new member. PID control is not an easy subject to grasp for starters but here is a link to a site that does explain it: http://www.csimn.com/CSI_pages/PIDforDummies.html

The first thing about your project is that your output is digital (HIGH/LOW) whereas traditionally PID control is analog. AnalogIN to PID to AnalogOUT. So ideally you would need to vary the current through an electromagnet and have the hall-effect make very quick updates (feedback) to the PID controller. Now most young engineers (armed with some maths) learn to control the level of a tank (water or chemical or whatever) and yes, they follow a calibration method. The P is done first and this is the GAIN of the system the Integral usually follows & this is like a 'speed of response' type of adjustment. (Derivative rarely used).

I'm guessing your levitation system would require and extremely fast response & I think it would suffer from:

  1. The time of the Analog to digital conversion at the input to the PID

  2. The speed of the Arduino to process the code it runs

  3. The time it takes for the OUTPUT to be applied to the electromagnet (e.g. the PWM updates inside the Arduino if you decided to use this as a pseudo 'analog output').

I know this is not good news for you but don't give up as someone will have found a work-around to this problem I'm sure. It just seems like a bit more research is required. Maybe some other members can assist further.

  • Thanks for the clarification. I must admit that I could never totally get PIDs to work properly, particularly with the derivative part. Saying it is rarely used helps to concentrate on the more important parts. – Nick Gammon Nov 17 '16 at 4:10
  • this is a really nice, very extensive answer. I was thinking about that analog PID might be better aswell. My project is a reproduction of an existing project: forum.arduino.cc/index.php?topic=89241.0 so I don't think the design is the problem, it's probably something with my implementation. For example, how short long wires would start causing a problem? – Himmators Nov 17 '16 at 15:54
0

Well, the response time should not be a major issue, since the ball movement is based solely on the acceleration due to gravity and the magnetic force due to the coil. If you let the ball free fall from being stopped, it would take 14.3 ms for it to move the first 1 mm, and that is a lot of time for computing and for electrical circuits to respond.

To calibrate the PID, a process usually called PID tuning, you would start with the proportional constant, Kp. Since your system is unstable, that is, without the PID either the ball will fall or stick to the electromagnet, it will be hard to tune the Kp, even with minor errors on your hall sensor readings. If the position error is large or the sensor takes too much time to update, that can also complicate the tuning.

You should check the furthest away the ball can be and still be drawn to the magnet, that point is the limit where your PID can save the ball from falling. As a rule of thumb, try tuning the Kp with the setpoint being in between the magnet and that point. At first you want just the Kp being non-zero, and it should have a value great enough so that, if the ball falls a little bit bellow the setpoint the magnet is fully powered and if it goes a bit over the setpoint the magnet turns off. This should lead to the ball oscillating around the setpoint. This method will work on your system because the output is limited between a PWM with duty 100% and 0% (you can change these limiting values if needed!), (I suppose) turning the coil to its maximum and minimum doesn't damage the system and the system is relatively slow (the gravity effect is always the same and the acceleration due to the magnet depends on the mass of the ball).

After you got it to work so-so, try lowering the Kp to the minimum value that keeps the ball oscillating around the setpoint and then read on Ziegler–Nichols tuning method.

When I hold a magnet close to the hallsensor, I can sense a pulsation from the electro-magnet, however, I if let go of the magnet, it always sticks to the electro-magnet every time, as though the pid is not properly calibrated,

That behavior is odd, even a bad PID should eventually turn off the magnet after detecting the ball went above the setpoint. Maybe the sensor reading is causing the problem, e.g. the ball goes close to the magnet but the sensor/code interpret it as going away from the magnet.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.