SimpleFoc Sensor Eccentricity Calibration

Sensor Eccentricity Calibration

Hi all, I’ve recently managed to implement a ‘Sensor Eccentricity Calibration’ for SimpleFoc with the help of Richard. The calibration is based on the blog post from Ben Katz (BuildIts in Progress: Encoder Autocalibration for Brushless Motors: Offset and Eccentricity).

So, what is the goal of this calibration? When you mount your (magnetic) sensor on your frame or motor, there will always be a slight misalignment between magnet and sensor (measurement system). This misalignment between center of rotation and the center of the sensor is called the eccentricity error.

As a result your measurement system output is non-linear with respect to the rotor of the motor. This will cause an error with respect to the ideal torque you attempt to create with the I_q vector as function of the position. You could interpret this as a disturbance on your control loop which you want to minimize for optimal performance. This calibration compensates the sensor reading in a feed forward fashion such that your performance improves.

disclaimer: I’ve not peer reviewed this content so it may not be 100% accurate. I want to post it anyway so people may continue the work or want to test it already (see final chapter). I’m away from SimpleFoc for some time because my son was born last week :).

What does the calibration do?

  • Find the direction of sensor.
  • Move open loop a mechanical 2PI positive rotation (trace sensor angle & electrical angle) and calculate the difference.
  • Move open loop a mechanical 2PI negative rotation (trace sensor angle & electrical angle) and calculate the difference.
  • Store the average electrical angle offset
  • Post process the data (from forward/backward rotation) to find the error between sensor angle and electrical angle.
  • Build the eccentricity LUT (Look Up Table).
  • Save this LUT to compensate your calibrated sensor output.

What is the output of the calibration?

The LUT represents the amount of radians the sensor is wrong with respect to the actual electrical angle. The CalibratedSensor class will read the uncalibrated sensor angle and adds the (interpolated) offset it gets from the LUT. Note that this is a relative measurement between sensor angle and electrical angle. This calibration assumes the electrical angle to be the reference because we want to compensate the sensor towards the correct electrical angle.

This error data is filtered as implemented by Ben Katz and removes the error caused by cogging, which is unrelated to the eccentricity. More details can be found in his blog post.

In my example the peak-to-peak error is around 0.12 rad. However, since this motor has seven pole pairs, the error it makes during commutation is 7x0.12 = 0.84 rad in electrical angle, which is significant. As you can imagine this becomes more problematic if you have high pole pair motors or if your sensor is mounted more eccentric since the commutation error is NPP*Eccentricity Error. Disclaimer: the performance increase is not so easily observable if you have a well aligned sensor and a low pole pair motor.

Figure 1 Eccentricty error (Look up Table)

What is the impact on the actual torque output?

In the graph below I visualize the impact of the position dependent measurement error on your actual torque output. In the top graph the optimal FOC output for the three phases is represented. In the second graph I plot the eccentricity error as discussed in the previous section.

In the bottom two plots you see the impact of this eccentricity on the actual torque that is generated. In my case, where I deliberately mounted the sensor with an offset of 0.5 mm, the efficiency will drop down locally to below 90%. Also important to understand is that this disturbance is position dependent and as you will rotate faster, the disturbance will become more high frequent and it will become more troublesome for your feedback controller to compensate. Therefore, you want a known an error like this in advance such that we can compensate it in a feedforward fashion.

Figure 2 Simulated Torque Actual versus Ideal

What is the result of the calibration on the actual velocity for a Torque Control Example?

I’ve tried to visualize the impact of this calibration with a Torque control example where the control loop is trying to keep the voltage constant. In a perfect system, a constant voltage would result in a constant velocity.

As you can see in the graph below the disturbance (eccentricity) is mostly compensated for by our calibrated sensor class. As the target voltage increases and thus the velocity, the disturbance impact becomes larger (it becomes more high frequent). After this calibration the velocity becomes ‘very’ constant (red) as compared to a non-calibrated system (blue).

Table 1: Calibration Performance 2V

Test Average Velocity Standard Deviation
Uncalibrated Sensor Target 2V 38.22 6.71 rad/s
Calibrated Sensor Target 2V 50.09 0.62 rad/s

Figure 3 Calibrated and Uncalibrated Velocity 2V

Table 2: Calibration Performance 5V

Test Average Velocity Standard Deviation
Uncalibrated Sensor Target 5V 73.14 rad/s 9.23 rad/s
Calibrated Sensor Target 5V 78.78 rad/s 1.34 rad/s

Figure 4 Calibrated and Uncalibrated Velocity 5V

Table 3: Calibration Performance 10V

Test Average Velocity Standard Deviation
Uncalibrated Sensor Target 10V 87.47 rad/s 12.58 rad/s
Calibrated Sensor Target 10V 94.85 rad/s 1.86 rad/s

Figure 5 Calibrated and Uncalibrated Velocity10V


Some work yet to be done is to quantify the improvement on actual achieved velocity. You can see that the shaft velocity improves most and that there is still some variation in the uncalibrated sensor output. Note that the spikes in the calibrated velocity are likely a bug with the getVelocity() function being called twice.

Both figures are with a target velocity of 5V.

Figure 6 Uncalibrated Velocity 5V shaft_velocity and sensor output
Figure 7 Calibrated Velocity 5V shaft_velocity, sensor and CalibratedSensor output


First of all, if you sensor is well aligned and you have a low pole pair motor you likely will not notice the improvement that easily. Anyway, two video’s with and without calibration.


Uncalibrated Phase Voltage


Calibrated Phase Voltage

How can I test this code?

It is a pretty raw version of the code and not optimized for memory/performance. So it should likely be improved in the future.

All you need to add to your code is the following. Instantiate the CalibratedSensor class directly after your normal sensor class. In the setup you choose your open loop voltage for this calibration (don’t start too high) and you call the calibration. After the calibration you link the sensor to your motor and you are ready to roll.

I’ve tested it with both the AS5074 and AS5600 with different boards (STM446RE/Arduino) and different drivers (DRV8302 and SimpleFocShield v2)

The Calibration will be made part of the dev branch in the nearby future and can be found here:

An example can be taken from here:

Until it is available you in the dev branch you can get it here:



Amazing work, I will definitely test this out and look into the code, ofcourse the difference is quite a lot because the magnet was placed wrong on purpose but great improvement anyway!

Thanks for your work.


Carelsbergh Stijn

1 Like

What amazing work, and what an excellent detailed write-up!

Thank you so much, @marethyu ! :heart: :heart: :heart:

We’re working on a SimpleFOC release which will bring all this to the released versions of SimpleFOC and SimpleFOC drivers, so soon it should be even easier to try it out!


Tested this last night just by copy-pasting the .cpp and .h into my Arduino Project and it worked right away! Haven’t done any tests to measure the improvements with this, but this is something I’ve been wanting to test and I’m happy you decided to implement it! One feature we might want is the ability to save the LUT and if provided skip the calibration. :slight_smile:

1 Like

Hi David, Awesome that you have tested it already! Good to hear it worked. What you mention would indeed be a nice feature to store it in memory. As long as you don’t touch the hardware, it should not (significantly) change.

1 Like

Hey @marethyu!

I’ve just tested the code and it works wonders!
I am impressed :smiley:
Great work

1 Like

Would be super happy if this would make it into the main repo. I’m currently running my own version of the eccentricity LUT: Stepper FOC with just 12bit encoder - #8 by aokholm didn’t have the time to make such a well documented test and write up!

Using a stepper motor this is almost required to get decent performance!

I’m trying this and I’m getting some rather baffling results.

The forwards direction calculates errors fine, but during the reverse section it does stuff like this:

Columns: i, elec_angle/_NPP, theta_actual, error_b[i])

00> 442 0.84675 0.83142 0.00000
00> 443 0.83448 0.81915 0.00000
00> 444 0.82220 0.80381 ovf
00> 445 0.80993 0.79460 24611.71875
00> 446 0.79766 0.78540 0.00000
00> 447 0.78539 0.77926 0.00000
00> 448 0.77312 0.77006 0.00000
00> 449 0.76085 0.76392 0.00000
00> 450 0.74858 0.75779 0.00000
00> 451 0.73630 0.74245 420.50125
00> 452 0.72403 0.72404 -0.00000
00> 453 0.71176 0.70563 ovf
00> 454 0.69949 0.69029 9118912.00000
00> 455 0.68722 0.66882 0.00000
00> 456 0.67495 0.65961 0.00000
00> 457 0.66268 0.65041 2.41168
00> 458 0.65040 0.64120 ovf
00> 459 0.63813 0.63507 0.00000
00> 460 0.62586 0.62586 0.00208
00> 461 0.61359 0.61973 0.00000
00> 462 0.60132 0.60132 0.00000
00> 463 0.58905 0.58291 2.03771
00> 464 0.57678 0.56450 ovf

As you can see, it’s… wrong, or giant, or “ovf” (which puts NANs in the table, resulting in the sliding window basically wiping the entire table since any ovf values within the window compute a NaN…

I’m very confused. I tried reducing the size of the various buffers n_ticks = 64*_NPP, which is 7 in my case.

Any ideas what’s going on? I’m using the Mosquito board with an stm32g031F8Ux.

Mystery seems somewhat solved: The micro only has 8kb of ram, so dynamically allocating 3584 bytes (128float74byte/float) plus another 512b for the calibrationLut was running the micro out of memory. The platformio advisory says RAM: [===== ] 50.4% (used 4132 bytes from 8192 bytes) on compile. Ignoring everything else, just the statically allocated ram at compile time plus the amount allocated when calibrate is called puts me over :confused: .

I can stream the values to serial, compute the calibrationLut off the micro, and then set them in a const float calibrationLut[128] = {}, although I haven’t done this yet. My first attempt to do so was really messy and doesn’t seem to have worked.

Great work! Thank you!

Looking at this off the micro… I’m not entirely sure what I’m looking at :frowning:

Forward and backward error seem really consistent which eachother, but I was expecting maybe a bumpy S curve. I don’t think a 128 value LUT is going to do anything productive here. Any ideas where the high frequency oscillation is coming from?

(Green shows a moving window average +/-63. I’m not handling wrapping around at the ends properly, but it illustrates the problem well enough)

1 Like

Hi, nice that you got it working. This calibration is intended for the low frequent part so the 128 LUT is (probably) sufficient.

The error you find you need to multiply with the number of pole pairs to end up with the error you make in the electrical angle ‘coordinates’.

The high frequent variations are likely Cogging. The interaction between the coils and the stator. To calibrate that we need a similar procedure but then we need to log the current usage at each encoder increment. That LUT would likely need to have larger resolution

You can actually count the peaks over one revolution and that should roughly equal your pole pairs. However you also have secondary peaks. So it is not always clear, I think

There is actually interpolation happening between LUT points, so you don’t need that many points.

I would recommend a new forum thread for this. I had similar NaN problems, but that appeared to be caused by the PWM sensor code not wrapping around properly. For some reason when the sensor value was very slightly higher than 2pi it caused an entry in the lut which later led to a nan.

In my experience the high frequency noise comes from phase electromagnetic field influencing magnetic angle sensor.

That is, if it was only cogging, you would expect the noise to disappear if data was captured every full step for 200 steps, but it doesn’t.

The noise can be eliminated fully when data is captured every 50 full steps. This is because every time the same phase is actuated with the same polarity.
However I imagine the data to be skewed slightly if one does it so perhaps capturing 200 points and then reducing to 50 points by doing postprocessing averaging would be optimal.
In practice none of this matter and I don’t see any difference between 400, 200, or 50 raw points. Even 25 was still smooth. And the calibration can be faster when we stop rarely. And it takes less memory.

Side note, I am basing this on my own implementation and not simplefoc. I also do two passes - forward and backward, because of what I believe is some mysterious magnetic hysteresis.