Skip to content

compass: learn the offsets in flight (mag_learn) - #12142

Open
MrScothh wants to merge 3 commits into
iNavFlight:maintenance-10.xfrom
MrScothh:mag-learn
Open

MrScothh wants to merge 3 commits into
iNavFlight:maintenance-10.xfrom
MrScothh:mag-learn

Conversation

@MrScothh

@MrScothh MrScothh commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

A compass calibrated on the bench drifts once it sits in the airframe: a battery placed differently, a new servo, the
current of the motor. INAV only learns the offsets in the calibration dance, so the error stays until someone notices
the heading wandering and calibrates again. ArduPilot learns them in flight (COMPASS_LEARN), PX4 estimates them in
EKF2. This adds an optional in-flight refit of the offsets, saved after disarm, for airplanes and multirotors on
targets with more than 512 KB of flash.

How it works

  • With mag_learn ON (default OFF) and a calibrated compass, the flight controller keeps one raw reading for each
    direction the field takes in the aircraft's frame while it flies: 12 headings by 6 equal-area elevation bands. One
    reading per direction, so a long leg weighs no more than a turn.
  • About once a second, and once more at disarm, it fits a sphere to those readings with the solver the ground
    calibration already uses (sensorCalibrationSolveForOffset), on the readings scaled by maggain so that axes of
    different sensitivity weigh alike. A ridge toward the stored offset keeps what the flight did not observe (the
    vertical in level flight, the normal of a circle flown at one bank angle) where it was.
  • After disarm it writes magzero and saves, like the servo autotrim, only if all of these hold:
    • the flight turned through at least 270 degrees and filled at least 12 of the 72 sectors;
    • the distances from the fitted centre vary by no more than 5% (a field that changes with throttle, or iron
      nearby, fails it);
    • the radius is within 30% of what maggain implies;
    • no axis moves by more than 30% of the radius in one flight.
  • maggain is never changed. A calibration started while armed, or mag_learn switched off, drops the flight's
    readings.
  • The save waits out the 5 s in which INAV accepts an emergency rearm after a disarm, so a rearm finds the same
    calibration and no flash write, and then for the aircraft to be still, since it may be carried away right after
    landing; if it still moves a minute after the disarm, nothing is saved. Powering off before the save loses the
    flight's result. EMERGENCY_INFLIGHT_REARM_TIME_WINDOW_MS moves to fc_core.h so both use the same window.
  • Samples are taken only while INAV considers the aircraft flying (isProbablyStillFlying(), no landing detected),
    so an airplane needs a GPS; rovers and boats never collect, as the flight detection does not run for them.

What the user sees

  • Blackbox: a field group of its own, MAG_LEARN (blackbox MAG_LEARN, logged only while mag_learn is ON), with
    magBias[0..2] (the change a save would write, raw counts), magBiasFlags, magBiasSectors and magBiasSpread
    in the slow frame, refitted about once a second. The header has mag_learn and the mag_zero the flight started
    from; with the default blackbox_arm_control the log closes at disarm, before the save, so the next log's
    mag_zero shows what was written. The group is on by default in a new configuration; one kept from an earlier
    version needs blackbox MAG_LEARN, and the released Configurator's Blackbox tab clears the bit on save, which the
    Configurator PR below fixes.
  • MSP2_INAV_MAG_LEARN (0x2235) reports the same state, plus whether the last disarm saved. The Configurator side is
    Calibration and Blackbox tabs: in-flight compass learning (mag_learn) inav-configurator#2832: the switch and a status line in the Calibration tab, the field group
    in the Blackbox tab. The Blackbox Explorer side is Fields and header lines of INAV's in-flight compass offset learning (mag_learn) blackbox-log-viewer#126: the fields by name, the
    flags decoded, a graph preset and the header lines.
  • docs/Sensors.md describes it; Settings.md, Blackbox.md and the MSP docs are updated.

Commits

  • "blackbox: slow-frame fields can carry a condition": the slow-frame fields get the condition the main-frame fields
    already have, every existing one ALWAYS, so a group can leave its slow fields out of the log and of its S header.
  • "compass: learn the offsets in flight (mag_learn)": the feature.

It builds on #12127 (12 motors in the log header), which widens the blackbox condition cache: it is full on the
USE_DUAL_GYRO targets (63 conditions in a uint64_t). Until #12127 is merged its commit shows here too.

Settings

mag_learn is appended to compassConfig_t with a zero default, so the PG version stays: on ARM (short enums) the
struct grows from 24 to 26 bytes with no tail padding before; on SITL the new byte lands in what was tail padding,
which a stored record holds as zero (checked on an eeprom saved by a SITL without this change).

Cost

Against #12127 (both commits):

Target Flash RAM Samples (fast RAM) ITCM
MATEKF722SE (feature off) +120 B, 98.49% to 98.51% 0 0 0
MATEKF405SE +2268 B +52 B CCM +432 B
KAKUTEF7, KAKUTEF7HDV +1936 B, +1952 B +52 B, +60 B DTCM +432 B 0 (99.07%)
MATEKH743 +2240 B +288 B DTCM +432 B 0
NEUTRONRCF435WING +2380 B +52 B RAM1 +448 B

112 to 120 B of the flash is the first commit (the condition byte of every slow field and its test); on F722 the
rest is the CLI name (see below). On the H7 most of the RAM is section alignment: the new variables are about 40 B.

The 432 B of samples are in FASTRAM (CCM on F405, RAM1 on AT32, DTCM on H7); the AT32 startup does not clear it, and
the code reads only the sectors it has written. The fit runs in the compass task, once a second while armed.

Other PRs

Tested

  • Unit tests: 13 new ones for the fit and its checks (compass_learn_unittest.cc), including axes of different
    sensitivity, level turns, maggain 0 and negative; all 633 pass. Removing the gain scaling, or the ridge, makes a
    test fail.
  • SITL end to end, with a scripted flight fed over the X-Plane interface (airplane, GPS, climb, three right circles
    at 30 degrees of bank, a straight leg, a left circle) and magzero set wrong by (60, -40, 80):
    • landing, then disarm: flags 0x010 (save due) right after the disarm, 0x410 (saved) after the 5 s, magzero from
      (60, -40, 80) to (2, -1, 10), true value 0; the log shows the estimate settling as the turns fill the sectors;
    • disarm at 45 m and 15 m/s, the scripted aircraft flying on: 0x010 right after, 0x800 (still moving) a minute
      later, magzero unchanged;
    • with the field group turned off (blackbox -MAG_LEARN), learning still works and saves, and the six fields are
      left out of the log and its S header, as selected;
    • blackbox_decode reads every field.
  • The same fit against compasses modelled from their datasheets (IST8310 v1.5: 0.3 uT/LSB, 0.3 uT RMS; HMC5883L:
    1090 LSB/G, 2 mG, cross-axis 0.2 %FS/G, gains within 5 %; QMC5883L: 2 mG), a 45 uT field at 56 degrees and the
    field of the battery leads (a wire's mu0 I / 2 pi d, 30 A at full throttle): offsets off by (4, -3, 5) uT, i.e. 11
    degrees of heading, come back to 0.4 to 1.8 degrees in one flight, plane or multirotor, with a twisted pair or an
    open pair 10 cm away; an open pair 5 cm away (72 uT at full throttle, more than the Earth's field) is refused.
  • On a TBS Lucid H7 Wing, which has no compass on the bench: INAV's FAKE compass fed with a field shaped after the
    IST8310 (148 counts, 1 count of noise, turns banked both ways), offsets off by (13, -10, 17) counts. magzero went
    from (53, -109, 43) to (41, -99, 30), true value (40, -99, 26), with the spread steady at about 1 %; it was still
    there after a reboot; one fit takes 24 us on the H743.
  • Builds without warnings: MATEKF405SE, MATEKF722SE, KAKUTEF7, KAKUTEF7HDV, MATEKH743, AETH743Basic (two gyros),
    NEUTRONRCF435WING.

Not tested, testing wanted

  • No real flight and no real compass: the SITL compass has no noise, no soft iron and no motor current. The 5% spread
    and 30% limits are reasoned, and an offline simulation with noise and a throttle-dependent field agrees, but the
    real margin is what testers will see in magBiasSpread. A log of a few flights with mag_learn ON, on a plane and
    on a multirotor, would be the test I would like most.

The header declared motor[0..7], while the I and P frames write
getMotorCount() values, up to MAX_SUPPORTED_MOTORS (12). With 9 to 12 motors
and motor logging on, every frame carried more values than the header
described, and no frame of the log decoded.

The header now declares motor[8..11] behind four more conditions. The
condition cache was a uint64_t already full on dual-gyro targets, so it
becomes an array of 32-bit words sized for every condition. The MSP enum
reference gets the new values (that enum's section only).
With mag_learn ON, the compass keeps one raw reading per field direction
during each flight (12 headings x 6 equal-area elevation bands), refits the
offsets with the sphere fit the ground calibration already uses and saves
magzero at disarm when the flight turned through at least 270 degrees and
the fit agrees with the stored calibration: radius spread within 5%, radius
within 30% of what maggain implies, no axis moving more than 30% of the
radius. maggain is kept.

The fit runs on samples scaled by maggain, so axes of different sensitivity
weigh alike, and a ridge toward the stored offset keeps a direction the
flight never swept (the vertical in level flight, the normal of a circle
flown at one bank) where it was.

A calibration started while armed, or mag_learn switched off, drops what the
flight had collected. The save waits out the 5 s in which INAV accepts an
emergency rearm after a disarm, so a rearm finds the same calibration and no
flash write, and then for the aircraft to be still; if it still moves a minute
after the disarm, nothing is saved. EMERGENCY_INFLIGHT_REARM_TIME_WINDOW_MS
moves to fc_core.h for that.

The estimate is refitted about once a second in flight and logged in the
blackbox slow frame (magBias, magBiasFlags, magBiasSectors, magBiasSpread;
header mag_learn and mag_zero) under its own field group, MAG_LEARN (on by
default in a new configuration, logged only while mag_learn is ON), and
MSP2_INAV_MAG_LEARN reports it for the Configurator. Built on iNavFlight#12127 for the wider condition cache and on
the slow-frame conditions.

USE_MAG_LEARN on USE_MAG targets with more than 512 KB of flash. The setting
is appended to compassConfig_t with a zero default, so the PG version stays:
on ARM (short enums) the struct had no tail padding and grows from 24 to 26
bytes; on SITL the new byte lands in former tail padding, which the reset
template writes as zero.
@qodo-code-review

Copy link
Copy Markdown
Contributor

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Learn compass offsets in flight and correct Blackbox motor headers

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add optional in-flight compass offset learning to correct calibration drift after installation.
Diagram

graph TD
    C["mag_learn setting"] --> M["Compass task"] --> G{"Flying and armed?"} --> B["Direction samples"] --> F["Sphere refit"] --> V{"Fit acceptable?"} --> S["Deferred save"] --> T["Status outputs"]
    F --> T
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Estimate compass bias in the navigation filter
  • ➕ Could update bias continuously without a separate sample store or post-flight fit.
  • ➖ Couples calibration persistence to filter state and observability.
  • ➖ Requires broader changes to flight estimation and validation.

Recommendation: Keep the isolated, opt-in post-flight refit: it reuses the calibration solver and avoids changing heading calibration during flight. An estimator-integrated approach is more invasive for this scope; real compass flight logs remain important for validating the acceptance limits.

Files changed (24) +982 / -158

Enhancement (8) +405 / -49
blackbox.cLog conditional learning status and all supported motors +92/-49

Log conditional learning status and all supported motors

• Adds conditional MAG_LEARN slow-frame fields and starting calibration headers. Extends motor declarations through 12, expands the condition cache, and filters slow-field headers to match emitted data.

src/main/blackbox/blackbox.c

blackbox.hReserve the MAG_LEARN Blackbox feature bit +1/-0

Reserve the MAG_LEARN Blackbox feature bit

• Adds the field-group mask used to select learning telemetry.

src/main/blackbox/blackbox.h

cli.cExpose MAG_LEARN in the Blackbox CLI +1/-0

Expose MAG_LEARN in the Blackbox CLI

• Adds a stable positional name for the new field-group bit on all targets.

src/main/fc/cli.c

fc_msp.cReturn compass-learning status over MSP +13/-0

Return compass-learning status over MSP

• Serializes flags, sector and heading counts, spread, and three offset deltas when the feature is built.

src/main/fc/fc_msp.c

msp_protocol_v2_inav.hAssign the compass-learning MSP command +2/-0

Assign the compass-learning MSP command

• Reserves 0x2235 for reading current or last-flight learning status.

src/main/msp/msp_protocol_v2_inav.h

compass.cCoordinate learning with flight and disarm state +74/-0

Coordinate learning with flight and disarm state

• Collects readings only during detected flight, periodically evaluates them, and discards interrupted sessions. Saves accepted offsets after the emergency-rearm window once stationary, leaving maggain unchanged.

src/main/sensors/compass.c

compass_learn.cFit and validate in-flight compass offsets +153/-0

Fit and validate in-flight compass offsets

• Bins raw readings by field direction and fits gain-scaled samples with a ridge toward stored offsets. Reports coverage, fit quality, scale, and step checks before permitting a save.

src/main/sensors/compass_learn.c

compass_learn.hDefine learning limits, status, and API +69/-0

Define learning limits, status, and API

• Declares sector geometry, acceptance thresholds, status flags, and the collection and evaluation functions.

src/main/sensors/compass_learn.h

Bug fix (1) +6 / -0
blackbox_fielddefs.hAdd motor and learning field conditions +6/-0

Add motor and learning field conditions

• Defines conditions for motors 9–12 and the MAG_LEARN field group.

src/main/blackbox/blackbox_fielddefs.h

Refactor (2) +2 / -1
fc_core.cRemove the local emergency-rearm window definition +0/-1

Remove the local emergency-rearm window definition

• Uses the shared header definition instead of a file-local constant.

src/main/fc/fc_core.c

fc_core.hShare the emergency-rearm window +2/-0

Share the emergency-rearm window

• Makes the five-second window available to both arming and compass-learning logic.

src/main/fc/fc_core.h

Tests (2) +314 / -0
CMakeLists.txtBuild the compass-learning unit tests +4/-0

Build the compass-learning unit tests

• Links the learner and required math and bit-array sources with feature definitions.

src/test/unit/CMakeLists.txt

compass_learn_unittest.ccTest fitting and rejection scenarios +310/-0

Test fitting and rejection scenarios

• Simulates turns, repeated flights, unequal gains, and interference. Checks insufficient coverage, invalid gains, scale mismatch, oversized steps, reset behavior, and status flags.

src/test/unit/compass_learn_unittest.cc

Documentation (7) +239 / -108
Blackbox.mdDocument the MAG_LEARN field group +1/-0

Document the MAG_LEARN field group

• Adds the optional slow-frame compass-learning group to the Blackbox field list.

docs/Blackbox.md

Sensors.mdExplain in-flight compass learning +10/-0

Explain in-flight compass learning

• Describes eligibility, coverage and fit checks, delayed saving, and Blackbox and MSP status.

docs/Sensors.md

Settings.mdDocument the mag_learn setting +10/-0

Document the mag_learn setting

• Adds the default-off setting, its supported targets, and its calibration prerequisites.

docs/Settings.md

README.mdSpecify the compass-learning MSP reply +17/-0

Specify the compass-learning MSP reply

• Documents command 0x2235, its payload, status flags, and availability.

docs/development/msp/README.md

inav_enums.jsonExtend MSP enum references +77/-54

Extend MSP enum references

• Adds learning flags and the Blackbox feature bit; records motor conditions through 12 and the expanded condition numbering.

docs/development/msp/inav_enums.json

inav_enums_ref.mdPublish updated enum reference +79/-53

Publish updated enum reference

• Lists the learning flags, Blackbox feature bit, and expanded field conditions.

docs/development/msp/inav_enums_ref.md

msp_messages.jsonDefine the compass-learning MSP message +45/-1

Define the compass-learning MSP message

• Bumps the specification patch version and defines the reply fields for command 0x2235.

docs/development/msp/msp_messages.json

Other (4) +16 / -0
CMakeLists.txtInclude the compass-learning module +2/-0

Include the compass-learning module

• Adds the learning source and header to the main build sources.

src/main/CMakeLists.txt

settings.yamlAdd the default-off mag_learn setting +6/-0

Add the default-off mag_learn setting

• Exposes the target-gated switch in compass configuration settings.

src/main/fc/settings.yaml

compass.hStore the compass-learning switch +3/-0

Store the compass-learning switch

• Appends a feature-gated magLearn field to compass configuration.

src/main/sensors/compass.h

common_post.hGate learning by compass and flash size +5/-0

Gate learning by compass and flash size

• Enables compilation when a compass is present and MCU flash exceeds 512 KB.

src/main/target/common_post.h

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Pico 2 pilots cannot enable compass learning 🐞 Bug ≡ Correctness
Description
The USE_MAG_LEARN gate tests MCU_FLASH_SIZE, but the RP2350 build defines only
PICO_FLASH_SIZE_BYTES for its 4 MiB flash. On the Pico 2 target, which enables USE_MAG, the
undefined size evaluates as zero in this preprocessor test, so the setting and learning code are
compiled out.
Code

src/main/target/common_post.h[R177-178]

+#if defined(USE_MAG) && !defined(USE_MAG_LEARN) && (MCU_FLASH_SIZE > 512)
+#define USE_MAG_LEARN
Evidence
The Pico 2 target enables a compass, and its CMake definitions specify 4 MiB through
PICO_FLASH_SIZE_BYTES without defining the macro required by the new gate. The setting is
conditional on the resulting USE_MAG_LEARN definition.

src/main/target/RP2350_PICO/target.h[124-125]
cmake/rp2350.cmake[224-237]
src/main/target/common_post.h[176-179]
src/main/fc/settings.yaml[667-672]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new flash-size gate excludes the 4 MiB RP2350 Pico 2 target because that build does not define `MCU_FLASH_SIZE`.

## Fix Focus Areas
- src/main/target/common_post.h[176-179]
- cmake/rp2350.cmake[224-237]

## Recommended Fix
Define `MCU_FLASH_SIZE` in KiB for RP2350, consistently with the other target builds, so the existing feature gate enables learning there.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Compass read faults prevent offset saves 🐞 Bug ☼ Reliability
Description
compassLearnUpdate() handles the pending post-disarm evaluation and save, but it is called only
after mag.dev.read() succeeds. If reads keep failing after a flight, compassUpdate() returns
before advancing that pending work, leaving the learned offsets unsaved.
Code

src/main/sensors/compass.c[R662-664]

+#ifdef USE_MAG_LEARN
+    compassLearnUpdate(currentTimeUs);
+#endif
Evidence
A failed compass read exits compassUpdate() before the newly added call. That call contains the
only path from a pending disarm through evaluation to saveConfigAndNotify().

src/main/sensors/compass.c[530-536]
src/main/sensors/compass.c[471-499]
src/main/sensors/compass.c[662-664]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Post-disarm learning can remain pending indefinitely when compass reads fail, because the learning lifecycle runs only on the successful-read path.

## Fix Focus Areas
- src/main/sensors/compass.c[530-536]
- src/main/sensors/compass.c[471-499]
- src/main/sensors/compass.c[662-664]

## Recommended Fix
Advance the disarmed pending-save lifecycle even when a compass read fails, while continuing to require a valid reading before collecting an armed sample.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: Auto: 🧠 Deep: Bug-dense flight-control feature spans calibration, persistence, telemetry, logging, and many code paths.

Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/main/target/common_post.h
Comment thread src/main/sensors/compass.c
@github-actions

Copy link
Copy Markdown

RAM / Flash usage vs. base commit e87050f — commit d26d41f

Using the nearest available size baseline — the PR's exact base commit has no stored baseline yet.

Target Flash Δ RAM Δ
MATEKF405 +2092 B (+0.29%) CCM: +432 B (+1.33%)
RAM: +60 B (+0.06%)
MATEKF722 +168 B (+0.03%) ITCM_RAM: ±0 B (±0.00%)
RAM: +8 B (+0.01%)
TCM: ±0 B (±0.00%)
MATEKF765 +2024 B (+0.27%) DTCM_RAM: +432 B (+1.51%)
SRAM1: +76 B (+0.06%)
MATEKH743 +2128 B (+0.27%) D2_RAM: ±0 B (±0.00%)
DTCM_RAM: +432 B (+3.34%)
ITCM_RAM: ±0 B (±0.00%)
RAM: +288 B (+0.20%)

See RAM/flash optimization guide for techniques to reduce usage.

@github-actions

Copy link
Copy Markdown

Test firmware build ready — commit d26d41f

Download firmware for PR #12142

251 targets built. Find your board's .hex file by name on that page (e.g. MATEKF405SE.hex). Files are individually downloadable — no GitHub login required.

Development build for testing only. Use Full Chip Erase when flashing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant