rigless 0.2.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 AK8963, the three-axis magnetometer die inside the MPU-9250, reached over that package's own I2C bus.
//!
//! Written against chapter 5 of the MPU-9250 Register Map and Descriptions, `RM-MPU-9250A-00`
//! rev 1.4, 9/9/2013, which carries the die's register map. Its scale comes from section 3.3 of the
//! MPU-9250 Product Specification, `PS-MPU-9250A-01` rev 1.1. Every section below is RM's unless it
//! says PS.
//!
//! | Register or section | Behavior | Source | Test |
//! |---|---|---|---|
//! | Address, increment | `0x0C` on the bus, and "addresses from 00H to 0CH and from 10H to 12H are compliant with automatic increment function of serial interface respectively" | RM 5, table 2 | `every_register_reads_at_its_address_and_a_burst_increments` |
//! | `WIA` | "Device ID of AKM … 48H: fixed" | RM 5.3 | `wia_is_0x48` |
//! | `ST1` | `DRDY` "turns to 1 when data is ready" and "returns to 0 when any one of ST2 register or measurement data register (HXL to HZH) is read" | RM 5.5 | `drdy_clears_when_data_or_st2_is_read` |
//! | `HXL` to `HZH` | "Two's complement and Little Endian". The range is −32760 to 32760 at 0.15 µT per count in 16-bit output, table 4, and 14-bit output is 0.6 µT per count | RM 5.6, PS 3.3 | `the_data_is_little_endian_at_the_selected_width` |
//! | `ST2` | `HOFL` is set "when magnetic sensor overflow occurred" and cleared when the next measurement starts. `BITM` mirrors `BIT`. `ST2` has "a role as data reading end register": a read of any measurement register begins a reading that holds the data until `ST2` is read | RM 5.7 | `a_reading_holds_until_st2_ends_it`, `hofl_stands_for_a_field_past_the_range` |
//! | `CNTL1` | `MODE`: power-down `0000`, single `0001`, continuous 1 `0010` and 2 `0110`, external trigger `0100`, self-test `1000`, Fuse ROM access `1111`. `BIT` selects 14 or 16 bits. "When CNTL register is accessed to be written, registers from 02H to 09H are initialized" | RM 5.8 | `writing_cntl1_initializes_the_data_registers`, `the_data_is_little_endian_at_the_selected_width` |
//! | `CNTL2` | `SRST`: "all registers are initialized. After reset, SRST bit turns to 0 automatically" | RM 5.9 | `srst_initializes_everything_and_clears_itself` |
//! | `ASAX`, `ASAY`, `ASAZ` | Fuse ROM, "can be read only in Fuse access mode. In other modes, read data is not correct". The adjustment is `Hadj = H × ((ASA − 128) × 0.5 / 128 + 1)`. A driver applies it, so the die undoes it: the word it hands out is the field over that factor | RM 5.13 | `the_adjustment_values_read_only_in_fuse_mode`, `the_field_round_trips_through_the_adjustment_equation` |
//! | What it measures | A field in nanotesla on its own three axes, [`Ak8963::set_field`], PS figure 5 for their orientation | PS 9.1 | `the_field_round_trips_through_the_adjustment_equation` |
//!
//! # What is not, and why
//!
//! | Departure | Reason |
//! |---|---|
//! | The rate of the two continuous modes, and single mode's return to power-down | Neither is on RM's page. The die has data ready at every transaction that reads it, and single mode measures like continuous. A driver that polls `DRDY` sees it set |
//! | `DOR` | Data is never skipped where every read finds fresh data |
//! | `I2CDIS`, `ASTC` self-test, `TS1`, `TS2`, `RSV`, `INFO` | Stored and answered. The self-test field, the bus disable and the test registers do nothing |
//! | External trigger mode | No trigger pin is wired |

use crate::chip::Timeline;
use crate::chip::i2c::Peer;
use crate::snapshot::store;

/// The die's address on the package's bus, RM 4.11 and PS 4.11: "the slave address for the AK8963 is 0X0C".
pub const ADDRESS: u8 = 0x0C;

/// The registers of RM 5.1, table 3.
const WIA: usize = 0x00;
const INFO: usize = 0x01;
const ST1: usize = 0x02;
const HXL: usize = 0x03;
const HZH: usize = 0x08;
const ST2: usize = 0x09;
const CNTL1: usize = 0x0A;
const CNTL2: usize = 0x0B;
const ASAX: usize = 0x10;
const ASAZ: usize = 0x12;
const REGISTERS: usize = 0x13;

const DEVICE_ID: u8 = 0x48;
/// `ST1` bit 0.
const DRDY: u8 = 0x01;
/// `ST2` bits 4 and 3.
const BITM: u8 = 0x10;
const HOFL: u8 = 0x08;
/// `CNTL1` bits 3:0 and 4.
const MODE: u8 = 0x0F;
const POWER_DOWN: u8 = 0b0000;
const FUSE_ROM: u8 = 0b1111;
const BIT: u8 = 0x10;
/// `CNTL2` bit 0.
const SRST: u8 = 0x01;

/// Table 4: the words at the ends of the range, and the field at each count in 16-bit output.
const FULL_SCALE: i32 = 32760;
const NANOTESLA_PER_COUNT_16: i32 = 150;
const NANOTESLA_PER_COUNT_14: i32 = 600;

/// The adjustment values this die carries, chosen away from 128 so the adjustment isn't the
/// identity.
pub const ADJUSTMENT: [u8; 3] = [0xB0, 0xAC, 0xB4];
/// A fixed byte of "device information for AKM", RM 5.4.
const INFORMATION: u8 = 0x09;

/// Where a transaction is.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
enum Phase {
    Idle,
    /// Addressed for a write. The first byte sets the register pointer.
    Register,
    /// Bytes go to or come from the pointer, which increments.
    Data,
}

/// One AK8963.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Ak8963 {
    registers: [u8; REGISTERS],
    /// A reading is under way: a measurement register was read and `ST2` hasn't been.
    reading: bool,
    phase: Phase,
    /// The register the next byte goes to or comes from, kept across a stop.
    pointer: usize,
    /// The field it measures, in nanotesla on its own axes.
    nanotesla: [i32; 3],
}

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

impl Ak8963 {
    /// A die at power-on, RM 5.1's note: "When VDD is turned ON, POR function works and all
    /// registers of AK893 are initialized." It's powered down, and it measures a field of about
    /// fifty microtesla with a downward component, like the Earth's.
    #[must_use]
    pub fn new() -> Self {
        let mut die = Self {
            registers: [0; REGISTERS],
            reading: false,
            phase: Phase::Idle,
            pointer: 0,
            nanotesla: [22_000, 0, -42_000],
        };
        die.initialize();
        die
    }

    /// The field the die measures from now on, in nanotesla on its own axes.
    pub fn set_field(&mut self, x: i32, y: i32, z: i32) {
        self.nanotesla = [x, y, z];
    }

    /// Every register to its initial value, RM 5.1 and 5.9.
    fn initialize(&mut self) {
        self.registers = [0; REGISTERS];
        self.registers[WIA] = DEVICE_ID;
        self.registers[INFO] = INFORMATION;
        self.registers[ASAX..=ASAZ].copy_from_slice(&ADJUSTMENT);
        self.reading = false;
    }

    fn mode(&self) -> u8 {
        self.registers[CNTL1] & MODE
    }

    /// One measurement into `HXL` to `ST2`: the field over the adjustment a driver will apply, at the width
    /// `BIT` selects, held to table 4's range with `HOFL` standing where the field is past it.
    fn measure(&mut self) {
        let sixteen = self.registers[CNTL1] & BIT != 0;
        let per_count = if sixteen {
            NANOTESLA_PER_COUNT_16
        } else {
            NANOTESLA_PER_COUNT_14
        };
        let mut overflow = false;
        for (axis, adjustment) in ADJUSTMENT.iter().enumerate() {
            let adjusted = i64::from(self.nanotesla[axis]) * 256
                / (i64::from(*adjustment) + 128)
                / i64::from(per_count);
            let word = adjusted.clamp(-i64::from(FULL_SCALE), i64::from(FULL_SCALE));
            overflow |= word != adjusted;
            let bytes = i16::try_from(word)
                .expect("held to the range")
                .to_le_bytes();
            self.registers[HXL + 2 * axis] = bytes[0];
            self.registers[HXL + 2 * axis + 1] = bytes[1];
        }
        self.registers[ST2] = if sixteen { BITM } else { 0 } | if overflow { HOFL } else { 0 };
        self.registers[ST1] = DRDY;
    }

    fn read_register(&mut self, register: usize) -> u8 {
        let value = self.registers.get(register).copied().unwrap_or(0);
        match register {
            HXL..=HZH => {
                self.reading = true;
                self.registers[ST1] &= !DRDY;
            }
            ST2 => {
                self.reading = false;
                self.registers[ST1] &= !DRDY;
            }
            ASAX..=ASAZ if self.mode() != FUSE_ROM => return 0,
            _ => {}
        }
        value
    }

    fn write_register(&mut self, register: usize, byte: u8) {
        match register {
            CNTL1 => {
                self.registers[CNTL1] = byte & (BIT | MODE);
                for status in ST1..=ST2 {
                    self.registers[status] = 0;
                }
                self.reading = false;
            }
            CNTL2 => {
                if byte & SRST != 0 {
                    self.initialize();
                }
            }
            0x0C..=0x0F => self.registers[register] = byte,
            _ => {}
        }
    }
}

store!(Ak8963);

impl Peer for Ak8963 {
    fn start(&mut self, address: u8, read: bool, _context: &mut Timeline<'_, '_>) -> bool {
        if address != ADDRESS {
            self.phase = Phase::Idle;
            return false;
        }
        if read {
            if !self.reading && self.mode() != POWER_DOWN && self.mode() != FUSE_ROM {
                self.measure();
            }
            self.phase = Phase::Data;
        } else {
            self.phase = Phase::Register;
        }
        true
    }

    fn write(&mut self, byte: u8, _context: &mut Timeline<'_, '_>) -> bool {
        match self.phase {
            Phase::Register => {
                self.pointer = usize::from(byte);
                self.phase = Phase::Data;
            }
            Phase::Data => {
                self.write_register(self.pointer, byte);
                self.pointer += 1;
            }
            Phase::Idle => return false,
        }
        true
    }

    fn read(&mut self, _last: bool, _context: &mut Timeline<'_, '_>) -> u8 {
        match self.phase {
            Phase::Data => {
                let register = self.pointer;
                self.pointer += 1;
                self.read_register(register)
            }
            Phase::Register | Phase::Idle => 0,
        }
    }

    fn stop(&mut self, _context: &mut Timeline<'_, '_>) {
        self.phase = Phase::Idle;
    }
}