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 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 withKODE_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
CTRL9command handshake. The driver writes the command, waits forSTATUSINTbit 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_DATAis read in a burst and that a burst walks on to the next register. With auto-increment off both are true: every byte isFIFO_DATA. The driver switchesCTRL1to0x18for 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:
- It removes each axis's mean, so the 1 g of gravity does not leak into the bin of the requested frequency.
- It weights the samples with a Hann window and runs a Goertzel detector at the requested frequency on each axis.
- It takes the loudest axis, so the reading is the same whichever way the board lies.
- 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:
- Both sensors are disabled.
- The stimulus bit of the sensor under test is set.
- The part works on its own (about 25 ms for the accelerometer, about 400 ms for the gyroscope).
- The driver polls
STATUSINTbit 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
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
components/kdrv/motion: the IMU driver and the witness arithmetic, beside the magnetometer's and the haptic driver's.kdrv_qmi8658.h: the calls, the self-test floors and the window constants.components/kaos/imu: AO_Imu, the owner of both motion sensors.