# 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

```mermaid
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:

- **The subtraction is not the same on every axis.** Section 7.3 computes `off - on` for X and `on - off` for Y and Z. The driver does the same and does not correct what looks like an inconsistency.
- **The part changes mode by itself.** Once the self-test data is out the part drops to suspend. The driver takes it back to continuous mode through suspend, the path the datasheet requires for any mode change, and it does so whatever the read answered. If one of the two restore writes fails, the driver marks itself not started and returns `KODE_IO_ERROR`, and `read` and `self_test` return `KODE_DENIED` until the next `kdrv_qmc5883p_create()`.

## Specifications

### The board

| Thing | Value |
| --- | --- |
| Address | `0x2C` (`KBSP_I2C_ADDR_MAG`) |
| Identity | Register `0x00` reads `0x80` (`KDRV_QMC5883P_CHIP_ID`) |
| Range | +-30 G, 1000 LSB per gauss, 1 mG per LSB |
| Mode | Continuous at 50 Hz while awake, suspend between uses |
| Oversampling | OSR1 = 8, OSR2 = 1 (down-sampling 1) |
| Reading | Raw milligauss on the chip's three axes. No remap, no calibration, no heading |
| Self-test | 10 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.

| Reg | Name | Value the driver writes, and what it is |
| --- | --- | --- |
| `0x00` | chip id | Reads `0x80` |
| `0x01` to `0x06` | `X`, `Y`, `Z` | Low byte then high byte of each axis, read as one burst |
| `0x09` | status | Bit 0 `DRDY` a conversion is ready. Bit 1 `OVFL` it overflowed |
| `0x0A` | `CTRL1` | `0x07`: continuous mode (`MODE` 11), 50 Hz (`ODR` 01), over-sampling 8 (`OSR1` 00), down-sampling 1 (`OSR2` 00). `0x00` is suspend |
| `0x0B` | `CTRL2` | `0x00`: 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 |
| `0x29` | axis sign | `0x06`, written first |

### The driver

| Call | What 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

| File | What it does |
| --- | --- |
| `motion/include/kdrv_qmc5883p.h` | The four calls and `KDRV_QMC5883P_CHIP_ID` |
| `motion/src/kdrv_qmc5883p.c` | The driver, written by hand from the datasheet (QMC5883P Rev A) |
| `kaos/imu/src/imu_ao.c` | AO_Imu: rest and wake, `kode_mag_read()`, the production row |

## Gotchas

> **WARNING**
>
> **The numbers do not point at north.** The Kode Dot's own field is several gauss and the reading includes it, so a heading from this reading describes the board first and the room second. Calibrate against the board's own field and compensate for tilt before you use it as a compass.

> **WARNING**
>
> **A QMC5883L driver reads the wrong bytes.** The registers differ. Data starts at `0x01`, status is `0x09`, and `0x00` is the chip id. Use the register table above.

> **NOTE**
>
> **A read can return `KODE_ERROR` with nothing wrong.** The first conversion after a wake is one 20 ms period away, and the driver waits 25 ms once. If your call still finds nothing ready, or an overflow, ask again. Do not call `kode_mag_read()` from an Active Object, and know that a second concurrent caller of either motion read gets `KODE_DENIED`.

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

```c
/*
 * 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

- [`components/kdrv/motion`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/motion): the magnetometer driver, beside the IMU's and the haptic driver's.
- [`kdrv_qmc5883p.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kdrv/motion/include/kdrv_qmc5883p.h): the four calls.
- [`components/kaos/imu`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/imu): AO_Imu, the owner.
