SDK
AO_Imu
Owns the accelerometer, gyroscope and magnetometer of a Kode Dot: wake and rest of the parts, the haptic witness window and the two self-tests.
What it is
AO_Imu is the single owner of the motion chips of a Kode Dot. The QMI8658A holds an accelerometer and a gyroscope, and the QMC5883P is a magnetometer. Your app asks AO_Imu for a sample and gets milli-g, milli-degrees per second, tenths of a degree and milligauss.
The QMI8658A has one FIFO that empties when read, so two readers would each get half a window and neither would know. That is the reason the whole chip has one owner. The magnetometer is its own chip on the same bus, so the hardware does not force one owner for both. The design does: the production rows of both chips and the witness window of the haptic test are one group of work, and one Active Object gives it one claim and one queue.
AO_Imu also serves as the witness of the haptic test: while the motor runs, it collects a window of accelerometer samples that AO_Haptic judges (sdk/kaos/haptic). The chips and their registers are on sdk/kdrv/qmi8658a and sdk/kdrv/qmc5883p.
How it is connected
flowchart LR app["kode_imu_read()<br/>kode_mag_read()"] -- "IMU_READ_REQ_SIG<br/>MAG_READ_REQ_SIG" --> ao["AO_Imu<br/>priority 21"] hap["AO_Haptic"] -- "IMU_CAPTURE_REQ_SIG<br/>IMU_DRAIN_REQ_SIG" --> ao aut["AO_Autotest"] -- "IMU_TEST_REQ_SIG<br/>MAG_TEST_REQ_SIG" --> ao ao -- "I2C 0x6A" --> imu["QMI8658A"] ao -- "I2C 0x2C" --> mag["QMC5883P"] class app app class ao,hap,aut ao class imu,mag chip
AO_Imu in numbers:
| Thing | Value |
|---|---|
| Priority | 21 (KODE_PRIO_IMU), one level under AO_Haptic |
| Queue | 16 events |
| Stack | 4096 bytes |
| Claims | KBSP_RES_IMU, one resource for both chips |
| Subscribes to | Nothing |
| Kconfig gate | None, since both parts are on the board |
AO_Haptic outranks it because AO_Haptic feeds a motor on a deadline that the IMU's drain does not have. AO_Imu listens to no other owner: it answers requests and publishes test records. kode_init() starts it right before AO_Haptic, which posts to it from its first step.
One state and two flags
Imu_up is the only state. There is no absent state. Two flags, imu_up and mag_up, say whether each chip answered at boot. A chip that did not answer is refused at once with KODE_IO_ERROR and is not tried again on every call. A production row is different: an operator may have recently fitted the part, so a row always tries again, and a witness capture asks the QMI8658A again too. Inside Imu_up, a phase says what holds the IMU.
stateDiagram-v2 [*] --> NONE NONE --> ST_XL: IMU_TEST_REQ_SIG ST_XL --> ST_GY: accelerometer self-test done ST_GY --> SETTLE: sensors back on SETTLE --> NONE: 300 ms NONE --> CAP_ARMED: IMU_CAPTURE_REQ_SIG CAP_ARMED --> CAP_DRAIN: IMU_DRAIN_REQ_SIG CAP_DRAIN --> SETTLING: last chunk measured SETTLING --> NONE: 300 ms
Parts rest until asked
At boot the QMI8658A is set to ±8 g and ±512 dps at 896.8 Hz with both sensors disabled, and the QMC5883P to ±30 G and suspended; awake it converts at 50 Hz.
A request wakes a part. A part nobody has asked for 2 s (KODE_IMU_REST_AFTER_MS) goes back to rest, on a separate one-shot timer for each part. An app that reads a few times a second always finds the part awake, and an app that reads only the magnetometer never wakes the IMU. Each kind of request behaves differently:
- Read of an awake IMU. Answered in the step that receives it.
- Read of an IMU at rest. AO_Imu enables both sensors and holds the request, as a copy, for 200 ms (
GYRO_READY_MS), since the gyroscope needs about 150 ms before its sample means anything. It holds one read at a time. A second read is refused withKODE_DENIED, unless the caller of the held read has stopped waiting, in which case the new read takes its place. - Read of the magnetometer. AO_Imu starts the part if it is suspended and reads in the same step. The first conversion is one 20 ms period away, and the driver waits for it once.
- A request while a row or a window holds the IMU. Refused with
KODE_DENIED, never queued. A magnetometer read is still answered. - Gyroscope turn-on. After a wake, a self-test or a witness window the gyroscope needs 300 ms (
SETTLE_MS), and the part ignores a FIFO reset or a self-test trigger meanwhile. A capture or a row that arrives then is held until the 300 ms end. Before a self-test, a part that is awake is put to rest and given 150 ms (ROW_REST_MS), because a trigger written together with the disable was ignored about one row in eight.
The witness window
IMU_CAPTURE_REQ_SIG arms the FIFO, which then collects at most 128 accelerometer samples, and answers IMU_ARMED_SIG. IMU_DRAIN_REQ_SIG reads the window out 16 samples at a time (CAP_CHUNK), one chunk every 10 ms (CAP_CHUNK_MS), so the bus is not held long enough to starve the audio codec's refill. A one-shot QTimeEvt brings AO_Imu back for each chunk, because a self-post would keep the queue non-empty and starve every lower priority.
At the last chunk it measures the window at the frequency the caller asked for and posts IMU_ENERGY_SIG. A window under 32 samples is refused, and AO_Haptic itself refuses to judge fewer than 120. A capture the holder never drains times out after 1000 ms (KODE_IMU_CAPTURE_MAX_MS).
The IMU row
test component=imu has three steps:
- Read
WHO_AM_I. - Run the manufacturer's self-test, accelerometer then gyroscope, polled every 20 ms on the one-shot timer with limits of 15 and 50 polls. The stimulus is an electrostatic force inside the die, so a bench that is being carried still gets a verdict.
- Judge the board at rest: gravity within 150 mg of 1 g, rotation under 20 dps and the die between 0 and 60 °C.
The magnetometer row
test component=mag numbers three steps and prints two:
- Read the chip id.
- Run the part's self-test, log the shift it reports and publish nothing: the data sheet gives no threshold to judge it against.
- Report the field at rest and do not pass or fail it, since the Kode Dot's own field is large and a verdict would judge the board's magnetism instead of the part.
Resting with the board
With CONFIG_KODE_LOWPOWER, AO_Imu joins the rounds of AO_LowPower as imu (sdk/kaos/lowpower). On a round it puts both parts into standby, the state they sit in between requests anyway, and stops their rest one-shots. A row, a witness window or a read waiting for the gyroscope holds a part in the middle of something the caller is timing, so then it answers busy with imu busy.
Nothing is restored when the board wakes. The parts wake for whoever asks next, as they always do, so AO_Imu listens to the rounds and not to LOWPOWER_STATE_SIG.
Specifications
Signals
| Signal | Event | Direction | Meaning |
|---|---|---|---|
IMU_READ_REQ_SIG | ImuReadEvt | in, posted | One sample into the caller's own storage. Sent by kode_imu_read() |
MAG_READ_REQ_SIG | MagReadEvt | in, posted | One magnetometer sample. Sent by kode_mag_read() |
IMU_CAPTURE_REQ_SIG | ImuCaptureReqEvt: who | in, posted | Arm the FIFO for a witness window |
IMU_DRAIN_REQ_SIG | ImuDrainReqEvt: who, hz | in, posted | Drain the window and measure it at hz |
IMU_TEST_REQ_SIG, MAG_TEST_REQ_SIG | none | in, posted by AO_Autotest | The two rows |
IMU_WAIT_SIG | none | self, one-shot | A drain chunk, a capture deadline, a gyroscope turn-on or a self-test poll |
IMU_REST_SIG, MAG_REST_SIG | none | self, one-shot | A part unused for 2 s: back to rest |
LOWPOWER_ROUND_SIG | LowPowerRoundEvt | in, subscribed | With CONFIG_KODE_LOWPOWER: both parts to standby, or busy imu busy |
IMU_ARMED_SIG | ImuArmedEvt | out, posted to who | The window is filling |
IMU_ENERGY_SIG | ImuEnergyEvt: rc, n, bin_x10, rms_x10, axis | out, posted to who | Levels in 0.1 mg; n is the samples the window held |
IMU_TEST_SIG | ImuTestEvt: step, phase, flags, a to d | out, published | Phases IMU_TEST_WHOAMI, _ST, _GRAVITY; flag IMU_TF_PASS |
MAG_TEST_SIG | MagTestEvt | out, published | One record of the magnetometer row |
TEST_RESULT_SIG | named imu or mag | out, published | The verdict |
Numbers and calls
| Thing | Value |
|---|---|
| Addresses | QMI8658A 0x6A, QMC5883P 0x2C, on the shared I2C bus |
| IMU configuration at boot | ±8 g, ±512 dps, 896.8 Hz, sensors disabled |
| Magnetometer configuration at boot | ±30 G, suspended; 50 Hz when awake |
kode_imu_read(&sample) | Blocks up to 600 ms (KODE_IMU_READ_WAIT_MS); never from an AO. Fills a_mg[3], w_mdps[3] and temp_dc (tenths of a degree). KODE_TIMEOUT, KODE_IO_ERROR for an absent part, KODE_NO_RESOURCES, KODE_DENIED for NULL, a second caller or a row holding the part |
kode_mag_read(mg) | Blocks up to 600 ms (KODE_MAG_READ_WAIT_MS); raw milligauss on the QMC5883P's own axes, no heading and no calibration. KODE_ERROR when no conversion was ready or one overflowed. The two reads share one claim |
| Row budgets | IMU 2.5 s (KODE_IMU_TEST_BUDGET_MS), magnetometer 5 s (KODE_MAG_TEST_BUDGET_MS) |
| Row limits | Gravity ±150 mg, rotation 20 dps, temperature 0 to 60 °C |
| Console | test component=imu and test component=mag only. There is no imu or mag verb and neither part has a field table |
| Settings and Kconfig | mag_cal, the magnetometer's stored correction (internals/settings). No Kconfig |
Gotchas
Example code
This prints the acceleration on the three axes twice a second. With the Kode Dot flat on a table, the third axis reads about 1000 mg and the other two near 0. The log level must be i: config key="log" value=i.
/*
* imu_read.c - 2026-10-28 - from Kode with love
*
* Reads the accelerometer of the Kode Dot twice a second through AO_Imu and
* logs the three axes in milli-g. Lying flat on a table, one axis reads about
* 1000 mg and the other two read near 0.
*/
#include "kode.h"
/* Bring the system up, then read and log a sample every 500 ms. */
void app_main(void)
{
kode_init();
while (1) {
kdrv_qmi8658_sample_t sample;
if (kode_imu_read(&sample) == KODE_OK) {
KLOG_I("imu_read", "a = %d, %d, %d mg",
(int)sample.a_mg[0], (int)sample.a_mg[1],
(int)sample.a_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/kaos/imu:imu_ao.c, the wake and rest logic, the witness window and the two rows.kode.h:kode_imu_read()andkode_mag_read().components/kdrv/motion: the QMI8658A and QMC5883P drivers; seesdk/kdrv/qmi8658aandsdk/kdrv/qmc5883p.