// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The MS5611-01BA03, a barometric pressure sensor on a serial interface: a conversion commanded, waited
//! for and read out, and the calibration a driver turns it into a pressure with.
//!
//! Written against TE Connectivity document ENG_DS_MS5611-01BA03_B3, "MS5611-01BA03 Barometric Pressure
//! Sensor, with stainless steel cap", 06/2017. Every page number below is that document's. It sits behind
//! a chip select on the same bus as the parameter memory, so it sees `select`, words and `deselect` and
//! nothing else, as [`crate::chip::fram::Fm25v02`] does.
//!
//! The chip does no arithmetic. It hands a driver two raw converter words and six coefficients, and the
//! driver computes pressure and temperature from them by the datasheet's figures 2 and 3. The model holds
//! a pressure and a temperature instead — the signals a bench or a plant drives — and at each conversion
//! runs those figures backwards, so that what the driver computes is what the model was told.
//!
//! | Register or section | Behaviour | Source | Test |
//! |---|---|---|---|
//! | Commands | Five, one byte each: reset `1Eh`; convert `D1` at `40h` to `48h` and `D2` at `50h` to `58h`, the oversampling ratio in bits `Os2..Os0`; ADC read `00h`; PROM read `A0h` to `AEh`, the word in bits `Ad2..Ad0` so word *n* is `A0h + 2n`. The first byte after the select falls is the command | Figure 4, p. 10; p. 7 | `each_prom_word_reads_at_its_own_command_most_significant_byte_first`, `a_conversion_answers_after_its_time_and_not_before` |
//! | Reset | "Shall be sent once after power-on to make sure that the calibration PROM gets loaded into the internal register"; the reload takes 2.8 ms. Until a reset has reloaded it the PROM answers nothing, which is the zero a driver refuses | Figures 5 and 6, pp. 10–11 | `the_prom_answers_nothing_until_the_reset_has_reloaded_it` |
//! | PROM | Eight 16-bit words, most significant byte first: word 0 "factory data and the setup", 1 to 6 the coefficients `C1` to `C6`, 7 "the serial code and CRC" with the CRC in the low four bits | PROM read sequence, p. 11; Figure 16, p. 13 | `each_prom_word_reads_at_its_own_command_most_significant_byte_first` |
//! | CRC-4 | "A 4-bit CRC has been implemented to check the data validity in memory", per application note AN520, which is not on the shelf; the driver that reads this chip is the second witness, and word 7 is computed to satisfy it | p. 13 | `the_crc_in_word_seven_is_the_drivers` |
//! | Conversion | `D1` or `D2` at OSR 256, 512, 1024, 2048 or 4096, taking `tc` of 0.54, 1.06, 2.08, 4.13 or 8.22 ms, the typical figures | ADC table, p. 3; Figure 7, p. 11 | `a_conversion_answers_after_its_time_and_not_before` |
//! | ADC read | 24 bits, "the MSB first". "If the conversion is not executed before the ADC read command, or the ADC read command is repeated, it will give 0 as the output result. If the ADC read command is sent during conversion the result will be 0, the conversion will not stop and the final result will be wrong. Conversion sequence sent during the already started conversion process will yield incorrect result as well" | p. 11; Figure 8 | `an_adc_read_during_a_conversion_answers_nothing_and_spoils_it`, `a_repeated_adc_read_answers_nothing`, `an_adc_read_with_nothing_converted_answers_nothing`, `a_conversion_started_over_another_spoils_both` |
//! | Chip select | "The CSB pin can be pulled high after the command is sent or after the end of the command execution (for example end of conversion)": the select rising ends the command and nothing else, and a conversion runs on | SPI mode, p. 6 | `the_select_may_rise_after_the_command_and_the_conversion_runs_on` |
//! | What it measures | A pressure in pascals and a temperature in hundredths of a degree, [`Ms5611::set_pressure`] and [`Ms5611::set_temperature`], which the next conversion turns into the `D2` whose temperature by figures 2 and 3 is the one held and the `D1` whose compensated pressure is the one held, with the coefficients in this chip's own PROM | Figure 2, p. 8; Figure 3, p. 9 | `the_datasheet_example_round_trips`, `a_cold_pressure_round_trips_through_the_second_order_terms`, `the_signals_move_the_next_conversion` |
//! | Resolution | The datasheet gives it as an RMS figure at each ratio, 0.027 mbar at OSR 1024. Each pressure conversion differs from the last by one count in a three-step cycle, a count being about 0.0002 mbar, because a driver that calls a sensor stuck when its reading stands still would otherwise be right about this one | p. 4 | `a_pressure_conversion_never_answers_the_same_word_four_times` |
//! | Ranges | Pressure 10 to 1200 mbar and temperature −40 to +85 °C; `D1` and `D2` are 24-bit and are held to it | p. 2; Figure 2, p. 8 | `a_cold_pressure_round_trips_through_the_second_order_terms` |
//! | An unknown command | Not specified; ignored until the select rises | Figure 4 | `an_unknown_command_is_ignored_until_the_select_rises` |
//!
//! # What is not, and why
//!
//! | Departure | Reason |
//! |---|---|
//! | The I2C interface, `PS` high | The first board wires `PS` low. The commands are the same bytes; a board that wires it high needs an I2C bus model first |
//! | The 20 MHz ceiling on `SCLK`, and modes 0 and 3 | The seam carries words, not a clock, a phase or a polarity |
//! | What "wrong" is | Page 11 promises a wrong result three times and does not say which; the model gives zero, the one wrong answer a driver can tell from a right one |
//! | Conversion time as a spread | The typical figure; the table also gives a minimum and a maximum, and a driver reading at the minimum would be right here and might not be on the part |
//! | Noise beyond one count, self-heating, long-term drift | Characteristics, not a programming contract; a plant that wants them drives the signals |
//! | Word 0 and the serial code in word 7 | Arbitrary, chosen once, and the CRC is computed over them |
//! | Power-up before the first reset | The datasheet asks for a reset once after power-on; the PROM is unloaded until it |
use crate::chip::spi::Peer;
use crate::device::DeviceContext;
use crate::time::VirtualNanos;
/// Figure 4.
const RESET: u8 = 0x1E;
const ADC_READ: u8 = 0x00;
const CONVERT_D1: u8 = 0x40;
const CONVERT_D2: u8 = 0x50;
const PROM_READ: u8 = 0xA0;
/// Bits `Os2..Os0` of a conversion command and `Ad2..Ad0` of a PROM read, Figure 4.
const INDEX: u8 = 0b0000_1110;
/// The reload after a reset, Figures 5 and 6.
pub const RELOAD: VirtualNanos = VirtualNanos(2_800_000);
/// `tc`, typical, for OSR 256 to 4096, the ADC table on page 3.
pub const CONVERSION: [VirtualNanos; 5] = [
VirtualNanos(540_000),
VirtualNanos(1_060_000),
VirtualNanos(2_080_000),
VirtualNanos(4_130_000),
VirtualNanos(8_220_000),
];
/// The coefficients of Figure 2's worked example, which is the one set of numbers the datasheet gives, so
/// the datasheet's own example is the model's test.
pub const COEFFICIENTS: [u16; 6] = [40127, 36924, 23317, 23282, 33464, 28312];
/// Word 0 and the serial code of word 7: arbitrary, chosen once.
const FACTORY: u16 = 0x3C00;
const SERIAL: u16 = 0xB100;
/// A 24-bit converter word, Figure 2.
const WORD: i128 = 1 << 24;
/// 20 °C in hundredths, the reference temperature of Figure 2.
const REFERENCE: i128 = 2000;
/// The standard atmosphere at the reference temperature: where `dT` is zero and neither figure's second
/// order is in play.
pub const STANDARD_PASCALS: u32 = 101_325;
/// Where a select cycle is.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Phase {
Command,
/// A PROM word going out, and how many of its two bytes are left.
Prom(u16, u8),
/// An ADC result going out, and how many of its three bytes are left.
Adc(u32, u8),
Ignored,
}
/// A conversion the chip has started.
#[derive(Debug, Clone, Copy)]
struct Conversion {
done_at: VirtualNanos,
result: u32,
/// Read during conversion, or started over another: page 11's wrong result, given as zero.
spoiled: bool,
read: bool,
}
/// One MS5611-01BA03.
#[derive(Debug)]
pub struct Ms5611 {
prom: [u16; 8],
/// When the last reset began reloading the PROM, or `None` before the first.
reset_at: Option<VirtualNanos>,
conversion: Option<Conversion>,
pascals: u32,
centi_celsius: i32,
/// Pressure conversions so far, for the one-count cycle.
converted: u64,
phase: Phase,
selected: bool,
}
impl Default for Ms5611 {
fn default() -> Self {
Self::new()
}
}
impl Ms5611 {
/// A chip at power-up, measuring the standard atmosphere at 20 °C, its PROM not yet loaded.
#[must_use]
pub fn new() -> Self {
let mut prom = [0; 8];
prom[0] = FACTORY;
prom[1..7].copy_from_slice(&COEFFICIENTS);
prom[7] = SERIAL;
prom[7] |= u16::from(crc4(&prom));
Self {
prom,
reset_at: None,
conversion: None,
pascals: STANDARD_PASCALS,
centi_celsius: 2000,
converted: 0,
phase: Phase::Command,
selected: false,
}
}
/// The pressure the chip measures from now on, in pascals.
pub fn set_pressure(&mut self, pascals: u32) {
self.pascals = pascals;
}
/// The temperature the chip measures from now on, in hundredths of a degree Celsius.
pub fn set_temperature(&mut self, centi_celsius: i32) {
self.centi_celsius = centi_celsius;
}
/// The eight PROM words as the chip holds them, for a test to hold word 7 against the driver's check.
#[must_use]
pub fn prom(&self) -> [u16; 8] {
self.prom
}
/// Whether the PROM has been reloaded by a reset by `now`.
fn loaded(&self, now: VirtualNanos) -> bool {
self.reset_at
.is_some_and(|reset| now.0 >= reset.0.saturating_add(RELOAD.0))
}
/// `D2` for the temperature held: the smallest `dT` at which figures 2 and 3 come to it, found by
/// bisection because the second order makes it quadratic, then `C5 * 2^8 + dT`, held to 24 bits. The
/// smallest, so that at the reference temperature `dT` is zero as figure 2 has it.
fn d2(&self) -> u32 {
let c5 = i128::from(self.prom[5]);
let (mut low, mut high) = (-(c5 << 8), WORD - 1 - (c5 << 8));
let wanted = i128::from(self.centi_celsius);
while low < high {
let middle = (low + high).div_euclid(2);
if self.temperature(middle) < wanted {
low = middle + 1;
} else {
high = middle;
}
}
u32::try_from((c5 << 8) + low).expect("held to twenty-four bits")
}
/// Figure 2's first-order `TEMP` for a `dT`, `2000 + dT * C6 / 2^23`, the division rounding down so
/// that a `dT` below zero is a temperature below the reference, as the figure's arithmetic has it.
fn first_order(&self, dt: i128) -> i128 {
REFERENCE + (dt * i128::from(self.prom[6])).div_euclid(1 << 23)
}
/// `TEMP` of figures 2 and 3 for a `dT`: the first order, less `dT^2 / 2^31` below 20 °C.
fn temperature(&self, dt: i128) -> i128 {
let first = self.first_order(dt);
if first < REFERENCE {
first - (dt * dt) / (1 << 31)
} else {
first
}
}
/// `OFF` and `SENS` of figures 2 and 3 for a `dT`, with the second order below 20 °C and the extra
/// below −15 °C.
fn offset_and_sensitivity(&self, dt: i128) -> (i128, i128) {
let [c1, c2, c3, c4] = [1, 2, 3, 4].map(|n| i128::from(self.prom[n]));
let mut off = (c2 << 16) + (c4 * dt) / (1 << 7);
let mut sens = (c1 << 15) + (c3 * dt) / (1 << 8);
let first = self.first_order(dt);
if first < REFERENCE {
let square = (first - REFERENCE) * (first - REFERENCE);
let mut off2 = 5 * square / 2;
let mut sens2 = 5 * square / 4;
if first < -1500 {
let square = (first + 1500) * (first + 1500);
off2 += 7 * square;
sens2 += 11 * square / 2;
}
off -= off2;
sens -= sens2;
}
(off, sens)
}
/// `D1` for the pressure held at the temperature held: figure 2's `P = (D1 * SENS / 2^21 - OFF) / 2^15`
/// solved for `D1`, rounded to the nearest count, one count of cycle added, held to 24 bits.
fn d1(&mut self) -> u32 {
let dt = i128::from(self.d2()) - (i128::from(self.prom[5]) << 8);
let (off, sens) = self.offset_and_sensitivity(dt);
let pressure = i128::from(self.pascals);
let numerator = ((pressure << 15) + off) << 21;
let exact = if sens > 0 {
(numerator + sens / 2) / sens
} else {
0
};
let cycle = [0, 1, -1][usize::try_from(self.converted % 3).expect("three steps")];
self.converted += 1;
u32::try_from((exact + cycle).clamp(0, WORD - 1)).expect("held to twenty-four bits")
}
/// One command byte, page 10.
fn command(&mut self, byte: u8, now: VirtualNanos) -> Phase {
match byte {
RESET => {
self.reset_at = Some(now);
self.conversion = None;
Phase::Ignored
}
ADC_READ => {
let answer = match &mut self.conversion {
Some(conversion) if now.0 < conversion.done_at.0 => {
conversion.spoiled = true;
0
}
Some(conversion) if !conversion.read => {
conversion.read = true;
if conversion.spoiled {
0
} else {
conversion.result
}
}
_ => 0,
};
Phase::Adc(answer, 3)
}
_ if byte & !INDEX == CONVERT_D1 || byte & !INDEX == CONVERT_D2 => {
let osr = usize::from((byte & INDEX) >> 1);
if osr >= CONVERSION.len() {
return Phase::Ignored;
}
let spoiled = self
.conversion
.is_some_and(|under_way| now.0 < under_way.done_at.0);
let result = if byte & !INDEX == CONVERT_D1 {
self.d1()
} else {
self.d2()
};
self.conversion = Some(Conversion {
done_at: VirtualNanos(now.0 + CONVERSION[osr].0),
result,
spoiled,
read: false,
});
Phase::Ignored
}
_ if byte & !INDEX == PROM_READ => {
let word = usize::from((byte & INDEX) >> 1);
let value = if self.loaded(now) { self.prom[word] } else { 0 };
Phase::Prom(value, 2)
}
_ => Phase::Ignored,
}
}
}
/// The CRC-4 of the PROM as the driver checks it: word 7's low byte cleared, sixteen bytes fed
/// through the polynomial `0x3000`, the high nibble of the remainder.
#[must_use]
pub fn crc4(prom: &[u16; 8]) -> u8 {
let mut remainder: u16 = 0;
for (index, word) in prom.iter().enumerate() {
let word = if index == 7 { word & 0xFF00 } else { *word };
for byte in word.to_be_bytes() {
remainder ^= u16::from(byte);
for _ in 0..8 {
remainder = if remainder & 0x8000 != 0 {
(remainder << 1) ^ 0x3000
} else {
remainder << 1
};
}
}
}
u8::try_from((remainder >> 12) & 0xF).expect("four bits")
}
impl Peer for Ms5611 {
fn select(&mut self, _context: &DeviceContext<'_>) {
self.selected = true;
self.phase = Phase::Command;
}
/// The select rising ends the command and nothing else: a conversion under way runs on.
fn deselect(&mut self, _context: &DeviceContext<'_>) {
self.selected = false;
self.phase = Phase::Command;
}
/// One byte on the wire while selected; the chip is eight bits wide and takes the low byte.
fn transfer(&mut self, mosi: u16, context: &DeviceContext<'_>) -> u16 {
if !self.selected {
return 0;
}
let byte = (mosi & 0xFF) as u8;
let (answer, next) = match self.phase {
Phase::Command => (0, self.command(byte, context.now())),
Phase::Prom(value, left) => (
(value >> (8 * (left - 1))) & 0xFF,
if left == 1 {
Phase::Ignored
} else {
Phase::Prom(value, left - 1)
},
),
Phase::Adc(value, left) => (
u16::try_from((value >> (8 * (left - 1))) & 0xFF).expect("one byte"),
if left == 1 {
Phase::Ignored
} else {
Phase::Adc(value, left - 1)
},
),
Phase::Ignored => (0, Phase::Ignored),
};
self.phase = next;
answer
}
}