SDK

QMC5883P magnetometer

The magnetic field sensor of a Kode Dot: its registers, the raw milligauss reading, and why the SDK gives you the field and not a compass heading.

What it is

A magnetometer measures the strength and direction of a magnetic field. The QMC5883P from QST measures it on three axes. Earth's usual field is weak, 250 to 650 mG, and that is what a compass reads to find north.

The SDK gives you the raw field: milligauss on the three axes of the chip, exactly as the chip measures them, with no remap onto the board or the world. It does not compute a heading. The board's own field is several gauss on the bench, far above Earth's, so a heading computed from a raw reading would point at the Kode Dot itself. A compass needs hard-iron calibration and tilt compensation on top of this reading, and the SDK does neither.

The chip is a part of its own. It is not behind the QMI8658A, which sits beside it on the same bus. See sdk/kdrv/qmi8658a.

How it is connected

flowchart LR
subgraph board["On the Kode Dot"]
  mag["QMC5883P"] -- "I2C 0x2C" --> bus["shared I2C bus"]
  bus --> ao["AO_Imu"]
end
ao -- "kode_mag_read()" --> app["your app"]
class mag chip
class ao ao
class app app
The magnetometer on the Kode Dot

The chip is U5 on the main PCB, at 0x2C (KBSP_I2C_ADDR_MAG) on the shared bus. The chip-id register 0x00 reads 0x80. There is no interrupt line in use: the driver checks the data-ready bit.

The owner

AO_Imu owns the magnetometer together with the accelerometer (KBSP_RES_IMU) at priority 21, because the two production rows and the haptic witness window are one conversation.

The part rests until asked. At boot the driver configures it and leaves it suspended. A request starts continuous conversion at 50 Hz, and the part goes back to suspend 2 s after the last request (KODE_IMU_REST_AFTER_MS). kode_mag_read() waits up to 600 ms (KODE_MAG_READ_WAIT_MS). A board without the part still starts the AO. A read is then refused at once with KODE_IO_ERROR.

Bring-up and a reading

This is not a QMC5883L. The names are close and the registers differ. On this part the data starts at 0x01, not 0x00, the status register is 0x09, and the chip id is at 0x00. A driver copied from the L part reads the chip-id byte as part of the data from the first transaction on.

The driver writes the configuration in the order of the datasheet:

  1. The axis sign register 0x29, with 0x06. Every datasheet example does this before anything else. If it is skipped the part still converts, but the sign convention differs from the one every other reading on the board assumes.
  2. The range in CTRL2: +-30 G, 1000 LSB per gauss, so one LSB is one milligauss. The board's own field is several gauss, and a range that clips is a reading that lies.
  3. The mode in CTRL1: suspend. The part is at rest when kdrv_qmc5883p_create() returns.

kdrv_qmc5883p_standby(false) starts continuous conversion at 50 Hz. The first conversion is one 20 ms period away.

kdrv_qmc5883p_read() reads the status register first and checks data-ready. If it finds nothing ready it waits 25 ms once (QMC_CONVERSION_MS), a period plus a margin, and asks again. A second miss, or an overflow bit set together with data-ready, is KODE_ERROR. When the status is good it reads the six axis bytes in one burst. The conversion to milligauss is a cast, since one LSB is one milligauss.

The self-test

The self-test reports three deltas and judges none. The datasheet describes the procedure: a reading with the stimulus off, the stimulus on, a reading with it on. It then says only "compare with threshold value" and gives no number. kdrv_qmc5883p_self_test() returns the three deltas, and test component=mag prints them. A threshold has to be measured on known-good boards first, and the driver uses none until then.

Two details of the self-test follow the datasheet exactly:

Specifications

The board

ThingValue
Address0x2C (KBSP_I2C_ADDR_MAG)
IdentityRegister 0x00 reads 0x80 (KDRV_QMC5883P_CHIP_ID)
Range+-30 G, 1000 LSB per gauss, 1 mG per LSB
ModeContinuous at 50 Hz while awake, suspend between uses
OversamplingOSR1 = 8, OSR2 = 1 (down-sampling 1)
ReadingRaw milligauss on the chip's three axes. No remap, no calibration, no heading
Self-test10 ms stimulus (the datasheet's 5 ms, doubled). No threshold. TBD: the number to judge by, from known-good boards

The registers

Only the registers the driver uses.

RegNameValue the driver writes, and what it is
0x00chip idReads 0x80
0x01 to 0x06X, Y, ZLow byte then high byte of each axis, read as one burst
0x09statusBit 0 DRDY a conversion is ready. Bit 1 OVFL it overflowed
0x0ACTRL10x07: continuous mode (MODE 11), 50 Hz (ODR 01), over-sampling 8 (OSR1 00), down-sampling 1 (OSR2 00). 0x00 is suspend
0x0BCTRL20x00: no reset, no self-test, +-30 G, set/reset on. Bit 6 (0x40) starts the self-test and clears itself once the data is updated
0x29axis sign0x06, written first

The driver

CallWhat it does
kdrv_qmc5883p_create(&chip_id)Probes 0x2C, checks the chip id, writes sign, range and mode, leaves the part suspended. KODE_IO_ERROR when nothing answers, KODE_ERROR when something else does. Once it succeeded a further call changes nothing
kdrv_qmc5883p_standby(on)true suspends, false converts continuously at 50 Hz. KODE_DENIED before create
kdrv_qmc5883p_read(mg)The latest conversion in milligauss. KODE_ERROR when no conversion is ready after the one 25 ms wait, or one overflowed
kdrv_qmc5883p_self_test(delta_mg)The three deltas in milligauss. Blocks about 10 ms and leaves the part converting as create left it

The driver has no field table. AO_Imu is the only caller.

The files

FileWhat it does
motion/include/kdrv_qmc5883p.hThe four calls and KDRV_QMC5883P_CHIP_ID
motion/src/kdrv_qmc5883p.cThe driver, written by hand from the datasheet (QMC5883P Rev A)
kaos/imu/src/imu_ao.cAO_Imu: rest and wake, kode_mag_read(), the production row

Gotchas

Example code

Log the raw magnetic field twice a second. Hold the Kode Dot still and the three values stay steady and large. Turn it and they change, but they do not give you north.

/*
 * mag_log.c - 2026-10-28 - from Kode with love
 *
 * Reads the Kode Dot's magnetometer twice a second and logs the raw field in
 * milligauss on the chip's own three axes. The board's own field is several
 * gauss, so the numbers are large and do not point at north: this is not a
 * compass.
 */
#include "kode.h"
 
/* Bring the system up, then log the raw magnetic field every 500 ms. */
void app_main(void)
{
    int32_t mg[3];
 
    kode_init();
 
    while (1) {
        if (kode_mag_read(mg) == KODE_OK) {
            KLOG_I("mag", "mg=%ld,%ld,%ld", (long)mg[0], (long)mg[1],
                   (long)mg[2]);
        }
        kode_delay_ms(500);
    }
}

Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.

In the SDK