# 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

```mermaid
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 and its callers*

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.

```mermaid
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
```

*What can hold the IMU*

### 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 with `KODE_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:

1. Read `WHO_AM_I`.
2. 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.
3. 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:

1. Read the chip id.
2. Run the part's self-test, log the shift it reports and publish nothing: the data sheet gives no threshold to judge it against.
3. 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

> **NOTE**
>
> **The first read after a rest takes about 200 ms.** The wake waits for the gyroscope. Reads within the next 2 s are answered at once. The facade's 600 ms wait covers the wake. A read right after a row or a witness window waits too, since both turn the gyroscope back on when they end.

> **WARNING**
>
> **A read is refused, not queued, while a row or a window holds the IMU.** You get `KODE_DENIED`, so ask again later. The magnetometer is not refused for this reason.

> **WARNING**
>
> **The IMU row fails if the board is in your hand.** Step 3 judges rest. Put the Kode Dot flat on a table and do not touch it. Opening the console port restarts the board, so wait for it to boot first.

> **NOTE**
>
> **An absent part stays absent until a row or a capture asks again.** After a failed start every sample request for it answers `KODE_IO_ERROR`. The magnetometer row's self-test shift and its field are readings, not verdicts.

## 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`.

```c
/*
 * 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`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/imu): `imu_ao.c`, the wake and rest logic, the witness window and the two rows.
- [`kode.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kode/include/kode.h): `kode_imu_read()` and `kode_mag_read()`.
- [`components/kdrv/motion`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/motion): the QMI8658A and QMC5883P drivers; see `sdk/kdrv/qmi8658a` and `sdk/kdrv/qmc5883p`.
