SDK

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

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:

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:

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

ThingValue
Address0x6A (KBSP_I2C_ADDR_IMU), SA0 high. WHO_AM_I 0x05 (KDRV_QMI8658_WHOAMI)
Normal configurationAccelerometer +-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 windowAccelerometer only, +-8 g, 1000 Hz, 128 samples (KDRV_QMI8658_FIFO_DEPTH), 244 µg per LSB (KDRV_QMI8658_UG_PER_LSB)
Self-test resultFixed point whatever the scale: 2048 LSB per g for the accelerometer, 16 LSB per dps for the gyroscope
Self-test floorsEvery axis shifts at least 200 mg (accelerometer, datasheet 15.1) or 300 dps (gyroscope, datasheet 15.2), in either direction
IMU row limitsGravity 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, INT2Both driven outputs with nothing mapped. INT1 to expander P5

The registers

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

RegNameValue the driver writes, and what it is
0x00WHO_AM_IReads 0x05
0x01revisionReads 0x7C. Logged, not judged
0x02CTRL10x58: auto-increment on, little-endian, INT1 and INT2 driven. 0x18 during a FIFO drain (auto-increment off)
0x03CTRL20x23: accelerometer +-8 g (aFS 010), rate code 0011. Bit 7 aST starts the accelerometer self-test
0x04CTRL30x53: gyroscope +-512 dps (gFS 101), rate code 0011. Bit 7 gST starts the gyroscope self-test
0x06CTRL50x00: no low-pass filter
0x08CTRL7Sensor enables: 0x03 both, 0x01 accelerometer only (witness), 0x00 neither (rest)
0x09CTRL80x80: report a CTRL9 command's completion on STATUSINT, not on INT1
0x0ACTRL9Host commands: 0x04 reset the FIFO, 0x05 request the FIFO, 0x00 acknowledge
0x14FIFO_CTRL0x0D: 128 samples (FIFO_SIZE 11), FIFO mode (FIFO_MODE 01). 0x00 bypass, which empties the FIFO
0x15FIFO_SMPL_CNT, FIFO_STATUSHow many samples the FIFO holds
0x17FIFO_DATAThe FIFO, read as a burst of up to 32 samples
0x2DSTATUSINTBit 7: a CTRL9 command is done. Bit 0: the self-test is done
0x33TEMP_LStart of the 14-byte sample burst: temperature low and high, AX_L to AZ_H, GX_L to GZ_H
0x51dVX_LStart of the self-test result, dVX_L to dVZ_H

The driver

CallWhat 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

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

Gotchas

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.

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