Hackster is hosting Hackster Holidays, Ep. 6: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Monday!Stream Hackster Holidays, Ep. 6 on Monday!
Hao Jie Chan
Published © GPL3+

Magnetic Levitation

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

IntermediateShowcase (no instructions)12 hours55,934
Magnetic Levitation

Things used in this project

Hardware components

Arduino UNO
Arduino UNO
×1
Power MOSFET N-Channel
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
Resistor 221 ohm
Resistor 221 ohm
For the LED.
×1
Resistor 221k ohm
Resistor 221k ohm
Connected to the Gate of MOSFET to switch it off faster.
×1
LED (generic)
LED (generic)
As an indicator for the strength of electromagnet.
×1
1N4007 – High Voltage, High Current Rated Diode
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

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

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.

Code

Magnetic Levitation

Arduino
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();      
}

Credits

Hao Jie Chan

Hao Jie Chan

2 projects • 100 followers
An electronics hobbyist very interested in the vast world of electronics.

Comments