Project showcase

Magnetic Levitation © GPL3+

Magnetic Levitation, need I say more? Levitate some small magnets with a bigger electromagnet!

  • 22,909 views
  • 56 comments
  • 51 respects

Components and supplies

A000066 iso both
Arduino UNO & Genuino UNO
×1
Fairchild semiconductor fqu13n06ltu image 75px
Power MOSFET N-Channel
Actually I am using a FQP30N06L, but any N-channel logic level MOSFET should be usable.
×1
Enameled copper wire
×1
Copper wire
×1
Mfr 25fbf52 221r sml
Resistor 221 ohm
For the LED.
×1
Mfr 25fbf52 221k sml
Resistor 221k ohm
Connected to the Gate of MOSFET to switch it off faster.
×1
09590 01
LED (generic)
As an indicator for the strength of electromagnet.
×1
Fairchild semiconductor 1n4004. image
1N4007 – High Voltage, High Current Rated Diode
Any general purpose diode should suffice, it is to prevent the high EMF generated from the electromagnet from damaging the MOSFET, but my coil does't have high enough inductance to actually generate anything.
×1
3503 Hall Effect Sensor
×1
Neodymium Magnets
×1

Necessary tools and machines

3drag
3D Printer (generic)

About this project

Magnetic levitation fascinates me since I was a child. And the amazement never dies off even after I knew the working principle of the device. Hence, off I go creating a simple 'pull' type magnetic levitation project! My next plan is to make something similar to Levitron, a 'push' type magnetic levitator.

The concept is quite simple: using the U3503 Hall Effect Sensor, the Uno is constantly measuring the magnetic field generated by the permanent magnet ( some neodymium magnets in my case). Then it calculates the error between the reading and my setpoint and feed the number to a PID equation, which then adjusts the strength of PWM output. The PWM controls the on-off of the MOSFET.

The sensor is put under the electromagnet coiled with enameled copper wire. Some magnets are put on top of the electromagnet to enhance the field. The hard part was to tune the PID values. However, the codes written by Reid Borsuk made it very simple to adjust the values. In fact, I just used his code directly, with my PID values. 

I showed my crude prototype to one of my friends, and he generously designed a stand and printed it out with a 3D printer! Thank you very much!

Schematics

Magnetic Levitation Schematic
It is this simple! The MOSFET is controlled by PWM on pin 10. The output of Hall Effect Sensor is read by A0 pin.
New%20doc%203 1

Code

Magnetic LevitationArduino
From http://www.reidb.net/MagLevitator.html
/******************************************************************************************************
 *                                                                                                    *
 *                Magnetic levitation program for Arduino Uno/ATMega328                               *
 *                                                                                                    *
 *                                                                                                    *
 *  Copyright (C) 2014 Reid Borsuk (reid.borsuk@live.com)                                             *
 *                                                                                                    *
 *  "MIT Three Clause License"                                                                        *
 *                                                                                                    *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of this software     *
 *  and associated documentation files (the "Software"), to deal in the Software without restriction, *
 *  including without limitation the rights to use, copy, modify, merge, publish, distribute,         *
 *  sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is     *
 *  furnished to do so, subject to the following conditions:                                          *
 *                                                                                                    *
 *  The above copyright notice and this permission notice shall be included in all copies             *
 *  or substantial portions of the Software.                                                          *
 *                                                                                                    *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING     *
 *  BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND        *
 *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,      *
 *  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,    *
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.           *
 *                                                                                                    *
 *                                                                                                    *

 *******************************************************************************************************/
 
//#define QUIETMODE                        //Quietmode slows the time it takes to update the full PWM duty cycle. This makes it take a LOT longer to stabilize (on the order of 30 seconds), but makes almost no audible noise

#define MIN_PWM_VALUE         0          //Minimum PWM duty cycle
#define MAX_PWM_VALUE         255        //Maximum PWM duty cycle
#define IDLE_TIMEOUT_PERIOD   3000       //Milliseconds. Must be < gIdleTime's maximum value
#define MIN_MAG_LIMIT         400        //Trigger point for idle/active mode. If a permanent magnet is in range, hall sensor should read below this value
#define PID_UPDATE_INTERVAL   1          //PWM update interval, in milliseconds. 0 = as fast as possible, likely unstable due to conditional branching & timing interrupts. Must be < gNextSensorReadout's maximum value

#define DEFAULT_TARGET_VALUE  300       //Default target hall effect readout
#define DEFAULT_KP            0.7        //Default Kp, proportional gain parameter
#ifndef QUIETMODE
  #define DEFAULT_KD          1.7
  //Default Kd, derivative gain parameter
  #else //not QUIETMODE
  #define DEFAULT_KD            23.7
#endif //not QUIETMODE

#define DEFAULT_KI            0.0002     //Default Ki, integral gain parameter
#define DEFAULT_MAX_INTEGRAL  5000      //Maximum integral term (limited by signed int below, change to long if > (32,767 - 1024) [1024 because that's the maximum that can be inserted before a constrain operation]

#define KP_INCREMENT          0.1        //Increment used for serial commands (gKp)
#define KD_INCREMENT          0.1        //Increment used for serial commands (gKd)
#define KI_INCREMENT          0.0001     //Increment used for serial commands (gKi)
#define VALUE_INCREMENT       1          //Increment used for serial commands (gTargetValue)

#define FILTERFACTOR 3                   //Weighting factor for hall sensor reading. We calculate a running average, with the most recent reading making up 1/FILTERFACTOR of the average. Lower = faster, higher = smoother

int roundValue(float value)
{
  return (int)(value + 0.5);
}

const int coilPin = 10; //Timer 1B on the Uno (ATmega328), Timer 2A on the Mega (ATmega2560)
const int hallSensorPin = 0;
const int redLedPin = 12;
const int blueLedPin = 13; //Onboard LED on Arduino, used mostly to see what the bootloader is doing so we know when we're booted
const int gMidpoint = roundValue((MAX_PWM_VALUE - MIN_PWM_VALUE) / 2); //The midpoint of our PWM range

boolean gIdle = false; //Used to track if we're in idle mode (magnet turned off due to no permanent magnet detected for idle time-out period)
signed int gIdleTime = 0; //KEEP AS SIGNED. Holds the next time we could go into idle mode. Uses overflow safe arithmetic so does not need to hold the entire output of millis(). Must be able to hold IDLE_TIMEOUT_PERIOD without overflow
signed int gNextPIDCycle = 0; ///KEEP AS SIGNED. Holds the next time we need to recalculate PID outputs. Uses overflow safe arithmetic so does not need to hold the entire output of millis(). Must be able to hold PID_UPDATE_INTERVAL without overflow

int gCurrentDutyCycle = 0; //Current PWM duty cycle for the coil
int gLastSensorReadout = 0; //Last sensor readout to calculate derivative term
int gNextSensorReadout = 0; //The "next" sensor value. Declared global so we can use it as a running average and move ot to gLastSensorReadout after PWM calculation

int gTargetValue = DEFAULT_TARGET_VALUE;
float gKp = DEFAULT_KP;
float gKd = DEFAULT_KD;
float gKi = DEFAULT_KI;
int gIntegralError = 0;  //Calculates running error over time

void writeCoilPWM(int value)
{
    OCR1B = value;  
}


void setup()
{  
    //First thing we set up is the blue LED so we can use it for signaling boot status
    pinMode(blueLedPin, OUTPUT);
    digitalWrite(blueLedPin, HIGH);
    
    /* Setting up coil PWM settings
    
     [Only for ATmega 2560]
     timer 0 (controls pin 13, 4);
     timer 1 (controls pin 12, 11);
     timer 2 (controls pin 10, 9);
     timer 3 (controls pin 5, 3, 2);
     timer 4 (controls pin 8, 7, 6);
     [For ATmega328]
     timer 0 (controls pin 5, 6);
     timer 1 (controls pin 9, 10);
     timer 2 (controls pin 1, 3);
     
     [Timer 1 (ATmega328) or Timer 2 (ATmega2560)]
     prescaler = 1 ---> PWM frequency is 31374 Hz
     prescaler = 2 ---> PWM frequency is 3921 Hz
     prescaler = 3 ---> PWM frequency is 980.3 Hz
     prescaler = 4 ---> PWM frequency is 490.1 Hz (default value)
     prescaler = 5 ---> PWM frequency is 245 Hz
     prescaler = 6 ---> PWM frequency is 122.5 Hz
     prescaler = 6 ---> PWM frequency is 122.5 Hz
    */
    // Setup timer 1 as Phase Correct non-inverted PWM, 31372.55 Hz.
    pinMode(9, OUTPUT);
    pinMode(10, OUTPUT);
    // WGM20 is used for Phase Correct PWM, COM2A1/COM2B1 sets output to non-inverted
    TCCR1A = 0;
    TCCR1A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM20);
    // PWM frequency is 16MHz/255/2/<prescaler>, prescaler is 1 here by using CS20
    TCCR1B = 0;
    TCCR1B = _BV(CS20);
    
    pinMode(redLedPin, OUTPUT);
    pinMode(hallSensorPin, INPUT);
    
    Serial.begin(9600);
    
    //Boot complete, turn of blue LED
    digitalWrite(blueLedPin, LOW);
}

//Used while in idle mode
void idleLoop()
{
    digitalWrite(redLedPin, HIGH);
    
    //Turn off magnet
    if(0 != gCurrentDutyCycle)
    {
      gCurrentDutyCycle = 0;
      writeCoilPWM(gCurrentDutyCycle);
    }
    
    int sensorReadout = analogRead(hallSensorPin);
    
    //Transition back to active mode if there's a magnet in detection range
    if(MIN_MAG_LIMIT > sensorReadout)
    {
      gIdle = false;
      gNextSensorReadout = sensorReadout; //Prime the sensor readout so it's not super-stale (higher filtering factors will result in longer time to stabilize)
      gLastSensorReadout = sensorReadout; //Reduce the derivative term to 0
      digitalWrite(redLedPin, LOW);
      
      //This seems pointless, but is used to deal with overflow in millis()'s output when downcast. If it overflows while in idle mode, it would stick in filter waiting for millis to go all the way back up to the last calculated point. 
      //Can also set to type's min value to do the same thing, but now is more intuitive.
      gNextPIDCycle = millis();
      gIdleTime = gNextPIDCycle + IDLE_TIMEOUT_PERIOD;
    }
}

//Used while in active mode
void controlLoop()
{
  //Downcast millis to the type of the stored cycle argument, then calculate the difference as a signed number. If negative, the time hasn't passed yet. This allows us to do overflow-safe arithmetic
  if(0 <= ((typeof(gNextPIDCycle))millis() - gNextPIDCycle))
  {
    //By default, downcast to signed 16-bit int (safe), but can be changed to use longer intervals by changing the type of gNextPWMCycle
    gNextPIDCycle = millis() + PID_UPDATE_INTERVAL;
    
    //Read the sensor at least once in an update cycle
    gNextSensorReadout = roundValue(((gNextSensorReadout * (FILTERFACTOR - 1)) + analogRead(hallSensorPin)) / FILTERFACTOR);

    
    if(MIN_MAG_LIMIT <= gNextSensorReadout) //We don't see a permanent magnet right now
    {
      if(0 <= ((typeof(gIdleTime))millis() - gIdleTime)) //We haven't seen a permanent magnet in IDLE_TIMEOUT_PERIOD. Overflow safe for the same reason as the above calculation for gNextPWMCycle
      {
        gIdle = true;
        return; //Early exit to fall into idle mode
      }
    }
    else //There is a permanent magnet in range during this update
    {
      //Cast overflow is acceptable here as long as IDLE_TIMEOUT_PERIOD < typeof(gIdleTime)'s maximum value
      gIdleTime = millis() + IDLE_TIMEOUT_PERIOD; 
    }
    
    int error = gTargetValue - gNextSensorReadout; //Difference between current and expected values (for proportional term)
 
    //Slope of the input over time (for derivative term). This is called Derivative on Measurement, as opposed to the more normal Derivative on Error. Used to reduce "derivative kick" when changing the set point, not a huge deal at our frequency
    int dError = gNextSensorReadout - gLastSensorReadout; 
    
    gIntegralError = constrain(gIntegralError + error, -DEFAULT_MAX_INTEGRAL, DEFAULT_MAX_INTEGRAL); //Roughly constant error over time (for integral term)
    
    //This is the actual PID magic. See http://en.wikipedia.org/wiki/PID_controller
#ifdef QUIETMODE
    //This slows down the change in the electromagnet, making the device substantially quieter but taking a lot longer to stabilize (on the order of 30 seconds) 
    int gNextDutyCycle = gMidpoint - roundValue((gKp*error) - (gKd*dError) + (gKi*gIntegralError));
    gCurrentDutyCycle = roundValue(((gCurrentDutyCycle * 2) + gNextDutyCycle) / 3);
#else //not QUIETMODE
    gCurrentDutyCycle = gMidpoint - roundValue((gKp*error) - (gKd*dError) + (gKi*gIntegralError));
#endif //not QUIETMODE
    //It's possible to overshoot in the above, so constrain to between our max and min
    gCurrentDutyCycle = constrain(gCurrentDutyCycle, MIN_PWM_VALUE, MAX_PWM_VALUE);
    
    writeCoilPWM(gCurrentDutyCycle);
    
    //Store for next calculation of dError
    gLastSensorReadout = gNextSensorReadout;
  }
  else //We're waiting for our next PID update cycle, just read the hall sensor for our filtering routine and return. We could also spin on this if we wanted more samples...
  {
    //This is a weighted average function. It basically takes FILTERFACTOR samples, replaces one with the current hall sensor value, and averages over that number of inputs.
    //The higher the FILTERFACTOR, the slower the response (and the less important erroneous readings are)
    gNextSensorReadout = roundValue(((gNextSensorReadout * (FILTERFACTOR - 1)) + analogRead(hallSensorPin)) / FILTERFACTOR);
  }
}

void serialCommand(char command)
{
  char output[255];
  
  switch(command)
  {
    case 'P':
      gKp += KP_INCREMENT;
      break;
    case 'p':
      gKp -= KP_INCREMENT;
      if(0 > gKp) gKp = 0;
      break;
      
    case 'D':
      gKd += KD_INCREMENT;
      break;
    case 'd':
      gKd -= KD_INCREMENT;
      if(0 > gKd) gKd = 0;
      break;
    
    case 'I':
      gKi += KI_INCREMENT;
      break;
    case 'i':
      gKi -= KI_INCREMENT;
      if(0 > gKi) gKi = 0;
      break;
      
    case 'T':
      gTargetValue += VALUE_INCREMENT;
      break;
    case 't':
      gTargetValue -= VALUE_INCREMENT;
      if(0 > gTargetValue) gTargetValue = 0;
      break;
    
    //Print current settings. Also printed after any of the above cycles.
    case 'V':
    case 'v':
      break;
    
    //Ignore unrecognised characters
    default:
      return;
  }
  
  //Why so complicated? Arduino doesn't include support for %f by default and requires an additional library, so we rip it open manually. Will not support negative numbers or indefinite precision.
  //This one line causes 3026 bytes of ROM to be used, almost half the sketch size...so if you run out of space, disable this (or simplify it).
  sprintf(output, "Target Value: [%3d] Current PWM duty cycle [%3d] Current sensor value [%4d] Kp [%2d.%02d] Kd [%2d.%02d] Ki,Integral Error [.%04d,%d] Idle timeout [%d]\n",
    gTargetValue, 
    gCurrentDutyCycle, 
    gNextSensorReadout, 
    (int)(gKp+0.0001),
    roundValue(gKp*100)%100, 
    (int)(gKd+0.0001), 
    roundValue(gKd*100)%100, 
    roundValue(gKi*10000)%10000, 
    gIntegralError, 
    gIdleTime);
   
  Serial.print(output);
}

void loop()
{
    //User commands waiting
    if(0 < Serial.available())
    {
      //Process one character at a time
      serialCommand(Serial.read());
    }
    
    if(gIdle)
      idleLoop();  
    else
      controlLoop();      
}

Comments

Similar projects you might like

Hacking My Toaster

Project tutorial by javier muñoz sáez

  • 541 views
  • 5 comments
  • 12 respects

Rickroll Box

Project showcase by slagestee

  • 1,561 views
  • 0 comments
  • 6 respects

Gyroscope Fun with NeoPixel Ring

Project tutorial by danionescu

  • 2,551 views
  • 0 comments
  • 10 respects

START: A STandalone ARduino Terminal

Project tutorial by Alessio Villa

  • 1,871 views
  • 0 comments
  • 6 respects

Music Reactive LED Strip

Project showcase by buzzandy

  • 614 views
  • 2 comments
  • 12 respects

Pavlov's Cat

Project tutorial by Arduino

  • 1,077 views
  • 0 comments
  • 3 respects
Add projectSign up / Login