rigless 0.0.5

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model, and unit of virtual time in Rust; a fork of QEMU is built in and executes the instructions
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The ICM-20608-G, an inertial unit: a gyroscope and an accelerometer on a serial interface with a FIFO
//! a driver drains in bursts, and no master inside the package.
//!
//! Written against the ICM-20608-G Register Map and Descriptions, `RM-000030` rev 1.0, 06/15/2015,
//! cited as **RM**, and the ICM-20608-G Datasheet, `DS-000081` rev 1.0, 06/15/2015, cited as **DS**.
//! Every section below is one or the other's. RM 3 numbers its sections twice in places; a register is
//! cited by its section as printed and its register number.
//!
//! The FIFO is derived from virtual time, the same way as [`Mpu9250`](crate::chip::mpu9250::Mpu9250)'s
//! and on the same mechanics: what a driver can observe is the same; what it cannot is a signal changing
//! between two reads and the samples either side of the change differing.
//!
//! | Register or section | Behaviour | Source | Test |
//! |---|---|---|---|
//! | The serial protocol | "The first bit of the first byte contains the Read/Write bit and indicates the Read (1) or Write (0) operation. The following 7 bits contain the Register Address"; a burst increments | DS 6.5 | `every_register_reads_and_writes_at_its_own_address_and_bursts_increment` |
//! | Reset values | `0x00` "for all registers other than" `PWR_MGMT_1` (`0x40`) and `WHO_AM_I` (`0xAF`) | RM 2 | `who_am_i_is_0xaf`, `it_comes_up_asleep_and_a_reset_restores_that` |
//! | `WHO_AM_I` | "The default value of the register is 0xAF" | RM 3.28 | `who_am_i_is_0xaf` |
//! | `PWR_MGMT_1` | `DEVICE_RESET` "restores the default settings. The bit automatically clears"; `SLEEP` "the chip comes up in Sleep mode"; `CLKSEL` 7 "Stops the clock" | RM 3, 3.24 | `it_comes_up_asleep_and_a_reset_restores_that`, `a_stopped_clock_takes_no_samples` |
//! | Writes while asleep | The map's *Accessible (writable) in Sleep Mode* column: `Y` for `INT_PIN_CFG`, `INT_ENABLE`, `PWR_MGMT_1` and `PWR_MGMT_2`, `N` for every other register; a write to an `N` register while `SLEEP` is set does not land | RM 2 | `a_write_while_asleep_lands_only_where_the_map_allows` |
//! | `PWR_MGMT_2` | `STBY_xA`, `STBY_xG`: "disabled", read as zero | RM 3.25 | `a_disabled_axis_reads_zero` |
//! | The sample rate | `FCHOICE_B[0]` set or `FCHOICE_B` = `10`: 32 kHz; `FCHOICE_B` = `00` with `DLPF_CFG` 0 or 7: 8 kHz; else `INTERNAL_SAMPLE_RATE / (1 + SMPLRT_DIV)` at 1 kHz, the divider "only effective when `FCHOICE_B` register bits are 2'b00, and (0 < `DLPF_CFG` < 7)" | RM 3.6 (25), 3.7 (26) | `the_sample_rate_follows_fchoice_the_filter_and_the_divider` |
//! | The words | Gyro at 131 LSB/(°/s) at `FS_SEL` 0 — DS table 1's 131, 65.5, 32.8, 16.4 — plus `X_OFFS_USR`, "added to the gyroscope sensor value before going into the sensor register"; accelerometer at 16 384, 8 192, 4 096 or 2 048 LSB/g by `ACCEL_FS_SEL`, less the fifteen-bit offset in "0.98-mg steps"; temperature `TEMP_degC = ((TEMP_OUT − RoomTemp_Offset)/Temp_Sensitivity) + 25degC` at 0 and 326.8 LSB/°C; big-endian, sixteen bits | RM 3.3 (19), 3.19, 3.20, 3.29; DS 3.1, 3.2, 3.3.3 | `the_words_are_the_signals_at_the_selected_range`, `the_temperature_word_is_the_datasheets`, `the_gyro_offset_is_added_as_the_page_says`, `the_accelerometer_offsets_survive_a_round_trip_and_are_subtracted` |
//! | `FIFO_EN` | Temperature, each gyro axis and the accelerometer written "at the sample rate", "in order of register number" | RM 3.13, 3.27 | `the_fifo_holds_what_fifo_en_says_in_register_order` |
//! | `FIFO_COUNT`, `FIFO_R_W` | "a 512-byte FIFO"; a thirteen-bit count, "Reading this byte latches the data for both"; a read takes the oldest byte, and empty "will return a unique value of 0xFF until new data is available"; overflowed, `FIFO_OFLOW_INT` is set and "the oldest data will be lost … unless … `FIFO_MODE` = 1", when "additional writes will not be written" | DS 4.12; RM 3.26, 3.27, 3.7 | `the_fifo_count_is_the_samples_since_the_reset`, `an_empty_fifo_answers_0xff`, `the_fifo_stops_at_512_under_fifo_mode_and_drops_the_oldest_otherwise` |
//! | `USER_CTRL` | `FIFO_EN` "Enable FIFO operation mode"; `FIFO_RST` "Reset FIFO module … auto clears"; `SIG_COND_RST` "also clears all the sensor registers"; `I2C_IF_DIS` stored | RM 3.23 | `a_fifo_reset_empties_it_and_clears_itself`, `a_signal_condition_reset_clears_the_sensor_registers` |
//! | `INT_STATUS`, `INT_PIN_CFG` | `DATA_RDY_INT` "when a Data Ready interrupt is generated", `FIFO_OFLOW_INT`; "READ to CLEAR", or under `INT_RD_CLEAR` "cleared if any read operation is performed" | RM 3.15, 3.17 | `data_ready_is_set_by_a_sample_and_cleared_as_int_pin_cfg_says` |
//! | Unmapped addresses | RM 2 lists what exists; an address it does not list reads zero and takes no write, `0x11` included | RM 2 | `an_address_off_the_map_takes_nothing_and_reads_zero` |
//! | What it measures | Angular rate in thousandths of a degree per second, specific force in millionths of a gravity, temperature in hundredths of a degree, on the unit's own axes, DS figure 13; [`Icm20608::set_rate`], [`Icm20608::set_acceleration`], [`Icm20608::set_temperature`] | DS 7, figure 13 | `the_words_are_the_signals_at_the_selected_range` |
//!
//! # What is not, and why
//!
//! | Departure | Reason |
//! |---|---|
//! | A signal changing between two reads | Every sample not yet materialised is generated at the read from the value held then. The accelerometer's own 4 kHz under `ACCEL_FCHOICE_B` against the gyro's 8 kHz is the same rule, and comes out the same |
//! | The self-test registers | RM 2: "pre-programmed values and will not be 0x00 after reset", values the page does not give; they read zero. The driver runs no self-test |
//! | `0xFF` never in FIFO data | RM 3.27: "Normal data is precluded from ever indicating 0xFF"; how the part alters a word whose byte is `0xFF` is not on the page, so a word is written as computed |
//! | The low-pass filters, `ACCEL_FCHOICE_B`, `DEC2_CFG`, `LP_MODE_CFG`, wake-on-motion, `ACCEL_CYCLE`, `GYRO_STANDBY`, `TEMP_DIS`, `FIFO_LP_EN`, `FSYNC`, `EXT_SYNC_SET`, `SIGNAL_PATH_RESET` | Stored. A filter's bandwidth is nothing to a signal a plant holds; `SIGNAL_PATH_RESET` says "Sensor registers are not cleared" |
//! | The `INT` pin | Not driven; on this board it is an input the driver never reads |
//! | Start-up: register access 11 ms from power-up and 5 ms from sleep, the gyroscope 35 ms and the accelerometer 20 ms from sleep | DS 3.1, 3.2, 3.3.3; data is valid from the first sample. The driver waits 100 ms after its reset |
//! | The 8 MHz ceiling on `SCLK` | The seam carries words, not a clock |
//! | Writes to `FIFO_R_W` | "Read/Write command" — a write would push a byte; the driver never does, and it is dropped |

use crate::chip::invensense::{self, Access, Fifo, SampleClock, Serial, Signals};
use crate::chip::spi::Peer;
use crate::device::DeviceContext;
use crate::time::VirtualNanos;

/// RM 2's register map, by address.
const XG_OFFS_USRH: usize = 0x13;
const SMPLRT_DIV: usize = 0x19;
const CONFIG: usize = 0x1A;
const GYRO_CONFIG: usize = 0x1B;
const ACCEL_CONFIG: usize = 0x1C;
const FIFO_EN: usize = 0x23;
const FSYNC_INT: usize = 0x36;
const INT_PIN_CFG: usize = 0x37;
const INT_ENABLE: usize = 0x38;
const INT_STATUS: usize = 0x3A;
const ACCEL_XOUT_H: usize = 0x3B;
const GYRO_ZOUT_L: usize = 0x48;
const USER_CTRL: usize = 0x6A;
const PWR_MGMT_1: usize = 0x6B;
const PWR_MGMT_2: usize = 0x6C;
const FIFO_COUNTH: usize = 0x72;
const FIFO_COUNTL: usize = 0x73;
const WHO_AM_I: usize = 0x75;
const XA_OFFSET_H: usize = 0x77;
const REGISTERS: usize = 0x80;
/// RM 2: the registers writable in sleep mode.
const WRITABLE_ASLEEP: [usize; 4] = [INT_PIN_CFG, INT_ENABLE, PWR_MGMT_1, PWR_MGMT_2];

/// The bits the model acts on, by register, RM 3.
const DEVICE_ID: u8 = 0xAF;
const DEVICE_RESET: u8 = 0x80;
const SLEEP: u8 = 0x40;
const CLKSEL: u8 = 0x07;
const CLOCK_STOPPED: u8 = 0x07;
const FIFO_MODE: u8 = 0x40;
const DLPF_CFG: u8 = 0x07;
const FS_SEL: u8 = 0x18;
const FCHOICE_B: u8 = 0x03;
const INT_RD_CLEAR: u8 = 0x10;
const FIFO_OFLOW_INT: u8 = 0x10;
const DATA_RDY_INT: u8 = 0x01;
const USER_FIFO_EN: u8 = 0x40;
const FIFO_RST: u8 = 0x04;
const SIG_COND_RST: u8 = 0x01;
const STBY_ACCEL: [u8; 3] = [0x20, 0x10, 0x08];
const STBY_GYRO: [u8; 3] = [0x04, 0x02, 0x01];

/// DS 4.12: "a 512-byte FIFO register".
pub const FIFO_BYTES: usize = invensense::FIFO_BYTES;
/// RM 3.7's table: the three rates the gyroscope samples at.
const FASTEST_HZ: u64 = 32_000;
const FAST_HZ: u64 = 8_000;
const SLOW_HZ: u64 = 1_000;
/// DS 3.3.3: 326.8 LSB per degree and no offset at 25 °C, as tenths and a thousandth.
const TEMP_TENTHS_PER_DEGREE: i64 = 3_268;
const TEMP_ZERO_CENTI: i32 = 2_500;

/// One ICM-20608-G.
#[derive(Debug)]
pub struct Icm20608 {
    registers: [u8; REGISTERS],
    signals: Signals,
    clock: SampleClock,
    fifo: Fifo,
    serial: Serial,
}

impl Default for Icm20608 {
    fn default() -> Self {
        Self::new()
    }
}

impl Icm20608 {
    /// A unit at power-on: the registers at RM 2's reset values, asleep, the FIFO empty, and the signals
    /// a still, level unit at 25 °C gives — no rate, one gravity on Z.
    #[must_use]
    pub fn new() -> Self {
        let mut unit = Self {
            registers: [0; REGISTERS],
            signals: Signals::still(TEMP_ZERO_CENTI),
            clock: SampleClock::new(),
            fifo: Fifo::new(),
            serial: Serial::new(),
        };
        unit.reset(VirtualNanos(0));
        unit
    }

    /// The angular rate the unit measures from now on, in thousandths of a degree per second on its axes.
    pub fn set_rate(&mut self, x: i32, y: i32, z: i32) {
        self.signals.rate = [x, y, z];
    }

    /// The specific force the unit measures from now on, in millionths of a gravity on its axes.
    pub fn set_acceleration(&mut self, x: i32, y: i32, z: i32) {
        self.signals.acceleration = [x, y, z];
    }

    /// The die temperature from now on, in hundredths of a degree.
    pub fn set_temperature(&mut self, centi_celsius: i32) {
        self.signals.centi_celsius = centi_celsius;
    }

    /// RM 2's reset values, the FIFO emptied, sampling restarted.
    fn reset(&mut self, now: VirtualNanos) {
        self.registers = [0; REGISTERS];
        self.registers[PWR_MGMT_1] = SLEEP;
        self.registers[WHO_AM_I] = DEVICE_ID;
        self.fifo.clear();
        self.clock.restart(now);
    }

    /// RM 3.7's table and 3.6: the sample rate the registers now give, in hertz, or nothing while the
    /// clock is stopped or the unit asleep.
    fn sample_hz(&self) -> u64 {
        let power = self.registers[PWR_MGMT_1];
        if power & SLEEP != 0 || power & CLKSEL == CLOCK_STOPPED {
            return 0;
        }
        if self.registers[GYRO_CONFIG] & FCHOICE_B != 0 {
            return FASTEST_HZ;
        }
        let dlpf = self.registers[CONFIG] & DLPF_CFG;
        if dlpf == 0 || dlpf == 7 {
            FAST_HZ
        } else {
            SLOW_HZ / (1 + u64::from(self.registers[SMPLRT_DIV]))
        }
    }

    fn rebase(&mut self, now: VirtualNanos) {
        self.clock.rebase(now, self.sample_hz());
    }

    /// Bits 4:3 of a configuration register, the range step.
    fn range_step(register: u8) -> u32 {
        u32::from((register & FS_SEL) >> 3)
    }

    /// The fourteen bytes of one sample, registers 59 to 72 in order, from the signals as held.
    fn sample(&self) -> [u8; invensense::SAMPLE_BYTES] {
        let accel_step = Self::range_step(self.registers[ACCEL_CONFIG]);
        let gyro_step = Self::range_step(self.registers[GYRO_CONFIG]);
        let mut accel = [0; 3];
        let mut gyro = [0; 3];
        for axis in 0..3 {
            if self.registers[PWR_MGMT_2] & STBY_ACCEL[axis] == 0 {
                let offset = invensense::offset15(
                    self.registers[XA_OFFSET_H + 3 * axis],
                    self.registers[XA_OFFSET_H + 3 * axis + 1],
                );
                accel[axis] = self.signals.accel_counts(axis, accel_step, offset);
            }
            if self.registers[PWR_MGMT_2] & STBY_GYRO[axis] == 0 {
                let offset = i64::from(i16::from_be_bytes([
                    self.registers[XG_OFFS_USRH + 2 * axis],
                    self.registers[XG_OFFS_USRH + 2 * axis + 1],
                ]));
                gyro[axis] = self.signals.gyro_counts(axis, gyro_step) + offset;
            }
        }
        let temperature = invensense::round(
            i64::from(self.signals.centi_celsius - TEMP_ZERO_CENTI) * TEMP_TENTHS_PER_DEGREE,
            1_000,
        );
        invensense::pack(accel, temperature, gyro)
    }

    /// Everything that happened between the last select and `now`: the samples that fell due land in the
    /// data registers and the FIFO.
    fn catch_up(&mut self, now: VirtualNanos) {
        let taken = self.clock.catch_up(now, self.sample_hz());
        if taken.is_empty() {
            return;
        }
        let sample = self.sample();
        self.registers[ACCEL_XOUT_H..=GYRO_ZOUT_L].copy_from_slice(&sample);
        self.registers[INT_STATUS] |= DATA_RDY_INT;
        if self.registers[USER_CTRL] & USER_FIFO_EN != 0 {
            let bytes = invensense::fifo_bytes(&sample, self.registers[FIFO_EN]);
            let keep_oldest = self.registers[CONFIG] & FIFO_MODE != 0;
            if self.fifo.push(&bytes, taken.end - taken.start, keep_oldest) {
                self.registers[INT_STATUS] |= FIFO_OFLOW_INT;
            }
        }
    }

    /// RM 2's table: whether an address is a register at all.
    fn mapped(register: usize) -> bool {
        matches!(
            register,
            0x00..=0x02
                | 0x0D..=0x0F
                | 0x13..=0x1F
                | 0x23
                | 0x36..=0x38
                | 0x3A..=0x48
                | 0x68..=0x6C
                | 0x72..=0x75
                | 0x77
                | 0x78
                | 0x7A
                | 0x7B
                | 0x7D
                | 0x7E
        )
    }

    /// RM 2's "Serial I/F" column, and its sleep column while `SLEEP` is set: the registers a write
    /// lands in now.
    fn writable(&self, register: usize) -> bool {
        Self::mapped(register)
            && !matches!(
                register,
                FSYNC_INT | INT_STATUS | ACCEL_XOUT_H
                    ..=GYRO_ZOUT_L | FIFO_COUNTH | FIFO_COUNTL | WHO_AM_I
            )
            && (self.registers[PWR_MGMT_1] & SLEEP == 0 || WRITABLE_ASLEEP.contains(&register))
    }

    fn read_register(&mut self, register: usize) -> u8 {
        let value = match register {
            FIFO_COUNTH => self.fifo.latch(),
            FIFO_COUNTL => self.fifo.latched_low(),
            invensense::FIFO_R_W => self.fifo.pop().unwrap_or(0xFF),
            INT_STATUS => {
                let value = self.registers[INT_STATUS];
                self.registers[INT_STATUS] = 0;
                value
            }
            // Every unmapped address reads zero because no write lands in one; RM 2.
            _ => self.registers[register],
        };
        if self.registers[INT_PIN_CFG] & INT_RD_CLEAR != 0 {
            self.registers[INT_STATUS] = 0;
        }
        value
    }

    fn write_register(&mut self, register: usize, byte: u8, now: VirtualNanos) {
        if !self.writable(register) {
            return;
        }
        match register {
            PWR_MGMT_1 => {
                if byte & DEVICE_RESET != 0 {
                    self.reset(now);
                    return;
                }
                self.rebase(now);
                self.registers[PWR_MGMT_1] = byte;
            }
            USER_CTRL => {
                self.registers[USER_CTRL] = byte & !FIFO_RST;
                if byte & FIFO_RST != 0 {
                    self.fifo.clear();
                }
                if byte & SIG_COND_RST != 0 {
                    for data in ACCEL_XOUT_H..=GYRO_ZOUT_L {
                        self.registers[data] = 0;
                    }
                }
            }
            SMPLRT_DIV | CONFIG | GYRO_CONFIG => {
                self.rebase(now);
                self.registers[register] = byte;
            }
            _ => self.registers[register] = byte,
        }
    }
}

impl Peer for Icm20608 {
    /// The select falling: everything due by now happens, then the first byte is an address.
    fn select(&mut self, context: &DeviceContext<'_>) {
        self.serial.select();
        self.catch_up(context.now());
    }

    fn deselect(&mut self, _context: &DeviceContext<'_>) {
        self.serial.deselect();
    }

    /// DS 6.5: the address byte with its direction bit, then a byte per register, incrementing; DS 4.12:
    /// "The FIFO register supports burst reads" — a burst stays on it.
    fn transfer(&mut self, mosi: u16, context: &DeviceContext<'_>) -> u16 {
        match self.serial.step((mosi & 0xFF) as u8) {
            Some(Access::Read(register)) => u16::from(self.read_register(register)),
            Some(Access::Write(register, byte)) => {
                self.write_register(register, byte, context.now());
                0
            }
            None => 0,
        }
    }
}