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 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:
- The axis sign register
0x29, with0x06. 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. - 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. - The mode in
CTRL1: suspend. The part is at rest whenkdrv_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 - onfor X andon - offfor 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, andreadandself_testreturnKODE_DENIEDuntil the nextkdrv_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
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
components/kdrv/motion: the magnetometer driver, beside the IMU's and the haptic driver's.kdrv_qmc5883p.h: the four calls.components/kaos/imu: AO_Imu, the owner.