Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 32 of 56

Sensors: the QMI8658C IMU and SHT41

One register-mapped device and one command-based device — the two shapes almost every I2C sensor takes, and how each reports that its reading is trustworthy.

The shared shape

Every I2C device driver here follows the same pattern, and it is worth stating once because it repeats across the rest of these pages:

The driver hard-codes no board wiring. You tell Setup which host, which SDA/SCL pins, optionally which INT pin, and the address; the Device remembers them. Each operation then opens a short-lived I2C session for one complete transaction and lets it release the host on scope exit. So devices share a bus safely, and a fault mid-transaction cannot leak the lock. A Device is limited — it owns the wiring it was set up with and cannot be copied.

QMI8658C: a register-mapped IMU

A 6-axis part — 3-axis accelerometer plus 3-axis gyroscope. SA0 selects address 0x6A or 0x6B.

type Accel_Range is (Range_2G, Range_4G, Range_8G, Range_16G);
type Gyro_Range  is ...;
type Output_Rate is ...;
type Axes        is record ... end record;
type Status      is (OK, Bus_Error);

procedure Read_Accelerometer (Dev : Device; A : out Axes; Result : out Status);
procedure Read_Gyroscope     (Dev : Device; G : out Axes; Result : out Status);
procedure Read_Temperature   (Dev : Device; Raw : out Interfaces.Integer_16; Result : out Status);
procedure Data_Ready (Dev : Device; Accel, Gyro : out Boolean; Result : out Status);

Reads use the chip's auto-incrementing address pointer, set by Configure via CTRL1.ADDR_AI: a one-byte write sets the pointer and the following read streams from it. The driver runs the device little-endian (CTRL1.BE = 0).

Raw counts are not physical units, and the scale depends on the range you configured. That is what these two are for:

function Accel_LSB_Per_G   (Dev : Device) return Positive;
function Gyro_LSB_Per_DPS  (Dev : Device) return Positive;

Ask the device rather than hard-coding a divisor, and changing Accel_Range later cannot silently scale every reading wrong. Both are SPARK-mode functions.

SHT41: a command-based sensor

The SHT4x has no registers at all. A measurement is: write a one-byte command, wait the conversion time, read six bytes. That is a genuinely different protocol shape from the IMU, and it is why the driver exposes Measure rather than register accessors.

type Precision   is (Low, Medium, High);
type Measurement is record ... end record;
type Status      is (OK, Bus_Error, CRC_Error);

procedure Measure (...);
procedure Read_Serial_Number (...);
procedure Reset (Dev : Device; Result : out Status);

Precision trades conversion time against noise. The six returned bytes are two CRC-8-protected words, and the driver checks them — hence the third Status value:

CRC_Error is not Bus_Error. The device ACKed and returned data, but the checksum failed — so the wiring is probably fine and you are looking at interference, an over-long cable, or too fast a bus. Treat it as "retry", not "device missing".

There is no interrupt line: the sensor is read on request.