Monday, July 27, 2015

Yet Another Arduino Magnetic Levitator

I'd wanted to build a magnetic levitator since high school. I finally got off my butt and got around to putting one together recently. It was totally worth the effort and is thoroughly, as the kids say, the bee's knees. Here's how I put it together.

The general plan is to have an electromagnet whose magnetic field strength varies with the distance the magnetic levitating object, such that the distance is maintained constant. The distance from the levitating object is monitored with a Hall effect sensor. A second Hall effect sensor on the top of the electromagnet detects the magnetic field from the electromagnet and corrects the first sensor's reading to remove the influence of the electromagnet. The current through the coil is constantly adjusted to maintain a proper distance.

First, the electromagnet. I had a steel bar about 20 cm long by 1 cm diameter laying around. A soft iron core would have been better, but not free. I had several small spools of magnet wire, including 22, 26, and 30 AWG wire. The power supply I planned to use was a ATX power supply with 3.3V, 5V, and 12V taps. According to the Wikipedia page, magnet wire can typically handle 2.5-6 A/mm^2, depending on surroundings. Assuming the least conservative ampaciy, this corresponds to:

30 AWG: Current = 6 A/mm^2 * 0.0509 mm^2 = 0.3 A
200 feet at 103 ohm/1000 ft -> about 21 ohms resistance

V = I R = 0.3 * 21 = 6.3 V

26 AWG: Current = 6 A/mm^2 * 0.129 mm^2 = 0.8 A
75 feet at 40.81 ohm/1000 ft -> about 3.6 ohms resistance

V = 0.8 * 3.6 = 2.9 V

22 AWG: Current = 6 A/mm^2 * 0.326 mm^2 = 2.0 A
40 feet at 16.14 ohm/1000 ft -> about 0.65 ohms resistance

V = I R = 1.3 V

Ampere's law says that the strength of the magnetic field is proportional to the number of turns times the current through the wire. Since we want the maximum magnetic field we can get, if we multiply the lengths I had on hand by the max current in each wire, we get:

30 AWG : 0.3 A * 200 ft = 60

26 AWG : 0.8 A * 75 ft = 60

22 AWG : 2.0 A * 40 ft = 80

To get the strongest magnetic field, I should have chosen the 22 AWG wire. However, the length of wire I had has a max voltage of only 1.3 V, which would be rather difficult to control with a power supply with a minimum output of 3.3 volts. I'd need to keep the PWM down to a low level just to limit the current through the wire, leaving less room for control. The 30 AWG and 26 AWG wire should give the same magnetic field strength with voltages much closer to what the power supply puts out, so I arbitrarily chose the 30 AWG wire to use for the magnet.

I wrapped the steel bar with the 200 feet of the 30 AWG wire, which on a 1 cm diameter should have been a bit short of 2000 or so turns.

L = 200 ft 
= 6096 cm 
= # turns * circumference per turn 
= # turns * Pi * diameter

# turns = 6096 cm/ Pi / 1 cm 
= 1940 turns

Since there were several layers of wire on the bar, the actual number of turns is probably somewhat smaller, but should be good enough.

I wrapped the coil by placing the bar in the chuck of a drill, taping the beginning of the wire to the bar, and running the drill, similar to this. I didn't take any particular care to make sure it wrapped smoothly, since Ampere don't care if it looks pretty, as long as it's got the loops.


Once I had the electromagnet coil wound, I made a wooden frame to mount the electromagnet in. I drilled a hole through a 1x2 and cut a slit through the hole and the wood. I then drilled a hole on the short side and put a machine screw through the hole, and attached a wing nut on the opposite end. This let me tighten down on the coil to hold it in place.

I placed the coil in the frame and attached two linear Hall effect sensors I'd bought from Digikey. These sensors output a 0-5V signal based on the strength of the magnetic field in their vicinity. The Hall sensor on the top of the electromagnet measures the strength of the magnetic field from the electromagnet. The Hall sensor measuring the distance to the floating magnet will also see the magnetic field from the electromagnet. Since we vary the strength of the electromagnet's field to keep the floating magnet steady, we need to compensate for this field to get a good estimate of the actual distance to the floating magnet. This could probably also be accomplished by running the electromagnet through its full current range and measuring the Hall reading at each point, then creating a calibration curve from these readings, but I chose to go the two-sensor route.

Tip - twist wires together with a drill to keep them organized. I first saw it here.

The circuit itself is pretty darn simple. It's just an n-Mosfet to control the coil, a flyback diode, two Hall sensors, and a potentiometer to adjust the distance between the floating magnet and the bottom of the electromagnet.

I'm proud of this soldering job. So fresh and so clean clean! Outkast would be proud, were he a worker in an electronics factory.

I used the Arduino PWM function to control the n-Mosfet to turn the coil on and off. The real heart of the project is the controller to take the Hall reading and output a PWM current to the coil. The controller has to work crazy fast, adjusting the current hundreds of times per second to keep the magnet stable. I ended up using a PID library to manage the calculation. Getting the PID parameters right took more time than the rest of the project combined. The final parameters have the coil power buzzing around like a pissed off yellowjacket, but at least it works!

I used the serial port to download Hall sensor readings and power output data to tweak the parameters. This chart is how the position of the magnet moves while floating with the final, working, PID parameters. I pulled the magnet out at the very end of this snapshot. The time axis on this chart is in milliseconds, so this entire span is only about a second and a half. Note how quickly the controller has to react to changes in the magnet position - it crosses the setpoint (of 150) about 40 times per second.

Another thing - the duty cycle of the coil while it's floating is only about 60%, meaning that, on average, the coil is only on about 60% of the time. This means that we can actually get away with a higher peak current through the coil than originally premised. Using the full 12V output from the power supply while the magnet is floating causes the coil to get a little warm, but definitely not hot enough to be concerning.

Here it is in action!

Here's the code:
>#include <pid_v1.h>  //PID control library

int Coil = 5; //PWM output pin that controls transistor for coil

double Setpoint, Input, Output; //PID variables

int PotPin = A2;    // select the input pin for the potentiometer

int TimeConst = 1;  //PID calculation frequency in milliseconds

int potValue = 0;  // variable to store the value coming from the pot

//Specify the links and initial tuning parameters for PID controller
double Kp=5, Ki=4, Kd=.069;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);

int HALLA = 0; //Pin for reference hall effect sensor on top of coil

int HALLB = 1; //Pin for hall effect sensor on bottom of coil

int StopThreshold = 100; //Threshold below set point at which magnet will be turned off (if the floating magnet falls off or is removed, don't leave the coil just going full blast)

float CALSLOPE; //Slope of calibration curve for hall sensors

float CALINTERCEPT; //intercept of calibration curve for hall sensors

//variables for hall effect sensor calibraction
float CALSLOPEtemp = 0; 
float CALINTERCEPTtemp = 0;
float XA;
float YA;
float XB;
float YB;

float CorrectedVal; //Compensated hall reading

int Power; //Coil PWM variable

void setup(){
   //Reset PWM frequency to the coil pin to 62500/16 = 3906 Hz
  setPwmFrequency(5, 16);
  //Set PID sample time to defined value. Default is 200 ms, which is way too slow.
  Serial.begin(115200); //Need to transmit serial at 115200 bps to keep up  
//Calibrate sensors by reading them when the coil is off, then reading them while coil is on. This allows you to compensate for the coil's magnetic field and just identify the magnetic field due to the floating magnet.
 for (int i=0; i < 10; i++){
  digitalWrite(Coil, LOW);
  XA = analogRead(HALLA);
  YA = analogRead(HALLB);
  analogWrite(Coil, 255);
  XB = analogRead(HALLA);
  YB = analogRead(HALLB);
  //Two point slope-intercept form
 //Average slope and intercept
  digitalWrite(Coil, LOW);
  //arbitrary starting setpoint. We'll reset it as soon as we enter the loop
  Setpoint = 45;
    //turn the PID on

void loop(){
   //Read potentiometer to get setpoint desired
  potValue = analogRead(PotPin);
  Setpoint = map(potValue, 0, 1023, 0, 255);
   //Read hall effect sensors five times in a row, take average corrected reading
  for (int i=0; i < 5; i++){
    CorrectedVal =CorrectedVal + CALSLOPE * analogRead(HALLA) + CALINTERCEPT - analogRead(HALLB);
  CorrectedVal = CorrectedVal / 5;
  //Turn off coil if the magnet is nowhere nearby
  if (CorrectedVal < Setpoint - StopThreshold) {
    digitalWrite(Coil, LOW);
  } else {
    //Otherwise, do PID control to keep floating magnet at setpoint
    Input =CorrectedVal;
     //Take PID output and apply to coil
     Power = Output;
     analogWrite(Coil, Power);
     //Print out timestamp, coil power, corrected hall effect reading, and target set point for hall effect reading.
    Serial.println(String(millis()) + ", "+String(int(Power)) + ", " + String(int(CorrectedVal))+ ", " + String(int(Setpoint)));  

void setPwmFrequency(int pin, int divisor) {
  byte mode;
  if(pin == 5 || pin == 6 || pin == 9 || pin == 10) {
    switch(divisor) {
      case 1: mode = 0x01; break;
      case 8: mode = 0x02; break;
      case 64: mode = 0x03; break;
      case 256: mode = 0x04; break;
      case 1024: mode = 0x05; break;
      default: return;
    if(pin == 5 || pin == 6) {
      TCCR0B = TCCR0B & 0b11111000 | mode;
    } else {
      TCCR1B = TCCR1B & 0b11111000 | mode;
  } else if(pin == 3 || pin == 11) {
    switch(divisor) {
      case 1: mode = 0x01; break;
      case 8: mode = 0x02; break;
      case 32: mode = 0x03; break;
      case 64: mode = 0x04; break;
      case 128: mode = 0x05; break;
      case 256: mode = 0x06; break;
      case 1024: mode = 0x7; break;
      default: return;
    TCCR2B = TCCR2B & 0b11111000 | mode;



  1. Hi, nice job.I have a cuestiĆ³n. If I put a magnet over the top hall sensor, would the code work as well? If so, what parameters could be affected? Thanks

    1. It depends. Would the magnet be moving or stationary? If stationary, yes, the code should work. It will see a constant magnetic force that it will cancel out when it performs the initial calibration. If the top magnet is moving, the top hall effect sensor will not be able to properly cancel out the electromagnet's effect on the bottom magnet.

    2. Hi, thanks for your reply. It would be stationary, in order to increase the Setpoint.

      I also want to do the PID calibration by mapping the Output to
      MyOutput = map(Power, 0,255, SetPoint*.5, SetPoint*1.5)

      and try the Ziegler Nichols tunning through this process:
      1. Place the magnet close to the SetPoint
      2. Kd, Ki = 0
      3. Change the setpoint to find the constant proportional Kg that gives the more stable wave , and then do the calculations
      Serial.print(" ");

      I guess MyOutput will move along with the setpoint in order to find the optimum
      I am having trouble with the map, since the data is double, but I don't know yet how to do so.

      Do you think my aproach should work?


    3. It should work then if it's stationary.

      For mapping double (or float) values, I've used the function here before -

      I did all of the tuning for mine by brute force, and it took a really long time. Every time I changed something (different power supply voltage, different magnet), I had to go through the entire process again, so I'd be interested to hear if you have success with the Ziegler Nichols technique.

    4. Hi there again...
      I am about to change it to ir-led, because the correctedval does not seem to respond to the desired speed. Am I supposed to do the average calculations with the ball removed or placed at the desired setpoint? I tried to used instead HallB- HallA readings, but it goes up and down before moving. I've also tried with matlab and simulink, system identification process...but my skills seem to reach just linear models, not non-linear models. I have quite a good hint about the values, but I got saturation. Any help??

    5. Yes, I did the calibration calculation average with the levitating magnet removed from the coil. That corrects for any movement in the hall effect sensors that affects their sensitivity, and tells the program what the hall effect sensors would see without any additional magnets, so it can figure out what the effect of the magnet is.

      If the heuristic tuning techniques aren't working, I'd recommend just trying to brute force it. Keep the derivative and integral values at zero, and find a proportional value that works the best (it still probably won't work well enough for it to be stable). Then, slowly increase the integral coefficient until it works slightly better. Finally, add the derivative coefficient and tweak the other two coefficients around until it works.

      If that still doesn't work, it is possible that the power supply and coil combination aren't strong enough to make any combination work. Mine just barely works - any lower voltage on the coil, or fewer turns on the electromagnet, and it probably wouldn't function regardless of the tuning. You might want to give some attention to improving the electromagnet part if the tuning just isn't working out no matter what parameters you try.

      I also found that the mass and shape of the levitated object has a big impact - wide, well balanced objects like the screwdriver in the picture were much easier to make stable than small compact objects like a single bare magnet (which I never managed to keep stable).

  2. Hi Peter,

    Just to tell you I finally made it! I used ir led sensors instead, but my setup includes both (hall & ir leds). You can have a look here:

    Thanks a lot for your help!


  3. Sorry, here 