# Magnetometer

The compass in a Kode Dot: a QMC5883P on the shared I2C bus, what it measures, why the board itself is the first thing it sees, and the code to read it.

## What it is

The magnetometer measures the magnetic field where the Kode Dot is, on three
axes, in gauss. Away from magnets and iron, that field is the Earth's: about
half a gauss, pointing roughly north and, in Europe, steeply down. The
direction of the field across the two horizontal axes is a compass heading.

The part is a **QMC5883P** from QST: a magneto-resistive bridge on each axis,
a 16-bit converter, and temperature compensation on the chip, so what you
read out is already corrected. It is a sensor of *field*, not of *north*:
anything magnetic near it adds to what it reads, and on the Kode Dot several
things are.

## How it is connected

```mermaid
flowchart LR
  p4["ESP32-P4"] -- "I2C, 0x2C" --> mag["QMC5883P"]
```

*One part on the shared I2C bus, at its own address*

The QMC5883P sits on the P4's shared I2C bus at `0x2C`, brought up by
`kode_bsp_init` and reached through `kode_bsp_i2c()`. It has no interrupt
line and needs none: you configure it, then read six bytes whenever you want
the field. Whether its data-ready pin reaches the P4 is **TBD**; no Kode
example uses it.

## Specifications

| Thing | Value |
| --- | --- |
| Address | `0x2C`. Register `0x00` reads `0x80`, the part's identity |
| Range | ±30, ±12, ±8 or ±2 gauss, bits 3:2 of `CTRL2` (`0x0B`): 1000, 2500, 3750 or 15000 counts per gauss |
| Rate | 10, 50, 100 or 200 Hz, bits 3:2 of `CTRL1` (`0x0A`). Mode in bits 1:0: suspend, normal, single, continuous. Suspended out of reset |
| Output | `0x01`–`0x06`: x y z, `int16_t`, low byte first, temperature-compensated on the chip. Bit 0 of `0x09` says a sample is ready |
| Setup | The datasheet's sequence: `0x29` ← `0x06`, then `CTRL2`, then `CTRL1` |
| Resolution | 2 mG on X and Y, 3 mG on Z, at ±30 gauss |
| Supply | 2.5–3.6 V, 35 µA typical at 10 Hz. Bit 7 of `CTRL2` is a soft reset |
| Kode driver | None yet. **TBD**: a QMC5883P driver in `kode-sdk` |
| Validated | **TBD**: not yet run on a V1 board. Register map from QST's datasheet, rev. E |

## Gotchas

> **NOTE**
>
> **The compass sees the board before it sees the Earth.** The magnetic
> connector, the speaker and the haptic motor all sit millimetres from the
> QMC5883P, and their field moves with the sensor while the Earth's does not.
> Raw readings are a heading only after you rotate the Kode Dot, collect the
> per-axis minimum and maximum, and subtract the centre. The Earth is 250–650
> mG; if the peak-to-peak swing on rotation is far below that, you are reading
> the board. **TBD**: the offset of a production Kode Dot.

> **NOTE**
>
> **A heading is only right when the Kode Dot is flat.** Tilt it and the
> Earth's field, which points steeply down, leaks into the horizontal axes.
> Correcting for that takes the accelerometer's idea of down: see
> `device/motion/overview` for why the parts are used together.

## Example code

Put the compass in normal mode and log the field ten times a second. Turn
the Kode Dot a full circle on the table and each axis swings by hundreds of
milligauss.

```c
/*
 * compass.c - 2026-09-17 - from Kode with love
 *
 * Reads the Kode Dot's QMC5883P over the shared I2C bus. Puts the part in
 * normal mode at 200 Hz on the ±8 gauss range and logs the field in
 * milligauss ten times a second. Turn the Kode Dot on the table and the
 * three numbers swing by hundreds of milligauss.
 */
#include "driver/i2c_master.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "kode.h"

#define MAG_ADDR    0x2C
#define REG_XOUT_L  0x01   /* six bytes: x y z, low byte first */
#define REG_CTRL1   0x0A   /* mode, rate, oversampling */
#define REG_CTRL2   0x0B   /* range, set/reset, soft reset */
#define REG_SIGN    0x29   /* axis sign; the datasheet's setup writes 0x06 */

static i2c_master_dev_handle_t s_mag;

/* One register write: address byte, then the value. */
static void mag_write(uint8_t reg, uint8_t value)
{
    uint8_t frame[2] = { reg, value };
    ESP_ERROR_CHECK(i2c_master_transmit(s_mag, frame, 2, 100));
}

/* Attach the compass to the bus kode_bsp owns, configure it, then log forever. */
void app_main(void)
{
    ESP_ERROR_CHECK(kode_bsp_init());
    i2c_device_config_t cfg = { .device_address = MAG_ADDR, .scl_speed_hz = KODE_I2C_FREQ_HZ };
    ESP_ERROR_CHECK(i2c_master_bus_add_device(kode_bsp_i2c(), &cfg, &s_mag));

    mag_write(REG_SIGN, 0x06);    /* the datasheet's setup sequence, in its order */
    mag_write(REG_CTRL2, 0x08);   /* ±8 gauss, set/reset on */
    mag_write(REG_CTRL1, 0xCD);   /* normal mode, 200 Hz, oversampling 8 */

    while (1) {
        uint8_t reg = REG_XOUT_L, raw[6];
        ESP_ERROR_CHECK(i2c_master_transmit_receive(s_mag, &reg, 1, raw, 6, 100));
        int16_t x = (int16_t)(raw[0] | raw[1] << 8);
        int16_t y = (int16_t)(raw[2] | raw[3] << 8);
        int16_t z = (int16_t)(raw[4] | raw[5] << 8);
        ESP_LOGI("mag", "field %6ld %6ld %6ld mG", x * 1000L / 3750, y * 1000L / 3750, z * 1000L / 3750);
        vTaskDelay(pdMS_TO_TICKS(100));   /* 3750 counts per gauss on the ±8 G range */
    }
}
```

Compiled on the date in the header against the `kode` component with
ESP-IDF v6.1. **TBD**: a run on a V1 board.

## In the SDK

- [`kdrv_motion`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/kdrv_motion): the motion drivers in `kode-sdk`. **TBD**: a QMC5883P driver; today the folder holds the prototype's `lis2mdl`.
