rigless 0.3.0

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-42688-P, 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-42688-P Datasheet, `DS-000347` rev 1.6, 06/20/2021. 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-42605 is the same model against its
//! own datasheet, [`Icm42605`](crate::chip::icm42605::Icm42605).
//!
//! | 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 Reads and Single Writes", so each byte after an address byte is the next register and a write takes one byte. Bytes after it are dropped, since the page doesn't say what they do | 9.6 | `a_burst_read_walks_the_registers_and_a_write_takes_one_byte` |
//! | 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 0x47" | 14.58 | `who_am_i_is_0x47` |
//! | 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 of last valid sample". The sense registers "hold the last valid sample" | 14.34, 12.8 | `a_sensor_that_is_off_is_the_invalid_code_or_its_last_sample` |
//! | The output data rate | `GYRO_ODR` and `ACCEL_ODR` choose 32, 16, 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. Packets and the sense registers update at "max(Accelerometer ODR, Gyroscope ODR)" of the sensors running. A reserved code takes no samples | 5.6, 14.37, 14.38, 4.13 | `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 12.8's table for the sense registers "Power On Reset till First Sample" gives | 14.5 to 14.18, 12.8 | `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 and -32767 are 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` |
//! | The 20-bit packet | `FIFO_HIRES_EN` makes a 20 byte packet whatever the sensor enables say: header `0111_10xx`, then the accelerometer's and gyroscope's bits 19 to 4 for X, Y and Z, a two byte temperature, the timestamp, and three bytes of the low nibbles, each axis' accelerometer in the high half. The words are fixed at 2000 dps and 16 g, 131 and 8192 LSB with the low one and two bits zero, and the range of 14.34 holds them to -524256 or -524288 | 6.1, 6.2, 6.4, 14.34, 14.45 | `fifo_hires_makes_the_20_byte_packet` |
//! | 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 counter "toggles between 937 and 938" at 1 kHz, which is its microseconds scaled by 30/32 | 6.1, 14.42, 12.7 | `the_timestamp_counts_as_12_7_gives` |
//! | `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 16. Set them with [`Icm42688p::set_rate`], [`Icm42688p::set_acceleration`] and [`Icm42688p::set_temperature`] | 10.1, figure 16 | `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_icm42688p_and_reads_samples_at_1_khz`, `ardupilot_starts_the_icm42688p_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 |
//! | Clock inputs: `INTF_CONFIG1`'s `CLKSEL` and `RTC_MODE`, `INTF_CONFIG5`'s `PIN9_FUNCTION` | Stored. Neither the internal clock's accuracy nor an external one moves the output data rate |
//! | The timestamp's zero, and 12.7's RTC case | 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. The scale for a clock on `CLKIN` is a second case that no pin here carries |

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

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

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

impl Icm42688p {
    /// 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::Icm42688p))
    }

    /// 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!(Icm42688p);

impl Peer for Icm42688p {
    /// 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()))
    }
}