embedded_devices/devices/analog_devices/max31865/
mod.rs

1//! The MAX31865 is an easy-to-use resistance-to-digital converter optimized for platinum
2//! resistance temperature detectors (RTDs). An external resistor sets the sensitivity
3//! for the RTD being used and a precision delta-sigma ADC converts the ratio of the RTD
4//! resistance to the reference resistance into digital form. The MAX31865's inputs are
5//! protected against overvoltage faults as large as ±45V. Programmable detection of RTD
6//! and cable open and short conditions is included.
7//!
8//! ## Usage (sync)
9//!
10//! ```rust
11//! # #[cfg(feature = "sync")] mod test {
12//! # fn test<I, D>(mut spi: I, delay: D) -> Result<(), embedded_devices::devices::analog_devices::max31865::MeasurementError<I::Error>>
13//! # where
14//! #   I: embedded_hal::spi::SpiDevice,
15//! #   D: embedded_hal::delay::DelayNs
16//! # {
17//! use embedded_devices::devices::analog_devices::max31865::{MAX31865Sync, registers::{FilterMode, Resistance, WiringMode}};
18//! use embedded_devices::sensor::OneshotSensorSync;
19//! use uom::si::thermodynamic_temperature::degree_celsius;
20//!
21//! // Create and initialize the device
22//! let mut max31865 = MAX31865Sync::new_spi(delay, spi, 4.3);
23//! max31865.init(WiringMode::ThreeWire, FilterMode::F_50Hz).unwrap();
24//!
25//! let temp = max31865.measure()?
26//!     .temperature.get::<degree_celsius>();
27//! println!("Current temperature: {:?}°C", temp);
28//! # Ok(())
29//! # }
30//! # }
31//! ```
32//!
33//! ## Usage (async)
34//!
35//! ```rust
36//! # #[cfg(feature = "async")] mod test {
37//! # async fn test<I, D>(mut spi: I, delay: D) -> Result<(), embedded_devices::devices::analog_devices::max31865::MeasurementError<I::Error>>
38//! # where
39//! #   I: embedded_hal_async::spi::SpiDevice,
40//! #   D: embedded_hal_async::delay::DelayNs
41//! # {
42//! use embedded_devices::devices::analog_devices::max31865::{MAX31865Async, registers::{FilterMode, Resistance, WiringMode}};
43//! use embedded_devices::sensor::OneshotSensorAsync;
44//! use uom::si::thermodynamic_temperature::degree_celsius;
45//!
46//! // Create and initialize the device
47//! let mut max31865 = MAX31865Async::new_spi(delay, spi, 4.3);
48//! max31865.init(WiringMode::ThreeWire, FilterMode::F_50Hz).await.unwrap();
49//!
50//! let temp = max31865.measure().await?
51//!     .temperature.get::<degree_celsius>();
52//! println!("Current temperature: {:?}°C", temp);
53//! # Ok(())
54//! # }
55//! # }
56//! ```
57
58use embedded_devices_derive::forward_register_fns;
59use embedded_devices_derive::sensor;
60use embedded_interfaces::TransportError;
61use registers::{
62    Configuration, ConversionMode, FaultDetectionCycle, FaultThresholdHigh, FaultThresholdLow, FilterMode, WiringMode,
63};
64use uom::si::f64::ThermodynamicTemperature;
65use uom::si::thermodynamic_temperature::degree_celsius;
66
67use crate::utils::callendar_van_dusen;
68
69pub mod registers;
70
71#[cfg_attr(feature = "defmt", derive(defmt::Format))]
72#[derive(Debug, thiserror::Error)]
73pub enum FaultDetectionError<BusError> {
74    /// Transport error
75    #[error("transport error")]
76    Transport(#[from] TransportError<(), BusError>),
77    /// Timeout (the detection never finished in the allocated time frame)
78    #[error("fault detection timeout")]
79    Timeout,
80    /// A fault was detected. Read the FaultStatus register for details.
81    #[error("fault detected")]
82    FaultDetected,
83}
84
85#[cfg_attr(feature = "defmt", derive(defmt::Format))]
86#[derive(Debug, thiserror::Error)]
87pub enum MeasurementError<BusError> {
88    /// Transport error
89    #[error("transport error")]
90    Transport(#[from] TransportError<(), BusError>),
91    /// A fault was detected. Read the FaultStatus register for details.
92    #[error("fault detected")]
93    FaultDetected,
94}
95
96/// Measurement data
97#[derive(Debug, embedded_devices_derive::Measurement)]
98pub struct Measurement {
99    /// Current temperature
100    #[measurement(Temperature)]
101    pub temperature: ThermodynamicTemperature,
102}
103
104/// The MAX31865 is an easy-to-use resistance-to-digital converter optimized for platinum
105/// resistance temperature detectors (RTDs).
106///
107/// For a full description and usage examples, refer to the [module documentation](self).
108#[maybe_async_cfg::maybe(
109    idents(hal(sync = "embedded_hal", async = "embedded_hal_async"), RegisterInterface),
110    sync(feature = "sync"),
111    async(feature = "async")
112)]
113pub struct MAX31865<D: hal::delay::DelayNs, I: embedded_interfaces::registers::RegisterInterface> {
114    /// The delay provider
115    delay: D,
116    /// The interface to communicate with the device
117    interface: I,
118    /// The reference resistor value over to the nominal resistance of
119    /// the temperature element at 0°C (100Ω for PT100, 1000Ω for PT1000).
120    /// In many designs a resistor with a value of 4.3 times the nominal resistance is used,
121    /// but your design may vary.
122    reference_resistor_ratio: f64,
123}
124
125pub trait MAX31865Register {}
126
127#[maybe_async_cfg::maybe(
128    idents(hal(sync = "embedded_hal", async = "embedded_hal_async"), SpiDevice),
129    sync(feature = "sync"),
130    async(feature = "async")
131)]
132impl<D, I> MAX31865<D, embedded_interfaces::spi::SpiDevice<I>>
133where
134    I: hal::spi::r#SpiDevice,
135    D: hal::delay::DelayNs,
136{
137    /// Initializes a new device from the specified SPI device.
138    /// This consumes the SPI device `I`.
139    ///
140    /// The device supports SPI modes 1 and 3.
141    ///
142    /// The reference resistor ratio is defined as the reference resistor value over the nominal resistance of
143    /// the temperature element at 0°C (100Ω for PT100, 1000Ω for PT1000).
144    /// In many designs a resistor with a value of 4.3 times the nominal resistance is used,
145    /// but your design may vary.
146    #[inline]
147    pub fn new_spi(delay: D, interface: I, reference_resistor_ratio: f64) -> Self {
148        Self {
149            delay,
150            interface: embedded_interfaces::spi::SpiDevice::new(interface),
151            reference_resistor_ratio,
152        }
153    }
154}
155
156#[forward_register_fns]
157#[sensor(Temperature)]
158#[maybe_async_cfg::maybe(
159    idents(hal(sync = "embedded_hal", async = "embedded_hal_async"), RegisterInterface),
160    sync(feature = "sync"),
161    async(feature = "async")
162)]
163impl<D: hal::delay::DelayNs, I: embedded_interfaces::registers::RegisterInterface> MAX31865<D, I> {
164    /// Initializes the device by configuring important settings and
165    /// running an initial fault detection cycle.
166    // TODO call this configure, make init verify device communication and reset
167    pub async fn init(
168        &mut self,
169        wiring_mode: WiringMode,
170        filter_mode: FilterMode,
171    ) -> Result<(), FaultDetectionError<I::BusError>> {
172        // Configure the wiring mode and filter mode
173        self.write_register(
174            Configuration::default()
175                .with_wiring_mode(wiring_mode)
176                .with_filter_mode(filter_mode),
177        )
178        .await?;
179
180        self.write_register(FaultThresholdLow::default().with_resistance_ratio(0))
181            .await?;
182        self.write_register(FaultThresholdHigh::default().with_resistance_ratio(0x7fff))
183            .await?;
184
185        self.detect_faults().await
186    }
187
188    /// Runs the automatic fault detection.
189    pub async fn detect_faults(&mut self) -> Result<(), FaultDetectionError<I::BusError>> {
190        let reg_conf = self.read_register::<Configuration>().await?;
191
192        // The automatic fault detection waits 100µs before checking for faults,
193        // which should be plenty of time to charge the RC filter.
194        // Typically a 100nF cap is used which should charge significantly faster.
195        self.write_register(
196            reg_conf
197                .with_enable_bias_voltage(true)
198                .with_conversion_mode(ConversionMode::NormallyOff)
199                .with_clear_fault_status(false)
200                .with_fault_detection_cycle(FaultDetectionCycle::Automatic),
201        )
202        .await?;
203
204        // According to the flow diagram in the datasheet, automatic calibration waits
205        // a total of 510µs. We will wait for a bit longer initially and then check
206        // the status in the configuration register up to 5 times with some additional delay.
207        self.delay.delay_us(550).await;
208        const TRIES: u8 = 5;
209        for _ in 0..TRIES {
210            let cycle = self
211                .read_register::<Configuration>()
212                .await?
213                .read_fault_detection_cycle();
214
215            // Check if fault detection is done
216            if cycle == FaultDetectionCycle::Finished {
217                // Disable VBIAS
218                self.write_register(reg_conf.with_enable_bias_voltage(false)).await?;
219
220                let has_fault = self.read_register::<registers::Resistance>().await?.read_fault();
221                if has_fault {
222                    // Fault detected
223                    return Err(FaultDetectionError::FaultDetected);
224                } else {
225                    // Everything's fine!
226                    return Ok(());
227                }
228            }
229
230            self.delay.delay_us(100).await;
231        }
232
233        // Disable VBIAS
234        self.write_register(reg_conf.with_enable_bias_voltage(false)).await?;
235
236        Err(FaultDetectionError::Timeout)
237    }
238
239    /// Converts a temperature into a resistance ratio as understood
240    /// by the device by factoring in the reference resistor ratio
241    /// and converting it to the required raw data format for this device.
242    ///
243    /// If the temperature results in a ratio >= 1.0, the resulting value will be clamped
244    /// to the maximum representable ratio (1.0).
245    pub fn temperature_to_raw_resistance_ratio(&mut self, temperature: ThermodynamicTemperature) -> u16 {
246        let temperature = temperature.get::<degree_celsius>() as f32;
247        let resistance = callendar_van_dusen::temperature_to_resistance_r100(temperature);
248        let ratio = resistance / (100.0 * self.reference_resistor_ratio) as f32;
249        if ratio >= 1.0 {
250            (1 << 15) - 1
251        } else {
252            (ratio * (1 << 15) as f32) as u16
253        }
254    }
255
256    /// Converts a raw resistance ratio into a temperature by
257    /// utilizing a builtin inverse Callendar-Van Dusen lookup table.
258    pub fn raw_resistance_ratio_to_temperature(&mut self, raw_resistance: u16) -> ThermodynamicTemperature {
259        // We always calculate with a 100Ω lookup table, because the equation
260        // linearly scales to other temperature ranges. Only the ratio between
261        // reference resistor and PT element is important.
262        let resistance = (100.0 * raw_resistance as f32 * self.reference_resistor_ratio as f32) / ((1 << 15) as f32);
263        let temperature = callendar_van_dusen::resistance_to_temperature_r100(resistance);
264        ThermodynamicTemperature::new::<degree_celsius>(temperature as f64)
265    }
266
267    /// Read the latest resistance measurement from the device register and convert
268    /// it to a temperature by using the internal Callendar-Van Dusen lookup table.
269    ///
270    /// Checks for faults.
271    pub async fn read_temperature(&mut self) -> Result<ThermodynamicTemperature, MeasurementError<I::BusError>> {
272        let resistance = self.read_register::<registers::Resistance>().await?;
273
274        if resistance.read_fault() {
275            return Err(MeasurementError::FaultDetected);
276        }
277
278        Ok(self.raw_resistance_ratio_to_temperature(resistance.read_resistance_ratio()))
279    }
280}
281
282#[maybe_async_cfg::maybe(
283    idents(
284        hal(sync = "embedded_hal", async = "embedded_hal_async"),
285        RegisterInterface,
286        OneshotSensor
287    ),
288    sync(feature = "sync"),
289    async(feature = "async")
290)]
291impl<D: hal::delay::DelayNs, I: embedded_interfaces::registers::RegisterInterface> crate::sensor::OneshotSensor
292    for MAX31865<D, I>
293{
294    type Error = MeasurementError<I::BusError>;
295    type Measurement = Measurement;
296
297    /// Performs a one-shot measurement. This will enable bias voltage, transition the device into
298    /// oneshot mode, which will cause it to take a measurement and return to sleep mode
299    /// afterwards.
300    async fn measure(&mut self) -> Result<Self::Measurement, Self::Error> {
301        let reg_conf = self
302            .read_register::<Configuration>()
303            .await?
304            .with_enable_bias_voltage(false)
305            .with_conversion_mode(ConversionMode::NormallyOff)
306            .with_oneshot(false);
307
308        // Enable VBIAS before initiating one-shot measurement,
309        // 10.5 RC time constants are recommended plus 1ms extra.
310        // So we just wait 2ms which should be plenty of time.
311        self.write_register(reg_conf.with_enable_bias_voltage(true)).await?;
312        self.delay.delay_us(2000).await;
313
314        // Initiate measurement
315        self.write_register(reg_conf.with_enable_bias_voltage(true).with_oneshot(true))
316            .await?;
317
318        // Wait until measurement is ready, plus 2ms extra
319        let measurement_time_us = match reg_conf.read_filter_mode() {
320            FilterMode::F_60Hz => 52000,
321            FilterMode::F_50Hz => 62500,
322        };
323        self.delay.delay_us(2000 + measurement_time_us).await;
324
325        // Revert config (disable VBIAS)
326        self.write_register(reg_conf).await?;
327
328        // Return conversion result
329        Ok(Measurement {
330            temperature: self.read_temperature().await?,
331        })
332    }
333}
334
335#[maybe_async_cfg::maybe(
336    idents(
337        hal(sync = "embedded_hal", async = "embedded_hal_async"),
338        RegisterInterface,
339        ContinuousSensor
340    ),
341    sync(feature = "sync"),
342    async(feature = "async")
343)]
344impl<D: hal::delay::DelayNs, I: embedded_interfaces::registers::RegisterInterface> crate::sensor::ContinuousSensor
345    for MAX31865<D, I>
346{
347    type Error = MeasurementError<I::BusError>;
348    type Measurement = Measurement;
349
350    /// Starts continuous measurement.
351    async fn start_measuring(&mut self) -> Result<(), Self::Error> {
352        let reg_conf = self.read_register::<Configuration>().await?;
353
354        // Enable VBIAS and automatic conversions
355        self.write_register(
356            reg_conf
357                .with_enable_bias_voltage(true)
358                .with_conversion_mode(ConversionMode::Automatic)
359                .with_oneshot(false),
360        )
361        .await?;
362
363        Ok(())
364    }
365
366    /// Stops continuous measurement.
367    async fn stop_measuring(&mut self) -> Result<(), Self::Error> {
368        let reg_conf = self.read_register::<Configuration>().await?;
369
370        // Disable VBIAS and automatic conversions, back to sleep.
371        self.write_register(
372            reg_conf
373                .with_enable_bias_voltage(false)
374                .with_conversion_mode(ConversionMode::NormallyOff)
375                .with_oneshot(false),
376        )
377        .await?;
378
379        Ok(())
380    }
381
382    /// Expected amount of time between measurements in microseconds.
383    async fn measurement_interval_us(&mut self) -> Result<u32, Self::Error> {
384        let reg_conf = self.read_register::<Configuration>().await?;
385        let measurement_time_us = match reg_conf.read_filter_mode() {
386            FilterMode::F_60Hz => 1_000_000 / 60,
387            FilterMode::F_50Hz => 1_000_000 / 50,
388        };
389
390        Ok(measurement_time_us)
391    }
392
393    /// Returns the most recent measurement. Will never return None.
394    async fn current_measurement(&mut self) -> Result<Option<Self::Measurement>, Self::Error> {
395        Ok(Some(Measurement {
396            temperature: self.read_temperature().await?,
397        }))
398    }
399
400    /// Not supported through registers
401    async fn is_measurement_ready(&mut self) -> Result<bool, Self::Error> {
402        // TODO: could be supported by supplying ¬DRDY pin optionally in new
403        Ok(true)
404    }
405
406    /// Opportunistically waits one conversion interval and returns the measurement.
407    async fn next_measurement(&mut self) -> Result<Self::Measurement, Self::Error> {
408        let interval = self.measurement_interval_us().await?;
409        self.delay.delay_us(interval).await;
410        self.current_measurement().await?.ok_or_else(|| {
411            MeasurementError::Transport(TransportError::Unexpected(
412                "measurement was not ready even though we expected it to be",
413            ))
414        })
415    }
416}