Chapter 73: Magnetometer / compass¶
What: three I²C magnetometers: Honeywell HMC5883L (legacy classic, EOL but ubiquitous on hobbyist boards), QST QMC5883L (cheap clone with quirky register-set differences), Memsic MMC5983MA (modern low-noise). Most of this chapter is not driver code. It is calibration: hard-iron and soft-iron calibration, the universal “my compass points 23° wrong” problem and how to fix it.
Why: any product that needs to know which way it’s facing, drone, robot vacuum, AR headset, GPS-assisted navigation, needs a magnetometer. The math is simple. The calibration is what separates a useful compass from a useless one. Many products ship without calibration. Their compass is off by 10–30°, and the IMU often gets blamed.
Focus: Calibration runs in user-space. The driver’s job is to deliver raw X/Y/Z in stable, scaled units. The driver reports raw
µT × scale. User-space collects samples, fits an ellipsoid model, computes hard-iron (offset) and soft-iron (skew matrix). After applying the correction, raw 3-axis readings become Earth-magnetic-field vectors with < 1° error. Without calibration: 10–30° error is typical, depending on what’s mounted near the sensor.
73.1 Chip comparison¶
Honeywell HMC5883L |
QST QMC5883L |
Memsic MMC5983MA |
|
|---|---|---|---|
Range |
±0.88 / 1.3 / 1.9 / 2.5 / 4 / 4.7 / 5.6 / 8.1 Gauss |
±2 / 8 Gauss |
±8 Gauss |
Resolution |
12-bit (in ±0.88 G), ~0.7 mG/LSB |
16-bit, ~30 µG/LSB |
18-bit, ~0.06 mG/LSB |
Noise floor |
2 mG RMS |
2 mG RMS |
0.4 mG RMS |
Update rate |
0.75, 75 Hz |
10/50/100/200 Hz |
up to 1 kHz |
I²C address |
0x1E |
0x0D |
0x30 |
Reg map |
“real” HMC layout |
shifted (different from HMC) |
new |
Compatible? |
yes mainline ( |
yes mainline ( |
yes ( |
Lifecycle |
EOL since 2018 |
active (cheap China supply) |
active |
Volume price |
$4–6 (eBay clones $1) |
$0.50–1.50 |
$4–6 |
Watch for this: every $1 eBay “HMC5883L” breakout is actually a QMC5883L. The pinout is similar. The protocol is different. If your “HMC5883L” doesn’t probe at 0x1E, try QMC5883L at 0x0D.
Pick guide:
HMC5883L for legacy maintenance only.
QMC5883L for cheap projects. Understand the register-set quirk.
MMC5983MA for serious products needing low-noise + high-rate.
73.2 The physics, why magnetometers care so much about the environment¶
A magnetometer measures the local magnetic field vector in three orthogonal axes. The Earth produces ~50 µT (0.5 Gauss). Local sources can be much stronger:
Hard iron (permanent magnets, magnetised steel): adds a fixed DC offset to each axis.
Soft iron (any ferromagnetic material): bends the field unevenly, different axes see different scale factors, axes may not be exactly orthogonal anymore.
If you take the chip out of the box and read raw, the data lies on a 3D ellipsoid (not a sphere centered on origin) with the ellipsoid’s center offset from origin. The ellipse-fitting calibration recovers two corrections:
Offset vector b: subtract from each raw reading to center on origin (hard-iron).
3×3 transformation matrix A: multiply the centered reading to make the ellipsoid a sphere (soft-iron).
Calibrated reading = A × (raw − b). The radius of the resulting sphere equals the magnitude of Earth’s field (~50 µT) at your location. Heading angle from the X/Y components is then accurate to ~1°.
Without calibration: heading error of 10–30°, wrong enough to be useless for navigation.
73.3 Protocol, HMC5883L¶
Register map (the “real” HMC):
Reg |
Name |
Purpose |
|---|---|---|
0x00 |
CONFIG_A |
Output rate, oversampling, measurement mode |
0x01 |
CONFIG_B |
Gain/range select |
0x02 |
MODE |
Continuous / single / idle |
0x03..0x08 |
X_MSB, X_LSB, Z_MSB, Z_LSB, Y_MSB, Y_LSB |
16-bit signed values (note: Z then Y!) |
0x09 |
STATUS |
DATA_READY bit |
0x0A..0x0C |
ID_A/B/C |
“H43” identification |
Bring-up:
Read identification bytes (0x0A..0x0C), should be ‘H’, ‘4’, ‘3’.
Write CONFIG_A (0x00) = 0x70: 8-sample averaging, 15 Hz output rate.
Write CONFIG_B (0x01) = 0x20: gain ±1.3 Gauss, 1090 LSB/G.
Write MODE (0x02) = 0x00: continuous measurement.
After ~7 ms (per datasheet), data is ready. Read 6 bytes from 0x03.
Apply scale:
field_uT = raw / 1090 * 100(to get µT from 1090 LSB/Gauss).
Each measurement read is 6 bytes back-to-back. Note the X/Z/Y order (HMC’s idiosyncrasy, most other chips do X/Y/Z).
73.4 Protocol, QMC5883L (different!)¶
QMC5883L uses a different register layout despite the similar-looking name and pinout. The differences that matter:
Reg |
QMC |
HMC |
|---|---|---|
Data start |
0x00 |
0x03 |
Status |
0x06 |
0x09 |
Temperature |
0x07–0x08 |
none |
Control 1 |
0x09 |
(different bits) |
Control 2 |
0x0A |
(different) |
Period (refresh) |
0x0B |
none |
I²C address |
0x0D |
0x1E |
Mode bits in QMC’s control register are also different. HMC5883L code does not work on a QMC5883L, the chip will simply not respond. This is the #1 reason “my $1 HMC5883L doesn’t work”, it’s not an HMC5883L.
QMC bring-up:
Write 0x0B = 0x01: set the “period” register (mandatory. Chip won’t work without it).
Write 0x09 = 0x1D: 200 Hz output rate, ±8 G range, 512 oversampling, continuous mode.
Read 6 bytes from 0x00, X / Y / Z order, little-endian per axis (vs HMC’s big-endian).
73.5 How the mainline drivers work¶
drivers/iio/magnetometer/hmc5843_i2c.c + hmc5843_core.c covers HMC5843 + HMC5883 + HMC5883L. Standard regmap-based pattern: read ID, configure, register IIO channels for IIO_MAGN with X/Y/Z modifiers.
MCU bridge: Think of regmap like a typed wrapper around your read_reg() and write_reg() helpers, with caching, locking, and bus differences handled centrally. IIO: Industrial I/O, Linux’s subsystem for sensors, ADCs, DACs, and buffered sampled data. regmap: a kernel helper that wraps register reads and writes over I2C, SPI, or MMIO.
drivers/iio/magnetometer/qmc5883.c is its own driver. Can’t share with HMC due to the register-set incompatibility.
drivers/iio/magnetometer/mmc56x3.c covers MMC5983MA and MMC56x3 family.
All three expose:
in_magn_x_raw
in_magn_y_raw
in_magn_z_raw
in_magn_scale ← LSB to Gauss (or µT) conversion
sampling_frequency
sampling_frequency_available
User-space reads raw × scale to get the field in physical units. The driver doesn’t do calibration, that’s user-space’s job.
73.6 Writing a QMC5883L driver from scratch¶
We’ll target QMC5883L since it’s what most people actually have. ~180 lines.
myqmc5883.c:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/delay.h>
#include <linux/iio/iio.h>
#define REG_DATA 0x00
#define REG_STATUS 0x06
#define REG_CONTROL1 0x09
#define REG_CONTROL2 0x0A
#define REG_PERIOD 0x0B
struct myqmc {
struct i2c_client *client;
struct mutex lock;
};
static int mq_write(struct myqmc *m, u8 reg, u8 val)
{
return i2c_smbus_write_byte_data(m->client, reg, val);
}
static int mq_read_block(struct myqmc *m, u8 reg, u8 *buf, int n)
{
int r = i2c_smbus_read_i2c_block_data(m->client, reg, n, buf);
return r == n ? 0 : (r < 0 ? r : -EIO);
}
static int mq_init(struct myqmc *m)
{
int err;
/* Soft reset */
err = mq_write(m, REG_CONTROL2, 0x80);
if (err) return err;
msleep(20);
/* Period register — mandatory magic value */
err = mq_write(m, REG_PERIOD, 0x01);
if (err) return err;
/* CONTROL1: OSR=512(00), RNG=±8G(10), ODR=200Hz(11), MODE=continuous(01)
binary: 0001_1101 = 0x1D */
err = mq_write(m, REG_CONTROL1, 0x1D);
if (err) return err;
return 0;
}
static int mq_read_axes(struct myqmc *m, s16 axes[3])
{
u8 buf[6];
int err;
err = mq_read_block(m, REG_DATA, buf, 6);
if (err) return err;
/* QMC: little-endian per axis */
axes[0] = (s16)(buf[0] | (buf[1] << 8)); /* X */
axes[1] = (s16)(buf[2] | (buf[3] << 8)); /* Y */
axes[2] = (s16)(buf[4] | (buf[5] << 8)); /* Z */
return 0;
}
static int mq_read_raw(struct iio_dev *idev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
struct myqmc *m = iio_priv(idev);
s16 axes[3];
int err;
switch (mask) {
case IIO_CHAN_INFO_RAW:
mutex_lock(&m->lock);
err = mq_read_axes(m, axes);
mutex_unlock(&m->lock);
if (err) return err;
switch (chan->channel2) {
case IIO_MOD_X: *val = axes[0]; return IIO_VAL_INT;
case IIO_MOD_Y: *val = axes[1]; return IIO_VAL_INT;
case IIO_MOD_Z: *val = axes[2]; return IIO_VAL_INT;
}
return -EINVAL;
case IIO_CHAN_INFO_SCALE:
/* ±8 G range, 16-bit signed ⇒ 32768 LSB / 8 G = 4096 LSB/G
* Convert to Tesla: 1 G = 100 µT, so 1 LSB = 100/4096 µT */
*val = 0; *val2 = 24414; /* ~24.4 nT/LSB */
return IIO_VAL_INT_PLUS_NANO;
}
return -EINVAL;
}
#define MAGN_CH(axis) { \
.type = IIO_MAGN, .modified = 1, .channel2 = (axis), \
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \
.scan_index = 0, \
.scan_type = { .sign='s', .realbits=16, .storagebits=16, \
.endianness=IIO_LE }, \
}
static const struct iio_chan_spec mq_channels[] = {
MAGN_CH(IIO_MOD_X),
MAGN_CH(IIO_MOD_Y),
MAGN_CH(IIO_MOD_Z),
};
static const struct iio_info mq_iio_info = {
.read_raw = mq_read_raw,
};
static int mq_probe(struct i2c_client *client)
{
struct iio_dev *idev;
struct myqmc *m;
int err;
idev = devm_iio_device_alloc(&client->dev, sizeof(*m));
if (!idev) return -ENOMEM;
m = iio_priv(idev);
m->client = client;
mutex_init(&m->lock);
err = mq_init(m);
if (err) return dev_err_probe(&client->dev, err, "init failed\n");
idev->name = "myqmc5883";
idev->info = &mq_iio_info;
idev->modes = INDIO_DIRECT_MODE;
idev->channels = mq_channels;
idev->num_channels = ARRAY_SIZE(mq_channels);
return devm_iio_device_register(&client->dev, idev);
}
static const struct of_device_id mq_of_match[] = {
{ .compatible = "linuxlearn,myqmc5883" },
{ }
};
MODULE_DEVICE_TABLE(of, mq_of_match);
static const struct i2c_device_id mq_id[] = { { "myqmc5883", 0 }, { } };
MODULE_DEVICE_TABLE(i2c, mq_id);
static struct i2c_driver mq_driver = {
.driver = {
.name = "myqmc5883",
.of_match_table = mq_of_match,
},
.probe = mq_probe,
.id_table = mq_id,
};
module_i2c_driver(mq_driver);
MODULE_LICENSE("GPL");
DT:
&i2c1 {
qmc5883@d {
compatible = "linuxlearn,myqmc5883";
reg = <0x0d>;
};
};
Test:
[root@pa-mini:~]# insmod myqmc5883.ko
[root@pa-mini:~]# cat /sys/bus/iio/devices/iio:device0/in_magn_x_raw
1042
[root@pa-mini:~]# cat /sys/bus/iio/devices/iio:device0/in_magn_y_raw
-832
[root@pa-mini:~]# cat /sys/bus/iio/devices/iio:device0/in_magn_z_raw
-1620
# Calibrated heading (raw, no soft/hard-iron correction!):
[root@pa-mini:~]# awk "BEGIN { print atan2(-832, 1042) * 180 / 3.14159 }"
-38.6 ← "compass says 38.6° west of north"... but it's wrong because no calibration
73.7 Calibration, the part most products skip¶
A user-space calibration script collects samples while you slowly rotate the sensor in all 3D orientations (cover an imaginary sphere). The samples fall on an ellipsoid. From the ellipsoid you compute two things:
The offset (center of the ellipsoid), hard-iron correction.
The 3x3 matrix to rotate-and-scale the ellipsoid into a sphere, soft-iron correction.
A simple Python sketch:
#!/usr/bin/env python3
import numpy as np, time, sys
def read_axis(axis):
return int(open(f"/sys/bus/iio/devices/iio:device0/in_magn_{axis}_raw").read())
# Phase 1: collect ~1000 samples while the user rotates the sensor
samples = []
print("Rotate the sensor in all directions for 30 seconds...")
t0 = time.time()
while time.time() - t0 < 30:
samples.append([read_axis(a) for a in "xyz"])
time.sleep(0.03)
samples = np.array(samples, dtype=float)
# Phase 2: fit ellipsoid → extract center (hard-iron) and matrix (soft-iron)
# Simplified: just center + scale-axis-by-axis (ignores rotation)
center = (samples.max(axis=0) + samples.min(axis=0)) / 2
ranges = (samples.max(axis=0) - samples.min(axis=0)) / 2
avg_range = ranges.mean()
scale = avg_range / ranges # per-axis scale
# Phase 3: save calibration
np.savez("magcal.npz", center=center, scale=scale)
print(f"Hard-iron offset: {center}")
print(f"Soft-iron scale: {scale}")
Apply at runtime:
cal = np.load("magcal.npz")
def read_calibrated():
raw = np.array([read_axis(a) for a in "xyz"])
return cal["scale"] * (raw - cal["center"])
import math
m = read_calibrated()
heading_deg = math.atan2(m[1], m[0]) * 180 / math.pi
if heading_deg < 0: heading_deg += 360
print(f"Heading: {heading_deg:.1f}°")
This is the simplified calibration. The proper version fits a full ellipsoid model (10 parameters: center 3 + axes 3 + rotation 3 + radius 1) using least-squares. See Calibration of triaxial magnetometers literature for the math. Libraries like MotionCal (Adafruit’s GUI tool) do this. The resulting offset + 3x3 matrix is portable.
After proper calibration: heading error < 1°, indoor or out.
73.8 Mainline driver enablement¶
DT for QMC5883L:
&i2c1 {
qmc5883@d {
compatible = "qst,qmc5883l";
reg = <0x0d>;
};
};
For HMC5883L: compatible = "honeywell,hmc5883l"; reg = <0x1e>;.
For MMC5983MA: compatible = "memsic,mmc5983ma"; reg = <0x30>;.
The mainline drivers expose richer sysfs:
sysfs: a kernel-generated filesystem under /sys that exposes devices, drivers, and attributes.
[root@pa-mini:~]# ls /sys/bus/iio/devices/iio:device0/
in_magn_scale
in_magn_scale_available
in_magn_x_raw
in_magn_y_raw
in_magn_z_raw
sampling_frequency
sampling_frequency_available
Plus buffered capture via trigger (Ch 70) for high-rate logging.
73.9 Lab¶
Probe.
i2cdetect -y 1. Check 0x0D and 0x1E. Whichever responds tells you what chip you have.Build and load
myqmc5883.ko. Read X/Y/Z while moving the sensor. Verify values change.Compute uncalibrated heading. Use atan2(Y, X) in radians, convert to degrees. Compare to a real compass. Note the error.
Run the calibration script. Rotate the sensor for 30 seconds in all directions. Save offset + scale.
Apply calibration. Compute heading again. Compare to real compass. Should now be within 5°.
Move near a steel object. Watch the heading change abruptly. Calibration done on the bench doesn’t help if the in-product environment has different ferromagnetics, recalibrate in-place.
Switch to mainline driver. Verify same data flows. Verify sampling_frequency_available works.
73.10 Pitfalls¶
Mistaking QMC5883L for HMC5883L. #1 hobbyist trap. If it doesn’t respond at 0x1E, try 0x0D.
No calibration. Heading off by 10–30°. Calibrate, every time, in-place.
Calibration baked into firmware but environment changed. A magnetometer on a robot calibrated on workbench, then mounted near a motor: recalibrate on the robot.
XY-only heading assumes the sensor is held level. If the device can tilt, combine the magnetometer with the accelerometer and project the magnetic vector onto the horizontal plane. This is “tilt compensation” and it is mandatory for any device a user holds in their hand.
Buck converters within 5 cm. Switching power supplies emit strong RF magnetic noise. Magnetometer reads garbage. Layout: keep mag far from switchers, or filter heavily.
Iron rich PCB substrate. Cheap PCBs sometimes have ferromagnetic impurities. Affects calibration repeatability across boards.
Phone case magnets. A 30-cm gap to a magnet still measures 100s of µT. Test your product in its real-world envelope.
Sensor saturation. A strong nearby field (relay coil during switch) can saturate the magnetometer. Readings stick at ±MAX for a while afterward. Verify range is wide enough.
Slow rotation during calibration. Need to rotate slowly enough that samples cover the sphere uniformly. Too fast = patches uncovered = poor ellipsoid fit.
73.11 Going deeper¶
drivers/iio/magnetometer/hmc5843_core.c+hmc5843_i2c.c, HMC family.drivers/iio/magnetometer/qmc5883.c: QMC.drivers/iio/magnetometer/mmc56x3.c: MMC.HMC5883L datasheet (Honeywell): register layout.
QMC5883L datasheet (QST): for comparison with HMC. Note the differences.
MMC5983MA datasheet (Memsic): set-reset cycle (a calibration improvement specific to MMC).
“Markovsky-Van Huffel ellipsoid fitting”: the math behind proper soft-iron calibration.
MotionCal tool by Adafruit, interactive calibration capture + analysis.
NOAA WMM (World Magnetic Model): for converting magnetic-north to true-north (declination varies by location and year).
Next chapter: Chapter 74: Hall-effect & rotary position sensors. Magnetic sensing applied to mechanical position, AS5048 (absolute rotary), A1324 (linear Hall), TLE5012 (high-rate angular).