rigless 0.2.2

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The LIS3MDL, a three-axis magnetometer: a register file on a serial interface whose command byte
//! carries the direction, an increment flag and the address.
//!
//! The part speaks two interfaces to the same register file. [`Lis3mdl::new`] is the SPI part,
//! driven through [`crate::chip::spi::Peer`]. [`Lis3mdl::on_i2c`] is the I2C part, strapped by its
//! `SA1` pin ([`Sa1`]) and driven through [`crate::chip::i2c::Peer`]. Only the framing of bytes
//! differs. The registers, the conversions and their timing are shared, and a part built for one
//! interface doesn't answer on the other.
//!
//! Written against ST's LIS3MDL datasheet, `DS9463` rev 7, December 2023. Every section, table and
//! figure below is that document's.
//!
//! **Conversions are derived from virtual time.** Nothing is scheduled per conversion. At each
//! select, the conversions that fell due since the rate or the mode last changed are counted from
//! the rate the registers give. If any did, the output registers take the field as held. What a
//! driver polling `STATUS_REG` can observe is the same. What it can't is the field changing between
//! two reads.
//!
//! | Register or section | Behavior | Source | Test |
//! |---|---|---|---|
//! | The serial protocol | Bit 0 of the first byte is `RW`, "When 0, the data DI(7:0) is written into the device. When 1, the data DO(7:0) from the device is read". Bit 1 is `MS`, "When 0, the address remains unchanged in multiple read/write commands. When 1, the address is autoincremented". Bits 2 to 7 are the address. Then comes a byte per register | 5.2, 5.2.1, 5.2.2 | `the_command_byte_carries_the_direction_the_increment_and_the_address` |
//! | The map | Table 15's registers exist. A reserved address "must not be changed" and reads zero, and the write is dropped | 6 | `a_reserved_address_takes_nothing_and_reads_zero`, `a_read_only_register_takes_no_write` |
//! | Defaults | `CTRL_REG1` `00010000`, `CTRL_REG3` `00000011`, `INT_CFG` `11101000`, every other register zero | 6, table 15 | `the_defaults_are_table_15s` |
//! | `WHO_AM_I` | `00111101` | 7.4, table 16 | `who_am_i_is_0x3d` |
//! | Data rate | `DO[2:0]`: 0.625, 1.25, 2.5, 5, 10, 20, 40, 80 Hz. With `FAST_ODR` set: 1000, 560, 300, 155 Hz by `OM[1:0]`. With `LP` in `CTRL_REG3`: "`DO[2:0]` is set to 0.625 Hz" | 7.5 tables 19 and 21, 7.7 table 26 | `continuous_mode_converts_at_the_data_rate` |
//! | Mode | `MD[1:0]`: `00` continuous-conversion, `01` single-conversion, `10` and `11` power-down, the default | 7.7 table 27 | `power_down_converts_nothing`, `single_conversion_mode_converts_once` |
//! | The output | "The value of the magnetic field is expressed as two's complement" at 6842, 3421, 2281 or 1711 LSB/gauss by `FS[1:0]`. The low byte is at the lower address unless `BLE`, "data `MSb` at lower address" | 7.11–7.13, 7.6 table 24, 2.1 table 2, 7.8 table 29 | `the_words_are_the_field_at_the_selected_full_scale`, `ble_puts_the_high_byte_first` |
//! | Hard-iron offsets | Sixteen bits two's complement, "act on the magnetic output data value in order to delete the environmental offset": subtracted in counts | 7.1–7.3 | `the_hard_iron_offsets_are_taken_from_the_output` |
//! | `STATUS_REG` | `ZYXDA` and the axis bits at each set. `ZYXOR` and the axis bits when "a new set of data has overwritten the previous set" | 7.10 table 34 | `zyxda_stands_until_the_set_is_read_and_a_second_set_marks_overrun` |
//! | Temperature | "8 LSB/°C", "acquired at each conversion cycle" when `TEMP_EN` is set. Two's complement, low byte first | 2.2 table 3, 7.5, 7.14 | `the_temperature_is_eight_counts_a_degree_when_enabled` |
//! | `SOFT_RST` | "Configuration registers and user register reset function": the control registers and the offsets back to table 15 | 7.6 table 23 | `a_soft_reset_restores_the_defaults` |
//! | The I2C address | "The slave address (SAD) associated to the LIS3MDL is 00111x0b", `0011110b` (`0x1E`) with `SDO/SA1` "connected to the voltage supply", `0011100b` (`0x1C`) "if the SDO/SA1 pin is connected to ground", chosen by [`Sa1`]. The part acknowledges that address and no other. A part built with [`Lis3mdl::new`] acknowledges none | 5.1.1, table 10 | `it_answers_at_0x1e_with_sa1_high_and_at_0x1c_with_sa1_low`, `nothing_is_acknowledged_at_the_other_address` |
//! | The I2C sub-address | After the address and its acknowledge "an 8-bit subaddress (SUB) is transmitted: the 7 `LSb` represent the actual register address". The sub-address is acknowledged like any byte the part receives. What its `MSb` does is a departure, below | 5.1.1 | `a_burst_read_walks_the_registers_with_or_without_the_increment_bit` |
//! | I2C writes | After the sub-address each byte is a register's value, acknowledged, "to allow multiple data read/write". Tables 11 and 12 | 5.1.1, tables 11, 12 | `a_write_then_a_read_back_over_i2c` |
//! | I2C reads | A write start carrying the sub-address, then "a repeated START (SR)" with the address and the read bit, then a byte per master acknowledge until the not-acknowledge and the stop. Tables 13 and 14 | 5.1.1, tables 13, 14 | `a_burst_read_walks_the_registers_with_or_without_the_increment_bit`, `who_am_i_reads_0x3d_over_i2c` |
//! | One register file | The words an I2C read returns are those an SPI read returns at the same instant. The conversions due land at each start | 5.1.1, 5.2 | `i2c_and_spi_return_the_same_field_words_at_the_same_instant` |
//! | What it measures | A field in nanotesla on its own three axes, [`Lis3mdl::set_field`], and the die temperature in hundredths of a degree, [`Lis3mdl::set_temperature`] | 3.1 | `the_words_are_the_field_at_the_selected_full_scale` |
//!
//! # What is not, and why
//!
//! | Departure | Reason |
//! |---|---|
//! | When `ZYXDA` clears | Table 34 says when it sets and not when it clears. The model clears the `DA` bits, and the `OR` bits with them, when `OUT_Z_H`, the last register of the set, is read. Every read of the set the driver makes is a six-byte burst that ends there |
//! | `BDU` | A set never changes under a burst here, with or without the bit, because the output registers change only at the select falling. The bit is stored |
//! | The temperature's zero | Table 3 gives the slope and not the count at any temperature. The model takes 25 °C as the zero. The driver never reads it |
//! | Single-conversion mode returning to power-down | Table 27 doesn't say what `MD` reads after the one conversion. The model converts once and leaves `MD` as written |
//! | The operating modes' averaging, `ST`, `FAST_READ`, `REBOOT`, `SIM` | Stored. A filter's averaging is nothing to a field a plant holds, and the seam is four-wire whatever `SIM` says |
//! | `INT_CFG`, `INT_SRC`, `INT_THS`, the `INT` and `DRDY` pins | Stored and answered as written, `INT_SRC` at zero. No pin is driven, and the driver reads neither |
//! | The 10 MHz ceiling on `SPC` | The seam carries words, not a clock |
//! | An I2C burst without `SUB(7)` | 5.1.1 says "the `MSb` enables address autoincrement" and "in order to read multiple bytes ... `SUB(7)` must be equal to 1". The part doesn't hold to it. Drivers that run on boards with the part on I2C send the plain address and read six distinct bytes from `OUT_X_L`: Adafruit's Arduino library, whose bus layer adds no bit on I2C, and its CircuitPython library, which reads `<hhh` at `0x28`. A part that held the address would give the same word on all three axes. ArduPilot's driver reads the same way. So over I2C the address moves on after every byte, read or written, whatever `SUB(7)` says. Tested by `a_burst_read_walks_the_registers_with_or_without_the_increment_bit` and `ardupilot_reads_the_field_over_i2c_without_the_increment_bit` |
//! | A burst past `OUT_Z_H` | The page doesn't say where an incremented address goes after `0x2D`. On the part it goes back to `0x28`, `OUT_X_L`, and a burst from the output registers never reaches `TEMP_OUT_L`. ST confirmed it as standard behavior on its community forum in October 2018. The model does the same on both interfaces, and the temperature takes a read of its own. Tested by `an_spi_burst_wraps_from_out_z_h_to_out_x_l` and `an_i2c_burst_wraps_from_out_z_h_to_out_x_l` |
//! | The I2C sub-address past `0x3F` | The sub-address has seven bits and the map ends at `0x3F`. The page doesn't say what the upper half does. The model reads zero there, drops a write, acknowledges it, and wraps the increment at `0x7F` |
//! | An I2C read without a sub-address | Tables 13 and 14 always send one first. The page doesn't say what a read start alone returns. The model acknowledges it and reads zero |
//! | The I2C clock | Fast mode (400 kHz) is the page's ceiling. The seam carries bytes, not a clock |
//! | Clock stretching | "it can hold the clock line SCL low". The part never needs to |
//! | Reserved registers doing "permanent damage" when written | The write is dropped and nothing is damaged |

use crate::chip::{Timeline, round};
use crate::chip::{i2c, spi};
use crate::snapshot::store;
use crate::time::VirtualNanos;

/// Table 15's register map, by address.
const OFFSET_X_REG_L_M: usize = 0x05;
const OFFSET_Z_REG_H_M: usize = 0x0A;
const WHO_AM_I: usize = 0x0F;
const CTRL_REG1: usize = 0x20;
const CTRL_REG2: usize = 0x21;
const CTRL_REG3: usize = 0x22;
const CTRL_REG4: usize = 0x23;
const CTRL_REG5: usize = 0x24;
const STATUS_REG: usize = 0x27;
const OUT_X_L: usize = 0x28;
const OUT_Z_H: usize = 0x2D;
const TEMP_OUT_L: usize = 0x2E;
const TEMP_OUT_H: usize = 0x2F;
const INT_CFG: usize = 0x30;
const INT_SRC: usize = 0x31;
const INT_THS_H: usize = 0x33;
/// Six address bits, 5.2.
const REGISTERS: usize = 0x40;

/// The bits the model acts on, section 7.
const DEVICE_ID: u8 = 0x3D;
const CTRL_REG1_DEFAULT: u8 = 0x10;
const CTRL_REG3_DEFAULT: u8 = 0x03;
const INT_CFG_DEFAULT: u8 = 0xE8;
const TEMP_EN: u8 = 0x80;
const OM: u8 = 0x60;
const DO: u8 = 0x1C;
const FAST_ODR: u8 = 0x02;
const FS: u8 = 0x60;
const SOFT_RST: u8 = 0x04;
const LP: u8 = 0x20;
const MD: u8 = 0x03;
const CONTINUOUS: u8 = 0b00;
const SINGLE: u8 = 0b01;
const BLE: u8 = 0x02;
const DATA_AVAILABLE: u8 = 0x0F;
const OVERRUN: u8 = 0xF0;
/// The command byte, 5.2: bit 0 on the wire is the byte's top bit.
const READ: u8 = 0x80;
const INCREMENT: u8 = 0x40;
const ADDRESS: u8 = 0x3F;
/// 5.1.1: the seven-bit addresses, `0011110b` and `0011100b`.
const I2C_ADDRESS_SA1_HIGH: u8 = 0b001_1110;
const I2C_ADDRESS_SA1_LOW: u8 = 0b001_1100;
/// 5.1.1: the sub-address, whose "7 `LSb` represent the actual register address". Its `MSb` moves
/// nothing here, as the departures say.
const SUB_ADDRESS: u8 = 0x7F;

/// Table 21, in millihertz, by `DO[2:0]`.
const RATE_MILLIHERTZ: [u64; 8] = [625, 1_250, 2_500, 5_000, 10_000, 20_000, 40_000, 80_000];
/// Table 19, in millihertz, by `OM[1:0]` with `FAST_ODR` set.
const FAST_RATE_MILLIHERTZ: [u64; 4] = [1_000_000, 560_000, 300_000, 155_000];
/// Table 2, LSB per gauss, by `FS[1:0]`.
const SENSITIVITY: [i64; 4] = [6_842, 3_421, 2_281, 1_711];
const NANOTESLA_PER_GAUSS: i64 = 100_000;
/// Table 3, LSB per degree, about the zero the page doesn't give.
const TEMP_LSB_PER_DEGREE: i64 = 8;
const TEMP_ZERO_CENTI: i32 = 2_500;

/// How the board wires the `SDO/SA1` pin, which sets the address bit the page calls `x`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Sa1 {
    /// Connected to the supply, which makes the address `0011110b`.
    High,
    /// Connected to ground, which makes the address `0011100b`.
    Low,
}

impl Sa1 {
    /// The seven-bit address the pin selects, 5.1.1.
    #[must_use]
    pub fn address(self) -> u8 {
        match self {
            Self::High => I2C_ADDRESS_SA1_HIGH,
            Self::Low => I2C_ADDRESS_SA1_LOW,
        }
    }
}

/// Where a select cycle or an I2C transaction is.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
enum Phase {
    /// The next byte is the command on SPI, or the sub-address on I2C.
    Command,
    /// Nothing is addressed to this part.
    Idle,
    /// The register the next byte reads or writes, and whether the address moves on after it.
    Read(usize, bool),
    Write(usize, bool),
}

/// One LIS3MDL.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Lis3mdl {
    #[serde(with = "crate::snapshot::array")]
    registers: [u8; REGISTERS],
    /// The field it measures, in nanotesla on its own axes.
    nanotesla: [i32; 3],
    centi_celsius: i32,
    /// Where the current rate began: the instant and the conversion count.
    epoch_at: VirtualNanos,
    epoch_count: u64,
    /// The conversion count single-conversion mode stops at.
    single_until: Option<u64>,
    converted: u64,
    phase: Phase,
    selected: bool,
    /// The I2C address the part answers at, or none for the SPI part.
    i2c_address: Option<u8>,
}

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

impl Lis3mdl {
    /// A part at power-on: table 15's defaults, powered down, measuring a plausible northern-hemisphere
    /// field at 25 °C.
    #[must_use]
    pub fn new() -> Self {
        let mut part = Self {
            registers: [0; REGISTERS],
            nanotesla: [22_000, 0, -42_000],
            centi_celsius: TEMP_ZERO_CENTI,
            epoch_at: VirtualNanos(0),
            epoch_count: 0,
            single_until: None,
            converted: 0,
            phase: Phase::Command,
            selected: false,
            i2c_address: None,
        };
        part.reset(VirtualNanos(0));
        part
    }

    /// A part at power-on on an I2C bus, answering at the address `sa1` selects and at no other. The
    /// registers and the field are those of [`Lis3mdl::new`].
    #[must_use]
    pub fn on_i2c(sa1: Sa1) -> Self {
        Self {
            i2c_address: Some(sa1.address()),
            ..Self::new()
        }
    }

    /// The field the part 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];
    }

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

    /// Table 15's defaults for every register, and the conversion count restarted.
    fn reset(&mut self, now: VirtualNanos) {
        self.registers = [0; REGISTERS];
        self.registers[WHO_AM_I] = DEVICE_ID;
        self.registers[CTRL_REG1] = CTRL_REG1_DEFAULT;
        self.registers[CTRL_REG3] = CTRL_REG3_DEFAULT;
        self.registers[INT_CFG] = INT_CFG_DEFAULT;
        self.single_until = None;
        self.rebase(now);
    }

    /// Tables 19, 21, 26 and 27: the rate conversions come at, in millihertz, or nothing powered down.
    fn rate_millihertz(&self) -> u64 {
        let control1 = self.registers[CTRL_REG1];
        let control3 = self.registers[CTRL_REG3];
        if control3 & MD != CONTINUOUS && control3 & MD != SINGLE {
            0
        } else if control3 & LP != 0 {
            RATE_MILLIHERTZ[0]
        } else if control1 & FAST_ODR != 0 {
            FAST_RATE_MILLIHERTZ[usize::from((control1 & OM) >> 5)]
        } else {
            RATE_MILLIHERTZ[usize::from((control1 & DO) >> 2)]
        }
    }

    fn due(&self, now: VirtualNanos) -> u64 {
        let since = u128::from(now.0.saturating_sub(self.epoch_at.0));
        let taken = since * u128::from(self.rate_millihertz()) / 1_000_000_000_000;
        let due = self.epoch_count + u64::try_from(taken).expect("a run outlasts no count");
        self.single_until.map_or(due, |until| due.min(until))
    }

    /// A change of rate or of mode: the conversions so far stand, the next come at the new rate.
    fn rebase(&mut self, now: VirtualNanos) {
        self.epoch_count = self.due(now);
        self.epoch_at = now;
    }

    /// The conversions that fell due by `now`, if any, landing as one set from the field as held.
    fn catch_up(&mut self, now: VirtualNanos) {
        let due = self.due(now);
        if due <= self.converted {
            return;
        }
        let unread = self.registers[STATUS_REG] & DATA_AVAILABLE != 0 || due - self.converted > 1;
        self.converted = due;
        let scale = SENSITIVITY[usize::from((self.registers[CTRL_REG2] & FS) >> 5)];
        let high_first = self.registers[CTRL_REG4] & BLE != 0;
        for axis in 0..3 {
            let offset = i64::from(i16::from_le_bytes([
                self.registers[OFFSET_X_REG_L_M + 2 * axis],
                self.registers[OFFSET_X_REG_L_M + 2 * axis + 1],
            ]));
            let counts =
                round(i64::from(self.nanotesla[axis]) * scale, NANOTESLA_PER_GAUSS) - offset;
            let bytes = word(counts, high_first);
            self.registers[OUT_X_L + 2 * axis..OUT_X_L + 2 * axis + 2].copy_from_slice(&bytes);
        }
        let temperature = if self.registers[CTRL_REG1] & TEMP_EN != 0 {
            round(
                i64::from(self.centi_celsius - TEMP_ZERO_CENTI) * TEMP_LSB_PER_DEGREE,
                100,
            )
        } else {
            0
        };
        self.registers[TEMP_OUT_L..=TEMP_OUT_H].copy_from_slice(&word(temperature, false));
        if unread {
            self.registers[STATUS_REG] |= OVERRUN;
        }
        self.registers[STATUS_REG] |= DATA_AVAILABLE;
    }

    /// Table 15: whether an address is a register at all.
    fn mapped(register: usize) -> bool {
        matches!(
            register,
            OFFSET_X_REG_L_M..=OFFSET_Z_REG_H_M | WHO_AM_I | CTRL_REG1..=CTRL_REG5 | STATUS_REG..=INT_THS_H
        )
    }

    /// Table 15's "Type" column: the registers a write lands in.
    fn writable(register: usize) -> bool {
        Self::mapped(register) && !matches!(register, WHO_AM_I | STATUS_REG..=TEMP_OUT_H | INT_SRC)
    }

    fn read_register(&mut self, register: usize) -> u8 {
        // The I2C sub-address has a bit more than the map has.
        let Some(&value) = self.registers.get(register) else {
            return 0;
        };
        if register == OUT_Z_H {
            self.registers[STATUS_REG] = 0;
        }
        value
    }

    fn write_register(&mut self, register: usize, byte: u8, now: VirtualNanos) {
        if !Self::writable(register) {
            return;
        }
        match register {
            CTRL_REG1 => {
                self.rebase(now);
                self.registers[CTRL_REG1] = byte;
            }
            CTRL_REG2 if byte & SOFT_RST != 0 => {
                for register in OFFSET_X_REG_L_M..=OFFSET_Z_REG_H_M {
                    self.registers[register] = 0;
                }
                self.rebase(now);
                self.registers[CTRL_REG1] = CTRL_REG1_DEFAULT;
                self.registers[CTRL_REG2] = 0;
                self.registers[CTRL_REG3] = CTRL_REG3_DEFAULT;
                self.registers[CTRL_REG4] = 0;
                self.registers[CTRL_REG5] = 0;
                self.single_until = None;
            }
            CTRL_REG3 => {
                self.rebase(now);
                self.registers[CTRL_REG3] = byte;
                self.single_until = (byte & MD == SINGLE).then_some(self.epoch_count + 1);
            }
            _ => self.registers[register] = byte,
        }
    }
}

/// A sixteen-bit two's complement word held to its range, low byte first unless `high_first`.
fn word(counts: i64, high_first: bool) -> [u8; 2] {
    let word = i16::try_from(counts.clamp(i64::from(i16::MIN), i64::from(i16::MAX)))
        .expect("held to sixteen bits");
    if high_first {
        word.to_be_bytes()
    } else {
        word.to_le_bytes()
    }
}

/// The register after `register` under the increment flag, wrapping at `mask`, and from `OUT_Z_H`
/// back to `OUT_X_L` as the part does.
fn step(register: usize, increment: bool, mask: usize) -> usize {
    match (increment, register) {
        (false, _) => register,
        (true, OUT_Z_H) => OUT_X_L,
        (true, _) => (register + 1) & mask,
    }
}

store!(Lis3mdl);

impl spi::Peer for Lis3mdl {
    /// The select falling: the conversions due by now land, then the first byte is the command. A part
    /// strapped for I2C isn't on the SPI bus and stays deselected.
    fn select(&mut self, context: &mut Timeline<'_, '_>) {
        if self.i2c_address.is_some() {
            return;
        }
        self.selected = true;
        self.phase = Phase::Command;
        self.catch_up(context.now());
    }

    fn deselect(&mut self, _context: &mut Timeline<'_, '_>) {
        self.selected = false;
        self.phase = Phase::Command;
    }

    /// 5.2: the command byte, then a byte per register, the address moving on only under `MS`.
    fn transfer(&mut self, mosi: u32, _bits: u8, context: &mut Timeline<'_, '_>) -> u32 {
        if !self.selected {
            return 0;
        }
        let byte = (mosi & 0xFF) as u8;
        match self.phase {
            Phase::Command => {
                let register = usize::from(byte & ADDRESS);
                let increment = byte & INCREMENT != 0;
                self.phase = if byte & READ != 0 {
                    Phase::Read(register, increment)
                } else {
                    Phase::Write(register, increment)
                };
                0
            }
            Phase::Read(register, increment) => {
                self.phase = Phase::Read(step(register, increment, 0x3F), increment);
                u32::from(self.read_register(register))
            }
            Phase::Write(register, increment) => {
                self.write_register(register, byte, context.now());
                self.phase = Phase::Write(step(register, increment, 0x3F), increment);
                0
            }
            Phase::Idle => 0,
        }
    }
}

impl i2c::Peer for Lis3mdl {
    /// 5.1.1: acknowledges its own address only. A write start expects the sub-address. A read start
    /// after a sub-address is the repeated start of tables 13 and 14 and reads from it. The
    /// conversions due by now land at every start.
    fn start(&mut self, address: u8, read: bool, context: &mut Timeline<'_, '_>) -> bool {
        if self.i2c_address != Some(address) {
            self.phase = Phase::Idle;
            return false;
        }
        self.catch_up(context.now());
        self.phase = match (read, self.phase) {
            (false, _) => Phase::Command,
            (true, Phase::Write(register, increment)) => Phase::Read(register, increment),
            (true, _) => Phase::Idle,
        };
        true
    }

    /// 5.1.1, tables 11 and 12: the sub-address, then a register's value per byte, each acknowledged,
    /// the address moving on after each whatever `SUB(7)` says.
    fn write(&mut self, byte: u8, context: &mut Timeline<'_, '_>) -> bool {
        match self.phase {
            Phase::Command => {
                self.phase = Phase::Write(usize::from(byte & SUB_ADDRESS), true);
                true
            }
            Phase::Write(register, increment) => {
                self.write_register(register, byte, context.now());
                self.phase = Phase::Write(step(register, increment, 0x7F), increment);
                true
            }
            Phase::Read(..) | Phase::Idle => false,
        }
    }

    /// 5.1.1, tables 13 and 14: a byte per master acknowledge, the address moving on after each.
    fn read(&mut self, _last: bool, _context: &mut Timeline<'_, '_>) -> u8 {
        match self.phase {
            Phase::Read(register, increment) => {
                self.phase = Phase::Read(step(register, increment, 0x7F), increment);
                self.read_register(register)
            }
            Phase::Command | Phase::Write(..) | Phase::Idle => 0,
        }
    }

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