A lightweight library for magnetometer calibration and compass heading computation suitable for microcontrollers. Supports ellipsoid fitting and Min/Max diagonal calibration with optional tilt compensation.
This library calibrates for hard iron and soft iron distortions using ellipsoid fitting, based on the work of sailboatinstruments1.
-
Dual calibration methods
- Ellipsoid fitting: accurate multi-axis calibration using least-squares optimisation
- Min/Max diagonal: simple fallback method for resource-constrained scenarios
-
Tilt compensation: optional accelerometer-based tilt compensation
- Magnetometer (QMC5883L, AK8963, HMC5883L, etc.)
- Optional: accelerometer or 6-axis IMU (for tilt compensation)
- MCU: Arduino, ESP32, etc
This library gives very similar result to the Magneto 1.2 app . See ellipsoid.test folder
void compassConfigureReferenceField(float reference_field_gauss, float lsb_per_gauss);Set the reference magnetic field strength and magnetometer sensitivity.
Parameters:
reference_field_gauss: expected magnetic field magnitude. Find it here.lsb_per_gauss: sensor sensitivity (e.g. for QMC5883L at +/- 8 Gauss range: 3000 LSB/Gauss). Only affects global gain, not direction, for compass use can leave at default.
bool compassCalibrateFromSamplesWithFallback(
const float* mag_samples_xyz, // Flattened [x, y, z, x, y, z, ...]
size_t sample_count,
CompassCalibrationMatrices* out_matrices,
CompassCalibrationFitMethod* out_method
);
bool compassScoreCalibrationQuality(
const float* mag_samples_xyz,
size_t sample_count,
const CompassCalibrationMatrices* matrices,
CompassCalibrationQuality* out_quality
);Run calibration on collected samples. Automatically tries ellipsoid fitting first; falls back to Min/Max if ellipsoid fails. Rotate in all directions during data collection.
Parameters:
mag_samples_xyz: Array of raw magnetometer readings (3 floats per sample, minimum 50 samples required)sample_count: Number of (x, y, z) tripletsout_matrices: Output struct containing hard_iron, soft_iron, reference field, and validity flagout_method: Enum indicating which method succeeded (Ellipsoid or Min/Max)
Returns: true if calibration succeeded, false otherwise
CompassCalibrationQuality quality = {};
bool scored = compassScoreCalibrationQuality(
samples.data(),
samples.size() / 3,
&matrices,
&quality
);
if (scored && quality.is_valid) {
Serial.print("Samples used: ");
Serial.println(quality.used_sample_count);
Serial.print("Calibration score: ");
Serial.print(quality.score_percent, 1);
Serial.println(" %");
Serial.print(" Raw span: ");
Serial.println(quality.raw_span_score, 1);
Serial.print(" Octant coverage: ");
Serial.println(quality.octant_coverage_score, 1);
Serial.print(" Unit-vector PCA ratio: ");
Serial.println(quality.unit_vector_pca_ratio_score, 1);
Serial.print(" Radius std: ");
Serial.println(quality.calibrated_radius_std_score, 1);Use this function after calibration to evaluate fit quality.
100%: perfect fit>= 80%: good fit>= 60%: acceptable fit< 40%: bad fit
bool compassSetCalibrationMatrices(const CompassCalibrationMatrices* matrices);
bool compassGetCalibrationMatrices(CompassCalibrationMatrices* out_matrices);Store/retrieve calibration matrices globally for use in heading calculations.
void compassApplyCalibration(const float raw_xyz[3], float calibrated_xyz[3]);Apply hard iron + soft iron correction to raw magnetometer readings.
void compassApplyCalibrationAndTiltCompensation(
const float raw_xyz[3], // Raw magnetometer
const float accel_xyz[3], // Accelerometer
float compensated_xyz[3]
);Apply calibration plus tilt compensation (pitch + roll) using accelerometer data.
Coordinate convention: X = right, Y = down (toward earth), Z = forward (direction of travel).
float compassHeadingFromMagneticVector(const float magnetic_xyz[3]);Convert calibrated/tilt compensated magnetic vector to compass heading (0–360 degrees). Z axis points to direction of travel. Returns: Heading in degrees
struct CompassCalibrationMatrices {
float hard_iron[3]; // Bias offsets: [Bx, By, Bz]
float soft_iron[9]; // 3×3 scale/rotation matrix (row-major)
float reference_field_gauss;
float lsb_per_gauss;
float fitted_field_lsb;
bool is_valid;
};struct CompassCalibrationQuality {
size_t used_sample_count;
float score_percent;
float raw_span_score;
float octant_coverage_score;
float unit_vector_pca_ratio_score;
float calibrated_radius_std_score;
bool is_valid;
};enum class CompassCalibrationFitMethod : uint8_t {
None = 0, // Calibration not run
Ellipsoid = 1, // Ellipsoid fitting succeeded
MinMax = 2, // Min/Max fallback used
Error = 3 // Calibration failed
};Quick workflow:
-
Initialise with calibration parameters
compassConfigureReferenceField(0.49f, 3000.0f);
-
Collect samples
std::vector<float> samples; // Read magnetometer while rotating device for (int i = 0; i < 10000; i++) { samples.push_back(readMagX()); samples.push_back(readMagY()); samples.push_back(readMagZ()); }
-
Calibrate
CompassCalibrationMatrices matrices = {}; CompassCalibrationFitMethod method = CompassCalibrationFitMethod::Error; bool success = compassCalibrateFromSamplesWithFallback( samples.data(), samples.size() / 3, &matrices, &method );Output format (
CompassCalibrationMatrices):hard_iron[3]: bias offsets[bx, by, bz]soft_iron[9]: row-major 3x3 correction matrix[m00, m01, m02, m10, m11, m12, m20, m21, m22]reference_field_gauss: configured local magnetic field in Gausslsb_per_gauss: configured sensor sensitivity in LSB/Gfitted_field_lsb: fitted field magnitude in sensor countsis_valid: calibration validity flag
Save calibration result to NVS after success.
#include <Preferences.h> if (success && matrices.is_valid) { Preferences prefs; prefs.begin("compass", false); prefs.putBytes("hard_iron", matrices.hard_iron, sizeof(matrices.hard_iron)); prefs.putBytes("soft_iron", matrices.soft_iron, sizeof(matrices.soft_iron)); prefs.putFloat("ref_gauss", matrices.reference_field_gauss); prefs.putFloat("lsb_per_g", matrices.lsb_per_gauss); prefs.putFloat("fit_lsb", matrices.fitted_field_lsb); prefs.putUChar("fit_method", static_cast<uint8_t>(method)); prefs.end(); }
-
Apply calibration
if (success) { compassSetCalibrationMatrices(&matrices); float raw[3] = {readMagX(), readMagY(), readMagZ()}; float calibrated[3]; compassApplyCalibration(raw, calibrated); float heading = compassHeadingFromMagneticVector(calibrated); }
-
Score calibration quality
CompassCalibrationQuality quality = {}; bool scored = compassScoreCalibrationQuality( samples.data(), samples.size() / 3, &matrices, &quality ); if (scored && quality.is_valid) { // quality.used_sample_count = number of samples used in scoring // quality.score_percent = combined score in [0, 100] // Interpretation: 100 perfect, >=80 good, >=60 acceptable, <40 bad }