rigless 0.2.1

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, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The ICM-42605, an inertial unit: a gyroscope and an accelerometer on a serial interface, a 2 kB FIFO
//! of packets a driver drains in bursts, and no master inside the package.
//!
//! Written against the ICM-42605 Datasheet, `DS-000292` rev 1.7, 09/12/2022. Every section, table and
//! figure below is that document's.
//!
//! **The FIFO is derived from virtual time.** The part makes samples at the output data rate and a
//! driver reads a thousand times a second or more. Nothing is scheduled per sample. At each select, and
//! before each write, the samples that fell due since the rate or the mode last changed are counted
//! from the rate the registers give, and made into the sense registers and the FIFO from the signals as
//! held. What a driver can observe is the same. What it can't is a signal changing between two reads,
//! and the packets either side of the change differing. The ICM-42688-P is the same model against its
//! own datasheet, [`Icm42688p`](crate::chip::icm42688p::Icm42688p), and the two differ where this table
//! says so: the identity, the registers of banks 1 and 2, the rates, burst writes, what the sense
//! registers hold for an invalid sample and the timestamp.
//!
//! | Register or section | Behavior | Source | Test |
//! |---|---|---|---|
//! | The serial protocol | "The first bit of the first byte contains the Read/Write bit and indicates the Read (1) operation. The following 7 bits contain the Register Address". The part "Supports Single or Burst Read/Writes", so each byte after an address byte is the next register, for a write as for a read | 9.6 | `a_burst_read_walks_the_registers_and_a_burst_write_increments` |
//! | The banks | The registers sit in user banks 0, 1, 2 and 4, and `REG_BANK_SEL` picks one and is "accessible from all register banks". A bank answers its own registers and no others. Bank 3 and the reserved codes hold none | 13, 14.59 | `the_bank_select_reaches_each_banks_own_registers` |
//! | Reset values | Every register at the value 14 to 17 give. A register whose value reads "0xXX", trimmed at the factory or written at manufacturing test, reads zero and keeps a write | 14 to 17 | `every_register_resets_to_the_datasheets_value`, `the_factory_trimmed_registers_read_zero_and_keep_what_is_written` |
//! | `WHO_AM_I` | "The default value of the register is 0x42" | 14.58 | `who_am_i_is_0x42` |
//! | Access | A register marked R takes no write. `INT_STATUS`, `INT_STATUS2` and `INT_STATUS3` are R/C and clear on read. `SIGNAL_PATH_RESET` is W/C and reads zero. An address no map lists reads zero and takes no write | 13 | `a_read_only_register_takes_no_write_and_an_unmapped_address_reads_zero` |
//! | `DEVICE_CONFIG` | `SOFT_RESET_CONFIG` "Enable reset" restores every register, empties the FIFO and sets `RESET_DONE_INT`, "when software reset is complete" | 14.1, 14.21 | `a_soft_reset_restores_every_register_and_raises_reset_done` |
//! | Reserved bits | A register keeps every bit written to it, reserved ones included. The driver clears bit 7 of `INTF_CONFIG1` and sets bit 6, its fix for the gyroscope sticking, and reads the register back as a checked register | 13, 14.35 | `intf_config1_keeps_what_the_driver_writes` |
//! | `PWR_MGMT0` | The part "powers up in sleep mode". `GYRO_MODE` 11 is low noise, the one mode that outputs gyroscope data. `ACCEL_MODE` 10 and 11 output accelerometer data. Nothing is sampled while both are off. `IDLE` and `TEMP_DIS` are stored | 14, 14.36 | `nothing_samples_until_a_sensor_is_on` |
//! | A sensor that isn't running | "When any sensor is disabled" its data is invalid. A packet carries -32768 for it, or with `FIFO_HOLD_LAST_DATA_EN` a "Copy last valid sample". The sense registers "hold last valid sample until new one arrives" with the bit set, and with it clear take the invalid value, "-32768" | 14.34 | `a_sensor_that_is_off_is_the_invalid_code_or_its_last_sample` |
//! | The output data rate | `GYRO_ODR` and `ACCEL_ODR` choose 8, 4, 2, 1 kHz, 500, 200, 100, 50, 25 and 12.5 Hz, and the accelerometer 6.25, 3.125 and 1.5625 Hz as well. The codes for 32 and 16 kHz are "Reserved" on this part. A packet comes at the rate of the faster sensor running, since "the sensor with lower ODR will generate invalid samples when it has no new data". A reserved code takes no samples | 5.6, 14.37, 14.38, 14.34 | `the_odr_codes_set_the_rate_the_packets_come_at`, `a_reserved_odr_code_takes_no_samples` |
//! | The accelerometer's mode against its rate | 14.38 marks codes `0001` to `0110` "LN mode" and `1100` to `1110` "LP mode", and 12.1 asks software to change a rate its mode doesn't support. The page doesn't say what a mismatch does, so the model reads the code as reserved and takes no samples | 14.38, 12.1 | `an_accel_odr_the_mode_does_not_offer_takes_no_samples` |
//! | Restarting the counter | `ABORT_AND_RESET` "restarting the ODR counter", so the next sample is one period after the write | 14.33 | `abort_and_reset_restarts_the_odr_counter` |
//! | The gyroscope's words | `GYRO_FS_SEL` 0 to 7 is 2000, 1000, 500, 250, 125, 62.5, 31.25 and 15.625 dps, at 16.4, 32.8, 65.5, 131, 262, 524.3, 1048.6 and 2097.2 LSB/(°/s). Those are the table's roundings of 2^15 counts over the full scale, which the model uses. Sixteen bits, two's complement | 3.1 table 1, 14.37 | `the_gyro_words_are_the_rate_at_each_full_scale` |
//! | The accelerometer's words | `ACCEL_FS_SEL` 0 to 3 is 16, 8, 4 and 2 g at 2048, 4096, 8192 and 16384 LSB/g. Codes 4 to 7 are "Reserved" and read as code 0 | 3.2 table 2, 14.38 | `the_accel_words_are_the_force_at_each_full_scale` |
//! | The temperature | Degrees Centigrade are `(TEMP_DATA / 132.48) + 25` in the sense registers and `(FIFO_TEMP_DATA / 2.07) + 25` in a packet, where it is eight bits and holds to -128 to 127 | 4.13, 14.5, 14.6, table 4 | `the_temperature_words_are_the_datasheets` |
//! | The sense registers | `TEMP_DATA`, `ACCEL_DATA` and `GYRO_DATA` hold the latest sample. From reset until the first they read -32768, their reset value, or 0 where `FIFO_HOLD_LAST_DATA_EN` is set, as 14.34's table for the sense registers "Power On Reset till First Sample" gives | 14.5 to 14.18, 14.34 | `the_sense_registers_start_invalid_and_hold_the_last_sample` |
//! | Endianness | `SENSOR_DATA_ENDIAN` clear is "Little Endian format", set is "Big Endian format (default)", for the sense registers and the packets, the timestamp included | 14.34, 6.1 | `sensor_data_endian_puts_the_low_byte_first_in_registers_and_packets` |
//! | The range of a valid sample | -32766 to 32767, since -32768 is what an invalid one reads, or with `FIFO_HOLD_LAST_DATA_EN` "-32768 to +32767" | 14.34 | `the_hold_bit_sets_the_range_a_valid_sample_can_take` |
//! | `FIFO_MODE` | `00` bypass writes no packets, `01` stream-to-FIFO overwrites the oldest, `10` and `11` stop on full and drop the new. `FIFO_FULL_INT` is set when no packet more fits. A read that stopped part way into a packet leaves its remainder, which a stream drops before it overwrites, so the packets after it stay framed | 14.4, 14.21 | `bypass_writes_nothing_stream_overwrites_and_stop_on_full_stops`, `a_partial_read_does_not_misframe_a_stream_that_overflows` |
//! | The packets | `FIFO_ACCEL_EN` and `FIFO_GYRO_EN` together make a 16 byte packet: header `0110_10xx`, accelerometer X, Y, Z, gyroscope X, Y, Z, the temperature byte and the two byte timestamp. The header's bits 3:2 are `10`, "Packet contains ODR Timestamp", whatever `FIFO_TMST_FSYNC_EN` says. One sensor makes an eight byte packet, header `0100_00xx` for the accelerometer and `0010_00xx` for the gyroscope, with the temperature byte and no timestamp. None writes nothing | 6.1, 6.2, 6.4 | `the_packet_follows_the_enables_and_carries_its_header` |
//! | `FIFO_HIRES_EN` | Reserved on this part, which has no 20-bit packet, so the bit is stored and packets stay 8 or 16 bytes | 6.1, 14.45 | `fifo_hires_en_makes_no_20_byte_packet_on_this_part` |
//! | The FIFO's size | "the physical FIFO size is 2048 bytes. A number of bytes equal to the packet size selected (see section 6.1) is reserved to prevent reading a packet during write operation", so 127 packets of 16 bytes | 6.3 | `the_fifo_holds_its_size_less_one_packet` |
//! | `FIFO_COUNTH`, `FIFO_COUNTL` | The count is of bytes, or of records under `FIFO_COUNT_REC`, "1 record = 16 bytes for header + gyro + accel + temp sensor data + time stamp". "Reading this byte latches the data for both". `FIFO_COUNT_ENDIAN` clear is "Little Endian", which puts the low byte at the lower address, the one a driver reading two bytes into a little-endian integer needs | 14.22, 14.23, 14.34 | `the_count_is_in_bytes_or_records_and_latches_with_the_high_byte`, `count_endian_puts_the_low_byte_at_the_lower_address` |
//! | `FIFO_DATA` | The "FIFO data port", reset value `0xFF`. A read takes the oldest byte, and an empty FIFO answers `0xFF`, which is also a header with `HEADER_MSG` set, "FIFO is empty". The page doesn't say whether a burst read advances the address, and the driver reads a whole burst of packets from it, so a burst stays on the port | 14.24, 6.2 | `an_empty_fifo_answers_0xff`, `a_burst_read_of_the_fifo_stays_on_it` |
//! | `FIFO_FLUSH` | "When set to 1, FIFO will get flushed". W/C, so it keeps nothing | 14.33 | `a_fifo_flush_empties_it_and_keeps_nothing` |
//! | The timestamp | A two byte field of the 16 byte packet, in microseconds, or in sixteen of them when `TMST_RES` is set. The page gives the counter no scale, so it counts true time | 6.1, 14.42 | `the_timestamp_counts_microseconds` |
//! | `TMST_DELTA_EN` | "the time stamp field contains the measurement of time since the last occurrence of ODR", so the field is the period between samples in the counter's own ticks | 14.42 | `the_timestamp_field_can_be_the_time_since_the_last_odr` |
//! | `INT_STATUS` | `RESET_DONE_INT` is set from reset, `DATA_RDY_INT` at each sample and `FIFO_FULL_INT` when the FIFO is full. Each "clears to 0 after the register has been read" | 14.21 | `int_status_reports_reset_done_data_ready_and_full_and_clears_on_read` |
//! | The filter registers | `GYRO_CONFIG_STATIC2` to `5` in bank 1 and `ACCEL_CONFIG_STATIC2` to `4` in bank 2 keep what the driver writes for the anti-alias filters, and the driver reads the first back | 15.2 to 15.5, 16.1 to 16.3 | `the_anti_alias_registers_keep_what_the_driver_writes` |
//! | 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, figure 15. Set them with [`Icm42605::set_rate`], [`Icm42605::set_acceleration`] and [`Icm42605::set_temperature`] | 10.1, figure 15 | `the_gyro_words_are_the_rate_at_each_full_scale` |
//! | A driver's start and reads | ArduPilot's `AP_InertialSensor_Invensensev3` as it runs the part: identity, FIFO setup, filters, power, then a count and a burst of packets every period | 6, 9.6, 14 | `ardupilot_starts_the_icm42605_and_reads_samples_at_1_khz`, `ardupilot_starts_the_icm42605_at_8_khz_and_keeps_up` |
//! | Determinism | The same accesses at the same instants give the same bytes, and a chip restored from its saved state goes on as the original does | Not a datasheet behavior | `the_same_accesses_give_the_same_bytes_and_a_restored_chip_goes_on_alike` |
//!
//! # What is not, and why
//!
//! | Departure | Reason |
//! |---|---|
//! | The timestamp's FSYNC case | 6.4's table gives `0110_11xx` for `FIFO_TMST_FSYNC_EN` and 6.2 says `11` flags the first ODR after an FSYNC. No pin carries `FSYNC` here, so the header says `10` and 6.2 is the one followed |
//! | `TMST_EN` and `TMST_TO_REGS_EN` | Stored. `TMST_EN` gates the time stamp register and `TMST_TO_REGS_EN` what `TMSTVAL0` to `2` read, and those registers aren't modeled, so the packet's field is always written |
//! | A signal changing between two reads | Every sample not yet made is made at the next select from the value held then, so the packets either side of a change don't differ. A driver averaging the packets of one read can't tell |
//! | Two sensors at different rates | 14.34 has the slower one's packets invalid, "-32768" or a copy of the last, where it has no new data, and the header says so. The model gives both sensors a fresh word in every packet. The driver sets one rate for both |
//! | `HEADER_ODR_ACCEL` and `HEADER_ODR_GYRO` | Read zero. They flag a packet after a rate change, and the driver masks them |
//! | `FIFO_TEMP_EN` and `TEMP_DIS` | Stored. 6.1's packets always carry the temperature byte and 14.34 gives no code for an invalid eight bit temperature, so the temperature is always valid here |
//! | The per-axis disables of `SENSOR_CONFIG0` | Stored. 14.34 makes a disabled sensor invalid and doesn't say whether an axis is one |
//! | `FIFO_RESUME_PARTIAL_RD` | Stored. 14.45 says a read that stops early "requires re-reading of the entire FIFO" when it is clear and gives no pointer rule. The model resumes where a read stopped, which is what the set bit says. The driver writes it clear and reads whole bursts of what it counted |
//! | The watermark, `FIFO_THS_INT` and `FIFO_LOST_PKT0` and `1` | `FIFO_CONFIG2`, `FIFO_CONFIG3` and `FIFO_WM_GT_TH` are stored and no threshold is checked. The lost count reads zero, since 14.55 doesn't say when it clears. The driver uses neither |
//! | The read cache of 6.3 | "either 1 or 2 of the packet entries in read cache may become available" depending on the serial activity, so the page's own capacity isn't a number. The model never counts them |
//! | The `INT1` and `INT2` pins, `INT_CONFIG`, `INT_CONFIG0` to `1`, `INT_SOURCE0` to `4` and the clear options | Stored. A chip's seam carries no line back to a pin, and a status bit clears only when `INT_STATUS` is read. The driver uses the FIFO and never the interrupts |
//! | Start-up: register access 1 ms from power-up, the gyroscope's 30 ms and the accelerometer's 10 ms | Table 1, 2 and 4. Samples start one period after the mode is written. 14.36 also asks for 200 µs without a write after a mode change and 45 ms of the gyroscope on, which the model doesn't enforce. The driver waits 300 µs |
//! | The filters: anti-alias, notch, `GYRO_UI_FILT_ORD`, `GYRO_UI_FILT_BW`, `TEMP_FILT_BW` and the averaging | Stored. A filter's bandwidth is nothing to a signal a plant holds |
//! | `APEX_CONFIG0` to `9`, `SMD_CONFIG`, `ACCEL_WOM_X_THR` to `Z`, `APEX_DATA0` to `5` | Stored, and the data registers read their resets. No pedometer, tilt, tap, wake or motion detection runs |
//! | `FSYNC_CONFIG`, `TMST_FSYNCH`, `TMST_FSYNCL`, `TMST_STROBE`, `TMSTVAL0` to `2` | Stored, and the registers read their resets, since no pin carries `FSYNC` and the strobe's counter has no reader |
//! | `OFFSET_USER0` to `8` | Stored, and not subtracted. 5.4 gives the offsets' range and not a rule for applying them at each range. The driver writes none |
//! | Self-test | `SELF_TEST_CONFIG` and the `ST_DATA` registers are stored. The driver runs no self-test |
//! | `I3C`, `I2C` and `UI_SIFS_CFG` | The seam is a serial peripheral interface, and `UI_SIFS_CFG` is stored, so a write that disables the interface it is reached through doesn't silence it. `DEVICE_CONFIG`'s `SPI_MODE` and `INTF_CONFIG4`'s `SPI_AP_4WIRE` are stored for the same reason, since the seam carries words and not a clock or a wire count |
//! | The 24 MHz ceiling on `SCLK` | The seam carries words, not a clock |
//! | The timestamp's zero | The page doesn't give the count at power-up or say a soft reset restarts it, so the counter is zero at power-up and runs on |

use crate::chip::Timeline;
use crate::chip::icm426xx::{Engine, Part};
use crate::chip::spi::Peer;
use crate::snapshot::store;

/// One ICM-42605.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Icm42605(Engine);

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

impl Icm42605 {
    /// A unit at power-on: the registers at the datasheet's reset values, asleep, and the FIFO empty. The
    /// signals are a still, level unit's at 25 °C, with no rate and one gravity on Z.
    #[must_use]
    pub fn new() -> Self {
        Self(Engine::new(Part::Icm42605))
    }

    /// 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.0.set_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.0.set_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.0.set_temperature(centi_celsius);
    }
}

store!(Icm42605);

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

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

    /// 9.6: the address byte with its direction bit, then a byte per register, incrementing.
    fn transfer(&mut self, mosi: u32, _bits: u8, context: &mut Timeline<'_, '_>) -> u32 {
        u32::from(self.0.transfer((mosi & 0xFF) as u8, context.now()))
    }
}