Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Magnetometer Hard and Soft Iron Calibration Library

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.

Features

  • 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

Hardware Requirements

  • Magnetometer (QMC5883L, AK8963, HMC5883L, etc.)
  • Optional: accelerometer or 6-axis IMU (for tilt compensation)
  • MCU: Arduino, ESP32, etc

Testing

This library gives very similar result to the Magneto 1.2 app . See ellipsoid.test folder

Functions

Configuration

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.

Calibration

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) triplets
  • out_matrices: Output struct containing hard_iron, soft_iron, reference field, and validity flag
  • out_method: Enum indicating which method succeeded (Ellipsoid or Min/Max)

Returns: true if calibration succeeded, false otherwise

Calibration Quality Scoring

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

Set/Get Calibration

bool compassSetCalibrationMatrices(const CompassCalibrationMatrices* matrices);
bool compassGetCalibrationMatrices(CompassCalibrationMatrices* out_matrices);

Store/retrieve calibration matrices globally for use in heading calculations.

Apply Calibration

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).

Heading Computation

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

Data Types

CompassCalibrationMatrices

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;
};

CompassCalibrationQuality

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;
};

CompassCalibrationFitMethod

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
};

Usage Example

Quick workflow:

  1. Initialise with calibration parameters

    compassConfigureReferenceField(0.49f, 3000.0f);
  2. 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());
    }
  3. 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 Gauss
    • lsb_per_gauss: configured sensor sensitivity in LSB/G
    • fitted_field_lsb: fitted field magnitude in sensor counts
    • is_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();
    }
  4. Apply calibration

    if (success) {
        compassSetCalibrationMatrices(&matrices);
        float raw[3] = {readMagX(), readMagY(), readMagZ()};
        float calibrated[3];
        compassApplyCalibration(raw, calibrated);
        float heading = compassHeadingFromMagneticVector(calibrated);
    }
  5. 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
    }

About

Magnetometer hard and soft iron calibration Library

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages