php-esp32 · v1.5.x
Docs · Recipes

Read the QMI8658 IMU

Read the QMI8658 6-axis IMU (accelerometer + gyroscope + on-chip temperature) over I²C: direct reads, background polling on core 1, or as a SamplesReady event source in the event-driven model.

The QMI8658 is a 6-axis IMU — a 3-axis accelerometer and a 3-axis gyroscope, plus an on-chip temperature sensor — on the I²C bus. It ships as a driver in the i2c extension, exposed as Baremetal\I2c\Driver\Qmi8658.

Enable it

toml php-esp32.config.toml
[extensions.i2c]
enabled = true
drivers = ["qmi8658"]

Wire it up

It sits on an I²C bus; you pass the pins when you make the bus. On the ESP32-S3-Touch boards the IMU is on SDA=11, SCL=10:

php make the bus and driver
use Baremetal\I2c\Bus;
use Baremetal\I2c\Driver\Qmi8658;

$imu = new Qmi8658(new Bus(sda: 11, scl: 10));

new Qmi8658(Bus $bus, int $address = auto, int $hz = 400000) — the address defaults to the chip's (0x6B/0x6A) and the bus runs at 400 kHz. Other devices can share the same bus (different addresses).

Direct reads

The simplest use — read the current values whenever you like (each call touches the bus):

  • accel(): array → [x, y, z] in g
  • gyro(): array → [x, y, z] in °/s (dps)
  • temp(): float → the on-chip temperature in °C
php project-src/index.php (init-loop)
<?php
use Baremetal\I2c\Bus;
use Baremetal\I2c\Driver\Qmi8658;

$imu = new Qmi8658(new Bus(sda: 11, scl: 10));

function loop(int $tick): void
{
    global $imu;
    [$ax, $ay, $az] = $imu->accel();
    [$gx, $gy, $gz] = $imu->gyro();
    printf("accel %+.2f %+.2f %+.2f g   gyro %+.1f %+.1f %+.1f dps   %.1f C\n",
        $ax, $ay, $az, $gx, $gy, $gz, $imu->temp());
    delay(200);
}

At rest one accel axis reads ≈ ±1 g (gravity) and the gyro reads ≈ 0.

Polling on core 1

For a steady, jitter-free sample rate, hand the sampling to the executor on core 1: it reads the sensor at a fixed rate into a ring buffer, independent of how slow your loop is. Your code then reads snapshots from the ring — no bus traffic, never blocked by a busy loop.

  • poll(int $hz, int $depth = 64, ?string $event = null): void — start the core-1 poller.
  • sample(): ?string — the most recent raw sample (12 bytes), or null if none yet.
  • drain(): array — every raw sample taken since the last call (oldest first).
  • decode(string $raw): array → ['accel' => [x,y,z], 'gyro' => [x,y,z]].
php project-src/index.php (init-loop + core-1 poll)
<?php
use Baremetal\I2c\Bus;
use Baremetal\I2c\Driver\Qmi8658;

$imu = new Qmi8658(new Bus(sda: 11, scl: 10));
$imu->poll(hz: 100, depth: 64);          // sampling now runs on core 1

function loop(int $tick): void
{
    global $imu;
    $batch = $imu->drain();              // ~20 raw samples at a 5 Hz loop, none lost
    foreach ($batch as $raw) {
        ['accel' => [$ax, $ay, $az]] = $imu->decode($raw);
        // ... process every sample ...
    }
    delay(200);
}

See the imu-poll-core1 example.

As an event source (event-driven)

Add event: and each new core-1 sample raises a Baremetal\Sensor\Imu\SamplesReady event on the reactor, carrying the sensor as $e->device. No loop, no polling in your code — you react to fresh data:

php project-src/index.php (event-driven)
<?php
use Baremetal\Events;
use Baremetal\I2c\Bus;
use Baremetal\I2c\Driver\Qmi8658;
use Baremetal\Sensor\Imu\SamplesReady;

$imu = new Qmi8658(new Bus(sda: 11, scl: 10));

Events::listen(SamplesReady::class, function (SamplesReady $e): void {
    ['accel' => [$x, $y, $z]] = $e->device->decode($e->device->sample());
    printf("accel %+.2f %+.2f %+.2f g\n", $x, $y, $z);
});

$imu->poll(hz: 30, depth: 16, event: SamplesReady::class);

Keep the rate modest (each event shares the reactor queue with anything else — HTTP, timers): ~30 Hz is plenty for a UI and leaves the queue room. The imu-ws-stream example streams accel + gyro + temperature to a browser over WebSocket this way.

The Imu capability

The driver implements Baremetal\Sensor\Imu, a marker interface. Code that only needs "an IMU" can accept any chip that implements it, so swapping the QMI8658 for another IMU driver later doesn't change the consumer:

php the Imu capability
if ($imu instanceof Baremetal\Sensor\Imu) {
    // it's an IMU, whatever the chip
}

Notes

  • accel()/gyro()/temp() each read the bus on call; while a core-1 poller is running they still work (the bus is locked per transaction), but prefer sample()/drain() from the ring to avoid extra I/O.
  • Temperature is a separate register (not part of the polled block), so read it with temp() when you need it rather than every sample.
  • The accelerometer is ±4 g and the gyroscope ±512 °/s by default.