Recently, I finished my first Arduino project, a magnet levitation device. It levitates a magnetic object under an electromagnet. What makes it different from similar projects is the position sensor for the feedback loop. I use a single Hall effect sensor placed directly on the bottom of the core of the electromagnet. This creates a problem of separating the electromagnet’s field from the levitating magnet’s field. Fortunately, this can be done in software by the microcontroller ATmega168 that is the heart of Arduino Duemilanove, the board that I use to control the levitation. This article gives a detailed description of the project together with source code at https://github.com/rekka/levitation, as I promised.
WARNING: This is an advanced project that requires quite a substantial amount of tweaking and understanding of the software internals to make it work. Please consider this post just a description of what I did. Furthermore, it is more than 3 years old and therefore hopelessly outdated; the software most likely needs nontrivial changes.
This is my first attempt to describe something that I don’t have much experience with. If you find an error, if there is something that is not clear or if there is something I could improve, please, leave a comment.
The general setup is quite straightforward. The main part is an electromagnet consisting of a coil on an iron core. The current through my coil from a push-type solenoid is 300mA at 12V. At the bottom of the electromagnet, there is a Hall effect sensor directly on the iron core, positioned in such a way that the sensor detection axis is aligned with the core axis (figure on the right).
- Arduino Duemilanove
- Switched power supply 12V
- Linear Hall effect sensor Honeywell SS19
- Norton operational amplifier MC3401P
- NPN transistor MPSA06
- Rectifier 1N4001
- Electromagnet (I used a coil from a 12V push-type solenoid)
- Resistors 2x 1k, 5k6, 47k, 68k, 330k, 4x 1M
- Capacitors 2x 1μ
Since I don’t have much experience, I tried to keep the circuit as simple as possible. The project consists of two quite independent parts.
The first part is the coil driver. I used a small transistor to turn the coil on or off, added a reverse-biased diode across the coil for protection of the transistor against fly-back currents, and put a capacitor across the supply for noise reduction. The base of the transistor is connected to a Arduino digital output through a 1k resistor, with an extra LED to indicate the pin state.
The second part is a bit more involved. I used linear Hall effect sensor SS19 from Honeywell. It is a tiny black box that has 3 pins, 2 of which are connected to GND and +5V, the 3rd one is the output. The sensor translates the perpendicular (to the two largest faces of the sensor) component of a magnetic field into voltage on the output. In my case, it was 2.15V with no field and 3.0 V with the maximal field (coil on and magnet nearby). Thus I attached 2 Norton operational amplifiers (out of 4 in MC3401). The first stage subtracts ~1.5V while the second amplifies it by a factor of ~3. That gives a signal in the range 1.8 — 4.5V, in the working range of the amplifier. This amplified signal then connects to an Arduino analog input pin. Note that I used a Norton op amp. It amplifies current difference, unlike the usual op amp that amplifies voltage difference. That’s why the wiring is slightly different. I also had to add a load resistor 5k6 on the sensor output to make it work properly.
A bit of theory
The magnetic field needed to keep a magnet from falling changes with the distance from the electromagnet. In our case, the position of the magnet is monitored by the Hall effect sensor. The closer the magnet is to the sensor the higher voltage is on the output. And the closer the magnet is the smaller field is required to keep it from falling. The situation is shown in the following figure:
The purple straight line represents the magnetic field required to keep a magnet from falling with respect to the reading on the sensor. In reality, it is nonlinear curve but that’s not important for us. We need to produce a field that will keep a magnet at a specific position. For that we will modulate the coil’s power so that the field produced is given by the blue line in the previous plot. The two extreme values are the maximal field of the coil and the field when the coil is off. If we are successful, the magnet should be stable at the intersection of the two curves in the middle of the plot. If the magnet gets too close to the iron core the shear attractive force of these two is enough to attract it. That is represented by the rightmost intersection. We need to prevent this situation.
That sounds quite easy, doesn’t it? But there is a small complication. The reading on the sensor includes the magnetic field of the coil. In fact, because the sensor is right on the core this component will probably be bigger than the component we are interested in, the field of the magnet itself. Fortunately for us, we know what signal we use to drive the coil. However, if we plot the driving signal and the magnetic field of the coil, we get the following plot:
This behavior is caused by the large inductance of the coil. When the transistor is open, the there is +12V on the coil. But the change in current induced magnetic field that resist this change and it takes more time for the full current to flow. When the transistor is closed, the current continues to flow through the diode in parallel. The resistance of the coil and the diode disipates the energy and the current decreases to zero. In my case, it takes around 5ms for the coil to energize or deenergize. That is way too long for us to ignore. But we can model this behavior. A coil can be approximated as a resistance R and inductance L in series. The circuit can be simplified like this:
The differential equation for the current I through the coil then reads
where V is the voltage on the coil. In our case, it is +12V when the driving signal is 1 and -0.6V (the diode voltage) when the signal is 0. The magnetic field is proportional to the current. For simplicity, we rewrite the equation for dimensionless field power y and applied voltage x, x being -0.05 = -0.6/12 or 1 and y being in the range 0 — 1. That yields the equation
λ is a parameter depending on the coil. But this is a equation for a low-pass filter. This continuous form can be discretized, see Low-pass filter on wikipedia. Assuming constant timesteps, Δt = 1, we get the final formula
The parameter α depends on the coil and on the timestep. It can be found using
where T is the number of timesteps it take for the coil to energize from 0 to 0.632 of its maximal field. This has to be found by some experimentation. Generally, α is different for the energization and deenergization phase since the diode gives rise to extra resistance in the circuit.
Now with some theoretical background, we can proceed to describe the algorithm that controls the output signal. First, we have to find the following parameters (field shall denote the reading on the sensor):
- sample frequency = how often to sample field and adjust the output signal, I use 10kHz
- baseline = field with no magnetic fields
- coilMag = the maximal strength of the coil, i.e. the difference of field with the coil on and baseline
- alphaInc = the constant α from above for energization of the coil
- alphaDec = the constant α from above for deenergization
We have to keep track of the following variables:
- signal = last signal ouput, 0 or 1
- filter = value in the range 0 to 1, this is the field of the coil in the last step, the result of the discretized formula.
With these parameters, we perform the following steps with the sample frequency:
- Update the filter
- If ouput == 1, use filter += alphaInc(1 – filter),
- else filter -= alphaDec(filter + 0.05).
- Read the sensor input, field. Based on this value, find the value of the permanent magnet’s field alone, mag = field – coilMag * filter – baseline.
- Using the value mag that represents the distance of the magnet from the sensor, estimate the required coil power required to keep the magnet from falling, power. This is estimated based on the simple model shown in Figure 1.
- Produce new signal.
- If power > filter, set signal = 1 (need to energize)
- else set signal = 0 (need to deenergize)
I should make some remarks on my software implementation:
To achieve a constant sampling and output rate, some constant time-base is necessary. The standard millis() method doesn’t allow for the desired precision. One option is to use the build-in timers, like millis() is using, and change their resolution. But the simplest way is to use the A/D converter itself for timing purposes. The time it takes for ADC to perform a reading is constant and can be changed by changing the ADC prescaler settings. It is given by the formula
clock = 16,000,000 prescaler = 2, 4, 8, 16, 32, 64 or 128 conversionCycles = 13 * clock / prescaler;
The default prescaler value is 128, which gives the maximal conversion rate to be ~~9.6kHz. I’m using 64 which leads to 19230.8Hz sampling rate. The ADC clock value, clock/prescaler, affects the ADC accuracy. ATMEL recommends the range 50-200kHz for maximal accuracy, with rates up to 1MHz if less accuracy is needed. My choice gives 250kHz ADC clock, that’s quite close to the recommendation.
Now for the synchronization. When a conversion is finished, an interrupt flag in the ADC registers is set. Waiting for this flag provides the necessary synchronization. To ensure that the next conversion is started as soon as one is finished, the ADC is configured to work in a free running mode with an auto-trigger enabled. The functions analogSetup and analogStart configure the ADC and start the first conversion while analogNext waits for a conversion to finish and thus provides the synchronization for the program.
The coil simulation is done by fixed-point integer math. That means that values of filter, alphaDec and alphaInc in the range 0 — 1 are multiplied by 2^16 = 65536. When a multiplication such as coilMag * filter is performed, the actual code is
result = coilMag * filter >> 16;
Since AVG GCC is adding extra unnecessary instructions for multibyte multiplications like this, I implemented my own assembler routines. This is not really required for this simple code, but I also experiment with additional digital signal processing to achieve greater stability of the magnet, and this processing is quite costly in terms of multiplications. Also, my own multiplication routine allows me to perform rounding of the result of >> 16 and that adds extra precision to the computation.
The Arduino performs all computations but the code is written in such a way that it is necessary for it to be connected to the computer. The levitation routine is initiated when Arduino receives 32 bytes from the computer. These 32 bytes must contain 16 integers and are written in the array ap. The significance of the individual fields is
ap = alphaInc * 2^16 ap = alphaDec * 2^16 ap = 0.05 * 2^16 ap = lowestMag ap = powerDecay ap = counter
alphaInc and alphaDec are the constants from above, 0.05 = 0.6/12 is the diode voltage. lowestMag and powerDecay are two parameters of the linear function used to compute the power of the coil magnetic field using the formula
power = 255 + (ap - (mag)) * ap;
The levitation routine is stopped when a byte 0 is received by Arduino.
Finding the constants
Since the constants alphaInc, alphaDec and diode voltage seem fixed, I enter them by hand. To find their value, I simply guess, it is not that hard. To control the Arduino code and to monitor the performance, I use a simple Mathematica program. It looks like this:
The plots allow me to see the sensor reading, the signal output and the computed field of the permanent magnet (the three lines from top to bottom). To find a value alphaDec, for example, I guess some value and use the counter setting (ap) to be 10. This causes the signal to be a simple square wave. Then I take a look at the computed magnet’s field. With no permanent magnet around, it should be constant and the plots should look like this:
If I guess the value too low or too high, I see some bumps like:
Finally, here is the code:
To make it work with the 1Mbaud communication, you will also have to
download check my optimized Serial library, wiring_serial.c (the library is too outdated now. Fortunately, the official Arduino libraries have been updated and the issue has been fixed, it seems.)
With the current implementation, the project faces a serious problem, instability. It works well for levitating a magnet for short time, minutes at most. After a while, oscillations develop and a magnet eventually falls. The reason for this is the lack of energy dissipation, or damping. The magnet under an electromagnet can be approximated by the equation
where y is the distance from an equilibrium. In words, the higher the magnet is, the closer it is to the electromagnet’s core and the stronger it is attracted. The lower it is, the further from the core, the less attraction there is. This system is unstable, the solutions of the equation diverge. By applying power to the electromagnet, we are trying to modify the equation to have the form
where c is some positive constant. Now this equation is stable, it is a harmonic oscillator. If there is a oscillation, it will be preserved, but it won’t grow. The problem appears when there is a delay between reading of the sensor and producing new output. And this will be there in every real system. This translates into appearance of a new term in the equation
Again, a is a positive constant. Since –a indicates how much oscillations are damped, this term will increase the osciallations. The question is how to fight this term. I experimented with a couple ideas that I had, like diferentiation or integration of the signal, or using some other more involved filters. So far, I wasn’t able to produce a consistent oscillation damping. But research continues.