# QMI8658A motion sensor

The accelerometer and gyroscope of a Kode Dot: how the board knows which way is down and how it turns, the registers the driver uses and the FIFO window.

## What it is

The **QMI8658A** from QST answers two questions: which way is down, and how fast the board is turning. It is a six-axis inertial sensor. The accelerometer measures acceleration on three axes, and at rest it measures gravity. The gyroscope measures rotation on three axes. The chip also reads its own die temperature and keeps a FIFO of 128 samples, a buffer that fills on the chip's clock and is emptied over the bus.

It has two jobs on the Kode Dot. An app can read the board's motion and orientation with `kode_imu_read()`. And it is the **witness** of the haptic production row: the motor and the sensor sit in the same case, so the accelerometer feels whether the motor vibrates. See `sdk/kdrv/aw86233`.

The compass is a separate chip. The QMC5883P sits beside this one on the same bus and is not behind it. See `sdk/kdrv/qmc5883p`.

## How it is connected

```mermaid
flowchart LR
  subgraph board["On the Kode Dot"]
    imu["QMI8658A"] -- "I2C 0x6A" --> bus["shared I2C bus"]
    imu -- "INT1" --> p5["TCA6408A P5"]
    bus --> ao["AO_Imu"]
    ao -- "witness window" --> hap["AO_Haptic"]
  end
  ao -- "kode_imu_read()" --> app["your app"]
  class imu,p5 chip
  class ao,hap ao
  class app app
```

*The IMU on the Kode Dot*

The chip is U10 on the main PCB, at `0x6A` (`KBSP_I2C_ADDR_IMU`) on the shared bus: SDA on GPIO4, SCL on GPIO5, 400 kHz. The address pin SA0 is tied high. `WHO_AM_I` reads `0x05`.

### The interrupt pins

**INT1 goes to pin P5 of the expander and nothing is mapped to it.** P5 has no pull-up on the board, so a floating input there would be an interrupt source nobody asked for. The driver makes INT1 a driven output. INT2 is wired to nothing.

The driver drives it as well, because the datasheet describes the self-test's done flag (`STATUSINT` bit 0) as mirroring "the same value of INT2 level", and one reading of that sentence is that the flag never rises on a floating pad. Driving a pin wired to nothing costs nothing. Command completion is read from `STATUSINT` bit 7 and not from INT1, so a `CTRL9` command raises no interrupt that wakes AO_IoExp for nothing.

### The owner

**AO_Imu** owns both motion sensors (`KBSP_RES_IMU`) at priority 21. The parts rest until asked:

- At boot both parts are configured and left in standby. The QMI8658A has both sensors disabled.
- A request wakes the part, and it stays awake for 2 s after the last request (`KODE_IMU_REST_AFTER_MS`). A caller reading a few times a second finds the part awake every time.
- Waking costs the gyroscope its turn-on, about 150 ms before its first sample means anything. A read that finds the IMU resting is held, as one copy, and answered after that. A read that finds it awake is answered inside the step that receives it.
- `kode_imu_read()` waits up to 600 ms (`KODE_IMU_READ_WAIT_MS`), never from an Active Object. A second concurrent caller of either motion read is refused with `KODE_DENIED`. When a production row or a witness window holds the IMU the read is refused too, because the rates and scales are then the batch's and a number at the wrong scale looks right.
- A board without the part still starts the AO and claims the resource. A sample request is refused at once with `KODE_IO_ERROR`.

A witness window is 128 samples and each costs one I2C read, so taking them in one step would hold the bus for tens of milliseconds at a priority above the codec's I2S refill.

The window comes out 16 samples at a time (`CAP_CHUNK`) on a 10 ms one-shot. A timer is used instead of a self-post because a self-post keeps the queue non-empty and would run all eight chunks back to back. A window nobody drains is disarmed after 1 s (`KODE_IMU_CAPTURE_MAX_MS`).

### The driver

**The driver is conservative at every register.** `CTRL1` resets to `0x20`: big-endian with register auto-increment off. Either would turn the 14-byte sample burst into one byte read fourteen times, in the wrong order, and the only symptom would be a gravity vector that makes no sense. So `kdrv_qmi8658_create()` writes `CTRL1` and every other configuration register and reads each back. Identity is `WHO_AM_I` alone. The revision byte at `0x01` reads `0x7C` and the driver logs it without judging it.

The driver is written by hand from the datasheet (QMI8658A Rev A), since QST publishes no driver to wrap, and it has no field table.

### The FIFO

The FIFO needs two special cases:

- **FIFO control uses the `CTRL9` command handshake.** The driver writes the command, waits for `STATUSINT` bit 7, writes the acknowledge, and waits for the bit to fall. A working part takes a few milliseconds over a FIFO reset (sixteen back-to-back reads, about 2 ms, were not enough), so each wait reads once a millisecond for up to 50 ms. The handshake separates "the FIFO is empty" from "the FIFO was told to empty and never confirmed it".
- **A FIFO burst needs auto-increment off.** The datasheet says `FIFO_DATA` is read in a burst and that a burst walks on to the next register. With auto-increment off both are true: every byte is `FIFO_DATA`. The driver switches `CTRL1` to `0x18` for the drain and back afterwards.

Every window starts with up to two records that are not samples. Measured on a Kode Dot at every window: the first record reads `0x8000` on all three axes, the part's full-scale negative, and one window in four has a torn second record with a full-scale axis. `kdrv_qmi8658_fifo_open()` skips at most `QMI_FIFO_JUNK_MAX` (2) of them and reports the count that is left.

### The haptic witness

**The witness uses the accelerometer alone at 1000 Hz.** Rate code `0011` gives 1000 Hz with the gyroscope off and 896.8 Hz with both on, so a window turns the gyroscope off.

At 1000 Hz the 128-sample FIFO is 128 ms of data, and a 200 ms buzz fills it completely. Every window fills the same way, so every window compares with every other. `kimu_energy()` measures a window:

1. It removes each axis's mean, so the 1 g of gravity does not leak into the bin of the requested frequency.
2. It weights the samples with a Hann window and runs a Goertzel detector at the requested frequency on each axis.
3. It takes the loudest axis, so the reading is the same whichever way the board lies.
4. It reports the broadband RMS over the three axes, without the Hann weights.

Levels leave as integers in 0.1 mg. A window of fewer than `KIMU_ENERGY_MIN_N` (32) samples is refused with `KODE_DENIED`. The Goertzel kernel is `kgoertzel.h`, shared with the audio detector in `sdk/kdrv/es8311`.

### The self-test

**The self-test is a force inside the die.** It does not need the board to be still, so it is the one measurement a moving bench cannot disturb. The steps follow section 15 of the datasheet:

1. Both sensors are disabled.
2. The stimulus bit of the sensor under test is set.
3. The part works on its own (about 25 ms for the accelerometer, about 400 ms for the gyroscope).
4. The driver polls `STATUSINT` bit 0, clears the trigger, waits up to 8 reads for the bit to fall, and reads the result registers.

A self-test triggered in the same breath as the write that disables the sensors is one the part sometimes never acts on, about one row in eight, so AO_Imu gives an awake part 150 ms of rest first.

## Specifications

### The board

| Thing | Value |
| --- | --- |
| Address | `0x6A` (`KBSP_I2C_ADDR_IMU`), SA0 high. `WHO_AM_I` `0x05` (`KDRV_QMI8658_WHOAMI`) |
| Normal configuration | Accelerometer +-8 g, gyroscope +-512 dps, both at rate code `0011` = 896.8 Hz, no low-pass filter, little-endian, auto-increment on |
| Scales | +-8 g: 4096 LSB per g (0.244 mg per LSB). +-512 dps: 64 LSB per dps. Temperature: 256 LSB per degree, reported in tenths of a degree |
| Witness window | Accelerometer only, +-8 g, 1000 Hz, 128 samples (`KDRV_QMI8658_FIFO_DEPTH`), 244 µg per LSB (`KDRV_QMI8658_UG_PER_LSB`) |
| Self-test result | Fixed point whatever the scale: 2048 LSB per g for the accelerometer, 16 LSB per dps for the gyroscope |
| Self-test floors | Every axis shifts at least 200 mg (accelerometer, datasheet 15.1) or 300 dps (gyroscope, datasheet 15.2), in either direction |
| IMU row limits | Gravity within 150 mg of 1000 mg (`KODE_IMU_GRAVITY_TOL_MG`), rotation at rest under 20000 mdps (`KODE_IMU_REST_MAX_MDPS`), die temperature 0 to 60.0 °C |
| INT1, INT2 | Both driven outputs with nothing mapped. INT1 to expander P5 |

### The registers

Only the registers the driver uses. The reset value of `CTRL1` is `0x20`.

| Reg | Name | Value the driver writes, and what it is |
| --- | --- | --- |
| `0x00` | `WHO_AM_I` | Reads `0x05` |
| `0x01` | revision | Reads `0x7C`. Logged, not judged |
| `0x02` | `CTRL1` | `0x58`: auto-increment on, little-endian, INT1 and INT2 driven. `0x18` during a FIFO drain (auto-increment off) |
| `0x03` | `CTRL2` | `0x23`: accelerometer +-8 g (`aFS` 010), rate code `0011`. Bit 7 `aST` starts the accelerometer self-test |
| `0x04` | `CTRL3` | `0x53`: gyroscope +-512 dps (`gFS` 101), rate code `0011`. Bit 7 `gST` starts the gyroscope self-test |
| `0x06` | `CTRL5` | `0x00`: no low-pass filter |
| `0x08` | `CTRL7` | Sensor enables: `0x03` both, `0x01` accelerometer only (witness), `0x00` neither (rest) |
| `0x09` | `CTRL8` | `0x80`: report a `CTRL9` command's completion on `STATUSINT`, not on INT1 |
| `0x0A` | `CTRL9` | Host commands: `0x04` reset the FIFO, `0x05` request the FIFO, `0x00` acknowledge |
| `0x14` | `FIFO_CTRL` | `0x0D`: 128 samples (`FIFO_SIZE` 11), FIFO mode (`FIFO_MODE` 01). `0x00` bypass, which empties the FIFO |
| `0x15` | `FIFO_SMPL_CNT`, `FIFO_STATUS` | How many samples the FIFO holds |
| `0x17` | `FIFO_DATA` | The FIFO, read as a burst of up to 32 samples |
| `0x2D` | `STATUSINT` | Bit 7: a `CTRL9` command is done. Bit 0: the self-test is done |
| `0x33` | `TEMP_L` | Start of the 14-byte sample burst: temperature low and high, `AX_L` to `AZ_H`, `GX_L` to `GZ_H` |
| `0x51` | `dVX_L` | Start of the self-test result, `dVX_L` to `dVZ_H` |

### The driver

| Call | What it does |
| --- | --- |
| `kdrv_qmi8658_create(&id)` | Probes `0x6A`, checks `WHO_AM_I`, writes and reads back the configuration, leaves both sensors disabled. `KODE_IO_ERROR` when nothing answers, `KODE_ERROR` when something else does. Safe to call again after a failure |
| `kdrv_qmi8658_standby(on)` | `true` disables both sensors, `false` enables both. The output registers keep the last conversion in standby, so a read there returns a stale sample |
| `kdrv_qmi8658_read(&out)` | One 14-byte burst, so temperature, acceleration and rotation belong to the same instant. Converted to mg, mdps and tenths of a degree. `out` is untouched when the bus refuses |
| `kdrv_qmi8658_st_begin(which)`, `_st_poll(which, &done, shift)`, `_st_judge(which, shift)`, `_st_end()` | The self-test: start it, ask without waiting, judge by the datasheet's floors (arithmetic only), and restore the resting configuration. `_st_end()` is safe whether a test finished, timed out or never began |
| `kdrv_qmi8658_fifo_arm()`, `_fifo_open(&samples)`, `_fifo_read(xyz, n)`, `_fifo_disarm()` | One window: gyroscope off and FIFO armed at 1000 Hz; read mode and the count; at most 32 samples per call in one burst; FIFO off and both sensors back on |
| `kimu_energy(xyz, n, hz, fs_hz, ug_per_lsb, &out)` | The energy at one frequency in a window, in 0.1 mg |

Every call returns `KODE_DENIED` before a successful `kdrv_qmi8658_create()`. The driver takes no decisions. When to wake, rest or sample belongs to AO_Imu.

### The files

| File | What it does |
| --- | --- |
| `motion/include/kdrv_qmi8658.h`, `motion/src/kdrv_qmi8658.c` | The IMU: configuration, sample, self-test, FIFO window |
| `motion/include/kimu_energy.h`, `motion/src/kimu_energy.c` | The witness arithmetic, on its own so the host tests run it with no bus |
| `kaos/imu/src/imu_ao.c` | AO_Imu: rest and wake, the rows, the window drain |

## Gotchas

> **WARNING**
>
> **The gravity vector makes no sense.** The chip resets big-endian with auto-increment off, and either breaks the 14-byte burst. Write `CTRL1` and read it back, as `kdrv_qmi8658_create()` does, before you trust a sample.

> **WARNING**
>
> **The first read after a quiet spell takes about 200 ms, and a read can be refused.** The gyroscope needs about 150 ms to turn on, and AO_Imu puts the part to rest 2 s after the last request. During a production row or a haptic witness window the read is refused with `KODE_DENIED`. Retry later, and do not call `kode_imu_read()` from an Active Object.

> **NOTE**
>
> **The rate is 896.8 Hz with both sensors and 1000 Hz with the accelerometer alone.** The same rate code gives both, which is why a witness window turns the gyroscope off. Check the sample rate before you analyse a window.

> **NOTE**
>
> **The head of a FIFO window is not samples.** Skip up to two records that read full scale on an axis, as `kdrv_qmi8658_fifo_open()` does, or an analysis starts with a spike.

## Example code

Log the board's acceleration and rotation twice a second. Lay the Kode Dot flat and the three acceleration values combine to about 1000 mg, one g, while the rotation values stay near zero. Pick it up and turn it, and the rotation values move.

```c
/*
 * imu_log.c - 2026-10-28 - from Kode with love
 *
 * Reads the Kode Dot's accelerometer and gyroscope twice a second and logs
 * one line per sample. At rest the three accelerations (mg) combine to about
 * 1000, one g, whichever way up the board lies, and the rotation stays near 0.
 */
#include "kode.h"

/* Bring the system up, then log a motion sample every 500 ms. */
void app_main(void)
{
    kdrv_qmi8658_sample_t sample;

    kode_init();

    while (1) {
        if (kode_imu_read(&sample) == KODE_OK) {
            KLOG_I("imu", "a_mg=%ld,%ld,%ld w_mdps=%ld,%ld,%ld",
                   (long)sample.a_mg[0], (long)sample.a_mg[1],
                   (long)sample.a_mg[2], (long)sample.w_mdps[0],
                   (long)sample.w_mdps[1], (long)sample.w_mdps[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 IMU driver and the witness arithmetic, beside the magnetometer's and the haptic driver's.
- [`kdrv_qmi8658.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kdrv/motion/include/kdrv_qmi8658.h): the calls, the self-test floors and the window constants.
- [`components/kaos/imu`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/imu): AO_Imu, the owner of both motion sensors.
